// Channel adapter trait, registry, error model. Mirrors Rust kez-channels. import { COMPACT_PROOF_PREFIX, Identity, type SignedClaimEnvelope, type VerificationStatus, extractMarkdownProof, fromCompact, fromJson, parseDnsTxtValue, verifyClaim, } from "@kez/core"; export type ChannelErrorKind = | "Unreachable" | "NotFound" | "Invalid" | "SubjectMismatch" | "NoChannelForSystem" | "Other"; export class ChannelError extends Error { readonly kind: ChannelErrorKind; readonly expected?: Identity; readonly found?: Identity; readonly cause?: unknown; constructor( kind: ChannelErrorKind, message: string, opts: { cause?: unknown; expected?: Identity; found?: Identity } = {}, ) { super(message); this.name = "ChannelError"; this.kind = kind; this.cause = opts.cause; this.expected = opts.expected; this.found = opts.found; } static unreachable(msg: string, cause?: unknown): ChannelError { return new ChannelError("Unreachable", `channel unreachable: ${msg}`, { cause }); } static notFound(identity: Identity): ChannelError { return new ChannelError("NotFound", `no KEZ proof found for ${identity}`); } static invalid(reason: string, cause?: unknown): ChannelError { return new ChannelError("Invalid", `proof failed verification: ${reason}`, { cause }); } static subjectMismatch(expected: Identity, found: Identity): ChannelError { return new ChannelError( "SubjectMismatch", `proof subject ${found} did not match expected identity ${expected}`, { expected, found }, ); } static noChannelForSystem(system: string): ChannelError { return new ChannelError("NoChannelForSystem", `no channel registered for system: ${system}`); } static other(msg: string, cause?: unknown): ChannelError { return new ChannelError("Other", msg, { cause }); } } export interface ChannelHit { proof: SignedClaimEnvelope; status: VerificationStatus; } export interface Channel { /** The `system:` prefix this channel handles. */ readonly system: string; fetchAndVerify(identity: Identity): Promise; } /** system: prefix → channel adapter, with alias support. */ export class Registry { private channels = new Map(); register(channel: Channel): void { this.channels.set(channel.system, channel); } /** Register the same adapter under a different scheme (e.g. `mastodon` → `ap`). */ registerAs(system: string, channel: Channel): void { this.channels.set(system, channel); } get(system: string): Channel | undefined { return this.channels.get(system); } async verify(identity: Identity): Promise { const channel = this.channels.get(identity.scheme); if (!channel) throw ChannelError.noChannelForSystem(identity.scheme); return channel.fetchAndVerify(identity); } } /** Build a Registry with every channel shipped in this package. */ export async function defaultRegistry(): Promise { const r = new Registry(); const { GithubChannel } = await import("./github.js"); const { DnsChannel } = await import("./dns.js"); const { NostrChannel } = await import("./nostr.js"); const { BlueskyChannel } = await import("./bluesky.js"); const { ActivityPubChannel } = await import("./activitypub.js"); r.register(new GithubChannel()); r.register(new DnsChannel()); r.register(new NostrChannel()); r.register(new BlueskyChannel()); const ap = new ActivityPubChannel(); r.register(ap); r.registerAs("mastodon", ap); return r; } // ───────────────────────────────────────────────────────────────────────────── // parseProof / parseAndVerifyFor — shared by every channel // ───────────────────────────────────────────────────────────────────────────── /** Try all four wire encodings. Compact form may be embedded in prose. */ export function parseProof(raw: string): SignedClaimEnvelope { const trimmed = raw.trim(); if (trimmed.includes("```kez")) return extractMarkdownProof(trimmed); if (trimmed.startsWith("{")) return fromJson(trimmed); if (trimmed.startsWith("kez1:")) return parseDnsTxtValue(trimmed); const token = extractCompactToken(trimmed); if (token) return fromCompact(token); throw new Error("unknown KEZ proof format"); } /** Parse, verify signature, and require the subject to equal `expected`. */ export function parseAndVerifyFor(raw: string, expected: Identity): ChannelHit { let proof: SignedClaimEnvelope; try { proof = parseProof(raw); } catch (e) { throw ChannelError.invalid((e as Error).message, e); } let status: VerificationStatus; try { status = verifyClaim(proof); } catch (e) { throw ChannelError.invalid((e as Error).message, e); } if (proof.payload.subject !== expected.toString()) { throw ChannelError.subjectMismatch(expected, Identity.parse(proof.payload.subject)); } return { proof, status }; } /** Find `kez:z1:` anywhere in `text` and return the full token. */ export function extractCompactToken(text: string): string | undefined { const idx = text.indexOf(COMPACT_PROOF_PREFIX); if (idx < 0) return undefined; const after = text.slice(idx + COMPACT_PROOF_PREFIX.length); let end = 0; while (end < after.length) { const c = after.charCodeAt(end); const isAlphaNum = (c >= 0x30 && c <= 0x39) || // 0-9 (c >= 0x41 && c <= 0x5a) || // A-Z (c >= 0x61 && c <= 0x7a); // a-z const isUrlSafe = c === 0x5f /* _ */ || c === 0x2d; /* - */ if (!isAlphaNum && !isUrlSafe) break; end++; } if (end === 0) return undefined; return COMPACT_PROOF_PREFIX + after.slice(0, end); }