Skip to content
Merged
6 changes: 4 additions & 2 deletions docs/reference/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -572,7 +572,9 @@ See [Environment Variables](../guide/env-vars.md) for the full guide.
void domain add <hostname> [--project <name>]
```

Add a custom domain to a project. Prints the CNAME target you need to add in your DNS provider.
Add a custom domain to a project. Prints the two DNS records to add at your DNS provider: a traffic **CNAME** pointing `<hostname>` at the CNAME target shown in the command output, and a non-rotating `_cf-custom-hostname` ownership **TXT**. Certificates are validated over HTTP at Cloudflare's edge and renew automatically — there are no `_acme-challenge` records to publish, at first issuance or ever. After adding the records the domain activates automatically (no polling required); run `void domain status <hostname>` to check progress.

> Wildcard custom hostnames (`*.example.com`) are not supported — register each subdomain individually.

### `void domain delete`

Expand All @@ -596,7 +598,7 @@ List all custom domains and their status (active/pending).
void domain status <hostname> [--project <name>] [--verbose]
```

Check verification and SSL status for a specific domain. Prints a rolled-up state (`awaiting_dns`, `verifying_dns`, `issuing_cert`, `deploying_cert`, `awaiting_deployment`, `active`, `error`, or `pending`) with a one-line diagnostic explaining what Cloudflare is doing and any user action required. When DNS records are still pending, the command also prints the exact TXT records to add at your DNS provider.
Check verification and SSL status for a specific domain. Prints a rolled-up state (`awaiting_dns`, `verifying_dns`, `issuing_cert`, `deploying_cert`, `awaiting_deployment`, `active`, `error`, or `pending`) with a one-line diagnostic explaining what Cloudflare is doing and any user action required. While the certificate is not yet active, the command also surfaces the records to configure — the traffic **CNAME** and the `_cf-custom-hostname` ownership **TXT** — so you can verify them. Activation is automatic: a background job reconciles pending domains (about every 2 minutes for the first 30 minutes after adding, then hourly), so this command is for instant feedback rather than required polling.

Pass `--verbose` to additionally print the raw multi-line status breakdown (DB status, SSL status, ownership state, verification errors) underneath the rollup.

