@dloizides/auth-client 3.2.1 → 3.3.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,5 +1,43 @@
1
1
  # Changelog
2
2
 
3
+ ## 3.3.0 (2026-06-02)
4
+
5
+ Additive release for unified-login Increment 3 — device-bound PIN unlock +
6
+ `/bff/config`. Extracts kefi-web's proven device-PIN client (the "second use"),
7
+ giving `@dloizides/auth-web` 1.4.0 a shared same-origin client with
8
+ **discriminated** results. The published `login` / `pinLogin` throw an opaque
9
+ error on any non-2xx, collapsing 401 (wrong PIN) and 429 (locked / rate-limited);
10
+ the new device-PIN methods never throw and route on a discriminated `status`
11
+ instead. No breaking changes.
12
+
13
+ ### Added
14
+
15
+ - `BffAuthClient.getLoginConfig()` → `GET /bff/config`. Returns the advertised
16
+ login methods (lowercase, de-duplicated), `registrationEnabled`, and the
17
+ optional per-device state (`rememberedUsername`, `hasPin`, `pinDigits`,
18
+ `preferredMethod`). NEVER throws — on a non-2xx, a network error, or a
19
+ malformed body it returns a safe fallback (`['password']`, registration off,
20
+ empty device-state). Unauthenticated, no CSRF (GET).
21
+ - `BffAuthClient.enrollDevicePin({ pin, digits })` → `POST /bff/pin/enroll`.
22
+ Discriminated `DevicePinEnrollResult`: `success` (200), `unauthorized` (401),
23
+ `forbidden` (403, PIN-established session), `invalidPin` (400), `error`
24
+ (anything else / network). Never throws.
25
+ - `BffAuthClient.unlockWithDevicePin({ pin })` → `POST /bff/pin/unlock`.
26
+ Discriminated `DevicePinUnlockResult`: `success` + `user` (200), `invalid`
27
+ (401), `locked` (429 **with** a JSON body — device lockout), `rateLimited`
28
+ (429 with an **empty** body — per-IP limiter), `error` (anything else /
29
+ malformed 200 / network). Both 429 variants carry the parsed `Retry-After`
30
+ seconds (`null` when absent / unparseable). Never throws. Distinguishing
31
+ `locked` vs `rateLimited` lets UIs poll through rate limits but show lockout
32
+ copy.
33
+ - `BffAuthClient.disableDevicePin()` → `POST /bff/pin/disable`. `true` on a 2xx,
34
+ `false` otherwise. Never throws.
35
+ - Types: `BffLoginConfig`, `BffDeviceState`, `BffDevicePinEnrollRequest`,
36
+ `BffDevicePinUnlockRequest`, `DevicePinUnlockResult`, `DevicePinEnrollResult`.
37
+ - `HttpResponse.header?(name)` — an optional, case-insensitive response-header
38
+ accessor (the bundled `createFetchHttpClient` always provides it). Needed to
39
+ read `Retry-After` off the unlock `429`. Backward-compatible (optional).
40
+
3
41
  ## 3.2.0 (2026-05-22)
4
42
 
5
43
  Additive release for Phase 3d of the unified-auth plan — event-scoped PIN
package/README.md CHANGED
@@ -151,6 +151,43 @@ await bff.resetPassword({ token, newPassword });
151
151
  await bff.logout();
