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

83 lines
7.9 KiB
Markdown

# Add @suaveplan/react-auth — core (Wave 1)
**Package**: `@suaveplan/react-auth`
**Status**: PROPOSED
**Type**: new-package
- **Spec delta**: ./specs/react-auth/spec.md
## Why
Genesis needs a React Better-Auth integration so downstream apps can authenticate users, gate routes, and check permissions. The upstream package at `../suaveplan/packages/react/react-auth/` ships a typed `AuthClient` wrapper around `better-auth/client`, an `AuthProvider` context, `AuthGuard` route gate, `useAuthGuard`/`usePermission` hooks, a `betterAuthAdapter` bridge, locale packs for `en-GB`/`cy-GB`/`zh-CN`, plus full page and form surfaces. Wave 1 ships the foundational *core* — adapter, client, context, hooks, guard, translations data, types — so that downstream packages (the parallel `react-auth-whitelabel` and consumer apps) can compile and authenticate today. Forms (Wave 2) and pages (Wave 3) layer on top of this core without changing it.
## What changes
- ADDED: `AuthClient` + `createAuthClient(config)` — typed wrapper around `better-auth/client` with `x-site-domain` header propagation.
- ADDED: `AuthProvider` + `useAuth` — React context wrapping `AuthClient` with `signIn`/`signUp`/`signOut`/`updateProfile`/`changePassword`/`forgotPassword`/`resetPassword`/`verifyEmail`/`resendVerificationEmail`/`deleteAccount`/`refreshSession` actions and `{ user, session, isLoading, isAuthenticated, error }` state.
- ADDED: `AuthGuard` component — declarative `requireAuth` / `requireGuest` / `requireVerified` / `allowedRoles` gate with `loadingComponent` + `fallback` slots.
- ADDED: `useAuthGuard(options)` hook — imperative `react-router-dom` redirect counterpart to `AuthGuard`.
- ADDED: `usePermission()` hook — bitwise permission check (16 flags) against `user.roles`.
- ADDED: `transformSessionResponse` / `transformAuthResponse` / `transformGenericResponse` / `transformUpdateProfileResponse` / `isBetterAuthError` / `hasBetterAuthData` — better-auth response adapter functions.
- ADDED: Locale packs `enGb` / `cyGb` / `zhCn` and aggregate `authTranslations` (Wave 2 forms consume these).
- ADDED: `User` / `Session` / `Account` / `LoginCredentials` / `RegisterData` / `ResetPasswordData` / `ResetPasswordCredentials` / `UpdateProfileData` / `ChangePasswordData` / `AuthResponse` / `AuthError` (`auth.types.ts`) and `Site` / `UserSettings` / `BankAccount` / `Friend` / `Group` / `Post` / `Message` / `Notification` / `Event` / `Role` / `DeviceToken` / `PushNotification` (`crud.types.ts`).
- ADDED: `REACT_AUTH_CODES` registry (10 stable codes) + `ReactAuthError` base class + 10 typed subclasses.
- ADDED: OTel instruments under meter `suaveplan.react-auth` — counters for sign-in/sign-up/sign-out/guard-allow/guard-deny, histogram for session-fetch duration.
## Out of scope
- Form components (`LoginForm`, `RegisterForm`, `ForgotPasswordForm`, `ResetPasswordForm`, `EmailVerifyForm`) — deferred to Wave 2.
- Page components (`Login`, `Register`, `ForgotPassword`, `ResetPassword`, `EmailVerify`, `Profile`) and `PermissionGate` — deferred to Wave 3.
### Excluded from this migration
- **Form components** (`login-form`, `register-form`, `forgot-password-form`, `reset-password-form`, `email-verify-form`) — Deferred to Wave 2 — scope-bounded migration to keep ship velocity at 100/100/100/100 quality.
- **Page components** (`login`, `register`, `forgot-password`, `reset-password`, `email-verify`, `profile`) and `PermissionGate` — Deferred to Wave 3 — scope-bounded migration to keep ship velocity at 100/100/100/100 quality.
## Source mapping
This change derives from `../suaveplan/enterprise/react-auth.md` and `../suaveplan/packages/react/react-auth/src/`.
| Requirement | Source lines | Notes |
|---|---|---|
| `AuthClient` wraps `better-auth/client` | `client/auth-client.ts:1-237` | Reworded: `@ts-expect-error` on `forgetPassword` replaced with conditional dispatch through an optional-method lookup, removing the suppression. |
| `AuthProvider` context + 11 actions | `context/auth-context.tsx:1-435` | Verbatim semantics; replaced loose `(error as Error)` casts with `instanceof Error ? error : new Error(String(error))` to satisfy `useThrowOnlyError`. |
| `AuthGuard` declarative gate | `components/auth-guard/auth-guard.tsx:1-69` | Verbatim. |
| `useAuthGuard` imperative gate | `hooks/use-auth-guard.ts:1-86` | Verbatim. |
| `usePermission` bitwise hook | `hooks/use-permission.ts:1-88` | Verbatim. |
| `betterAuthAdapter` response transforms | `lib/better-auth-adapter.ts:1-215` | Verbatim; `any` payloads narrowed via `unknown` + structural type-guards. |
| Locale packs (`en-GB`, `cy-GB`, `zh-CN`) | `translations/{en-gb,cy-gb,zh-cn}.ts` | Verbatim string tables. |
| Auth + CRUD types | `types/{auth.types,crud.types}.ts` | Verbatim shapes. |
### Requirements with no source line
| Requirement | Justification |
|---|---|
| `REACT_AUTH_CODES` error-code registry + `ReactAuthError` subclasses | Genesis-wide invariant (`@suaveplan/error` `defineCodes`); upstream threw plain `Error`. Audit `enterprise/react-auth.md §3` flagged loss of programmatic handling and OTel tagging. |
| OTel instruments under `suaveplan.react-auth` | Genesis-wide invariant (every package emits structured metrics). |
| Zod validation at `createAuthClient` config + `useAuthGuard` options + `AuthGuardProps` boundary | Genesis rule §0a — Zod boundary on every public option bundle. |
| `FetchLike` injection seam at `AuthClient` construction | Required by the testing rule "no mocks; real services or injectable boundaries". |
## Verification approach
- `src/client/auth-client.test.ts` — covers `AuthClient` construction, `siteDomain` header propagation, every wrapper method's call shape, session-list date normalisation.
- `src/lib/better-auth-adapter.test.ts` — covers all 6 transform functions over success/error/empty payloads.
- `src/context/auth-context.test.tsx` — covers `AuthProvider` initial-session, all 11 actions, error state, `skipInitialCheck`, `onAuthStateChange` notification, unmount cleanup, `useAuth` outside-provider throw.
- `src/hooks/use-auth-guard.test.tsx` — covers all 4 redirect paths (auth, guest, verified, role) + `canAccess` matrix + loading bail-out.
- `src/hooks/use-permission.test.tsx` — covers `hasPermission` / `hasAnyPermission` / `hasAllPermissions` / `getPermissionNames` with string & numeric inputs, unauthenticated bail-outs.
- `src/components/auth-guard/auth-guard.test.tsx` — covers loading state, every grant/deny branch, custom `loadingComponent` + `fallback` slots, multi-child rendering.
- `src/components/auth-provider/auth-provider.test.tsx` — covers the re-export surface compiles to the canonical `AuthProvider` and type re-exports.
- `src/types/auth.types.test.ts` + `src/types/crud.types.test.ts` — `expectTypeOf` shape pins for every exported interface.
- `src/translations/translations.test.ts` — locale-pack key-parity across `en-gb` / `cy-gb` / `zh-cn`, default export shape.
- `src/error-codes/error-codes.test.ts` — every code thrown by at least one production path; error name + code field round-trip.
- `src/observability/react-auth-metrics.test.ts` — counters + histogram emit through a real in-memory `MeterProvider`.
## Migration impact
- `../suaveplan/packages/react/react-auth/src/components/auth-provider/auth-provider.types.ts` imports `PartialResources` + `SupportedLanguage` from `@suaveplan/react-translate`. Wave 1 does **not** consume these types at runtime (the legacy `AuthProviderProps.translationConfig` is a Wave-2 form concern); they are re-declared as local `TranslationConfig { defaultLanguage?: string; resources?: Record<string, unknown> }` so this Wave-1 surface compiles without an `@suaveplan/react-i18n` dependency. Wave 2 will tighten the type when it wires `useTranslation`.
## Impact
### Affected dependents
No dependents yet — this is a new package landing on a clean genesis branch. Verified by `bunx turbo ls --filter=...@suaveplan/react-auth` (empty downstream list).