# Tasks: enterprise-permissions **Change ID**: enterprise-permissions **Package**: `@suaveplan/permissions` **Target path**: `universal/auth/permissions` **Prerequisite packages on Verdaccio**: `@suaveplan/error`, `@suaveplan/types`, `@suaveplan/events`, `@suaveplan/logger` --- ## 1. Scaffold - [x] Create directory `universal/auth/permissions/` with `src/`, `tests/`, `src/__benchmarks__/`, and all feature subdirectories per the layout in `proposal.md`. - [x] Write `package.json` at version `0.1.0` with: - `"type": "module"`, `"sideEffects": false` - `exports` map with top-level `"."` plus subpaths: `"./adapters/adapter-interface/adapter-interface"`, `"./adapters/memory-adapter/memory-adapter"`, `"./cache/permission-cache/permission-cache"`, `"./conditions/condition-matcher/condition-matcher"`, `"./conditions/field-matcher/field-matcher"`, `"./conditions/subject-detector/subject-detector"`, `"./core/ability/ability"`, `"./core/ability-builder/ability-builder"`, `"./core/permission-manager/permission-manager"`, `"./core/policy-engine/policy-engine"`, `"./core/role-manager/role-manager"`, `"./core/rule/rule"`, `"./middleware/permission-middleware/permission-middleware"`, `"./presets/role-presets/role-presets"`, `"./schemas/permissions.schemas"`, `"./types/permissions.types"`, `"./events/permission-events"` - `dependencies`: `@suaveplan/error workspace:^`, `@suaveplan/types workspace:^`, `@suaveplan/events workspace:^`, `@suaveplan/logger workspace:^`, `zod catalog:forms`, `@opentelemetry/api catalog:observability`, `tslib catalog:build` - `devDependencies`: `vitest catalog:testing`, `@vitest/coverage-istanbul catalog:testing`, `fast-check catalog:testing`, `mitata catalog:testing`, `@suaveplan/testing workspace:^` - [x] Write `tsconfig.json` extending `@suaveplan/typescript-config/library.json`. - [x] Write `vitest.config.ts` using `createTestConfig()` from `@suaveplan/testing/config` (pool: `forks`). - [x] Add `lint`, `typecheck`, `test`, `test:coverage`, `build`, `docs:check`, `bench` scripts to `package.json`. --- ## 2. Types and errors ### 2a. Types (`src/types/permissions.types.ts` + `.md`) - [x] Define all public interfaces and type aliases from `proposal.md`: `PermissionAction`, `ConditionOperator`, `Permission`, `PermissionCondition`, `Role`, `UserRole`, `PermissionFilter`, `RoleFilter`, `PolicyRequest`, `AbilityRule`, `PermissionManagerOptions`, `RoleManagerOptions`, `PolicyEngineOptions`, `RoleAssignOptions`, `MiddlewareFn`. - [x] Write `permissions.types.md` (≥200 words, API reference for every type). ### 2b. Error codes (`src/error-codes.ts`) - [x] Define `PermissionsErrorCode` union — seven codes: `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`. - [x] Define `PERMISSIONS_ERROR_METADATA` with `httpStatus`, `severity`, `retryable`, `userMessageKey`, `category`, `description` per code. - [x] Export `registerPermissionsErrorCodes()` as idempotent eager side-effect. - [x] Export `PermissionsError extends SuaveplanError`. ### 2c. Zod schemas (`src/schemas/permissions.schemas.ts` + `.md`) - [x] Write Zod schemas: `PermissionSchema`, `RoleSchema`, `UserRoleSchema`, `PermissionManagerOptionsSchema`, `RoleManagerOptionsSchema`, `PolicyEngineOptionsSchema`, `PermissionCacheOptionsSchema`. - [x] Write `permissions.schemas.md`. - [x] Write `permissions.schemas.test.ts` — parse valid/invalid for each schema. --- ## 3. Adapter interface - [x] Write `src/adapters/adapter-interface/adapter-interface.ts` — full `GenericPermissionAdapter` interface from `proposal.md`. - [x] Write `adapter-interface.types.ts` with `StoredPermission`, `StoredRole`, `StoredPolicy`, `StoredPolicyCondition`, `StoredUserRole`. - [x] Write `adapter-interface.md`. --- ## 4. MemoryAdapter - [x] Implement `MemoryAdapter` using `Map`-backed storage for permissions, roles, and user-role assignments. - [x] Support optional `clock?: Clock` for expiry checks on `UserRole.expiresAt`. - [x] Filter expired user-roles on lookup (lazy removal — no `setInterval`). - [x] Write `memory-adapter.test.ts` — CRUD for permissions/roles/assignments, expiry with FakeClock, list filters, edge cases (not found). Each `it` has `{ timeout: 5000 }`. - [x] Write `memory-adapter.md`. --- ## 5. Conditions system ### 5a. ConditionMatcher - [x] Implement `DefaultConditionMatcher` and `compareValues` supporting all 11 operators from `ConditionOperator`. - [x] Write `condition-matcher.test.ts` — each operator for string/number/array/null inputs, boundary values. Each `it` has `{ timeout: 5000 }`. - [x] Write `condition-matcher.md`. ### 5b. FieldMatcher - [x] Implement `DefaultFieldMatcher` with dot-notation path traversal and `fieldMatches` helper. - [x] Write `field-matcher.test.ts` — nested paths, missing fields, array index access. Each `it` has `{ timeout: 5000 }`. - [x] Write `field-matcher.md`. ### 5c. SubjectDetector - [x] Implement `DefaultSubjectDetector` — infers resource type from `__type` property, constructor name, or explicit override. - [x] Write `subject-detector.test.ts`. - [x] Write `subject-detector.md`. --- ## 6. Core — Rule - [x] Implement `Rule` with `matches(resource, action)` (exact + wildcard `*`) and `evaluateConditions(context)`. - [x] Write `rule.test.ts` — wildcard action, wildcard resource, condition evaluation, negative conditions. Each `it` has `{ timeout: 5000 }`. - [x] Write `rule.md`. --- ## 7. Core — Ability and AbilityBuilder - [x] Implement `Ability` with `can`, `cannot`, `rulesFor`. Wildcard `*` on action or resource grants all. - [x] Implement `AbilityBuilder` with fluent `can` / `cannot` methods; `build()` returns frozen `Ability`. - [x] Implement `buildAbility(fn)` convenience factory. - [x] Validate that `cannot` rules override `can` rules (deny-overrides semantics). - [x] Write `ability.test.ts` — basic can/cannot, wildcard, conditions, rule precedence. Each `it` has `{ timeout: 5000 }`. - [x] Write `ability-builder.test.ts` — fluent builder, complex ability, override ordering. - [x] Write `ability.md`, `ability-builder.md`. --- ## 8. Core — PermissionManager - [x] Implement `PermissionManager(adapter, options?)`: - `hasPermission(userId, resource, action, context?)` — loads user roles, expands inheritance, evaluates conditions via `DefaultConditionMatcher`; consults `PermissionCache` if wired. - `getUserPermissions(userId, siteId?)` — resolves full effective permission set including inherited roles. - Emit `permission:check` event on `bus` after evaluation. - Instrument with `permissions.check` span and histogram. - Accept optional `cache?: PermissionCache`, `bus?: EventBusLike`, `logger?: LoggerLike`, `clock?: Clock`. - [x] Write `permission-manager.test.ts` (≤500 lines) — CRUD ops, `hasPermission` yes/no/conditions/inheritance, cache hit/miss, event emission, logger calls. `{ timeout: 5000 }` / `{ timeout: 30000 }` as appropriate. - [x] Write `permission-manager.md`. --- ## 9. Core — RoleManager - [x] Implement `RoleManager(adapter, options?)`: - `getRoleInheritanceChain(roleId)` — DFS traversal; throws `PermissionsError` with `PERM_CIRCULAR_INHERITANCE` on cycle detection. - `assignRole` / `revokeRole` emits `permission:grant` / `permission:revoke` events on `bus`. - Guard `updateRole` / `deleteRole` against `isSystem === true` roles; throw `PERM_SYSTEM_ROLE_MUTATION`. - Accept optional `bus?: EventBusLike`, `logger?: LoggerLike`, `clock?: Clock`. - [x] Write `role-manager.test.ts` — create/update/delete roles, inheritance chain, cycle detection, system-role guard, assignment/revocation, event emission. Each `it` has `{ timeout: 5000 }`. - [x] Write `role-manager.md`. --- ## 10. Core — PolicyEngine - [x] Implement `PolicyEngine(permissionManager, options?)`: - `evaluate(userId, resource, action, context?)` — delegates to `PermissionManager.hasPermission`; returns `PolicyResult` with `matchedRule`, `reason`, `conditions`. - `evaluateMany(requests)` — runs all in parallel with `Promise.allSettled`. - Instrument with `permissions.check` span. - [x] Write `policy-engine.test.ts` — single and bulk evaluation, denied/allowed paths, condition-driven. Each `it` has `{ timeout: 5000 }`. - [x] Write `policy-engine.md`. --- ## 11. PermissionCache - [x] Implement `PermissionCache(options)`: - LRU eviction: when `maxSize` is reached, evict the oldest entry. - TTL eviction: entries older than `options.ttl` ms return `undefined` on `get` (lazy check via injectable `clock`). - `invalidate(userId)` removes all entries whose key starts with `userId:`. - `invalidateAll()` clears the entire cache. - [x] Write `permission-cache.test.ts` — get/set, TTL expiry with FakeClock, LRU eviction, invalidate userId, invalidateAll, size getter. Each `it` has `{ timeout: 5000 }`. - [x] Write `permission-cache.md`. --- ## 12. Middleware - [x] Implement `createPermissionMiddleware(options)` — framework-agnostic; include `createHonoAdapter` bridge for Hono consumers. - [x] Implement standalone `checkPermission(userId, resource, action, adapter)` helper. - [x] Write `permission-middleware.test.ts` — allowed/denied paths, user extractor, resource extractor. Each `it` has `{ timeout: 5000 }`. - [x] Write `permission-middleware.md`. --- ## 13. Presets - [x] Implement `createAdminRole`, `createViewerRole`, `createEditorRole`, `createOwnerRole` and `CRUD_PERMISSIONS` constant. - [x] Write `role-presets.test.ts` — verify preset permissions match expected access pattern. Each `it` has `{ timeout: 5000 }`. - [x] Write `role-presets.md`. --- ## 14. Events - [x] Define `PermissionEventMap` interface and `permissionEvents` singleton (re-exported `EventBus`). - [x] Write `permission-events.md`. --- ## 15. OTel instrumentation (cross-cutting review) - [x] Confirm all spans, counters, and histograms from `proposal.md §4` are wired. - [x] Add `tests/otel.test.ts` — in-memory exporter; assert `permissions.check` histogram records on `evaluate`, counters increment on assign/revoke, cache counters increment on hit/miss. --- ## 16. Property-based tests (`tests/property.test.ts`) Use `fast-check`. All test timeouts `{ timeout: 30000 }`. - [x] **Ability consistency**: for any action/resource pair, `cannot(a, r)` iff `!can(a, r)`. - [x] **Role inheritance transitivity**: three-level chain; `getUserPermissions` contains all inherited permissions. - [x] **Cache invalidation completeness**: after `invalidate(userId)`, `get(userId + ':' + any_suffix)` returns `undefined`. - [x] **Condition evaluation determinism**: `compareValues` returns the same result for identical inputs across multiple calls and arbitrary ordering. - [x] **PolicyEngine bulk consistency**: `evaluateMany(requests)` returns same results as sequential `evaluate` calls. --- ## 17. Benchmarks (`src/__benchmarks__/permissions.bench.ts`) Use `mitata`. - [x] `hasPermission_no_cache`: 10 000 checks, no-cache path, MemoryAdapter with 20 permissions. - [x] `hasPermission_cached`: 10 000 checks, warm PermissionCache. - [x] `getRoleInheritanceChain_deep5`: 10 000 calls, 5-level deep inheritance tree. - [x] `ability_can_50rules`: 100 000 `Ability.can` calls on a 50-rule `Ability`. - [x] Document placeholder numbers in `README.md` under `## Benchmarks`. --- ## 18. Docs Per-file `.md` (all ≥200 words): - [x] `src/types/permissions.types.md` - [x] `src/adapters/adapter-interface/adapter-interface.md` - [x] `src/adapters/memory-adapter/memory-adapter.md` - [x] `src/cache/permission-cache/permission-cache.md` - [x] `src/conditions/condition-matcher/condition-matcher.md` - [x] `src/conditions/field-matcher/field-matcher.md` - [x] `src/conditions/subject-detector/subject-detector.md` - [x] `src/core/ability/ability.md` - [x] `src/core/ability-builder/ability-builder.md` - [x] `src/core/permission-manager/permission-manager.md` - [x] `src/core/policy-engine/policy-engine.md` - [x] `src/core/role-manager/role-manager.md` - [x] `src/core/rule/rule.md` - [x] `src/middleware/permission-middleware/permission-middleware.md` - [x] `src/presets/role-presets/role-presets.md` - [x] `src/schemas/permissions.schemas.md` - [x] `src/events/permission-events.md` Root README: - [x] Write `universal/auth/permissions/README.md`: purpose, install, quick-start (AbilityBuilder + PolicyEngine), modules table, benchmarks placeholder. --- ## 19. Verify and publish - [x] Run `bunx turbo lint --filter=@suaveplan/permissions` — zero errors. - [x] Run `bunx turbo typecheck --filter=@suaveplan/permissions` — zero errors. - [x] Run `bunx turbo test --filter=@suaveplan/permissions` — all tests pass. - [x] Run `bunx turbo test:coverage --filter=@suaveplan/permissions` — 100/100/100/100. - [x] Run `bun run docs:check --filter=@suaveplan/permissions` — zero errors. - [x] Confirm no `TODO`, `FIXME`, `@deprecated`, untyped `any`, `ts-ignore`, or `.skip` in `src/` or `tests/`. - [x] Confirm `"sideEffects": false` in `package.json`. - [x] Run `bun scripts/publish.ts --package @suaveplan/permissions`. - [x] Update `CLAUDE.md` package listing to include `@suaveplan/permissions`. - [x] Archive: move `openspec/changes/enterprise-permissions/` to `openspec/archive/2026-04-24-enterprise-permissions/` and fold spec delta into `openspec/specs/permissions/spec.md`.