10 KiB
Capability Spec Delta: react-auth-whitelabel (add-react-auth-whitelabel)
Package: @suaveplan/react-auth-whitelabel
Status: PROPOSED
Every requirement below carries a
**Verified by:**line at authoring time.
ADDED Requirements
Requirement: AuthProvider exposes auth verbs over an injectable transport
The package SHALL ship an AuthProvider React context provider that exposes login, loginWithProvider, register, logout, requestPasswordReset, and resetPassword verbs. Each verb invokes the configured backend through config.fetch (a FetchLike, defaulting to globalThis.fetch) and routes redirect side-effects through config.navigate (defaulting to a window.location.href writer). The context value also exposes user, isLoading, isAuthenticated, and error.
Scenario: login posts to authUrl and stores the returned user
- WHEN
useAuth().login("a@b.c", "pw")is called withfetchreturning{ ok: true, json: { user } } - THEN the request body is
{"email":"a@b.c","password":"pw"}, the request URL ends with/login, anduserbecomes the context'suser
Scenario: loginWithProvider routes through the navigate seam
- WHEN
useAuth().loginWithProvider("google")is called - THEN
config.navigate(url)is invoked exactly once with a URL that includes/oauth/google?redirect=
Scenario: useAuth outside provider throws PROVIDER_OUTSIDE_TREE
- WHEN
useAuth()is called with no<AuthProvider>ancestor - THEN a
ReactAuthWhitelabelErrorwith.code === "REACT_AUTH_WL.PROVIDER_OUTSIDE_TREE"is thrown
Verified by: src/context/auth-context.test.tsx::"login() posts credentials to {authUrl}/login and sets user" AND src/context/auth-context.test.tsx::"loginWithProvider routes through the navigate seam" AND src/context/auth-context.test.tsx::"useAuth outside provider throws REACT_AUTH_WL.PROVIDER_OUTSIDE_TREE"
Requirement: BrandingProvider writes tenant branding through the react-ui theme mutator
The package SHALL ship a BrandingProvider that, on every change of the resolved TenantBranding, calls useThemeMutator().setExtensionTokens({...}) with keys --brand-primary, --brand-secondary, --brand-accent, and --brand-font. Direct document.documentElement.style.setProperty("--brand-*", ...) writes from package code are forbidden. Non-token branding surfaces (document title, favicon <link rel="icon">, and tenant custom CSS <style id="tenant-custom-css">) remain direct DOM writes. The mutator's automatic unmount cleanup removes every --brand-* declaration.
Scenario: static branding applies extension tokens after mount
- WHEN
<ThemeProvider>...<BrandingProvider staticBranding={…}>mounts - THEN
document.documentElement.style.getPropertyValue("--brand-primary")returns the static branding'sprimaryColor
Scenario: extension tokens are removed on unmount
- WHEN the
<BrandingProvider>is unmounted - THEN
document.documentElement.style.getPropertyValue("--brand-primary")returns the empty string
Scenario: Zod failure on fetched branding raises BRANDING_INVALID_PAYLOAD
- WHEN the fetched branding response fails
TenantBrandingSchema.safeParse - THEN the context exposes a
ReactAuthWhitelabelErrorwhose.code === "REACT_AUTH_WL.BRANDING_INVALID_PAYLOAD"
Scenario: network failure raises BRANDING_FETCH_FAILED
- WHEN the configured
fetchresolves with{ ok: false, status: 503 } - THEN the context exposes a
ReactAuthWhitelabelErrorwhose.code === "REACT_AUTH_WL.BRANDING_FETCH_FAILED"
Verified by: src/context/branding-context.test.tsx::"setExtensionTokens propagates --brand-primary after mount" AND src/context/branding-context.test.tsx::"unmount removes every --brand-* property" AND src/context/branding-context.test.tsx::"invalid fetched payload exposes REACT_AUTH_WL.BRANDING_INVALID_PAYLOAD" AND src/context/branding-context.test.tsx::"fetch failure exposes REACT_AUTH_WL.BRANDING_FETCH_FAILED"
Requirement: TenantBranding has a Zod schema
The package SHALL export a Zod schema TenantBrandingSchema that validates every field of TenantBranding. The BrandingProvider SHALL run TenantBrandingSchema.safeParse(...) on every fetched payload before storing it.
Scenario: schema accepts a fully-populated branding payload
- WHEN
TenantBrandingSchema.parse({ tenantId, name, primaryColor: "#fff", secondaryColor: "#000", … })is called - THEN parsing succeeds
Scenario: schema rejects payload without primaryColor
- WHEN
TenantBrandingSchema.safeParse({ tenantId: "t", name: "n", secondaryColor: "#000" })is called - THEN the result is
{ success: false }
Verified by: src/types/branding.schema.test.ts::"accepts a fully-populated branding payload" AND src/types/branding.schema.test.ts::"rejects payload without primaryColor"
Requirement: LoginForm renders accessible email/password form with optional OAuth providers
The package SHALL ship a LoginForm React component with controlled email + password inputs, an optional rememberMe checkbox, an optional OAuth provider button list, an optional forgot-password affordance, and an optional register-link affordance. The submit button calls onSubmit? if supplied else useAuth().login(...). Errors are rendered in a role="alert" region. Every visible string is sourced from useTranslation().
Scenario: renders email + password inputs with required attribute
- WHEN
<LoginForm />is mounted under an<AuthProvider> - THEN an
input[name="email"][required]andinput[name="password"][required]are present
Scenario: submit calls onSubmit override when supplied
- WHEN
<LoginForm onSubmit={spy} />is mounted and the form is submitted with valid values - THEN
spyis called once with{ email, password, rememberMe }
Scenario: OAuth provider button click invokes loginWithProvider
- WHEN a
LoginFormrendered withproviders=[{id:"google",name:"Google"}]has its Google button clicked - THEN the auth context's
loginWithProvideris called once with"google"
Verified by: src/components/login-form/login-form.test.tsx::"renders required email and password inputs" AND src/components/login-form/login-form.test.tsx::"submit invokes onSubmit override when provided" AND src/components/login-form/login-form.test.tsx::"OAuth provider button click routes through loginWithProvider"
Requirement: RegisterForm validates password + terms + name before submitting
The package SHALL ship a RegisterForm React component that validates locally before invoking onSubmit? or useAuth().register(...). Validation rules: password length ≥ minPasswordLength (default 8); password equals confirmPassword; if requireName, name is non-empty; if showTermsCheckbox, terms checkbox is checked.
Scenario: password mismatch sets inline error and blocks submission
- WHEN the user submits with password "12345678" and confirmPassword "00000000"
- THEN the form renders a
role="alert"text matching/match/iand no auth call is made
Scenario: terms unchecked blocks submission when showTermsCheckbox is on
- WHEN the user submits without checking the terms checkbox
- THEN the form renders an error matching
/terms/iand no auth call is made
Verified by: src/components/register-form/register-form.test.tsx::"password mismatch blocks submission with inline error" AND src/components/register-form/register-form.test.tsx::"unchecked terms checkbox blocks submission"
Requirement: PasswordResetForm operates in request and confirm modes
The package SHALL ship a PasswordResetForm React component with mode: "request" | "confirm". Request mode collects email and calls useAuth().requestPasswordReset(email). Confirm mode collects new password + confirmation, validates min length + match, and calls useAuth().resetPassword(token, password). If mode === "confirm" and token is undefined, an inline MISSING_RESET_TOKEN error is shown and no auth call is made. On success a success state is rendered with an optional back-to-login affordance.
Scenario: request mode submits email and shows success state
- WHEN the request form is submitted with
email = "a@b.c" - THEN
requestPasswordResetis called once with"a@b.c"and the success copy is rendered
Scenario: confirm mode without token surfaces MISSING_RESET_TOKEN
- WHEN the confirm form is submitted with
token = undefined - THEN the form renders a
role="alert"containing the missing-token copy andresetPasswordis NOT called
Verified by: src/components/password-reset-form/password-reset-form.test.tsx::"request mode submits email and shows success state" AND src/components/password-reset-form/password-reset-form.test.tsx::"confirm mode without token surfaces MISSING_RESET_TOKEN"
Requirement: REACT_AUTH_WL error codes are registered and thrown
The package SHALL register the REACT_AUTH_WL error namespace via defineCodes and SHALL throw or surface each of these codes from at least one production code path: BRANDING_FETCH_FAILED, BRANDING_INVALID_PAYLOAD, AUTH_NETWORK_FAILED, AUTH_REJECTED, MISSING_RESET_TOKEN, PROVIDER_OUTSIDE_TREE.
Scenario: every registered code is thrown by ≥1 production file
- WHEN the test scans every
.ts/.tsxnon-test source file - THEN for each code in
REACT_AUTH_WL_CODES, at least one production file matches the code literal in a throw or surface context
Verified by: src/errors/error-codes.test.ts::"every REACT_AUTH_WL code is thrown by at least one production file"
Requirement: i18n every user-visible string
The package SHALL source every user-visible string through useTranslation() from @suaveplan/react-i18n with the { defaultValue } options-bag signature. No bare string literals appear in JSX text content inside form components.
Scenario: every form translates its title and submit button via useTranslation
- WHEN a form is rendered with no overriding
title/subtitleprop - THEN the visible title and submit-button text come from a
t(key, { defaultValue })call
Verified by: src/components/login-form/login-form.test.tsx::"title and submit copy resolve through useTranslation"