Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 5 additions & 11 deletions src/commands/compute.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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".
Expand All @@ -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) {
Expand Down Expand Up @@ -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)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3: The special-casing only covers the unchecked state: an edge-ownership TXT that the plane reports as ok (reachable via the r.configured ? 'ok' fallback when the plane omits the per-record status) falls into this branch and renders under the generic txt label, so the new distinction disappears right when the record validates. Render it as edge TXT there too, or the purpose distinction is only half applied.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At src/commands/compute.ts, line 198:

<comment>The special-casing only covers the `unchecked` state: an edge-ownership TXT that the plane reports as `ok` (reachable via the `r.configured ? 'ok'` fallback when the plane omits the per-record status) falls into this branch and renders under the generic `txt` label, so the new distinction disappears right when the record validates. Render it as `edge TXT` there too, or the purpose distinction is only half applied.</comment>

<file context>
@@ -194,14 +188,14 @@ export function domainStatusLines(r: DomainView, ctx: DomainCmdCtx = {}): string
-    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}`) }
</file context>
Suggested change
} else if (st === 'ok') stage(lbl, 'ok', where)
} else if (st === 'ok') stage(d.purpose === 'edge_ownership' ? 'edge TXT' : lbl, 'ok', where)

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Declining: this state is not emitted by the current compute contract. internal/api/domains.go:domainRecords always sets both Purpose: purposeEdgeOwnership and Status: "unchecked" on this TXT, including active domains. The platform companion #602 preserves both. For hypothetical future statuses the existing name/value and explicit verdict are still displayed; changing that label does not fix a current defect.

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`) }
Expand Down
28 changes: 28 additions & 0 deletions test/compute-domain-region.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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')
Expand Down
Loading