@dloizides/auth-web 1.12.1 → 1.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,386 +1,431 @@
1
- # Changelog
2
-
3
- ## 1.12.1 (2026-07-17)
4
-
5
- Internal refactor only — **no API change, no behaviour change**. The public
6
- export surface (`src/index.ts`) is byte-identical to 1.12.0; every consumer
7
- (zygos, katalogos, erevna, kefi, ichnos) keeps importing the same symbols with
8
- the same types from the same entry point.
9
-
10
- Two oversized source files from the demo-credential work were split to satisfy
11
- the repo size limits (files < 300 lines, components < 200):
12
-
13
- - **`src/components/labels.ts` (511 lines) → `src/components/labels/`.** The
14
- single label bag is now one file per component family (`loginLabels.ts`,
15
- `forgotPasswordLabels.ts`, `otpLabels.ts`, `pinLabels.ts`, `devicePinLabels.ts`,
16
- `passkeyLabels.ts`, `preferredMethodLabels.ts`, `demoLabels.ts`,
17
- `resetPasswordLabels.ts`) behind a `labels/index.ts` barrel that re-exports the
18
- exact same names, so `./labels` remains a single import site.
19
- - **`src/components/LoginForm.tsx` (253 lines) → render body slimmed.** The
20
- prop-doc-heavy `LoginFormProps` interface moved to a co-located
21
- `LoginForm.types.ts` (re-exported from `LoginForm.tsx`), and the submit branch
22
- logic moved into a unit-tested `useLoginSubmit` hook.
23
-
24
- ### Changed
25
-
26
- - No runtime change. Splitting only; identical bundled output.
27
-
28
- Completes the demo-credential feature for **BFF-fronted apps**, whose demo pair
29
- arrives ASYNChronously from `GET /bff/config` rather than a module-level const (a
30
- const would bake the credentials into a bundle handed to a customer to
31
- self-deploy). Two gaps closed:
32
-
33
- 1. `prefillOnMount` was inert for async credentials (1.11.0 bug).
34
- 2. `useBffLoginConfig().config` dropped the `demo` field the BFF now serves.
35
-
36
- Requires `@dloizides/auth-client` **>= 4.2.0** (which surfaces `demo` on
37
- `BffLoginConfig`) — the peer floor is raised accordingly. Additive for existing
38
- consumers: the new `LoginForm` prop is optional and the synchronous /
39
- module-const path is byte-identical.
40
-
41
- ### Fixed
42
-
43
- - **`prefillOnMount` now honours credentials that arrive AFTER mount.** `useState`
44
- reads its initial argument on the first render only, so a demo pair landing a
45
- render later (every BFF app) was silently dropped and the form stayed empty
46
- beside a populated hint. `useDemoCredentials` now seeds when the resolved pair
47
- transitions to non-null (or to a genuinely different pair), gated by VALUE so a
48
- react-query refetch handing back a fresh-but-equal object does not re-seed on a
49
- loop.
50
-
51
- ### Added
52
-
53
- - **`config.demo` carries through `useBffLoginConfig`.** `BffLoginConfig` gains
54
- `demo?: DemoCredentials | null`, so the intended wiring works end-to-end:
55
- ```tsx
56
- const { config } = useBffLoginConfig(bffAuthClient);
57
- const demo = useDemoCredentials({ credentials: config.demo ?? undefined, prefillOnMount: true });
58
- ```
59
- - **`PrefillIntent`** enum + **`LoginForm` gains `prefillIntent`** — the "don't
60
- fight the user" control. An `Opportunistic` re-seed (a late config fetch) is
61
- DROPPED unless both fields are still empty, so it never overwrites what a real
62
- account holder is typing while `/bff/config` is in flight; an `Explicit` re-seed
63
- (the user tapped "use these credentials") always applies. `useDemoCredentials`
64
- sets the right intent automatically — most apps never touch this prop.
65
- - **`DemoCredentials` is now re-exported from `@dloizides/auth-client`** (its
66
- canonical owner) rather than defined locally, so there is exactly ONE shape
67
- across the two packages. Structurally identical to 1.11.0 — no consumer change.
68
-
69
- ### "Don't fight the user" — what the hook can and cannot know
70
-
71
- `useDemoCredentials` does not own the input fields (`<LoginForm>` does), so it
72
- cannot observe keystrokes. The interaction test it CAN make is on the field
73
- values: an opportunistic seed applies only when both fields are still empty
74
- (pristine). This is intentionally conservative — a user who typed and then
75
- cleared a field keeps control for the rest of the session (a dropped
76
- opportunistic bump is not retried). An explicit user request is never subject to
77
- this test.
78
-
79
- ## 1.11.0 (2026-07-17)
80
-
81
- Additive release adding **public demo-credential support** for products with an
82
- open, published demo account (Zygos is the first). Purely additive — every 1.10.0
83
- export is unchanged, and an app that imports none of the new exports renders
84
- byte-identically: the three new `<LoginForm>` props are all optional and inert
85
- when omitted.
86
-
87
- It fills form fields and nothing else. **No auth bypass**: the demo login travels
88
- the ordinary path (same BFF, same Keycloak, same session cookie, same roles), and
89
- no credential ships inside this package — the host supplies the pair.
90
-
91
- ### Added
92
-
93
- - **`<DemoCredentialsHint>`** — a themed panel for the login card showing a
94
- published demo username + password with a one-tap "use these credentials"
95
- action. Takes the usual `AuthTheme` / `labels` / `testIdPrefix` props. Values
96
- render as selectable monospace chips (an `l`/`1` in a published password must
97
- not be a guess). Omit `onUse` for a read-only panel.
98
-
99
- **Fails closed**: no credentials — or a half-configured pair (blank username,
100
- blank password, or a field that is `undefined` at runtime because an env var
101
- was never set) — renders `null`. Zero DOM. A misconfigured demo can never
102
- publish an empty credentials panel.
103
-
104
- - **`useDemoCredentials({ credentials?, prefillOnMount? })`** — the hint↔form
105
- wiring, so no app hand-rolls it. Returns two prop bags to spread:
106
- `hintProps` (`{ credentials, onUse }`) and `loginFormProps`
107
- (`{ initialUsername, initialPassword, prefillNonce }`).
108
-
109
- `prefillOnMount` (default `false`) makes the form arrive already filled, so
110
- signing in as the demo user is a single tap on "Sign in".
111
-
112
- - **`resolveDemoCredentials(credentials?)`** — the exported fail-closed gate the
113
- above share: returns a trimmed pair, or `null` when there is no usable demo.
114
-
115
- - **`DemoCredentials`** type and `DEFAULT_DEMO_CREDENTIALS_HINT_LABELS` /
116
- `DemoCredentialsHintLabels` (all copy stays app-supplied, as everywhere else).
117
-
118
- - **`<LoginForm>` gains `initialUsername` / `initialPassword` / `prefillNonce`.**
119
- The `initial*` props **seed** the fields; they do not own them — every value
120
- stays editable and clearable, so a real account holder can always wipe the demo
121
- values and sign in with their own. `prefillNonce` is the re-seed signal:
122
- changing it re-applies the seed over fields the user has edited or cleared.
123
-
124
- The nonce is not ceremony. `useState` ignores its initial argument on
125
- re-render, so a fill button wired to `initialUsername` alone would render a
126
- form that stays stubbornly empty — the nonce is what makes "click the hint →
127
- form fills" work at all. `useDemoCredentials` manages it for you.
128
-
129
- ### Internal
130
-
131
- - `useLoginPrefill` — extracts `<LoginForm>`'s credential field state (the only
132
- part of that component with real logic), so the seed/re-seed contract is unit
133
- tested directly rather than through the JSX.
134
-
135
- ## 1.10.0 (2026-07-17)
136
-
137
- Additive release adding the shared **logout sequencer**. Purely additive — every
138
- 1.9.x export is unchanged, no component behaviour changes, and apps that do not
139
- import `performBffLogout` are byte-identical.
140
-
141
- ### Added
142
-
143
- - **`performBffLogout({ client, onClearSession?, onRedirect?, onError?, navigateToIdp? })`**
144
- — the shared logout sequence, extracted because all six consuming apps
145
- hand-rolled it in their own `AuthProvider` and the orderings disagreed. Runs:
146
- 1. `onClearSession()` — local state first, so the UI reflects the sign-out
147
- immediately.
148
- 2. `await client.logout({ redirect: false })` — lets the `POST /bff/logout`
149
- SETTLE, and takes the IdP logout URL rather than letting the client navigate
150
- mid-sequence.
151
- 3. Navigates exactly once, last: to the IdP logout URL when the BFF returned
152
- one (ending the IdP browser session), otherwise `onRedirect()`. When it
153
- navigates to the IdP it deliberately does NOT also call `onRedirect` — two
154
- competing navigations would race, and the IdP round-trip already returns the
155
- user to the app.
156
-
157
- Never rejects; always navigates, even when the BFF logout fails (a down BFF
158
- must not trap the user on a page they asked to leave).
159
- - **`BffLogoutClient`** / **`PerformBffLogoutOptions`** types.
160
-
161
- ### Why
162
-
163
- `zygos-web` and `agora-web` navigated FIRST and called the BFF second:
164
-
165
- ```ts
166
- redirectTo('/(auth)/login'); // → window.location.replace() → document unload
167
- await bffAuthClient.logout(); // races the unload — may never be sent
168
- ```
169
-
170
- That is wrong twice over:
171
-
172
- 1. **The BFF call can be cancelled.** A document navigation kills in-flight
173
- fetches, so `POST /bff/logout` may never reach the server: the SPA looks
174
- logged out while the httpOnly session cookie stays alive. The `catch` logged
175
- it "non-fatal", so the defect was silent.
176
- 2. **The IdP session survives.** `BffAuthClient.logout()` ends the identity
177
- provider's browser session with a top-level navigation to its RP-initiated
178
- logout URL (only a real navigation carries the IdP's own cookie — a
179
- back-channel POST cannot clear it). An app that navigates to its own login
180
- route first pre-empts that navigation, so the IdP still considers the user
181
- signed in and the next authorize round-trip silently re-authenticates them —
182
- the "sign-out that signs you back in" bug the client's own docs warn about.
183
-
184
- Synthetic tests that stub the client never reproduce either failure (there is no
185
- real navigation to cancel the request), which is why they survived. The helper
186
- owns the sequence so neither can be reintroduced per-app.
187
-
188
- ## 1.8.0 (2026-07-09)
189
-
190
- Additive release for the Move 2b passkey in-app rebuild — the passkey ceremony
191
- now runs **in-page** (`navigator.credentials.*` against the BFF's same-origin
192
- `/bff/passkey/*` fetch endpoints) instead of a full-page redirect to Keycloak.
193
- **Inert by default / no behaviour change until the BFF's WebAuthn RP is
194
- configured:** when the RP is off the options endpoint returns 501 and the hook
195
- transparently falls back to the legacy redirect. No breaking changes; every
196
- 1.7.x export is unchanged and the legacy `startPasskeyLogin` /
197
- `startPasskeyRegistration` navigation helpers stay exported (they ARE the
198
- fallback).
199
-
200
- ### Added
201
-
202
- - **`usePasskeyCeremony({ mode, returnUrl, onSuccess })`** — the react-query-free
203
- in-page ceremony hook: options fetch → `navigator.credentials.create/get` →
204
- verify fetch. Returns `{ status, error, isSupported, start, reset }`. Falls
205
- back to the legacy Keycloak redirect on a 501 (RP not configured), any non-2xx
206
- options response, or an unsupported browser — so nothing regresses until the
207
- owner flips `Bff:Passkey`. A cancelled/timed-out ceremony (`NotAllowedError` /
208
- `AbortError`) surfaces as `'cancelled'`, anything else as `'failed'`.
209
- - **`PasskeyCeremonyMode`** (`Login` / `Register`) and **`PasskeyCeremonyStatus`**
210
- (`Idle` / `Running` / `Success` / `Error`) `const enum`s.
211
-
212
- ### Changed
213
-
214
- - **`<PasskeyLoginButton>`** and **`<PasskeySettingsCard>`** now drive
215
- `usePasskeyCeremony` instead of `window.location.assign`. Same exported props,
216
- same testIDs — a drop-in bump for consumers; the in-page path only activates
217
- once the BFF RP is configured, otherwise they behave exactly as before.
218
-
219
- ## 1.7.0 (2026-07-07)
220
-
221
- Additive release for the Kefi multi-role overhaul — the shared machinery for a
222
- cross-dashboard nav. No breaking changes; every 1.6.x export is unchanged.
223
-
224
- ### Added
225
-
226
- - **`resolveAccessibleRoutes(user, table)`** — the multi-role companion to
227
- `resolvePostLoginRoute`. Returns the full, ordered list of `RoleRoute` entries
228
- the user's role-set unlocks (not just the top one), reusing the same
229
- `collectUserRoles` reader and `RoleRouteTable`. Table (priority) order is
230
- preserved; the table `fallback` is never included; a role-less user gets `[]`.
231
- Pure and app-agnostic — an app renders the result as a cross-dashboard nav.
232
- - **`RoleRoute.labelKey?`** — optional translation key carried per route so a
233
- consuming app can render a localized label per accessible dashboard. Ignored
234
- by `resolvePostLoginRoute`; the package stays i18n-agnostic (carries the key,
235
- the app resolves it).
236
-
237
- ## 1.3.0 (2026-05-31)
238
-
239
- Additive release for the auth-UX reuse roll-out (feature-reuse-roadmap #1).
240
- Promotes the "Forgot password?" modal body the product apps each hand-rolled
241
- into a single shared, **react-query-free** export so katalogos / erevna / kefi
242
- stop duplicating it. No breaking changes; every 1.2.x export is unchanged.
243
-
244
- ### Added
245
-
246
- - **`<ForgotPasswordFields>`** — the embedded, themeable "request a reset link"
247
- body, sized to live inside an app-owned modal shell (no screen/card wrapper,
248
- no title — the host modal owns those). Adds the two modal affordances a
249
- screen-shaped form lacks: a `Cancel` button beside submit and a `Close` button
250
- on the success state, each rendered only when its handler is supplied. Clears
251
- itself when `visible` flips to `false` so reopening starts fresh.
252
- - **`useForgotPasswordSubmit`** — the headless, **react-query-free** forgot-
253
- password logic: a plain `useState` + `client.forgotPassword(...)` promise
254
- chain that runs on a provider-less login route (where the existing
255
- `useBffForgotPassword` `useMutation` crashes with "No QueryClient set"). Email
256
- validation gating via `canSubmit`, anti-enumeration success, `hasNetworkError`,
257
- and `reset()`. Also exports the `isValidForgotPasswordEmail` helper.
258
- - **`ForgotPasswordFieldsLabels`** + **`DEFAULT_FORGOT_PASSWORD_FIELDS_LABELS`** —
259
- the forgot-password label bag plus the modal `cancel` / `close` strings.
260
- - New `AuthTestIds` entries `forgotPasswordCancelButton` /
261
- `forgotPasswordCloseButton`.
262
-
263
- ### Notes
264
-
265
- - The existing screen-shaped `<ForgotPasswordForm>` (react-query-based) is
266
- unchanged. A future cleanup may re-base it on `useForgotPasswordSubmit` to drop
267
- its react-query coupling; deferred to keep this release additive.
268
-
269
- ## 1.2.0 (2026-05-22)
270
-
271
- Additive release for Phase 3d of the unified-auth plan — the event-scoped PIN
272
- UI. Adds the native, branded event-PIN login surface so an app (Kefi) can offer
273
- "sign in with your event PIN" for operational staff (door / DJ / media) without
274
- the user ever bouncing to Keycloak's hosted UI. No breaking changes; every
275
- 1.1.0 export is unchanged.
276
-
277
- ### Added
278
-
279
- - **`<PinForm>`** — the ready-made, themeable single-step event-PIN login form,
280
- built on `usePinLogin`. A PIN field + a "sign in" button; the
281
- `eventExternalId` is a prop (the event context comes from the route/page, it
282
- is never typed by the user). Styled by the same `AuthTheme` token bag as the
283
- other forms; copy is supplied through the i18n-agnostic `PinFormLabels` bag
284
- (English `DEFAULT_PIN_LABELS` fallback). `onSuccess` hands back the signed-in
285
- `BffUser`.
286
- - **`usePinLogin`** — the headless PIN hook (the custom-layout escape hatch). A
287
- single-step flow: `submit(pin)` → `POST /bff/pin/login` → `BffUser`, with
288
- `reset()`. Mirrors `useOtpLogin` (mounted-guard, memoised result,
289
- latest-`eventExternalId` ref).
290
- - **`PinFormLabels`** + **`DEFAULT_PIN_LABELS`** — the PIN label bag.
291
- - New `AuthTestIds` entries for every PIN interactive element.
292
- - Re-exports `BffPinLoginRequest` from `@dloizides/auth-client`.
293
-
294
- ### Changed
295
-
296
- - Peer dependency `@dloizides/auth-client` raised to `>=3.2.0` — the version
297
- that adds `BffAuthClient.pinLogin`.
298
-
299
- ## 1.1.0 (2026-05-22)
300
-
301
- Additive release for Phase 2d of the unified-auth plan — the email-OTP UI. Adds
302
- the native, branded email-OTP login surface so an app can offer "sign in with a
303
- code" without the user ever bouncing to Keycloak's hosted UI. No breaking
304
- changes; every 1.0.0 export is unchanged.
305
-
306
- ### Added
307
-
308
- - **`<OtpForm>`** — the ready-made, themeable two-step email-OTP login form,
309
- built on `useOtpLogin`. Step 1 collects the email and sends a code; step 2
310
- collects the code, verifies it, and offers "resend code" and "use a different
311
- email". Styled by the same `AuthTheme` token bag as the other forms; copy is
312
- supplied through the i18n-agnostic `OtpFormLabels` bag (English
313
- `DEFAULT_OTP_LABELS` fallback). `onSuccess` hands back the signed-in `BffUser`.
314
- - **`useOtpLogin`** — the headless OTP hook (the custom-layout escape hatch). A
315
- small forward state machine over `OtpLoginStep`: `requestCode(identifier)` →
316
- `EnterCode`, then `verifyCode(otp)` → `BffUser`, with `resend()` and
317
- `reset()`. Mirrors `useBffAuth` (mounted-guard, memoised result).
318
- - **`OtpLoginStep`** — the `RequestCode` / `EnterCode` step enum.
319
- - **`OtpFormLabels`** + **`DEFAULT_OTP_LABELS`** — the OTP label bag.
320
- - New `AuthTestIds` entries for every OTP interactive element.
321
- - Re-exports `BffOtpRequestRequest`, `BffOtpVerifyRequest` and
322
- `BffOtpRequestResult` from `@dloizides/auth-client`.
323
-
324
- ### Changed
325
-
326
- - Peer dependency `@dloizides/auth-client` raised to `>=3.1.0` — the version
327
- that ships `BffAuthClient.requestOtp` / `.verifyOtp` (`POST /bff/otp/request`
328
- and `POST /bff/otp/verify`).
329
-
330
- ### Notes
331
-
332
- - Still strictly thin: UI + the same-origin `/bff/*` client. The OTP client
333
- methods carry the `X-BFF-Csrf` header like every other state-changing call;
334
- no token handling, no secrets in the browser.
335
- - This release adds the OTP surface to the package only. Wiring an app onto it
336
- (the Kefi pilot) is Phase 3.
337
-
338
- ## 1.0.0 (2026-05-22)
339
-
340
- Initial release — the frontend half of the unified-auth plan (Phase 1b).
341
-
342
- `@dloizides/auth-web` is the shared, themeable, branded auth UI for the
343
- dloizides.com portfolio. It is extracted and generalised from the per-app auth
344
- code that lived inside `apps/katalogos-web` so katalogos, erevna and kefi can
345
- share one implementation instead of copy-pasting auth adapters.
346
-
347
- ### Added
348
-
349
- - **Themeable components**
350
- - `<LoginForm>` — password login, built on `useBffAuth`.
351
- - `<ForgotPasswordForm>` — request-a-reset-link, built on
352
- `useBffForgotPassword`; no-enumeration confirmation state.
353
- - `<ResetPasswordForm>` — choose-a-new-password, built on
354
- `useResetPasswordForm`.
355
- - All three are styled by an `AuthTheme` design-token bag so visually
356
- distinct apps share one implementation.
357
- - **Theming** — `AuthThemeProvider`, `useAuthTheme`, `defaultAuthTheme`, and
358
- the `AuthTheme` token contract (colors / radii / spacing / typography).
359
- Precedence: explicit `theme` prop → `<AuthThemeProvider>` context → default.
360
- - **i18n-agnostic label bags** — `LoginFormLabels`, `ForgotPasswordFormLabels`,
361
- `ResetPasswordFormLabels` plus English `DEFAULT_*` constants. The package
362
- ships no `t()` / `FM()`; consuming apps pass already-localised strings.
363
- - **Headless hooks** (the custom-layout escape hatch)
364
- - `useBffAuth` — login / logout / `/bff/me` session, lifecycle status.
365
- - `useBffForgotPassword`, `useBffResetPassword` — React Query mutations.
366
- - `useResetPasswordForm` — reset-password form-state + validation logic.
367
- - **`createBffAuthClient`** — one-line wiring of a same-origin `BffAuthClient`
368
- with a lazily-resolved `fetch` (load-time side-effect-free).
369
- - **Role-based post-login router** — `resolvePostLoginRoute(user, table)` and
370
- `collectUserRoles`. The consuming app supplies the role → route table.
371
- - **Password policy** — `validatePasswordPolicy`, `isPasswordValid`,
372
- `PasswordPolicyError`; mirrors the TenantService backend rules.
373
- - **Test IDs** — `AuthTestIds` + `withTestIdPrefix` for E2E targeting.
374
- - Re-exports `BffAuthClient`, `createFetchHttpClient` and the BFF request /
375
- response types from `@dloizides/auth-client` so consumers have a single
376
- import surface.
377
-
378
- ### Notes
379
-
380
- - Strictly thin: UI + a same-origin `/bff/*` fetch client + the router helper.
381
- **No secrets, no token handling, no Keycloak calls** — the per-app BFF owns
382
- all of that server-side.
383
- - Built on `@dloizides/auth-client` (peer dependency `>=3.0.0`), which ships
384
- `BffAuthClient`.
385
- - This release creates the package only. Porting `apps/katalogos-web` onto it
386
- is Phase 1c.
1
+ # Changelog
2
+
3
+ ## 1.13.0 (2026-07-19)
4
+
5
+ `<DemoCredentialsHint>` gets a **discreet** presentation, and it is now the
6
+ default. The demo stops competing with the sign-in form on login pages that real
7
+ customers use every day.
8
+
9
+ ### Added
10
+
11
+ - **`DemoCredentialsHintVariant`** (`Panel` | `Disclosure`), exported from the
12
+ package root.
13
+ - **`Disclosure` variant** — one quiet, centred "Try the demo" line that expands
14
+ the credentials and the fill button on tap. Collapsed on mount.
15
+ - **`toggle` / `toggleHint` labels** on `DemoCredentialsHintLabels`, so the
16
+ trigger copy is localised by the host like every other string in the package.
17
+ - **`AuthTestIds.demoCredentialsToggle`** (`auth-demo-credentials-toggle`).
18
+
19
+ ### Changed
20
+
21
+ - ⚠️ **`<DemoCredentialsHint>` now defaults to `Disclosure`.** A consumer that
22
+ wants the old always-open block must pass
23
+ `variant={DemoCredentialsHintVariant.Panel}`. No API broke — every existing
24
+ prop, `testID` and callback behaves identically — but the rendered output
25
+ changes, so this is called out as loudly as a breaking change.
26
+ - The credential rows + fill button moved into an internal
27
+ `<DemoCredentialsBody>` shared by both variants, so the two presentations can
28
+ never drift apart.
29
+
30
+ ### Notes on the design
31
+
32
+ Discretion is carried by **size, placement and the absence of chrome** — the
33
+ trigger is caption-sized, centred, and has no fill, border or brand rail. It is
34
+ NOT carried by lowering contrast: the trigger draws `colors.textSecondary`, the
35
+ same AA-held token as the rest of the package's helper copy, so it inherits
36
+ whatever contrast guarantee the consuming app's theme already makes.
37
+
38
+ The trigger stays a real focusable `accessibilityRole="button"` carrying
39
+ `accessibilityState.expanded`, so screen readers announce that it exists and
40
+ whether it is open. Visually quiet is not the same as hidden.
41
+
42
+ The expanded body renders **below** the trigger, and the component is intended to
43
+ sit **below** `<LoginForm>`. Expanding therefore grows the card downward and never
44
+ displaces the inputs the visitor is using — mounting it above the form is what
45
+ would make every expansion shove the fields down the page.
46
+
47
+
48
+ ## 1.12.1 (2026-07-17)
49
+
50
+ Internal refactor only — **no API change, no behaviour change**. The public
51
+ export surface (`src/index.ts`) is byte-identical to 1.12.0; every consumer
52
+ (zygos, katalogos, erevna, kefi, ichnos) keeps importing the same symbols with
53
+ the same types from the same entry point.
54
+
55
+ Two oversized source files from the demo-credential work were split to satisfy
56
+ the repo size limits (files < 300 lines, components < 200):
57
+
58
+ - **`src/components/labels.ts` (511 lines) → `src/components/labels/`.** The
59
+ single label bag is now one file per component family (`loginLabels.ts`,
60
+ `forgotPasswordLabels.ts`, `otpLabels.ts`, `pinLabels.ts`, `devicePinLabels.ts`,
61
+ `passkeyLabels.ts`, `preferredMethodLabels.ts`, `demoLabels.ts`,
62
+ `resetPasswordLabels.ts`) behind a `labels/index.ts` barrel that re-exports the
63
+ exact same names, so `./labels` remains a single import site.
64
+ - **`src/components/LoginForm.tsx` (253 lines) → render body slimmed.** The
65
+ prop-doc-heavy `LoginFormProps` interface moved to a co-located
66
+ `LoginForm.types.ts` (re-exported from `LoginForm.tsx`), and the submit branch
67
+ logic moved into a unit-tested `useLoginSubmit` hook.
68
+
69
+ ### Changed
70
+
71
+ - No runtime change. Splitting only; identical bundled output.
72
+
73
+ Completes the demo-credential feature for **BFF-fronted apps**, whose demo pair
74
+ arrives ASYNChronously from `GET /bff/config` rather than a module-level const (a
75
+ const would bake the credentials into a bundle handed to a customer to
76
+ self-deploy). Two gaps closed:
77
+
78
+ 1. `prefillOnMount` was inert for async credentials (1.11.0 bug).
79
+ 2. `useBffLoginConfig().config` dropped the `demo` field the BFF now serves.
80
+
81
+ Requires `@dloizides/auth-client` **>= 4.2.0** (which surfaces `demo` on
82
+ `BffLoginConfig`) — the peer floor is raised accordingly. Additive for existing
83
+ consumers: the new `LoginForm` prop is optional and the synchronous /
84
+ module-const path is byte-identical.
85
+
86
+ ### Fixed
87
+
88
+ - **`prefillOnMount` now honours credentials that arrive AFTER mount.** `useState`
89
+ reads its initial argument on the first render only, so a demo pair landing a
90
+ render later (every BFF app) was silently dropped and the form stayed empty
91
+ beside a populated hint. `useDemoCredentials` now seeds when the resolved pair
92
+ transitions to non-null (or to a genuinely different pair), gated by VALUE so a
93
+ react-query refetch handing back a fresh-but-equal object does not re-seed on a
94
+ loop.
95
+
96
+ ### Added
97
+
98
+ - **`config.demo` carries through `useBffLoginConfig`.** `BffLoginConfig` gains
99
+ `demo?: DemoCredentials | null`, so the intended wiring works end-to-end:
100
+ ```tsx
101
+ const { config } = useBffLoginConfig(bffAuthClient);
102
+ const demo = useDemoCredentials({ credentials: config.demo ?? undefined, prefillOnMount: true });
103
+ ```
104
+ - **`PrefillIntent`** enum + **`LoginForm` gains `prefillIntent`** — the "don't
105
+ fight the user" control. An `Opportunistic` re-seed (a late config fetch) is
106
+ DROPPED unless both fields are still empty, so it never overwrites what a real
107
+ account holder is typing while `/bff/config` is in flight; an `Explicit` re-seed
108
+ (the user tapped "use these credentials") always applies. `useDemoCredentials`
109
+ sets the right intent automatically — most apps never touch this prop.
110
+ - **`DemoCredentials` is now re-exported from `@dloizides/auth-client`** (its
111
+ canonical owner) rather than defined locally, so there is exactly ONE shape
112
+ across the two packages. Structurally identical to 1.11.0 — no consumer change.
113
+
114
+ ### "Don't fight the user" — what the hook can and cannot know
115
+
116
+ `useDemoCredentials` does not own the input fields (`<LoginForm>` does), so it
117
+ cannot observe keystrokes. The interaction test it CAN make is on the field
118
+ values: an opportunistic seed applies only when both fields are still empty
119
+ (pristine). This is intentionally conservative — a user who typed and then
120
+ cleared a field keeps control for the rest of the session (a dropped
121
+ opportunistic bump is not retried). An explicit user request is never subject to
122
+ this test.
123
+
124
+ ## 1.11.0 (2026-07-17)
125
+
126
+ Additive release adding **public demo-credential support** for products with an
127
+ open, published demo account (Zygos is the first). Purely additive — every 1.10.0
128
+ export is unchanged, and an app that imports none of the new exports renders
129
+ byte-identically: the three new `<LoginForm>` props are all optional and inert
130
+ when omitted.
131
+
132
+ It fills form fields and nothing else. **No auth bypass**: the demo login travels
133
+ the ordinary path (same BFF, same Keycloak, same session cookie, same roles), and
134
+ no credential ships inside this package — the host supplies the pair.
135
+
136
+ ### Added
137
+
138
+ - **`<DemoCredentialsHint>`** — a themed panel for the login card showing a
139
+ published demo username + password with a one-tap "use these credentials"
140
+ action. Takes the usual `AuthTheme` / `labels` / `testIdPrefix` props. Values
141
+ render as selectable monospace chips (an `l`/`1` in a published password must
142
+ not be a guess). Omit `onUse` for a read-only panel.
143
+
144
+ **Fails closed**: no credentials — or a half-configured pair (blank username,
145
+ blank password, or a field that is `undefined` at runtime because an env var
146
+ was never set) — renders `null`. Zero DOM. A misconfigured demo can never
147
+ publish an empty credentials panel.
148
+
149
+ - **`useDemoCredentials({ credentials?, prefillOnMount? })`** — the hint↔form
150
+ wiring, so no app hand-rolls it. Returns two prop bags to spread:
151
+ `hintProps` (`{ credentials, onUse }`) and `loginFormProps`
152
+ (`{ initialUsername, initialPassword, prefillNonce }`).
153
+
154
+ `prefillOnMount` (default `false`) makes the form arrive already filled, so
155
+ signing in as the demo user is a single tap on "Sign in".
156
+
157
+ - **`resolveDemoCredentials(credentials?)`** — the exported fail-closed gate the
158
+ above share: returns a trimmed pair, or `null` when there is no usable demo.
159
+
160
+ - **`DemoCredentials`** type and `DEFAULT_DEMO_CREDENTIALS_HINT_LABELS` /
161
+ `DemoCredentialsHintLabels` (all copy stays app-supplied, as everywhere else).
162
+
163
+ - **`<LoginForm>` gains `initialUsername` / `initialPassword` / `prefillNonce`.**
164
+ The `initial*` props **seed** the fields; they do not own them — every value
165
+ stays editable and clearable, so a real account holder can always wipe the demo
166
+ values and sign in with their own. `prefillNonce` is the re-seed signal:
167
+ changing it re-applies the seed over fields the user has edited or cleared.
168
+
169
+ The nonce is not ceremony. `useState` ignores its initial argument on
170
+ re-render, so a fill button wired to `initialUsername` alone would render a
171
+ form that stays stubbornly empty — the nonce is what makes "click the hint →
172
+ form fills" work at all. `useDemoCredentials` manages it for you.
173
+
174
+ ### Internal
175
+
176
+ - `useLoginPrefill` — extracts `<LoginForm>`'s credential field state (the only
177
+ part of that component with real logic), so the seed/re-seed contract is unit
178
+ tested directly rather than through the JSX.
179
+
180
+ ## 1.10.0 (2026-07-17)
181
+
182
+ Additive release adding the shared **logout sequencer**. Purely additive — every
183
+ 1.9.x export is unchanged, no component behaviour changes, and apps that do not
184
+ import `performBffLogout` are byte-identical.
185
+
186
+ ### Added
187
+
188
+ - **`performBffLogout({ client, onClearSession?, onRedirect?, onError?, navigateToIdp? })`**
189
+ — the shared logout sequence, extracted because all six consuming apps
190
+ hand-rolled it in their own `AuthProvider` and the orderings disagreed. Runs:
191
+ 1. `onClearSession()` — local state first, so the UI reflects the sign-out
192
+ immediately.
193
+ 2. `await client.logout({ redirect: false })` — lets the `POST /bff/logout`
194
+ SETTLE, and takes the IdP logout URL rather than letting the client navigate
195
+ mid-sequence.
196
+ 3. Navigates exactly once, last: to the IdP logout URL when the BFF returned
197
+ one (ending the IdP browser session), otherwise `onRedirect()`. When it
198
+ navigates to the IdP it deliberately does NOT also call `onRedirect` — two
199
+ competing navigations would race, and the IdP round-trip already returns the
200
+ user to the app.
201
+
202
+ Never rejects; always navigates, even when the BFF logout fails (a down BFF
203
+ must not trap the user on a page they asked to leave).
204
+ - **`BffLogoutClient`** / **`PerformBffLogoutOptions`** types.
205
+
206
+ ### Why
207
+
208
+ `zygos-web` and `agora-web` navigated FIRST and called the BFF second:
209
+
210
+ ```ts
211
+ redirectTo('/(auth)/login'); // → window.location.replace() → document unload
212
+ await bffAuthClient.logout(); // races the unload — may never be sent
213
+ ```
214
+
215
+ That is wrong twice over:
216
+
217
+ 1. **The BFF call can be cancelled.** A document navigation kills in-flight
218
+ fetches, so `POST /bff/logout` may never reach the server: the SPA looks
219
+ logged out while the httpOnly session cookie stays alive. The `catch` logged
220
+ it "non-fatal", so the defect was silent.
221
+ 2. **The IdP session survives.** `BffAuthClient.logout()` ends the identity
222
+ provider's browser session with a top-level navigation to its RP-initiated
223
+ logout URL (only a real navigation carries the IdP's own cookie — a
224
+ back-channel POST cannot clear it). An app that navigates to its own login
225
+ route first pre-empts that navigation, so the IdP still considers the user
226
+ signed in and the next authorize round-trip silently re-authenticates them —
227
+ the "sign-out that signs you back in" bug the client's own docs warn about.
228
+
229
+ Synthetic tests that stub the client never reproduce either failure (there is no
230
+ real navigation to cancel the request), which is why they survived. The helper
231
+ owns the sequence so neither can be reintroduced per-app.
232
+
233
+ ## 1.8.0 (2026-07-09)
234
+
235
+ Additive release for the Move 2b passkey in-app rebuild — the passkey ceremony
236
+ now runs **in-page** (`navigator.credentials.*` against the BFF's same-origin
237
+ `/bff/passkey/*` fetch endpoints) instead of a full-page redirect to Keycloak.
238
+ **Inert by default / no behaviour change until the BFF's WebAuthn RP is
239
+ configured:** when the RP is off the options endpoint returns 501 and the hook
240
+ transparently falls back to the legacy redirect. No breaking changes; every
241
+ 1.7.x export is unchanged and the legacy `startPasskeyLogin` /
242
+ `startPasskeyRegistration` navigation helpers stay exported (they ARE the
243
+ fallback).
244
+
245
+ ### Added
246
+
247
+ - **`usePasskeyCeremony({ mode, returnUrl, onSuccess })`** — the react-query-free
248
+ in-page ceremony hook: options fetch → `navigator.credentials.create/get` →
249
+ verify fetch. Returns `{ status, error, isSupported, start, reset }`. Falls
250
+ back to the legacy Keycloak redirect on a 501 (RP not configured), any non-2xx
251
+ options response, or an unsupported browser — so nothing regresses until the
252
+ owner flips `Bff:Passkey`. A cancelled/timed-out ceremony (`NotAllowedError` /
253
+ `AbortError`) surfaces as `'cancelled'`, anything else as `'failed'`.
254
+ - **`PasskeyCeremonyMode`** (`Login` / `Register`) and **`PasskeyCeremonyStatus`**
255
+ (`Idle` / `Running` / `Success` / `Error`) `const enum`s.
256
+
257
+ ### Changed
258
+
259
+ - **`<PasskeyLoginButton>`** and **`<PasskeySettingsCard>`** now drive
260
+ `usePasskeyCeremony` instead of `window.location.assign`. Same exported props,
261
+ same testIDs — a drop-in bump for consumers; the in-page path only activates
262
+ once the BFF RP is configured, otherwise they behave exactly as before.
263
+
264
+ ## 1.7.0 (2026-07-07)
265
+
266
+ Additive release for the Kefi multi-role overhaul — the shared machinery for a
267
+ cross-dashboard nav. No breaking changes; every 1.6.x export is unchanged.
268
+
269
+ ### Added
270
+
271
+ - **`resolveAccessibleRoutes(user, table)`** — the multi-role companion to
272
+ `resolvePostLoginRoute`. Returns the full, ordered list of `RoleRoute` entries
273
+ the user's role-set unlocks (not just the top one), reusing the same
274
+ `collectUserRoles` reader and `RoleRouteTable`. Table (priority) order is
275
+ preserved; the table `fallback` is never included; a role-less user gets `[]`.
276
+ Pure and app-agnostic — an app renders the result as a cross-dashboard nav.
277
+ - **`RoleRoute.labelKey?`** — optional translation key carried per route so a
278
+ consuming app can render a localized label per accessible dashboard. Ignored
279
+ by `resolvePostLoginRoute`; the package stays i18n-agnostic (carries the key,
280
+ the app resolves it).
281
+
282
+ ## 1.3.0 (2026-05-31)
283
+
284
+ Additive release for the auth-UX reuse roll-out (feature-reuse-roadmap #1).
285
+ Promotes the "Forgot password?" modal body the product apps each hand-rolled
286
+ into a single shared, **react-query-free** export so katalogos / erevna / kefi
287
+ stop duplicating it. No breaking changes; every 1.2.x export is unchanged.
288
+
289
+ ### Added
290
+
291
+ - **`<ForgotPasswordFields>`** — the embedded, themeable "request a reset link"
292
+ body, sized to live inside an app-owned modal shell (no screen/card wrapper,
293
+ no title — the host modal owns those). Adds the two modal affordances a
294
+ screen-shaped form lacks: a `Cancel` button beside submit and a `Close` button
295
+ on the success state, each rendered only when its handler is supplied. Clears
296
+ itself when `visible` flips to `false` so reopening starts fresh.
297
+ - **`useForgotPasswordSubmit`** — the headless, **react-query-free** forgot-
298
+ password logic: a plain `useState` + `client.forgotPassword(...)` promise
299
+ chain that runs on a provider-less login route (where the existing
300
+ `useBffForgotPassword` `useMutation` crashes with "No QueryClient set"). Email
301
+ validation gating via `canSubmit`, anti-enumeration success, `hasNetworkError`,
302
+ and `reset()`. Also exports the `isValidForgotPasswordEmail` helper.
303
+ - **`ForgotPasswordFieldsLabels`** + **`DEFAULT_FORGOT_PASSWORD_FIELDS_LABELS`** —
304
+ the forgot-password label bag plus the modal `cancel` / `close` strings.
305
+ - New `AuthTestIds` entries `forgotPasswordCancelButton` /
306
+ `forgotPasswordCloseButton`.
307
+
308
+ ### Notes
309
+
310
+ - The existing screen-shaped `<ForgotPasswordForm>` (react-query-based) is
311
+ unchanged. A future cleanup may re-base it on `useForgotPasswordSubmit` to drop
312
+ its react-query coupling; deferred to keep this release additive.
313
+
314
+ ## 1.2.0 (2026-05-22)
315
+
316
+ Additive release for Phase 3d of the unified-auth plan — the event-scoped PIN
317
+ UI. Adds the native, branded event-PIN login surface so an app (Kefi) can offer
318
+ "sign in with your event PIN" for operational staff (door / DJ / media) without
319
+ the user ever bouncing to Keycloak's hosted UI. No breaking changes; every
320
+ 1.1.0 export is unchanged.
321
+
322
+ ### Added
323
+
324
+ - **`<PinForm>`** — the ready-made, themeable single-step event-PIN login form,
325
+ built on `usePinLogin`. A PIN field + a "sign in" button; the
326
+ `eventExternalId` is a prop (the event context comes from the route/page, it
327
+ is never typed by the user). Styled by the same `AuthTheme` token bag as the
328
+ other forms; copy is supplied through the i18n-agnostic `PinFormLabels` bag
329
+ (English `DEFAULT_PIN_LABELS` fallback). `onSuccess` hands back the signed-in
330
+ `BffUser`.
331
+ - **`usePinLogin`** — the headless PIN hook (the custom-layout escape hatch). A
332
+ single-step flow: `submit(pin)` → `POST /bff/pin/login` → `BffUser`, with
333
+ `reset()`. Mirrors `useOtpLogin` (mounted-guard, memoised result,
334
+ latest-`eventExternalId` ref).
335
+ - **`PinFormLabels`** + **`DEFAULT_PIN_LABELS`** — the PIN label bag.
336
+ - New `AuthTestIds` entries for every PIN interactive element.
337
+ - Re-exports `BffPinLoginRequest` from `@dloizides/auth-client`.
338
+
339
+ ### Changed
340
+
341
+ - Peer dependency `@dloizides/auth-client` raised to `>=3.2.0` — the version
342
+ that adds `BffAuthClient.pinLogin`.
343
+
344
+ ## 1.1.0 (2026-05-22)
345
+
346
+ Additive release for Phase 2d of the unified-auth plan — the email-OTP UI. Adds
347
+ the native, branded email-OTP login surface so an app can offer "sign in with a
348
+ code" without the user ever bouncing to Keycloak's hosted UI. No breaking
349
+ changes; every 1.0.0 export is unchanged.
350
+
351
+ ### Added
352
+
353
+ - **`<OtpForm>`** — the ready-made, themeable two-step email-OTP login form,
354
+ built on `useOtpLogin`. Step 1 collects the email and sends a code; step 2
355
+ collects the code, verifies it, and offers "resend code" and "use a different
356
+ email". Styled by the same `AuthTheme` token bag as the other forms; copy is
357
+ supplied through the i18n-agnostic `OtpFormLabels` bag (English
358
+ `DEFAULT_OTP_LABELS` fallback). `onSuccess` hands back the signed-in `BffUser`.
359
+ - **`useOtpLogin`** — the headless OTP hook (the custom-layout escape hatch). A
360
+ small forward state machine over `OtpLoginStep`: `requestCode(identifier)` →
361
+ `EnterCode`, then `verifyCode(otp)` → `BffUser`, with `resend()` and
362
+ `reset()`. Mirrors `useBffAuth` (mounted-guard, memoised result).
363
+ - **`OtpLoginStep`** — the `RequestCode` / `EnterCode` step enum.
364
+ - **`OtpFormLabels`** + **`DEFAULT_OTP_LABELS`** — the OTP label bag.
365
+ - New `AuthTestIds` entries for every OTP interactive element.
366
+ - Re-exports `BffOtpRequestRequest`, `BffOtpVerifyRequest` and
367
+ `BffOtpRequestResult` from `@dloizides/auth-client`.
368
+
369
+ ### Changed
370
+
371
+ - Peer dependency `@dloizides/auth-client` raised to `>=3.1.0` — the version
372
+ that ships `BffAuthClient.requestOtp` / `.verifyOtp` (`POST /bff/otp/request`
373
+ and `POST /bff/otp/verify`).
374
+
375
+ ### Notes
376
+
377
+ - Still strictly thin: UI + the same-origin `/bff/*` client. The OTP client
378
+ methods carry the `X-BFF-Csrf` header like every other state-changing call;
379
+ no token handling, no secrets in the browser.
380
+ - This release adds the OTP surface to the package only. Wiring an app onto it
381
+ (the Kefi pilot) is Phase 3.
382
+
383
+ ## 1.0.0 (2026-05-22)
384
+
385
+ Initial release — the frontend half of the unified-auth plan (Phase 1b).
386
+
387
+ `@dloizides/auth-web` is the shared, themeable, branded auth UI for the
388
+ dloizides.com portfolio. It is extracted and generalised from the per-app auth
389
+ code that lived inside `apps/katalogos-web` so katalogos, erevna and kefi can
390
+ share one implementation instead of copy-pasting auth adapters.
391
+
392
+ ### Added
393
+
394
+ - **Themeable components**
395
+ - `<LoginForm>` — password login, built on `useBffAuth`.
396
+ - `<ForgotPasswordForm>` — request-a-reset-link, built on
397
+ `useBffForgotPassword`; no-enumeration confirmation state.
398
+ - `<ResetPasswordForm>` — choose-a-new-password, built on
399
+ `useResetPasswordForm`.
400
+ - All three are styled by an `AuthTheme` design-token bag so visually
401
+ distinct apps share one implementation.
402
+ - **Theming** — `AuthThemeProvider`, `useAuthTheme`, `defaultAuthTheme`, and
403
+ the `AuthTheme` token contract (colors / radii / spacing / typography).
404
+ Precedence: explicit `theme` prop → `<AuthThemeProvider>` context → default.
405
+ - **i18n-agnostic label bags** — `LoginFormLabels`, `ForgotPasswordFormLabels`,
406
+ `ResetPasswordFormLabels` plus English `DEFAULT_*` constants. The package
407
+ ships no `t()` / `FM()`; consuming apps pass already-localised strings.
408
+ - **Headless hooks** (the custom-layout escape hatch)
409
+ - `useBffAuth` — login / logout / `/bff/me` session, lifecycle status.
410
+ - `useBffForgotPassword`, `useBffResetPassword` — React Query mutations.
411
+ - `useResetPasswordForm` — reset-password form-state + validation logic.
412
+ - **`createBffAuthClient`** — one-line wiring of a same-origin `BffAuthClient`
413
+ with a lazily-resolved `fetch` (load-time side-effect-free).
414
+ - **Role-based post-login router** — `resolvePostLoginRoute(user, table)` and
415
+ `collectUserRoles`. The consuming app supplies the role → route table.
416
+ - **Password policy** — `validatePasswordPolicy`, `isPasswordValid`,
417
+ `PasswordPolicyError`; mirrors the TenantService backend rules.
418
+ - **Test IDs** — `AuthTestIds` + `withTestIdPrefix` for E2E targeting.
419
+ - Re-exports `BffAuthClient`, `createFetchHttpClient` and the BFF request /
420
+ response types from `@dloizides/auth-client` so consumers have a single
421
+ import surface.
422
+
423
+ ### Notes
424
+
425
+ - Strictly thin: UI + a same-origin `/bff/*` fetch client + the router helper.
426
+ **No secrets, no token handling, no Keycloak calls** — the per-app BFF owns
427
+ all of that server-side.
428
+ - Built on `@dloizides/auth-client` (peer dependency `>=3.0.0`), which ships
429
+ `BffAuthClient`.
430
+ - This release creates the package only. Porting `apps/katalogos-web` onto it
431
+ is Phase 1c.