/** * inspector-content-digest/1 — canonical form and digest for MOCK registry * content only (charter candidate 3 §6; feasibility r2 §6.1). * * Domain: objects, arrays, ASCII strings, booleans, null and safe integers. * Object keys are sorted by UTF-16 code unit; separators are compact; array * order is preserved; the digest is "sha256:" + hex(SHA-256(UTF-8 bytes)). * Anything outside the domain refuses with CanonicalError (reason * unsupported-capability): non-ASCII strings, non-integral or unsafe numbers, * negative zero, undefined, functions, symbols, bigints. * * This is NOT the candidate README launch-fingerprint projection and makes no * JCS (RFC 8785) claim. Pure module: imports only node:crypto. */ import { createHash } from "node:crypto"; export const DIGEST_ALGORITHM = "inspector-content-digest/1"; export class CanonicalError extends Error { constructor(code) { super(code); this.name = "CanonicalError"; this.code = code; this.reason = "unsupported-capability"; } } function isAscii(text) { for (let i = 0; i < text.length; i += 1) { if (text.charCodeAt(i) > 0x7f) return false; } return true; } function encodeString(text) { if (!isAscii(text)) throw new CanonicalError("string-not-ascii"); // JSON.stringify on an ASCII string escapes exactly ", \ and U+0000..U+001F // (short forms \b \f \n \r \t, otherwise \u00XX lowercase), matching Python // json.dumps for the ASCII domain. return JSON.stringify(text); } function encodeValue(value, depth) { if (depth > 64) throw new CanonicalError("depth-exceeded"); if (value === null) return "null"; switch (typeof value) { case "boolean": return value ? "true" : "false"; case "number": if (!Number.isSafeInteger(value) || Object.is(value, -0)) { throw new CanonicalError("number-not-safe-integer"); } return String(value); case "string": return encodeString(value); case "object": break; default: throw new CanonicalError("value-not-json"); } if (Array.isArray(value)) { const parts = []; for (const item of value) parts.push(encodeValue(item, depth + 1)); return `[${parts.join(",")}]`; } const keys = Object.keys(value); keys.sort((a, b) => (a < b ? -1 : a > b ? 1 : 0)); const parts = []; for (const key of keys) { const item = value[key]; if (item === undefined) throw new CanonicalError("value-not-json"); parts.push(`${encodeString(key)}:${encodeValue(item, depth + 1)}`); } return `{${parts.join(",")}}`; } /** Canonical compact encoding, or throws CanonicalError. */ export function canonicalize(value) { return encodeValue(value, 0); } /** "sha256:" of the canonical encoding, or throws CanonicalError. */ export function digestOf(value) { const canonical = canonicalize(value); return `sha256:${createHash("sha256").update(canonical, "utf8").digest("hex")}`; }