23 KiB
23 KiB
Tasks: enterprise-react-ui-pack-auth
Change ID: enterprise-react-ui-pack-auth
Package: @suaveplan/react-ui-pack-auth
Target path: packages/react/react-ui-pack-auth
Prerequisite changes (must be archived before this change ships): enterprise-react-i18n, enterprise-react-ui-v2, enterprise-react-forms-v2
Prerequisite packages on Verdaccio: @suaveplan/react-i18n, @suaveplan/react-ui, @suaveplan/react-forms, @suaveplan/api-schemas, @suaveplan/error, @suaveplan/types, @suaveplan/testing, @suaveplan/testing-dom, @suaveplan/testing-e2e
0. Pre-flight (non-negotiable)
- 0.1 Confirm clean working tree (
git statusreports nothing) and no in-flight worktrees with uncommitted work (git worktree listthengit statusin each) - 0.2 If any worktrees exist under
.claude/worktrees/, account for each one: still-active agent, work-pending-merge, or stale-and-prune. Stale worktrees MUST be removed (git worktree remove) before opening a new one — code that has been lost twice on main is the failure mode this guards against. - 0.3 Confirm the package set in
MANIFEST.mdmatchesfind packages -maxdepth 3 -name package.jsoncount. Drift here means a prior change archived without regenerating the manifest.
1. Spec authoring
- 1.1 Read
proposal.mdandspecs/react-ui-packs/spec.mdend-to-end - 1.2 Read
openspec/cross-cutting-designs/2026-05-05-react-ui-react-i18n-react-forms.md§5 (component-pack contract), §9 (createDomainForm), §11 (TranslationKey) - 1.3 Read the in-flight
enterprise-react-ui-v2proposal (specifically theComponentPack/PackRegistryProvider/usePackComponentsurface) and confirm the augmentation pattern this pack uses matches that proposal verbatim - 1.4 Read the in-flight
enterprise-react-forms-v2proposal (specificallycreateDomainForm's field-config types and theTranslationKey-typed label / placeholder / helperText / errorMessage constraint) - 1.5
openspec change validate enterprise-react-ui-pack-auth --strictexits with no errors
2. Adjacent api-schemas extension
- 2.1 In
packages/api/api-schemas/src/auth/, addpassword-reset-request.tsexportingPasswordResetRequestInput({ email }) andPasswordResetRequestOutput({ requestedAt }) plus inferred types and sibling.md - 2.2 Add
password-reset-confirm.tsexportingPasswordResetConfirmInput({ token, newPassword }) andPasswordResetConfirmOutput({ confirmedAt }) plus inferred types and sibling.md - 2.3 Add
two-factor-verify.tsexportingTwoFactorVerifyInput({ code }, six-digit string withregex(/^\d{6}$/)refinement) andTwoFactorVerifyOutput({ token, refreshToken, user: LoginUser }) plus inferred types and sibling.md - 2.4 Add
magic-link-request.tsexportingMagicLinkRequestInput({ email }) andMagicLinkRequestOutput({ requestedAt }) plus inferred types and sibling.md - 2.5 Add
magic-link-verify.tsexportingMagicLinkVerifyInput({ token }) andMagicLinkVerifyOutput({ token, refreshToken, user: LoginUser }) plus inferred types and sibling.md - 2.6 Update
packages/api/api-schemas/src/auth/index.tswith explicit named exports for all five new modules (noexport *, per CLAUDE.md §10) - 2.7 Add a sibling
.test.tsper new schema asserting parse success on a happy-path payload, parse failure on every refinement violation (empty, malformed email, invalid token, wrong code length), and idempotent round-trip throughsafeParse - 2.8
bunx turbo lint typecheck test --filter=@suaveplan/api-schemasgreen - 2.9
bunx turbo test:coverage --filter=@suaveplan/api-schemasat 100/100/100/100 across the touched files
3. Scaffold
- 3.1 Create
packages/react/react-ui-pack-auth/withpackage.jsonat version0.1.0,tsconfig.lib.json(extends@suaveplan/typescript-config/react-library.json),vitest.config.ts(usescreateTestConfig()from@suaveplan/testing/config,provider: 'istanbul', thresholds100/100/100/100),vite.config.ts(vite-plugin-dts),playwright.config.ts(usescreatePlaywrightConfig()from@suaveplan/testing-e2e) - 3.2 Dependencies:
@suaveplan/react-forms workspace:^,@suaveplan/api-schemas workspace:^,@suaveplan/error workspace:^,@suaveplan/types workspace:^,@opentelemetry/api catalog:observability,tslib catalog:build,zod catalog:forms - 3.3 Peer dependencies:
react ^18.0.0 || ^19.0.0,@suaveplan/react-ui workspace:^,@suaveplan/react-i18n workspace:^ - 3.4 Dev dependencies:
@suaveplan/react-ui workspace:^,@suaveplan/react-i18n workspace:^,@suaveplan/testing workspace:^,@suaveplan/testing-dom workspace:^,@suaveplan/testing-e2e workspace:^,@vitest/coverage-istanbul catalog:testing,react,react-dom,@types/react,@types/react-dom,vitest,vite,vite-plugin-dts,fast-check catalog:testing - 3.5 NO direct imports of
vitest/bun:test(CLAUDE.md §11) — all test-runner primitives via@suaveplan/testing/runner - 3.6 NO direct imports of
react-hook-form,@radix-ui/*,i18next,react-i18next— all consumed transitively through@suaveplan/react-forms,@suaveplan/react-ui,@suaveplan/react-i18n - 3.7
"sideEffects": ["./src/translations/augmentation.js"]— only the augmentation file has a side effect (registering the bundle); everything else is pure - 3.8 Subpath exports:
.,./pack,./components/login-form/login-form,./components/register-form/register-form,./components/password-reset-form/password-reset-form,./components/two-factor-form/two-factor-form,./components/magic-link-form/magic-link-form,./errors,./translations,./types/auth-submit-client
4. Errors and shared types
- 4.1 Create
src/errors/errors.tswithdefineCodes("REACT_UI_PACK_AUTH", ["SUBMIT_CLIENT_MISSING", "PACK_VERSION_MISMATCH", "UNKNOWN_FIELD_KIND", "MODE_REQUIRED"]); exportREACT_UI_PACK_AUTH_CODESandReactUiPackAuthError extends SuaveplanError<ReactUiPackAuthErrorCode> - 4.2 Sibling
errors.test.ts— every code resolves through the registry, error chain preservescause, JSON wire format round-trips - 4.3 Sibling
errors.md - 4.4 Create
src/types/auth-submit-client.types.tsdefining theAuthSubmitClientinterface — one method per form, each typed against the matching<Form>Input/<Form>Outputfrom@suaveplan/api-schemas/auth - 4.5 Sibling
auth-submit-client.md(no.test.ts— pure type file) - 4.6 Create
src/types/auth-submit-client-provider.tsxexporting<AuthSubmitClientProvider>anduseAuthSubmitClient()(returns the provider's value, throwsSUBMIT_CLIENT_MISSINGwhen no provider AND no prop is passed at the form level) - 4.7 Sibling
auth-submit-client-provider.test.tsxandauth-submit-client-provider.md
5. Translations
- 5.1 Create
src/translations/en-GB.json— every key the five forms reference:react-ui-pack-auth.<form>.title,.subtitle,.<field>.label,.<field>.placeholder,.<field>.helperText,.<field>.errors.<code>,.submit.label,.submit.busyLabel,.cancel.label,.success.title,.success.body,.error.<code>.title,.error.<code>.body. Add the OTP plural string with full Welsh six-form coverage ({remaining, plural, zero {…} one {…} two {…} few {…} many {…} other {…}}). - 5.2 Create
src/translations/zh-CN.jsonmirroring every key - 5.3 Create
src/translations/ar-AE.jsonmirroring every key (RTL — translation values are LTR-marked correctly via Unicode bidi where required) - 5.4 Create
src/translations/cy-GB.jsonmirroring every key (Welsh six-form plural for the OTP attempts string) - 5.5 Create
src/translations/ru-RU.jsonmirroring every key (Russian three-form plural where applicable) - 5.6 Create
src/translations/translations.schema.ts— Zod schema constraining the shape every locale JSON must satisfy (used by the locale-completeness gate to fail fast on missing keys) - 5.7 Create
src/translations/augmentation.d.ts(generated) —declare module "@suaveplan/react-i18n" { interface ContributedTranslationKeys { ... } }registering every key fromen-GB.jsoninto the brand union - 5.8 Create
src/translations/augmentation.ts— runtime side-effect entry that imports each locale JSON statically and exportsbundles: Record<SupportedLocale, Record<string, string>> - 5.9 Create
src/translations/translations.test.ts— locale-completeness gate (every key in en-GB.json exists in every other locale), parse-against-Zod test for every locale, augmentation-mirrors-en-GB test - 5.10 Create
src/translations/translations.md(the only.mdfor the directory; per-JSON sibling docs not required since JSON files have no exports) - 5.11 Wire
bun scripts/generate-translation-types.tsfor this package so re-running it produces a byte-identicalaugmentation.d.ts
6. ComponentPack export
- 6.1 Create
src/pack/pack.tsexportingauthPack: ComponentPack<'auth'>withname: 'auth',version: '0.0.0',peerRange: '^0.1.0',components: { LoginForm, RegisterForm, PasswordResetForm, TwoFactorForm, MagicLinkForm },translations: bundles,onRegister,onUnregister - 6.2
onRegister(ctx)— callsctx.i18n.addResourceBundle('react-ui-pack-auth', bundles[ctx.i18n.locale]), firespack.auth.lifecycle_total{phase=register, outcome=success}counter, logs structuredpack.version_mismatchwarning viactx.loggerwhen the runtime@suaveplan/react-uiversion falls outsidepeerRange - 6.3
onUnregister(ctx)— callsctx.i18n.removeResourceBundle('react-ui-pack-auth', ctx.i18n.locale)and fires the matching counter - 6.4 Create
src/pack/pack.augmentation.d.ts—declare module "@suaveplan/react-ui/pack" { interface PackComponents { auth: { LoginForm: ComponentType<LoginFormProps>; RegisterForm: ComponentType<RegisterFormProps>; PasswordResetForm: ComponentType<PasswordResetFormProps>; TwoFactorForm: ComponentType<TwoFactorFormProps>; MagicLinkForm: ComponentType<MagicLinkFormProps> } } } - 6.5 Create
src/pack/pack.contract.test.tsx—expectTypeOf(authPack).toMatchTypeOf<ComponentPack<'auth'>>(); registering it through<PackRegistryProvider packs={[authPack]}>makesusePackComponent('auth', '<form>')return the same component reference (Object.is) for all five forms - 6.6 Create
src/pack/pack.lifecycle.test.tsx—onRegisteris called with{ i18n, theme, logger, signal }on mount;onUnregisteris called on unmount;addResourceBundleandremoveResourceBundlewere called with the matching arguments;pack.version_mismatchfires only whenpeerRangeis unsatisfied - 6.7 Create
src/pack/pack.md
7. LoginForm
- 7.1 Create
src/components/login-form/login-form.tsbuilding the form viacreateDomainForm({ schema: LoginInput, fields: { email: { kind: 'email', label: 'react-ui-pack-auth.login.email.label', placeholder: 'react-ui-pack-auth.login.email.placeholder', autoComplete: 'email' }, password: { kind: 'password', label: 'react-ui-pack-auth.login.password.label', autoComplete: 'current-password' } }, layout: 'vertical', submit: { label: 'react-ui-pack-auth.login.submit.label', busyLabel: 'react-ui-pack-auth.login.submit.busyLabel', onSubmit: async (values, ctx) => { const result = await ctx.submitClient.login(values); ctx.onSuccess(result); } } }) - 7.2 Export
LoginFormandLoginFormProps - 7.3 Sibling
login-form.test.tsx— renders under all 5 locales (it.each); submits the typed payload exactly once; callsonSuccesswith theLoginOutput-typed value; renders the typed key from the catalog (no string literal); maps a thrownSuaveplanError("AUTH.INVALID_CREDENTIALS")to the correctreact-ui-pack-auth.login.error.AUTH.INVALID_CREDENTIALS.bodykey - 7.4 Sibling
login-form.stories.tsx— Default, Playground, WithI18n (parametric over the 5 locales), WithTheme (parametric over light/dark/highContrast/dracula), WithError (server-error mapping), WithDefaultEmail (the magic-link-prefilled flow) - 7.5 Sibling
login-form.md
8. RegisterForm
- 8.1 Create
src/components/register-form/register-form.tsbuilding the form viacreateDomainForm({ schema: RegisterInput.extend({ confirmPassword: z.string().min(8), terms: z.literal(true) }), fields: { email, name, password, confirmPassword: { kind: 'password', label: 'react-ui-pack-auth.register.confirmPassword.label', validate: { matches: 'password' } }, terms: { kind: 'checkbox', label: 'react-ui-pack-auth.register.terms.label', required: true } }, layout: 'vertical', submit: { ... onSuccess wires onto AuthSubmitClient.register ... } }) - 8.2 Export
RegisterFormandRegisterFormProps - 8.3 Sibling
register-form.test.tsx— under all 5 locales; cross-field validator fires whenconfirmPassword !== passwordand rendersreact-ui-pack-auth.register.confirmPassword.errors.mismatch; thetermscheckbox is required and submit stays disabled until checked; submit returns aRegisterOutput-typed value - 8.4 Sibling
register-form.stories.tsx— Default, Playground, WithI18n, WithTheme, WithMismatchedPasswords, WithUncheckedTerms, WithExistingEmailError - 8.5 Sibling
register-form.md
9. PasswordResetForm
- 9.1 Create
src/components/password-reset-form/password-reset-form.tsexporting a single component whosemode: 'request' | 'confirm'prop selects between two factory-built forms —mode = 'request'builds againstPasswordResetRequestInput,mode = 'confirm'builds againstPasswordResetConfirmInput - 9.2 When
modeis missing or invalid, throwReactUiPackAuthError(MODE_REQUIRED)at render time - 9.3 Export
PasswordResetFormandPasswordResetFormProps(the props discriminated union overmode) - 9.4 Sibling
password-reset-form.test.tsx— both modes render under all 5 locales; request-mode submit callssubmitClient.requestPasswordResetonce; confirm-mode submit callssubmitClient.confirmPasswordResetonce with{ token, newPassword }; missingmodethrowsMODE_REQUIRED - 9.5 Sibling
password-reset-form.stories.tsx— RequestDefault, ConfirmDefault, Playground, WithI18n, WithTheme, RequestSuccess, ConfirmSuccess, RequestRateLimitError, ConfirmInvalidTokenError - 9.6 Sibling
password-reset-form.md
10. TwoFactorForm
- 10.1 Create
src/components/two-factor-form/two-factor-form.tsbuilding viacreateDomainForm({ schema: TwoFactorVerifyInput, fields: { code: { kind: 'otp', length: 6, label: 'react-ui-pack-auth.twoFactor.code.label', helperText: 'react-ui-pack-auth.twoFactor.code.helperText' } }, layout: 'vertical', submit: { ... } })— thekind: 'otp'field renders thereact-uiInputOTPprimitive - 10.2 Add a "send new code" sub-component gated by a 60-second countdown; the countdown is driven by an injectable
clock?: Clockprop (defaultDate.now-based) so tests can use aFakeClockfrom@suaveplan/testing - 10.3 The remaining-attempts string uses the
react-ui-pack-auth.twoFactor.code.helperTextkey with acountargument, exercising Welsh six-form plurals undercy-GB - 10.4 Export
TwoFactorFormandTwoFactorFormProps - 10.5 Sibling
two-factor-form.test.tsx— renders under all 5 locales; six-digit input pattern is enforced (submit disabled until/^\d{6}$/); submit callssubmitClient.verifyTwoFactorwith{ code }; "send new code" is disabled while the countdown is active and re-enables when the FakeClock advances 60 s; Welsh plural categorieszero / one / two / few / many / otherall resolve to the right string - 10.6 Sibling
two-factor-form.stories.tsx— Default, Playground, WithI18n, WithTheme, WithCountdownActive, WithCountdownExpired, WithInvalidCode, WithWelshPlurals (one story per Welsh plural category) - 10.7 Sibling
two-factor-form.md
11. MagicLinkForm
- 11.1 Create
src/components/magic-link-form/magic-link-form.tsexporting a component whosemode: 'request' | 'verify'prop selects between two factory-built forms —mode = 'request'builds againstMagicLinkRequestInput,mode = 'verify'builds againstMagicLinkVerifyInput - 11.2 In
verifymode, accept an optionaltokenprop — when present, auto-submit on mount; when absent, render the manual fallback form - 11.3 Throw
ReactUiPackAuthError(MODE_REQUIRED)ifmodeis missing - 11.4 Export
MagicLinkFormandMagicLinkFormProps - 11.5 Sibling
magic-link-form.test.tsx— both modes render under all 5 locales; request-mode submit callssubmitClient.requestMagicLink; verify-mode auto-submit firessubmitClient.verifyMagicLinkexactly once on mount whentokenis provided; manual-fallback button submits the typed{ token }payload otherwise - 11.6 Sibling
magic-link-form.stories.tsx— RequestDefault, VerifyAutoSubmit, VerifyManualFallback, Playground, WithI18n, WithTheme, RequestRateLimitError, VerifyExpiredTokenError - 11.7 Sibling
magic-link-form.md
12. Telemetry
- 12.1 Create
src/telemetry/telemetry.tsexporting the three instruments (pack.auth.submit_total,pack.auth.submit.duration_ms,pack.auth.lifecycle_total); enforce theoutcomeandphaseallowlists at the recording site so unbounded label cardinality is impossible - 12.2 Wire each form's submit pipeline through the counter + histogram (success path increments
outcome=success, validation failuresoutcome=validation_error, thrown errorsoutcome=submit_error) - 12.3 Sibling
telemetry.test.ts— counters fire with the right attributes; histogram records duration; allowlist rejects unknown values - 12.4 Sibling
telemetry.md
13. Public root + README
- 13.1 Create
src/index.tswith explicit named re-exports (noexport *, per CLAUDE.md §10) forauthPack, the five<Form>components, the matching<Form>Propstypes,AuthSubmitClient,AuthSubmitClientProvider,useAuthSubmitClient,ReactUiPackAuthError,REACT_UI_PACK_AUTH_CODES. Side-effect import./translations/augmentation.jsand./pack/pack.augmentation.jsso type augmentation and runtime bundle registration happen on root import. - 13.2 Create
README.mdat the package root with: package name, one-paragraph description, install snippet (bun add @suaveplan/react-ui-pack-auth), quick-start (registering the pack via<PackRegistryProvider packs={[authPack]}>and rendering<LoginForm submitClient={mySubmitClient} onSuccess={...} />), Modules section linking to every co-located.md - 13.3 Run the per-package doc-coverage gate: every implementation
.ts/.tsx(excluding tests, types, stories, barrels) has a sibling.md≥ 200 words with simple + advanced examples + API reference
14. End-to-end + a11y
- 14.1 Create
e2e/auth-submit-success.spec.ts— for each of the five forms, fill required fields and submit against a stubAuthSubmitClient; assertonSuccesspayload matches the API-schema shape - 14.2 Create
e2e/auth-submit-error.spec.ts— submit against a stub that throws a structuredSuaveplanError; assert the form maps the error code to the correct translation key and renders next to the offending field (or as a form-level alert when no field maps) - 14.3 Create
e2e/auth-rtl.spec.ts— load each form underar-AE; assert<html dir="rtl">; assert OTP cells flow right-to-left; assert the password show/hide eye icon mirrors - 14.4 Create
e2e/auth-themes.spec.ts— loadLoginFormunder each built-in theme + the dracula example theme; assert no console errors; assert axe-core finds zero serious or critical violations - 14.5 Create
e2e/pack-lifecycle.spec.ts— mount the<PackRegistryProvider>withauthPack; assert bundles are added; unmount; assert bundles are removed andusePackComponent('auth', 'LoginForm')now returnsundefined
15. Pre-publish gate
- 15.1
bunx turbo lint typecheck test --filter=@suaveplan/react-ui-pack-authgreen - 15.2
bunx turbo test:coverage --filter=@suaveplan/react-ui-pack-authat 100/100/100/100 - 15.3
bunx turbo build --filter=@suaveplan/react-ui-pack-authgreen - 15.4 Banned-pattern scan zero matches:
grep -rn "@deprecated\|TODO\|FIXME\|ts-ignore\|ts-expect-error\|\.skip\|\.only\|\bany\b\|console\.log" packages/react/react-ui-pack-auth/src/ --include="*.ts" --include="*.tsx" - 15.5 Test-runner-import scan zero matches:
grep -rnE "from ['\"](vitest|bun:test)['\"]" packages/react/react-ui-pack-auth/src/ --include="*.ts" --include="*.tsx" - 15.6 Barrel-shape scan zero matches in feature barrels:
grep -rn "^\s*export\s\+\(type\s\+\)\?\*\s\+from" packages/react/react-ui-pack-auth/src/ --include="index.ts" | grep -v "/src/index\.ts:" - 15.7 Hardcoded-string scan against the §11 watchlist (label, placeholder, helperText, errorMessage, aria-label, title, alt) zero matches: every value resolves to a
TranslationKey - 15.8 Locale-completeness scan green — every key in
en-GB.jsonexists inzh-CN.json,ar-AE.json,cy-GB.json,ru-RU.json - 15.9 Translation-key-existence scan green — every
t('...')call references a key that exists inen-GB.json - 15.10 React single-version check:
find . -path "*/node_modules/react/package.json" | head -5returns one path - 15.11 Subpath-export count matches feature module count (per CLAUDE.md §6)
- 15.12 Dependent sweep
bunx turbo test --filter=...@suaveplan/react-ui-pack-auth(expect zero dependents on first ship; future packs that consume this will be picked up automatically)
16. Manifest + STATUS refresh
- 16.1 Add
@suaveplan/react-ui-pack-authto the package listing inCLAUDE.mdunder thereact-uiplugin's package list (immediately afterreact-uiandreact-ui-theme-dracula) - 16.2 Run
bun scripts/folder-reorg/generate-manifest.tsto refreshMANIFEST.md - 16.3 Run
bun scripts/generate-missing.tsto refreshMISSING.yml - 16.4 Update
STATUS.mdto move@suaveplan/react-ui-pack-auth(if it appears in Pending) to the Completed section, or add a new Completed row if it does not appear in Pending - 16.5 Update
SUMMARY.mdwith a per-package paragraph reflecting the shipped surface (five forms, pack contract proof, five locales, AuthSubmitClient injection) - 16.6
bunx markdownlint-cli2 --fix— re-stage modified files;bunx markdownlint-cli2exits 0
17. OpenSpec archival
- 17.1 Fold
specs/react-ui-packs/spec.mdintoopenspec/specs/react-ui-packs/spec.md(creating the canonical spec file since this is the first change against the capability) - 17.2 Move
openspec/changes/enterprise-react-ui-pack-auth/→openspec/archive/<yyyy-mm-dd>-enterprise-react-ui-pack-auth/ - 17.3 Confirm
git statusshows only intentional changes; no scratch / debug artifacts remain