Files
suaveplan/openspec/archive/2026-04-24-enterprise-permissions/tasks.md
2026-05-27 23:29:53 +01:00

13 KiB

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

  • Create directory universal/auth/permissions/ with src/, tests/, src/__benchmarks__/, and all feature subdirectories per the layout in proposal.md.
  • 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:^
  • Write tsconfig.json extending @suaveplan/typescript-config/library.json.
  • Write vitest.config.ts using createTestConfig() from @suaveplan/testing/config (pool: forks).
  • 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)

  • 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.
  • Write permissions.types.md (≥200 words, API reference for every type).

2b. Error codes (src/error-codes.ts)

  • 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.
  • Define PERMISSIONS_ERROR_METADATA with httpStatus, severity, retryable, userMessageKey, category, description per code.
  • Export registerPermissionsErrorCodes() as idempotent eager side-effect.
  • Export PermissionsError extends SuaveplanError<PermissionsErrorCode>.

2c. Zod schemas (src/schemas/permissions.schemas.ts + .md)

  • Write Zod schemas: PermissionSchema, RoleSchema, UserRoleSchema, PermissionManagerOptionsSchema, RoleManagerOptionsSchema, PolicyEngineOptionsSchema, PermissionCacheOptionsSchema.
  • Write permissions.schemas.md.
  • Write permissions.schemas.test.ts — parse valid/invalid for each schema.

3. Adapter interface

  • Write src/adapters/adapter-interface/adapter-interface.ts — full GenericPermissionAdapter interface from proposal.md.
  • Write adapter-interface.types.ts with StoredPermission, StoredRole, StoredPolicy, StoredPolicyCondition, StoredUserRole.
  • Write adapter-interface.md.

4. MemoryAdapter

  • Implement MemoryAdapter using Map-backed storage for permissions, roles, and user-role assignments.
  • Support optional clock?: Clock for expiry checks on UserRole.expiresAt.
  • Filter expired user-roles on lookup (lazy removal — no setInterval).
  • Write memory-adapter.test.ts — CRUD for permissions/roles/assignments, expiry with FakeClock, list filters, edge cases (not found). Each it has { timeout: 5000 }.
  • Write memory-adapter.md.

5. Conditions system

5a. ConditionMatcher

  • Implement DefaultConditionMatcher and compareValues supporting all 11 operators from ConditionOperator.
  • Write condition-matcher.test.ts — each operator for string/number/array/null inputs, boundary values. Each it has { timeout: 5000 }.
  • Write condition-matcher.md.

5b. FieldMatcher

  • Implement DefaultFieldMatcher with dot-notation path traversal and fieldMatches helper.
  • Write field-matcher.test.ts — nested paths, missing fields, array index access. Each it has { timeout: 5000 }.
  • Write field-matcher.md.

5c. SubjectDetector

  • Implement DefaultSubjectDetector — infers resource type from __type property, constructor name, or explicit override.
  • Write subject-detector.test.ts.
  • Write subject-detector.md.

6. Core — Rule

  • Implement Rule with matches(resource, action) (exact + wildcard *) and evaluateConditions(context).
  • Write rule.test.ts — wildcard action, wildcard resource, condition evaluation, negative conditions. Each it has { timeout: 5000 }.
  • Write rule.md.

7. Core — Ability and AbilityBuilder

  • Implement Ability with can, cannot, rulesFor. Wildcard * on action or resource grants all.
  • Implement AbilityBuilder with fluent can / cannot methods; build() returns frozen Ability.
  • Implement buildAbility(fn) convenience factory.
  • Validate that cannot rules override can rules (deny-overrides semantics).
  • Write ability.test.ts — basic can/cannot, wildcard, conditions, rule precedence. Each it has { timeout: 5000 }.
  • Write ability-builder.test.ts — fluent builder, complex ability, override ordering.
  • Write ability.md, ability-builder.md.

8. Core — PermissionManager

  • 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.
  • 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.
  • Write permission-manager.md.

9. Core — RoleManager

  • 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.
  • 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 }.
  • Write role-manager.md.

10. Core — PolicyEngine

  • 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.
  • Write policy-engine.test.ts — single and bulk evaluation, denied/allowed paths, condition-driven. Each it has { timeout: 5000 }.
  • Write policy-engine.md.

