{"record":{"id":"28a5390ddd6c92da","repo":"thedotmack/claude-mem","slug":"canonical-content-message","errorCode":null,"errorMessage":"canonical content: ${message}","messagePattern":"canonical content: (.+?)","errorType":"validation","errorClass":"Error","httpStatus":400,"severity":"error","filePath":"workers/sync-hub/src/canonical-content.ts","lineNumber":59,"sourceCode":"const ENVELOPE_KEYS = [\n\t\"body_schema_version\",\n\t\"deleted\",\n\t\"deleted_at\",\n\t\"entity_rev\",\n\t\"id\",\n\t\"kind\",\n\t\"mutation\",\n\t\"origin_device_id\",\n\t\"origin_local_id\",\n\t\"payload\",\n\t\"payload_schema_version\",\n\t\"payload_sha256\",\n] as const;\n\nconst encoder = new TextEncoder();\n\nfunction invalid(message: string): never {\n\tthrow new Error(`canonical content: ${message}`);\n}\n\n/** RFC-8259 JSON with recursively sorted object keys and preserved array order. */\nexport function canonicalJson(value: unknown): string {\n\treturn JSON.stringify(normalizeJson(value, new Set()));\n}\n\nfunction normalizeJson(value: unknown, seen: Set<object>): unknown {\n\tif (value === null || typeof value === \"string\" || typeof value === \"boolean\") return value;\n\tif (typeof value === \"number\") {\n\t\tif (!Number.isFinite(value)) invalid(\"numbers must be finite\");\n\t\tif (!Number.isSafeInteger(value) && Number.isInteger(value)) {\n\t\t\tinvalid(\"integers must be safe; use decimal strings for uint64 values\");\n\t\t}\n\t\tif (Object.is(value, -0)) return 0;\n\t\treturn value;\n\t}\n\tif (typeof value !== \"object\") invalid(`unsupported JSON value ${typeof value}`);","sourceCodeStart":41,"sourceCodeEnd":77,"githubUrl":"https://github.com/thedotmack/claude-mem/blob/d8bc9755e74915e5c3b999181e10a67c889bce2a/workers/sync-hub/src/canonical-content.ts#L41-L77","documentation":"Internal helper `invalid()` in canonical-content.ts throws this for any violation of the canonical-content rules: non-RFC-8259 JSON values, unsorted object keys, invalid decimal forms, or malformed operation envelopes. Nearly every exported function of the module (canonicalJson, stableDocumentId, wrapCanonicalBody, parseCanonicalOperation, decimal helpers) funnels through it, so the message names the specific invariant that failed.","triggerScenarios":"Calling `canonicalJson` with a value JSON.stringify can't represent deterministically (undefined, functions, cycles via normalizeJson's checks), passing a decimal string with leading zeros or non-canonical exponent form to `assertCanonicalDecimal`/`incrementCanonicalDecimal`, or feeding `parseCanonicalOperation` an object missing required fields like `payload_sha256`.","commonSituations":"Feeding user-supplied JSON that hasn't been normalized (duplicate/unsorted keys, -0, non-finite numbers); hand-built operations that skip canonicalization before hashing; upgrading the library and old persisted documents no longer satisfy stricter canonical rules; accidentally passing a JS number where a canonical decimal string is expected.","solutions":["Read the trailing `${message}` in the error to identify which invariant failed (key order, decimal form, missing field).","Pass raw data through `canonicalJson()`/`normalizeJson()` before hashing or building operation payloads.","Represent numeric values as canonical decimal strings (no leading zeros, explicit fraction/exponent rules), never raw JS numbers.","Validate operation envelopes against the required-field list (`payload_sha256`, etc.) before `parseCanonicalOperation`."],"exampleFix":"// before\nconst id = stableDocumentId(userDoc); // throws: canonical content: keys not sorted\n\n// after\nconst canonical = canonicalJson(userDoc);\nconst id = stableDocumentId(JSON.parse(canonical)); // normalized first\n","handlingStrategy":"validation","validationCode":"// validate inputs before calling canonical-content APIs\nfunction assertCanonicalizable(value: unknown): void {\n  if (value === undefined || typeof value === 'function' || typeof value === 'symbol') {\n    throw new Error('value is not RFC-8259 JSON serializable');\n  }\n  if (typeof value === 'number' && (!Number.isFinite(value) || Object.is(value, -0))) {\n    throw new Error('numbers must be finite and not -0');\n  }\n}\nfunction assertDecimalString(s: string): void {\n  if (!/^-?(0|[1-9]\\d*)(\\.\\d+)?$/.test(s)) throw new Error(`non-canonical decimal: ${s}`);\n}","typeGuard":"function isPlainJsonObject(v: unknown): v is Record<string, unknown> {\n  return typeof v === 'object' && v !== null && !Array.isArray(v) &&\n    Object.getPrototypeOf(v) === Object.prototype;\n}","tryCatchPattern":"try {\n  const canonical = canonicalJson(rawDocument);\n} catch (err) {\n  if (err instanceof Error && err.message.startsWith('canonical content: ')) {\n    console.error(`document rejected by canonicalizer: ${err.message}`);\n    // fall back to re-serializing through JSON.parse(JSON.stringify(...)) then retry once\n  } else throw err;\n}","preventionTips":["Always route external JSON through normalizeJson/canonicalJson before hashing or ID generation.","Store numbers as canonical decimal strings, never raw JS numbers.","Round-trip persisted documents through the canonicalizer in a migration test when upgrading the library.","Unit-test each invariant (key order, decimal form, required fields) with negative cases."],"tags":["canonicalization","json","validation","invariant"],"backgroundTag":"schema-validation-failed","analyzedSha":"d8bc9755e74915e5c3b999181e10a67c889bce2a","analyzedAt":"2026-09-17T16:40:26.182Z","contentChangedAt":"2026-09-17T16:40:26.182Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}