{"record":{"id":"b71c7dc0b6ec0a33","repo":"ruvnet/ruflo","slug":"claim-rejected-r-status-claim-error-claim-message-unknown","errorCode":null,"errorMessage":"claim rejected (${r.status}): ${claim.error ?? claim.message ?? 'unknown'}","messagePattern":"claim rejected \\((.+?)\\): (.+?)","errorType":"http","errorClass":null,"httpStatus":null,"severity":"error","filePath":"v3/@claude-flow/cli/src/mcp-tools/x-federation-join.ts","lineNumber":74,"sourceCode":"  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":56,"sourceCodeEnd":80,"githubUrl":"https://github.com/ruvnet/ruflo/blob/2602b642d92234c710ffbe96bfb33007d481ceab/v3/@claude-flow/cli/src/mcp-tools/x-federation-join.ts#L56-L80","documentation":"Thrown when the federation relay's invite-claim endpoint (`POST /api/invites/claim`) responds with a non-2xx status. The join tool parses the JSON body for an `error` or `message` field and surfaces it with the HTTP status, since the relay rejected the invite redemption. It means the invite was refused server-side — e.g. the code is invalid, expired, already fully redeemed, or the NIP-98 auth did not verify.","triggerScenarios":"Calling `x_federation_join` (or `ruflo federation join`) with an invite code the relay will not accept: an unknown/revoked code (404/403), an expired or use-exhausted code (409/410), a code already claimed by a different pubkey, or a NIP-98 Authorization header that fails signature/timestamp validation (401). The network request itself succeeded; the rejection is in the response status.","commonSituations":"Typing or truncating the invite code when copying it; joining with an invite whose 7-day TTL or maxUses (default 25) has lapsed; a replay attempt after the code was already redeemed; a system clock skewed far enough that the NIP-98 timestamp is rejected; the relay operator revoking the invite; hitting a different relay than the one that minted the code.","solutions":["Read the embedded relay error (e.g. 'invite expired', 'max uses exceeded') and act on it: request a fresh invite via `x_federation_invite_mint` from an operator.","Verify the invite code matches /^v2\\.[A-Za-z0-9._-]{8,}$/ and was copied in full (the client regex check catches format issues, but only the relay catches unknown/revoked codes).","Check system clock skew (NTP) if the status is 401, since NIP-98 auth windows reject badly skewed timestamps.","Confirm you are pointing at the relay that actually minted the code (relayHttp/relayWs options), not a different federation hub.","If the code was redeemed by the wrong key, have the operator mint a new single-use invite and retry with the intended key file."],"exampleFix":"// before: retrying an exhausted invite blindly\nawait xFederationJoin({ code: oldCode });\n// after: mint/obtain a fresh invite and validate format first\nif (!/^v2\\.[A-Za-z0-9._-]{8,}$/.test(newCode)) throw new Error('malformed invite code');\nawait xFederationJoin({ code: newCode });","handlingStrategy":"validation","validationCode":"const code = invite.code;\nif (typeof code !== 'string' || !/^v2\\.[A-Za-z0-9._-]{8,}$/.test(code)) {\n  throw new Error('invite code must look like v2.<token>');\n}\nif (invite.expiresAt && Date.now() > new Date(invite.expiresAt).getTime()) {\n  throw new Error('invite already expired locally; request a new one');\n}","typeGuard":"function isValidInviteCode(c: unknown): c is string {\n  return typeof c === 'string' && /^v2\\.[A-Za-z0-9._-]{8,}$/.test(c);\n}","tryCatchPattern":"try {\n  await xFederationJoin({ code });\n} catch (e) {\n  if (e instanceof Error && e.message.startsWith('claim rejected')) {\n    const status = e.message.match(/claim rejected \\((\\d+)\\)/)?.[1];\n    if (status === '401') console.error('NIP-98 auth failed — check clock skew');\n    else console.error('invite refused by relay, request a fresh invite:', e.message);\n  } else throw e;\n}","preventionTips":["Copy invite codes verbatim and verify the v2.<token> format before joining.","Track invite TTL (default 7 days) and maxUses (default 25); mint fresh invites for large onboardings.","Keep system clocks NTP-synced so NIP-98 auth windows validate.","Join against the same relay that minted the code.","Treat each code as single-claimant; do not share codes across multiple keys."],"tags":["http-error-response","federation","invite-claim","nostr"],"backgroundTag":"http-error-response","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"}