# 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: `` 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: `` — 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: `` — accessible input with show/hide toggle, optional integrated `` and HIBP-pwned check (debounced, opt-in). - ADDED: `` — wraps `@suaveplan/password-strength` `assessReactive` with cancellation on unmount, renders a 5-bucket bar with i18n-keyed feedback strings. - ADDED: `` — wraps `@suaveplan/hibp` k-anonymity check (caller injects the client; debounced ≥ 500 ms; no network on SSR). - ADDED: `` — 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: `` — `` wrapper, blur-cancels when no credential delivered. - ADDED: `` — 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: `` — 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: `` — orchestrates secret display + code entry + recovery-codes reveal in one accessible flow. - ADDED: `` — 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: `` — renders the "check your inbox" UX with resend cooldown timer. - ADDED: `` — 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 — `` exposes `forceLogout(reason)`; broadcasts to other tabs. ### Guards - ADDED: `` — renders children when authenticated; otherwise renders `fallback` (default: throws `REACT_AUTH.UNAUTHENTICATED` for the nearest error boundary). - ADDED: `` — 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: `` — 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. - `` 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 `