{"record":{"id":"e42c56330e8b8ca8","repo":"ruvnet/ruflo","slug":"channel-key-must-be-32-bytes-64-hex","errorCode":null,"errorMessage":"channel key must be 32 bytes (64 hex)","messagePattern":"channel key must be 32 bytes \\(64 hex\\)","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"v3/@claude-flow/cli/src/mcp-tools/x-federation-channels.ts","lineNumber":59,"sourceCode":"const degraded = () => ({ degraded: true, reason: 'nostr-tools not installed', hint: 'npm i nostr-tools  (secp256k1 + NIP-44 are not in node:crypto)' });\n\n/** Locally cached channel keys, 0600. Losing this file loses the channels in it — by design. */\nexport type ChannelStore = Record<string, { key: string; name?: string; grantedBy?: string; at: string }>;\nexport function readStore(file = STORE_FILE()): ChannelStore {\n  if (!existsSync(file)) return {};\n  try { return JSON.parse(readFileSync(file, 'utf8')) as ChannelStore; } catch { return {}; }\n}\nexport function writeStore(store: ChannelStore, file = STORE_FILE()): void {\n  mkdirSync(dirname(file), { recursive: true, mode: 0o700 });\n  writeFileSync(file, JSON.stringify(store, null, 2), { mode: 0o600 });\n}\nexport function publicChannelId(name: string): string {\n  if (!CHANNEL_NAME_RE.test(String(name))) throw new Error('channel name must match [a-z0-9][a-z0-9._-]{0,63}');\n  return `pub:${name}`;\n}\nexport function privateChannelId(keyHex: string): string {\n  const k = Buffer.from(keyHex, 'hex');\n  if (k.length !== 32) throw new Error('channel key must be 32 bytes (64 hex)');\n  return `prv:${createHash('sha256').update(k).digest('hex').slice(0, 16)}`;\n}\nexport function newChannelKey(): string { return randomBytes(32).toString('hex'); }\nexport function isPrivateChannel(id: string): boolean { return String(id).startsWith('prv:'); }\n\nasync function relayCall<T>(relayWs: string, sk: Uint8Array, nt: Nt, fn: (ws: WsLike) => Promise<T>): Promise<T> {\n  const { default: WebSocket } = await import('ws');\n  const ws = new WebSocket(relayWs, { perMessageDeflate: false }) as unknown as WsLike;\n  await new Promise<void>((resolve, reject) => {\n    const t = setTimeout(() => reject(new Error('relay auth timeout')), 15000);\n    ws.on('message', (d: Buffer) => {\n      const m = JSON.parse(d.toString());\n      if (m[0] === 'AUTH' && typeof m[1] === 'string') {\n        ws.send(JSON.stringify(['AUTH', nt.finalizeEvent({ kind: 22242, created_at: Math.floor(Date.now() / 1000), tags: [['relay', relayWs], ['challenge', m[1]]], content: '' }, sk)]));\n      } else if (m[0] === 'OK') { clearTimeout(t); m[2] ? resolve() : reject(new Error(`relay refused auth: ${m[3] || 'not a member'}`)); }\n    });\n    ws.on('error', (e: Error) => { clearTimeout(t); reject(e); });\n  });","sourceCodeStart":41,"sourceCodeEnd":77,"githubUrl":"https://github.com/ruvnet/ruflo/blob/2602b642d92234c710ffbe96bfb33007d481ceab/v3/@claude-flow/cli/src/mcp-tools/x-federation-channels.ts#L41-L77","documentation":"privateChannelId derives a private channel id (prv:<16-hex>) by hashing a 32-byte channel key supplied as 64 hex characters. It throws when Buffer.from(keyHex,'hex') does not yield exactly 32 bytes — i.e. the key string is not valid 64-char hex, or represents a different byte length. This guards NIP-44 encryption, which requires a 256-bit channel key.","triggerScenarios":"Creating or joining a private channel with a key that is not 64 hex chars (e.g. 32-hex half key, base64-encoded key, key with 0x prefix or whitespace); passing a randomBytes(16) key; truncated or hand-trimmed key strings.","commonSituations":"Generating the key with the wrong byte length (16 or 64 bytes instead of 32); storing the key in config with quotes/whitespace/0x prefix; confusing the channel key with a Nostr private key or another hex secret; copying only part of the key from a shared secret.","solutions":["Generate the key with newChannelKey() (randomBytes(32).toString('hex')) so it is exactly 64 hex chars","Strip whitespace and any 0x prefix from the key before use; verify length with /^[0-9a-f]{64}$/i","If the key is base64 or another encoding, convert to 64-char lowercase hex first"],"exampleFix":"// before\nprivateChannelId('0x' + key.slice(0, 32)); // throws: not 32 bytes\n// after\nconst key = newChannelKey(); // 64 hex chars\nif (!/^[0-9a-f]{64}$/i.test(key)) throw new Error('bad channel key');\nprivateChannelId(key.trim().replace(/^0x/, ''));","handlingStrategy":"validation","validationCode":"function isValidChannelKey(keyHex: string): boolean {\n  return /^[0-9a-f]{64}$/i.test(String(keyHex).trim().replace(/^0x/, ''));\n}","typeGuard":null,"tryCatchPattern":"try {\n  const id = privateChannelId(keyHex);\n} catch {\n  if (!isValidChannelKey(keyHex)) {\n    keyHex = newChannelKey(); // generate a proper 32-byte key\n  }\n  const id = privateChannelId(keyHex.trim().replace(/^0x/, ''));\n}","preventionTips":["Generate keys only via newChannelKey()","Never truncate, prefix (0x), or re-encode keys when storing/sharing them","Distinguish channel keys from Nostr keys and other hex secrets in your config naming","Validate with /^[0-9a-f]{64}$/i at config load time, before any crypto call"],"tags":["validation","crypto","hex","input"],"backgroundTag":"invalid-argument-format","analyzedSha":"2602b642d92234c710ffbe96bfb33007d481ceab","analyzedAt":"2026-09-15T22:58:14.805Z","contentChangedAt":"2026-09-15T22:58:14.805Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}