152
152
  ```
153
153
 
154
+ ### Device-bound PIN unlock (v3.3 — unified-login Increment 3)
155
+
156
+ A returning, remembered-device, logged-OUT user can re-establish a session with a
157
+ 4/6/8-digit device PIN. Unlike `login` / `pinLogin` (which throw an opaque error on
158
+ any non-2xx), the device-PIN methods **never throw** — they return discriminated
159
+ results so the UI can route on `status`.
160
+
161
+ ```ts
162
+ // Which methods does this BFF advertise + does this device remember a PIN?
163
+ // NEVER throws — safe fallback (['password'], registration off) on any failure.
164
+ const config = await bff.getLoginConfig();
165
+ if (config.deviceState.hasPin) {
166
+ /* render the device-PIN unlock screen */
167
+ }
168
+
169
+ // Bind a PIN to the current strong session.
170
+ const enroll = await bff.enrollDevicePin({ pin: '482913', digits: 6 });
171
+ // status: 'success' | 'unauthorized' | 'forbidden' | 'invalidPin' | 'error'
172
+
173
+ // Re-establish a session from a remembered device.
174
+ const unlock = await bff.unlockWithDevicePin({ pin: '482913' });
175
+ switch (unlock.status) {
176
+ case 'success': /* unlock.user — a session cookie was set */ break;
177
+ case 'invalid': /* wrong PIN / unknown-or-revoked device */ break;
178
+ case 'locked': /* device lockout — unlock.retryAfterSeconds */ break;
179
+ case 'rateLimited': /* per-IP limiter — may poll through it */ break;
180
+ case 'error': /* network / unexpected */ break;
181
+ }
182
+
183
+ await bff.disableDevicePin(); // true on success, never throws
184
+ ```
185
+
186
+ The two `429` outcomes are distinct on purpose: the per-IP `BffAuth` limiter
187
+ answers `429` with an **empty** body (`rateLimited` — a UI may poll through it),
188
+ whereas the device-PIN lockout answers `429` with a JSON `{ error }` body + a
189
+ `Retry-After` header (`locked` — show a "try again in N s" message).
190
+
154
191
  The direct-KC `AuthClient` / ROPC surface below is retained for consumers not
155
192
  yet on a BFF; it is deprecated and removed once every app has migrated.
156
193
 
@@ -191,7 +228,7 @@ auth.on('sessionExpired', () => {
191
228
 
192
229
  ### Core (`@dloizides/auth-client`)
193
230
 
194
- - `BffAuthClient` — same-origin client for a per-app Backend-For-Frontend. `login()`, `logout()`, `getCurrentUser()`, `register()`, `forgotPassword()`, `resetPassword()`. No token handling — the BFF owns tokens, the browser owns only an httpOnly cookie. **The recommended auth surface (v3).**
231
+ - `BffAuthClient` — same-origin client for a per-app Backend-For-Frontend. `login()`, `logout()`, `getCurrentUser()`, `register()`, `forgotPassword()`, `resetPassword()`, `requestOtp()`, `verifyOtp()`, `pinLogin()`, plus the v3.3 device-PIN surface: `getLoginConfig()`, `enrollDevicePin()`, `unlockWithDevicePin()`, `disableDevicePin()` (discriminated, never-throwing results). No token handling — the BFF owns tokens, the browser owns only an httpOnly cookie. **The recommended auth surface (v3).**
195
232
  - `AuthClient` — realm-aware orchestrator. `init()`, `refresh()`, `loginWithOtp()`, `loginWithPassword()`, `logout({ everywhere })`, `requestPasswordReset()`, `confirmPasswordReset()`, plus the v1 surface (`getAccessToken`, `getTokens`, `setTokens`, `clearTokens`, `buildAuthorizationUrl`, etc.). Direct-KC ROPC; deprecated in favour of `BffAuthClient`.
196
233
  - `AuthApiClient` — typed wrapper for IdentityService auth endpoints.
197
234
  - `AuthEventEmitter` — `sessionExpired` event.
@@ -1,4 +1,4 @@
1
- import { H as HttpClient, T as TokenResponse } from './TokenResponse-CY1CaU2l.mjs';
1
+ import { H as HttpClient, T as TokenResponse } from './TokenResponse-BkIDjenX.mjs';
2
2
 
3
3
  /**
4
4
  * Tiny dependency-free event emitter for auth lifecycle events.
@@ -1,4 +1,4 @@
1
- import { H as HttpClient, T as TokenResponse } from './TokenResponse-CY1CaU2l.js';
1
+ import { H as HttpClient, T as TokenResponse } from './TokenResponse-BkIDjenX.js';
2
2
 
3
3
  /**
4
4
  * Tiny dependency-free event emitter for auth lifecycle events.
@@ -20,6 +20,13 @@ interface HttpResponse {
20
20
  ok: boolean;
21
21
  /** Parsed body (already JSON-decoded). `undefined` for 204 / empty bodies. */
22
22
  data?: unknown;
23
+ /**
24
+ * Read a single response header by (case-insensitive) name, or `null` when
25
+ * absent. Optional so existing transports stay source-compatible; the bundled
26
+ * `createFetchHttpClient` always provides it. The device-PIN unlock flow needs
27
+ * it to read `Retry-After` off a `429`.
28
+ */
29
+ header?: (name: string) => string | null;
23
30
  }
24
31
  type HttpClient = (request: HttpRequest) => Promise<HttpResponse>;
25
32
  /**
@@ -20,6 +20,13 @@ interface HttpResponse {
20
20
  ok: boolean;
21
21
  /** Parsed body (already JSON-decoded). `undefined` for 204 / empty bodies. */
22
22
  data?: unknown;
23
+ /**
24
+ * Read a single response header by (case-insensitive) name, or `null` when
25
+ * absent. Optional so existing transports stay source-compatible; the bundled
26
+ * `createFetchHttpClient` always provides it. The device-PIN unlock flow needs
27
+ * it to read `Retry-After` off a `429`.
28
+ */
29
+ header?: (name: string) => string | null;
23
30
  }
24
31
  type HttpClient = (request: HttpRequest) => Promise<HttpResponse>;
25
32
  /**
package/dist/index.d.mts CHANGED
@@ -1,8 +1,8 @@
1
- import { T as TokenStorage, k as AuthTokens } from './AuthClient-D8Ul-aGa.mjs';
2
- export { A as AuthApiClient, a as AuthApiClientOptions, b as AuthClient, c as AuthClientCollaborators, d as AuthClientConfig, e as AuthClientFromIssuerInput, f as AuthEventEmitter, g as AuthEventListener, h as AuthEventName, i as AuthEventUnsubscribe, j as AuthSessionInfo, D as DirectKcOptions, F as ForgotPasswordRequest, I as InactivityStore, l as InactivityTracker, m as InactivityTrackerOptions, L as LoginOptions, n as LogoutOptions, O as OtpLoginRequest, P as PasswordLoginRequest, R as RawAuthLoginResponse, o as RefreshFn, p as RefreshInterceptor, q as RefreshInterceptorOptions, r as ResetPasswordRequest } from './AuthClient-D8Ul-aGa.mjs';
1
+ import { T as TokenStorage, k as AuthTokens } from './AuthClient-3lu6Y1bY.mjs';
2
+ export { A as AuthApiClient, a as AuthApiClientOptions, b as AuthClient, c as AuthClientCollaborators, d as AuthClientConfig, e as AuthClientFromIssuerInput, f as AuthEventEmitter, g as AuthEventListener, h as AuthEventName, i as AuthEventUnsubscribe, j as AuthSessionInfo, D as DirectKcOptions, F as ForgotPasswordRequest, I as InactivityStore, l as InactivityTracker, m as InactivityTrackerOptions, L as LoginOptions, n as LogoutOptions, O as OtpLoginRequest, P as PasswordLoginRequest, R as RawAuthLoginResponse, o as RefreshFn, p as RefreshInterceptor, q as RefreshInterceptorOptions, r as ResetPasswordRequest } from './AuthClient-3lu6Y1bY.mjs';
3
3
  export { ExchangeAuthorizationCodeInput, FetchDiscoveryDocumentInput, OidcDiscoveryDocument, PkcePair, RefreshAccessTokenInput, clearDiscoveryCache, deriveCodeChallenge, exchangeAuthorizationCode, fetchDiscoveryDocument, generateCodeVerifier, generatePkcePair, refreshAccessToken } from './oidc/index.mjs';
4
- import { H as HttpClient, R as RawTokenResponse, T as TokenResponse } from './TokenResponse-CY1CaU2l.mjs';
5
- export { a as HttpRequest, b as HttpResponse, c as createFetchHttpClient } from './TokenResponse-CY1CaU2l.mjs';
4
+ import { H as HttpClient, R as RawTokenResponse, T as TokenResponse } from './TokenResponse-BkIDjenX.mjs';
5
+ export { a as HttpRequest, b as HttpResponse, c as createFetchHttpClient } from './TokenResponse-BkIDjenX.mjs';
6
6
 
7
7
  /**
8
8
  * Roles emitted by Keycloak realms in the dloizides.com portfolio.
@@ -372,6 +372,98 @@ interface BffPinLoginRequest {
372
372
  /** External id of the event the PIN is scoped to (supplied by the page/route). */
373
373
  eventExternalId: string;
374
374
  }
375
+ /** Payload for `POST /bff/pin/enroll` — bind a device PIN to the current session. */
376
+ interface BffDevicePinEnrollRequest {
377
+ /** The numeric PIN the user chose. */
378
+ pin: string;
379
+ /** The chosen PIN length (must be one of 4 / 6 / 8). */
380
+ digits: number;
381
+ }
382
+ /** Payload for `POST /bff/pin/unlock` — re-establish a session from a remembered device. */
383
+ interface BffDevicePinUnlockRequest {
384
+ /** The numeric device PIN the returning user entered. */
385
+ pin: string;
386
+ }
387
+ /**
388
+ * The optional per-device state half of `GET /bff/config`, all fields safe-defaulted.
389
+ *
390
+ * Read off the BFF's per-device record (keyed by the device cookie). Older BFFs /
391
+ * tests omit the fields entirely, in which case the PIN-unlock gate simply never
392
+ * triggers (`hasPin` defaults to `false`, `rememberedUsername` to `null`).
393
+ */
394
+ interface BffDeviceState {
395
+ /** Non-secret username this device remembers, or `null` when none. */
396
+ rememberedUsername: string | null;
397
+ /** `true` when this device has an enrolled device PIN. */
398
+ hasPin: boolean;
399
+ /** The enrolled PIN length (4 / 6 / 8), or `null` when unknown / no PIN. */
400
+ pinDigits: number | null;
401
+ /** The device-local preferred-method hint (e.g. `"pin"`), or `null`. */
402
+ preferredMethod: string | null;
403
+ }
404
+ /**
405
+ * The parsed `GET /bff/config` response: which login methods this BFF advertises,
406
+ * whether self-serve registration is enabled, and the optional per-device state.
407
+ *
408
+ * `methods` are the lowercase strings the server-side `BffLoginMethod` enum
409
+ * serialises to (`"password"` | `"otp"` | `"pin"` | `"passkey"`), de-duplicated
410
+ * and order-preserving. On a network failure or malformed body the client returns
411
+ * a safe fallback (`["password"]`, registration off, empty device-state).
412
+ */
413
+ interface BffLoginConfig {
414
+ /** The enabled login methods, lowercase, in the order the BFF advertised them. */
415
+ methods: string[];
416
+ /** `true` when this BFF exposes self-serve registration. */
417
+ registrationEnabled: boolean;
418
+ /** The optional per-device state (remembered username + device PIN), safe-defaulted. */
419
+ deviceState: BffDeviceState;
420
+ }
421
+ /**
422
+ * Discriminated result of `unlockWithDevicePin`. Never rejects — the unlock UI
423
+ * routes on `status` (the published `login`/`pinLogin` throw an opaque error on
424
+ * any non-2xx, collapsing 401 vs 429; this is why the device-PIN flow needs its
425
+ * own client surface).
426
+ *
427
+ * The two `429` outcomes are distinct and MUST stay distinct: the BFF's per-IP
428
+ * `BffAuth` sliding-window limiter answers `429` with an EMPTY body
429
+ * (`rateLimited` — the UI may poll through it), whereas the device-PIN lockout
430
+ * answers `429` with a JSON `{ error }` body + a `Retry-After` header (`locked` —
431
+ * the UI shows a "try again in N s" message).
432
+ */
433
+ type DevicePinUnlockResult = {
434
+ status: 'success';
435
+ user: BffUser;
436
+ } | {
437
+ status: 'invalid';
438
+ } | {
439
+ status: 'locked';
440
+ retryAfterSeconds: number | null;
441
+ } | {
442
+ status: 'rateLimited';
443
+ retryAfterSeconds: number | null;
444
+ } | {
445
+ status: 'error';
446
+ };
447
+ /**
448
+ * Discriminated result of `enrollDevicePin`. Never rejects — the enrol form routes
449
+ * on `status`:
450
+ * - `success` — HTTP 200, the device PIN is bound to the current session;
451
+ * - `unauthorized` — HTTP 401, no session to bind to;
452
+ * - `forbidden` — HTTP 403, a PIN-established session can't enrol a new PIN;
453
+ * - `invalidPin` — HTTP 400, the PIN format was rejected;
454
+ * - `error` — anything else (501 disabled / 502 grant failed / network).
455
+ */
456
+ type DevicePinEnrollResult = {
457
+ status: 'success';
458
+ } | {
459
+ status: 'unauthorized';
460
+ } | {
461
+ status: 'forbidden';
462
+ } | {
463
+ status: 'invalidPin';
464
+ } | {
465
+ status: 'error';
466
+ };
375
467
  /**
376
468
  * The body `POST /bff/otp/request` relays from TenantService.
377
469
  *
@@ -484,6 +576,58 @@ declare class BffAuthClient {
484
576
  * is not an enabled method for this BFF.
485
577
  */
486
578
  pinLogin(request: BffPinLoginRequest): Promise<BffUser>;
579
+ /**
580
+ * `GET /bff/config` — which login methods this BFF advertises, whether
581
+ * registration is enabled, and the optional per-device state (remembered
582
+ * username + device PIN). Unauthenticated, no CSRF (GET).
583
+ *
584
+ * NEVER throws: on a non-2xx, a network error, or a malformed body it returns
585
+ * a safe fallback (`{ methods: ['password'], registrationEnabled: false,
586
+ * deviceState: { rememberedUsername: null, hasPin: false, pinDigits: null,
587
+ * preferredMethod: null } }`) so the login surface stays usable even when the
588
+ * config endpoint is unreachable.
589
+ */
590
+ getLoginConfig(): Promise<BffLoginConfig>;
591
+ /**
592
+ * `POST /bff/pin/enroll` — bind a device PIN to the CURRENT strong session.
593
+ * Requires an authenticated session (the cookie travels via
594
+ * `credentials: 'include'`); the BFF requests an offline token, hashes the
595
+ * PIN, and sets the device cookie.
596
+ *
597
+ * NEVER throws — resolves a discriminated {@link DevicePinEnrollResult}:
598
+ * - 200 → `success`;
599
+ * - 401 → `unauthorized` (no session);
600
+ * - 403 → `forbidden` (a PIN-established session can't enrol);
601
+ * - 400 → `invalidPin` (bad format);
602
+ * - anything else (501 disabled / 502 grant failed) / network → `error`.
603
+ */
604
+ enrollDevicePin(request: BffDevicePinEnrollRequest): Promise<DevicePinEnrollResult>;
605
+ /**
606
+ * `POST /bff/pin/unlock` — re-establish a session from a remembered device.
607
+ * No prior session; the device cookie travels via `credentials: 'include'`.
608
+ *
609
+ * NEVER throws — resolves a discriminated {@link DevicePinUnlockResult}:
610
+ * - 200 + `{ user }` → `success` (a session cookie was set);
611
+ * - 401 → `invalid` (wrong PIN / unknown-or-revoked device);
612
+ * - 429 + JSON body → `locked` (device lockout; `Retry-After` parsed);
613
+ * - 429 + empty body → `rateLimited` (per-IP limiter; `Retry-After` parsed);
614
+ * - anything else / malformed 200 body / network → `error`.
615
+ */
616
+ unlockWithDevicePin(request: BffDevicePinUnlockRequest): Promise<DevicePinUnlockResult>;
617
+ /**
618
+ * `POST /bff/pin/disable` — drop the device PIN for the CURRENT session. The
619
+ * BFF revokes the offline token at Keycloak, deletes the device record, and
620
+ * clears the device cookie. Requires an authenticated session.
621
+ *
622
+ * NEVER throws — resolves `true` on a 2xx, `false` on anything else.
623
+ */
624
+ disableDevicePin(): Promise<boolean>;
625
+ /**
626
+ * Shared POST for the never-throw device-PIN calls: same-origin, cookie
627
+ * included, `X-BFF-Csrf` header attached. Returns the raw {@link HttpResponse}
628
+ * (status + body + headers) so the caller can route on it instead of throwing.
629
+ */
630
+ private postRaw;
487
631
  /**
488
632
  * Shared POST for every state-changing `/bff/*` call: same-origin, cookie
489
633
  * included, `X-BFF-Csrf` header attached. Throws a labelled error on non-2xx.
@@ -664,4 +808,4 @@ declare function normalizeTokenResponse(raw: RawTokenResponse): TokenResponse;
664
808
  */
