Files
2026-05-27 23:29:53 +01:00

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 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. Scaffold

  • Create directory tree per proposal layout.
  • Write package.json at 0.1.0 with "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 PermissionError extending SuaveplanError; 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() throws PermissionError(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 TAbility instance from context. Write test + md.

useCan

  • useCan(action, subject): PermissionCheckResult — delegates to ability.can(action, subject). Emits OTel permission.denied event when allowed === 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; renders fallback prop (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 fallback component 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 given AbilityContext.
  • 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.

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.