4.0 KiB
4.0 KiB
Tasks: enterprise-react-permissions
Change ID: enterprise-react-permissions
Package: @suaveplan/react-permissions
Target path: react/auth/react-permissions
Prerequisite packages on Verdaccio: @suaveplan/error, @suaveplan/types
0. Pre-flight (non-negotiable)
- 0.1 Confirm clean working tree (
git statusreports nothing) and no in-flight worktrees with uncommitted work (git worktree listthengit statusin 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.mdmatchesfind packages -maxdepth 3 -name package.jsoncount. Drift here means a prior change archived without regenerating the manifest.
1. Scaffold
- Create directory tree per proposal layout.
- Write
package.jsonat0.1.0with"sideEffects": false, exports map (per-component subpaths). - Dependencies:
@suaveplan/error workspace:^,@suaveplan/types workspace:^,@opentelemetry/api catalog:observability,tslib catalog:build. - Write
tsconfig.json,vitest.config.ts, scripts.
2. Error codes and types
- Define
PermissionErrorextendingSuaveplanError; codes:PERMISSION_CONTEXT_MISSING,PERMISSION_DENIED. - Define
PermissionCheckResult:{ allowed: boolean; reason?: string }. - Define
CanProps,PermissionBoundaryProps,RoleGuardProps,AbilityProviderProps.
3. AbilityContext + createAbilityContext
createAbilityContext<TAbility>()— returns{ AbilityContext, AbilityProvider, useAbilityContext }for a given ability type.- Module-level default context created at import time.
useAbilityContext()throwsPermissionError(PERMISSION_CONTEXT_MISSING)outside provider.- Write
ability-context.test.tsx: provider renders, context consumed, missing throws. SSR test: render to string, no window errors.{ timeout: 5000 }. - Write
ability-context.md.
4. Hooks
useAbility
- Returns current
TAbilityinstance from context. Write test + md.
useCan
useCan(action, subject): PermissionCheckResult— delegates toability.can(action, subject). Emits OTelpermission.deniedevent whenallowed === false. Write test (allowed + denied cases) + md.
usePermissionCheck
usePermissionCheck(checks: Array<{action, subject}>): PermissionCheckResult[]— bulk check. Write test + md.
5. Components
Can
<Can action="read" subject="Post">renders children if allowed; rendersfallbackprop (default null) if denied. Emits OTel event on denial.- Write test (allowed/denied/fallback), stories (allowed, denied, with-fallback, playground), md.
PermissionBoundary
- Renders children if all specified permissions pass; renders
fallbackcomponent otherwise. - Write test, stories, md.
RoleGuard
<RoleGuard roles={['admin', 'editor']}>renders children if user has at least one role.- Write test, stories (single-role, multi-role, denied), md.
6. createContextualCan
- Factory returning a
<Can>-shaped component bound to a givenAbilityContext. - Write test (custom context) + md.
7. E2E
e2e/permissions.spec.ts— Playwright:- Render
<Can action="read" subject="Post">with allowed ability — children visible. - Render with denied ability — children hidden, fallback shown.
- Render
<RoleGuard roles={['admin']}>with non-admin user — children hidden.
- Render
8. Verify and publish
bunx turbo lint typecheck test --filter=@suaveplan/react-permissions— green.bunx turbo test:coverage --filter=@suaveplan/react-permissions— 100%.bun scripts/publish.ts --package @suaveplan/react-permissions.- Archive change folder.