665
809
  declare function tokenResponseToAuthTokens(response: TokenResponse, now?: number): AuthTokens;
666
810
 
667
- export { AuthTokens, type AuthorizationCodeBodyInput, type AuthorizationResponseLike, type AuthorizationUrlInput, BffAuthClient, type BffAuthClientOptions, type BffForgotPasswordRequest, type BffLoginRequest, type BffOtpRequestRequest, type BffOtpRequestResult, type BffOtpVerifyRequest, type BffPinLoginRequest, type BffRegisterRequest, type BffResetPasswordRequest, type BffUser, type BiometricFlagStore, BiometricGate, type BiometricGateLike, type BiometricGateOptions, BrowserStorageTokenStorage, type BrowserStorageTokenStorageOptions, CookieTokenStorage, HttpClient, InMemoryTokenStorage, KeycloakRoles, type KeycloakUserInfo, type LocalAuthLike, type NormalizedUser, RawTokenResponse, type RefreshTokenBodyInput, type SecureStoreLike, SecureStoreTokenStorage, type SecureStoreTokenStorageOptions, type StorageLike, TokenResponse, TokenStorage, buildAuthorizationCodeBody, buildAuthorizationEndpoint, buildAuthorizationUrl, buildIssuerUrl, buildLogoutEndpoint, buildRefreshTokenBody, buildTokenEndpoint, buildUserInfoEndpoint, computeExpiresAt, decodeJwt, extractAuthCode, isKeycloakRole, isTokenExpired, normalizeKeycloakUser, normalizeTokenResponse, parseBaseUrlFromIssuer, parseRealmFromIssuer, tokenResponseToAuthTokens };
811
+ export { AuthTokens, type AuthorizationCodeBodyInput, type AuthorizationResponseLike, type AuthorizationUrlInput, BffAuthClient, type BffAuthClientOptions, type BffDevicePinEnrollRequest, type BffDevicePinUnlockRequest, type BffDeviceState, type BffForgotPasswordRequest, type BffLoginConfig, type BffLoginRequest, type BffOtpRequestRequest, type BffOtpRequestResult, type BffOtpVerifyRequest, type BffPinLoginRequest, type BffRegisterRequest, type BffResetPasswordRequest, type BffUser, type BiometricFlagStore, BiometricGate, type BiometricGateLike, type BiometricGateOptions, BrowserStorageTokenStorage, type BrowserStorageTokenStorageOptions, CookieTokenStorage, type DevicePinEnrollResult, type DevicePinUnlockResult, HttpClient, InMemoryTokenStorage, KeycloakRoles, type KeycloakUserInfo, type LocalAuthLike, type NormalizedUser, RawTokenResponse, type RefreshTokenBodyInput, type SecureStoreLike, SecureStoreTokenStorage, type SecureStoreTokenStorageOptions, type StorageLike, TokenResponse, TokenStorage, buildAuthorizationCodeBody, buildAuthorizationEndpoint, buildAuthorizationUrl, buildIssuerUrl, buildLogoutEndpoint, buildRefreshTokenBody, buildTokenEndpoint, buildUserInfoEndpoint, computeExpiresAt, decodeJwt, extractAuthCode, isKeycloakRole, isTokenExpired, normalizeKeycloakUser, normalizeTokenResponse, parseBaseUrlFromIssuer, parseRealmFromIssuer, tokenResponseToAuthTokens };
package/dist/index.d.ts CHANGED
@@ -1,8 +1,8 @@
1
- import { T as TokenStorage, k as AuthTokens } from './AuthClient-Cv7btBX0.js';
2
- export { A as AuthApiClient, a as AuthApiClientOptions, b as AuthClient, c as AuthClientCollaborators, d as AuthClientConfig, e as AuthClientFromIssuerInput, f as AuthEventEmitter, g as AuthEventListener, h as AuthEventName, i as AuthEventUnsubscribe, j as AuthSessionInfo, D as DirectKcOptions, F as ForgotPasswordRequest, I as InactivityStore, l as InactivityTracker, m as InactivityTrackerOptions, L as LoginOptions, n as LogoutOptions, O as OtpLoginRequest, P as PasswordLoginRequest, R as RawAuthLoginResponse, o as RefreshFn, p as RefreshInterceptor, q as RefreshInterceptorOptions, r as ResetPasswordRequest } from './AuthClient-Cv7btBX0.js';
1
+ import { T as TokenStorage, k as AuthTokens } from './AuthClient-Bb7N2shJ.js';
2
+ export { A as AuthApiClient, a as AuthApiClientOptions, b as AuthClient, c as AuthClientCollaborators, d as AuthClientConfig, e as AuthClientFromIssuerInput, f as AuthEventEmitter, g as AuthEventListener, h as AuthEventName, i as AuthEventUnsubscribe, j as AuthSessionInfo, D as DirectKcOptions, F as ForgotPasswordRequest, I as InactivityStore, l as InactivityTracker, m as InactivityTrackerOptions, L as LoginOptions, n as LogoutOptions, O as OtpLoginRequest, P as PasswordLoginRequest, R as RawAuthLoginResponse, o as RefreshFn, p as RefreshInterceptor, q as RefreshInterceptorOptions, r as ResetPasswordRequest } from './AuthClient-Bb7N2shJ.js';
3
3
  export { ExchangeAuthorizationCodeInput, FetchDiscoveryDocumentInput, OidcDiscoveryDocument, PkcePair, RefreshAccessTokenInput, clearDiscoveryCache, deriveCodeChallenge, exchangeAuthorizationCode, fetchDiscoveryDocument, generateCodeVerifier, generatePkcePair, refreshAccessToken } from './oidc/index.js';
