133 lines
12 KiB
Markdown
133 lines
12 KiB
Markdown
# Tasks — Permissions completion
|
||
|
||
> Every checkbox MUST reflect work actually done in this change. Do not retroactively tick boxes for work that was already in main when this change opened. Do not tick a box for a test that doesn't exist or doesn't pin the requirement it claims to verify.
|
||
|
||
## 0. Pre-flight (non-negotiable)
|
||
|
||
- [ ] 0.1 Confirm clean working tree (`git status` reports nothing) and no in-flight worktrees with uncommitted work (`git worktree list` then `git status` in each)
|
||
- [ ] 0.2 If any worktrees exist under `.claude/worktrees/`, account for each one: still-active agent, work-pending-merge, or stale-and-prune. Stale worktrees MUST be removed (`git worktree remove`) before opening a new one — code that has been lost twice on main is the failure mode this guards against.
|
||
- [ ] 0.3 Confirm the package set in `MANIFEST.md` matches `find packages -maxdepth 3 -name package.json` count. Drift here means a prior change archived without regenerating the manifest.
|
||
|
||
## 1. Spec authoring
|
||
|
||
- [ ] 1.1 Read `../suaveplan/packages/universal/permissions/src/presets/default-preset/default-preset.ts` and `default-preset.types.ts` end-to-end
|
||
- [ ] 1.2 Read `../suaveplan/packages/universal/permissions/src/permission-events.ts` end-to-end
|
||
- [ ] 1.3 Read `../suaveplan/packages/universal/permissions/src/utils/deep-get.ts` and `utils/pattern-matching.ts` end-to-end
|
||
- [ ] 1.4 Read genesis `packages/auth/permissions/src/{core,conditions,adapters,events,presets}` to confirm what exists
|
||
- [ ] 1.5 Read `openspec/AGENTS.md` Sections 13 (spec-conformance) and 14 (source mapping)
|
||
- [ ] 1.6 Draft `proposal.md` with explicit ADDED / MODIFIED lists and a `## Source mapping` table citing upstream line ranges
|
||
- [ ] 1.7 Draft `specs/permissions/spec.md` with an `## ADDED Requirements` section per gap and `## MODIFIED Requirements` for the three behavior extensions
|
||
- [ ] 1.8 Every requirement carries a `**Verified by:**` line naming concrete `it(...)` titles
|
||
- [ ] 1.9 `openspec change validate permissions-completion` exits with no errors
|
||
|
||
## 2. `createPermissions` factory + `PermissionsInstance`
|
||
|
||
- [ ] 2.1 Create `packages/auth/permissions/src/presets/default-preset/default-preset.types.ts` with `PresetOptions<TAction, TSubject, TConditions>` (overrides for `conditionMatcher`, `fieldMatcher`, `subjectDetector`, `adapter`, `builder`)
|
||
- [ ] 2.2 Add `PermissionsInstance<TAction, TSubject, TConditions>` interface with readonly fields `ability`, `builder`, `adapter`, `conditionMatcher`, `fieldMatcher`, `subjectDetector`
|
||
- [ ] 2.3 Port `createPermissions` from upstream `default-preset.ts` L60-L97 into `packages/auth/permissions/src/presets/default-preset/default-preset.ts`. Preserve generic parameters (`TAction extends string = DefaultAction`, etc.)
|
||
- [ ] 2.4 Wire `MemoryAdapter`, `AbilityBuilder`, `DefaultConditionMatcher`, `DefaultFieldMatcher`, `DefaultSubjectDetector` defaults; honour every override from `PresetOptions`
|
||
- [ ] 2.5 `Object.freeze` the returned `PermissionsInstance` so callers can't mutate the matcher slot post-creation
|
||
- [ ] 2.6 Add subpath export `./presets/default-preset/default-preset` in `package.json`
|
||
- [ ] 2.7 Re-export `createPermissions`, `PermissionsInstance`, `PresetOptions` from `src/index.ts`
|
||
- [ ] 2.8 Sibling `default-preset.md` (≥200 words, basic + advanced examples)
|
||
|
||
## 3. `permissionEvents` singleton
|
||
|
||
- [ ] 3.1 Modify `packages/auth/permissions/src/events/permission-events.ts` to export a module-scope `permissionEvents` singleton typed as `EventBusLike<PermissionEventMap>` from `@suaveplan/events`
|
||
- [ ] 3.2 Use the genesis-canonical `createEventBus({ name: "permissions" })` factory (not the upstream custom `EventEmitter` class)
|
||
- [ ] 3.3 Update `core/permission-manager/permission-manager.ts` to default to `permissionEvents` when no bus is passed via constructor options
|
||
- [ ] 3.4 Update `core/role-manager/role-manager.ts` likewise
|
||
- [ ] 3.5 Update `core/policy-engine/policy-engine.ts` to emit `permission:checked` through the singleton
|
||
- [ ] 3.6 Add subpath export `./events/permission-events` in `package.json`
|
||
- [ ] 3.7 Re-export `permissionEvents` from `src/index.ts`
|
||
- [ ] 3.8 Sibling `permission-events.md` updated to document the singleton (≥200 words, basic listener + advanced filter-by-userId examples)
|
||
|
||
## 4. `deepGet` / `deepHas` utilities
|
||
|
||
- [ ] 4.1 Create `packages/auth/permissions/src/utils/deep-get/deep-get.ts` with `deepGet(obj, path)` and `deepHas(obj, path)`
|
||
- [ ] 4.2 Support dot-notation segments (`user.profile.role`)
|
||
- [ ] 4.3 Support numeric array-index segments (`tags.0` resolves `obj.tags[0]`); detected by `/^\d+$/.test(segment)`
|
||
- [ ] 4.4 Return `undefined` from `deepGet` (and `false` from `deepHas`) when any segment is missing or `current` is `null` / `undefined` / non-object
|
||
- [ ] 4.5 Update `conditions/condition-matcher/condition-matcher.ts` `DefaultConditionMatcher.match(conditions, context)` to use `deepGet(context, key)` instead of `context[key as keyof typeof context]`
|
||
- [ ] 4.6 Add subpath export `./utils/deep-get/deep-get` in `package.json`
|
||
- [ ] 4.7 Re-export `deepGet`, `deepHas` from `src/index.ts`
|
||
- [ ] 4.8 Sibling `deep-get.md` (≥200 words, basic + advanced array-index example)
|
||
|
||
## 5. `matchPattern` / `patternToRegex` utilities
|
||
|
||
- [ ] 5.1 Create `packages/auth/permissions/src/utils/pattern-match/pattern-match.ts` with `matchPattern(input, pattern)` and `patternToRegex(pattern)`
|
||
- [ ] 5.2 Patterns: `*` and `"manage"` match everything; `foo*` is prefix match; `*foo` is suffix match; `foo*bar` is prefix + suffix; otherwise exact equality
|
||
- [ ] 5.3 Escape regex special characters in non-wildcard portions before compiling: `.`, `+`, `?`, `^`, `$`, `{`, `}`, `(`, `)`, `|`, `[`, `]`, `\`
|
||
- [ ] 5.4 Cache compiled `RegExp` in a module-scope `Map<string, RegExp>` keyed by pattern string
|
||
- [ ] 5.5 LRU-evict at 1024 entries (insertion-order eviction via `Map.keys().next().value`)
|
||
- [ ] 5.6 Update `conditions/field-matcher/field-matcher.ts` `DefaultFieldMatcher.match(pattern, field)` to delegate to `matchPattern(field, pattern)` (keeping the existing exact-match fast path before pattern parse)
|
||
- [ ] 5.7 Update `core/ability/ability.ts` `Ability.can` action / subject lookup to delegate to `matchPattern` so registered wildcard rules (e.g. `can("read*", "Article")`) are honoured
|
||
- [ ] 5.8 Add subpath export `./utils/pattern-match/pattern-match` in `package.json`
|
||
- [ ] 5.9 Re-export `matchPattern`, `patternToRegex` from `src/index.ts`
|
||
- [ ] 5.10 Sibling `pattern-match.md` (≥200 words, basic + advanced example showing both prefix and suffix wildcards)
|
||
|
||
## 6. Tests
|
||
|
||
- [ ] 6.1 `presets/default-preset/default-preset.test.ts` — destructure-and-use; matcher overrides honoured; adapter override honoured; multiple instances are independent (changing one's matcher does not affect the other)
|
||
- [ ] 6.2 `presets/default-preset/default-preset.spec-conformance.test.ts` — `expectTypeOf` checks generics flow through; result is `Object.isFrozen`; `PresetOptions` accepts every documented override
|
||
- [ ] 6.3 `events/permission-events.test.ts` — listener registration; payload shapes for all 5 event names; manager auto-emits `permission:checked` and `permission:granted` when no bus passed
|
||
- [ ] 6.4 `events/permission-events.spec-conformance.test.ts` — re-importing the module returns the same `permissionEvents` instance; `EventBusLike<PermissionEventMap>` contract pinned (`expectTypeOf<typeof permissionEvents>().toMatchTypeOf<EventBusLike<PermissionEventMap>>()`)
|
||
- [ ] 6.5 `utils/deep-get/deep-get.test.ts` — dot-path resolution at depth 3; array-index resolution; missing segment returns `undefined`; null-safe traversal; `deepHas` reports correctly
|
||
- [ ] 6.6 `utils/deep-get/deep-get.spec-conformance.test.ts` — `deepHas(obj, p) === (deepGet(obj, p) !== undefined)` for every test fixture
|
||
- [ ] 6.7 `utils/pattern-match/pattern-match.test.ts` — exact match; `*` wildcard; `manage` wildcard; `foo*` prefix; `*foo` suffix; `foo*bar` both-side; special-char input must not be matched accidentally (e.g. `.` in input)
|
||
- [ ] 6.8 `utils/pattern-match/pattern-match.spec-conformance.test.ts` — RegExp cache: same pattern returns same RegExp instance; cache evicts at 1024 entries (insertion-order)
|
||
- [ ] 6.9 `conditions/condition-matcher/condition-matcher.test.ts` — extend with dot-path keys (`user.profile.role` resolves through `deepGet`)
|
||
- [ ] 6.10 `conditions/field-matcher/field-matcher.test.ts` — extend with wildcard-pattern matching (`field*` matches `fieldName`; `*Name` matches `firstName`)
|
||
- [ ] 6.11 `core/ability/ability.test.ts` — extend with wildcard-action matching (`can("read*", "Article")` matches `ability.can("read", "Article")` and `ability.can("readMeta", "Article")`)
|
||
- [ ] 6.12 Every `it(...)` in 6.1–6.11 carries `{ timeout: 5000 }`
|
||
|
||
## 7. Documentation
|
||
|
||
- [ ] 7.1 Sibling `.md` for every new implementation `.ts` (≥200 words, basic + advanced examples, full API reference)
|
||
- [ ] 7.2 Update `packages/auth/permissions/README.md` to link the four new co-located `.md` files under `## Modules`
|
||
- [ ] 7.3 Update `CLAUDE.md` package listing for `@suaveplan/permissions` to reflect the four new exports
|
||
|
||
## 8. Pre-archive verification (the 5-gate triple)
|
||
|
||
- [ ] 8.1 `bun run check` — Biome 0/0/0 across the whole monorepo
|
||
- [ ] 8.2 `bunx turbo lint typecheck test --filter=@suaveplan/permissions` — green
|
||
- [ ] 8.3 `bunx turbo test:coverage --filter=@suaveplan/permissions` — 100/100/100/100
|
||
- [ ] 8.4 `bun scripts/openspec-conformance.ts @suaveplan/permissions` — exit 0, no `error` findings
|
||
- [ ] 8.5 `bunx turbo build --filter=@suaveplan/permissions` — green
|
||
|
||
## 9. Pre-archive 7-row checklist (AGENTS.md Section 13)
|
||
|
||
- [ ] 9.1 Generics threaded; `expectTypeOf` assertions on `PermissionsInstance<TAction, TSubject, TConditions>` and `PresetOptions<...>`
|
||
- [ ] 9.2 Zod schemas wired at every declared boundary — `PresetOptions` validated at `createPermissions` entry; `permissionEvents` payloads typed against `PermissionEventMap`
|
||
- [ ] 9.3 Every existing error code in `errors/permission-error.ts` still thrown by at least one production path (no orphans introduced by the refactor)
|
||
- [ ] 9.4 Strict-mode behaviors tested both on and off (e.g. `Ability.can` strict-action mode vs. wildcard mode)
|
||
- [ ] 9.5 OTel instruments emitted from production paths and asserted with a real in-memory `MeterProvider`: `permissions.checks_total`, `permissions.grants_total`, `permissions.denies_total`, `permissions.event_emit_count`
|
||
- [ ] 9.6 Lifecycle dedup tested where claimed: re-importing `events/permission-events.ts` returns the same singleton; `patternToRegex` cache reuse
|
||
- [ ] 9.7 No retroactive tick — every box above reflects work done in this change
|
||
|
||
## 10. Archive
|
||
|
||
- [ ] 10.1 Fold `specs/permissions/spec.md` into `openspec/specs/permissions/spec.md` (creating the cap file — no `permissions` cap exists in `openspec/specs` today)
|
||
- [ ] 10.2 Move `openspec/changes/permissions-completion/` → `openspec/archive/<yyyy-mm-dd>-permissions-completion/`
|
||
- [ ] 10.3 Refresh `MANIFEST.md` (no package set or dep changes expected, but rerun for safety)
|
||
- [ ] 10.4 Update `STATUS.md` with the rolled-up gap closure for the four enumerated features
|
||
|
||
## Audit Note (2026-05-02)
|
||
|
||
Audit verdict: **DEFERRED — 0/74 tasks evidenced in code**.
|
||
|
||
Evidence reviewed:
|
||
|
||
- `src/index.ts` re-exports the existing surface (Ability, AbilityBuilder, PermissionManager, RoleManager, PolicyEngine, MemoryAdapter, role presets) but does NOT export `createPermissions`, `permissionEvents`, `deepGet`, `deepHas`, `matchPattern`, or `patternToRegex`.
|
||
- `src/utils/` does not exist; no `pattern-match/` or `deep-get/` directories.
|
||
- `src/presets/` contains only `role-presets/`; no `default-preset/` (and therefore no `createPermissions` factory + frozen `PermissionsInstance`).
|
||
- `events/permission-events.ts` exports a type only (`PermissionEventMap`), not the `permissionEvents` singleton wired through `@suaveplan/events`.
|
||
|
||
Missing pieces:
|
||
|
||
1. `presets/default-preset/default-preset.ts` (`createPermissions` factory returning a frozen `PermissionsInstance`).
|
||
2. `events/permission-events.ts` runtime export of the singleton `EventBusLike<PermissionEventMap>`, plus auto-wiring through PermissionManager / RoleManager / PolicyEngine.
|
||
3. `utils/deep-get/` and `utils/pattern-match/` (with cache eviction at 1024 entries) and the integrations in `condition-matcher`, `field-matcher`, and `Ability.can` wildcard handling.
|
||
|
||
Decision: leave change open. The four feature blocks (factory, events singleton, deep-get, pattern-match) are independent additions, each with its own tests, docs, and OTel instruments — not a tick-and-archive job.
|