{"record":{"id":"3e29a392b39af444","repo":"ruvnet/ruflo","slug":"pubkey-must-be-64-hex","errorCode":null,"errorMessage":"pubkey must be 64 hex","messagePattern":"pubkey must be 64 hex","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"v3/@claude-flow/cli/src/mcp-tools/x-federation-channels.ts","lineNumber":135,"sourceCode":"      if (visibility === 'public') return { channel: publicChannelId(name), visibility, note: 'Any relay member can read this channel.' };\n      const key = newChannelKey(); const channel = privateChannelId(key);\n      const store = readStore(); store[channel] = { key, name, at: new Date().toISOString() }; writeStore(store);\n      return { channel, visibility, name, keyStoredAt: STORE_FILE(),\n        note: 'The key never leaves this machine. Grant others with x_federation_channel_grant. There is no recovery if the key file is lost, and no revocation — removing someone means rotating to a new channel.' };\n    },\n  },\n  {\n    name: 'x_federation_channel_grant',\n    description: \"Grant a member access to a private channel by sealing its key to their pubkey with NIP-44 (ECDH), published as a ChannelGrant event only they can open. Use when adding a participant to an existing private channel. Publishing the raw key into a channel or a chat is wrong: it is a bearer secret, and anyone who sees it can read every past and future message, because there is no revocation.\",\n    inputSchema: { type: 'object', properties: {\n      channel: { type: 'string', description: 'Private channel id (prv:<16 hex>) you hold the key for.' },\n      pubkey: { type: 'string', description: \"The member's 64-hex Nostr pubkey.\" },\n      relayWs: { type: 'string', description: 'Relay URL; takes precedence over RUFLO_X_RELAY_WS (default wss://relay.ruv.io).' },\n    }, required: ['channel', 'pubkey'] },\n    handler: async (input) => {\n      const i = input as { channel: string; pubkey: string; relayWs?: string };\n      if (!isPrivateChannel(i.channel)) throw new Error('only private channels have keys to grant');\n      if (!/^[0-9a-f]{64}$/i.test(i.pubkey)) throw new Error('pubkey must be 64 hex');\n      const t = await loadTools(); if (!t) return degraded();\n      const entry = readStore()[i.channel];\n      if (!entry) throw new Error(`no key held for ${i.channel} — create it or accept a grant first`);\n      const { sk, pubkey } = loadOrCreateKey(t.nt as never, KEY_FILE());\n      const conv = t.nip44.v2.utils.getConversationKey(sk, i.pubkey);\n      const sealed = t.nip44.v2.encrypt(entry.key, conv);\n      const relay = RELAY_WS(i.relayWs);\n      const eventId = await relayCall(relay, sk, t.nt, (ws) => publishEvent(ws, t.nt, sk,\n        [['t', 'ruflo-swarm'], ['k', 'ChannelGrant'], ['c', i.channel], ['p', i.pubkey]],\n        JSON.stringify({ type: 'ChannelGrant', channel: i.channel, sealed, ts: new Date().toISOString() })));\n      return { ok: true, channel: i.channel, grantedTo: i.pubkey, grantedBy: pubkey, eventId,\n        note: 'Only that pubkey can open the seal. Grants are not revocable — rotate the channel to remove someone.' };\n    },\n  },\n  {\n    name: 'x_federation_channel_accept',\n    description: 'Accept private-channel grants addressed to your key: finds ChannelGrant events tagged to your pubkey, opens each with your own secret key, and caches the channel keys locally. Use when someone tells you they granted you a channel. Asking them to send you the key directly is wrong because it exposes a bearer secret in a channel you do not control.',\n    inputSchema: { type: 'object', properties: {","sourceCodeStart":117,"sourceCodeEnd":153,"githubUrl":"https://github.com/ruvnet/ruflo/blob/2602b642d92234c710ffbe96bfb33007d481ceab/v3/@claude-flow/cli/src/mcp-tools/x-federation-channels.ts#L117-L153","documentation":"The grant-key handler validates the member's Nostr pubkey against /^[0-9a-f]{64}$/i before deriving the NIP-44 conversation key; Nostr pubkeys are 32-byte x-only keys serialized as 64 hex characters. It throws when the pubkey is malformed, the wrong length, or in another encoding (bech32/nprofile, base64).","triggerScenarios":"Calling the grant-key tool with a pubkey that is shorter/longer than 64 hex chars, contains non-hex characters, has an npub/nprofile prefix, includes whitespace, or is empty.","commonSituations":"Pasting an npub1... bech32 address instead of the raw hex pubkey; truncating the pubkey during copy-paste; uppercase-hex is fine (i flag) but adding a 0x prefix is not; confusing the member pubkey with the local identity key.","solutions":["Use the member's raw 64-char hex pubkey (x-only, no prefix); convert npub1... to hex with a bech32 decoder (e.g. nip19 decode) first","Trim whitespace and strip 0x prefixes; verify with /^[0-9a-f]{64}$/i before calling","If only an npub is available, decode it: nip19.npubDecode(npub) -> hex pubkey"],"exampleFix":"// before\ngrant({ channel: 'prv:1a2b...', pubkey: 'npub1sg6plzptd64u6a8aa...' }); // throws\n// after\nconst hex = nip19.npubDecode('npub1sg6plzptd64u6a8aa...'); // 64 hex chars\ngrant({ channel: 'prv:1a2b...', pubkey: hex });","handlingStrategy":"validation","validationCode":"function isValidNostrPubkey(pk: string): boolean {\n  return /^[0-9a-f]{64}$/i.test(String(pk).trim());\n}","typeGuard":"function isHexPubkey(x: unknown): x is string {\n  return typeof x === 'string' && /^[0-9a-f]{64}$/i.test(x);\n}","tryCatchPattern":"try {\n  await grantKey({ channel, pubkey });\n} catch (e) {\n  if (e.message === 'pubkey must be 64 hex') {\n    const hex = npubToHex(pubkey); // decode bech32 npub if needed\n    if (isHexPubkey(hex)) return grantKey({ channel, pubkey: hex.trim() });\n  }\n  throw e;\n}","preventionTips":["Keep member pubkeys as raw 64-char hex everywhere; decode npub/nprofile at the boundary","Trim whitespace on copy-paste; reject 0x prefixes","Validate pubkeys when adding members to your roster, not at grant time","Use a nip19 decoder utility to convert npub1... addresses to hex"],"tags":["validation","nostr","hex","input"],"backgroundTag":"invalid-identifier-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"}