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

  1. Reassemble or slice using code-point-aware operations (Array.from, [...str], Intl.Segmenter) so pairs stay intact.
  2. Validate/sanitize external strings before they enter the product-plane payload: strip or replace unpaired surrogates.
  3. 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

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


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