Expand Down
98 changes: 38 additions & 60 deletions packages/void/src/cli/domain.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,11 +55,11 @@ async function domainAdd(
const domain = await client.addDomain(projectId, hostname);
log.success(`Domain ${hostname} added (status: ${domain.status})`);

// Named locals make the section order explicit (CNAME -> ownership -> SSL),
// Named locals make the section order explicit (CNAME -> ownership),
// rather than relying on the order of conditional pushes onto an array.
// Cert DCV uses HTTP-DCV, so no user-facing SSL-validation records exist.
let cnameSection: string | undefined;
let ownershipSection: string | undefined;
let sslSection: string | undefined;

if (domain.cname_target) {
cnameSection = `CNAME (traffic)\n ${hostname} → ${domain.cname_target}`;
Expand All @@ -69,28 +69,12 @@ async function domainAdd(
ownershipSection = `TXT (ownership verification)\n ${domain.ownership_verification.name} → ${domain.ownership_verification.value}`;
}

const sslRecords = (domain.ssl_validation_records ?? []).filter(
(r): r is { txt_name: string; txt_value: string; status?: string } =>
Boolean(r.txt_name) && Boolean(r.txt_value),
);
if (sslRecords.length > 0) {
const lines = sslRecords.map((r) => ` ${r.txt_name} → ${r.txt_value}`).join('\n');
sslSection = `TXT (SSL validation)\n${lines}`;
} else {
// Deadline-miss path: API short-poll didn't see SSL records before timeout.
// Tell the user they'll surface in `domain status` instead of silently
// omitting the section.
sslSection = `SSL validation records will appear in \`void domain status\` shortly.`;
}

const summary = `Add the records above to your DNS, then run \`void domain status ${hostname}\` to check progress.`;
const recordSections = [cnameSection, ownershipSection].filter((s) => s !== undefined);

const sections = [cnameSection, ownershipSection, sslSection, summary].filter(
(s) => s !== undefined,
);

if (sections.length > 0) {
note(sections.join('\n\n'), 'DNS Setup');
if (recordSections.length > 0) {
const recordWord = recordSections.length === 1 ? 'the record above' : 'the records above';
const summary = `Add ${recordWord} to your DNS, then run \`void domain status ${hostname}\` to check progress.`;
note([...recordSections, summary].join('\n\n'), 'DNS Setup');
}
}

Expand Down Expand Up @@ -177,7 +161,14 @@ export function computeRollup(status: DomainStatusResponse): RolledUpStatus {
) {
return {
state: 'issuing_cert',
diagnostic: 'Cloudflare is issuing the SSL certificate (typically <5 minutes).',
// Only cite the CNAME "below" when it will actually render. The CNAME block
// shows iff `status.cname_target` is set (ssl_status !== 'active' always
// holds in this branch), so a status response without a target (e.g. an
// older API during the deploy window) no longer points at a record we
// don't display.
diagnostic: status.cname_target
? 'Cloudflare is issuing the SSL certificate automatically (typically <5 minutes). If this persists, confirm the traffic CNAME below is published.'
: 'Cloudflare is issuing the SSL certificate automatically (typically <5 minutes).',
};
}

Expand Down Expand Up @@ -238,43 +229,30 @@ async function domainStatus(

note(rollupLines.join('\n'), 'Domain Status');

// Gate on actual validation states, not the aggregate `status.status`.
// Once the DB row is flipped to `active`, the API keeps returning
// `status: 'active'` even if CF later reports SSL/ownership re-validation
// (cert renewal, ownership re-check). Keying off the per-field states
// ensures pending records still surface in that window. CF stops returning
// `ownership_verification` once ownership is verified, so its presence is
// itself the pending signal.
const ownershipPending = Boolean(status.ownership_verification);
const sslPending = status.ssl_status !== 'active';

if (ownershipPending || sslPending) {
const pendingSections: Array<string> = [];

if (ownershipPending && status.ownership_verification) {
pendingSections.push(
`TXT (ownership verification)\n ${status.ownership_verification.name} → ${status.ownership_verification.value}`,
);
}
// Surface the DNS records the user may still need to publish. The ownership
// TXT shows while `ownership_verification` is present (CF drops it once
// verified). The traffic CNAME shows whenever the cert is not yet active —
// HTTP-DCV cannot validate until the hostname proxies through Cloudflare, so
// a missing/misconfigured CNAME silently blocks issuance even after ownership
// is verified. Once `ssl_status === 'active'` the domain is live and both
// records are correct, so neither is shown.
let cnameSection: string | undefined;
let ownershipSection: string | undefined;

if (sslPending) {
// Defensive: drop any per-record entries CF has already marked active.
// CF normally clears `ssl.validation_records` once SSL is active, but
// during cert renewals or partial states a stale `active` row can sit
// alongside a pending one — we only want to surface what the user still
// needs to add.
const sslRecords = (status.ssl_validation_records ?? []).filter(
(r): r is { txt_name: string; txt_value: string; status?: string } =>
Boolean(r.txt_name) && Boolean(r.txt_value) && r.status !== 'active',
);
if (sslRecords.length > 0) {
const lines = sslRecords.map((r) => ` ${r.txt_name} → ${r.txt_value}`).join('\n');
pendingSections.push(`TXT (SSL validation)\n${lines}`);
}
}
if (status.cname_target && status.ssl_status !== 'active') {
cnameSection = `CNAME (traffic)\n ${status.hostname} → ${status.cname_target}`;
}
if (status.ownership_verification) {
ownershipSection = `TXT (ownership verification)\n ${status.ownership_verification.name} → ${status.ownership_verification.value}`;
}

if (pendingSections.length > 0) {
note(pendingSections.join('\n\n'), 'Add these DNS records to your provider');
}
const pendingRecords = [cnameSection, ownershipSection].filter((s) => s !== undefined);
if (pendingRecords.length > 0) {
note(
pendingRecords.join('\n\n'),
pendingRecords.length === 1
? 'Add this DNS record to your provider'
: 'Add these DNS records to your provider',
);
Comment thread
Brooooooklyn marked this conversation as resolved.
}
}
Loading