HeyPuter/puter · error · HttpError

app_or_api_token_required

app_or_api_token_required

Error message

This API cannot be called with an account session token. Use an app or worker token, or create an API token from the dashboard (Account → API Token).

What it means

Raised by the `noUserSession` / `assertNotUserSession` gate: the actor is a bare browser session token (no app, no access token, and the session is not a `kind='worker'` session). Routes opt into this gate to forbid the 'root' session credential from doubling as an API/AI credential, pushing callers toward a delegated, revocable token instead.

Source

Thrown at src/backend/core/http/middleware/gates.ts:177

 * This gate only rejects the bare-session shape. Which delegated credentials
 * are acceptable is decided by the gates it composes with (`requireUserActor`
 *
 * - `allowFullAccessToken` to also keep apps out, `requireNonAccessTokenGate` for
 *   scoped tokens, etc.).
 */
export const assertNotUserSession = (
    actor: Pick<Actor, 'app' | 'accessToken' | 'session'> | null | undefined,
): void => {
    if (!actor) return; // anonymous requests are the auth gate's problem
    if (actor.app || actor.accessToken) return;
    // User-scoped workers (deployed with no app binding) authenticate with
    // a session-TYPE token whose session row is `kind='worker'` — a managed,
    // revocable deployment credential, not a browser sign-in. Workers are
    // never treated as root tokens: this gate is an annoyance for
    // sign-up-and-scrape abuse, and someone who deploys a worker to reach
    // an API has already left that path.
    if (actor.session?.kind === 'worker') return;
    throw new HttpError(
        403,
        'This API cannot be called with an account session token. ' +
            'Use an app or worker token, or create an API token from the ' +
            'dashboard (Account → API Token).',
        { legacyCode: 'app_or_api_token_required' },
    );
};

/** Route-option form of {@link assertNotUserSession} (`noUserSession: true`). */
export const noUserSessionGate = (): RequestHandler => {
    return (req, _res, next) => {
        const actor = req.actor;
        if (!actor) {
            next(rejectAuth(req));
            return;
        }
        try {
            assertNotUserSession(actor);

View on GitHub (pinned to 908ec23eda)

Solutions

  1. Create an API token from the dashboard (Account → API Token) and send it as the auth credential.
  2. Or register an app and call the route through its app token.
  3. Or deploy a worker and use its worker session token (kind='worker'), which is explicitly allowed.
  4. Stop reusing the browser session token for API/AI calls.

Example fix

// before
fetch('/drivers/call', { headers: { Cookie: sessionCookie } });
// after
fetch('/drivers/call', { headers: { Authorization: `Bearer ${apiToken}` } });
Defensive patterns

Strategy: validation

Validate before calling

// Before calling a noUserSession route, ensure you're using a delegated credential:
if (!apiToken && !appToken && !workerToken) {
  throw new Error('Route requires an app/API/worker token, not a browser session.');
}

Prevention

When it happens

Trigger: Calling an AI/API route (registered with `noUserSession: true`) while authenticated only via a browser session cookie or a token copied from `/login` — no app token, no access token, no worker session.

Common situations: A dev script that grabbed the session cookie to call the AI API; a puter.js token reused server-side; trying to drive an API endpoint with the sign-in token instead of minting an API token.

Related errors


AI-assisted analysis of HeyPuter/puter@908ec23eda (2026-08-12). Data as JSON: /api/errors/5ec7bb039c4baef8. Report an issue: GitHub.