42 lines
1.4 KiB
TypeScript
42 lines
1.4 KiB
TypeScript
/**
|
|
* Deterministic JSON serialization — sorted keys recursively, no whitespace.
|
|
* Required for manifest SHA-256 over exact bytes (SPIKE-04, ARCH 10.5).
|
|
* Trace: SPIKE-04 §2 manifest, IMPLEMENTATION-CONTRACT.md §15
|
|
*/
|
|
|
|
export function canonicalJson(value: unknown): string {
|
|
return stringify(value);
|
|
}
|
|
|
|
function stringify(value: unknown): string {
|
|
if (value === null) return "null";
|
|
if (value === undefined) return "null";
|
|
const t = typeof value;
|
|
if (t === "string") return JSON.stringify(value);
|
|
if (t === "number") {
|
|
if (!Number.isFinite(value as number)) throw new Error("non-finite number in canonical JSON");
|
|
return JSON.stringify(value);
|
|
}
|
|
if (t === "boolean") return value ? "true" : "false";
|
|
if (Array.isArray(value)) {
|
|
const items = (value as unknown[]).map((v) => stringify(v));
|
|
return `[${items.join(",")}]`;
|
|
}
|
|
if (t === "object") {
|
|
const obj = value as Record<string, unknown>;
|
|
const keys = Object.keys(obj).sort();
|
|
const parts: string[] = [];
|
|
for (const k of keys) {
|
|
const v = obj[k];
|
|
if (v === undefined) continue; // omit undefined like JSON.stringify
|
|
parts.push(`${JSON.stringify(k)}:${stringify(v)}`);
|
|
}
|
|
return `{${parts.join(",")}}`;
|
|
}
|
|
throw new Error(`unsupported canonical json type ${t}`);
|
|
}
|
|
|
|
export function canonicalBytes(value: unknown): Uint8Array {
|
|
const str = canonicalJson(value);
|
|
return new TextEncoder().encode(str);
|
|
}
|