4
- import { H as HttpClient, R as RawTokenResponse, T as TokenResponse } from './TokenResponse-CY1CaU2l.js';
5
- export { a as HttpRequest, b as HttpResponse, c as createFetchHttpClient } from './TokenResponse-CY1CaU2l.js';
4
+ import { H as HttpClient, R as RawTokenResponse, T as TokenResponse } from './TokenResponse-BkIDjenX.js';
5
+ export { a as HttpRequest, b as HttpResponse, c as createFetchHttpClient } from './TokenResponse-BkIDjenX.js';
6
6
 
7
7
  /**
8
8
  * Roles emitted by Keycloak realms in the dloizides.com portfolio.
@@ -372,6 +372,98 @@ interface BffPinLoginRequest {
372
372
  /** External id of the event the PIN is scoped to (supplied by the page/route). */
373
373
  eventExternalId: string;
374
374
  }
375
+ /** Payload for `POST /bff/pin/enroll` — bind a device PIN to the current session. */
376
+ interface BffDevicePinEnrollRequest {
377
+ /** The numeric PIN the user chose. */
378
+ pin: string;
379
+ /** The chosen PIN length (must be one of 4 / 6 / 8). */
380
+ digits: number;
381
+ }
382
+ /** Payload for `POST /bff/pin/unlock` — re-establish a session from a remembered device. */
383
+ interface BffDevicePinUnlockRequest {
384
+ /** The numeric device PIN the returning user entered. */
385
+ pin: string;
386
+ }
387
+ /**
388
+ * The optional per-device state half of `GET /bff/config`, all fields safe-defaulted.
389
+ *
390
+ * Read off the BFF's per-device record (keyed by the device cookie). Older BFFs /
391
+ * tests omit the fields entirely, in which case the PIN-unlock gate simply never
392
+ * triggers (`hasPin` defaults to `false`, `rememberedUsername` to `null`).
393
+ */
394
+ interface BffDeviceState {
395
+ /** Non-secret username this device remembers, or `null` when none. */
396
+ rememberedUsername: string | null;
397
+ /** `true` when this device has an enrolled device PIN. */
398
+ hasPin: boolean;
399
+ /** The enrolled PIN length (4 / 6 / 8), or `null` when unknown / no PIN. */
400
+ pinDigits: number | null;
401
+ /** The device-local preferred-method hint (e.g. `"pin"`), or `null`. */
402
+ preferredMethod: string | null;
403
+ }
404
+ /**
405
+ * The parsed `GET /bff/config` response: which login methods this BFF advertises,
406
+ * whether self-serve registration is enabled, and the optional per-device state.
407
+ *
408
+ * `methods` are the lowercase strings the server-side `BffLoginMethod` enum
409
+ * serialises to (`"password"` | `"otp"` | `"pin"` | `"passkey"`), de-duplicated
410
+ * and order-preserving. On a network failure or malformed body the client returns
411
+ * a safe fallback (`["password"]`, registration off, empty device-state).
412
+ */
413
+ interface BffLoginConfig {
414
+ /** The enabled login methods, lowercase, in the order the BFF advertised them. */
415
+ methods: string[];
416
+ /** `true` when this BFF exposes self-serve registration. */
417
+ registrationEnabled: boolean;
418
+ /** The optional per-device state (remembered username + device PIN), safe-defaulted. */
419
+ deviceState: BffDeviceState;
420
+ }
421
+ /**
422
+ * Discriminated result of `unlockWithDevicePin`. Never rejects — the unlock UI
423
+ * routes on `status` (the published `login`/`pinLogin` throw an opaque error on
424
+ * any non-2xx, collapsing 401 vs 429; this is why the device-PIN flow needs its
425
+ * own client surface).
426
+ *
427
+ * The two `429` outcomes are distinct and MUST stay distinct: the BFF's per-IP
428
+ * `BffAuth` sliding-window limiter answers `429` with an EMPTY body
429
+ * (`rateLimited` — the UI may poll through it), whereas the device-PIN lockout
430
+ * answers `429` with a JSON `{ error }` body + a `Retry-After` header (`locked` —
431
+ * the UI shows a "try again in N s" message).
432
+ */
433
+ type DevicePinUnlockResult = {
434
+ status: 'success';
435
+ user: BffUser;
436
+ } | {
437
+ status: 'invalid';
438
+ } | {
439
+ status: 'locked';
440
+ retryAfterSeconds: number | null;
441
+ } | {
442
+ status: 'rateLimited';
443
+ retryAfterSeconds: number | null;
444
+ } | {
445
+ status: 'error';
446
+ };
447
+ /**
448
+ * Discriminated result of `enrollDevicePin`. Never rejects — the enrol form routes
449
+ * on `status`:
450
+ * - `success` — HTTP 200, the device PIN is bound to the current session;
451
+ * - `unauthorized` — HTTP 401, no session to bind to;
452
+ * - `forbidden` — HTTP 403, a PIN-established session can't enrol a new PIN;
453
+ * - `invalidPin` — HTTP 400, the PIN format was rejected;
454
+ * - `error` — anything else (501 disabled / 502 grant failed / network).
455
+ */
456
+ type DevicePinEnrollResult = {
457
+ status: 'success';
458
+ } | {
459
+ status: 'unauthorized';
460
+ } | {
461
+ status: 'forbidden';
462
+ } | {
463
+ status: 'invalidPin';
464
+ } | {
465
+ status: 'error';
466
+ };
375
467
  /**
376
468
  * The body `POST /bff/otp/request` relays from TenantService.
377
469
  *
@@ -484,6 +576,58 @@ declare class BffAuthClient {
484
576
  * is not an enabled method for this BFF.
485
577
  */
