/*
RFC-0753: dns.record.validate command handler — validates that live Cloudflare
DNS records match the declaration file. Returns per-record validation status
and overall state. Exit code 0 for all validation states.
- Do not create or update records — that is dns.record.upsert's responsibility.
- Do not delete records — that is dns.record.delete's responsibility.
- RFC-0753: initial dns.record.validate command handler.
- RFC-0753: live lookup falls back to the HTTPS wire-alias record for declared SVCB when no exact-type match exists at the name.
- RFC-0753: best-fit declared→live matching with consumed-live tracking — duplicate type+name identities (parallel MX, dual apex TXT) validate correctly, and unconsumed same-identity live records surface as extra instead of being hidden by the identity set.
- RFC-1249: wire werkstatt.commands.validate into packages.check + retire CMD-OUTPUT debt
*/
import type {
KernelCommandInput,
KernelCommandResult,
KernelRuntimeContext,
} from "@warpgogol/werkstatt-engine/kernel";
import { discoverSystems } from "../sternsystem/registry-io.ts";
import { listDnsRecords } from "../leitstand/adapters/cloudflare-api.ts";
import {
flagString,
loadDnsRecordFile,
resolveDnsZoneId,
resolveDnsEnv,
recordsMatch,
pickBestLiveMatch,
} from "./dns-helpers.ts";
export interface DnsRecordValidateResult {
command: "dns.record.validate";
systemId: string;
zone: string;
records: Array<{
identity: string;
state: "match" | "mismatch" | "missing" | "extra";
liveId: string | null;
detail: string | null;
}>;
state: "valid" | "drifted";
counts: { match: number; mismatch: number; missing: number; extra: number };
}
export async function runDnsRecordValidate(
input: KernelCommandInput,
context: KernelRuntimeContext,
): Promise> {
const { workspaceRoot } = context;
const systemId = flagString(input, "system");
if (!systemId) throw new Error("[dns.record.validate] --system is required");
const declaration = await loadDnsRecordFile(workspaceRoot, systemId);
if (!declaration) {
throw new Error(`[dns.record.validate] No dns-records.yaml found for system '${systemId}'.`);
}
const { systems } = await discoverSystems(workspaceRoot);
const zoneId = resolveDnsZoneId(systems, declaration.zone);
const env = await resolveDnsEnv();
const apiToken = env["CLOUDFLARE_API_TOKEN"];
if (!apiToken) {
throw new Error(
"[dns.record.validate] CLOUDFLARE_API_TOKEN is not set. " +
"Set it in the environment or .env file.",
);
}
const liveRecords = await listDnsRecords(zoneId, apiToken);
const records: DnsRecordValidateResult["records"] = [];
let match = 0;
let mismatch = 0;
let missing = 0;
let extra = 0;
const consumedLiveIds = new Set();
for (const declared of declaration.records) {
const identity = `${declared.type}:${declared.name}`;
const live = pickBestLiveMatch(declared, liveRecords, consumedLiveIds);
if (!live) {
missing++;
records.push({ identity, state: "missing", liveId: null, detail: null });
continue;
}
consumedLiveIds.add(live.id);
if (recordsMatch(declared, live)) {
match++;
records.push({ identity, state: "match", liveId: live.id, detail: null });
} else {
mismatch++;
const diffs: string[] = [];
if (live.content !== declared.content) {
diffs.push(`content: live='${live.content}' vs declared='${declared.content}'`);
}
if (live.proxied !== (declared.proxied ?? false)) {
diffs.push(`proxied: live=${live.proxied} vs declared=${declared.proxied ?? false}`);
}
if (declared.priority !== undefined && live.priority !== declared.priority) {
diffs.push(`priority: live=${live.priority} vs declared=${declared.priority}`);
}
records.push({
identity,
state: "mismatch",
liveId: live.id,
detail: diffs.join("; "),
});
}
}
for (const live of liveRecords) {
if (!consumedLiveIds.has(live.id)) {
const identity = `${live.type}:${live.name}`;
extra++;
records.push({ identity, state: "extra", liveId: live.id, detail: null });
}
}
const state: DnsRecordValidateResult["state"] =
mismatch === 0 && missing === 0 ? "valid" : "drifted";
return {
data: {
command: "dns.record.validate",
systemId,
zone: declaration.zone,
records,
state,
counts: { match, mismatch, missing, extra },
},
exitCode: state === "valid" ? 0 : 1,
summary: `[dns.record.validate] ${systemId}: ${state} (${match} match, ${mismatch} mismatch, ${missing} missing, ${extra} extra)`,
nextSteps:
state === "valid"
? undefined
: [
{
action: `Fix DNS records: ${mismatch} mismatch(es), ${missing} missing, ${extra} extra — then re-run: pnpm exec werkstatt run dns.record.validate --site ${systemId}`,
kind: "required",
},
],
};
}