HeyPuter/puter · error · HttpError

forbidden

forbidden

Error message

Forbidden

What it means

Thrown by POST /sign when the caller is an app actor (actor.app is set) but the actor is missing either the owning user's username or the app's own uid. The endpoint computes an app-sandbox root `/<username>/AppData/<appUid>`; without both identifiers it cannot scope the app and refuses rather than signing anything.

Source

Thrown at src/backend/controllers/fs/LegacyFSController.ts:1270

        const actor = this.#requireActor(req);
        const body = asRecord(req.body);
        const items = Array.isArray(body.items) ? body.items : [];
        if (items.length === 0)
            throw new HttpError(400, '`items` is required', {
                legacyCode: 'bad_request',
            });

        const isApp = Boolean((actor as { app?: unknown }).app);
        const signingCfg = signingConfigFromAppConfig(this.config);

        // Apps can only sign inside their AppData root.
        let appDataRoot: string | null = null;
        if (isApp) {
            const username = (actor as { user?: { username?: string } }).user
                ?.username;
            const appUid = (actor as { app?: { uid?: string } }).app?.uid;
            if (!username || !appUid)
                throw new HttpError(403, 'Forbidden', {
                    legacyCode: 'forbidden',
                });
            appDataRoot = `/${username}/AppData/${appUid}`;
        }

        type SignedOrEmpty =
            (SignedFile & { path?: string }) | Record<string, never>;
        const result: { signatures: SignedOrEmpty[]; token?: string } = {
            signatures: [],
        };

        // Optional app grant: provide app_uid to grant permissions + token.
        let grantApp: { uid: string } | null = null;
        if (typeof body.app_uid === 'string' && body.app_uid.length > 0) {
            const app = await this.stores.app.getByUid(body.app_uid);
            if (!app)
                throw new HttpError(404, 'App not found', {
                    legacyCode: 'not_found',

View on GitHub (pinned to 908ec23eda)

Solutions

  1. Obtain a proper app-under-user token via getUserAppToken (which binds both user and app) before calling /sign.
  2. If you intend to sign as a user, call /sign from a user session token instead of an app token.
  3. Inspect req.actor server-side to confirm both user.username and app.uid are populated.

Example fix

// before — app token with no user bound
await fetch('/sign', { method:'POST', headers:{Authorization:'Bearer '+appToken}, body: JSON.stringify({items}) })
// after — mint an app-under-user token first
const { token } = await puter.auth.getUserAppToken(appUid)
await fetch('/sign', { method:'POST', headers:{Authorization:'Bearer '+token}, body: JSON.stringify({items}) })
Defensive patterns

Strategy: validation

Validate before calling

// before calling /sign as an app, ensure the actor is fully bound
function assertAppUnderUser(actor) {
  const u = actor?.user?.username;
  const a = actor?.app?.uid;
  if (!u || !a) throw new Error('App session is missing user.username or app.uid; mint an app-under-user token');
}

Type guard

/** @param {unknown} a @returns {a is { user: { username: string }, app: { uid: string } }} */
function isAppUnderUserActor(a) {
  return !!a && typeof a === 'object' &&
    typeof a.user?.username === 'string' && typeof a.app?.uid === 'string';
}

Prevention

When it happens

Trigger: An app-under-user token whose session is half-formed (app present but user.username missing, or app.uid missing); a misconfigured app token minted without a user context; calling /sign from a pure app (no user) authority.

Common situations: App token generated by a non-user flow; partial actor object in tests; an extension that constructed an actor manually without both fields.

Understand the failure class

Related errors


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