@dloizides/auth-web 1.2.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.
@@ -0,0 +1,806 @@
1
+ import { ReactElement, ReactNode } from 'react';
2
+ import { BffAuthClient, BffUser, BffLoginRequest, BffForgotPasswordRequest, BffResetPasswordRequest, BffOtpRequestResult, HttpClient } from '@dloizides/auth-client';
3
+ export { BffAuthClient, BffAuthClientOptions, BffForgotPasswordRequest, BffLoginRequest, BffOtpRequestRequest, BffOtpRequestResult, BffOtpVerifyRequest, BffPinLoginRequest, BffRegisterRequest, BffResetPasswordRequest, BffUser, HttpClient, HttpRequest, HttpResponse, createFetchHttpClient } from '@dloizides/auth-client';
4
+ import { UseMutationOptions, UseMutationResult } from '@tanstack/react-query';
5
+
6
+ /**
7
+ * Label bags for the auth components.
8
+ *
9
+ * `@dloizides/auth-web` ships **no** translation framework — it is i18n-agnostic
10
+ * by design (each app already owns its own i18n). Every user-facing string a
11
+ * component renders is supplied by the consuming app through a typed `labels`
12
+ * prop. The app passes strings it has already localised with `FM()` / `t()`.
13
+ *
14
+ * Each bag has a `DEFAULT_*` constant (English) so a component renders sensibly
15
+ * with no `labels` prop and a consuming app can spread-override only the keys
16
+ * it wants to change.
17
+ */
18
+ /** Strings rendered by `<LoginForm>`. */
19
+ interface LoginFormLabels {
20
+ title: string;
21
+ subtitle: string;
22
+ usernameLabel: string;
23
+ usernamePlaceholder: string;
24
+ passwordLabel: string;
25
+ passwordPlaceholder: string;
26
+ submit: string;
27
+ /** Shown while the login request is in flight. */
28
+ submitting: string;
29
+ /** The "Forgot password?" link text. */
30
+ forgotPassword: string;
31
+ /** Generic credentials-rejected message. */
32
+ invalidCredentials: string;
33
+ /** Shown when one or both fields are empty on submit. */
34
+ missingFields: string;
35
+ }
36
+ /** Strings rendered by `<ForgotPasswordForm>`. */
37
+ interface ForgotPasswordFormLabels {
38
+ title: string;
39
+ /** Copy above the email field. */
40
+ description: string;
41
+ emailLabel: string;
42
+ emailPlaceholder: string;
43
+ submit: string;
44
+ submitting: string;
45
+ /** Generic confirmation, shown regardless of whether the email is registered. */
46
+ successMessage: string;
47
+ /** Shown on a network / 5xx failure. */
48
+ networkError: string;
49
+ /** Shown when the entered value is not a valid email. */
50
+ invalidEmail: string;
51
+ }
52
+ /** Strings rendered by `<OtpForm>` — the two-step email-OTP login form. */
53
+ interface OtpFormLabels {
54
+ /** Title shown on the request-a-code step. */
55
+ requestTitle: string;
56
+ /** Copy above the email field. */
57
+ requestDescription: string;
58
+ emailLabel: string;
59
+ emailPlaceholder: string;
60
+ /** The "send code" button. */
61
+ requestSubmit: string;
62
+ /** Shown while the request is in flight. */
63
+ requesting: string;
64
+ /** Shown when the entered value is not a valid email. */
65
+ invalidEmail: string;
66
+ /** Title shown on the enter-the-code step. */
67
+ verifyTitle: string;
68
+ /** Copy above the code field; `{identifier}` is replaced with the email. */
69
+ verifyDescription: string;
70
+ codeLabel: string;
71
+ codePlaceholder: string;
72
+ /** The "verify" button. */
73
+ verifySubmit: string;
74
+ /** Shown while verification is in flight. */
75
+ verifying: string;
76
+ /** Shown when the code field is empty on submit. */
77
+ missingCode: string;
78
+ /** Generic "that code was wrong / expired" message. */
79
+ invalidCode: string;
80
+ /** The "resend code" link text. */
81
+ resend: string;
82
+ /** Shown while a resend is in flight. */
83
+ resending: string;
84
+ /** The "use a different email" link text — returns to step 1. */
85
+ changeEmail: string;
86
+ }
87
+ /** Strings rendered by `<PinForm>` — the single-step event-PIN login form. */
88
+ interface PinFormLabels {
89
+ /** Title shown above the PIN field. */
90
+ title: string;
91
+ /** Copy above the PIN field. */
92
+ description: string;
93
+ pinLabel: string;
94
+ pinPlaceholder: string;
95
+ /** The "sign in" button. */
96
+ submit: string;
97
+ /** Shown while the PIN exchange is in flight. */
98
+ submitting: string;
99
+ /** Shown when the PIN field is empty on submit. */
100
+ missingPin: string;
101
+ /** Generic "that PIN was wrong / expired / locked out" message. */
102
+ invalidPin: string;
103
+ }
104
+ /** Strings rendered by `<ResetPasswordForm>`. */
105
+ interface ResetPasswordFormLabels {
106
+ title: string;
107
+ description: string;
108
+ newPasswordLabel: string;
109
+ newPasswordPlaceholder: string;
110
+ confirmPasswordLabel: string;
111
+ confirmPasswordPlaceholder: string;
112
+ submit: string;
113
+ submitting: string;
114
+ /** Error: one or both fields empty. */
115
+ errorEmpty: string;
116
+ /** Error: the new password fails the policy. */
117
+ errorWeakPassword: string;
118
+ /** Error: confirm does not match. */
119
+ errorMismatch: string;
120
+ /** Error: the reset token is missing / expired / consumed. */
121
+ errorTokenInvalid: string;
122
+ /** Error: a network / 5xx failure. */
123
+ errorNetwork: string;
124
+ }
125
+ declare const DEFAULT_LOGIN_LABELS: LoginFormLabels;
126
+ declare const DEFAULT_FORGOT_PASSWORD_LABELS: ForgotPasswordFormLabels;
127
+ declare const DEFAULT_OTP_LABELS: OtpFormLabels;
128
+ declare const DEFAULT_PIN_LABELS: PinFormLabels;
129
+ declare const DEFAULT_RESET_PASSWORD_LABELS: ResetPasswordFormLabels;
130
+
131
+ /**
132
+ * `AuthTheme` — the design-token contract every `@dloizides/auth-web` component
133
+ * is styled against.
134
+ *
135
+ * The package owns **no** brand. Katalogos, Erevna and Kefi each look different;
136
+ * each one maps its own theme system onto this flat token bag and passes it in
137
+ * (via the `theme` prop or `<AuthThemeProvider>`). The component code only ever
138
+ * reads these tokens — never an app-specific theme object — so the same
139
+ * `<LoginForm>` renders on-brand in three visually distinct apps.
140
+ *
141
+ * Keep this shape minimal and stable: it is a public API surface.
142
+ */
143
+ /** Colour tokens — the only colours the auth components reference. */
144
+ interface AuthThemeColors {
145
+ /** Page / screen background behind the form card. */
146
+ background: string;
147
+ /** The form card surface colour. */
148
+ surface: string;
149
+ /** Primary text (titles, labels, input text). */
150
+ text: string;
151
+ /** Muted / secondary text (subtitles, helper copy). */
152
+ textSecondary: string;
153
+ /** Input + card border colour; also the disabled-button fill. */
154
+ border: string;
155
+ /** Brand primary — the submit button fill and link colour. */
156
+ primary: string;
157
+ /** Text drawn on top of `primary` (e.g. the submit button label). */
158
+ onPrimary: string;
159
+ /** Error text and error-state borders. */
160
+ danger: string;
161
+ /** Success text (e.g. the forgot-password confirmation). */
162
+ success: string;
163
+ }
164
+ /** Corner-radius tokens. */
165
+ interface AuthThemeRadii {
166
+ /** Inputs and buttons. */
167
+ input: number;
168
+ /** The form card. */
169
+ card: number;
170
+ }
171
+ /** Spacing scale (in density-independent pixels). */
172
+ interface AuthThemeSpacing {
173
+ /** Tight gap — e.g. label-to-input. */
174
+ xs: number;
175
+ /** Small gap. */
176
+ sm: number;
177
+ /** Default gap — between stacked fields. */
178
+ md: number;
179
+ /** Large gap — section separation. */
180
+ lg: number;
181
+ /** Card inner padding. */
182
+ xl: number;
183
+ }
184
+ /** Font-size tokens. */
185
+ interface AuthThemeTypography {
186
+ /** Form title. */
187
+ title: number;
188
+ /** Form subtitle. */
189
+ subtitle: number;
190
+ /** Field labels. */
191
+ label: number;
192
+ /** Input text and button text. */
193
+ body: number;
194
+ /** Helper / error / footer copy. */
195
+ caption: number;
196
+ }
197
+ /** The full token bag a consuming app supplies. */
198
+ interface AuthTheme {
199
+ colors: AuthThemeColors;
200
+ radii: AuthThemeRadii;
201
+ spacing: AuthThemeSpacing;
202
+ typography: AuthThemeTypography;
203
+ }
204
+ /**
205
+ * A neutral, accessible light-mode theme. Apps are expected to override this —
206
+ * it exists so a component renders sensibly with zero configuration and so
207
+ * `<AuthThemeProvider>` has a default value.
208
+ */
209
+ declare const defaultAuthTheme: AuthTheme;
210
+
211
+ /**
212
+ * `<LoginForm>` — the ready-made, themeable password-login form.
213
+ *
214
+ * The "themeable component" half of the package design: drop it in, pass a
215
+ * `client`, a `theme` (or wrap in `<AuthThemeProvider>`) and a localised
216
+ * `labels` bag, and you have a branded login surface. It is built on the
217
+ * headless `useBffAuth` hook, so the ready-made and custom-layout paths share
218
+ * one code path.
219
+ *
220
+ * The package ships no router and no i18n. `onSuccess` hands the signed-in
221
+ * `BffUser` back to the app, which decides where to navigate (typically via
222
+ * `resolvePostLoginRoute`). All copy comes from `labels`.
223
+ */
224
+
225
+ interface LoginFormProps {
226
+ /** The same-origin BFF client (build it with `createBffAuthClient`). */
227
+ client: BffAuthClient;
228
+ /** Explicit theme; overrides any `<AuthThemeProvider>`. Falls back to the default theme. */
229
+ theme?: AuthTheme;
230
+ /** Localised copy. Partial — unspecified keys fall back to English defaults. */
231
+ labels?: Partial<LoginFormLabels>;
232
+ /** Called with the signed-in user after a successful login. */
233
+ onSuccess: (user: BffUser) => void;
234
+ /** Called when the user taps the "Forgot password?" link. Omit to hide the link. */
235
+ onForgotPassword?: () => void;
236
+ /** Prefix applied to every `testID` so multiple forms can share a screen. */
237
+ testIdPrefix?: string;
238
+ }
239
+ /** Themeable password-login form built on `useBffAuth`. */
240
+ declare function LoginForm({ client, theme: themeProp, labels: labelsProp, onSuccess, onForgotPassword, testIdPrefix, }: Readonly<LoginFormProps>): ReactElement;
241
+
242
+ /**
243
+ * `<ForgotPasswordForm>` — the ready-made, themeable "request a reset link"
244
+ * form.
245
+ *
246
+ * Captures an email and POSTs to `/bff/forgot-password` via the headless
247
+ * `useBffForgotPassword` hook. The backend answers 200 unconditionally (no
248
+ * email enumeration), so on success the form swaps to a generic confirmation
249
+ * message — it never reveals whether the address is registered.
250
+ *
251
+ * Extracted + generalised from `apps/katalogos-web/src/components/Auth/
252
+ * ForgotPasswordModal.tsx` — minus the modal shell (apps own the surface) and
253
+ * the app-specific `FM()` calls (copy now comes from `labels`).
254
+ */
255
+
256
+ interface ForgotPasswordFormProps {
257
+ /** The same-origin BFF client (build it with `createBffAuthClient`). */
258
+ client: BffAuthClient;
259
+ /** Explicit theme; overrides any `<AuthThemeProvider>`. */
260
+ theme?: AuthTheme;
261
+ /** Localised copy. Partial — unspecified keys fall back to English defaults. */
262
+ labels?: Partial<ForgotPasswordFormLabels>;
263
+ /**
264
+ * Full URL with a `{token}` placeholder; forwarded to the backend so it can
265
+ * build the reset-email link without hardcoding any frontend host. Optional —
266
+ * apps that configure the template server-side can omit it.
267
+ */
268
+ resetUrlTemplate?: string;
269
+ /** Called once the request succeeds (the generic-confirmation state). */
270
+ onSuccess?: () => void;
271
+ /** Prefix applied to every `testID`. */
272
+ testIdPrefix?: string;
273
+ }
274
+ /** Themeable "request a reset link" form built on `useBffForgotPassword`. */
275
+ declare function ForgotPasswordForm({ client, theme: themeProp, labels: labelsProp, resetUrlTemplate, onSuccess, testIdPrefix, }: Readonly<ForgotPasswordFormProps>): ReactElement;
276
+
277
+ /**
278
+ * `<ResetPasswordForm>` — the ready-made, themeable "choose a new password"
279
+ * form.
280
+ *
281
+ * Built on the headless `useResetPasswordForm` hook, which owns all the logic
282
+ * (shared password policy, confirm-match, backend-error mapping). The component
283
+ * is a thin render layer: two fields, one button, one error line.
284
+ *
285
+ * Extracted + generalised from `apps/katalogos-web/src/auth/useResetPasswordForm.ts`
286
+ * + the katalogos reset-password route — minus the app-specific `FM()` and
287
+ * router calls (copy is in `labels`, navigation is the app's `onSuccess`).
288
+ */
289
+
290
+ interface ResetPasswordFormProps {
291
+ /** The same-origin BFF client (build it with `createBffAuthClient`). */
292
+ client: BffAuthClient;
293
+ /** The reset token, typically read from the deep-link URL by the app. */
294
+ token: string;
295
+ /** Explicit theme; overrides any `<AuthThemeProvider>`. */
296
+ theme?: AuthTheme;
297
+ /** Localised copy. Partial — unspecified keys fall back to English defaults. */
298
+ labels?: Partial<ResetPasswordFormLabels>;
299
+ /** Called once the password has been reset successfully. */
300
+ onSuccess: () => void;
301
+ /** Prefix applied to every `testID`. */
302
+ testIdPrefix?: string;
303
+ }
304
+ /** Themeable "choose a new password" form built on `useResetPasswordForm`. */
305
+ declare function ResetPasswordForm({ client, token, theme: themeProp, labels: labelsProp, onSuccess, testIdPrefix, }: Readonly<ResetPasswordFormProps>): ReactElement;
306
+
307
+ /**
308
+ * `<OtpForm>` — the ready-made, themeable two-step email-OTP login form.
309
+ *
310
+ * The "themeable component" half of the OTP design: drop it in, pass a
311
+ * `client`, a `theme` (or wrap in `<AuthThemeProvider>`) and a localised
312
+ * `labels` bag, and you have a branded email-OTP surface. It is built on the
313
+ * headless `useOtpLogin` hook, so the ready-made and custom-layout paths share
314
+ * one code path — the same pattern as `<LoginForm>`.
315
+ *
316
+ * Two steps, switched on `OtpLoginStep`:
317
+ * 1. `RequestCode` — `<OtpRequestStep>`: an email field + a "send code" button.
318
+ * 2. `EnterCode` — `<OtpVerifyStep>`: a code field + a "verify" button, plus
319
+ * "resend code" and "use a different email" affordances.
320
+ *
321
+ * The package ships no router and no i18n. `onSuccess` hands the signed-in
322
+ * `BffUser` back to the app; all copy comes from `labels`.
323
+ */
324
+
325
+ interface OtpFormProps {
326
+ /** The same-origin BFF client (build it with `createBffAuthClient`). */
327
+ client: BffAuthClient;
328
+ /** Explicit theme; overrides any `<AuthThemeProvider>`. Falls back to the default theme. */
329
+ theme?: AuthTheme;
330
+ /** Localised copy. Partial — unspecified keys fall back to English defaults. */
331
+ labels?: Partial<OtpFormLabels>;
332
+ /** Called with the signed-in user after a successful code verification. */
333
+ onSuccess: (user: BffUser) => void;
334
+ /** Prefix applied to every `testID` so multiple forms can share a screen. */
335
+ testIdPrefix?: string;
336
+ }
337
+ /** Themeable two-step email-OTP login form built on `useOtpLogin`. */
338
+ declare function OtpForm({ client, theme: themeProp, labels: labelsProp, onSuccess, testIdPrefix, }: Readonly<OtpFormProps>): ReactElement;
339
+
340
+ /**
341
+ * `<PinForm>` — the ready-made, themeable single-step event-PIN login form.
342
+ *
343
+ * The "themeable component" half of the PIN design: drop it in, pass a
344
+ * `client`, the `eventExternalId` of the event the staff member is signing in
345
+ * to, a `theme` (or wrap in `<AuthThemeProvider>`) and a localised `labels`
346
+ * bag, and you have a branded event-PIN surface. It is built on the headless
347
+ * `usePinLogin` hook, so the ready-made and custom-layout paths share one code
348
+ * path — the same pattern as `<OtpForm>` / `<LoginForm>`.
349
+ *
350
+ * Unlike `<OtpForm>` the PIN flow is a single step: a PIN field + a "sign in"
351
+ * button. The `eventExternalId` is a prop, never typed by the user — the event
352
+ * context comes from the route/page the form is rendered on.
353
+ *
354
+ * The package ships no router and no i18n. `onSuccess` hands the signed-in
355
+ * `BffUser` back to the app; all copy comes from `labels`.
356
+ */
357
+
358
+ interface PinFormProps {
359
+ /** The same-origin BFF client (build it with `createBffAuthClient`). */
360
+ client: BffAuthClient;
361
+ /**
362
+ * External id of the event the PIN is scoped to. Supplied by the route/page
363
+ * — the event context is never typed by the user.
364
+ */
365
+ eventExternalId: string;
366
+ /** Explicit theme; overrides any `<AuthThemeProvider>`. Falls back to the default theme. */
367
+ theme?: AuthTheme;
368
+ /** Localised copy. Partial — unspecified keys fall back to English defaults. */
369
+ labels?: Partial<PinFormLabels>;
370
+ /** Called with the signed-in user after a successful PIN exchange. */
371
+ onSuccess: (user: BffUser) => void;
372
+ /** Prefix applied to every `testID` so multiple forms can share a screen. */
373
+ testIdPrefix?: string;
374
+ }
375
+ /** Themeable single-step event-PIN login form built on `usePinLogin`. */
376
+ declare function PinForm({ client, eventExternalId, theme: themeProp, labels: labelsProp, onSuccess, testIdPrefix, }: Readonly<PinFormProps>): ReactElement;
377
+
378
+ /**
379
+ * React context for the `AuthTheme`.
380
+ *
381
+ * Two ways to theme `@dloizides/auth-web` components:
382
+ *
383
+ * 1. Wrap the auth screens in `<AuthThemeProvider theme={appAuthTheme}>` —
384
+ * every component below reads the theme from context.
385
+ * 2. Pass `theme` directly as a prop to an individual component — the prop
386
+ * always wins over context.
387
+ *
388
+ * With neither, components fall back to `defaultAuthTheme`. `useAuthTheme()`
389
+ * implements that precedence (prop → context → default) so each component does
390
+ * not re-derive it.
391
+ */
392
+
393
+ interface AuthThemeProviderProps {
394
+ /** The token bag every descendant auth component is styled against. */
395
+ theme: AuthTheme;
396
+ children: ReactNode;
397
+ }
398
+ /** Provides an `AuthTheme` to every `@dloizides/auth-web` component below it. */
399
+ declare function AuthThemeProvider({ theme, children, }: Readonly<AuthThemeProviderProps>): ReactElement;
400
+ /**
401
+ * Resolve the effective theme for a component.
402
+ *
403
+ * Precedence: an explicit `themeProp` (if given) wins over the context value,
404
+ * which itself defaults to `defaultAuthTheme` when no provider is mounted.
405
+ */
406
+ declare function useAuthTheme(themeProp?: AuthTheme): AuthTheme;
407
+
408
+ /**
409
+ * Stable `testID` values for every interactive element the auth components
410
+ * render. Exported so E2E suites and unit tests reference one source of truth
411
+ * rather than hard-coding string literals.
412
+ *
413
+ * Each component also accepts a `testIdPrefix` prop; when set, these base IDs
414
+ * are prefixed so two forms can coexist on one screen without ID collisions.
415
+ *
416
+ * `sonarjs/no-hardcoded-passwords` is disabled for this file: it flags keys
417
+ * such as `loginPasswordInput`, but every value here is a UI test ID, not a
418
+ * credential. There are no secrets in this module.
419
+ */
420
+ declare const AuthTestIds: {
421
+ readonly loginForm: "auth-login-form";
422
+ readonly loginUsernameInput: "auth-login-username";
423
+ readonly loginPasswordInput: "auth-login-password";
424
+ readonly loginSubmitButton: "auth-login-submit";
425
+ readonly loginForgotLink: "auth-login-forgot-link";
426
+ readonly loginError: "auth-login-error";
427
+ readonly forgotPasswordForm: "auth-forgot-form";
428
+ readonly forgotPasswordEmailInput: "auth-forgot-email";
429
+ readonly forgotPasswordSubmitButton: "auth-forgot-submit";
430
+ readonly forgotPasswordError: "auth-forgot-error";
431
+ readonly forgotPasswordSuccess: "auth-forgot-success";
432
+ readonly resetPasswordForm: "auth-reset-form";
433
+ readonly resetPasswordNewInput: "auth-reset-new";
434
+ readonly resetPasswordConfirmInput: "auth-reset-confirm";
435
+ readonly resetPasswordSubmitButton: "auth-reset-submit";
436
+ readonly resetPasswordError: "auth-reset-error";
437
+ readonly otpForm: "auth-otp-form";
438
+ readonly otpEmailInput: "auth-otp-email";
439
+ readonly otpRequestButton: "auth-otp-request";
440
+ readonly otpCodeInput: "auth-otp-code";
441
+ readonly otpVerifyButton: "auth-otp-verify";
442
+ readonly otpResendButton: "auth-otp-resend";
443
+ readonly otpChangeEmailButton: "auth-otp-change-email";
444
+ readonly otpError: "auth-otp-error";
445
+ readonly pinForm: "auth-pin-form";
446
+ readonly pinInput: "auth-pin-input";
447
+ readonly pinSubmitButton: "auth-pin-submit";
448
+ readonly pinError: "auth-pin-error";
449
+ };
450
+ /** Apply an optional prefix to a base test ID. */
451
+ declare function withTestIdPrefix(baseId: string, prefix?: string): string;
452
+
453
+ /** Lifecycle status of the auth session. */
454
+ declare const enum BffAuthStatus {
455
+ /** The initial `/bff/me` probe has not finished yet. */
456
+ Loading = "loading",
457
+ /** A live session exists; `user` is populated. */
458
+ Authenticated = "authenticated",
459
+ /** No session; `user` is `null`. */
460
+ Unauthenticated = "unauthenticated"
461
+ }
462
+ interface UseBffAuthOptions {
463
+ /** The same-origin BFF client (build it with `createBffAuthClient`). */
464
+ client: BffAuthClient;
465
+ /**
466
+ * When `true` (default), the hook probes `GET /bff/me` on mount to bootstrap
467
+ * `status`/`user`. Set `false` to skip the probe (e.g. on a public page).
468
+ */
469
+ probeOnMount?: boolean;
470
+ }
471
+ interface UseBffAuthResult {
472
+ /** The signed-in user, or `null` when there is no session. */
473
+ user: BffUser | null;
474
+ /** Session lifecycle status. */
475
+ status: BffAuthStatus;
476
+ /** `true` while a `login` or `logout` call is in flight. */
477
+ isSubmitting: boolean;
478
+ /** The last `login`/`logout`/`refresh` error, cleared on the next attempt. */
479
+ error: Error | null;
480
+ /** ROPC password login via `POST /bff/login`. Resolves to the user; rejects on failure. */
481
+ login: (request: BffLoginRequest) => Promise<BffUser>;
482
+ /** End the session via `POST /bff/logout`. Always resolves; never throws to the caller. */
483
+ logout: () => Promise<void>;
484
+ /** Re-read `GET /bff/me` and resync `user`/`status`. */
485
+ refresh: () => Promise<void>;
486
+ }
487
+ /**
488
+ * Headless BFF auth. Returns the current user, the session status, and
489
+ * `login` / `logout` / `refresh` callbacks. No rendering — the consumer (or
490
+ * `<LoginForm>`) wires it to UI.
491
+ */
492
+ declare function useBffAuth(options: UseBffAuthOptions): UseBffAuthResult;
493
+
494
+ /**
495
+ * React Query mutation hooks for the BFF password-reset flow.
496
+ *
497
+ * Headless counterparts to `<ForgotPasswordForm>` / `<ResetPasswordForm>` —
498
+ * apps with a custom layout use these directly. They POST to the same-origin
499
+ * `/bff/forgot-password` and `/bff/reset-password`, which the BFF proxies to
500
+ * TenantService.
501
+ *
502
+ * Extracted + generalised from `apps/katalogos-web/src/auth/bffPasswordHooks.ts`.
503
+ * Difference: the katalogos version closed over a module-level singleton
504
+ * `bffAuthClient`; the package version takes the `client` in the options so it
505
+ * carries no global state.
506
+ *
507
+ * The mutation result type is `undefined` (not `void`): the BFF endpoints
508
+ * return no body, and `undefined` is a valid generic argument where the lint
509
+ * config rejects `void`.
510
+ */
511
+
512
+ type UseBffForgotPasswordOptions = Omit<UseMutationOptions<undefined, Error, BffForgotPasswordRequest>, 'mutationFn'> & {
513
+ /** The same-origin BFF client. */
514
+ client: BffAuthClient;
515
+ };
516
+ /**
517
+ * Mutation that POSTs to `/bff/forgot-password`.
518
+ *
519
+ * The backend returns 200 unconditionally (no email enumeration); the UI
520
+ * should show the same "if that email exists, we sent a link" message whether
521
+ * `onSuccess` or `onError` fires.
522
+ */
523
+ declare function useBffForgotPassword(options: UseBffForgotPasswordOptions): UseMutationResult<undefined, Error, BffForgotPasswordRequest>;
524
+ type UseBffResetPasswordOptions = Omit<UseMutationOptions<undefined, Error, BffResetPasswordRequest>, 'mutationFn'> & {
525
+ /** The same-origin BFF client. */
526
+ client: BffAuthClient;
527
+ };
528
+ /**
529
+ * Mutation that POSTs to `/bff/reset-password`. A `400` (invalid / expired
530
+ * token) surfaces as a rejected mutation with the status in the error message.
531
+ */
532
+ declare function useBffResetPassword(options: UseBffResetPasswordOptions): UseMutationResult<undefined, Error, BffResetPasswordRequest>;
533
+
534
+ /**
535
+ * The discrete failure states the reset-password flow can surface.
536
+ *
537
+ * Each member is the stable key a consuming app maps onto a localised message.
538
+ * `useResetPasswordForm` reports exactly one of these at a time, preferring the
539
+ * earliest-fixable issue so the user resolves one thing at a time.
540
+ */
541
+ declare const enum ResetPasswordError {
542
+ /** One or both password fields are empty. */
543
+ Empty = "empty",
544
+ /** The new password fails the shared password policy. */
545
+ WeakPassword = "weakPassword",
546
+ /** The confirm field does not match the new password. */
547
+ Mismatch = "mismatch",
548
+ /** The reset token is missing, expired, or already consumed. */
549
+ TokenInvalid = "tokenInvalid",
550
+ /** A network or 5xx failure prevented the reset from completing. */
551
+ Network = "network"
552
+ }
553
+
554
+ interface UseResetPasswordFormArgs {
555
+ /** The same-origin BFF client. */
556
+ client: BffAuthClient;
557
+ /** The reset token, typically read from the deep-link URL. */
558
+ token: string;
559
+ /** Invoked once the password has been reset successfully. */
560
+ onSuccess: () => void;
561
+ }
562
+ interface UseResetPasswordFormResult {
563
+ newPassword: string;
564
+ confirmPassword: string;
565
+ setNewPassword: (value: string) => void;
566
+ setConfirmPassword: (value: string) => void;
567
+ /** `true` while the reset request is in flight. */
568
+ isSubmitting: boolean;
569
+ /** The single active error, or `null` when the form is clean. */
570
+ errorKey: ResetPasswordError | null;
571
+ /** `true` once a token-related failure is detected (drives a "request a new link" UI). */
572
+ hasInvalidToken: boolean;
573
+ /** Validate, then submit. */
574
+ submit: () => void;
575
+ }
576
+ /**
577
+ * Headless reset-password form logic. Returns the field values, setters, the
578
+ * single active error, and a `submit` callback.
579
+ */
580
+ declare function useResetPasswordForm({ client, token, onSuccess, }: UseResetPasswordFormArgs): UseResetPasswordFormResult;
581
+
582
+ /**
583
+ * The two discrete steps of the email-OTP login flow.
584
+ *
585
+ * `useOtpLogin` is at exactly one of these at a time, and `<OtpForm>` renders a
586
+ * different surface per step. The flow is strictly forward: a request moves
587
+ * `RequestCode` → `EnterCode`; a resend stays on `EnterCode`. Each member is a
588
+ * stable key — `<OtpForm>` switches on it, never on a string literal.
589
+ */
590
+ declare const enum OtpLoginStep {
591
+ /** Step 1: collect the email and ask the BFF to send a code. */
592
+ RequestCode = "requestCode",
593
+ /** Step 2: the code is on its way — collect and verify it. */
594
+ EnterCode = "enterCode"
595
+ }
596
+
597
+ interface UseOtpLoginOptions {
598
+ /** The same-origin BFF client (build it with `createBffAuthClient`). */
599
+ client: BffAuthClient;
600
+ }
601
+ interface UseOtpLoginResult {
602
+ /** Which of the two steps the flow is currently on. */
603
+ step: OtpLoginStep;
604
+ /** The identifier (email) the code was last requested for; `''` before step 1. */
605
+ identifier: string;
606
+ /**
607
+ * The latest `POST /bff/otp/request` result, or `null` before any request.
608
+ * Carries `expiresIn` for a countdown — never depend on `code` being present.
609
+ */
610
+ lastRequest: BffOtpRequestResult | null;
611
+ /** `true` while a `requestCode` / `verifyCode` / `resend` call is in flight. */
612
+ isSubmitting: boolean;
613
+ /** The last error, cleared at the start of the next attempt. */
614
+ error: Error | null;
615
+ /**
616
+ * Step 1 — ask the BFF to email a code to `identifier`. On success the flow
617
+ * advances to `EnterCode`. Rejects (and stays on `RequestCode`) on failure.
618
+ */
619
+ requestCode: (identifier: string) => Promise<BffOtpRequestResult>;
620
+ /**
621
+ * Step 2 — verify the entered `otp` for the stored identifier. Resolves to the
622
+ * signed-in `BffUser`; rejects on a bad / expired code (the flow stays on
623
+ * `EnterCode` so the user can retry or resend).
624
+ */
625
+ verifyCode: (otp: string) => Promise<BffUser>;
626
+ /**
627
+ * Re-send a code to the stored identifier without leaving `EnterCode`. A
628
+ * convenience wrapper over `requestOtp` for the step-2 "resend" affordance.
629
+ */
630
+ resend: () => Promise<BffOtpRequestResult>;
631
+ /** Return to step 1 (e.g. the user mistyped their email). Clears the error. */
632
+ reset: () => void;
633
+ }
634
+ /**
635
+ * Headless email-OTP login. Returns the current step, the in-flight status, and
636
+ * `requestCode` / `verifyCode` / `resend` / `reset` callbacks. No rendering —
637
+ * the consumer (or `<OtpForm>`) wires it to UI.
638
+ */
639
+ declare function useOtpLogin(options: UseOtpLoginOptions): UseOtpLoginResult;
640
+
641
+ interface UsePinLoginOptions {
642
+ /** The same-origin BFF client (build it with `createBffAuthClient`). */
643
+ client: BffAuthClient;
644
+ /**
645
+ * External id of the event the PIN is scoped to. Supplied by the page/route
646
+ * — the event context is never typed by the user.
647
+ */
648
+ eventExternalId: string;
649
+ }
650
+ interface UsePinLoginResult {
651
+ /** `true` while a `submit` call is in flight. */
652
+ isSubmitting: boolean;
653
+ /** The last error, cleared at the start of the next attempt. */
654
+ error: Error | null;
655
+ /**
656
+ * Exchange the entered `pin` (for the hook's `eventExternalId`) for a
657
+ * session. Resolves to the signed-in `BffUser`; rejects on a bad / expired /
658
+ * locked-out PIN (the consumer can let the user retry).
659
+ */
660
+ submit: (pin: string) => Promise<BffUser>;
661
+ /** Clear the current error so the form can be retried cleanly. */
662
+ reset: () => void;
663
+ }
664
+ /**
665
+ * Headless event-scoped PIN login. Returns the in-flight status, the last
666
+ * error, and `submit` / `reset` callbacks. No rendering — the consumer (or
667
+ * `<PinForm>`) wires it to UI.
668
+ */
669
+ declare function usePinLogin(options: UsePinLoginOptions): UsePinLoginResult;
670
+
671
+ /**
672
+ * `createBffAuthClient` — the one-line wiring of a same-origin `BffAuthClient`.
673
+ *
674
+ * `BffAuthClient` (from `@dloizides/auth-client`) needs an `HttpClient`. Every
675
+ * app wired it the same way — `createFetchHttpClient(fetch)` — and every app
676
+ * hit the same trap: binding `fetch` at module load throws in a non-browser
677
+ * runtime (the Jest/jsdom test environment has no `fetch` global). The lesson
678
+ * of Phase 1 was "copy-pasted auth adapters ship the same bug N times", so the
679
+ * correct wiring lives here once.
680
+ *
681
+ * `fetch` is resolved **lazily, per request** — module load stays
682
+ * side-effect-free. `baseUrl` is omitted by default → same-origin: every
683
+ * `/bff/*` call goes to the SPA's own host, which is fronted by the per-app
684
+ * BFF (`bff-katalogos`, `bff-erevna`, ...).
685
+ *
686
+ * Extracted + generalised from `apps/katalogos-web/src/auth/bffAuthClient.ts`.
687
+ */
688
+
689
+ interface CreateBffAuthClientOptions {
690
+ /**
691
+ * BFF origin. Omit (the production default) for same-origin — the SPA's own
692
+ * host is the BFF. An explicit origin is only useful for a non-same-origin
693
+ * BFF or for tests.
694
+ */
695
+ baseUrl?: string;
696
+ /**
697
+ * Override the HTTP transport. Defaults to a lazily-resolved native `fetch`.
698
+ * Tests pass a fake `HttpClient` here; production never needs to.
699
+ */
700
+ http?: HttpClient;
701
+ }
702
+ /**
703
+ * Build a same-origin `BffAuthClient`. With no arguments this is the exact
704
+ * production wiring: same-origin, lazy `fetch`, no token handling.
705
+ */
706
+ declare function createBffAuthClient(options?: CreateBffAuthClientOptions): BffAuthClient;
707
+
708
+ /**
709
+ * `resolvePostLoginRoute` — the role-based post-login router helper.
710
+ *
711
+ * The unified-auth plan's secondary goal is to kill the "too many login links"
712
+ * problem (KUCY V2 has seven, one per role). One login surface; after a
713
+ * successful login the app reads the user's role from the token claims and
714
+ * routes to the right dashboard.
715
+ *
716
+ * This helper is intentionally pure and app-agnostic. The package does NOT
717
+ * know any app's routes — the consuming app supplies a `RoleRouteTable`
718
+ * mapping role → path. The helper picks the highest-priority matching role.
719
+ *
720
+ * Role priority: a user can hold several roles (`superUser` + `admin` + ...).
721
+ * The table is consulted in the order its entries are listed — the FIRST
722
+ * entry whose role the user holds wins. So an app lists the most privileged
723
+ * role first. A `fallback` covers a user whose roles match no entry.
724
+ */
725
+
726
+ /** One role → route mapping. */
727
+ interface RoleRoute {
728
+ /** The Keycloak role name to match against the user's claims. */
729
+ role: string;
730
+ /** The route/path to send a user holding that role to. */
731
+ route: string;
732
+ }
733
+ /** An app-supplied, ordered role-routing table. */
734
+ interface RoleRouteTable {
735
+ /**
736
+ * Ordered list of role → route entries. Order is priority: the first entry
737
+ * whose `role` the user holds is chosen. List the most privileged role first.
738
+ */
739
+ routes: RoleRoute[];
740
+ /**
741
+ * Route used when the user holds none of the listed roles. When omitted and
742
+ * nothing matches, `resolvePostLoginRoute` returns `null` and the caller
743
+ * decides what to do (e.g. show an "no access" screen).
744
+ */
745
+ fallback?: string;
746
+ }
747
+ /**
748
+ * Collect every role on a `BffUser` — both the flat `roles` array and the
749
+ * nested `realm_access.roles` — into a de-duplicated set. The BFF may surface
750
+ * roles in either shape; this normalises both.
751
+ */
752
+ declare function collectUserRoles(user: BffUser): string[];
753
+ /**
754
+ * Resolve the post-login route for a user against an app-supplied table.
755
+ *
756
+ * Returns the route of the first table entry whose role the user holds; if no
757
+ * entry matches, returns the table's `fallback`, or `null` when there is none.
758
+ */
759
+ declare function resolvePostLoginRoute(user: BffUser, table: RoleRouteTable): string | null;
760
+
761
+ /**
762
+ * The discrete ways a password can fail the shared client-side policy.
763
+ *
764
+ * Each member is the stable key a consuming app maps onto a localised message —
765
+ * the package never ships translated copy.
766
+ */
767
+ declare const enum PasswordPolicyError {
768
+ /** Shorter than `PASSWORD_MIN_LENGTH`. */
769
+ TooShort = "tooShort",
770
+ /** Longer than `PASSWORD_MAX_LENGTH`. */
771
+ TooLong = "tooLong",
772
+ /** Contains no `A-Z` character. */
773
+ MissingUppercase = "missingUppercase",
774
+ /** Contains no `a-z` character. */
775
+ MissingLowercase = "missingLowercase",
776
+ /** Contains no `0-9` character. */
777
+ MissingDigit = "missingDigit"
778
+ }
779
+
780
+ /**
781
+ * Client-side password policy that mirrors the backend
782
+ * (TenantService `ResetPasswordRequestValidator`):
783
+ *
784
+ * - 8 ≤ length ≤ 128
785
+ * - at least one uppercase letter
786
+ * - at least one lowercase letter
787
+ * - at least one digit
788
+ *
789
+ * Shared so every app's `<ResetPasswordForm>` (and any future password input)
790
+ * rejects ~99% of bad passwords before a network round-trip. Extracted from
791
+ * `apps/katalogos-web/src/auth/passwordPolicy.ts`.
792
+ */
793
+
794
+ /** Minimum acceptable password length. */
795
+ declare const PASSWORD_MIN_LENGTH = 8;
796
+ /** Maximum acceptable password length. */
797
+ declare const PASSWORD_MAX_LENGTH = 128;
798
+ /**
799
+ * Return every policy rule the given password violates. An empty array means
800
+ * the password is acceptable.
801
+ */
802
+ declare function validatePasswordPolicy(password: string): PasswordPolicyError[];
803
+ /** `true` when the password satisfies every policy rule. */
804
+ declare function isPasswordValid(password: string): boolean;
805
+
806
+ export { AuthTestIds, type AuthTheme, type AuthThemeColors, AuthThemeProvider, type AuthThemeProviderProps, type AuthThemeRadii, type AuthThemeSpacing, type AuthThemeTypography, BffAuthStatus, type CreateBffAuthClientOptions, DEFAULT_FORGOT_PASSWORD_LABELS, DEFAULT_LOGIN_LABELS, DEFAULT_OTP_LABELS, DEFAULT_PIN_LABELS, DEFAULT_RESET_PASSWORD_LABELS, ForgotPasswordForm, type ForgotPasswordFormLabels, type ForgotPasswordFormProps, LoginForm, type LoginFormLabels, type LoginFormProps, OtpForm, type OtpFormLabels, type OtpFormProps, OtpLoginStep, PASSWORD_MAX_LENGTH, PASSWORD_MIN_LENGTH, PasswordPolicyError, PinForm, type PinFormLabels, type PinFormProps, ResetPasswordError, ResetPasswordForm, type ResetPasswordFormLabels, type ResetPasswordFormProps, type RoleRoute, type RoleRouteTable, type UseBffAuthOptions, type UseBffAuthResult, type UseBffForgotPasswordOptions, type UseBffResetPasswordOptions, type UseOtpLoginOptions, type UseOtpLoginResult, type UsePinLoginOptions, type UsePinLoginResult, type UseResetPasswordFormArgs, type UseResetPasswordFormResult, collectUserRoles, createBffAuthClient, defaultAuthTheme, isPasswordValid, resolvePostLoginRoute, useAuthTheme, useBffAuth, useBffForgotPassword, useBffResetPassword, useOtpLogin, usePinLogin, useResetPasswordForm, validatePasswordPolicy, withTestIdPrefix };