{"record":{"id":"48c86756b6d982f2","repo":"ruvnet/ruflo","slug":"jcs-canonicalization-rejects-an-unpaired-low-surro","errorCode":null,"errorMessage":"JCS canonicalization rejects an unpaired low surrogate","messagePattern":"JCS canonicalization rejects an unpaired low surrogate","errorType":"exception","errorClass":"TypeError","httpStatus":null,"severity":"error","filePath":"v3/@claude-flow/security/src/policy/product-plane.ts","lineNumber":1078,"sourceCode":"      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';\n  if (typeof value === 'string') {\n    assertUnicodeScalarString(value);\n    return JSON.stringify(value);\n  }","sourceCodeStart":1060,"sourceCodeEnd":1096,"githubUrl":"https://github.com/ruvnet/ruflo/blob/fa13ee4ad60ac2090b1480656eb233521790d640/v3/@claude-flow/security/src/policy/product-plane.ts#L1060-L1096","documentation":"The mirror case of the high-surrogate check: any low surrogate code unit (U+DC00-U+DFFF) encountered on its own — i.e. not immediately preceded by a high surrogate, which the loop detects by throwing when it reaches a low surrogate in a non-pair position — is rejected with TypeError. Canonical (JCS) output must be valid UTF-8, and lone low surrogates never are.","triggerScenarios":"A string starting with a low surrogate ('\\\\udc00' literal or String.fromCharCode(0xDC00)); slicing off the first half of a surrogate pair and keeping the second; concatenating a tail fragment that begins mid-pair.","commonSituations":"Chunked transport (streaming, Kafka, WebSocket frames) splitting multi-byte characters at code-unit boundaries and reassembling in the wrong order; regex-based text processing that deletes the leading half of a pair; corrupted fixtures in tests.","solutions":["Reassemble or slice using code-point-aware operations (Array.from, [...str], Intl.Segmenter) so pairs stay intact.","Validate/sanitize external strings before they enter the product-plane payload: strip or replace unpaired surrogates.","Repair chunk boundaries by re-joining fragments before canonicalization instead of hashing pieces."],"exampleFix":"// before\nconst parts = [value.slice(0, 10), value.slice(10)]; // boundary may split a pair\ncanonicalizeProductPlane({ v: parts[1] });\n\n// after\nconst cps = Array.from(value);\nconst tail = cps.slice(10).join(''); // pairs never split\ncanonicalizeProductPlane({ v: tail });","handlingStrategy":"validation","validationCode":"function noLoneLowSurrogate(value: string): boolean {\n  for (let i = 0; i < value.length; i++) {\n    const code = value.charCodeAt(i);\n    if (code >= 0xdc00 && code <= 0xdfff) {\n      const prev = value.charCodeAt(i - 1);\n      if (!(prev >= 0xd800 && prev <= 0xdbff)) return false;\n    }\n  }\n  return true;\n}\nif (!noLoneLowSurrogate(fragment)) throw new Error('fragment boundary split a surrogate pair');","typeGuard":"function isPairCompleteFragment(value: string): boolean {\n  return noLoneLowSurrogate(value) && !(value.charCodeAt(0) >= 0xdc00 && value.charCodeAt(0) <= 0xdfff);\n}","tryCatchPattern":"try {\n  return canonicalizeProductPlane({ v: fragment });\n} catch (err) {\n  if (err instanceof TypeError && /low surrogate/.test(err.message)) {\n    throw new Error('chunk reassembly lost a surrogate lead — rejoin full payload before hashing');\n  }\n  throw err;\n}","preventionTips":["Reassemble chunked streams fully before canonicalization; never hash fragments split at code-unit boundaries.","Use code-point-aware slicing and searching ([...str], Array.from) around emoji/CJK extension characters.","Add fixtures with emoji payloads to serialization tests to catch pair-splitting early."],"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"}