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 intoregister-form-fields.tsx+register-form-terms.tsxsub-components to honour the 300-LOC budget. - ADDED:
PasswordResetForm— dual-mode (request|confirm) reset form with success state. Split intopassword-reset-request.tsx+password-reset-confirm.tsx+password-reset-success.tsxsub-components to honour the 300-LOC budget. - ADDED:
AuthProvider— context provider that wireslogin/loginWithProvider/register/logout/requestPasswordReset/resetPasswordto a backend reachable atconfig.authUrl. Accepts an injectablefetch: FetchLikeand an injectablenavigate(url)seam. - ADDED:
BrandingProvider— context provider that fetches per-tenant branding (or accepts static branding) and dispatches CSS custom properties throughuseThemeMutator().setExtensionTokens(...). Wrapsdocument.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
TenantBrandingSchemaso tenant payloads are validated at the branding context boundary. - ADDED: Error registry
REACT_AUTH_WLwith codesBRANDING_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 Playwrighte2e/*.spec.tskeep 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/webauthnand@suaveplan/otpare 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-loggingevent 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-authWave 1 (different concurrent session); revisit once that ships. - axe-core CI gate across stories — depends on
@suaveplan/storybook-test-utilsnot 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:
- Theming — Compose under
<ThemeProvider>via the newuseThemeMutator()returned by@suaveplan/react-ui.BrandingProvidercallssetExtensionTokens({ "--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). Nodocument.documentElement.style.setProperty("--brand-*", ...)calls in package code. useTranslationrewrite — Every upstreamt("k", "default")call site is rewritten tot("k", { defaultValue: "default" })to match the genesis@suaveplan/react-i18nsignature.react-hook-formdropped — The upstream forms are alreadyuseState-based; no Hook Form usage exists in*.tsx. We do not introduce it.- File-size budget —
register-form.tsx(360 LOC upstream) andpassword-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 injectablefetch+navigate; PROVIDER_OUTSIDE_TREE branch.src/context/branding-context.test.tsx— static branding + fetched branding +--brand-*extension tokens propagated viauseThemeMutator; 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— everyREACT_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.