# OpenSpec Change: enterprise-permissions - **Change ID**: enterprise-permissions - **Package**: `@suaveplan/permissions` - **Category**: universal - **Target path**: `universal/auth/permissions` - **Status**: PENDING - **Date**: 2026-04-24 - **Reference spec**: `/mnt/s/web/suaveplan/enterprise/permissions.md` --- ## Summary Deliver `@suaveplan/permissions` on the genesis branch from scratch at enterprise quality. The package provides a full RBAC/ABAC permissions engine: an `Ability` / `AbilityBuilder` CASL-inspired API, `PermissionManager` for role and permission CRUD, `PolicyEngine` for multi-rule policy evaluation, `RoleManager` for inheritance-aware role graphs, a `PermissionCache` layer, pluggable storage adapters, and framework-agnostic middleware. The genesis rewrite is provider-agnostic (no direct Hono/Node coupling), adds structured errors via `@suaveplan/error`, Zod option schemas, `EventBusLike` integration, injectable clock, OTel instrumentation, property-based tests, and mitata benchmarks. --- ## Current-state audit (genesis) The package does not exist on genesis yet — this is a fresh build. --- ## Available workspace deps on genesis | Package | Role | |---|---| | `@suaveplan/error` (`workspace:^`) | Base class for `PermissionsError`; `defineCodes` for `PERM.*` error codes | | `@suaveplan/types` (`workspace:^`) | `Clock` (injectable for cache TTL), `LoggerLike`, `EventBusLike`, `StorageLike` | | `@suaveplan/events` (`workspace:^`) | Concrete `EventBus`; lifecycle events on permission grant/revoke/check | | `@suaveplan/logger` (`workspace:^`) | Structured logging with trace correlation | | `zod` (`catalog:forms`) | Zod schemas for all public option types | | `@opentelemetry/api` (`catalog:observability`) | Spans, counters, histograms | | `tslib` (`catalog:build`) | TypeScript runtime helpers | Dev deps: `vitest`, `@vitest/coverage-istanbul`, `fast-check`, `mitata` (all `catalog:testing`), `@suaveplan/testing` (`workspace:^`). --- ## Preserved public API surface (from main branch) ### Types ```ts type PermissionAction = "create" | "read" | "update" | "delete" | "list" | "manage" | "*" type ConditionOperator = "eq" | "neq" | "gt" | "gte" | "lt" | "lte" | "in" | "nin" | "contains" | "startsWith" | "endsWith" | "matches" interface Permission { readonly id: string readonly resource: string readonly action: PermissionAction | string readonly conditions?: PermissionCondition[] readonly description?: string } interface PermissionCondition { readonly field: string readonly operator: ConditionOperator readonly value: unknown } interface Role { readonly id: string readonly name: string readonly permissions: Permission[] readonly inherits?: string[] readonly metadata?: Record readonly isSystem?: boolean readonly siteId?: string } interface UserRole { readonly id: string readonly userId: string readonly roleId: string readonly siteId?: string readonly assignedAt: Date readonly expiresAt?: Date readonly assignedBy?: string } ``` ### Adapter interface ```ts interface GenericPermissionAdapter { // Permissions createPermission(permission: Permission): Promise updatePermission(id: string, updates: Partial): Promise deletePermission(id: string): Promise findPermission(id: string): Promise listPermissions(filter?: PermissionFilter): Promise // Roles createRole(role: Role): Promise updateRole(id: string, updates: Partial): Promise deleteRole(id: string): Promise findRole(id: string): Promise listRoles(filter?: RoleFilter): Promise // User-role assignments assignRole(userRole: UserRole): Promise revokeRole(userId: string, roleId: string, siteId?: string): Promise getUserRoles(userId: string, siteId?: string): Promise getRoleUsers(roleId: string, siteId?: string): Promise } // In-memory adapter class MemoryAdapter implements GenericPermissionAdapter // Stored shapes for adapter persistence interface StoredPermission { ... } interface StoredRole { ... } interface StoredPolicy { ... } interface StoredPolicyCondition { ... } interface StoredUserRole { ... } ``` ### Core ```ts class PermissionManager { constructor(adapter: GenericPermissionAdapter, options?: PermissionManagerOptions) createPermission(permission: Permission): Promise updatePermission(id: string, updates: Partial): Promise deletePermission(id: string): Promise findPermission(id: string): Promise listPermissions(filter?: PermissionFilter): Promise hasPermission(userId: string, resource: string, action: string, context?: Record): Promise getUserPermissions(userId: string, siteId?: string): Promise } class RoleManager { constructor(adapter: GenericPermissionAdapter, options?: RoleManagerOptions) createRole(role: Role): Promise updateRole(id: string, updates: Partial): Promise deleteRole(id: string): Promise findRole(id: string): Promise listRoles(filter?: RoleFilter): Promise assignRole(userId: string, roleId: string, options?: RoleAssignOptions): Promise revokeRole(userId: string, roleId: string, siteId?: string): Promise getUserRoles(userId: string, siteId?: string): Promise getRoleInheritanceChain(roleId: string): Promise } interface PolicyResult { allowed: boolean matchedRule?: string reason?: string conditions?: PermissionCondition[] } class PolicyEngine { constructor(permissionManager: PermissionManager, options?: PolicyEngineOptions) evaluate(userId: string, resource: string, action: string, context?: Record): Promise evaluateMany(requests: PolicyRequest[]): Promise } class Rule { constructor(resource: string, action: PermissionAction | string, conditions?: PermissionCondition[]) matches(resource: string, action: string): boolean evaluateConditions(context: Record): boolean } ``` ### Ability system (CASL-inspired) ```ts class Ability { constructor(rules: AbilityRule[]) can(action: string, resource: string, context?: Record): boolean cannot(action: string, resource: string, context?: Record): boolean rulesFor(action: string, resource: string): AbilityRule[] } class AbilityBuilder { can(action: PermissionAction | string, resource: string, conditions?: PermissionCondition[]): AbilityBuilder cannot(action: PermissionAction | string, resource: string, conditions?: PermissionCondition[]): AbilityBuilder build(): Ability } function buildAbility(fn: (builder: AbilityBuilder) => void): Ability ``` ### Cache ```ts interface PermissionCacheOptions { ttl: number // ms maxSize?: number clock?: Clock } class PermissionCache { constructor(options: PermissionCacheOptions) get(key: string): boolean | undefined set(key: string, value: boolean): void invalidate(userId: string): void invalidateAll(): void readonly size: number } ``` ### Conditions ```ts interface ConditionMatcher { matches(condition: PermissionCondition, context: Record): boolean } class DefaultConditionMatcher implements ConditionMatcher function compareValues(a: unknown, b: unknown, operator: ConditionOperator): boolean interface FieldMatcher { get(context: Record, field: string): unknown } class DefaultFieldMatcher implements FieldMatcher function fieldMatches(context: Record, condition: PermissionCondition): boolean interface SubjectDetector { detect(obj: unknown): string | undefined } class DefaultSubjectDetector implements SubjectDetector ``` ### Events ```ts interface PermissionEventMap { 'permission:grant': { userId: string; roleId: string; siteId?: string } 'permission:revoke': { userId: string; roleId: string; siteId?: string } 'permission:check': { userId: string; resource: string; action: string; allowed: boolean } } const permissionEvents: EventEmitter ``` ### Presets ```ts // Pre-built role sets function createAdminRole(permissions?: Permission[]): Role function createViewerRole(permissions?: Permission[]): Role function createEditorRole(permissions?: Permission[]): Role function createOwnerRole(permissions?: Permission[]): Role // Preset permission bundles const CRUD_PERMISSIONS: Record ``` ### Middleware ```ts interface PermissionMiddlewareOptions { resource: string action: string adapter?: GenericPermissionAdapter } type ResourceExtractor = (req: unknown) => string type UserExtractor = (req: unknown) => string | null function createPermissionMiddleware(options: PermissionMiddlewareOptions): MiddlewareFn function checkPermission(userId: string, resource: string, action: string, adapter: GenericPermissionAdapter): Promise ``` ### Error codes ```ts type PermissionsErrorCode = | "PERM_ROLE_NOT_FOUND" | "PERM_PERMISSION_NOT_FOUND" | "PERM_ASSIGNMENT_NOT_FOUND" | "PERM_CIRCULAR_INHERITANCE" | "PERM_SYSTEM_ROLE_MUTATION" | "PERM_ADAPTER_ERROR" | "PERM_CACHE_ERROR" class PermissionsError extends SuaveplanError function registerPermissionsErrorCodes(): void ``` --- ## New additions for genesis ### 1. Structured errors via `@suaveplan/error` `PermissionsError` extends `SuaveplanError`; all throw sites use named factory helpers keyed to the `PERM.*` code registry. ### 2. Clock injection `PermissionCache` and `UserRole` expiry checks accept an injectable `Clock` (from `@suaveplan/types`) so TTL behaviour is deterministic in tests. ### 3. EventBus integration `PermissionManager` and `RoleManager` accept an optional `EventBusLike`. Emit typed events from `PermissionEventMap` on every grant, revoke, and check. ### 4. OTel instrumentation - `permissions.check` span + histogram (`duration_ms`), attribute `allowed: bool` - `permissions.role_assign_total` / `permissions.role_revoke_total` counters - `permissions.cache_hit_total` / `permissions.cache_miss_total` counters - `permissions.inheritance_depth` histogram (role chain depth on lookup) ### 5. Zod schemas `PermissionSchema`, `RoleSchema`, `UserRoleSchema`, `PermissionManagerOptionsSchema`, `RoleManagerOptionsSchema`, `PolicyEngineOptionsSchema`, `PermissionCacheOptionsSchema`. ### 6. Property-based tests (fast-check) - Ability `can`/`cannot` consistency: `cannot(a, r)` iff `!can(a, r)` for any action/resource pair. - Role inheritance transitivity: if role A inherits B and B inherits C, `getUserPermissions` includes C's permissions. - Cache invalidation completeness: after `invalidate(userId)`, no stale entry remains for that user. - Condition evaluation determinism: `compareValues(a, b, op)` returns the same result for same inputs regardless of call order. ### 7. Benchmark suite - `PermissionManager.hasPermission` with no-cache path (direct adapter lookup) — 10 000 checks. - `PermissionManager.hasPermission` with warm `PermissionCache` — 10 000 checks. - `RoleManager.getRoleInheritanceChain` with 5-level deep inheritance — 10 000 calls. - `Ability.can` evaluation on a 50-rule set — 100 000 checks. --- ## File layout ```text universal/auth/permissions/ package.json tsconfig.json vitest.config.ts src/ index.ts error-codes.ts constants/ index.ts # CRUD_PERMISSIONS, system role constants types/ permissions.types.ts permissions.types.md adapters/ adapter-interface/ adapter-interface.ts adapter-interface.md adapter-interface.types.ts index.ts memory-adapter/ memory-adapter.ts memory-adapter.md memory-adapter.test.ts index.ts cache/ permission-cache/ permission-cache.ts permission-cache.md permission-cache.types.ts permission-cache.test.ts index.ts conditions/ condition-matcher/ condition-matcher.ts condition-matcher.md condition-matcher.types.ts condition-matcher.test.ts index.ts field-matcher/ field-matcher.ts field-matcher.md field-matcher.types.ts field-matcher.test.ts index.ts subject-detector/ subject-detector.ts subject-detector.md subject-detector.types.ts subject-detector.test.ts index.ts core/ ability/ ability.ts ability.md ability.test.ts index.ts ability-builder/ ability-builder.ts ability-builder.md ability-builder.test.ts index.ts permission-manager/ permission-manager.ts permission-manager.md permission-manager.types.ts permission-manager.test.ts index.ts policy-engine/ policy-engine.ts policy-engine.md policy-engine.types.ts policy-engine.test.ts index.ts role-manager/ role-manager.ts role-manager.md role-manager.types.ts role-manager.test.ts index.ts rule/ rule.ts rule.md rule.test.ts index.ts middleware/ permission-middleware/ permission-middleware.ts permission-middleware.md permission-middleware.types.ts permission-middleware.test.ts index.ts presets/ role-presets/ role-presets.ts role-presets.md role-presets.test.ts index.ts schemas/ permissions.schemas.ts permissions.schemas.md permissions.schemas.test.ts events/ permission-events.ts permission-events.md __benchmarks__/ permissions.bench.ts tests/ integration.test.ts property.test.ts ``` --- ## Impact **Downstream packages**: `@suaveplan/auth` uses `UserContext` and `checkPermission` from this package. Future API gateway plugins will consume `PolicyEngine`. **Breaking changes from main-branch API**: Hono-specific `createPermissionMiddleware` types are replaced with framework-agnostic equivalents; a `createHonoAdapter` bridge is provided for backward compatibility. All other symbols are preserved. **Workspace dep prerequisites**: `@suaveplan/error`, `@suaveplan/types`, `@suaveplan/events`, `@suaveplan/logger` must be published to Verdaccio before this package can be built or tested.