// DNS channel: queries `_kez.` TXT records. Resolver is abstracted // so tests can substitute a fake. import { resolveTxt } from "node:dns/promises"; import { COMPACT_PROOF_PREFIX, type Identity, dnsTxtName } from "@kez/core"; import { ChannelError, type Channel, type ChannelHit, parseAndVerifyFor } from "./index.js"; export interface TxtResolver { lookupTxt(name: string): Promise; } export class SystemResolver implements TxtResolver { async lookupTxt(name: string): Promise { try { // Node returns TXT as string[][] (one inner array per record, // each segment <=255 bytes). Concat segments per record. const records = await resolveTxt(name); return records.map((parts) => parts.join("")); } catch (e) { throw ChannelError.unreachable(`TXT lookup ${name}: ${(e as Error).message}`, e); } } } export class DnsChannel implements Channel { readonly system = "dns"; private resolver: TxtResolver; constructor(resolver: TxtResolver = new SystemResolver()) { this.resolver = resolver; } async fetchAndVerify(identity: Identity): Promise { const name = dnsTxtName(identity); const records = await this.resolver.lookupTxt(name); let lastError: ChannelError | undefined; for (const value of records) { if (!looksLikeKezTxt(value)) continue; try { return parseAndVerifyFor(value, identity); } catch (e) { if (e instanceof ChannelError) lastError = e; else lastError = ChannelError.invalid((e as Error).message, e); } } throw lastError ?? ChannelError.notFound(identity); } } export function looksLikeKezTxt(value: string): boolean { return value.startsWith(COMPACT_PROOF_PREFIX) || value.startsWith("kez1:"); }