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