Skip to content

Commit 2263964

Browse files
Brooooooklynclaude
andauthored
feat(cli): simplify domain add/status output for HTTP-DCV (#214)
## What Custom-domain certificate validation is moving from TXT-DCV to **HTTP-DCV** (validated at Cloudflare's edge once the hostname proxies through it). Under HTTP-DCV there are **no user-facing `_acme-challenge` SSL-validation records** and renewals are fully automatic, so the CLI's domain output can be simplified. This is the **framework half** of the migration. The platform half (API activation gate + create-flow + janitor) is a separate PR in `voidzero-dev/void-platform` that bumps the submodule pointer to this commit. ## Changes - **`domain add`** now prints exactly **two** sections — traffic **CNAME** + ownership **TXT** — and drops the "TXT (SSL validation)" section plus its deadline-miss fallback hint (both dead/misleading under HTTP-DCV). Summary updated to "Add both records above". - **`domain status`** only surfaces the ownership TXT while it is still outstanding; the SSL-DCV record block is removed (issuance and renewals never surface user records). `computeRollup` state machine is unchanged. ## Why Adding a custom domain previously forced **3+ DNS records** (CNAME + `_cf-custom-hostname` ownership TXT + 1–2 **rotating** `_acme-challenge` TXT). The `_acme-challenge` set rotates on every ~90-day renewal and silently drops domains back to pending/404 when nobody re-publishes it (hit live on `viteplus.dev`). HTTP-DCV collapses the set to **1 CNAME + 1 non-rotating ownership TXT**. ## Tests `packages/void/test/unit/domain-command.test.ts` — 39 passing (two-section reveal assertions; SSL-record/SSL-pending-block assertions removed). 🤖 Generated with [Claude Code](https://claude.com/claude-code) <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Low Risk** > CLI and documentation-only UX changes for custom domains; no auth or deploy logic touched, though users on older TXT-DCV expectations may need the paired platform PR. > > **Overview** > Aligns **`void domain add`** and **`void domain status`** with **HTTP-DCV**: users only configure a traffic **CNAME** and a `_cf-custom-hostname` ownership **TXT**. All **`_acme-challenge` / TXT (SSL validation)** output and the “records will appear in status shortly” fallback are removed. > > **`domain status`** now shows pending DNS as **CNAME (while `ssl_status` is not active) + ownership TXT (while present)**, with singular/plural note titles and **`issuing_cert`** diagnostics that mention the CNAME only when a **`cname_target`** is actually printed. CLI reference docs are updated (HTTP validation, no wildcards, automatic activation / background reconcile). > > Unit tests in **`domain-command.test.ts`** are rewritten for the two-record model and the new status pending-block behavior. > > <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit 206a073. Bugbot is set up for automated code reviews on this repo. Configure [here](https://www.cursor.com/dashboard/bugbot).</sup> <!-- /CURSOR_SUMMARY --> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent f0706f8 commit 2263964

3 files changed

Lines changed: 142 additions & 158 deletions

File tree

‎docs/reference/cli.md‎

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -572,7 +572,9 @@ See [Environment Variables](../guide/env-vars.md) for the full guide.
572572
void domain add <hostname> [--project <name>]
573573
```
574574

575-
Add a custom domain to a project. Prints the CNAME target you need to add in your DNS provider.
575+
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.
576+
577+
> Wildcard custom hostnames (`*.example.com`) are not supported — register each subdomain individually.
576578
577579
### `void domain delete`
578580

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

599-
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.
601+
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.
600602

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

‎packages/void/src/cli/domain.ts‎

Lines changed: 38 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -55,11 +55,11 @@ async function domainAdd(
5555
const domain = await client.addDomain(projectId, hostname);
5656
log.success(`Domain ${hostname} added (status: ${domain.status})`);
5757

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

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

72-
const sslRecords = (domain.ssl_validation_records ?? []).filter(
73-
(r): r is { txt_name: string; txt_value: string; status?: string } =>
74-
Boolean(r.txt_name) && Boolean(r.txt_value),
75-
);
76-
if (sslRecords.length > 0) {
77-
const lines = sslRecords.map((r) => ` ${r.txt_name} → ${r.txt_value}`).join('\n');
78-
sslSection = `TXT (SSL validation)\n${lines}`;
79-
} else {
80-
// Deadline-miss path: API short-poll didn't see SSL records before timeout.
81-
// Tell the user they'll surface in `domain status` instead of silently
82-
// omitting the section.
83-
sslSection = `SSL validation records will appear in \`void domain status\` shortly.`;
84-
}
85-
86-
const summary = `Add the records above to your DNS, then run \`void domain status ${hostname}\` to check progress.`;
72+
const recordSections = [cnameSection, ownershipSection].filter((s) => s !== undefined);
8773

88-
const sections = [cnameSection, ownershipSection, sslSection, summary].filter(
89-
(s) => s !== undefined,
90-
);
91-
92-
if (sections.length > 0) {
93-
note(sections.join('\n\n'), 'DNS Setup');
74+
if (recordSections.length > 0) {
75+
const recordWord = recordSections.length === 1 ? 'the record above' : 'the records above';
76+
const summary = `Add ${recordWord} to your DNS, then run \`void domain status ${hostname}\` to check progress.`;
77+
note([...recordSections, summary].join('\n\n'), 'DNS Setup');
9478
}
9579
}
9680

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

@@ -238,43 +229,30 @@ async function domainStatus(
238229

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

241-
// Gate on actual validation states, not the aggregate `status.status`.
242-
// Once the DB row is flipped to `active`, the API keeps returning
243-
// `status: 'active'` even if CF later reports SSL/ownership re-validation
244-
// (cert renewal, ownership re-check). Keying off the per-field states
245-
// ensures pending records still surface in that window. CF stops returning
246-
// `ownership_verification` once ownership is verified, so its presence is
247-
// itself the pending signal.
248-
const ownershipPending = Boolean(status.ownership_verification);
249-
const sslPending = status.ssl_status !== 'active';
250-
251-
if (ownershipPending || sslPending) {
252-
const pendingSections: Array<string> = [];
253-
254-
if (ownershipPending && status.ownership_verification) {
255-
pendingSections.push(
256-
`TXT (ownership verification)\n ${status.ownership_verification.name} → ${status.ownership_verification.value}`,
257-
);
258-
}
232+
// Surface the DNS records the user may still need to publish. The ownership
233+
// TXT shows while `ownership_verification` is present (CF drops it once
234+
// verified). The traffic CNAME shows whenever the cert is not yet active —
235+
// HTTP-DCV cannot validate until the hostname proxies through Cloudflare, so
236+
// a missing/misconfigured CNAME silently blocks issuance even after ownership
237+
// is verified. Once `ssl_status === 'active'` the domain is live and both
238+
// records are correct, so neither is shown.
239+
let cnameSection: string | undefined;
240+
let ownershipSection: string | undefined;
259241

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

276-
if (pendingSections.length > 0) {
277-
note(pendingSections.join('\n\n'), 'Add these DNS records to your provider');
278-
}
249+
const pendingRecords = [cnameSection, ownershipSection].filter((s) => s !== undefined);
250+
if (pendingRecords.length > 0) {
251+
note(
252+
pendingRecords.join('\n\n'),
253+
pendingRecords.length === 1
254+
? 'Add this DNS record to your provider'
255+
: 'Add these DNS records to your provider',
256+
);
279257
}
280258
}

0 commit comments

Comments
 (0)