/** * Deterministic JSON serializer used by idempotentAppend's byte-compare. * Sorts object keys lexicographically at every depth; preserves array order; * round-trips numbers/strings/booleans/null. Throws on circular structures. * * NOT a general-purpose canonical-JSON impl (no RFC 8785 number normalisation, * no Unicode escape rewriting). Sufficient for comparing two Message envelopes * we wrote ourselves moments apart. */ export function canonicalJSON(value: unknown): string { return JSON.stringify(canonicalize(value)); } export function canonicalBytes(value: unknown): Uint8Array { return new TextEncoder().encode(canonicalJSON(value)); } function canonicalize(v: unknown, seen: WeakSet = new WeakSet()): unknown { if (v === null || typeof v !== "object") return v; if (v instanceof Uint8Array) { // Stable representation: array-of-bytes. Symmetric with envelope.ts replacer. return Array.from(v); } if (seen.has(v as object)) throw new Error("canonicalJSON: circular"); seen.add(v as object); if (Array.isArray(v)) return v.map((x) => canonicalize(x, seen)); const src = v as Record; const out: Record = {}; for (const k of Object.keys(src).sort()) { out[k] = canonicalize(src[k], seen); } return out; }