// RBAC backend for the {{projectName}} project. // // `rbacPlugin` compiles a role→scope map; an rpc interceptor resolves each // caller's ROLES to SCOPES and publishes them to the framework's effective- // scope seam. From there BOTH authorization forms see them: // // • declarative `guards:` on a descriptor — enforced by the framework in the // dispatch spine, before the executor and (for a mutation) before the // transaction opens. PREFER THIS. It is the only form `voltro check` can // inspect, so a guard requiring a scope no role grants is caught // statically instead of becoming a permanently uncallable procedure. // • in-handler `permission()` / `can()` — for authz that needs LOADED data a // descriptor guard cannot see (see `notes.delete`). // // The `admin:full` scope is a blanket bypass for both. // // The role map itself lives in `authz.ts` so the tests can build the real // plugin from the real roles. // // Config-only — no external infra. import { defineEnv, envVar } from '@voltro/env' import { rbacPlugin } from '@voltro/plugin-rbac' import { demoRolesForTenant, roles, rolesOnTeam } from './authz' export const env = defineEnv({ LOG_LEVEL: envVar.enum(['debug', 'info', 'warn', 'error'], { access: 'public', default: 'info' }), }) export default { type: 'api' as const, name: '{{capProjectName}}{{capAppName}}', store: 'memory' as const, plugins: [ rbacPlugin({ roles, resolveRoles: (subject) => demoRolesForTenant(subject.tenantId), // PER-RESOURCE roles — what makes `guards: [{ scope, resource }]` mean // "owner OF THIS TEAM" instead of "owner globally". Consulted only for // the gap: a globally-held scope (or `admin:full`) passes without ever // calling this. Fail-CLOSED — if it throws or rejects the guard DENIES, // it does not fall back to the global answer. resolveResourceRoles: (subject, resource) => rolesOnTeam(subject.id, resource), }), ], env, }