{"record":{"id":"aa23bdaee1eb18d2","repo":"HeyPuter/puter","slug":"forbidden-aa23bd","errorCode":"forbidden","errorMessage":"Forbidden","messagePattern":"Forbidden","errorType":"http","errorClass":"HttpError","httpStatus":403,"severity":"error","filePath":"src/backend/controllers/fs/LegacyFSController.ts","lineNumber":1270,"sourceCode":"        const actor = this.#requireActor(req);\n        const body = asRecord(req.body);\n        const items = Array.isArray(body.items) ? body.items : [];\n        if (items.length === 0)\n            throw new HttpError(400, '`items` is required', {\n                legacyCode: 'bad_request',\n            });\n\n        const isApp = Boolean((actor as { app?: unknown }).app);\n        const signingCfg = signingConfigFromAppConfig(this.config);\n\n        // Apps can only sign inside their AppData root.\n        let appDataRoot: string | null = null;\n        if (isApp) {\n            const username = (actor as { user?: { username?: string } }).user\n                ?.username;\n            const appUid = (actor as { app?: { uid?: string } }).app?.uid;\n            if (!username || !appUid)\n                throw new HttpError(403, 'Forbidden', {\n                    legacyCode: 'forbidden',\n                });\n            appDataRoot = `/${username}/AppData/${appUid}`;\n        }\n\n        type SignedOrEmpty =\n            (SignedFile & { path?: string }) | Record<string, never>;\n        const result: { signatures: SignedOrEmpty[]; token?: string } = {\n            signatures: [],\n        };\n\n        // Optional app grant: provide app_uid to grant permissions + token.\n        let grantApp: { uid: string } | null = null;\n        if (typeof body.app_uid === 'string' && body.app_uid.length > 0) {\n            const app = await this.stores.app.getByUid(body.app_uid);\n            if (!app)\n                throw new HttpError(404, 'App not found', {\n                    legacyCode: 'not_found',","sourceCodeStart":1252,"sourceCodeEnd":1288,"githubUrl":"https://github.com/HeyPuter/puter/blob/908ec23eda38526170322c3edf71ba45ecb1ca95/src/backend/controllers/fs/LegacyFSController.ts#L1252-L1288","documentation":"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.","triggerScenarios":"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.","commonSituations":"App token generated by a non-user flow; partial actor object in tests; an extension that constructed an actor manually without both fields.","solutions":["Obtain a proper app-under-user token via getUserAppToken (which binds both user and app) before calling /sign.","If you intend to sign as a user, call /sign from a user session token instead of an app token.","Inspect req.actor server-side to confirm both user.username and app.uid are populated."],"exampleFix":"// before — app token with no user bound\nawait fetch('/sign', { method:'POST', headers:{Authorization:'Bearer '+appToken}, body: JSON.stringify({items}) })\n// after — mint an app-under-user token first\nconst { token } = await puter.auth.getUserAppToken(appUid)\nawait fetch('/sign', { method:'POST', headers:{Authorization:'Bearer '+token}, body: JSON.stringify({items}) })","handlingStrategy":"validation","validationCode":"// before calling /sign as an app, ensure the actor is fully bound\nfunction assertAppUnderUser(actor) {\n  const u = actor?.user?.username;\n  const a = actor?.app?.uid;\n  if (!u || !a) throw new Error('App session is missing user.username or app.uid; mint an app-under-user token');\n}","typeGuard":"/** @param {unknown} a @returns {a is { user: { username: string }, app: { uid: string } }} */\nfunction isAppUnderUserActor(a) {\n  return !!a && typeof a === 'object' &&\n    typeof a.user?.username === 'string' && typeof a.app?.uid === 'string';\n}","tryCatchPattern":null,"preventionTips":["Mint app tokens via getUserAppToken so both user and app are bound.","In tests, construct the actor with both fields rather than only app.","If signing as a user, use a user session token instead."],"tags":["auth","app-session","sign","forbidden"],"backgroundTag":null,"analyzedSha":"908ec23eda38526170322c3edf71ba45ecb1ca95","analyzedAt":"2026-08-12T20:53:15.911Z","schemaVersion":2},"datasetVersion":"2026-08-12T23:17:12.415Z"}