{"record":{"id":"0ca0a81c9ebbf681","repo":"ruvnet/ruflo","slug":"jcs-canonicalization-rejects-an-unpaired-high-surr","errorCode":null,"errorMessage":"JCS canonicalization rejects an unpaired high surrogate","messagePattern":"JCS canonicalization rejects an unpaired high surrogate","errorType":"exception","errorClass":"TypeError","httpStatus":null,"severity":"error","filePath":"v3/@claude-flow/security/src/policy/product-plane.ts","lineNumber":1074,"sourceCode":"      abstained: input.abstained,\n      modelDigest: modelDigest as `sha256:${string}`,\n      ...(calibrationDigest ? { calibrationDigest: calibrationDigest as `sha256:${string}` } : {}),\n      ...(hardwareRef ? { hardwareRef } : {}),\n      privacyClass,\n      observedAt,\n      expiresAt,\n      sequence,\n    },\n  };\n}\n\nfunction assertUnicodeScalarString(value: string): void {\n  for (let index = 0; index < value.length; index++) {\n    const code = value.charCodeAt(index);\n    if (code >= 0xd800 && code <= 0xdbff) {\n      const next = value.charCodeAt(index + 1);\n      if (!(next >= 0xdc00 && next <= 0xdfff)) {\n        throw new TypeError('JCS canonicalization rejects an unpaired high surrogate');\n      }\n      index++;\n    } else if (code >= 0xdc00 && code <= 0xdfff) {\n      throw new TypeError('JCS canonicalization rejects an unpaired low surrogate');\n    }\n  }\n}\n\n/**\n * RFC 8785/JCS-compatible canonical JSON for this I-JSON profile.\n *\n * ECMAScript's JSON number serialization supplies the JCS number rendering.\n * Non-finite numbers, sparse arrays, undefined values, non-plain objects, and\n * invalid Unicode are rejected instead of being silently coerced.\n */\nexport function canonicalizeProductPlane(value: unknown): string {\n  if (value === null) return 'null';\n  if (typeof value === 'boolean') return value ? 'true' : 'false';","sourceCodeStart":1056,"sourceCodeEnd":1092,"githubUrl":"https://github.com/ruvnet/ruflo/blob/fa13ee4ad60ac2090b1480656eb233521790d640/v3/@claude-flow/security/src/policy/product-plane.ts#L1056-L1092","documentation":"assertUnicodeScalarString() enforces the I-JSON/RFC 8785 rule that strings be sequences of Unicode scalar values: a high surrogate (U+D800-U+DBFF) must be immediately followed by a low surrogate. A high surrogate with anything else (or nothing) after it throws TypeError before the string reaches canonical JSON — lone surrogates would otherwise hash differently across platforms.","triggerScenarios":"Strings assembled via String.fromCharCode(0xD800); truncation that splits a surrogate pair in half (e.g. value.slice(0, 20) cutting between the two code units); concatenating fragments whose boundaries land inside a pair; upstream data produced by a sloppy JSON.parse repair.","commonSituations":"Truncating user text or identifiers containing emoji/CJK extension characters to fit a length limit; log-line or payload chunking at byte/code-unit boundaries; test fixtures pasted from editors that mangle pairs.","solutions":["Slice by code points, not code units: Array.from(value).slice(0, n).join('').","Sanitize strings from untrusted sources before canonicalization with a well-formed check (iterate code units; drop or replace unpaired surrogates with U+FFFD).","Fix the producer that emits lone surrogates rather than catching at hash time."],"exampleFix":"// before\nconst truncated = payload.slice(0, 64); // may split a surrogate pair\ncanonicalizeProductPlane({ note: truncated });\n\n// after\nconst truncated = Array.from(payload).slice(0, 64).join('');\ncanonicalizeProductPlane({ note: truncated });","handlingStrategy":"validation","validationCode":"function isWellFormedString(value: string): boolean {\n  for (let i = 0; i < value.length; i++) {\n    const code = value.charCodeAt(i);\n    if (code >= 0xd800 && code <= 0xdbff) {\n      const next = value.charCodeAt(i + 1);\n      if (!(next >= 0xdc00 && next <= 0xdfff)) return false;\n      i++;\n    } else if (code >= 0xdc00 && code <= 0xdfff) {\n      return false;\n    }\n  }\n  return true;\n}\nif (!isWellFormedString(input)) input = sanitize(input); // strip or replace with U+FFFD","typeGuard":"function isUnicodeScalarString(value: string): boolean {\n  return isWellFormedString(value); // pair-check as above; use as a narrowing predicate on external input\n}","tryCatchPattern":"try {\n  return canonicalizeProductPlane(payload);\n} catch (err) {\n  if (err instanceof TypeError && /surrogate/.test(err.message)) {\n    return canonicalizeProductPlane(sanitizeStrings(payload)); // replace unpaired surrogates, retry once\n  }\n  throw err;\n}","preventionTips":["Truncate by code points (Array.from(s).slice(0, n).join('')), never by String.slice on untrusted text.","Validate strings from external sources at the boundary, before they reach signing/hashing.","Prefer newer engines' String.prototype.toWellFormed() where available for sanitization."],"tags":["unicode","serialization","jcs","validation"],"backgroundTag":"invalid-unicode-surrogate","analyzedSha":"fa13ee4ad60ac2090b1480656eb233521790d640","analyzedAt":"2026-08-18T21:34:22.708Z","contentChangedAt":"2026-08-18T21:34:22.708Z","schemaVersion":2},"datasetVersion":"2026-09-14T00:17:10.932Z"}