// The status page's authorization vocabulary, in ONE place — split out of // app.config.ts so the tests build the real plugin from the real role map (a // test that redeclares the roles only proves its own copy is self-consistent). // // A status page has exactly two kinds of caller: the PUBLIC (anonymous // visitors, who only READ — the live queries declare `openAccess` with their // reason) and OPERATORS (who post incidents). So there is one write scope, // `status:write`, held by the `operator` role; `admin` gets the wildcard // bypass. Both kinds of caller are DECIDED about: `security.defaultDeny` // refuses to boot a procedure that declares neither form. // // This map is ALSO the app's declared scope vocabulary: `rbacPlugin` publishes // its union, and `voltro check` errors on any descriptor guard requiring a // scope that appears nowhere here — so a guard and its grant can't silently // drift out of sync. export const roles = { operator: ['status:write'], admin: ['*'], } /** * DEMO role resolver — grants `operator` to callers in the `ops` tenant so * every operator flow is reachable with an `x-tenant: ops` header and zero auth * setup. Everyone else (including anonymous visitors) gets NO role, so the * ungated read queries still work but every `status:write` mutation is denied. * * PRODUCTION reads the caller's REAL roles: the default resolver reads * `subject.metadata.roles` (set by your auth strategy — pair this with the * api-auth / api-saas-starter auth wiring), or do a DB lookup here. If this * throws, rbac degrades to the subject's own scopes — it never grants on * failure. */ export const demoRolesForTenant = ( tenantId: string | null | undefined, ): ReadonlyArray => (tenantId === 'ops' ? ['operator'] : [])