thedotmack/claude-mem · error

BadRequest

BadRequest

Error message

Legacy /api/sessions/summarize requires a project-scoped API key

What it means

Returned by the legacy POST /api/sessions/summarize compat route when the key is team-bound but has no project binding. Legacy semantics pin every summarize call to one project, so req.authContext.projectId must be non-null; a team-wide key fails this check with 400 even though it passed the team check.

Source

Thrown at src/server/compat/SessionsSummarizeAdapter.ts:63

      authMode: this.options.authMode,
      allowLocalDevBypass: this.options.allowLocalDevBypass,
      requiredScopes: ['memories:write'],
    });

    app.post('/api/sessions/summarize', writeAuth, this.asyncHandler(async (req, res) => {
      const parsed = summarizeSchema.safeParse(req.body);
      if (!parsed.success) {
        res.status(400).json({ error: 'ValidationError', issues: parsed.error.issues });
        return;
      }
      const teamId = req.authContext?.teamId ?? null;
      const projectId = req.authContext?.projectId ?? null;
      if (!teamId) {
        res.status(403).json({ error: 'Forbidden', message: 'API key is not bound to a team' });
        return;
      }
      if (!projectId) {
        res.status(400).json({
          error: 'BadRequest',
          message: 'Legacy /api/sessions/summarize requires a project-scoped API key',
        });
        return;
      }

      // Subagent contexts in legacy code emit summarize calls but the worker
      // skipped them. We preserve the legacy semantics so existing clients
      // see the same response shape.
      if (parsed.data.agentId) {
        res.json({ status: 'skipped', reason: 'subagent_context' });
        return;
      }

      try {
        await this.summarizeSession(req, res, parsed.data, teamId, projectId);
      } catch (error) {
        logger.error('SYSTEM', 'compat summarize adapter failed', {

View on GitHub (pinned to e2d1df569a)

Solutions

  1. Use a project-scoped API key: create one via POST /v1/keys with both the team and the target projectId.
  2. If the key should cover one project, rebind it so its projectId is set.
  3. Confirm the key's projectId via key introspection before switching the client to the legacy endpoint.
  4. Move the client to the modern route that accepts projectId in the payload instead of relying on key scope.

Example fix

// before: team-scoped key -> 400 BadRequest
const res = await fetch(`${base}/api/sessions/summarize`, {
  method: 'POST',
  headers: { Authorization: `Bearer ${TEAM_KEY}` },
  body: JSON.stringify(payload),
});

// after: mint a project-scoped key for the legacy route
// POST /v1/keys { teamId, projectId, scopes: ['memories:write'] }
const res = await fetch(`${base}/api/sessions/summarize`, {
  method: 'POST',
  headers: { Authorization: `Bearer ${PROJECT_SCOPED_KEY}` },
  body: JSON.stringify(payload),
});
Defensive patterns

Strategy: validation

Validate before calling

function assertProjectScopedKey(keyMeta: { teamId: string | null; projectId: string | null }) {
  if (!keyMeta.teamId || !keyMeta.projectId) {
    throw new Error('Legacy /api/sessions/summarize needs a project-scoped (team+project bound) API key');
  }
}

Type guard

interface CompatBadRequest { error: string; message: string }
function isProjectScopeRequired(body: unknown): body is CompatBadRequest {
  return (
    typeof body === 'object' && body !== null &&
    (body as CompatBadRequest).error === 'BadRequest' &&
    (body as CompatBadRequest).message?.includes('project-scoped API key')
  );
}

Try / catch

if (res.status === 400) {
  const body = await res.json();
  if (isProjectScopeRequired(body)) {
    // key scope problem, not payload: remint key, never resend same request
  }
}

Prevention

When it happens

Trigger: Calling /api/sessions/summarize with a team-scoped API key (no projectId on the key record); reusing a key minted for team-level read endpoints on the legacy compat route; upgrading a deployment to project-scoped keys while the client still holds an old team-only key.

Common situations: The same key works on /v1/events (project in body) but fails on the legacy route; ops minted a broad team key for convenience and legacy Claude Code clients then break with 400.

Related errors


AI-assisted analysis of thedotmack/claude-mem@e2d1df569a (2026-08-20). Data as JSON: /api/errors/0f8aa3fa1787397f. Report an issue: GitHub.