/*
RFC-0753: SVCB/HTTPS record content normalization for declared-vs-live comparison.
Cloudflare normalizes SVCB rdata presentation (canonical target gains a trailing
dot, param values are quoted) so a literal string compare flags cosmetic drift on
every DNS-AID record. Semantics mirror agent.dns-aid.validate (AGD-05): priority
and target compare after normalization, declared params must be a subset of live
params, and a live HTTPS record satisfies a declared SVCB record (wire alias).
- Do not normalize non-SVCB record types — TXT has its own normalizer.
- Do not require exact param-set equality — Cloudflare-synthesized extra params (ipv4hint/ipv6hint) are tolerated by the subset rule.
- RFC-0753: SVCB/HTTPS content normalization for recordsMatch — parse-compare instead of literal string compare; mirrors AGD-05 subset semantics from agent.dns-aid.validate.
*/
export interface ParsedSvcbContent {
priority: string;
target: string;
params: Map;
}
/** Parse an SVCB/HTTPS rdata string into priority, target and params. */
export function parseSvcbContent(content: string): ParsedSvcbContent | null {
const tokens = content.trim().split(/\s+/);
if (tokens.length < 2) return null;
const [priority, rawTarget, ...paramTokens] = tokens;
const target = rawTarget.replace(/\.$/, "");
const params = new Map();
for (const token of paramTokens) {
const eq = token.indexOf("=");
if (eq === -1) {
// Bare key — normalize as key="" (e.g. `mandatory` without value).
params.set(token.replace(/"/g, ""), "");
} else {
params.set(token.slice(0, eq), token.slice(eq + 1).replace(/"/g, ""));
}
}
return { priority, target, params };
}
/** Record types whose rdata is SVCB presentation format. */
export function isSvcbType(type: string): boolean {
return type === "SVCB" || type === "HTTPS";
}
/**
* Declared SVCB content matches live content when priority and target are equal
* after normalization and every declared key=value param is present in the live
* record (declared ⊆ live). Extra live params such as Cloudflare-synthesized
* ipv4hint/ipv6hint are tolerated. A live HTTPS record at the same name
* satisfies a declared SVCB record (draft-normative form vs its wire alias).
*/
export function svcbContentsMatch(
declaredContent: string,
liveType: string,
liveContent: string,
): boolean {
if (!isSvcbType(liveType)) return false;
const d = parseSvcbContent(declaredContent);
const l = parseSvcbContent(liveContent);
if (!d || !l) return false;
// Numeric comparison — "1" and "01" are the same SVCB priority.
if (Number(d.priority) !== Number(l.priority) || d.target !== l.target) return false;
for (const [key, value] of d.params) {
if (l.params.get(key) !== value) return false;
}
return true;
}