{"record":{"id":"29c7af3dd7407a0f","repo":"ruvnet/ruflo","slug":"only-private-channels-have-keys-to-grant","errorCode":null,"errorMessage":"only private channels have keys to grant","messagePattern":"only private channels have keys to grant","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"v3/@claude-flow/cli/src/mcp-tools/x-federation-channels.ts","lineNumber":134,"sourceCode":"      const { name, visibility } = input as { name: string; visibility: 'public' | 'private' };\n      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.',","sourceCodeStart":116,"sourceCodeEnd":152,"githubUrl":"https://github.com/ruvnet/ruflo/blob/2602b642d92234c710ffbe96bfb33007d481ceab/v3/@claude-flow/cli/src/mcp-tools/x-federation-channels.ts#L116-L152","documentation":"The x-federation grant-key handler only operates on private channels: keys are distributed encrypted via NIP-44 to members, and public (pub:) channels have no key to grant. It throws when the supplied channel id does not start with 'prv:'. This is an operation-applicability check, not a data-corruption error.","triggerScenarios":"Calling the grant-key MCP tool with channel='pub:<name>', a bare channel name like 'general', or any id lacking the prv: prefix; mixing up a public channel id with a private one when scripting tool calls.","commonSituations":"Passing the friendly channel name instead of the prv:<16-hex> id returned at creation; attempting to add members to a public channel (just post to it instead); copying the wrong channel id from the store.","solutions":["Pass the private channel id exactly as returned when the channel was created (prv:<16 hex>)","If the channel is public (pub: prefix), do not grant keys — public channels are open; post/join directly","Look up the correct prv: id in the channel store before calling the tool"],"exampleFix":"// before\ngrant({ channel: 'general', pubkey: 'abc...' }); // throws\n// after\ngrant({ channel: 'prv:1a2b3c4d5e6f7a8b', pubkey: 'abc...' });","handlingStrategy":"validation","validationCode":"function isPrivateChannel(id: string): boolean { return String(id).startsWith('prv:'); }\nif (!isPrivateChannel(channelId)) throw new Error('grant-key requires a prv: channel id');","typeGuard":"function isPrivateChannelId(x: unknown): x is `prv:${string}` {\n  return typeof x === 'string' && x.startsWith('prv:') && /^prv:[0-9a-f]{16}$/.test(x);\n}","tryCatchPattern":"try {\n  await grantKey({ channel, pubkey });\n} catch (e) {\n  if (e.message === 'only private channels have keys to grant') {\n    // public channels need no key grant — join/post directly, or use the correct prv: id\n    return { skipped: true, reason: 'public channel' };\n  }\n  throw e;\n}","preventionTips":["Store the prv:<16-hex> id returned at channel creation and reuse it verbatim","Check the prefix (pub: vs prv:) before choosing grant vs post/join operations","Never pass bare channel names to tool calls that expect channel ids"],"tags":["validation","channels","precondition"],"backgroundTag":"unsupported-operation","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"}