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,103 @@
# Ship @suaveplan/react-auth-whitelabel — white-label auth React components
**Package**: `@suaveplan/react-auth-whitelabel`
**Status**: PROPOSED
**Type**: new-package
## Why
Genesis needs a tenant-aware, white-label authentication surface that lets a single
build serve many tenants with their own logo, palette, font, and copy. Upstream
(`/mnt/s/web/suaveplan/packages/react/react-auth-whitelabel`) ships three forms
(`LoginForm`, `RegisterForm`, `PasswordResetForm`), two contexts (`AuthProvider`,
`BrandingProvider`), and six hooks; it powers every tenant-branded sign-in flow
in the upstream stack but was never enterprise-graded. We re-author it on top of
the new `@suaveplan/react-ui` runtime theme mutator so branding writes flow
through the canonical token-layer stack instead of poking
`document.documentElement.style` from package code.
## What changes
- ADDED: `LoginForm` — controlled email/password form with optional OAuth provider list, remember-me, forgot-password, and register-link affordances.
- ADDED: `RegisterForm` — controlled register form with optional name field, terms checkbox, OAuth provider list, password confirmation, configurable min length. Split into `register-form-fields.tsx` + `register-form-terms.tsx` sub-components to honour the 300-LOC budget.
- ADDED: `PasswordResetForm` — dual-mode (`request` | `confirm`) reset form with success state. Split into `password-reset-request.tsx` + `password-reset-confirm.tsx` + `password-reset-success.tsx` sub-components to honour the 300-LOC budget.
- ADDED: `AuthProvider` — context provider that wires `login` / `loginWithProvider` / `register` / `logout` / `requestPasswordReset` / `resetPassword` to a backend reachable at `config.authUrl`. Accepts an injectable `fetch: FetchLike` and an injectable `navigate(url)` seam.
- ADDED: `BrandingProvider` — context provider that fetches per-tenant branding (or accepts static branding) and dispatches CSS custom properties through `useThemeMutator().setExtensionTokens(...)`. Wraps `document.title`, `<link rel="icon">`, and `<style id="tenant-custom-css">` for non-token surfaces.
- ADDED: Hooks `useAuth`, `useBranding`, `useIsAuthenticated`, `useLogo`, `useTenantBranding`, `useUser`.
- ADDED: Types `AuthConfig`, `AuthContextValue`, `AuthProvider as AuthProviderConfig`, `AuthUser`, `BrandingContextValue`, `BrandingProviderProps`, `RegisterData`, `TenantBranding`.
- ADDED: Zod schema `TenantBrandingSchema` so tenant payloads are validated at the branding context boundary.
- ADDED: Error registry `REACT_AUTH_WL` with codes `BRANDING_FETCH_FAILED`, `BRANDING_INVALID_PAYLOAD`, `AUTH_NETWORK_FAILED`, `AUTH_REJECTED`, `MISSING_RESET_TOKEN`, `PROVIDER_OUTSIDE_TREE` (each thrown by ≥1 production path).
- ADDED: Three `.stories.tsx` (default + playground + variants) per form so Storybook + downstream Playwright `e2e/*.spec.ts` keep the parity surface.
## Out of scope
- MFA / TOTP / passkey forms (`MfaChallengeForm`, `PasskeyRegistrationForm`, `PasskeyAuthenticationForm`) — enterprise plan §4 lists these as feature-rich enhancements, not parity baseline. Deferred to a follow-up change once `@suaveplan/webauthn` and `@suaveplan/otp` are migrated to genesis.
- Magic-link / SMS OTP login flows — depend on `@suaveplan/security-codes`, `@suaveplan/emailer`, `@suaveplan/sms`. Out of dep order.
- Audit-log integration (`@suaveplan/audit-logging` event emission) — depends on packages not yet in genesis.
- Password-strength meter via `@suaveplan/password-formats` — dep not yet in genesis.
- Rate-limited submission via `@suaveplan/rate-limiter` — dep not yet in genesis.
- Better-Auth first-class adapter (`createBetterAuthProvider(client)`) — overlaps with `@suaveplan/react-auth` Wave 1 (different concurrent session); revisit once that ships.
- axe-core CI gate across stories — depends on `@suaveplan/storybook-test-utils` not yet migrated.
- Bundle-size snapshot — repo-wide tooling not yet wired.
## Source mapping
This change derives from `../suaveplan/enterprise/react-auth-whitelabel.md` and the
companion source tree at `../suaveplan/packages/react/react-auth-whitelabel/src/`.
| Requirement | Source lines | Notes |
|---|---|---|
| Three form components with parity feature set | L11-L13, L22-L27 | Verbatim baseline; sub-components added in genesis to honour 300-LOC budget. |
| Two context providers (AuthProvider, BrandingProvider) | L11, L25-L26 | Verbatim. |
| Six hooks (useAuth, useBranding, useIsAuthenticated, useLogo, useTenantBranding, useUser) | L11, L27 | Verbatim. |
| Public types: AuthConfig, AuthContextValue, AuthProvider (AuthProviderConfig), AuthUser, BrandingContextValue, BrandingProviderProps, RegisterData, TenantBranding | L28-L29 | Verbatim. |
| Replace `window.location.href` seam with injectable `navigate(url)` on `AuthConfig` | L57-L58, L67, L102 | Enterprise §3 gap, enterprise §6 acceptance criterion. |
| Inject `fetch: FetchLike` on `AuthConfig` and `BrandingProvider` | enterprise §3, §6 | Required to keep auth + branding contexts testable without `vi.mock` (CLAUDE.md rule 2). |
| Zod schema for `TenantBranding` (`TenantBrandingSchema`) | L60, L104 | Enterprise §3 + §6 acceptance criterion. |
| i18n every user-visible string via `t(...)` | L88 ("Cross-cutting") | Verbatim. Migrate from `@suaveplan/react-translate` to `@suaveplan/react-i18n`; rewrite `t("k", "default")` → `t("k", { defaultValue: "default" })`. |
| Error hierarchy via `@suaveplan/error` | L86 ("Cross-cutting") | Verbatim. Use `defineCodes("REACT_AUTH_WL", [...])` + `ReactAuthWhitelabelError extends SuaveplanError`. |
| 100% test coverage; 250/300/500 file budget; `sideEffects: false`; Biome clean; TypeScript strict; no `any`/`ts-ignore`; co-located `.md` per export | L80-L88 | Cross-cutting. |
| Branding writes flow through canonical theme-token surface | enterprise §4 ("Theme system") | Re-implemented in genesis via the new `useThemeMutator()` from `@suaveplan/react-ui` (commit `ce7c179c1`); BrandingProvider no longer pokes `document.documentElement.style` directly. |
| Three Playwright E2E specs for parity | L7 | Ported to genesis `e2e/` under `### Excluded` if the upstream Storybook glue isn't available; documented decision below. |
### Requirements with no source line
| Requirement | Justification |
|---|---|
| Error code `BRANDING_INVALID_PAYLOAD` thrown via Zod failure | Required to fulfil rule "every error code thrown by ≥1 production path" once we add the Zod schema. |
| Error code `PROVIDER_OUTSIDE_TREE` thrown by hooks called outside their provider | Replaces the upstream's plain `throw new Error("...")` with a typed code so consumers can branch on `.code` (cross-cutting policy). |
| Split `register-form.tsx` + `password-reset-form.tsx` into sub-components | CLAUDE.md 300-LOC hard budget, not an upstream requirement. |
| Test-runner imports only from `@suaveplan/testing/runner` | CLAUDE.md rule 11; not in upstream. |
## Decisions
Four clarifications were locked in before authoring:
1. **Theming** — Compose under `<ThemeProvider>` via the new `useThemeMutator()` returned by `@suaveplan/react-ui`. `BrandingProvider` calls `setExtensionTokens({ "--brand-primary": branding.primaryColor, ... })` on every payload change. Document title swap, favicon swap, and custom-CSS `<style>` injection STAY as direct DOM writes (they are not CSS variables). No `document.documentElement.style.setProperty("--brand-*", ...)` calls in package code.
2. **`useTranslation` rewrite** — Every upstream `t("k", "default")` call site is rewritten to `t("k", { defaultValue: "default" })` to match the genesis `@suaveplan/react-i18n` signature.
3. **`react-hook-form` dropped** — The upstream forms are already `useState`-based; no Hook Form usage exists in `*.tsx`. We do not introduce it.
4. **File-size budget** — `register-form.tsx` (360 LOC upstream) and `password-reset-form.tsx` (388 LOC upstream) are split into named sub-components so every source file stays ≤300 LOC hard.
## Verification approach
Every requirement in the spec delta carries a `**Verified by:**` line naming concrete
`it(...)` titles in the test files below. Coverage is enforced at 100/100/100/100 via
`provider: 'istanbul'`.
- `src/components/login-form/login-form.test.tsx` — LoginForm rendering, submission, OAuth click, forgot/register link routing.
- `src/components/register-form/register-form.test.tsx` — RegisterForm validation, password mismatch, terms enforcement, name-required path, success/error callbacks.
- `src/components/register-form/register-form-fields.test.tsx` — sub-component field rendering + required-indicator.
- `src/components/register-form/register-form-terms.test.tsx` — terms checkbox + links rendering.
- `src/components/password-reset-form/password-reset-form.test.tsx` — mode switching, missing-token guard, success state, validation.
- `src/components/password-reset-form/password-reset-request.test.tsx` — request mode submit + back-to-login link.
- `src/components/password-reset-form/password-reset-confirm.test.tsx` — confirm mode submit + min-length + mismatch.
- `src/components/password-reset-form/password-reset-success.test.tsx` — success copy + back-to-login.
- `src/context/auth-context.test.tsx` — login / register / logout / requestPasswordReset / resetPassword / loginWithProvider via injectable `fetch` + `navigate`; PROVIDER_OUTSIDE_TREE branch.
- `src/context/branding-context.test.tsx` — static branding + fetched branding + `--brand-*` extension tokens propagated via `useThemeMutator`; cleanup on unmount; Zod failure → BRANDING_INVALID_PAYLOAD; network error → BRANDING_FETCH_FAILED.
- `src/types/branding.schema.test.ts` — Zod schema accept/reject cases.
- `src/errors/error-codes.test.ts` — every `REACT_AUTH_WL.*` code thrown by ≥1 production path (regex grep).
- `e2e/login-form.spec.ts` — ported subset of upstream login-form Playwright specs (parity smoke).
## Migration impact
None — additive only. No genesis consumer depends on this package yet.