{"record":{"id":"021de9f2b08aca25","repo":"ruvnet/ruflo","slug":"channel-name-must-match-a-z0-9-a-z0-9-0-63","errorCode":null,"errorMessage":"channel name must match [a-z0-9][a-z0-9._-]{0,63}","messagePattern":"channel name must match \\[a-z0-9\\]\\[a-z0-9\\._-\\](.+?)","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"v3/@claude-flow/cli/src/mcp-tools/x-federation-channels.ts","lineNumber":54,"sourceCode":"    const nt = (await import('nostr-tools/pure')) as unknown as Nt;\n    const { nip44 } = (await import('nostr-tools')) as unknown as { nip44: Nip44 };\n    return { nt, nip44 };\n  } catch { return null; }\n}\nconst 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') {","sourceCodeStart":36,"sourceCodeEnd":72,"githubUrl":"https://github.com/ruvnet/ruflo/blob/2602b642d92234c710ffbe96bfb33007d481ceab/v3/@claude-flow/cli/src/mcp-tools/x-federation-channels.ts#L36-L72","documentation":"publicChannelId validates a federation channel name against CHANNEL_NAME_RE (lowercase alphanumeric start, then [a-z0-9._-], max 64 chars) and throws when the name does not match. Channel ids become pub:<name>, so the regex keeps store keys, relay topics, and URLs safe and canonical. This is a strict input-validation guard, not an environmental failure.","triggerScenarios":"Calling any x-federation channel tool (create/join public channel) with a name containing uppercase letters, spaces, slashes, or other symbols; empty name; name longer than 64 characters; name starting with '.', '_' or '-' (must start [a-z0-9]).","commonSituations":"Typing a human-readable channel name like 'Team Updates' or 'My_Channel' instead of kebab-case; passing a full channel id ('pub:general') instead of the bare name; auto-generated names with timestamps containing ':' or '/'; whitespace from copy-paste.","solutions":["Rename the channel to lowercase kebab-case: lowercase letters/digits start, then only [a-z0-9._-], max 64 chars","Trim whitespace and strip any 'pub:'/'prv:' prefix before passing the name","If the desired name has uppercase or symbols, slugify it (e.g. 'Team Updates' -> 'team-updates')"],"exampleFix":"// before\npublicChannelId('Team Updates'); // throws\n// after\npublicChannelId('team-updates');\n// or slugify first\nconst safe = name.trim().toLowerCase().replace(/[^a-z0-9._-]+/g, '-').replace(/^[._-]+/, '').slice(0, 64);","handlingStrategy":"validation","validationCode":"const CHANNEL_NAME_RE = /^[a-z0-9][a-z0-9._-]{0,63}$/;\nfunction isValidChannelName(n: string): boolean { return CHANNEL_NAME_RE.test(n.trim()); }","typeGuard":null,"tryCatchPattern":"try {\n  const id = publicChannelId(name);\n} catch {\n  const slug = name.trim().toLowerCase().replace(/[^a-z0-9._-]+/g, '-').replace(/^[._-]+/, '').slice(0, 64);\n  if (!CHANNEL_NAME_RE.test(slug)) throw new Error(`cannot slugify channel name: ${name}`);\n  const id = publicChannelId(slug);\n}","preventionTips":["Always slugify user-supplied channel names to lowercase kebab-case before creating","Strip pub:/prv: prefixes and trim whitespace before validation","Keep names under 64 characters and starting with a letter or digit","Validate names in UI/CLI input layers with the same regex"],"tags":["validation","input","naming"],"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"}