{"record":{"id":"35ef40e2e27d2a2f","repo":"thedotmack/claude-mem","slug":"forbidden-35ef40","errorCode":"Forbidden","errorMessage":"Invalid API key or insufficient scope","messagePattern":"Invalid API key or insufficient scope","errorType":"http","errorClass":null,"httpStatus":403,"severity":"error","filePath":"src/server/middleware/postgres-auth.ts","lineNumber":100,"sourceCode":"      apiKeyId: null,\n      mode: 'local-dev',\n    };\n    req.authContext = ctx;\n    next();\n    return;\n  }\n\n  if (!rawKey) {\n    res.status(401).json({\n      error: 'Unauthorized',\n      message: 'Missing API key (Authorization: Bearer <key> or X-Api-Key: <key>)',\n    });\n    return;\n  }\n\n  const verified = await verifyPostgresApiKey(pool, rawKey, options.requiredScopes ?? []);\n  if (!verified) {\n    res.status(403).json({ error: 'Forbidden', message: 'Invalid API key or insufficient scope' });\n    return;\n  }\n\n  const ctx: AuthContext = {\n    userId: null,\n    organizationId: null,\n    teamId: verified.teamId,\n    projectId: verified.projectId,\n    scopes: verified.scopes,\n    apiKeyId: verified.apiKeyId,\n    mode: 'api-key',\n  };\n  req.authContext = ctx;\n  next();\n}\n\ninterface VerifiedPostgresApiKey {\n  apiKeyId: string;","sourceCodeStart":82,"sourceCodeEnd":118,"githubUrl":"https://github.com/thedotmack/claude-mem/blob/e2d1df569a8f04075d40e92461128ece7cf04c82/src/server/middleware/postgres-auth.ts#L82-L118","documentation":"403 from the Postgres auth middleware: a key was presented but verifyPostgresApiKey returned null — unknown, revoked, or scope-deficient key. The route's requiredScopes are checked as part of verification, so a valid key lacking the needed scope (e.g. events:write for writeAuth routes) produces the same 403 as an invalid key.","triggerScenarios":"POST /v1/events with a read-only key against writeAuth; a key minted in a different Postgres database (env mismatch); revoked or expired key still cached by the client; whitespace-corrupted key value.","commonSituations":"The MCP link flow mints read-only keys and a script reuses one for writes; staging vs production databases hold different keys; rotation happened but a long-running process kept the old key in memory.","solutions":["Verify the key exists and is active in the Postgres instance the server points at.","Compare the route's requiredScopes with the key's scopes; mint a new key including the missing scope via POST /v1/keys.","Strip whitespace/newlines from the configured key value.","Restart long-lived clients after key rotation so they reload credentials."],"exampleFix":"# before: read-only key against a write route -> 403\ncurl -X POST https://host/v1/events -H 'X-Api-Key: ro-key' -d '{}'\n\n# after: mint a key with the write scope\n# POST /v1/keys { scopes: ['events:write', ...] } -> { key }\ncurl -X POST https://host/v1/events -H 'X-Api-Key: write-key' -d '{}'","handlingStrategy":"validation","validationCode":"// Match route requirements to key scopes before sending\nconst ROUTE_SCOPES = { '/v1/events': ['events:write'] } as const;\nfunction assertScopes(url: string, keyScopes: string[]) {\n  const need = ROUTE_SCOPES[url];\n  if (need && !need.every(s => keyScopes.includes(s))) {\n    throw new Error(`Key lacks ${need.join(',')} for ${url}`);\n  }\n}","typeGuard":"function isInvalidKey403(res: Response): boolean { return res.status === 403; }","tryCatchPattern":null,"preventionTips":["Mint keys with exactly the scopes each client's route list requires.","Run a scope-matrix test in CI: every client route x key scope must pass.","After rotation, restart long-lived processes to reload credentials."],"tags":["auth","http-403","api-key","scopes","postgres","middleware"],"backgroundTag":"invalid-api-key","analyzedSha":"e2d1df569a8f04075d40e92461128ece7cf04c82","analyzedAt":"2026-08-20T23:58:13.836Z","schemaVersion":2},"datasetVersion":"2026-08-29T17:17:51.833Z"}