{"record":{"id":"09ee74792990d896","repo":"koala73/worldmonitor","slug":"api-key-required","errorCode":null,"errorMessage":"API key required","messagePattern":"API key required","errorType":"http","errorClass":"ApiError","httpStatus":401,"severity":"error","filePath":"server/worldmonitor/shipping/v2/list-webhooks.ts","lineNumber":35,"sourceCode":"  ownerIndexKey,\n  callerFingerprint,\n  type WebhookRecord,\n} from './webhook-shared';\n\nexport async function listWebhooks(\n  ctx: ServerContext,\n  _req: ListWebhooksRequest,\n): Promise<ListWebhooksResponse> {\n  // Without forceKey, Clerk-authenticated pro callers reach this handler with\n  // no API key, callerFingerprint() returns the 'anon' fallback, and the\n  // ownerTag !== ownerHash defense-in-depth below collapses because both\n  // sides equal 'anon' — exposing every 'anon'-bucket tenant's webhooks to\n  // every Clerk-session holder. See registerWebhook for full rationale.\n  const apiKeyResult = (await validateApiKey(ctx.request, { forceKey: true })) as {\n    valid: boolean; required: boolean; error?: string; credential?: string;\n  };\n  if (apiKeyResult.required && !apiKeyResult.valid) {\n    throw new ApiError(401, apiKeyResult.error ?? 'API key required', '');\n  }\n\n  await requirePremiumRpcAccess(ctx.request, ApiError, 'PRO subscription required');\n\n  const ownerHash = await callerFingerprint(ctx.request, apiKeyResult.credential);\n  const smembersResult = await runRedisPipeline([['SMEMBERS', ownerIndexKey(ownerHash)]]);\n  const memberIds = (smembersResult[0]?.result as string[] | null) ?? [];\n\n  if (memberIds.length === 0) {\n    return { webhooks: [] };\n  }\n\n  const getResults = await runRedisPipeline(memberIds.map(id => ['GET', webhookKey(id)]));\n  const webhooks: WebhookSummary[] = [];\n  for (const r of getResults) {\n    if (!r.result || typeof r.result !== 'string') continue;\n    try {\n      const record = JSON.parse(r.result) as WebhookRecord;","sourceCodeStart":17,"sourceCodeEnd":53,"githubUrl":"https://github.com/koala73/worldmonitor/blob/eeab0a219fce0f02a00603b532dbae9041b934ac/server/worldmonitor/shipping/v2/list-webhooks.ts#L17-L53","documentation":"listWebhooks calls validateApiKey with forceKey: true, so an explicit shipping v2 API key is mandatory even for Clerk-authenticated PRO callers. Without a key, callerFingerprint() would collapse to the shared 'anon' bucket and the ownerTag !== ownerHash defense would degenerate (both sides 'anon'), exposing every anon-bucket tenant's webhooks — hence the up-front 401.","triggerScenarios":"Calling ListWebhooks with only a Clerk session and no X-WorldMonitor-Key header; sending an invalid, revoked, or expired API key; a server-side script that never loaded the key. The 401 fires before requirePremiumRpcAccess and before the SMEMBERS owner-index read.","commonSituations":"Integration reusing the browser session token instead of the issued API key; key rotated or revoked server-side; local dev .env missing the key; curl testing without the header; assuming PRO subscription alone authorizes webhook RPCs.","solutions":["Send the shipping v2 API key header (X-WorldMonitor-Key, per docs/api-shipping-v2.mdx) on every ListWebhooks and RegisterWebhook call","Confirm the key is active — re-issue from the dashboard if revoked or expired","Keep sending the key even when already Clerk-authenticated; the session alone is intentionally insufficient for this handler"],"exampleFix":"// before\nawait shippingClient.listWebhooks({}); // 401: API key required\n// after\nawait shippingClient.listWebhooks({}, {\n  headers: { 'X-WorldMonitor-Key': process.env.WORLDMONITOR_API_KEY! },\n});","handlingStrategy":"validation","validationCode":"if (!process.env.WORLDMONITOR_API_KEY) throw new Error('X-WorldMonitor-Key missing — issue and configure a shipping v2 key before calling listWebhooks');","typeGuard":null,"tryCatchPattern":"catch (e) { if (e?.status === 401) { halt and prompt for API key provisioning; do not retry with the same credentials } else throw e; }","preventionTips":["Wrap all shipping v2 RPC calls in a client that always attaches X-WorldMonitor-Key","Fail fast at startup when the key env var is absent","Never assume a Clerk session authorizes webhook RPCs — the key is mandatory by design"],"tags":["authentication","api-key","http-401","webhooks","authorization","shipping"],"backgroundTag":"missing-api-key","analyzedSha":"eeab0a219fce0f02a00603b532dbae9041b934ac","analyzedAt":"2026-08-21T16:51:25.751Z","contentChangedAt":"2026-08-21T16:51:25.751Z","schemaVersion":2},"datasetVersion":"2026-09-08T20:17:18.057Z"}