Initial Commit

This commit is contained in:
2026-05-27 23:29:53 +01:00
commit fbc70f96e6
10000 changed files with 1102152 additions and 0 deletions

View File

@@ -0,0 +1,197 @@
# OpenSpec Change: complete-react-auth
- **Change ID**: complete-react-auth
- **Package**: `@suaveplan/react-auth` (NEW)
- **Target path**: `packages/auth/react-auth/`
- **Tier**: browser (React 19 peer)
- **Status**: PROPOSED
- **Date**: 2026-05-17
- **Design**: ./design.md
- **Spec delta**: ./specs/react-auth/spec.md
- **Supersedes**: `openspec/changes/enterprise-react-auth/` (narrow OAuth-only scaffold) AND `openspec/changes/_superseded-narrow-deferrals-2026-05-17/add-react-auth-passkey-autofill/` (passkey-autofill-only follow-up). Both are folded in.
- **Upstream reference**: `/mnt/s/web/suaveplan/enterprise/react-auth.md`
- **Upstream source**: `/mnt/s/web/suaveplan/packages/react/react-auth/src/` (Better-Auth-bound; pages, forms, guards, three translation packs, E2E)
---
## Why
`@suaveplan/react-auth` is the React surface for **every** authentication interaction in the genesis stack. The two pre-existing proposals each scoped a single slice (OAuth context-only in `enterprise-react-auth`; conditional-UI passkey autofill in `add-react-auth-passkey-autofill`) and deliberately left every other auth flow — passwords, passkey registration & modal authentication, MFA, OAuth providers beyond a token exchange, session refresh, multi-tab sync, email/phone verification, error boundaries, accessibility, i18n — on a "follow-up" pile. The user has explicitly rejected that pattern: shipping one piece at a time produces a package that consumers cannot adopt without hand-rolling the rest, which defeats the purpose of a `react-auth` package existing.
This change lands the **entire intended React-auth surface in one shot** at enterprise quality (100% coverage, sibling `.md` per file, Storybook, Playwright, accessibility, SSR-safe, i18n-keyed). Every primitive the package needs already ships on genesis: `@suaveplan/webauthn` (register / authenticate / `isConditionalMediationAvailable`), `@suaveplan/oauth-client` (Authorization Code + PKCE, refresh, revoke, discovery, provider presets), `@suaveplan/srp` (zero-knowledge password proof), `@suaveplan/otp` (HOTP / TOTP / recovery codes), `@suaveplan/hibp` (k-anonymity breach check), `@suaveplan/password-strength` (zxcvbn-style estimator), `@suaveplan/password-generator`, `@suaveplan/error`, `@suaveplan/events`, `@suaveplan/logger`, `@suaveplan/i18n`, `@suaveplan/react-ui`. The React layer is the last missing piece.
Consumers (`@suaveplan/web-app`, downstream tenant apps) need a single import surface that wires those primitives into hooks and components: `<AuthProvider>` for context, hooks for every state read and every lifecycle mutation, components for the canonical UX (password input with strength + HIBP, passkey autofill field, passkey list with revoke, MFA enrollment, recovery codes, OAuth button, verification flows), an error boundary that turns typed `AuthError` codes into friendly i18n-keyed messages, multi-tab session sync via `BroadcastChannel`, and SSR-safe default behaviour everywhere. Shipping anything less forces every consumer to write the same wiring badly.
## What changes
### NEW package: `@suaveplan/react-auth`
Browser-tier React 19 package at `packages/auth/react-auth/`. Single import surface; every feature lives behind a tree-shakeable subpath export. No global state, no required server. Pure-client logic; the caller wires `AuthBackend` (transport adapter) to whatever server they run.
### Context + provider
- ADDED: `<AuthProvider>` — top-level provider. Accepts `AuthBackend` (transport), `StorageLike` (token + session persistence), `BroadcastChannel`-like factory (multi-tab sync), optional `IdleConfig`, optional `Logger`, optional `Clock`. Loads persisted session on mount; subscribes to `BroadcastChannel` for cross-tab forced-logout / refresh events; installs idle-timeout timer; never touches `window` at import time.
- ADDED: `AuthContext` + `useAuthContext()` — raw context handle; throws `REACT_AUTH.CONTEXT_MISSING` outside provider.
- ADDED: `AuthBackend` interface — every server call (password sign-in, sign-up, password-reset request + confirm, change password, email-verify request + confirm, phone-verify request + confirm, OAuth code exchange, OAuth refresh, OAuth revoke, passkey register options + finish, passkey assertion options + finish, list passkeys, revoke passkey, TOTP enroll begin + finish, TOTP verify, recovery-codes generate + verify, SRP exchange, session refresh, session revoke). Caller implements; the package never assumes a wire format.
### State + lifecycle hooks
- ADDED: `useAuth()` — full context value (state + every mutation).
- ADDED: `useUser()` — current `AuthUser | null`.
- ADDED: `useIsAuthenticated()` — boolean.
- ADDED: `useSession()` — `{ user, accessToken, expiresAt, refreshAt, status }`.
- ADDED: `useSignIn()`, `useSignUp()`, `useSignOut()` — async mutation hooks with `{ mutate, status, error, reset }`.
- ADDED: `useRefreshToken()` — manual refresh; the provider also auto-refreshes via timer at `refreshAt`.
### Passwords
- ADDED: `useChangePassword()`, `usePasswordReset()` (returns `requestReset(email)` + `confirmReset(token, newPassword)`).
- ADDED: `<PasswordInput>` — accessible input with show/hide toggle, optional integrated `<PasswordStrengthMeter>` and HIBP-pwned check (debounced, opt-in).
- ADDED: `<PasswordStrengthMeter>` — wraps `@suaveplan/password-strength` `assessReactive` with cancellation on unmount, renders a 5-bucket bar with i18n-keyed feedback strings.
- ADDED: `<HibpPwnedIndicator>` — wraps `@suaveplan/hibp` k-anonymity check (caller injects the client; debounced ≥ 500 ms; no network on SSR).
- ADDED: `<PasswordGeneratorButton>` — opens a `@suaveplan/password-generator` popover; emits chosen password via callback.
### Passkeys (WebAuthn)
- ADDED: `useRegisterPasskey()` — wraps `webauthn.register` with the typed options/result + error translation.
- ADDED: `useAuthenticateWithPasskey()` — wraps `webauthn.authenticate` (modal mediation: `"required"` or unset).
- ADDED: `usePasskeyAutofill()` — wraps `webauthn.authenticate({ mediation: "conditional" })` with the lifecycle from the superseded passkey-autofill design (SSR-gated, `isConditionalMediationAvailable` feature detect, composed `AbortController`, stable `abort` callback).
- ADDED: `<PasskeyAutofillField>` — `<input autocomplete="username webauthn">` wrapper, blur-cancels when no credential delivered.
- ADDED: `<PasskeyList>` — lists the user's registered authenticators with friendly name, last-used timestamp, and a per-row revoke button (accessible confirm dialog).
### OAuth
- ADDED: `useOAuthSignIn()` — `signIn({ provider, mode: "popup" | "redirect", scopes? })`. Popup mode opens a centred window, waits for `postMessage` from the callback page, exchanges code → tokens via the backend, closes the popup. Redirect mode persists return-state and calls `window.location.assign`.
- ADDED: `<OAuthProviderButton>` — accessible button with provider-branded icon + i18n-keyed label; uses `useOAuthSignIn` under the hood.
- ADDED: `OAuthProviderRegistry` — name-keyed registry of `OAuthProviderDescriptor` (label key, icon, scopes). Caller registers providers at provider-setup time.
- ADDED: `handleOAuthCallback()` — page-level helper for the redirect-callback URL; parses query, validates state, exchanges code, posts result back to the opener (popup mode) or persists session + navigates (redirect mode).
### MFA
- ADDED: `useEnrollTotp()` — `begin()` returns secret + provisioning URI (caller renders QR via `@suaveplan/qrcode`); `finish(code)` verifies.
- ADDED: `useVerifyTotp()` — sign-in second factor.
- ADDED: `useEnrollWebauthnSecondFactor()` — re-uses `webauthn.register` with the `"cross-platform" | "platform"` selector.
- ADDED: `useRecoveryCodes()` — `generate()`, `verify(code)`.
- ADDED: `<TotpEnrollmentPanel>` — orchestrates secret display + code entry + recovery-codes reveal in one accessible flow.
- ADDED: `<RecoveryCodesPanel>` — accessible list with copy-to-clipboard + download-as-text + acknowledge-saved checkbox before dismiss.
### Email / phone verification
- ADDED: `useEmailVerification()` — `request()`, `confirm(token)`.
- ADDED: `usePhoneVerification()` — `request()`, `confirm(code)`.
- ADDED: `<EmailVerificationView>` — renders the "check your inbox" UX with resend cooldown timer.
- ADDED: `<PhoneVerificationView>` — code-entry input optimised for mobile (numeric, autocomplete=`one-time-code`).
### Session lifecycle
- ADDED: token persistence via injected `StorageLike` (memory / `localStorage` / `sessionStorage` adapters bundled).
- ADDED: auto-refresh — provider installs a timer firing at `refreshAt = expiresAt - skewMs` via injected `Clock`.
- ADDED: multi-tab sync — provider opens a `BroadcastChannel("@suaveplan/react-auth")` (factory injectable for SSR / test); emits `signed-in`, `signed-out`, `session-refreshed`, `forced-logout`; receivers update local state synchronously.
- ADDED: idle timeout — optional `IdleConfig` ({ ttlMs, warnBeforeMs, onWarn, onTimeout }) triggers automatic sign-out via DOM activity listeners (pointer/key/visibility); accessibility-friendly warning callback so consumers can render a modal.
- ADDED: forced-logout — `<AuthProvider>` exposes `forceLogout(reason)`; broadcasts to other tabs.
### Guards
- ADDED: `<AuthGuard>` — renders children when authenticated; otherwise renders `fallback` (default: throws `REACT_AUTH.UNAUTHENTICATED` for the nearest error boundary).
- ADDED: `<PermissionGate>` — role / permission gating using injected `PermissionResolver`.
- ADDED: `withAuth(Component)`, `usePermission(name)`, `useAuthGuard()` — hook/HOC variants for the two guards.
### Error UX + i18n
- ADDED: `defineCodes("REACT_AUTH", [...])` registry — `CONTEXT_MISSING`, `UNAUTHENTICATED`, `BACKEND_ERROR`, `SIGN_IN_FAILED`, `SIGN_UP_FAILED`, `SIGN_OUT_FAILED`, `REFRESH_FAILED`, `PASSWORD_TOO_WEAK`, `PASSWORD_PWNED`, `PASSWORD_RESET_FAILED`, `CHANGE_PASSWORD_FAILED`, `PASSKEY_REGISTER_FAILED`, `PASSKEY_AUTHENTICATE_FAILED`, `PASSKEY_AUTOFILL_UNAVAILABLE`, `PASSKEY_AUTOFILL_FAILED`, `PASSKEY_AUTOFILL_ABORTED`, `OAUTH_POPUP_BLOCKED`, `OAUTH_STATE_MISMATCH`, `OAUTH_CALLBACK_FAILED`, `MFA_REQUIRED`, `MFA_VERIFY_FAILED`, `TOTP_ENROLL_FAILED`, `RECOVERY_CODE_INVALID`, `EMAIL_VERIFY_FAILED`, `PHONE_VERIFY_FAILED`, `INVALID_OPTIONS`, `SSR_NO_OP`, `IDLE_TIMEOUT`, `FORCED_LOGOUT`. Every code reachable from a real call site; every translation preserves underlying `cause`.
- ADDED: `<AuthErrorBoundary>` — React error boundary that catches `SuaveplanError<*REACT_AUTH.*>` and renders an i18n-keyed user-facing message via the injected `MessageResolver`; unrecognised errors re-throw.
- ADDED: i18n catalog — `en-GB` (canonical), `cy-GB`, `zh-CN` (parity-checked via `bun scripts/check-i18n-parity.ts`); message keys namespaced under `react-auth.*`.
### SSR safety
- Every hook returns a stable no-op shape during SSR (`typeof window === "undefined"`); zero `window` / `navigator` / `document` access at module load.
- `<AuthProvider>` resolves persisted-session loading inside `useEffect`, so the server render is deterministic.
- Subpath exports stay tree-shakeable; importing the OAuth surface alone does not pull WebAuthn.
### Accessibility
- Every component meets WCAG 2.1 AA: explicit `<label>` association, `aria-describedby` for help/error text, `aria-live="polite"` for status changes, keyboard focus management on dialog open/close, `prefers-reduced-motion` respected on transitions, contrast ratios verified in stories.
- Live-region announcements on sign-in success/failure and on idle-timeout warning.
### Storybook + tests + E2E
- ADDED: `.stories.tsx` for every component (default + playground + variants including unauthenticated / loading / error / RTL / dark).
- ADDED: Vitest tests for every hook and component using `@suaveplan/testing/runner` (rule 11) and `@suaveplan/testing-dom` render helpers.
- ADDED: Playwright `e2e/` covering: password sign-in, sign-up, password reset, change-password, passkey register, passkey autofill, modal passkey authenticate, OAuth popup, OAuth redirect, TOTP enroll + verify, recovery-code redeem, email verification, phone verification, multi-tab sign-out propagation, idle-timeout sign-out, error-boundary fallback rendering, RTL locale rendering.
### Subpath exports
`./auth-provider`, `./hooks/use-auth`, `./hooks/use-user`, … (one per hook), `./components/auth-guard`, `./components/permission-gate`, `./components/password-input`, `./components/password-strength-meter`, `./components/hibp-pwned-indicator`, `./components/password-generator-button`, `./components/passkey-autofill-field`, `./components/passkey-list`, `./components/oauth-provider-button`, `./components/totp-enrollment-panel`, `./components/recovery-codes-panel`, `./components/email-verification-view`, `./components/phone-verification-view`, `./components/auth-error-boundary`, `./oauth/handle-callback`, `./oauth/provider-registry`, `./session/storage`, `./session/broadcast-channel`, `./session/idle-timer`, `./errors`, `./i18n/en-gb`, `./i18n/cy-gb`, `./i18n/zh-cn`.
### Dependencies
- `@suaveplan/types workspace:^` — `StorageLike`, `Clock`, `LoggerLike`, `IdGenerator`, `MessageResolver`.
- `@suaveplan/error workspace:^` — `SuaveplanError`, `defineCodes`.
- `@suaveplan/events workspace:^` — internal event bus for auth lifecycle.
- `@suaveplan/logger workspace:^` — structured logging.
- `@suaveplan/i18n workspace:^` — message resolution.
- `@suaveplan/webauthn workspace:^` — passkey ceremonies.
- `@suaveplan/oauth-client workspace:^` — OAuth + OIDC.
- `@suaveplan/srp workspace:^` — SRP-6a password proof (optional sign-in path).
- `@suaveplan/otp workspace:^` — TOTP + recovery codes.
- `@suaveplan/hibp workspace:^` — pwned-password check.
- `@suaveplan/password-strength workspace:^` — strength estimator.
- `@suaveplan/password-generator workspace:^` — passphrase / password generator.
- `@suaveplan/react-ui workspace:^` — button, dialog, input primitives.
- `react ^19.0.0` (peer), `react-dom ^19.0.0` (peer).
- `zod catalog:forms`, `tslib catalog:build`.
## Source mapping (upstream → genesis)
| Upstream (`/mnt/s/web/suaveplan/packages/react/react-auth/src/`) | Genesis (`packages/auth/react-auth/src/`) | Notes |
|---|---|---|
| `client/auth-client.ts` (Better-Auth wrapper) | (replaced) `auth-backend/` interface | Genesis transport-agnostic; consumer wires backend. Removes `@ts-expect-error` (upstream gap §3). |
| `components/auth-provider/` | `auth-provider/` | Re-architected for multi-tab + idle + injectable Clock/Storage. |
| `context/auth-context.tsx` | `auth-provider/auth-context.ts` | Inlined into provider surface; explicit `useAuthContext()` accessor. |
| `components/auth-guard/`, `hooks/use-auth-guard.ts` | `auth-guard/` | Adds typed-error fallback; keeps HOC `withAuth`. |
| `components/permission-gate/`, `hooks/use-permission.ts` | `permission-gate/` | Adds injectable `PermissionResolver`. |
| `components/login-form/`, `register-form/`, `forgot-password-form/`, `reset-password-form/`, `email-verification-view/` | `components/<form>/` | Each gains its own test (upstream gap §3); composed from `<PasswordInput>` etc. |
| `pages/{login,register,forgot-password,reset-password,email-verification,profile}/` | (out) | Page-level orchestration belongs to the consuming app — this package ships the reusable hooks + components only (eliminates the upstream pages/forms duplication noted in `enterprise/react-auth.md` §3). |
| `translations/{en-gb,cy-gb,zh-cn}.ts` | `i18n/{en-gb,cy-gb,zh-cn}/` | Parity-checked at CI. |
| `lib/better-auth-adapter.ts` | (replaced) example in `README.md` | Better Auth is one possible backend; the canonical interface is `AuthBackend`. |
| `types/` | `types/` | Trimmed to active types; `crud.types.ts` moved out (belongs to consuming-app boilerplate, not auth). |
| n/a (new) | `use-passkey-autofill/`, `passkey-autofill-field/` | Folded in from `add-react-auth-passkey-autofill` (superseded). |
| n/a (new) | TOTP / recovery / passkey-register / passkey-list / OAuth popup + redirect / idle timer / broadcast-channel sync / pwned indicator / strength meter / password-generator-button / phone verification | Closes the upstream "feature-rich enhancements" list (`enterprise/react-auth.md` §4) — MFA, passkeys, SSO orchestration, device management, telemetry, accessibility polish — and the user's explicit no-deferral list. |
## Verification approach
Each `### Requirement` in `specs/react-auth/spec.md` carries a `**Verified by:**` line at draft time naming the planned test file and test-name (CLAUDE.md §0c / OpenSpec §12a). Coverage gate: 100/100/100/100 Istanbul (rule 7). Test runner: `@suaveplan/testing/runner` (rule 11). Real-DOM hook + component testing via `@suaveplan/testing-dom`. Playwright E2E gates the multi-tab + popup-OAuth + idle-timeout flows that JSDOM cannot exercise. Accessibility verified per component via `@axe-core/playwright` in the E2E sweep.
## Impact
### Affected dependents
This is a NEW package. Run `bunx turbo ls --filter=...@suaveplan/react-auth` after landing.
- No dependents at landing time (verified by `bunx turbo ls --filter=...@suaveplan/react-auth` — currently empty). Adopters (`@suaveplan/web-app`, downstream tenant apps) will migrate in their own follow-up changes.
### Foundation packages touched
- None modified. Every dependency (`webauthn`, `oauth-client`, `srp`, `otp`, `hibp`, `password-strength`, `password-generator`, `i18n`, `react-ui`, `error`, `events`, `logger`, `types`) is consumed at its current published surface.
## Out of scope
Strictly limited to capabilities that belong to *other* packages, not deferred react-auth UX. The user explicitly rejected scoping any react-auth feature out.
- **Server-side session storage** — lives in `@suaveplan/server-session` (separate package). This package is browser-tier; sessions on the server are the server's responsibility.
- **Server-side OAuth callback handling on a different origin** — handled by the consumer's API route or `@suaveplan/api-auth-*` plugins. This package handles the browser-side callback page only.
- **WebAuthn server verification** — `@suaveplan/webauthn`'s `verifyAttestation` / `verifyAssertion` run on the server. The React hook only invokes the ceremony; the backend (caller-implemented) calls the verifier.
- **Backend transport implementation** — the package defines `AuthBackend`; concrete implementations against Better Auth / Hono routes / tRPC / GraphQL live in consumer code or sibling adapter packages.
- **Page-level orchestration** — `<LoginPage>`, `<ProfilePage>` etc. live in the consuming app. This package ships hooks + composable components; the app stitches them.
- **QR code rendering** — `@suaveplan/qrcode` already exists; `<TotpEnrollmentPanel>` consumes it via an injectable `renderQrCode` prop rather than re-implementing.
## Supersedes
On archival of `complete-react-auth`, the following are folded in / superseded and moved aside:
1. `openspec/changes/enterprise-react-auth/` — narrow OAuth-only scaffold. Every requirement it would have specified is covered here in expanded form. Move to `openspec/changes/_superseded-narrow-deferrals-2026-05-17/enterprise-react-auth/` as part of this proposal landing.
2. `openspec/changes/_superseded-narrow-deferrals-2026-05-17/add-react-auth-passkey-autofill/` — already moved aside in a prior superseding wave; its design notes for the autofill lifecycle are reused verbatim in `design.md` and `specs/react-auth/spec.md` here.
## Migration
No prior shipped surface on genesis — additive only. The two superseded change folders never archived, so there is no canonical spec to migrate from. Future consumers adopt `@suaveplan/react-auth` directly.