import { parseIpv4, parseIpv6 } from "@add-ideas/toolbox-helpers"; export type DnsRecordType = "A" | "AAAA" | "CAA" | "CNAME" | "MX" | "SRV" | "TXT"; export interface DnsRecordInput { owner: string; ttl: number; type: DnsRecordType; value: string; priority?: number; weight?: number; port?: number; flag?: number; tag?: "issue" | "issuewild" | "iodef"; } const LABEL = /^(?:[A-Za-z0-9_](?:[A-Za-z0-9_-]{0,61}[A-Za-z0-9_])?)(?:\.(?:[A-Za-z0-9_](?:[A-Za-z0-9_-]{0,61}[A-Za-z0-9_])?))*\.?$/u; function integer( value: number | undefined, name: string, maximum = 65_535, ): number { if (!Number.isInteger(value) || value! < 0 || value! > maximum) throw new Error(`${name} must be an integer from 0 to ${maximum}.`); return value!; } function domain(value: string, name: string): string { const candidate = value.trim(); if (candidate === "@") return candidate; if (!candidate || candidate.length > 253 || !LABEL.test(candidate)) throw new Error(`${name} must be a valid ASCII DNS name.`); return candidate; } function quoteTxt(value: string): string { const bytes = new TextEncoder().encode(value); if (bytes.length > 255) throw new Error("A TXT character-string is limited to 255 UTF-8 bytes."); return `"${value.replaceAll("\\", "\\\\").replaceAll('"', '\\"')}"`; } export function buildDnsRecord(input: DnsRecordInput): string { const owner = domain(input.owner, "Owner"); const ttl = integer(input.ttl, "TTL", 2_147_483_647); const value = input.value.trim(); if (!value) throw new Error("Record value is required."); let data: string; switch (input.type) { case "A": try { data = parseIpv4(value).canonical; } catch { throw new Error("A records require a dotted-decimal IPv4 address."); } break; case "AAAA": try { data = parseIpv6(value).canonical; } catch { throw new Error("AAAA records require an IPv6 address."); } break; case "CNAME": data = domain(value, "Target"); break; case "MX": data = `${integer(input.priority, "Priority")} ${domain(value, "Mail exchanger")}`; break; case "SRV": data = `${integer(input.priority, "Priority")} ${integer(input.weight, "Weight")} ${integer(input.port, "Port")} ${domain(value, "Target")}`; break; case "CAA": { const tag = input.tag ?? "issue"; data = `${integer(input.flag ?? 0, "Flag", 255)} ${tag} ${quoteTxt(value)}`; break; } case "TXT": data = quoteTxt(value); break; } return `${owner} ${ttl} IN ${input.type} ${data}`; }