{"record":{"id":"a142eb124008268c","repo":"thedotmack/claude-mem","slug":"forbidden-a142eb","errorCode":"Forbidden","errorMessage":"API key is not bound to a team","messagePattern":"API key is not bound to a team","errorType":"http","errorClass":null,"httpStatus":403,"severity":"error","filePath":"src/server/compat/SessionsSummarizeAdapter.ts","lineNumber":59,"sourceCode":"  constructor(private readonly options: SessionsSummarizeAdapterOptions) {}\n\n  setupRoutes(app: Application): void {\n    const writeAuth = requirePostgresServerAuth(this.options.pool, {\n      authMode: this.options.authMode,\n      allowLocalDevBypass: this.options.allowLocalDevBypass,\n      requiredScopes: ['memories:write'],\n    });\n\n    app.post('/api/sessions/summarize', writeAuth, this.asyncHandler(async (req, res) => {\n      const parsed = summarizeSchema.safeParse(req.body);\n      if (!parsed.success) {\n        res.status(400).json({ error: 'ValidationError', issues: parsed.error.issues });\n        return;\n      }\n      const teamId = req.authContext?.teamId ?? null;\n      const projectId = req.authContext?.projectId ?? null;\n      if (!teamId) {\n        res.status(403).json({ error: 'Forbidden', message: 'API key is not bound to a team' });\n        return;\n      }\n      if (!projectId) {\n        res.status(400).json({\n          error: 'BadRequest',\n          message: 'Legacy /api/sessions/summarize requires a project-scoped API key',\n        });\n        return;\n      }\n\n      // Subagent contexts in legacy code emit summarize calls but the worker\n      // skipped them. We preserve the legacy semantics so existing clients\n      // see the same response shape.\n      if (parsed.data.agentId) {\n        res.json({ status: 'skipped', reason: 'subagent_context' });\n        return;\n      }\n","sourceCodeStart":41,"sourceCodeEnd":77,"githubUrl":"https://github.com/thedotmack/claude-mem/blob/e2d1df569a8f04075d40e92461128ece7cf04c82/src/server/compat/SessionsSummarizeAdapter.ts#L41-L77","documentation":"Thrown by the legacy Claude Code compat endpoint POST /api/sessions/summarize when the authenticated API key carries no team binding. The auth middleware fills req.authContext.teamId from the key record, and the compat adapter requires it because sessions are stored per team. Local-dev mode also yields teamId null unless a local-dev team is configured, so this fires there too.","triggerScenarios":"POST /api/sessions/summarize with a Bearer/X-Api-Key key whose record has team_id = null; running the server with local-dev auth where authContext.teamId is null; using a key minted without a team; pointing a legacy Claude Code client at a deployment whose keys predate team scoping.","commonSituations":"Test scripts copy an env key from another deployment that was never bound to a team; server runs in local-dev mode without localDevTeamId configured; a read-only key minted for the MCP link endpoint is reused for the legacy summarize endpoint.","solutions":["Call POST /api/sessions/summarize with an API key that is bound to the team owning the sessions (create/bind one via POST /v1/keys with a team).","If this is local development, configure the local-dev team id on the auth middleware so local-dev requests get a teamId.","Introspect the key (key management endpoint or keys table) and confirm team_id is set before wiring the client.","Migrate the client off the legacy route to the modern /v1 event/summarize surface that matches the key's scope."],"exampleFix":"// before\nawait fetch(`${base}/api/sessions/summarize`, {\n  method: 'POST',\n  headers: { 'X-Api-Key': process.env.API_KEY!, 'Content-Type': 'application/json' },\n  body: JSON.stringify({ contentSessionId: sid }),\n}); // 403: key bound to no team\n\n// after — mint/use a team-bound key first\n// POST /v1/keys { teamId, scopes: ['memories:write'] } -> { key }\nawait fetch(`${base}/api/sessions/summarize`, {\n  method: 'POST',\n  headers: { 'X-Api-Key': TEAM_BOUND_KEY, 'Content-Type': 'application/json' },\n  body: JSON.stringify({ contentSessionId: sid }),\n});","handlingStrategy":"validation","validationCode":"// Before first summarize call, confirm the key is team-bound\n// (e.g. via your key registry / minting response)\nfunction assertTeamBoundKey(keyMeta: { teamId: string | null }) {\n  if (!keyMeta.teamId) {\n    throw new Error('API key has no team binding; mint a team-bound key before calling /api/sessions/summarize');\n  }\n}","typeGuard":"function isMissingTeamBinding(res: Response): boolean {\n  return res.status === 403;\n}\n\ninterface CompatError {\n  error: string;\n  message: string;\n}\nfunction isForbiddenTeamBody(body: unknown): body is CompatError {\n  return (\n    typeof body === 'object' && body !== null &&\n    'error' in body && (body as CompatError).error === 'Forbidden' &&\n    'message' in body && (body as CompatError).message === 'API key is not bound to a team'\n  );\n}","tryCatchPattern":"try {\n  const res = await fetch(url, opts);\n  if (res.status === 403) {\n    const body = await res.json();\n    if (isForbiddenTeamBody(body)) {\n      // configuration error: fix key binding, do not retry\n      throw new KeyConfigError('rebind key to a team');\n    }\n  }\n} catch (e) { if (!(e instanceof KeyConfigError)) throw e; }","preventionTips":["Store the key's team binding alongside the key in client config and assert it at startup.","Run a preflight health check request after configuring a new key.","Never reuse MCP link read-only keys for the legacy summarize route."],"tags":["auth","api-key","http-403","claude-code-compat","team-binding"],"backgroundTag":"api-key-scope-mismatch","analyzedSha":"e2d1df569a8f04075d40e92461128ece7cf04c82","analyzedAt":"2026-08-20T23:58:13.836Z","schemaVersion":2},"datasetVersion":"2026-08-29T17:17:51.833Z"}