ruvnet/ruflo · error · TypeError
JCS canonicalization rejects an unpaired low surrogate
Error message
JCS canonicalization rejects an unpaired low surrogate
What it means
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.
Source
Thrown at v3/@claude-flow/security/src/policy/product-plane.ts:1078
privacyClass,
observedAt,
expiresAt,
sequence,
},
};
}
function assertUnicodeScalarString(value: string): void {
for (let index = 0; index < value.length; index++) {
const code = value.charCodeAt(index);
if (code >= 0xd800 && code <= 0xdbff) {
const next = value.charCodeAt(index + 1);
if (!(next >= 0xdc00 && next <= 0xdfff)) {
throw new TypeError('JCS canonicalization rejects an unpaired high surrogate');
}
index++;
} else if (code >= 0xdc00 && code <= 0xdfff) {
throw new TypeError('JCS canonicalization rejects an unpaired low surrogate');
}
}
}
/**
* RFC 8785/JCS-compatible canonical JSON for this I-JSON profile.
*
* ECMAScript's JSON number serialization supplies the JCS number rendering.
* Non-finite numbers, sparse arrays, undefined values, non-plain objects, and
* invalid Unicode are rejected instead of being silently coerced.
*/
export function canonicalizeProductPlane(value: unknown): string {
if (value === null) return 'null';
if (typeof value === 'boolean') return value ? 'true' : 'false';
if (typeof value === 'string') {
assertUnicodeScalarString(value);
return JSON.stringify(value);
}View on GitHub (pinned to fa13ee4ad6)
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.
Example fix
// before
const parts = [value.slice(0, 10), value.slice(10)]; // boundary may split a pair
canonicalizeProductPlane({ v: parts[1] });
// after
const cps = Array.from(value);
const tail = cps.slice(10).join(''); // pairs never split
canonicalizeProductPlane({ v: tail }); Defensive patterns
Strategy: validation
Validate before calling
function noLoneLowSurrogate(value: string): boolean {
for (let i = 0; i < value.length; i++) {
const code = value.charCodeAt(i);
if (code >= 0xdc00 && code <= 0xdfff) {
const prev = value.charCodeAt(i - 1);
if (!(prev >= 0xd800 && prev <= 0xdbff)) return false;
}
}
return true;
}
if (!noLoneLowSurrogate(fragment)) throw new Error('fragment boundary split a surrogate pair'); Type guard
function isPairCompleteFragment(value: string): boolean {
return noLoneLowSurrogate(value) && !(value.charCodeAt(0) >= 0xdc00 && value.charCodeAt(0) <= 0xdfff);
} Try / catch
try {
return canonicalizeProductPlane({ v: fragment });
} catch (err) {
if (err instanceof TypeError && /low surrogate/.test(err.message)) {
throw new Error('chunk reassembly lost a surrogate lead — rejoin full payload before hashing');
}
throw err;
} Prevention
- 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.
When it happens
Trigger: 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.
Common situations: 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.
Related errors
- JCS canonicalization rejects an unpaired high surrogate
- JCS canonicalization rejects non-finite numbers
- canonical JSON does not support lone UTF-16 surrogates
- Federation canonicalization rejects cyclic values
- Policy canonicalization rejects non-finite numbers
AI-assisted analysis of ruvnet/ruflo@fa13ee4ad6 (2026-08-18).
Data as JSON: /api/errors/48c86756b6d982f2.
Report an issue: GitHub.