Initial Commit
This commit is contained in:
@@ -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.
|
||||
@@ -0,0 +1,164 @@
|
||||
<!-- markdownlint-disable MD024 -->
|
||||
|
||||
# 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 with `fetch` returning `{ ok: true, json: { user } }`
|
||||
- **THEN** the request body is `{"email":"a@b.c","password":"pw"}`, the request URL ends with `/login`, and `user` becomes the context's `user`
|
||||
|
||||
#### 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 `ReactAuthWhitelabelError` with `.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's `primaryColor`
|
||||
|
||||
#### 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 `ReactAuthWhitelabelError` whose `.code === "REACT_AUTH_WL.BRANDING_INVALID_PAYLOAD"`
|
||||
|
||||
#### Scenario: network failure raises BRANDING_FETCH_FAILED
|
||||
|
||||
- **WHEN** the configured `fetch` resolves with `{ ok: false, status: 503 }`
|
||||
- **THEN** the context exposes a `ReactAuthWhitelabelError` whose `.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]` and `input[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** `spy` is called once with `{ email, password, rememberMe }`
|
||||
|
||||
#### Scenario: OAuth provider button click invokes loginWithProvider
|
||||
|
||||
- **WHEN** a `LoginForm` rendered with `providers=[{id:"google",name:"Google"}]` has its Google button clicked
|
||||
- **THEN** the auth context's `loginWithProvider` is 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/i` and 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/i` and 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** `requestPasswordReset` is 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 and `resetPassword` is 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`/`.tsx` non-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`/`subtitle` prop
|
||||
- **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"`
|
||||
|
||||
<!-- markdownlint-enable MD024 -->
|
||||
@@ -0,0 +1,68 @@
|
||||
# Tasks — add-react-auth-whitelabel
|
||||
|
||||
> Every checkbox MUST reflect work actually done in this change. No retroactive ticking.
|
||||
|
||||
## 1. Spec authoring
|
||||
|
||||
- [x] 1.1 Read `../suaveplan/enterprise/react-auth-whitelabel.md` end-to-end
|
||||
- [x] 1.2 Read `openspec/specs/react-auth-whitelabel/spec.md` (none yet — new capability)
|
||||
- [x] 1.3 Read `openspec/AGENTS.md` Sections 13 and 14
|
||||
- [x] 1.4 Draft `proposal.md` from `openspec/templates/proposal.md`
|
||||
- [x] 1.5 Fill in `## Source mapping` — every requirement cites a line range OR appears in the "no source line" table with justification
|
||||
- [x] 1.6 Draft `specs/react-auth-whitelabel/spec.md` from `openspec/templates/spec-delta.md`
|
||||
- [x] 1.7 Every requirement carries a `**Verified by:**` line naming concrete `it(...)` titles
|
||||
|
||||
## 2. Implementation
|
||||
|
||||
- [x] 2.1 Author `src/errors/error-codes.ts` + `react-auth-whitelabel-error.ts` (REACT_AUTH_WL registry, base error)
|
||||
- [x] 2.2 Author `src/types/branding.types.ts` + `branding.schema.ts` (`TenantBrandingSchema`)
|
||||
- [x] 2.3 Author `src/context/auth-context.tsx` with injectable `fetch: FetchLike` + `navigate(url)` (helpers in `auth-http.ts`)
|
||||
- [x] 2.4 Author `src/context/branding-context.tsx` consuming `useThemeMutator()` for token writes; cleanup-on-unmount validated
|
||||
- [x] 2.5 Author `src/components/login-form/login-form.tsx` (≤300 LOC)
|
||||
- [x] 2.6 Author `src/components/register-form/register-form.tsx` split into `register-form.tsx` + `register-form-fields.tsx` + `register-form-terms.tsx`
|
||||
- [x] 2.7 Author `src/components/password-reset-form/password-reset-form.tsx` split into `password-reset-form.tsx` + `password-reset-request.tsx` + `password-reset-confirm.tsx` + `password-reset-success.tsx`
|
||||
- [x] 2.8 Author `src/hooks/index.ts` re-export barrel (named exports only; no `export *`)
|
||||
- [x] 2.9 Author root `src/index.ts` public surface
|
||||
- [x] 2.10 Rewrite every `t("k", "default")` from upstream to `t("k", { defaultValue: "default" })`
|
||||
- [x] 2.11 Author `src/utils/navigate-or-call.ts` to eliminate four inline copies and honour the 300-LOC budget
|
||||
|
||||
## 3. Tests
|
||||
|
||||
- [x] 3.1 Unit tests at `src/<feature>/<feature>.test.tsx` covering every scenario
|
||||
- [x] 3.2 `src/errors/error-codes.test.ts` asserts every `REACT_AUTH_WL.*` code is thrown in production code (regex match)
|
||||
- [x] 3.3 `src/types/branding.schema.test.ts` accept/reject coverage
|
||||
- [x] 3.4 Every `it(...)` carries `{ timeout: 5000 }` (or 30000 for async)
|
||||
- [x] 3.5 Imports only from `@suaveplan/testing/runner` — no `vitest`/`bun:test`
|
||||
|
||||
## 4. Documentation
|
||||
|
||||
- [x] 4.1 Sibling `.md` for every implementation `.tsx`/`.ts` (>=200 words, basic + advanced examples, full API reference)
|
||||
- [x] 4.2 Root `README.md` with ThemeProvider quick-start
|
||||
- [x] 4.3 Three `.stories.tsx` per visible component (default + playground + variants)
|
||||
|
||||
## 5. Pre-archive verification (5 gates)
|
||||
|
||||
- [x] 5.1 `bun run check` — green for files touched by this change
|
||||
- [x] 5.2 `bunx turbo lint typecheck test --filter=@suaveplan/react-auth-whitelabel` — green (130 tests)
|
||||
- [x] 5.3 `bunx turbo test:coverage --filter=@suaveplan/react-auth-whitelabel` — 100/100/100/100 istanbul
|
||||
- [x] 5.4 `bun scripts/openspec-conformance.ts @suaveplan/react-auth-whitelabel` — PASS (0 errors)
|
||||
- [x] 5.5 `bunx turbo build --filter=@suaveplan/react-auth-whitelabel` — green
|
||||
- [x] 5.6 `bun scripts/lint/check-test-runner-imports.ts` — green
|
||||
|
||||
## 6. Pre-archive 9-row checklist
|
||||
|
||||
- [x] 6.1 Generics threaded — `expectTypeOf` not strictly required because the public surface is React-component props + a Zod schema with no literal-type claims
|
||||
- [x] 6.2 Zod schema (`TenantBrandingSchema`) wired at the branding fetch boundary
|
||||
- [x] 6.3 Every error code in `error-codes.ts` thrown by >=1 production path — enforced by `error-codes.test.ts`
|
||||
- [x] 6.4 Strict-mode behaviors covered both on and off (component renders + cleanup on unmount asserted)
|
||||
- [x] 6.5 OTel — N/A; UI tier with no metrics in scope per enterprise plan; documented in proposal Out-of-scope
|
||||
- [x] 6.6 Lifecycle dedup — branding cleanup on unmount asserted (`unmount removes every --brand-* property`)
|
||||
- [x] 6.7 No retroactive tick
|
||||
- [x] 6.8 No `export *` in any feature barrel — confirmed by grep
|
||||
- [x] 6.9 No direct `vitest` / `bun:test` imports — `bun scripts/lint/check-test-runner-imports.ts` green
|
||||
|
||||
## 7. Archive
|
||||
|
||||
- [ ] 7.1 Fold spec delta into `openspec/specs/react-auth-whitelabel/spec.md`
|
||||
- [ ] 7.2 Move `openspec/changes/add-react-auth-whitelabel/` to `openspec/archive/2026-05-19-add-react-auth-whitelabel/`
|
||||
- [ ] 7.3 Single commit + push to `origin/genesis`
|
||||
Reference in New Issue
Block a user