{"record":{"id":"ab15fe388e5a1a28","repo":"ruvnet/ruflo","slug":"federation-canonicalization-accepts-only-plain-obj","errorCode":null,"errorMessage":"Federation canonicalization accepts only plain objects","messagePattern":"Federation canonicalization accepts only plain objects","errorType":"exception","errorClass":"TypeError","httpStatus":null,"severity":"error","filePath":"v3/@claude-flow/plugin-agent-federation/src/application/inbound-dispatcher.ts","lineNumber":189,"sourceCode":"    throw new TypeError('Federation canonicalization rejects cyclic values');\n  }\n  ancestors.add(object);\n\n  try {\n    if (Array.isArray(value)) {\n      const items: string[] = [];\n      for (let index = 0; index < value.length; index += 1) {\n        if (!Object.prototype.hasOwnProperty.call(value, index)) {\n          throw new TypeError('Federation canonicalization rejects sparse arrays');\n        }\n        items.push(canonicalizeJcsValue(value[index], ancestors));\n      }\n      return `[${items.join(',')}]`;\n    }\n\n    const prototype = Object.getPrototypeOf(value);\n    if (prototype !== Object.prototype && prototype !== null) {\n      throw new TypeError('Federation canonicalization accepts only plain objects');\n    }\n\n    const record = value as Record<string, unknown>;\n    const entries = Object.keys(record)\n      .sort()\n      .map((key) => `${JSON.stringify(key)}:${canonicalizeJcsValue(record[key], ancestors)}`);\n    return `{${entries.join(',')}}`;\n  } finally {\n    ancestors.delete(object);\n  }\n}\n\nfunction envelopeSignatureVersion(message: AgentMessage): EnvelopeSignatureVersion {\n  const version = (message.metadata as Record<string, unknown> | undefined)?.signatureVersion;\n  if (version === undefined) return 'legacy-v1';\n  if (version === 'jcs-v1') return version;\n  throw new TypeError(`Unsupported federation signature version: ${String(version)}`);\n}","sourceCodeStart":171,"sourceCodeEnd":207,"githubUrl":"https://github.com/ruvnet/ruflo/blob/fa13ee4ad60ac2090b1480656eb233521790d640/v3/@claude-flow/plugin-agent-federation/src/application/inbound-dispatcher.ts#L171-L207","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","solutions":["Convert to plain data before signing: JSON round-trip or an explicit to-POJO mapper","Dates to ISO strings; Maps/Sets to plain objects/arrays","Ensure received payloads go through JSON.parse before canonicalization (parse output is always plain)","For cross-realm data, re-create objects in the signing realm via structured copy"],"exampleFix":"// before\nmetadata.when = new Date(); // Date instance\n// after\nmetadata.when = new Date().toISOString();","handlingStrategy":"type-guard","validationCode":"// convert to plain data before signing: dates -> strings, keep only own props\nfunction toPojo(v: unknown): unknown {\n  if (Array.isArray(v)) return v.map(toPojo);\n  if (v instanceof Date) return v.toISOString();\n  if (v && typeof v === 'object') {\n    const out: Record<string, unknown> = {};\n    for (const [k, val] of Object.entries(v)) out[k] = toPojo(val);\n    return out;\n  }\n  return v;\n}","typeGuard":"function isPlainObject(v: unknown): v is Record<string, unknown> {\n  if (typeof v !== 'object' || v === null) return false;\n  const p = Object.getPrototypeOf(v);\n  return p === Object.prototype || p === null;\n}","tryCatchPattern":null,"preventionTips":["Never place Date/Map/Set or class instances in signed payloads","Route received data through JSON.parse before canonicalization","Watch cross-realm objects from vm/iframe contexts — re-create them locally","Map domain objects to POJOs explicitly at the envelope boundary"],"tags":["canonicalization","jcs","objects","prototype"],"backgroundTag":"non-plain-object","analyzedSha":"fa13ee4ad60ac2090b1480656eb233521790d640","analyzedAt":"2026-08-18T21:34:22.708Z","contentChangedAt":"2026-08-18T21:34:22.708Z","schemaVersion":2},"datasetVersion":"2026-09-14T00:17:10.932Z"}