HeyPuter/puter · error · HttpError

unauthorized

unauthorized

Error message

Unauthorized

What it means

Thrown by POST /auth/request-app-root-dir after the app ownership check passes, when the app-under-user actor has no user.username. The root path is built as `/<username>/AppData/<appUid>`, so a missing username means the path cannot be constructed and the session is incomplete — rejected as unauthorized.

Source

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

        const appUid = getString(body, 'app_uid');
        if (!appUid)
            throw new HttpError(400, '`app_uid` is required', {
                legacyCode: 'bad_request',
            });

        const actorApp = (actor as { app?: { uid?: string } }).app;
        if (!actorApp?.uid || actorApp.uid !== appUid) {
            throw new HttpError(
                403,
                'Only the app itself may request its root dir',
                { legacyCode: 'forbidden' },
            );
        }
        const userId = this.#getActorUserId(req);
        const username = (actor as { user?: { username?: string } }).user
            ?.username;
        if (!username)
            throw new HttpError(401, 'Unauthorized', {
                legacyCode: 'unauthorized',
            });

        const rootPath = `/${username}/AppData/${appUid}`;
        // Auto-create the AppData/<uid> tree on first call.
        const entry = await this.services.fs.mkdir(userId, {
            path: rootPath,
            createMissingParents: true,
        });
        res.json(await toLegacyEntry(this.clients.event, entry));
    };

    /**
     * POST /auth/check-app-acl — check whether an app has a given mode of
     * access to a subject FS entry.
     */
    checkAppAcl = async (req: Request, res: Response): Promise<void> => {
        this.#requireActor(req);

View on GitHub (pinned to 908ec23eda)

Solutions

  1. Use an app-under-user token minted via getUserAppToken so the user (with username) is bound.
  2. Verify the user record has a username before issuing the token.
  3. Re-authenticate the user to refresh the session's user binding.

Example fix

// before — app token with no user binding
await fetch('/auth/request-app-root-dir', { method:'POST', headers:{Authorization:'Bearer '+appOnlyToken}, body: JSON.stringify({ app_uid }) })
// after — app-under-user token
const { token } = await puter.auth.getUserAppToken(app_uid)
await fetch('/auth/request-app-root-dir', { method:'POST', headers:{Authorization:'Bearer '+token}, body: JSON.stringify({ app_uid }) })
Defensive patterns

Strategy: validation

Validate before calling

// ensure the app-under-user actor has a username before calling
function assertAppHasUsername(actor) {
  const u = actor?.user?.username;
  if (typeof u !== 'string' || u.length === 0) {
    throw new Error('App session lacks user.username; mint an app-under-user token');
  }
}

Type guard

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

Prevention

When it happens

Trigger: An app token minted without a bound user context (app present, user missing); a malformed actor in tests; a token that lost its user binding.

Common situations: App token generated by a non-user flow; partial actor object; user record missing username after a partial migration.

Understand the failure class

Related errors


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