{"record":{"id":"b2d8b480bcc7e01f","repo":"ruvnet/ruflo","slug":"signattributionartifact-privatekey-must-be-32-byt","errorCode":null,"errorMessage":"signAttributionArtifact: privateKey must be 32 bytes (got ${privateKey.length})","messagePattern":"signAttributionArtifact: privateKey must be 32 bytes \\(got (.+?)\\)","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"plugins/ruflo-neural-trader/src/signed-attribution.ts","lineNumber":101,"sourceCode":" * Sign the body of an attribution artifact and return the fully-formed\n * `SignedAttributionArtifact` envelope.\n *\n * The signature covers the artifact body WITHOUT `witnessSignature` and\n * WITHOUT `witnessPublicKey` (CWE-347 pattern, same as Phase 4). The\n * verifier MUST pin to a trusted key for the pin to be a real defense.\n *\n * @param body                — artifact body (everything except signature fields + schema)\n * @param privateKeyHex       — 32-byte Ed25519 private key as hex (no 'ed25519:' prefix)\n * @returns                     the signed artifact ready to be stored\n */\nexport async function signAttributionArtifact(\n  body: SignedAttributionArtifactBody,\n  privateKeyHex: string,\n): Promise<SignedAttributionArtifact> {\n  const ed = await import('@noble/ed25519');\n  const privateKey = hexToBytes(privateKeyHex);\n  if (privateKey.length !== 32) {\n    throw new Error(\n      `signAttributionArtifact: privateKey must be 32 bytes (got ${privateKey.length})`,\n    );\n  }\n\n  const canonical = canonicalBytes(body);\n  const signatureBytes = await ed.signAsync(canonical, privateKey);\n  const publicKeyBytes = await ed.getPublicKeyAsync(privateKey);\n\n  return {\n    schema: 'ruflo-neural-trader-attribution/v1',\n    ...body,\n    witnessPublicKey: `ed25519:${bytesToHex(publicKeyBytes)}`,\n    witnessSignature: bytesToHex(signatureBytes),\n  };\n}\n\n/**\n * Verify a signed attribution artifact against a caller-supplied trusted","sourceCodeStart":83,"sourceCodeEnd":119,"githubUrl":"https://github.com/ruvnet/ruflo/blob/6b01dc5a687b26b3e218f796de45ec51f8fa9e8c/plugins/ruflo-neural-trader/src/signed-attribution.ts#L83-L119","documentation":"The attribution-artifact counterpart to signBacktestArtifact: signAttributionArtifact() hex-decodes the private key and requires exactly 32 bytes before signing. Same Ed25519 contract — 64 hex chars, no 'ed25519:' prefix — applied to the attribution schema (ruflo-neural-trader-attribution/v1).","triggerScenarios":"Passing privateKeyHex whose decoded byte length is not 32: prefixed keys, expanded 64-byte secret keys, base64-encoded keys, truncated/oversized hex, or non-hex input.","commonSituations":"Reusing the same signing key variable across both artifact types but with a prefix left on for one of them; env var populated from a secrets manager that base64-encodes by default; key generated by a library that emits the 64-byte expanded form.","solutions":["Pass a 32-byte Ed25519 seed as 64 hex characters with no 'ed25519:' prefix.","Strip the prefix and trim whitespace before calling: key.replace(/^ed25519:/, '').trim().","Share one canonical key-loading helper across signBacktestArtifact and signAttributionArtifact so both receive an identically normalized 32-byte hex string.","Generate keys with crypto.randomBytes(32).toString('hex')."],"exampleFix":"// before\nawait signAttributionArtifact(body, signingKeyHex); // 128 hex chars (64 bytes)\n\n// after\nconst seedHex = signingKeyHex.slice(0, 64); // take the 32-byte seed portion\nawait signAttributionArtifact(body, seedHex);","handlingStrategy":"validation","validationCode":"const cleanKey = privateKeyHex.replace(/^ed25519:/, '').trim();\nif (!/^[0-9a-fA-F]{64}$/.test(cleanKey)) throw new Error('attribution signing key must be 64 hex chars');\nawait signAttributionArtifact(body, cleanKey);","typeGuard":"function isEd25519SeedHex(key: string): boolean { return /^[0-9a-fA-F]{64}$/.test(key.replace(/^ed25519:/, '').trim()); }","tryCatchPattern":"try { await signAttributionArtifact(body, key); } catch (e) { if (e instanceof Error && /privateKey must be 32 bytes/.test(e.message)) throw new Error('Attribution signing key invalid', { cause: e }); throw e; }","preventionTips":["Reuse the same key-loading helper as signBacktestArtifact.","Validate key length once at startup, not per call.","Document the 'no prefix, 64 hex chars' contract wherever the key is configured."],"tags":["crypto","ed25519","signing","attribution","neural-trader"],"backgroundTag":null,"analyzedSha":"6b01dc5a687b26b3e218f796de45ec51f8fa9e8c","analyzedAt":"2026-08-12T13:20:50.148Z","schemaVersion":2},"datasetVersion":"2026-08-12T23:17:12.415Z"}