486
578
  pinLogin(request: BffPinLoginRequest): Promise<BffUser>;
579
+ /**
580
+ * `GET /bff/config` — which login methods this BFF advertises, whether
581
+ * registration is enabled, and the optional per-device state (remembered
582
+ * username + device PIN). Unauthenticated, no CSRF (GET).
583
+ *
584
+ * NEVER throws: on a non-2xx, a network error, or a malformed body it returns
585
+ * a safe fallback (`{ methods: ['password'], registrationEnabled: false,
586
+ * deviceState: { rememberedUsername: null, hasPin: false, pinDigits: null,
587
+ * preferredMethod: null } }`) so the login surface stays usable even when the
588
+ * config endpoint is unreachable.
589
+ */
590
+ getLoginConfig(): Promise<BffLoginConfig>;
591
+ /**
592
+ * `POST /bff/pin/enroll` — bind a device PIN to the CURRENT strong session.
593
+ * Requires an authenticated session (the cookie travels via
594
+ * `credentials: 'include'`); the BFF requests an offline token, hashes the
595
+ * PIN, and sets the device cookie.
596
+ *
597
+ * NEVER throws — resolves a discriminated {@link DevicePinEnrollResult}:
598
+ * - 200 → `success`;
599
+ * - 401 → `unauthorized` (no session);
600
+ * - 403 → `forbidden` (a PIN-established session can't enrol);
601
+ * - 400 → `invalidPin` (bad format);
602
+ * - anything else (501 disabled / 502 grant failed) / network → `error`.
603
+ */
604
+ enrollDevicePin(request: BffDevicePinEnrollRequest): Promise<DevicePinEnrollResult>;
605
+ /**
606
+ * `POST /bff/pin/unlock` — re-establish a session from a remembered device.
607
+ * No prior session; the device cookie travels via `credentials: 'include'`.
608
+ *
609
+ * NEVER throws — resolves a discriminated {@link DevicePinUnlockResult}:
610
+ * - 200 + `{ user }` → `success` (a session cookie was set);
611
+ * - 401 → `invalid` (wrong PIN / unknown-or-revoked device);
612
+ * - 429 + JSON body → `locked` (device lockout; `Retry-After` parsed);
613
+ * - 429 + empty body → `rateLimited` (per-IP limiter; `Retry-After` parsed);
614
+ * - anything else / malformed 200 body / network → `error`.
615
+ */
616
+ unlockWithDevicePin(request: BffDevicePinUnlockRequest): Promise<DevicePinUnlockResult>;
617
+ /**
618
+ * `POST /bff/pin/disable` — drop the device PIN for the CURRENT session. The
619
+ * BFF revokes the offline token at Keycloak, deletes the device record, and
620
+ * clears the device cookie. Requires an authenticated session.
621
+ *
622
+ * NEVER throws — resolves `true` on a 2xx, `false` on anything else.
623
+ */
624
+ disableDevicePin(): Promise<boolean>;
625
+ /**
626
+ * Shared POST for the never-throw device-PIN calls: same-origin, cookie
627
+ * included, `X-BFF-Csrf` header attached. Returns the raw {@link HttpResponse}
628
+ * (status + body + headers) so the caller can route on it instead of throwing.
629
+ */
630
+ private postRaw;
487
631
  /**
488
632
  * Shared POST for every state-changing `/bff/*` call: same-origin, cookie
489
633
  * included, `X-BFF-Csrf` header attached. Throws a labelled error on non-2xx.
@@ -664,4 +808,4 @@ declare function normalizeTokenResponse(raw: RawTokenResponse): TokenResponse;
664
808
  */
