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

  1. Slice by code points, not code units: Array.from(value).slice(0, n).join('').
  2. Sanitize strings from untrusted sources before canonicalization with a well-formed check (iterate code units; drop or replace unpaired surrogates with U+FFFD).
  3. 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

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


AI-assisted analysis of ruvnet/ruflo@fa13ee4ad6 (2026-08-18). Data as JSON: /api/errors/0ca0a81c9ebbf681. Report an issue: GitHub.