// Four wire encodings: JSON, compact (kez:z1:...), Markdown fence, legacy // DNS (kez1:...). Round-trips match Rust exactly. import { base64url } from "@scure/base"; import { zstdCompressSync, zstdDecompressSync } from "node:zlib"; import { type SignedClaimEnvelope, COMPACT_PROOF_PREFIX, } from "./envelope.js"; import { Identity } from "./identity.js"; const MARKDOWN_FENCE = "```kez"; // ───────────────────────────────────────────────────────────────────────────── // JSON // ───────────────────────────────────────────────────────────────────────────── export function toPrettyJson(envelope: SignedClaimEnvelope): string { return JSON.stringify(envelope, null, 2); } export function fromJson(json: string): SignedClaimEnvelope { return JSON.parse(json) as SignedClaimEnvelope; } // ───────────────────────────────────────────────────────────────────────────── // Compact: kez:z1: // ───────────────────────────────────────────────────────────────────────────── export function toCompact(envelope: SignedClaimEnvelope): string { const json = new TextEncoder().encode(JSON.stringify(envelope)); const compressed = zstdCompressSync(json); return ( COMPACT_PROOF_PREFIX + base64url.encode(new Uint8Array(compressed)).replace(/=+$/, "") ); } export function fromCompact(value: string): SignedClaimEnvelope { const trimmed = value.trim(); if (!trimmed.startsWith(COMPACT_PROOF_PREFIX)) { throw new Error("compact proof missing kez:z1: prefix"); } const body = trimmed.slice(COMPACT_PROOF_PREFIX.length); // @scure/base's base64url requires standard padding; restore it. const padded = body + "=".repeat((4 - (body.length % 4)) % 4); const compressed = base64url.decode(padded); const json = zstdDecompressSync(compressed); return JSON.parse(new TextDecoder().decode(json)) as SignedClaimEnvelope; } // ───────────────────────────────────────────────────────────────────────────── // Markdown fence: ```kez ... ``` // ───────────────────────────────────────────────────────────────────────────── export function toMarkdown(envelope: SignedClaimEnvelope): string { const json = toPrettyJson(envelope); return [ "# KEZ Proof", "", "This account publishes a signed KEZ identity claim.", "", `- Primary: \`${envelope.payload.primary}\``, `- Subject: \`${envelope.payload.subject}\``, `- Created: \`${envelope.payload.created_at}\``, "", MARKDOWN_FENCE, json, "```", "", ].join("\n"); } export function extractMarkdownProof(markdown: string): SignedClaimEnvelope { const start = markdown.indexOf(MARKDOWN_FENCE); if (start < 0) { throw new Error("missing ```kez proof block"); } const bodyStart = start + MARKDOWN_FENCE.length; const endRel = markdown.slice(bodyStart).indexOf("```"); if (endRel < 0) { throw new Error("unterminated ```kez proof block"); } const json = markdown.slice(bodyStart, bodyStart + endRel).trim(); return JSON.parse(json) as SignedClaimEnvelope; } // ───────────────────────────────────────────────────────────────────────────── // DNS TXT // ───────────────────────────────────────────────────────────────────────────── export function dnsTxtName(identity: Identity): string { if (identity.scheme !== "dns") { throw new Error(`dns_txt_name requires dns: identity, got: ${identity}`); } return `_kez.${identity.id}`; } /** Legacy `kez1:` DNS encoding — Rust parser still accepts it. */ export function dnsTxtValue(envelope: SignedClaimEnvelope): string { return "kez1:" + JSON.stringify(envelope); } export function parseDnsTxtValue(value: string): SignedClaimEnvelope { if (!value.startsWith("kez1:")) { throw new Error("DNS TXT proof missing kez1: prefix"); } return JSON.parse(value.slice("kez1:".length)) as SignedClaimEnvelope; }