thedotmack/claude-mem · error

Forbidden

Forbidden

Error message

`include=payload` requires admin scope

What it means

GET /v1/jobs supports an include=payload query parameter that returns each job's full BullMQ payload, which can contain sensitive generation request data. The route refuses the call with 403 when the API key's scopes do not include '*', 'admin', or 'memories:admin'. The refusal is deliberate: rather than silently stripping the include, it keeps the attempted privilege elevation visible in the audit chain.

Source

Thrown at src/server/routes/v1/ServerV1PostgresRoutes.ts:622

    // project's jobs; team-scoped key sees the team's jobs. Filters: status,
    // source_type, limit, offset, since (ISO timestamp on created_at). The
    // BullMQ payload column is NEVER returned by default — even with admin
    // scope, the caller MUST opt in via `?include=payload`. This anti-pattern
    // guard prevents accidental exfil of sensitive event payloads.
    app.get('/v1/jobs', readAuth, this.asyncHandler(async (req, res) => {
      const teamId = this.requireTeamId(req, res);
      if (!teamId) return;
      const callerProjectId = req.authContext?.projectId ?? null;
      const includeRaw = typeof req.query.include === 'string' ? req.query.include : '';
      const includePayload = includeRaw.split(',').map(p => p.trim()).includes('payload');
      const callerScopes = req.authContext?.scopes ?? [];
      const isAdmin = callerScopes.includes('*') || callerScopes.includes('admin')
        || callerScopes.includes('memories:admin');
      if (includePayload && !isAdmin) {
        // Anti-pattern guard: refuse the include=payload elevation without
        // admin scope. Returning 403 (not silently stripping) makes the
        // attempted privilege escalation visible in the audit chain.
        res.status(403).json({
          error: 'Forbidden',
          message: '`include=payload` requires admin scope',
        });
        return;
      }
      const { status, sourceType, limit, offset, since } = parseGenericJobListingQuery(req);
      let jobs: JobListRow[] = [];
      let total = 0;
      try {
        ({ jobs, total } = await this.listJobsForScope({
          teamId, projectId: callerProjectId, status, sourceType, limit, offset, since,
        }));
      } catch (error) {
        const err = error instanceof Error ? error : new Error(String(error));
        logger.warn('SYSTEM', 'jobs.list query failed', { requestId: req.requestId ?? null }, err);
        this.handleDbError(err, res, 'jobs.list');
        return;
      }

View on GitHub (pinned to e2d1df569a)

Solutions

  1. Remove 'payload' from the include query parameter and re-issue the request
  2. If job payload data is genuinely required, issue the API key with the 'memories:admin' (or 'admin'/'*') scope
  3. Verify the key's effective scopes via key introspection before switching dashboards to include=payload

Example fix

// before
const res = await fetch(`${base}/v1/jobs?include=payload`, { headers: { Authorization: `Bearer ${key}` } });

// after — only request payload when the key is known to be admin-scoped
const isAdminKey = keyScopes.some(s => s === '*' || s === 'admin' || s === 'memories:admin');
const include = isAdminKey ? 'status,payload' : 'status';
const res = await fetch(`${base}/v1/jobs?include=${include}`, { headers: { Authorization: `Bearer ${key}` } });
Defensive patterns

Strategy: validation

Validate before calling

// Only ask for payloads when the key is known to carry an admin-ish scope
const ADMIN_SCOPES = new Set(['*', 'admin', 'memories:admin']);
const canIncludePayload = (keyScopes: string[]) => keyScopes.some(s => ADMIN_SCOPES.has(s));
const include = canIncludePayload(keyScopes) ? 'status,payload' : 'status';

Type guard

const isAdminScope = (scopes: string[] | undefined): boolean =>
  !!scopes && scopes.some(s => s === '*' || s === 'admin' || s === 'memories:admin');

Prevention

When it happens

Trigger: GET /v1/jobs?include=payload (or include=status,payload) issued with a key whose scopes are e.g. ['memories:read'] or any list without '*', 'admin', or 'memories:admin'.

Common situations: Ops dashboards or debug scripts copied from an admin-key environment onto a production read-only key; a newly minted key provisioned without admin scopes; tooling migrated from a team-admin key to a least-privilege key.

Related errors


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