665
809
  declare function tokenResponseToAuthTokens(response: TokenResponse, now?: number): AuthTokens;
666
810
 
667
- export { AuthTokens, type AuthorizationCodeBodyInput, type AuthorizationResponseLike, type AuthorizationUrlInput, BffAuthClient, type BffAuthClientOptions, type BffForgotPasswordRequest, type BffLoginRequest, type BffOtpRequestRequest, type BffOtpRequestResult, type BffOtpVerifyRequest, type BffPinLoginRequest, type BffRegisterRequest, type BffResetPasswordRequest, type BffUser, type BiometricFlagStore, BiometricGate, type BiometricGateLike, type BiometricGateOptions, BrowserStorageTokenStorage, type BrowserStorageTokenStorageOptions, CookieTokenStorage, HttpClient, InMemoryTokenStorage, KeycloakRoles, type KeycloakUserInfo, type LocalAuthLike, type NormalizedUser, RawTokenResponse, type RefreshTokenBodyInput, type SecureStoreLike, SecureStoreTokenStorage, type SecureStoreTokenStorageOptions, type StorageLike, TokenResponse, TokenStorage, buildAuthorizationCodeBody, buildAuthorizationEndpoint, buildAuthorizationUrl, buildIssuerUrl, buildLogoutEndpoint, buildRefreshTokenBody, buildTokenEndpoint, buildUserInfoEndpoint, computeExpiresAt, decodeJwt, extractAuthCode, isKeycloakRole, isTokenExpired, normalizeKeycloakUser, normalizeTokenResponse, parseBaseUrlFromIssuer, parseRealmFromIssuer, tokenResponseToAuthTokens };
811
+ export { AuthTokens, type AuthorizationCodeBodyInput, type AuthorizationResponseLike, type AuthorizationUrlInput, BffAuthClient, type BffAuthClientOptions, type BffDevicePinEnrollRequest, type BffDevicePinUnlockRequest, type BffDeviceState, type BffForgotPasswordRequest, type BffLoginConfig, type BffLoginRequest, type BffOtpRequestRequest, type BffOtpRequestResult, type BffOtpVerifyRequest, type BffPinLoginRequest, type BffRegisterRequest, type BffResetPasswordRequest, type BffUser, type BiometricFlagStore, BiometricGate, type BiometricGateLike, type BiometricGateOptions, BrowserStorageTokenStorage, type BrowserStorageTokenStorageOptions, CookieTokenStorage, type DevicePinEnrollResult, type DevicePinUnlockResult, HttpClient, InMemoryTokenStorage, KeycloakRoles, type KeycloakUserInfo, type LocalAuthLike, type NormalizedUser, RawTokenResponse, type RefreshTokenBodyInput, type SecureStoreLike, SecureStoreTokenStorage, type SecureStoreTokenStorageOptions, type StorageLike, TokenResponse, TokenStorage, buildAuthorizationCodeBody, buildAuthorizationEndpoint, buildAuthorizationUrl, buildIssuerUrl, buildLogoutEndpoint, buildRefreshTokenBody, buildTokenEndpoint, buildUserInfoEndpoint, computeExpiresAt, decodeJwt, extractAuthCode, isKeycloakRole, isTokenExpired, normalizeKeycloakUser, normalizeTokenResponse, parseBaseUrlFromIssuer, parseRealmFromIssuer, tokenResponseToAuthTokens };