11. PermissionCache

  • 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.
  • Write permission-cache.test.ts — get/set, TTL expiry with FakeClock, LRU eviction, invalidate userId, invalidateAll, size getter. Each it has { timeout: 5000 }.
  • Write permission-cache.md.

12. Middleware

  • Implement createPermissionMiddleware(options) — framework-agnostic; include createHonoAdapter bridge for Hono consumers.
  • Implement standalone checkPermission(userId, resource, action, adapter) helper.
  • Write permission-middleware.test.ts — allowed/denied paths, user extractor, resource extractor. Each it has { timeout: 5000 }.
  • Write permission-middleware.md.

13. Presets

  • Implement createAdminRole, createViewerRole, createEditorRole, createOwnerRole and CRUD_PERMISSIONS constant.
  • Write role-presets.test.ts — verify preset permissions match expected access pattern. Each it has { timeout: 5000 }.
  • Write role-presets.md.

14. Events

  • Define PermissionEventMap interface and permissionEvents singleton (re-exported EventBus<PermissionEventMap>).
  • Write permission-events.md.

15. OTel instrumentation (cross-cutting review)

  • Confirm all spans, counters, and histograms from proposal.md §4 are wired.
  • 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 }.

  • Ability consistency: for any action/resource pair, cannot(a, r) iff !can(a, r).
  • Role inheritance transitivity: three-level chain; getUserPermissions contains all inherited permissions.
  • Cache invalidation completeness: after invalidate(userId), get(userId + ':' + any_suffix) returns undefined.
  • Condition evaluation determinism: compareValues returns the same result for identical inputs across multiple calls and arbitrary ordering.
  • PolicyEngine bulk consistency: evaluateMany(requests) returns same results as sequential evaluate calls.

17. Benchmarks (src/__benchmarks__/permissions.bench.ts)

Use mitata.

  • hasPermission_no_cache: 10 000 checks, no-cache path, MemoryAdapter with 20 permissions.
  • hasPermission_cached: 10 000 checks, warm PermissionCache.
  • getRoleInheritanceChain_deep5: 10 000 calls, 5-level deep inheritance tree.
  • ability_can_50rules: 100 000 Ability.can calls on a 50-rule Ability.
  • Document placeholder numbers in README.md under ## Benchmarks.

18. Docs

Per-file .md (all ≥200 words):

  • src/types/permissions.types.md
  • src/adapters/adapter-interface/adapter-interface.md
  • src/adapters/memory-adapter/memory-adapter.md
  • src/cache/permission-cache/permission-cache.md
  • src/conditions/condition-matcher/condition-matcher.md
  • src/conditions/field-matcher/field-matcher.md
  • src/conditions/subject-detector/subject-detector.md
  • src/core/ability/ability.md
  • src/core/ability-builder/ability-builder.md
  • src/core/permission-manager/permission-manager.md
  • src/core/policy-engine/policy-engine.md
  • src/core/role-manager/role-manager.md
  • src/core/rule/rule.md
  • src/middleware/permission-middleware/permission-middleware.md
  • src/presets/role-presets/role-presets.md
  • src/schemas/permissions.schemas.md
  • src/events/permission-events.md

Root README:

  • Write universal/auth/permissions/README.md: purpose, install, quick-start (AbilityBuilder + PolicyEngine), modules table, benchmarks placeholder.

19. Verify and publish

  • Run bunx turbo lint --filter=@suaveplan/permissions — zero errors.
  • Run bunx turbo typecheck --filter=@suaveplan/permissions — zero errors.
  • Run bunx turbo test --filter=@suaveplan/permissions — all tests pass.
  • Run bunx turbo test:coverage --filter=@suaveplan/permissions — 100/100/100/100.
  • Run bun run docs:check --filter=@suaveplan/permissions — zero errors.
  • Confirm no TODO, FIXME, @deprecated, untyped any, ts-ignore, or .skip in src/ or tests/.
  • Confirm "sideEffects": false in package.json.
  • Run bun scripts/publish.ts --package @suaveplan/permissions.
  • Update CLAUDE.md package listing to include @suaveplan/permissions.
  • Archive: move openspec/changes/enterprise-permissions/ to openspec/archive/2026-04-24-enterprise-permissions/ and fold spec delta into openspec/specs/permissions/spec.md.