From 4b96c6bf381c1d2074eecb4cb0439bca6a354238 Mon Sep 17 00:00:00 2001 From: Lyu Date: Fri, 2 Oct 2026 01:00:27 -0700 Subject: [PATCH] Distinguish Cloudflare DNS validation in domain status --- src/commands/compute.ts | 16 +++++----------- test/compute-domain-region.test.ts | 28 ++++++++++++++++++++++++++++ 2 files changed, 33 insertions(+), 11 deletions(-) diff --git a/src/commands/compute.ts b/src/commands/compute.ts index af13b77..1c70074 100644 --- a/src/commands/compute.ts +++ b/src/commands/compute.ts @@ -61,7 +61,7 @@ export function resolveDomainTarget(services: ComputeRow[], host: string, group? // does not report them (older builds) — the renderers say so rather than inventing a value. export type DomainView = { hostname: string; flyApp: string; configured: boolean; status: string - dns: Array<{ type: string; name: string; value: string; note?: string; status?: string }> + dns: Array<{ type: string; name: string; value: string; note?: string; status?: string; purpose?: string }> service?: string | null; region?: string | null ssl?: string; errorReason?: string origin?: string; edgeOrigin?: string; originOk?: boolean @@ -142,7 +142,7 @@ export function domainStatusLines(r: DomainView, ctx: DomainCmdCtx = {}): string } const records = recordsOf(r) const out = [`${r.hostname} -> ${targetOf(r)}`] - const txt = records.find((d) => d.type === 'TXT') + const txt = records.find((d) => d.type === 'TXT' && d.purpose !== 'edge_ownership') // The ROUTING records for the hostname, whatever type they take: a CNAME for a subdomain, or the // A/AAAA PAIR an apex needs (Fly's apex path emits both). All of them, not the first one — // a correct A beside a missing AAAA is not "routing is fine". @@ -151,12 +151,6 @@ export function domainStatusLines(r: DomainView, ctx: DomainCmdCtx = {}): string const blockers: string[] = [] const stage = (label: string, state: string, detail: string) => out.push(` ${pad(label, 12)}${pad(state, 10)}${detail ? ` ${detail}` : ''}`) - // The ONE verdict rule, applied to EVERY record the platform returned regardless of its role. - // A record is settled only when the platform says `ok` — or, for a provider that reports no - // per-record status at all (Fly), when it vouched for the whole set with `configured`. missing, - // mismatch and never-checked are each outstanding and each add a blocker. Applying this to only - // some records lets an apex whose AAAA is missing, or a still-pending validation record, ride - // under a `serving https://…` line. const verdictOf = (d: DomainView['dns'][number]) => d.status ?? (r.configured ? 'ok' : 'unchecked') if (txt) { @@ -194,14 +188,14 @@ export function domainStatusLines(r: DomainView, ctx: DomainCmdCtx = {}): string else { stage(lbl, 'unchecked', `${d.type} ${d.name} -> ${d.value} (not checked yet — re-run insta domain check)`); blockers.push(`${d.type} unchecked`) } } - // Everything else the platform returned — a Let's Encrypt validation CNAME, any extra record. - // Same rule, no exemption: an outstanding record is outstanding whatever its role. for (const d of records) { if (d === txt || isRouting(d)) continue const st = verdictOf(d) const lbl = d.type.toLowerCase() const where = `${d.name} -> ${d.value}${d.note ? ` (${d.note})` : ''}` - if (st === 'ok') stage(lbl, 'ok', where) + if (d.type === 'TXT' && d.purpose === 'edge_ownership' && st === 'unchecked') { + stage('edge TXT', 'unchecked', `${where} (Cloudflare validates this record; see certificate status below)`) + } else if (st === 'ok') stage(lbl, 'ok', where) else if (st === 'mismatch') { stage(lbl, 'mismatch', `${d.type} ${d.name} must point at ${d.value}`); blockers.push(`fix the ${d.type} ${d.name}`) } else if (st === 'missing') { stage(lbl, 'pending', `add ${where}`); blockers.push(`add the ${d.type} ${d.name}`) } else { stage(lbl, 'unchecked', `${where} (not checked yet — re-run insta domain check)`); blockers.push(`${d.type} ${d.name} unchecked`) } diff --git a/test/compute-domain-region.test.ts b/test/compute-domain-region.test.ts index ae43f70..f755ec0 100644 --- a/test/compute-domain-region.test.ts +++ b/test/compute-domain-region.test.ts @@ -118,6 +118,34 @@ describe('domainStatusLines (check: every stage + where it routes)', () => { ]) }) + it.each(['active', 'pending_validation'])('edge TXT unchecked defers to certificate status %s', (ssl) => { + const edge = { type: 'TXT', name: '_cf-custom-hostname.app.customer.com', value: 'edge-token', status: 'unchecked', purpose: 'edge_ownership' } + const lines = domainStatusLines({ ...active, ssl, dns: [edge, ...active.dns] }) + expect(lines[1]).toContain('ownership verified') + expect(lines.find((line) => line.includes('edge TXT'))).toBe(' edge TXT unchecked _cf-custom-hostname.app.customer.com -> edge-token (Cloudflare validates this record; see certificate status below)') + expect(lines.join('\n')).not.toContain('re-run insta domain check') + expect(lines.at(-1)).not.toContain('unchecked') + if (ssl === 'active') expect(lines.at(-1)).toBe(' serving https://app.customer.com') + else expect(lines.at(-1)).toBe(' serving not yet (certificate)') + }) + + it('an edge TXT cannot stand in for the platform ownership record', () => { + const lines = domainStatusLines({ ...active, dns: [ + ...active.dns.filter((r) => r.type !== 'TXT'), + { type: 'TXT', name: '_cf-custom-hostname.app.customer.com', value: 'edge-token', status: 'unchecked', purpose: 'edge_ownership' }, + ] }) + expect(lines.at(-1)).toContain('no ownership TXT from the platform') + }) + + it.each(['missing', 'mismatch', 'unchecked'])('other validation TXT records remain blockers: %s', (status) => { + const lines = domainStatusLines({ ...active, dns: [ + ...active.dns, + { type: 'TXT', name: '_validation.app.customer.com', value: 'tok', status }, + ] }) + expect(lines.at(-1)).toContain('not yet') + expect(lines.at(-1)).toContain('_validation.app.customer.com') + }) + it('pending: each missing stage says what the user must still do', () => { const lines = domainStatusLines(bound) expect(lines[1]).toBe(' ownership pending add TXT _insta-verify.app.customer.com -> insta-verify=tok123')