ruvnet/ruflo · error · TypeError

Federation canonicalization accepts only plain objects

Error message

Federation canonicalization accepts only plain objects

What it means

After arrays, the JCS canonicalizer accepts only plain objects: Object.getPrototypeOf must return Object.prototype or null. Class instances, Map/Set/Date, and objects from other realms (vm contexts, iframes) fail because their JSON serialization is implementation-defined and cannot be canonicalized deterministically for signing.

Source

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

    throw new TypeError('Federation canonicalization rejects cyclic values');
  }
  ancestors.add(object);

  try {
    if (Array.isArray(value)) {
      const items: string[] = [];
      for (let index = 0; index < value.length; index += 1) {
        if (!Object.prototype.hasOwnProperty.call(value, index)) {
          throw new TypeError('Federation canonicalization rejects sparse arrays');
        }
        items.push(canonicalizeJcsValue(value[index], ancestors));
      }
      return `[${items.join(',')}]`;
    }

    const prototype = Object.getPrototypeOf(value);
    if (prototype !== Object.prototype && prototype !== null) {
      throw new TypeError('Federation canonicalization accepts only plain objects');
    }

    const record = value as Record<string, unknown>;
    const entries = Object.keys(record)
      .sort()
      .map((key) => `${JSON.stringify(key)}:${canonicalizeJcsValue(record[key], ancestors)}`);
    return `{${entries.join(',')}}`;
  } finally {
    ancestors.delete(object);
  }
}

function envelopeSignatureVersion(message: AgentMessage): EnvelopeSignatureVersion {
  const version = (message.metadata as Record<string, unknown> | undefined)?.signatureVersion;
  if (version === undefined) return 'legacy-v1';
  if (version === 'jcs-v1') return version;
  throw new TypeError(`Unsupported federation signature version: ${String(version)}`);
}

View on GitHub (pinned to fa13ee4ad6)

Solutions

  1. Convert to plain data before signing: JSON round-trip or an explicit to-POJO mapper
  2. Dates to ISO strings; Maps/Sets to plain objects/arrays
  3. Ensure received payloads go through JSON.parse before canonicalization (parse output is always plain)
  4. For cross-realm data, re-create objects in the signing realm via structured copy

Example fix

// before
metadata.when = new Date(); // Date instance
// after
metadata.when = new Date().toISOString();
Defensive patterns

Strategy: type-guard

Validate before calling

// convert to plain data before signing: dates -> strings, keep only own props
function toPojo(v: unknown): unknown {
  if (Array.isArray(v)) return v.map(toPojo);
  if (v instanceof Date) return v.toISOString();
  if (v && typeof v === 'object') {
    const out: Record<string, unknown> = {};
    for (const [k, val] of Object.entries(v)) out[k] = toPojo(val);
    return out;
  }
  return v;
}

Type guard

function isPlainObject(v: unknown): v is Record<string, unknown> {
  if (typeof v !== 'object' || v === null) return false;
  const p = Object.getPrototypeOf(v);
  return p === Object.prototype || p === null;
}

Prevention

When it happens

Trigger: Passing class instances (with getters or methods) as message metadata; Date, Map, or Set objects embedded in the payload; plain-looking objects created inside another vm/iframe realm whose prototype is a different Object.prototype.

Common situations: Domain objects copied into envelopes via spread (spread keeps the prototype for class instances is wrong — but direct reference does); timestamps left as Date; cross-realm messaging in Electron or vm-based sandboxes.

Related errors


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