import { Effect } from 'effect'; import { SchemaTable } from '@voltro/database'; import { ScopeError } from '@voltro/protocol'; import { Subject } from '@voltro/protocol'; import { VoltroPlugin } from '@voltro/protocol'; /** * Effect-form OR guard. Passes if the caller holds AT LEAST ONE of the * listed permissions. Fails `ScopeError` (naming the first option) when it * holds none. The `admin:full` bypass passes. * * ```ts * yield* anyPermission(ctx, ['notes:write', 'notes:admin']) * ``` */ export declare const anyPermission: (ctx: RbacContext, required: ReadonlyArray) => Effect.Effect; /** * Sync-form guard for async (non-Effect) handlers. THROWS `ScopeError` * when the caller lacks the required permission(s). Array = AND. * * ```ts * const execute = async (input, ctx) => { * assertPermission(ctx, 'notes:write') * return ctx.store.insert('notes', { ... }) * } * ``` */ export declare const assertPermission: (ctx: RbacContext, required: string | ReadonlyArray) => void; /** * Build the `userRoles` table. `tenantId` scopes a role assignment to the * active tenant (`null` = a global, all-tenant role). * * - `multitenant: true` → `tenantId` is a nullable FK to the `tenants` * table (resolved via `requireTenants()`), so the DDL carries the * foreign key when multitenancy is present. * - `multitenant: false` (default) → `tenantId` is a plain * `text().nullable()` column, so the package has NO hard dependency on * @voltro/plugin-multitenancy for single-tenant projects. */ export declare const buildUserRolesTable: (options?: { readonly multitenant?: boolean; }) => RbacTable; /** Pure boolean check — no throw, no Effect. For branching inside a handler. */ export declare const can: (ctx: RbacContext, required: string | ReadonlyArray) => boolean; export declare const compileRoles: (roleSlugs: ReadonlyArray, roleMap: RoleMap) => ReadonlyArray; /** * A {@link RoleResolver} backed by the `userRoles` table — resolves a * subject's role slugs from the store, scoped to the subject's tenant (so * tenant-aware role assignments Just Work). Drop into * `rbacPlugin({ resolveRoles: dataStoreRoleResolver(userRoleStore(ds)) })`. */ export declare const dataStoreRoleResolver: (store: UserRoleStore) => RoleResolver; /** * Compile a subject's roles + raw permissions into one resolved scope * set: `subject.scopes` ∪ compiled(roles) ∪ extraPermissions, deduped. * Pure given resolved inputs — the interceptor awaits the (possibly * async) resolvers, then calls this. */ export declare const mergeResolvedScopes: (subject: Subject, roleSlugs: ReadonlyArray, roleMap: RoleMap, extraPermissions?: ReadonlyArray) => ReadonlyArray; /** * Default `RoleResolver` — reads `subject.metadata.roles` if it's a * `string[]`, else `[]`. The framework never reads `metadata`; it's the * IdP slot, so a strategy that wants config-driven roles puts the slugs * there and the default resolver picks them up table-less. */ export declare const metadataRoleResolver: RoleResolver; /** * Effect-form guard. Fails with a typed `ScopeError` on the error channel * when the caller lacks the required permission(s). An array means ALL * are required (AND). The `admin:full` bypass passes everything. * * ```ts * export default (input, ctx) => Effect.gen(function* () { * yield* permission(ctx, 'notes:write') * // ... * }) * ``` */ export declare const permission: (ctx: RbacContext, required: string | ReadonlyArray) => Effect.Effect; /** * A resolver that yields raw permission strings merged into the resolved * set IN ADDITION to role-derived ones (per-row ACLs, feature flags, …). */ export declare type PermissionResolver = (subject: Subject) => ReadonlyArray | Promise>; /** * Anything carrying a resolved subject under `ctx.request.subject` — the * shape every handler executor receives (`AppContext`). Kept structural * so rbac needs no dependency on @voltro/runtime. */ export declare interface RbacContext { readonly request: { readonly subject: Subject; }; } /** * Build the rbac plugin. Returns the `VoltroPlugin` you pass into * `app.config.ts`'s `plugins: [...]` array. * * The interceptor runs once per mutation / query / action, BEFORE the * executor: it resolves the caller's roles, compiles them to scopes, * merges with the subject's raw scopes + any extra permissions, and * stashes the resolved set for `permission()` to read. A resolver failure * is logged and degrades to the subject's raw scopes — never silently * grants. */ export declare const rbacPlugin: (options: RbacPluginOptions) => VoltroPlugin; export declare interface RbacPluginOptions { /** * Namespace for this plugin's inspect surface + any rpc tags it contributes. Default `rbac`. * * Set it when your app already publishes under that name — an exact tag * collision is fatal at codegen, and this is the way out. */ readonly alias?: string; /** * Declarative role → permission map. Each role lists the permission * strings (== scope strings) it grants. `'*'` in any role's list * expands to the `admin:full` blanket bypass. * * ```ts * roles: { * viewer: ['notes:read'], * editor: ['notes:read', 'notes:write'], * admin: ['*'], * } * ``` */ readonly roles: RoleMap; /** * Where a subject's role slugs come from. Default: `metadataRoleResolver` * (reads `subject.metadata.roles` as a `string[]`). A project that * assigns roles at runtime supplies its own — e.g. a DB lookup filtered * by `subject.tenantId` for tenant-aware roles. */ readonly resolveRoles?: RoleResolver; /** * Extra raw permission strings merged into the resolved set in ADDITION * to role-derived ones (per-row ACLs, feature flags, …). Default: none. */ readonly resolvePermissions?: PermissionResolver; /** * PER-RESOURCE role resolution — the seam that makes a descriptor * `guards: [{ scope, resource }]` actually scope to that resource (a team, * a workspace, a document) instead of the caller's GLOBAL scopes. Given the * subject and the resource id the guard's `resource` extractor produced, * return the role slugs the caller holds ON THAT RESOURCE (e.g. `['owner']` * in team A, `['viewer']` in team B). rbac compiles them through the same * `roles` map and grants the scope iff the compiled set contains it. * * When set, rbac registers a process-global resource-scope resolver * (protocol's `setResourceScopeResolver`). A globally-held scope or * `admin:full` still passes without a per-resource lookup — this resolver is * only consulted for the gap. Omit for a purely global-scope app. * * ```ts * rbacPlugin({ * roles: { owner: ['roadmaps:write'], viewer: ['roadmaps:read'] }, * resolveResourceRoles: (subject, teamId) => db.rolesFor(subject.id, teamId), * }) * ``` */ readonly resolveResourceRoles?: ResourceRoleResolver; /** * Contribute the `roles` + `userRoles` tables via `extendSchema.tables`. * Default `false` — config-only / metadata-resolver projects run * table-less. Set `true` (or `{ multitenant: true }` for a FK to * `tenants`) when assigning roles at runtime against the DB. */ readonly tables?: boolean | { readonly multitenant?: boolean; }; } /** * The slice of a built `Table` this package exposes: the table name, its * columns, and its indexes. `SchemaTable` is @voltro/database's PUBLIC * structural table type — it names exactly this surface WITHOUT dragging in * the private column-builder class the fully-inferred `Table` generic would * (which is what forces TS4094 across a package boundary). Alias kept for * readability at the annotation sites below. */ export declare type RbacTable = SchemaTable; /** * Both rbac tables, ready to spread into `extendSchema.tables`. Pass * `{ multitenant: true }` to make `userRoles.tenantId` a FK to `tenants`. */ export declare const rbacTables: (options?: { readonly multitenant?: boolean; }) => ReadonlyArray; /** * Read the role slugs that produced the subject's resolved set, if the * interceptor recorded them. Empty when roles weren't resolved (or the * plugin isn't installed). For introspection only — NOT an authz signal. */ export declare const resolvedRoles: (subject: Subject) => ReadonlyArray; /** * Read the resolved scope set for a subject. Returns the interceptor's * compiled set if present; otherwise falls back to the subject's own * `scopes` (raw key-minted scopes still gate). Never returns undefined. */ export declare const resolvedScopes: (subject: Subject) => ReadonlyArray; /** * A resolver that yields the role slugs a subject holds ON A SPECIFIC RESOURCE * (a team, workspace, document) — the id the descriptor guard's `resource` * extractor produced. Powers resource-scoped `guards: [{ scope, resource }]`: * rbac compiles the returned slugs through its `roles` map and grants the scope * iff the compiled set contains it. Distinct from `RoleResolver`, which is * subject-global. Return `[]` for "no role on this resource". */ export declare type ResourceRoleResolver = (subject: Subject, resource: string) => ReadonlyArray | Promise>; /** * A role: a slug plus the flat permission strings it grants. The * permission strings ARE scope strings — `compileRoles` unions them into * the caller's resolved scope set. * * The wildcard `'*'` in any granted role's permission list expands to the * blanket `ADMIN_SCOPE` bypass — a caller who holds a `'*'` role passes * every `permission()` check. */ export declare interface RoleDefinition { /** Stable role identifier, e.g. `'editor'`. */ readonly slug: string; /** Flat permission strings, e.g. `['notes:read', 'notes:write']` or `['*']`. */ readonly permissions: ReadonlyArray; } /** * The declarative `roles:` map passed to `rbacPlugin({ roles })`. Maps a * role slug to the permission strings it grants. `'*'` is the wildcard * that compiles to `[ADMIN_SCOPE]`. * * ```ts * const roles = { * viewer: ['notes:read', 'comments:read'], * editor: ['notes:read', 'notes:write', 'comments:read', 'comments:write'], * admin: ['*'], * } * ``` */ export declare type RoleMap = Readonly>>; /** * A resolver that yields the role slugs a subject holds. The default * reads `subject.metadata.roles` (a `string[]`) when present; a project * that assigns roles at runtime supplies its own (e.g. a DB lookup * filtered by `subject.tenantId`). */ export declare type RoleResolver = (subject: Subject) => ReadonlyArray | Promise>; export declare const ROLES_TABLE = "_voltro_rbac_roles"; /** * Normalise a `RoleMap` into a list of `RoleDefinition`s. Convenience for * callers that want the structured shape (e.g. to seed the `roles` table * or render a role picker). */ export declare const rolesFromMap: (roleMap: RoleMap) => ReadonlyArray; /** * The `roles` table: one row per role, carrying its flat permission set. * `permissions` is a portable `json()` column (JSONB on pg, * JSON on mysql/mariadb, NVARCHAR(MAX) on mssql, TEXT on sqlite) — never * a postgres-only `text[]`. */ export declare const rolesTable: RbacTable; export { ScopeError } /** * Stash the resolved scope set (+ originating role slugs) for a subject. * Called once per request by the interceptor, before the executor runs. * Publishes the scope set to protocol's effective-scope seam so the * framework's declarative `guards:` enforcement sees role-derived scopes too. */ export declare const setResolvedScopes: (subject: Subject, scopes: ReadonlyArray, roleSlugs?: ReadonlyArray) => void; export declare const USER_ROLES_TABLE = "_voltro_rbac_user_roles"; /** A role assignment as stored: the subject, the role slug, and its tenant scope. */ export declare interface UserRoleAssignment { readonly userId: string; readonly roleSlug: string; /** `null` = a global (all-tenant) assignment. */ readonly tenantId?: string | null; } /** Narrow slice of the framework DataStore the user-role helpers need. */ export declare interface UserRoleDataStore { query: (descriptor: Record) => Promise>>; insert: (table: string, row: Record) => Promise; delete: (table: string, primaryKey: string) => Promise; } /** Single-tenant default — `tenantId` is a plain nullable text column. */ export declare const userRolesTable: RbacTable; /** * Assign / revoke / read role assignments over the `userRoles` table. All * methods are tenant-aware: `tenantId` scopes an assignment (or query) to a * tenant; `null` (the default) is a global assignment across all tenants. */ export declare interface UserRoleStore { /** Grant `roleSlug` to `userId` (idempotent — a duplicate assignment is a no-op). */ readonly assign: (userId: string, roleSlug: string, tenantId?: string | null) => Promise; /** Remove `roleSlug` from `userId` for the given tenant scope. */ readonly revoke: (userId: string, roleSlug: string, tenantId?: string | null) => Promise; /** * The role slugs `userId` holds. With a `tenantId`, returns the tenant's * assignments PLUS global (`tenantId: null`) ones; without, every assignment. */ readonly rolesOf: (userId: string, tenantId?: string | null) => Promise>; } /** A {@link UserRoleStore} over a framework DataStore. */ export declare const userRoleStore: (store: UserRoleDataStore) => UserRoleStore; export { VoltroPlugin } /** The wildcard permission string that compiles to the admin bypass. */ export declare const WILDCARD_PERMISSION = "*"; export { }