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

9.8 KiB

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.