ruvnet/ruflo · error · TypeError
JCS canonicalization rejects an unpaired high surrogate
Error message
JCS canonicalization rejects an unpaired high surrogate
What it means
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.
Source
Thrown at v3/@claude-flow/security/src/policy/product-plane.ts:1074
abstained: input.abstained,
modelDigest: modelDigest as `sha256:${string}`,
...(calibrationDigest ? { calibrationDigest: calibrationDigest as `sha256:${string}` } : {}),
...(hardwareRef ? { hardwareRef } : {}),
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';View on GitHub (pinned to fa13ee4ad6)
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.
Example fix
// before
const truncated = payload.slice(0, 64); // may split a surrogate pair
canonicalizeProductPlane({ note: truncated });
// after
const truncated = Array.from(payload).slice(0, 64).join('');
canonicalizeProductPlane({ note: truncated }); Defensive patterns
Strategy: validation
Validate before calling
function isWellFormedString(value: string): boolean {
for (let i = 0; i < value.length; i++) {
const code = value.charCodeAt(i);
if (code >= 0xd800 && code <= 0xdbff) {
const next = value.charCodeAt(i + 1);
if (!(next >= 0xdc00 && next <= 0xdfff)) return false;
i++;
} else if (code >= 0xdc00 && code <= 0xdfff) {
return false;
}
}
return true;
}
if (!isWellFormedString(input)) input = sanitize(input); // strip or replace with U+FFFD Type guard
function isUnicodeScalarString(value: string): boolean {
return isWellFormedString(value); // pair-check as above; use as a narrowing predicate on external input
} Try / catch
try {
return canonicalizeProductPlane(payload);
} catch (err) {
if (err instanceof TypeError && /surrogate/.test(err.message)) {
return canonicalizeProductPlane(sanitizeStrings(payload)); // replace unpaired surrogates, retry once
}
throw err;
} Prevention
- 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.
When it happens
Trigger: 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.
Common situations: 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.
Related errors
- JCS canonicalization rejects an unpaired low 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/0ca0a81c9ebbf681.
Report an issue: GitHub.