import type { AccessFn, RbacConfig, RoleDefinition } from './types' function defaultGetRoles(session: any): string | string[] | undefined { return session?.data?.role ?? session?.role } /** * Role-based access control. Resolves a session's roles to a flat permission set (expanding * inheritance + wildcards) and exposes helpers that slot directly into Nixxie * `access` functions. */ export class Rbac { private roles: Record private getRoles: (session: Session | undefined) => string | string[] | undefined constructor(config: RbacConfig) { this.roles = config.roles this.getRoles = config.getRoles ?? (defaultGetRoles as any) } private rolesOf(session: Session | undefined): string[] { const raw = this.getRoles(session) if (!raw) return [] return Array.isArray(raw) ? raw : [raw] } /** Expand a role to the full set of permission strings it grants, following `inherits`. */ private permissionsForRole(role: string, seen = new Set()): Set { const out = new Set() if (seen.has(role)) return out seen.add(role) const def = this.roles[role] if (!def) return out for (const p of def.permissions) out.add(p) for (const parent of def.inherits ?? []) { for (const p of this.permissionsForRole(parent, seen)) out.add(p) } return out } /** All permissions granted to a session across its roles. */ permissions(session: Session | undefined): Set { const out = new Set() for (const role of this.rolesOf(session)) { for (const p of this.permissionsForRole(role)) out.add(p) } return out } /** Whether the session holds a given role. */ hasRole(session: Session | undefined, role: string): boolean { return this.rolesOf(session).includes(role) } /** Whether the session is granted a permission, honouring `*` and `resource:*` wildcards. */ can(session: Session | undefined, permission: string): boolean { const perms = this.permissions(session) if (perms.has('*') || perms.has(permission)) return true const [resource] = permission.split(':') return perms.has(`${resource}:*`) } // ── access-function factories ── /** Access function granting access when the session has the permission. */ permission(permission: string): AccessFn { return ({ session }) => this.can(session, permission) } /** Access function granting access when the session has any of the permissions. */ anyPermission(...permissions: string[]): AccessFn { return ({ session }) => permissions.some(p => this.can(session, p)) } /** Access function granting access when the session has every permission. */ allPermissions(...permissions: string[]): AccessFn { // Guard the empty case: `[].every()` is `true`, which would grant access to everyone // (including unauthenticated requests). An all-of check over no permissions denies. return ({ session }) => permissions.length > 0 && permissions.every(p => this.can(session, p)) } /** Access function granting access when the session holds one of the roles. */ requireRole(...roles: string[]): AccessFn { return ({ session }) => roles.some(r => this.hasRole(session, r)) } } export function createRbac(config: RbacConfig): Rbac { return new Rbac(config) } export type { RbacConfig, RoleDefinition, AccessArgs, AccessFn } from './types'