{"record":{"id":"44138dd6f8909d27","repo":"HeyPuter/puter","slug":"forbidden-44138d","errorCode":"forbidden","errorMessage":"User actor required","messagePattern":"User actor required","errorType":"http","errorClass":"HttpError","httpStatus":403,"severity":"error","filePath":"src/backend/drivers/notification/NotificationDriver.ts","lineNumber":214,"sourceCode":"        const ok = await this.stores.notification.markAcknowledged(\n            uid,\n            actor.user.id,\n        );\n        return { success: ok };\n    }\n\n    // -- Permissions -------------------------------------------------\n\n    #requireUserActor(): Actor & {\n        user: { id: number; uuid: string; username: string };\n    } {\n        const actor = Context.get('actor') as Actor | undefined;\n        if (!actor)\n            throw new HttpError(401, 'Authentication required', {\n                legacyCode: 'unauthorized',\n            });\n        if (!actor.user?.id)\n            throw new HttpError(403, 'User actor required', {\n                legacyCode: 'forbidden',\n            });\n        // App-under-user actors are not allowed for notifications.\n        if (actor.app)\n            throw new HttpError(403, 'App actors cannot access notifications', {\n                legacyCode: 'forbidden',\n            });\n        return actor as Actor & {\n            user: { id: number; uuid: string; username: string };\n        };\n    }\n\n    // -- Serialization -----------------------------------------------\n\n    #toClient(\n        row: Record<string, unknown> | null,\n    ): Record<string, unknown> | null {\n        if (!row) return null;","sourceCodeStart":196,"sourceCodeEnd":232,"githubUrl":"https://github.com/HeyPuter/puter/blob/908ec23eda38526170322c3edf71ba45ecb1ca95/src/backend/drivers/notification/NotificationDriver.ts#L196-L232","documentation":"Thrown by NotificationDriver.#requireUserActor() (NotificationDriver.ts:214) when the request is authenticated but the actor is not a full user — actor.user?.id is falsy. The notification driver is strictly owner-limited (see the permission-model note at lines 43-47): every method (create, read, select, mark_shown, mark_acknowledged) calls this guard first. It exists so non-user principals (raw app actors, the system actor, scoped access tokens without a populated user row) can never read or write a user's notifications.","triggerScenarios":"Calling any puter-notifications driver method via /drivers/call (interface 'puter-notifications') with: (a) a raw app token where the app authenticated on its own and actor.user has no numeric id; (b) the system actor SYSTEM_ACTOR (actor.ts:88-92) which only carries user.uuid + user.username, no id; (c) a scoped access-token actor whose authorized user row was never hydrated with id; (d) AuthService built an actor literal that skipped makeActor so user is a partial. Any of these reaching create/read/select/mark_shown/mark_acknowledged trips the 403.","commonSituations":"A backend service pushes a notification through the driver with a system/app token instead of writing to NotificationStore directly (the class doc at lines 39-41 says server-internal push must go through the store, not the driver). A CLI/worker script reuses an app token expecting it to act 'as the user'. A test harness that constructs a minimal actor without a full user row. A version where the auth middleware stopped hydrating user.id on the actor, or a deployment that authenticates machine-to-machine calls without a user principal.","solutions":["If the caller is server-internal (another service pushing a notification), bypass the driver and call NotificationStore.create({ userId, value }) directly with the target user's id — this is the documented path in the class JSDoc.","If the caller must go through the driver, authenticate with a real user session token so AuthService runs makeActor with a full user row (user.id populated), not a system/app/access-token actor.","Verify the auth middleware on the route actually resolves a user principal; an actor with user = {} or user = { uuid, username } (no id) means the user row was never loaded — fix the loader so the numeric id is present.","For access-token actors, ensure the token is authorized by a user and that AuthService copies that user's full row (including id) onto actor.user rather than leaving a partial.","In tests, build the actor through makeActor with a complete user row (id, uuid, username) — mirroring NotificationDriver's own return-type contract — instead of hand-writing a partial actor literal."],"exampleFix":"// before — server-internal push goes through the driver with a system actor\nawait driver.call('puter-notifications', 'create', {\n  object: { value: { text: 'hi' } },\n}); // throws 403 'User actor required'\n\n// after — write straight to the store for server-internal push\nawait services.get('notification-store').create({\n  userId: targetUser.id,\n  value: { text: 'hi' },\n});\n\n// or, if calling the driver, ensure a real user actor is on the context\n// (auth middleware must populate actor.user.id before this runs)","handlingStrategy":"validation","validationCode":"// Server-internal caller: check the actor before touching the driver.\n// If it isn't a real user, route to the store directly.\nimport type { Actor } from '../../core/actor.js';\n\nfunction resolveNotificationTarget(actor: Actor | undefined) {\n  if (actor?.user?.id) return { via: 'driver' as const, actor };\n  // not a user actor — do NOT call the driver; use the store with an explicit userId\n  return { via: 'store' as const, userId: undefined };\n}","typeGuard":"import type { Actor } from '../../core/actor.js';\n\ntype UserActor = Actor & {\n  user: { id: number; uuid: string; username: string };\n};\n\n// Mirrors NotificationDriver.#requireUserActor's admission rules\n// (must have user.id AND no app — see NotificationDriver.ts:209-216).\nfunction isUserActor(a: Actor | undefined): a is UserActor {\n  return !!a && typeof a.user?.id === 'number' && !a.app;\n}","tryCatchPattern":"// Only when you cannot validate ahead of the call.\nimport { HttpError } from '../../core/http/HttpError.js';\n\ntry {\n  await driver.select({ predicate: 'unseen' });\n} catch (e) {\n  if (e instanceof HttpError && e.status === 403 &&\n      e.message === 'User actor required') {\n    // not a user principal — fall back to a direct store write\n    // or surface 'sign in as a user' to the caller; do not retry unchanged.\n  } else {\n    throw e;\n  }\n}","preventionTips":["For server-internal notification push, always go through NotificationStore, never the driver — it's the documented path and side-steps the user-actor guard entirely.","Build every request-path actor with makeActor and a complete user row (id, uuid, username); never hand-write a partial actor literal for a route that reaches NotificationDriver.","In tests, assert the actor satisfies isUserActor before calling any notification driver method, so a fixture regression fails loudly in the test instead of as a 403 in CI.","Treat the legacyCode 'forbidden' + message 'User actor required' as a wiring signal, not a transient error: a retry with the same token will fail identically — fix the principal, don't retry."],"tags":["authentication","actor","notifications","forbidden","permissions","access-token","system-actor"],"backgroundTag":null,"analyzedSha":"908ec23eda38526170322c3edf71ba45ecb1ca95","analyzedAt":"2026-08-12T20:53:15.911Z","schemaVersion":2},"datasetVersion":"2026-08-12T23:17:12.415Z"}