{"record":{"id":"0aa1d03967cbf4bb","repo":"ruvnet/ruflo","slug":"channel-must-be-pub-name-or-prv-16-hex","errorCode":null,"errorMessage":"channel must be pub:<name> or prv:<16 hex>","messagePattern":"channel must be pub:<name> or prv:<16 hex>","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"v3/@claude-flow/cli/src/mcp-tools/x-federation-channels.ts","lineNumber":195,"sourceCode":"          accepted.push(body.channel);\n        } catch { failed.push(body.channel); }\n      }\n      if (accepted.length) writeStore(store);\n      return { pubkey, accepted: [...new Set(accepted)], unopenable: [...new Set(failed)], keyStoredAt: STORE_FILE() };\n    },\n  },\n  {\n    name: 'x_federation_channel_publish',\n    description: 'Publish a message to a channel with YOUR OWN key. A private channel is encrypted locally under its channel key before it leaves this machine, and the message type is hidden behind k=enc so the relay sees only an opaque id and ciphertext. Use when the message should be attributable to you. The admin-gated gateway channel_publish is wrong for that, because it signs as the gateway and cannot reach private channels at all.',\n    inputSchema: { type: 'object', properties: {\n      channel: { type: 'string', description: 'Channel id (pub:<name> or prv:<16 hex>).' },\n      msgType: { type: 'string', description: 'Message type (Status, Task, Result, …). Hidden on private channels.' },\n      payload: { type: 'object', description: 'JSON body. Never put secrets or credentials in it, even on a private channel.' },\n      relayWs: { type: 'string', description: 'Relay URL; takes precedence over RUFLO_X_RELAY_WS.' },\n    }, required: ['channel', 'msgType', 'payload'] },\n    handler: async (input) => {\n      const i = input as { channel: string; msgType: string; payload: Record<string, unknown>; relayWs?: string };\n      if (!CHANNEL_ID_RE.test(i.channel)) throw new Error('channel must be pub:<name> or prv:<16 hex>');\n      const t = await loadTools(); if (!t) return degraded();\n      const { sk, pubkey } = loadOrCreateKey(t.nt as never, KEY_FILE());\n      const priv = isPrivateChannel(i.channel);\n      const body = { type: i.msgType, from: pubkey, ts: new Date().toISOString(), ...i.payload };\n      let content: string;\n      if (priv) {\n        const entry = readStore()[i.channel];\n        if (!entry) throw new Error(`no key held for ${i.channel} — accept a grant first (x_federation_channel_accept)`);\n        content = t.nip44.v2.encrypt(JSON.stringify(body), Uint8Array.from(Buffer.from(entry.key, 'hex')));\n      } else { content = JSON.stringify(body); }\n      const tags = [['t', 'ruflo-swarm'], ['c', i.channel], ['k', priv ? 'enc' : i.msgType]];\n      const eventId = await relayCall(RELAY_WS(i.relayWs), sk, t.nt, (ws) => publishEvent(ws, t.nt, sk, tags, content));\n      return { ok: true, channel: i.channel, visibility: priv ? 'private' : 'public', encrypted: priv, eventId, pubkey };\n    },\n  },\n  {\n    name: 'x_federation_channel_read',\n    description: 'Read a channel and decrypt what your keys can open. Public messages come back as JSON; private ones are decrypted locally with the cached channel key, and anything you have no key for is returned as encrypted:true rather than silently dropped. Use when the channel is private: reading it through the gateway channel_sync tool is wrong there, because the gateway holds no key and can only hand you ciphertext.',","sourceCodeStart":177,"sourceCodeEnd":213,"githubUrl":"https://github.com/ruvnet/ruflo/blob/2602b642d92234c710ffbe96bfb33007d481ceab/v3/@claude-flow/cli/src/mcp-tools/x-federation-channels.ts#L177-L213","documentation":"The federation publish tool validates the channel identifier against CHANNEL_ID_RE, which only accepts public channels of the form pub:<name> or private channels of the form prv:<16 hex chars>. Any other string (bare name, wrong prefix, wrong length/charset for private) is rejected before any network work.","triggerScenarios":"Passing a channel like 'general' (no prefix), 'private:abc' (wrong prefix), 'prv:xyz' (fewer than 16 hex chars), or 'prv:zzzz...' (non-hex characters) to the publish tool.","commonSituations":"Users echoing a channel name from logs without the pub:/prv: prefix; hand-crafting a private channel id that is not exactly 16 hex characters; confusing this CLI's channel format with another product's channel naming.","solutions":["Format public channels as pub:<name>, e.g. pub:general","Format private channels as exactly prv: plus 16 lowercase/uppercase hex characters, copying the id returned when the channel was created","Copy channel ids directly from the create/grant tool output instead of typing them"],"exampleFix":"// before\nawait publish({ channel: 'general', msgType: 'Status', payload: {} });\n// after\nawait publish({ channel: 'pub:general', msgType: 'Status', payload: {} });","handlingStrategy":"validation","validationCode":"const CHANNEL_ID_RE = /^(pub:[^\\s]+|prv:[0-9a-fA-F]{16})$/;\nif (!CHANNEL_ID_RE.test(channel)) throw new Error(`bad channel id: ${channel}`);","typeGuard":"const isValidChannelId = (ch: unknown): ch is string =>\n  typeof ch === 'string' && /^(pub:[^\\s]+|prv:[0-9a-fA-F]{16})$/.test(ch);","tryCatchPattern":"try {\n  await publish({ channel, msgType, payload });\n} catch (e) {\n  if (String(e.message).startsWith('channel must be pub:')) {\n    console.error(`Channel '${channel}' is malformed; expected pub:<name> or prv:<16 hex>`);\n  } else throw e;\n}","preventionTips":["Always include the pub:/prv: prefix on channel ids","Copy private channel ids (16 hex) from tool output instead of typing them","Validate channel strings at config load time before making calls"],"tags":["validation","federation","format"],"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"}