{"record":{"id":"02b5fa0c01abe70b","repo":"ruvnet/ruflo","slug":"invite-code-must-look-like-v2-token","errorCode":null,"errorMessage":"invite code must look like v2.<token>","messagePattern":"invite code must look like v2\\.<token>","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"v3/@claude-flow/cli/src/mcp-tools/x-federation-join.ts","lineNumber":68,"sourceCode":"    setTimeout(() => fin({ ok: false, reason: 'timeout' }), 15000);\n  });\n}\n\nexport const xFederationJoinTools: MCPTool[] = [{\n  name: 'x_federation_join',\n  description:\n    'Join the open swarm federation with YOUR OWN key using an invite code: generates (or reuses) a local Nostr key at ~/.ruflo/nostr.key (0600), redeems the code with a NIP-98-signed claim directly against the relay, proves membership via NIP-42, and returns your pubkey. Use when you have been handed an invite code and want to participate as yourself. Asking an admin to `federation_admit` you instead is wrong for an open swarm because it centralizes onboarding and requires trusting a pubkey out of band; the invite claim binds membership to the key you hold. Never share the invite code publicly — it is a bearer secret.',\n  inputSchema: { type: 'object', properties: {\n    code: { type: 'string', description: 'Invite code (v2.…) received privately from a member/admin.' },\n    relayHttp: { type: 'string', description: 'Relay HTTPS base for the claim; takes precedence over RUFLO_X_RELAY_HTTP.' },\n    relayWs: { type: 'string', description: 'Relay wss URL for NIP-42; takes precedence over RUFLO_X_RELAY_WS.' },\n    keyFile: { type: 'string', description: 'Key file path; takes precedence over RUFLO_NOSTR_KEY_FILE (default ~/.ruflo/nostr.key).' } }, required: ['code'] },\n  handler: async (input) => {\n    const i = input as { code: string; relayHttp?: string; relayWs?: string; keyFile?: string };\n    const nt = await loadNostrTools();\n    // Validate input before the optional-dependency check so a bad code fails fast and identically\n    // whether or not nostr-tools is present.\n    if (!/^v2\\.[A-Za-z0-9._-]{8,}$/.test(i.code)) throw new Error('invite code must look like v2.<token>');\n    if (!nt) return { degraded: true, reason: 'nostr-tools not installed', hint: 'npm i -g nostr-tools  (secp256k1 signing is not in node:crypto)' };\n    const { sk, pubkey, created } = loadOrCreateKey(nt, i.keyFile);\n    const url = `${HTTP_BASE(i.relayHttp)}/api/invites/claim`; const body = JSON.stringify({ code: i.code });\n    const r = await fetch(url, { method: 'POST', headers: { Authorization: nip98Header(nt, sk, url, 'POST', body), 'Content-Type': 'application/json' }, body, signal: AbortSignal.timeout(20_000) });\n    const claim = (await r.json().catch(() => ({}))) as { role?: string; error?: string; message?: string };\n    if (!r.ok) throw new Error(`claim rejected (${r.status}): ${claim.error ?? claim.message ?? 'unknown'}`);\n    const auth = await verifyMembership(nt, sk, RELAY_WS(i.relayWs));\n    return { ok: auth.ok, pubkey, keyCreated: created, role: claim.role ?? 'member', membershipVerified: auth.ok, ...(auth.ok ? {} : { reason: auth.reason }),\n      next: 'Publish kind-1 events tagged [\"t\",\"ruflo-swarm\"] — or run `ruflo federation sync` to read the swarm.' };\n  },\n}];\n","sourceCodeStart":50,"sourceCodeEnd":80,"githubUrl":"https://github.com/ruvnet/ruflo/blob/2602b642d92234c710ffbe96bfb33007d481ceab/v3/@claude-flow/cli/src/mcp-tools/x-federation-join.ts#L50-L80","documentation":"The federation join tool validates invite codes against /^v2\\.[A-Za-z0-9._-]{8,}$/ before doing anything else — deliberately even before the nostr-tools availability check, so a malformed code fails fast and identically regardless of environment. Codes must start with 'v2.' followed by at least 8 allowed characters.","triggerScenarios":"Calling the join tool with a code missing the v2. prefix, an empty/short token (fewer than 8 chars), or a token containing characters outside [A-Za-z0-9._-] (e.g. spaces, '+', '/', URL-encoded characters pasted from a browser).","commonSituations":"Copying only part of an invite code from chat/email; pasting a URL-wrapped code including percent-encoding; using an old v1-format invite against a tool that only accepts v2.","solutions":["Copy the full invite code exactly as issued, including the leading 'v2.' prefix and everything after it","Strip any wrapping (quotes, trailing whitespace, URL-encoding) and ensure the token is at least 8 characters of letters/digits/dots/underscores/hyphens","Request a fresh v2 invite code from the swarm administrator if only an old-format code is available"],"exampleFix":"// before\nawait join({ code: 'abc123' });\n// throws: invite code must look like v2.<token>\n// after\nawait join({ code: 'v2.a1B2c3D4e5F6g7H8' });","handlingStrategy":"validation","validationCode":"const INVITE_RE = /^v2\\.[A-Za-z0-9._-]{8,}$/;\nif (!INVITE_RE.test(code)) throw new Error(`malformed invite code: ${code.slice(0, 6)}...`);","typeGuard":"const isValidInviteCode = (code: unknown): code is string =>\n  typeof code === 'string' && /^v2\\.[A-Za-z0-9._-]{8,}$/.test(code);","tryCatchPattern":"try {\n  await join({ code });\n} catch (e) {\n  if (String(e.message).includes('invite code must look like')) {\n    console.error('Check the invite code: must be v2. followed by >=8 chars of [A-Za-z0-9._-]');\n  } else throw e;\n}","preventionTips":["Copy invite codes in full, including the v2. prefix","Avoid pasting codes through channels that add quoting or URL-encoding (strip %XX escapes first)","Treat v1-style codes as obsolete; request a v2 invite from the admin"],"tags":["validation","format","invite-code"],"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"}