{"record":{"id":"50b5941d4ff765b4","repo":"ruvnet/ruflo","slug":"no-key-held-for-i-channel-create-it-or-accept-a-grant-first","errorCode":null,"errorMessage":"no key held for ${i.channel} — create it or accept a grant first","messagePattern":"no key held for (.+?) — create it or accept a grant first","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"v3/@claude-flow/cli/src/mcp-tools/x-federation-channels.ts","lineNumber":138,"sourceCode":"      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: {\n      sinceSeconds: { type: 'number', description: 'Look-back window (default 7 days).' },\n      relayWs: { type: 'string', description: 'Relay URL; takes precedence over RUFLO_X_RELAY_WS.' },\n    }, required: [] },","sourceCodeStart":120,"sourceCodeEnd":156,"githubUrl":"https://github.com/ruvnet/ruflo/blob/2602b642d92234c710ffbe96bfb33007d481ceab/v3/@claude-flow/cli/src/mcp-tools/x-federation-channels.ts#L120-L156","documentation":"The federation channel-grant tool throws this when asked to publish a grant for a private channel whose decryption key is not present in the local key store. Grants are sealed with the channel key that must have been created locally (via channel create) or received via an earlier accept. Without a stored key entry there is nothing to encrypt, so the handler fails fast instead of publishing garbage.","triggerScenarios":"Calling the grant tool with a channel name that exists nowhere in the local store: the channel was never created on this node, the key file/store was deleted or points elsewhere, or the channel was created on a different machine.","commonSituations":"Running a fresh install and trying to grant access to a channel whose key lives on another host; wiping ~/.ruflo or changing the store path via env/config and losing local channel keys; typo in the channel name so it doesn't match the stored entry.","solutions":["Create the channel first (the create tool generates and stores its key), then retry the grant","If the key was created elsewhere, accept a grant for the channel on this node first (x_federation_channel_accept)","Verify the channel name spelling exactly matches the one used at creation"],"exampleFix":"// before: granting before creating\nawait grant({ channel: 'prv:abc123', pubkey: otherPubkey });\n// throws: no key held for prv:abc123\n// after: create the channel first\nawait createChannel({ channel: 'prv:abc123' });\nawait grant({ channel: 'prv:abc123', pubkey: otherPubkey });","handlingStrategy":"validation","validationCode":"const HEX64 = /^[0-9a-f]{64}$/i;\nif (!HEX64.test(input.pubkey)) throw new Error('pubkey must be 64 hex');\nconst store = JSON.parse(await fs.readFile(STORE_PATH, 'utf8'));\nif (!store[input.channel]) throw new Error(`create/accept key for ${input.channel} before granting`);","typeGuard":"const hasKey = (store: Record<string, { key: string }>, ch: string): boolean =>\n  Object.prototype.hasOwnProperty.call(store, ch);","tryCatchPattern":"try {\n  await grant({ channel, pubkey });\n} catch (e) {\n  if (String(e.message).includes('no key held')) {\n    await createChannel({ channel });\n    await grant({ channel, pubkey });\n  } else throw e;\n}","preventionTips":["Create every private channel on the node that will issue grants","Keep the key/store file persistent (back up ~/.ruflo) across reinstalls","Use the same store path configuration on create/grant/publish"],"tags":["federation","nostr","state-error"],"backgroundTag":"record-not-found","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"}