{"record":{"id":"d3a74afb70c64ba2","repo":"koala73/worldmonitor","slug":"buildauthheaders-free-tier-context-has-no-credent","errorCode":null,"errorMessage":"buildAuthHeaders: free-tier context has no credentials — a free-tier tool must not call a credentialed downstream","messagePattern":"buildAuthHeaders: free-tier context has no credentials — a free-tier tool must not call a credentialed downstream","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"warning","filePath":"api/mcp/auth.ts","lineNumber":137,"sourceCode":"  url: string,\n  body: BodyInit | null | undefined,\n): Promise<Record<string, string>> {\n  if (context.kind === 'env_key' || context.kind === 'user_key') {\n    // user_key (#4859): the downstream REST gateway validates the raw key\n    // itself (Convex hash lookup + the #4611 apiAccess gate + per-account\n    // limits), so usage attributes to the key owner exactly like a direct\n    // REST call — no internal-HMAC identity smuggling needed.\n    return { 'X-WorldMonitor-Key': context.apiKey };\n  }\n  if (context.kind === 'free') {\n    // U7: a free-tier context has no principal to authenticate as, so there is\n    // nothing honest to sign. Throwing is the fail-closed choice — the\n    // alternative (falling through to the `pro` HMAC branch below) would mint\n    // an internally-trusted signature for an anonymous caller, which is the\n    // one outcome the free tier must never produce. A free-tier tool that\n    // reaches here is misconfigured: it declared `_freeTier` while calling a\n    // credentialed downstream.\n    throw new Error('buildAuthHeaders: free-tier context has no credentials — a free-tier tool must not call a credentialed downstream');\n  }\n  // context.kind === 'pro'\n  const secret = process.env.MCP_INTERNAL_HMAC_SECRET ?? '';\n  if (!secret) {\n    // Should never happen in production (deploy gate at U10) — surface as\n    // an error so the tool fetch fails fast rather than silently 401-ing\n    // at the gateway with a confusing \"invalid_internal_mcp_signature\".\n    throw new Error('MCP_INTERNAL_HMAC_SECRET not configured');\n  }\n  const signed = await signInternalMcpRequest({\n    method,\n    url,\n    body,\n    userId: context.userId,\n    secret,\n  });\n  return buildInternalMcpHeaders(signed);\n}","sourceCodeStart":119,"sourceCodeEnd":155,"githubUrl":"https://github.com/koala73/worldmonitor/blob/a96956387a927b8cd7aa34b0c41fca357e746be9/api/mcp/auth.ts#L119-L155","documentation":"requireSignedInUserId(action) is the billing service's shared guard: it reads getCurrentClerkUser()?.id and throws `Sign in to ${action}.` (e.g., 'Sign in to claim Pro activation.') when no Clerk session exists. The action string names the exact billing operation, so the message tells the developer which gated flow fired. No billing mutation or Convex call is attempted.","triggerScenarios":"Invoking any billing operation routed through requireSignedInUserId (such as claimProActivationPresentation) while signed out, after session expiry, before Clerk loads, or with ClerkProvider misconfigured.","commonSituations":"Billing UI rendered from cached state after logout; expired sessions on long-lived dashboards; dev builds without Clerk env vars.","solutions":["Sign in and retry the billing action","Ensure Clerk is fully loaded before invoking billing flows","Gate billing buttons on auth state so the call never fires signed-out","Check ClerkProvider/publishable key configuration if the user should be signed in"],"exampleFix":"// before\nconst status = await claimProActivationPresentation(activationKey, claimNonce);\n// 'Sign in to claim Pro activation.'\n\n// after\nif (!getCurrentClerkUser()?.id) {\n  openSignIn();\n  return;\n}\nconst status = await claimProActivationPresentation(activationKey, claimNonce);","handlingStrategy":"validation","validationCode":"if (!getCurrentClerkUser()?.id) { openSignIn(); return; }\nconst outcome = await claimProActivationPresentation(activationKey, claimNonce);","typeGuard":"const hasActiveClerkUser = (u: { id: string } | null | undefined): u is { id: string } =>\n  typeof u?.id === 'string' && u.id.length > 0;","tryCatchPattern":"try {\n  await billingOperation();\n} catch (e) {\n  if (e instanceof Error && e.message.startsWith('Sign in to ')) openSignIn();\n  else throw e;\n}","preventionTips":["Route all billing UI through live auth state; the action name in the message identifies the flow","Do not call billing functions during the Clerk loading window","Prefix-matching 'Sign in to ' covers every current and future billing action string"],"tags":["clerk","auth","billing","precondition"],"backgroundTag":"user-not-authenticated","analyzedSha":"a96956387a927b8cd7aa34b0c41fca357e746be9","analyzedAt":"2026-08-21T16:51:25.751Z","contentChangedAt":"2026-08-21T16:51:25.751Z","schemaVersion":2},"datasetVersion":"2026-09-14T05:17:10.506Z"}