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

186 lines
25 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# OpenSpec Change: enterprise-react-ui-pack-auth
- **Change ID**: enterprise-react-ui-pack-auth
- **Package**: `@suaveplan/react-ui-pack-auth`
- **Category**: react
- **Target path**: `packages/react/react-ui-pack-auth`
- **Status**: PROPOSED
- **Date**: 2026-05-05
- **Design source**: `openspec/cross-cutting-designs/2026-05-05-react-ui-react-i18n-react-forms.md` §1.2 row 5, §5 (component-pack contract), §9 (`createDomainForm` factory), §11 (`TranslationKey`)
- **Naming note**: this package replaces what would historically have been a hand-rolled `@suaveplan/react-auth-forms`. Naming follows the `@suaveplan/react-ui-pack-<name>` convention introduced in the master design §5.8 so theme packs (`react-ui-theme-<name>`) and component packs share a discoverable prefix.
- **Prereqs (one SHIPPED, two in-flight)**:
- `enterprise-react-i18n` shipped 2026-05-05 — archived at `openspec/archive/2026-05-05-enterprise-react-i18n/`; capability spec at `openspec/specs/react-i18n/spec.md`. This pack consumes `TranslationKey` for every form-field label across LoginForm / RegisterForm / PasswordResetForm / TwoFactorForm / MagicLinkForm, contributes `react-ui-pack-auth.<form>.<key>` keys to `ContributedTranslationKeys` in all 5 locales, and uses `tDynamic` for any runtime-computed error-code-to-message lookups (e.g. server-side `auth.error.<code>`).
- `enterprise-react-ui-v2` (sibling, in-flight) — provides the `ComponentPack<TName>` contract, `PackComponents` global augmentation interface, `PackRegistryProvider`, `usePackComponent` typed lookup, and `peerRange` validation that this pack registers against.
- `enterprise-react-forms-v2` (sibling, in-flight) — provides the `createDomainForm` factory each of the five forms is built atop, and the validators (`required`, `email`, `passwordStrength`, `pattern`) the form schemas compose from.
## Why
This package exists for three reasons, each of which would justify it on its own:
1. **Prove the `ComponentPack` contract end-to-end as an external package.** Section 5 of the master design defines a TypeScript-augmented registry (`PackComponents`), a per-pack scoped React context, lifecycle hooks (`onRegister` / `onUnregister`), pack-shipped translation contributions (§5.5), and a `peerRange` compatibility check (§5.6). Until at least one external pack consumes this surface, the contract is unverified. `react-ui-pack-auth` is that external consumer — it lives in its own package, it has its own `package.json`, and it depends on `@suaveplan/react-ui` only as a peer. If the contract has gaps, this package is the smallest viable surface that surfaces them before downstream consumers (the ~40 `react-*` packages waiting on `react-ui`) start adopting it.
2. **Exercise the full `react-ui` + `react-i18n` + `react-forms` + `@suaveplan/api-schemas` stack in a single high-fidelity demo.** Every layer the React stack ships gets a real workout in one place — `createDomainForm` (forms factory, §9.4), `TranslationKey`-typed labels (§11.2), `<Trans>` for marketing copy in marketing slots, locale-bundle contribution via module augmentation (§5.5 + §11.2), pack-scoped lifecycle, runtime override via `usePackComponent`, and Zod validation sourced directly from `@suaveplan/api-schemas/auth`. If any of those boundaries are awkward, the auth pack will surface the awkwardness.
3. **Ship genuinely useful auth components every consumer app needs anyway.** Login, register, password reset, two-factor entry, and magic-link request are not bespoke flows — every Suaveplan-built product ships them. Packaging them as a contract-proving demo means consumers get a production-ready pack on day one rather than a contrived "Hello, Pack" example.
## Current-state audit (genesis)
`packages/react/react-ui-pack-auth/` does not exist. There is no `@suaveplan/react-ui-pack-auth` package in the workspace. The five forms this pack ships do not exist anywhere on genesis. `@suaveplan/api-schemas/auth` ships `LoginInput`, `RegisterInput`, `RefreshInput`, `LogoutInput` and the `LoginUser` / `LoginOutput` / `RegisterOutput` response types — the additional auth schemas this pack needs (`PasswordResetRequestInput`, `PasswordResetConfirmInput`, `TwoFactorVerifyInput`, `MagicLinkRequestInput`, `MagicLinkVerifyInput`) do not yet exist and are added in the same change set under §"What changes" below.
## Available workspace deps on genesis
| Package | Role | Version range |
|---|---|---|
| `@suaveplan/react-ui` | Provides `ComponentPack` contract, `usePackComponent`, `useThemedVariants`, every primitive (`Button`, `Field`, `Input`, `Label`, `Alert`, `Card`, `Sheet`) the forms render with | `peer` + `workspace:^` (dev) |
| `@suaveplan/react-i18n` | Provides `TranslationKey` brand, `useTranslation`, `<Trans>`, `ContributedTranslationKeys` augmentation point | `peer` + `workspace:^` (dev) |
| `@suaveplan/react-forms` | `createDomainForm` factory, `useDomainForm`, validation primitives, recipe surfaces (used by 2FA's countdown timer) | `workspace:^` |
| `@suaveplan/api-schemas` | Provides `LoginInput` / `RegisterInput` / `RefreshInput` / `LogoutInput` schemas; this change extends the `auth` subpath with five new schemas (see below) | `workspace:^` |
| `@suaveplan/error` | `defineCodes("REACT_UI_PACK_AUTH", [...])` registry, `SuaveplanError` base class | `workspace:^` |
| `@suaveplan/types` | Shared `Clock`, `MessageResolver`, `LoggerLike` contracts | `workspace:^` |
| `@suaveplan/testing` | `@suaveplan/testing/runner` — sole test-runner import surface (CLAUDE.md §11) | `workspace:^` (dev) |
| `@suaveplan/testing-dom` | `render`, `screen`, `userEvent`, jest-dom matchers, `composeStories` adapter | `workspace:^` (dev) |
| `@suaveplan/testing-e2e` | Playwright config factory, fixture extensions, axe-core helpers | `workspace:^` (dev) |
| React | Function-component peer | `peer` `^18.0.0` or `^19.0.0` |
| `tslib` | Compiled-helper runtime | `catalog:build` |
| `zod` | Runtime schema validation at every public boundary | `catalog:forms` |
| `@opentelemetry/api` | Submit-counter and submit-duration histogram | `catalog:observability` |
External: **no** Radix imports — Radix is a peer of `@suaveplan/react-ui`, not this package. **No** form-library imports — `react-hook-form` is a peer of `@suaveplan/react-forms`. **No** direct i18next / react-i18next imports — the `TranslationKey` brand and `useTranslation` come exclusively from `@suaveplan/react-i18n`.
## Public API surface
```ts
// Pack export — the canonical ComponentPack<'auth'> instance
export { authPack };
// Per-form components (direct-import path, tree-shake friendly)
export { LoginForm, type LoginFormProps } from "./components/login-form/login-form.js";
export { RegisterForm, type RegisterFormProps } from "./components/register-form/register-form.js";
export {
PasswordResetForm,
type PasswordResetFormProps,
} from "./components/password-reset-form/password-reset-form.js";
export { TwoFactorForm, type TwoFactorFormProps } from "./components/two-factor-form/two-factor-form.js";
export { MagicLinkForm, type MagicLinkFormProps } from "./components/magic-link-form/magic-link-form.js";
// Translation-key contribution (side-effect import on root `index.ts`)
import "./translations/augmentation.js";
// Error registry
export { ReactUiPackAuthError, REACT_UI_PACK_AUTH_CODES } from "./errors/errors.js";
```
Each form's `Props` is the union `{ onSuccess: (result: <SchemaOutput>) => void; onError?: (error: SuaveplanError) => void; defaultValues?: Partial<<SchemaInput>>; submitClient: AuthSubmitClient; redirectTo?: string }`. The `AuthSubmitClient` interface is published from `./types/auth-submit-client.types.ts` and is an injectable contract with one method per form (`login`, `register`, `requestPasswordReset`, `confirmPasswordReset`, `verifyTwoFactor`, `requestMagicLink`, `verifyMagicLink`). No global state assumptions — every form takes its submit client as a prop (or pulls it from a `<AuthSubmitClientProvider>` whose default is unset).
## Design decisions
1. **Implements `ComponentPack<'auth'>` from `@suaveplan/react-ui/pack`.** The exported `authPack` object satisfies the `ComponentPack<'auth'>` interface verbatim — `name: 'auth'`, `version: '0.0.0'`, `peerRange: '^0.1.0'`, `components: { LoginForm, RegisterForm, PasswordResetForm, TwoFactorForm, MagicLinkForm }`, plus the optional `translations`, `onRegister`, `onUnregister` slots. A `pack.contract.test.tsx` asserts the object literally satisfies the type via `expectTypeOf` and that registering it through `<PackRegistryProvider packs={[authPack]}>` causes `usePackComponent('auth', 'LoginForm')` to return the same component reference (`Object.is`).
2. **Each form is built via `createDomainForm` from `@suaveplan/react-forms` — no hand-rolled JSX.** The five forms are pure factory invocations. This is deliberate: it exercises `createDomainForm`'s expressiveness across four different submit-shape patterns (single-step credential, multi-field with cross-field validation, single-input with rate-limit feedback, OTP with countdown timer, magic-link with bifurcated request/verify states) and proves the factory handles them all without dropping into the render-prop escape hatch. Any form that *would* require the escape hatch is a signal that `createDomainForm` is incomplete and needs a follow-up change against `@suaveplan/react-forms` rather than a workaround here.
3. **Schemas come straight from `@suaveplan/api-schemas/auth` — no schema duplication.** `LoginForm` consumes `LoginInput`, `RegisterForm` consumes `RegisterInput`. The five new schemas (`PasswordResetRequestInput`, `PasswordResetConfirmInput`, `TwoFactorVerifyInput`, `MagicLinkRequestInput`, `MagicLinkVerifyInput`) are added to `@suaveplan/api-schemas/auth` in the same change set so the API server and the React client share a single source of truth. The pack never re-defines a Zod object inline; if the server changes the contract, the form fails at compile time the next time `bun install` runs.
4. **All field labels, placeholders, helper text, and error messages typed as `TranslationKey` (master design §11).** Every user-visible string the pack renders is a key, never a literal. Each form's field config uses keys under the namespace `react-ui-pack-auth.<form>.<field>.<role>` — e.g. `react-ui-pack-auth.login.email.label`, `react-ui-pack-auth.login.email.placeholder`, `react-ui-pack-auth.login.email.errors.required`. The pack's locale JSON files are the canonical source for the type-generation script that augments `ContributedTranslationKeys` so a typo in the field config fails `tsc` not at runtime.
5. **Per-locale translations under `react-ui-pack-auth.*` namespace, registered via module augmentation.** A generated `src/translations/augmentation.d.ts` module-augments `@suaveplan/react-i18n`'s `ContributedTranslationKeys` interface with every key in `src/translations/en-GB.json`, and `src/translations/augmentation.ts` imports each locale JSON for its runtime side effect (registering the bundle into the `authPack.translations` map). Importing the package's `index.ts` registers both the type-level union and the runtime bundles — consumers do not need a separate import.
6. **Five locales: `en-GB`, `zh-CN`, `ar-AE`, `cy-GB`, `ru-RU`.** Mirrors the master-design §1.1 set. `ar-AE` proves RTL flip on every form; `cy-GB` proves Welsh's six plural categories don't break the OTP "X attempts remaining" string (`{count, plural, zero {…} one {…} two {…} few {…} many {…} other {…}}`). The pack's locale completeness gate (`scripts/lint/check-locale-completeness.ts`, run via `bunx turbo lint`) blocks any commit that introduces a key in `en-GB.json` without translations in the other four locales.
7. **`onRegister(ctx)` lifecycle hook pre-warms i18n bundles for the auth namespace.** When `<PackRegistryProvider packs={[authPack]}>` mounts, it calls `authPack.onRegister(ctx)` with a `PackContext` exposing `i18n`, `theme`, `logger`, and a pack-scoped `AbortSignal`. The hook calls `ctx.i18n.addResourceBundle('react-ui-pack-auth', authPack.translations[ctx.i18n.locale])` and warms the message catalog so the first form-render does not Suspense. `onUnregister(ctx)` calls `removeResourceBundle` so the i18n core stays canonical and pack mount/unmount is symmetric.
8. **Each form takes typed `onSuccess` / `onError` callbacks — no global state assumptions.** The pack does not call `window.location.assign`, does not push to a router, does not write to `localStorage`. `LoginForm` returns a `LoginOutput`-shaped value to its `onSuccess` callback; the consumer wires it to whatever auth/session/router stack they ship. This keeps the pack composable across Next.js, Remix, vanilla Vite, and React Native Web consumers without per-host adapters.
9. **Single-direction import policy.** The pack imports from `@suaveplan/react-ui` (peer-dep) but never re-exports its types — consumers depend on `@suaveplan/react-ui` directly per CLAUDE.md rule 4 ("never re-export external deps"). The pack's `pack.ts` file is a thin object literal whose components reference internal-to-this-package implementations only.
10. **No hidden retries or backoff.** Submit calls are one-shot. If a consumer wants exponential backoff or rate-limit handling, they wrap their submit client with `@suaveplan/retry`. The pack stays at the rendering layer.
## What changes
### `@suaveplan/react-ui-pack-auth` (new package)
- ADDED: `authPack` — a `ComponentPack<'auth'>` instance exporting all five forms, all five locale bundles, the `onRegister` warmer, and the `onUnregister` unwinder.
- ADDED: `LoginForm` — built via `createDomainForm({ schema: LoginInput, fields: { email, password }, submit: { ... } })`. Field kinds: `email`, `password`. Optional `defaultValues.email` for the pre-filled-from-magic-link path.
- ADDED: `RegisterForm` — built via `createDomainForm({ schema: RegisterInput.extend({ confirmPassword: ..., terms: ... }), fields: { email, name, password, confirmPassword, terms }, submit: { ... } })`. Cross-field validator: `confirmPassword === password`. The `terms` checkbox is required and has its own translation key.
- ADDED: `PasswordResetForm` — two-mode factory (`mode: 'request' | 'confirm'`). Request mode uses `PasswordResetRequestInput` (single email field); confirm mode uses `PasswordResetConfirmInput` (token + new password + confirm). Mode is a prop, not internal state.
- ADDED: `TwoFactorForm` — built via `createDomainForm({ schema: TwoFactorVerifyInput, fields: { code }, ... })`. The `code` field is `kind: 'otp'` (six-digit InputOTP from `react-ui`). Includes a "send new code" button gated by a 60-second countdown surfaced via `@suaveplan/timing`'s countdown helper passed in as a prop (so tests can inject a fake clock).
- ADDED: `MagicLinkForm` — bifurcated factory. Request mode uses `MagicLinkRequestInput` (email only); verify mode uses `MagicLinkVerifyInput` (token from the URL — typically auto-submitted on mount, with a manual fallback button).
- ADDED: Translations namespace `react-ui-pack-auth.*` with full coverage in `en-GB`, `zh-CN`, `ar-AE`, `cy-GB`, `ru-RU` for every label, placeholder, helper text, success message, and error message every form renders.
- ADDED: Module augmentation `declare module "@suaveplan/react-i18n" { interface ContributedTranslationKeys { ... } }` registering every `react-ui-pack-auth.*` key into the brand union.
- ADDED: Module augmentation `declare module "@suaveplan/react-ui/pack" { interface PackComponents { auth: { LoginForm: ...; RegisterForm: ...; PasswordResetForm: ...; TwoFactorForm: ...; MagicLinkForm: ... } } }` registering the pack's component types into the `usePackComponent` lookup union.
- ADDED: `AuthSubmitClient` interface — the injectable contract every form takes as a prop. One method per form (`login`, `register`, `requestPasswordReset`, `confirmPasswordReset`, `verifyTwoFactor`, `requestMagicLink`, `verifyMagicLink`), each returning a typed `Promise<<SchemaOutput>>`.
- ADDED: Error codes via `defineCodes("REACT_UI_PACK_AUTH", [...])`:
- `REACT_UI_PACK_AUTH.SUBMIT_CLIENT_MISSING` — form rendered without a submit client (neither prop nor provider)
- `REACT_UI_PACK_AUTH.PACK_VERSION_MISMATCH` — `peerRange` check against running `@suaveplan/react-ui` version failed at registration
- `REACT_UI_PACK_AUTH.UNKNOWN_FIELD_KIND` — internal sanity check; thrown if a field config references a kind the pack-local registry has not whitelisted
- `REACT_UI_PACK_AUTH.MODE_REQUIRED` — `PasswordResetForm` or `MagicLinkForm` rendered without the mandatory `mode` prop
### `@suaveplan/api-schemas` (extension)
- ADDED to `./auth`: `PasswordResetRequestInput` (`{ email }`), `PasswordResetRequestOutput` (`{ requestedAt }`)
- ADDED to `./auth`: `PasswordResetConfirmInput` (`{ token, newPassword }`), `PasswordResetConfirmOutput` (`{ confirmedAt }`)
- ADDED to `./auth`: `TwoFactorVerifyInput` (`{ code }`), `TwoFactorVerifyOutput` (`{ token, refreshToken, user: LoginUser }`)
- ADDED to `./auth`: `MagicLinkRequestInput` (`{ email }`), `MagicLinkRequestOutput` (`{ requestedAt }`)
- ADDED to `./auth`: `MagicLinkVerifyInput` (`{ token }`), `MagicLinkVerifyOutput` (`{ token, refreshToken, user: LoginUser }`)
These five additions live in `packages/api/api-schemas/src/auth/` alongside `login.ts` / `register.ts`, follow the existing convention (input + output Zod schemas, inferred TypeScript types, sibling `.md`), and are exported from `packages/api/api-schemas/src/auth/index.ts`. They land in this change set rather than a separate change because `react-ui-pack-auth` is the first consumer that needs them — splitting them into a separate change would be a `@suaveplan/api-schemas` change with no consumer.
## Parity Checklist
**N/A — genesis-original; not a migration.**
This package does not exist in `../suaveplan/`. There is no upstream `react-auth-forms` (or equivalently named) library to port. The pack's existence is justified by the master design §1.2 row 5, which scopes it as one of the five new genesis packages. A parity checklist is included by `openspec/AGENTS.md` §0a only when a library is being migrated from upstream — this is a new-build, so the checklist is intentionally absent. The five form components ARE inspired in spirit by the legacy `react-auth` and `react-auth-whitelabel` packages on `main`, but the implementation, public API, and dependency layout are wholly new — there is no shape to preserve and no upstream feature set to enumerate.
## Telemetry
- ADDED: counter `pack.auth.submit_total` with attributes `{ form: "login" | "register" | "passwordResetRequest" | "passwordResetConfirm" | "twoFactor" | "magicLinkRequest" | "magicLinkVerify", outcome: "success" | "validation_error" | "submit_error" }` — incremented on every form-submit completion. Allowlist enforced at the histogram-write site so unbounded label cardinality cannot creep in.
- ADDED: histogram `pack.auth.submit.duration_ms` with attribute `{ form: "<same enum>" }` — wall-clock time from form submit through `onSuccess` / `onError` resolution.
- ADDED: counter `pack.auth.lifecycle_total` with attribute `{ phase: "register" | "unregister", outcome: "success" | "version_mismatch" | "failed" }` — fired by the `onRegister` / `onUnregister` hooks; the `version_mismatch` value carries the structured `pack.version_mismatch` warning out via `@suaveplan/logger` per master design §5.6.
All three instruments are no-ops when no exporter is registered (the `@opentelemetry/api` default behaviour). The pack does not register an SDK — that is the consumer's responsibility.
## Verification approach
Each spec requirement carries a `**Verified by:**` line listing the concrete tests that exercise it.
- `packages/react/react-ui-pack-auth/src/pack/pack.contract.test.tsx` — `authPack` type-conforms to `ComponentPack<'auth'>`; registering it makes `usePackComponent('auth', '<form>')` return the same component reference for all five forms.
- `packages/react/react-ui-pack-auth/src/pack/pack.lifecycle.test.tsx` — `onRegister` is called on mount with the active i18n / theme / logger / signal; `onUnregister` is called on unmount; calling order is FIFO across multiple packs; bundles are added on register and removed on unregister.
- `packages/react/react-ui-pack-auth/src/components/<form>/<form>.test.tsx` (five files) — each form renders without throwing under all five locales (parametric `it.each`); submitting calls the injected `AuthSubmitClient` method exactly once with the typed payload; `onSuccess` fires with the typed response; field-level validation errors render the correct translation key; field labels, placeholders, helper text, and error messages all resolve through `react-i18n`'s catalog (no string literal in the rendered DOM).
- `packages/react/react-ui-pack-auth/src/components/<form>/<form>.stories.tsx` (five files) — `composeStories` round-trips each story through `vitest`; one story per locale per form (× theme variant for the `WithTheme` story).
- `packages/react/react-ui-pack-auth/e2e/auth-submit-success.spec.ts` — Playwright; for each form, fills required fields, submits against a stubbed `AuthSubmitClient`, asserts `onSuccess` payload matches the API-schema shape.
- `packages/react/react-ui-pack-auth/e2e/auth-submit-error.spec.ts` — Playwright; submits against a stub that throws `SuaveplanError`; asserts the form maps the error code to the correct translation key and renders it next to the offending field (or as a form-level alert when no field maps).
- `packages/react/react-ui-pack-auth/e2e/auth-rtl.spec.ts` — Playwright; loads each form under `ar-AE`, asserts `<html dir="rtl">`, asserts the OTP cells flow right-to-left, asserts the password "show / hide" eye icon mirrors.
- `packages/react/react-ui-pack-auth/e2e/auth-themes.spec.ts` — Playwright; loads the `LoginForm` under `light`, `dark`, `highContrast`, and `dracula`; asserts each renders without console errors and axe-core finds zero serious or critical violations.
- `packages/react/react-ui-pack-auth/src/translations/translations.test.ts` — runs the locale-completeness gate; asserts every key in `en-GB.json` exists in the other four files; asserts the generated `augmentation.d.ts` mirrors `en-GB.json` exactly.
## Out of scope
These flows are deliberately deferred — the pack stays narrowly scoped to the five forms the master design names. Each is justified individually:
- **OAuth / social-login buttons** — defer until first consumer ask. `@suaveplan/react-ui-social` is a separate package on `STATUS.md`; combining it with the auth pack would entangle two independently versioned surfaces.
- **Passkey / WebAuthn** — credential management is a wholly different UX (browser-native prompts, no form fields), and the relevant browser APIs (`navigator.credentials.create` / `.get`) deserve their own pack so the auth pack does not pull `@suaveplan/crypto` into the React tree.
- **Password recovery via SMS** — telephony is not an `@suaveplan/api-schemas` capability today; adding it requires a new schema family and a transport contract that does not yet exist.
- **Multi-tenant org-picker** — pre-auth tenant selection is highly product-specific (subdomain routing, query-param routing, header-based routing); the master design does not name it as part of the v1 pack and a generic implementation would be a guess.
- **Session management UI** ("active sessions" list, "log out everywhere" action) — belongs in a follow-on `@suaveplan/react-ui-pack-account` once the auth pack contracts settle.
- **Email-verification confirmation page** — verification typically happens on a server-rendered route with a one-time token, not an interactive form; if a future consumer needs an interactive widget, add it as a sixth form in a follow-on change.
## Impact
### Affected dependents
Run `bunx turbo ls --filter=...@suaveplan/react-ui-pack-auth` to enumerate. Currently zero — this is a leaf pack that no genesis package depends on (verified by `grep -rl "@suaveplan/react-ui-pack-auth" /home/ubuntu/web/genesis/packages/ --include="package.json"` returning empty before this change lands).
- No dependents (leaf pack; verified by `bunx turbo ls --filter=...@suaveplan/react-ui-pack-auth` — empty result).
### Adjacent packages touched in the same change set
- `@suaveplan/api-schemas` — five new auth schemas added under `./auth` (see `## What changes`). Existing exports are not modified; no consumer of `LoginInput`, `RegisterInput`, `RefreshInput`, or `LogoutInput` sees a behaviour change. Tests run: `bunx turbo test --filter=@suaveplan/api-schemas`.
### Prerequisites
This change cannot ship until the four upstream packages it depends on are green on genesis:
- `@suaveplan/react-i18n` (change `enterprise-react-i18n`) — provides `TranslationKey`, `useTranslation`, `<Trans>`, `ContributedTranslationKeys`.
- `@suaveplan/react-ui` (change `enterprise-react-ui-v2` per master design §13) — provides `ComponentPack`, `PackComponents`, `usePackComponent`, every primitive the forms render with.
- `@suaveplan/react-forms` (change `enterprise-react-forms-v2` per master design §13) — provides `createDomainForm`, `useDomainForm`, validation primitives.
- `@suaveplan/api-schemas` — already on genesis; this change extends its `auth` subpath.
The order matches master design §14 Wave 6 ("`react-ui-pack-auth` and `react-ui-theme-dracula` ship together; end-to-end proof that contracts work").