Canonical JSON
Canonical JSON is a deterministic serialization where semantically-equal payloads with different key orders produce identical bytes, so a hash or signature over the value is reproducible everywhere. Caisson's kernel canonicalize sorts object keys recursively, keeps array order, and rejects non-finite numbers, feeding every audit-chain hash, signed anchor, and license claim signature.
In code
function sortValue(value: JsonValue): JsonValue {
if (value === null || typeof value !== "object") {
if (typeof value === "number" && !Number.isFinite(value)) {
throw new Error(
`audit-chain: non-finite number is not canonicalizable: ${String(value)}`,
);
}
return value;
}
if (Array.isArray(value)) return value.map(sortValue);
const obj = value as { readonly [key: string]: JsonValue };
const out: { [key: string]: JsonValue } = {};
for (const key of Object.keys(obj).sort()) {
out[key] = sortValue(obj[key] as JsonValue);
}
return out;
}
export function canonicalize(value: JsonValue): string {
return JSON.stringify(sortValue(value));
}How it holds
One function, every hash and signature in the platform
canonicalize is imported directly by audit-chain's hashChainLink and contentHash, the evidence pack's receipt hashing, migration-assembly's cumulative hash, license-issue's issueLicense (which signs canonicalize(parsedClaims) into the wire token), and license-verify's verifyLicense, one serialization primitive backs every place Caisson hashes or signs a JSON payload, with no second codepath that could quietly drift from it.
Format conformance, not just signature conformance
license-verify's verifyLicense checks the Ed25519 signature first, then re-canonicalizes the parsed claims and rejects the token outright if the signed bytes aren't byte-identical to that canonical form, a token can't be re-serialized with different key order or whitespace and still verify, even carrying an authentic signature.
Non-finite numbers throw instead of silently serializing
sortValue rejects Infinity and NaN with a thrown error rather than letting JSON.stringify silently print them as null; a value that can't round-trip through canonical bytes never gets hashed or signed as though it could.
A frozen algorithm, any byte change invalidates every stored hash
canonical.ts documents its own output as the single source of canonical bytes for the chain hash: recursive key sort, kept array order, JSON.stringify. Any change to that algorithm is a chain-format break, because it would silently invalidate every hash already computed and stored.