{"record":{"id":"954aafebb5d3facb","repo":"koala73/worldmonitor","slug":"invalid-api-key","errorCode":null,"errorMessage":"Invalid API key","messagePattern":"Invalid API key","errorType":"http","errorClass":"ApiError","httpStatus":401,"severity":"error","filePath":"server/worldmonitor/shipping/v2/list-webhooks.ts","lineNumber":41,"sourceCode":"  _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.error === USER_API_KEY_GATEWAY_VALIDATION_ERROR) {\n    const credential = getHeaderApiKey(ctx.request) as string;\n    let userKey;\n    try {\n      userKey = credential ? await validateUserApiKey(credential) : null;\n    } catch {\n      throw new ApiError(503, 'Service temporarily unavailable', '');\n    }\n    if (!userKey) throw new ApiError(401, 'Invalid API key', '');\n    // Revalidate the credential rather than trusting a caller-supplied user ID.\n    apiKeyResult.valid = true;\n    apiKeyResult.credential = credential;\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 records = await readOwnerWebhooks(ownerHash);\n  const webhooks: WebhookSummary[] = [];\n  for (const value of records) {\n    try {\n      const record = JSON.parse(value) as WebhookRecord;\n      if (record.ownerTag !== ownerHash) continue;\n      webhooks.push({","sourceCodeStart":23,"sourceCodeEnd":59,"githubUrl":"https://github.com/koala73/worldmonitor/blob/7d06c8633d256c18e38133030bc3613976a96ec9/server/worldmonitor/shipping/v2/list-webhooks.ts#L23-L59","documentation":"listWebhooks requires a valid user API key. After gateway validation, the handler revalidates the credential with validateUserApiKey; if it returns null (credential present but not recognized/valid), it throws HTTP 401 'Invalid API key'. A later check also produces the same message when the key is required but never marked valid.","triggerScenarios":"Calling list-webhooks with an Authorization/X-API-Key header containing a revoked, deleted, mistyped, or malformed user API key that fails validateUserApiKey (returns null).","commonSituations":"Key was rotated or revoked server-side; the environment variable holding the key contains a stale or truncated value; sending a gateway-level key where a user key is required; whitespace or quoting mistakes when copying the key.","solutions":["Regenerate the API key in the dashboard and update the client/env configuration","Verify the exact header name and that the value has no surrounding whitespace or quotes","Confirm you are using a user API key (not a project/gateway key) for the shipping v2 webhook endpoints","Check the request environment actually loads the key (e.g. the env var is set in the deployed target, not just locally)"],"exampleFix":"// before\nconst client = createClient({ apiKey: process.env.OLD_KEY });\n// after\nconst key = process.env.WORLDMONITOR_API_KEY?.trim();\nif (!key) throw new Error('WORLDMONITOR_API_KEY not set');\nconst client = createClient({ apiKey: key });","handlingStrategy":"validation","validationCode":"const key = process.env.WORLDMONITOR_API_KEY;\nif (!key || key.trim().length === 0) throw new Error('WORLDMONITOR_API_KEY is not set');","typeGuard":"function hasApiKey(cfg: { apiKey?: string }): cfg is { apiKey: string } {\n  return typeof cfg.apiKey === 'string' && cfg.apiKey.trim().length > 0;\n}","tryCatchPattern":"try {\n  return await client.listWebhooks();\n} catch (e) {\n  if (e.status === 401) {\n    throw new Error('API key rejected — regenerate the key and update secrets');\n  }\n  throw e;\n}","preventionTips":["Rotate and update keys in one place (secret manager), never hardcode","Trim keys and avoid shell quoting artifacts","Use the key type (user vs gateway) appropriate for the endpoint and environment"],"tags":["authentication","api-key","http-401","credentials"],"backgroundTag":"authentication-required","analyzedSha":"7d06c8633d256c18e38133030bc3613976a96ec9","analyzedAt":"2026-09-15T16:44:39.439Z","contentChangedAt":"2026-09-15T16:44:39.439Z","schemaVersion":2},"datasetVersion":"2026-09-15T18:17:12.389Z"}