ruvnet/ruflo · error · TypeError

Federation canonicalization rejects unsafe integers

Error message

Federation canonicalization rejects unsafe integers

What it means

JCS canonicalization rejects integers beyond the safe range: values where Number.isInteger is true but Number.isSafeInteger is false (magnitude above 2^53 - 1). Such values silently lose precision in IEEE-754 doubles, so sender and receiver could canonicalize different bytes and signature verification would fail spuriously or, worse, verify the wrong value.

Source

Thrown at v3/@claude-flow/plugin-agent-federation/src/application/inbound-dispatcher.ts:155

 * Federation messages are wrapped as `AgentMessage{id, type, payload,
 * metadata}` on the wire. The `payload` is the actual FederationEnvelope
 * (per `plugin.ts sendToNode`); we canonicalize the payload + the
 * metadata so the receiver verifies the same bytes the sender signed.
 */
function canonicalizeJcsValue(value: unknown, ancestors: Set<object>): string {
  if (value === null) return 'null';

  switch (typeof value) {
    case 'boolean':
      return value ? 'true' : 'false';
    case 'string':
      return JSON.stringify(value);
    case 'number':
      if (!Number.isFinite(value) || Object.is(value, -0)) {
        throw new TypeError('Federation canonicalization rejects non-canonical numbers');
      }
      if (Number.isInteger(value) && !Number.isSafeInteger(value)) {
        throw new TypeError('Federation canonicalization rejects unsafe integers');
      }
      return JSON.stringify(value);
    case 'bigint':
    case 'function':
    case 'symbol':
    case 'undefined':
      throw new TypeError(`Federation canonicalization rejects ${typeof value}`);
    case 'object':
      break;
    default:
      throw new TypeError(`Federation canonicalization rejects ${typeof value}`);
  }

  const object = value as object;
  if (ancestors.has(object)) {
    throw new TypeError('Federation canonicalization rejects cyclic values');
  }
  ancestors.add(object);

View on GitHub (pinned to fa13ee4ad6)

Solutions

  1. Serialize 64-bit identifiers and timestamps as strings inside signed envelopes
  2. Use safe-integer ID generation, or split hi/lo halves, for values that must remain numeric
  3. Validate at the boundary: reject integers where Number.isSafeInteger is false before signing

Example fix

// before
metadata.id = snowflakeId; // 64-bit integer, not safe
// after
metadata.id = String(snowflakeId);
Defensive patterns

Strategy: type-guard

Validate before calling

// boundary check: reject unsafe integers before signing
function assertSafeNumbers(value: unknown): void {
  if (typeof value === 'number' && Number.isInteger(value) && !Number.isSafeInteger(value)) {
    throw new RangeError('unsafe integer in signed payload: ' + value);
  }
  if (typeof value === 'object' && value !== null) {
    for (const v of Object.values(value)) assertSafeNumbers(v);
  }
}

Type guard

function isSafeCanonicalInteger(v: unknown): v is number {
  return (
    typeof v === 'number' &&
    Number.isInteger(v) &&
    Number.isSafeInteger(v)
  );
}

Prevention

When it happens

Trigger: 64-bit IDs (snowflake IDs, database bigint keys) or nanosecond timestamps placed as JSON numbers in signed metadata; values received from serializers in other languages that happily emit 64-bit JSON integers.

Common situations: Porting pipelines from languages whose JSON writers emit int64 numbers; mixing JS with services that use 64-bit identifiers; log-precision timestamps in envelopes.

Related errors


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