{"record":{"id":"9a54453b7ac7c885","repo":"koala73/worldmonitor","slug":"api-key-required-9a5445","errorCode":null,"errorMessage":"API key required","messagePattern":"API key required","errorType":"http","errorClass":"ApiError","httpStatus":401,"severity":"error","filePath":"server/worldmonitor/shipping/v2/register-webhook.ts","lineNumber":45,"sourceCode":"} from './webhook-shared';\n\nexport async function registerWebhook(\n  ctx: ServerContext,\n  req: RegisterWebhookRequest,\n): Promise<RegisterWebhookResponse> {\n  // Webhooks are per-tenant keyed on callerFingerprint(), which hashes the\n  // API key. Without forceKey, a Clerk-authenticated pro caller reaches this\n  // handler with no API key, callerFingerprint() falls back to 'anon', and\n  // every such caller collapses into a shared 'anon' owner bucket — letting\n  // one Clerk-session holder enumerate/overwrite other tenants' webhooks.\n  // Matches the legacy `api/v2/shipping/webhooks/[subscriberId]{,/[action]}.ts`\n  // gate and the documented \"X-WorldMonitor-Key required\" contract in\n  // docs/api-shipping-v2.mdx.\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 callbackUrl = (req.callbackUrl ?? '').trim();\n  if (!callbackUrl) {\n    throw new ValidationError([{ field: 'callbackUrl', description: 'callbackUrl is required' }]);\n  }\n\n  try {\n    await assertCallbackUrlRegistrationSafe(callbackUrl);\n  } catch (error) {\n    const message = error instanceof Error ? error.message : 'callbackUrl is not allowed';\n    throw new ValidationError([{ field: 'callbackUrl', description: message }]);\n  }\n\n  const chokepointIds = Array.isArray(req.chokepointIds) ? req.chokepointIds : [];\n  const invalidCp = chokepointIds.find(id => !VALID_CHOKEPOINT_IDS.has(id));","sourceCodeStart":27,"sourceCodeEnd":63,"githubUrl":"https://github.com/koala73/worldmonitor/blob/eeab0a219fce0f02a00603b532dbae9041b934ac/server/worldmonitor/shipping/v2/register-webhook.ts#L27-L63","documentation":"registerWebhook requires an explicit API key via validateApiKey({ forceKey: true }), matching the legacy api/v2/shipping/webhooks/[subscriberId] gate and the documented X-WorldMonitor-Key contract. The key is required because webhook ownership is keyed on callerFingerprint() (a hash of the API key); a keyless Clerk pro caller would fall into the shared 'anon' bucket and could enumerate or overwrite other tenants' webhooks, so the handler rejects with 401 first.","triggerScenarios":"POST RegisterWebhook without the X-WorldMonitor-Key header while relying on a Clerk session; invalid/expired/revoked key; automated partner client that never had the key configured. The check runs before premium gating, callbackUrl validation, and the SSRF safety check.","commonSituations":"Partner onboarding that skipped key issuance; key rotated and the old one cached in the client; env var for the key missing in CI; frontend code accidentally calling the RPC with session cookies only.","solutions":["Obtain a shipping v2 API key and send it as the X-WorldMonitor-Key header on RegisterWebhook calls","Validate the key is current and active; re-issue if rotated or revoked","Ensure server-to-server callers load the key from configuration at startup, not per-request from an optional env"],"exampleFix":"// before\nawait shippingClient.registerWebhook({ callbackUrl, chokepointIds }); // 401\n// after\nawait shippingClient.registerWebhook(\n  { callbackUrl, chokepointIds },\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 — required by registerWebhook even for Clerk-authenticated callers');","typeGuard":null,"tryCatchPattern":"catch (e) { if (e?.status === 401) { surface a configuration error for the API key; stop retrying } else throw e; }","preventionTips":["Attach X-WorldMonitor-Key on every registerWebhook/listWebhooks call via a shared client wrapper","Store the key in a secret manager and validate presence at boot","Key rotation: update clients atomically before revoking the old key"],"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"}