Initial Commit
This commit is contained in:
448
openspec/archive/2026-04-24-enterprise-permissions/proposal.md
Normal file
448
openspec/archive/2026-04-24-enterprise-permissions/proposal.md
Normal file
@@ -0,0 +1,448 @@
|
||||
# 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<string, unknown>
|
||||
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<Permission>
|
||||
updatePermission(id: string, updates: Partial<Permission>): Promise<Permission>
|
||||
deletePermission(id: string): Promise<void>
|
||||
findPermission(id: string): Promise<Permission | null>
|
||||
listPermissions(filter?: PermissionFilter): Promise<Permission[]>
|
||||
// Roles
|
||||
createRole(role: Role): Promise<Role>
|
||||
updateRole(id: string, updates: Partial<Role>): Promise<Role>
|
||||
deleteRole(id: string): Promise<void>
|
||||
findRole(id: string): Promise<Role | null>
|
||||
listRoles(filter?: RoleFilter): Promise<Role[]>
|
||||
// User-role assignments
|
||||
assignRole(userRole: UserRole): Promise<UserRole>
|
||||
revokeRole(userId: string, roleId: string, siteId?: string): Promise<void>
|
||||
getUserRoles(userId: string, siteId?: string): Promise<UserRole[]>
|
||||
getRoleUsers(roleId: string, siteId?: string): Promise<string[]>
|
||||
}
|
||||
|
||||
// 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<Permission>
|
||||
updatePermission(id: string, updates: Partial<Permission>): Promise<Permission>
|
||||
deletePermission(id: string): Promise<void>
|
||||
findPermission(id: string): Promise<Permission | null>
|
||||
listPermissions(filter?: PermissionFilter): Promise<Permission[]>
|
||||
hasPermission(userId: string, resource: string, action: string, context?: Record<string, unknown>): Promise<boolean>
|
||||
getUserPermissions(userId: string, siteId?: string): Promise<Permission[]>
|
||||
}
|
||||
|
||||
class RoleManager {
|
||||
constructor(adapter: GenericPermissionAdapter, options?: RoleManagerOptions)
|
||||
createRole(role: Role): Promise<Role>
|
||||
updateRole(id: string, updates: Partial<Role>): Promise<Role>
|
||||
deleteRole(id: string): Promise<void>
|
||||
findRole(id: string): Promise<Role | null>
|
||||
listRoles(filter?: RoleFilter): Promise<Role[]>
|
||||
assignRole(userId: string, roleId: string, options?: RoleAssignOptions): Promise<UserRole>
|
||||
revokeRole(userId: string, roleId: string, siteId?: string): Promise<void>
|
||||
getUserRoles(userId: string, siteId?: string): Promise<Role[]>
|
||||
getRoleInheritanceChain(roleId: string): Promise<Role[]>
|
||||
}
|
||||
|
||||
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<string, unknown>): Promise<PolicyResult>
|
||||
evaluateMany(requests: PolicyRequest[]): Promise<PolicyResult[]>
|
||||
}
|
||||
|
||||
class Rule {
|
||||
constructor(resource: string, action: PermissionAction | string, conditions?: PermissionCondition[])
|
||||
matches(resource: string, action: string): boolean
|
||||
evaluateConditions(context: Record<string, unknown>): boolean
|
||||
}
|
||||
```
|
||||
|
||||
### Ability system (CASL-inspired)
|
||||
|
||||
```ts
|
||||
class Ability {
|
||||
constructor(rules: AbilityRule[])
|
||||
can(action: string, resource: string, context?: Record<string, unknown>): boolean
|
||||
cannot(action: string, resource: string, context?: Record<string, unknown>): 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<string, unknown>): boolean
|
||||
}
|
||||
|
||||
class DefaultConditionMatcher implements ConditionMatcher
|
||||
function compareValues(a: unknown, b: unknown, operator: ConditionOperator): boolean
|
||||
|
||||
interface FieldMatcher {
|
||||
get(context: Record<string, unknown>, field: string): unknown
|
||||
}
|
||||
|
||||
class DefaultFieldMatcher implements FieldMatcher
|
||||
function fieldMatches(context: Record<string, unknown>, 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<PermissionEventMap>
|
||||
```
|
||||
|
||||
### 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<string, Permission[]>
|
||||
```
|
||||
|
||||
### 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<PolicyResult>
|
||||
```
|
||||
|
||||
### 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<PermissionsErrorCode>
|
||||
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.
|
||||
248
openspec/archive/2026-04-24-enterprise-permissions/tasks.md
Normal file
248
openspec/archive/2026-04-24-enterprise-permissions/tasks.md
Normal file
@@ -0,0 +1,248 @@
|
||||
# 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<PermissionsErrorCode>`.
|
||||
|
||||
### 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<PermissionEventMap>`).
|
||||
- [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`.
|
||||
Reference in New Issue
Block a user