@dloizides/auth-client 2.1.0 → 3.2.1

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,72 @@
1
1
  # Changelog
2
2
 
3
+ ## 3.2.0 (2026-05-22)
4
+
5
+ Additive release for Phase 3d of the unified-auth plan — event-scoped PIN
6
+ login. Extends `BffAuthClient` with the browser-facing PIN call so the new
7
+ `<PinForm>` in `@dloizides/auth-web` has a same-origin client. No breaking
8
+ changes.
9
+
10
+ ### Added
11
+
12
+ - `BffAuthClient.pinLogin({ pin, eventExternalId })` → `POST /bff/pin/login`.
13
+ The BFF runs the event-scoped PIN direct-grant against Keycloak server-side
14
+ (the `(event, pin)` pair resolves to the staff member's KC account + their
15
+ event-scoped role) and sets the httpOnly session cookie. Returns the
16
+ sanitised `BffUser`, exactly like `login` / `verifyOtp`. Throws on a non-2xx
17
+ (`401` for a bad / expired / locked-out PIN or an unknown event, `501` when
18
+ PIN login is not an enabled method). Carries the `X-BFF-Csrf` header like
19
+ every other state-changing call. No `username` / `password` ever leaves the
20
+ browser.
21
+ - Type: `BffPinLoginRequest`.
22
+
23
+ ## 3.1.0 (2026-05-22)
24
+
25
+ Additive release for Phase 2d of the unified-auth plan — email-OTP. Extends
26
+ `BffAuthClient` with the two browser-facing OTP calls so the new `<OtpForm>` in
27
+ `@dloizides/auth-web` has a same-origin client. No breaking changes.
28
+
29
+ ### Added
30
+
31
+ - `BffAuthClient.requestOtp({ identifier })` → `POST /bff/otp/request`. The BFF
32
+ proxies to TenantService, which emails a short-TTL code. The endpoint is
33
+ anti-enumeration (a `200` is the normal path), so the method **returns** the
34
+ relayed `{ success, expiresIn, code }` body — the UI uses `expiresIn` for a
35
+ countdown. It still throws on a non-2xx (`501` OTP not enabled, `502` upstream
36
+ down). Carries the `X-BFF-Csrf` header like every other state-changing call.
37
+ - `BffAuthClient.verifyOtp({ username, otp })` → `POST /bff/otp/verify`. The BFF
38
+ runs the OTP direct-grant against Keycloak server-side and sets the httpOnly
39
+ session cookie. Returns the sanitised `BffUser`, exactly like `login`. Throws
40
+ on a non-2xx (e.g. `401` for a bad / expired code).
41
+ - Types: `BffOtpRequestRequest`, `BffOtpVerifyRequest`, `BffOtpRequestResult`.
42
+
43
+ ## 3.0.0 (2026-05-19)
44
+
45
+ Major release for Phase 2 of the identity-hardening initiative. Adds the
46
+ shared `BffAuthClient` — the same-origin client for a per-app
47
+ **Backend-For-Frontend** (`bff-katalogos`, `bff-erevna`). The BFF terminates
48
+ authentication server-side: the browser holds only an opaque httpOnly session
49
+ cookie, never a token.
50
+
51
+ This is the **new recommended auth surface**. The major bump signals that
52
+ recommendation — it is **not** a breaking change: every v2.x export
53
+ (`AuthClient`, the direct-KC ROPC adapters, `useDirectKcAuth`, the OIDC
54
+ primitives, storage adapters, hooks) remains and is unchanged. BaseClient
55
+ still consumes the direct-KC path; it is removed in a later phase.
56
+
57
+ ### Added
58
+
59
+ - `BffAuthClient` — same-origin client for a per-app BFF. Methods:
60
+ `login({username,password})` → `POST /bff/login`; `logout()` →
61
+ `POST /bff/logout`; `getCurrentUser()` → `GET /bff/me`; `register(...)`,
62
+ `forgotPassword(...)`, `resetPassword(...)` → the matching `/bff/*`
63
+ endpoints. Every call is a same-origin `fetch` with
64
+ `credentials: 'include'`; state-changing calls carry the `X-BFF-Csrf: 1`
65
+ header the `Bff.AspNetCore` anti-forgery middleware requires. Does **no
66
+ token handling** — the BFF owns tokens, the browser owns only the cookie.
67
+ - Types: `BffAuthClientOptions`, `BffLoginRequest`, `BffRegisterRequest`,
68
+ `BffForgotPasswordRequest`, `BffResetPasswordRequest`, `BffUser`.
69
+
3
70
  ## 2.1.0 (2026-05-17)
4
71
 
5
72
  Additive release. Lays the groundwork for the "shrink identity service"
package/README.md CHANGED
@@ -119,6 +119,41 @@ const storage = new SecureStoreTokenStorage({
119
119
  await biometricGate.hydrate();
120
120
  ```
121
121
 
122
+ ## BFF auth (v3 — recommended)
123
+
124
+ `BffAuthClient` is the same-origin client for a per-app **Backend-For-Frontend**
125
+ (`bff-katalogos`, `bff-erevna`). The BFF terminates authentication
126
+ server-side: it does ROPC against Keycloak with a confidential client, stores
127
+ the tokens in a Redis vault, and hands the browser only an opaque httpOnly
128
+ session cookie. The SPA never sees a token — an XSS cannot exfiltrate one.
129
+
130
+ `BffAuthClient` does **no token handling**: every call is a same-origin
131
+ `fetch` with `credentials: 'include'`, and state-changing calls carry the
132
+ `X-BFF-Csrf: 1` header the BFF anti-forgery middleware requires.
133
+
134
+ ```ts
135
+ import { BffAuthClient, createFetchHttpClient } from '@dloizides/auth-client';
136
+
137
+ const bff = new BffAuthClient({
138
+ http: createFetchHttpClient(window.fetch.bind(window)),
139
+ // baseUrl defaults to '' (same-origin) — the production wiring.
140
+ });
141
+
142
+ // Login — the BFF does ROPC server-side and sets the session cookie.
143
+ const user = await bff.login({ username, password });
144
+
145
+ // Bootstrap on app load — null when there is no live session.
146
+ const current = await bff.getCurrentUser();
147
+
148
+ await bff.register({ firstName, lastName, username, email, password, tenantName });
149
+ await bff.forgotPassword({ email, resetUrlTemplate });
150
+ await bff.resetPassword({ token, newPassword });
151
+ await bff.logout();
152
+ ```
153
+
154
+ The direct-KC `AuthClient` / ROPC surface below is retained for consumers not
155
+ yet on a BFF; it is deprecated and removed once every app has migrated.
156
+
122
157
  ## React Query hooks
123
158
 
124
159
  ```ts
@@ -156,7 +191,8 @@ auth.on('sessionExpired', () => {
156
191
 
157
192
  ### Core (`@dloizides/auth-client`)
158
193
 
159
- - `AuthClient` — realm-aware orchestrator. `init()`, `refresh()`, `loginWithOtp()`, `loginWithPassword()`, `logout({ everywhere })`, `requestPasswordReset()`, `confirmPasswordReset()`, plus the v1 surface (`getAccessToken`, `getTokens`, `setTokens`, `clearTokens`, `buildAuthorizationUrl`, etc.).
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).**
195
+ - `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`.
160
196
  - `AuthApiClient` — typed wrapper for IdentityService auth endpoints.
161
197
  - `AuthEventEmitter` — `sessionExpired` event.
162
198
  - `RefreshInterceptor` — single-flight refresh queue.
@@ -457,4 +457,4 @@ declare class AuthClient {
457
457
  private resolveScope;
458
458
  }
459
459
 
460
- export { AuthApiClient as A, type DirectKcOptions as D, type ForgotPasswordRequest as F, type InactivityStore as I, type LoginOptions as L, type OtpLoginRequest as O, type PasswordLoginRequest as P, type ResetPasswordRequest as R, type TokenStorage as T, type AuthSessionInfo as a, AuthClient as b, type AuthTokens as c, type AuthApiClientOptions as d, type AuthClientCollaborators as e, type AuthClientConfig as f, type AuthClientFromIssuerInput as g, AuthEventEmitter as h, type AuthEventListener as i, type AuthEventName as j, type AuthEventUnsubscribe as k, InactivityTracker as l, type InactivityTrackerOptions as m, type LogoutOptions as n, type RawAuthLoginResponse as o, type RefreshFn as p, RefreshInterceptor as q, type RefreshInterceptorOptions as r };
460
+ export { AuthApiClient as A, type DirectKcOptions as D, type ForgotPasswordRequest as F, type InactivityStore as I, type LoginOptions as L, type OtpLoginRequest as O, type PasswordLoginRequest as P, type RawAuthLoginResponse as R, type TokenStorage as T, type AuthApiClientOptions as a, AuthClient as b, type AuthClientCollaborators as c, type AuthClientConfig as d, type AuthClientFromIssuerInput as e, AuthEventEmitter as f, type AuthEventListener as g, type AuthEventName as h, type AuthEventUnsubscribe as i, type AuthSessionInfo as j, type AuthTokens as k, InactivityTracker as l, type InactivityTrackerOptions as m, type LogoutOptions as n, type RefreshFn as o, RefreshInterceptor as p, type RefreshInterceptorOptions as q, type ResetPasswordRequest as r };
@@ -457,4 +457,4 @@ declare class AuthClient {
457
457
  private resolveScope;
458
458
  }
459
459
 
460
- export { AuthApiClient as A, type DirectKcOptions as D, type ForgotPasswordRequest as F, type InactivityStore as I, type LoginOptions as L, type OtpLoginRequest as O, type PasswordLoginRequest as P, type ResetPasswordRequest as R, type TokenStorage as T, type AuthSessionInfo as a, AuthClient as b, type AuthTokens as c, type AuthApiClientOptions as d, type AuthClientCollaborators as e, type AuthClientConfig as f, type AuthClientFromIssuerInput as g, AuthEventEmitter as h, type AuthEventListener as i, type AuthEventName as j, type AuthEventUnsubscribe as k, InactivityTracker as l, type InactivityTrackerOptions as m, type LogoutOptions as n, type RawAuthLoginResponse as o, type RefreshFn as p, RefreshInterceptor as q, type RefreshInterceptorOptions as r };
460
+ export { AuthApiClient as A, type DirectKcOptions as D, type ForgotPasswordRequest as F, type InactivityStore as I, type LoginOptions as L, type OtpLoginRequest as O, type PasswordLoginRequest as P, type RawAuthLoginResponse as R, type TokenStorage as T, type AuthApiClientOptions as a, AuthClient as b, type AuthClientCollaborators as c, type AuthClientConfig as d, type AuthClientFromIssuerInput as e, AuthEventEmitter as f, type AuthEventListener as g, type AuthEventName as h, type AuthEventUnsubscribe as i, type AuthSessionInfo as j, type AuthTokens as k, InactivityTracker as l, type InactivityTrackerOptions as m, type LogoutOptions as n, type RefreshFn as o, RefreshInterceptor as p, type RefreshInterceptorOptions as q, type ResetPasswordRequest as r };
package/dist/index.d.mts CHANGED
@@ -1,8 +1,8 @@
1
- import { T as TokenStorage, c as AuthTokens } from './AuthClient-BGr8L03W.mjs';
2
- export { A as AuthApiClient, d as AuthApiClientOptions, b as AuthClient, e as AuthClientCollaborators, f as AuthClientConfig, g as AuthClientFromIssuerInput, h as AuthEventEmitter, i as AuthEventListener, j as AuthEventName, k as AuthEventUnsubscribe, a 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, o as RawAuthLoginResponse, p as RefreshFn, q as RefreshInterceptor, r as RefreshInterceptorOptions, R as ResetPasswordRequest } from './AuthClient-BGr8L03W.mjs';
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';
3
3
  export { ExchangeAuthorizationCodeInput, FetchDiscoveryDocumentInput, OidcDiscoveryDocument, PkcePair, RefreshAccessTokenInput, clearDiscoveryCache, deriveCodeChallenge, exchangeAuthorizationCode, fetchDiscoveryDocument, generateCodeVerifier, generatePkcePair, refreshAccessToken } from './oidc/index.mjs';
4
- import { R as RawTokenResponse, T as TokenResponse } from './TokenResponse-CY1CaU2l.mjs';
5
- export { H as HttpClient, 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-CY1CaU2l.mjs';
5
+ export { a as HttpRequest, b as HttpResponse, c as createFetchHttpClient } from './TokenResponse-CY1CaU2l.mjs';
6
6
 
7
7
  /**
8
8
  * Roles emitted by Keycloak realms in the dloizides.com portfolio.
@@ -315,6 +315,182 @@ declare class BiometricGate {
315
315
  unlock(): Promise<void>;
316
316
  }
317
317
 
318
+ /** Credentials posted to `POST /bff/login`. */
319
+ interface BffLoginRequest {
320
+ username: string;
321
+ password: string;
322
+ }
323
+ /** Payload for `POST /bff/register` — proxied by the BFF to TenantService. */
324
+ interface BffRegisterRequest {
325
+ firstName: string;
326
+ lastName: string;
327
+ username: string;
328
+ email: string;
329
+ password: string;
330
+ tenantName: string;
331
+ [key: string]: unknown;
332
+ }
333
+ /** Payload for `POST /bff/forgot-password` — proxied to TenantService. */
334
+ interface BffForgotPasswordRequest {
335
+ email: string;
336
+ /** Full URL with a `{token}` placeholder; the backend substitutes the token. */
337
+ resetUrlTemplate?: string;
338
+ [key: string]: unknown;
339
+ }
340
+ /** Payload for `POST /bff/reset-password` — proxied to TenantService. */
341
+ interface BffResetPasswordRequest {
342
+ token: string;
343
+ newPassword: string;
344
+ }
345
+ /**
346
+ * Payload for `POST /bff/otp/request` — the BFF proxies it to TenantService,
347
+ * which generates a short-TTL code and emails it.
348
+ */
349
+ interface BffOtpRequestRequest {
350
+ /** The email address (or username) the one-time code is sent to. */
351
+ identifier: string;
352
+ }
353
+ /** Payload for `POST /bff/otp/verify` — the BFF exchanges it for a session. */
354
+ interface BffOtpVerifyRequest {
355
+ /** The email / username the code was requested for. */
356
+ username: string;
357
+ /** The one-time code the user entered. */
358
+ otp: string;
359
+ }
360
+ /**
361
+ * Payload for `POST /bff/pin/login` — the BFF exchanges an event-scoped PIN
362
+ * for a session.
363
+ *
364
+ * The `(event, pin)` pair alone identifies the staff member: no `username` /
365
+ * `password` ever leaves the browser. A PIN entered in an event's context
366
+ * grants that staff member their event-scoped role for that event only
367
+ * (the unified-auth plan §4.4 — event-scoped, per-individual PINs).
368
+ */
369
+ interface BffPinLoginRequest {
370
+ /** The numeric PIN the staff member entered. */
371
+ pin: string;
372
+ /** External id of the event the PIN is scoped to (supplied by the page/route). */
373
+ eventExternalId: string;
374
+ }
375
+ /**
376
+ * The body `POST /bff/otp/request` relays from TenantService.
377
+ *
378
+ * Anti-enumeration: the shape is identical whether or not the identifier is
379
+ * registered. `code` is non-null only outside production (a dev convenience);
380
+ * the UI must never depend on it being present.
381
+ */
382
+ interface BffOtpRequestResult {
383
+ /** Always `true` on a relayed 200 — the request was accepted. */
384
+ success: boolean;
385
+ /** Seconds until the emitted code expires — drives a countdown in the UI. */
386
+ expiresIn: number;
387
+ /** The code itself, non-production only; `null` (or absent) in production. */
388
+ code: string | null;
389
+ }
390
+ /**
391
+ * The user object returned by `GET /bff/me` and `POST /bff/login`. The BFF
392
+ * returns the sanitised KC claims under a `user` envelope and **never** a
393
+ * token. Kept permissive so server-added claims flow through without a bump.
394
+ */
395
+ interface BffUser {
396
+ sub?: string;
397
+ email?: string;
398
+ email_verified?: boolean;
399
+ name?: string;
400
+ preferred_username?: string;
401
+ given_name?: string;
402
+ family_name?: string;
403
+ tenantId?: string;
404
+ roles?: string[];
405
+ [key: string]: unknown;
406
+ }
407
+ interface BffAuthClientOptions {
408
+ /** Runtime-agnostic HTTP transport (wrap native `fetch` with `createFetchHttpClient`). */
409
+ http: HttpClient;
410
+ /**
411
+ * BFF origin. Defaults to `''` (same-origin) — the production wiring. An
412
+ * explicit origin is only useful for tests or a non-same-origin BFF.
413
+ */
414
+ baseUrl?: string;
415
+ }
416
+ /**
417
+ * Same-origin client for a per-app BFF.
418
+ *
419
+ * No token storage, no refresh logic, no realm awareness — the BFF owns all of
420
+ * that server-side. The browser's only auth artefact is the httpOnly cookie.
421
+ */
422
+ declare class BffAuthClient {
423
+ private readonly http;
424
+ private readonly baseUrl;
425
+ constructor(options: BffAuthClientOptions);
426
+ /**
427
+ * `POST /bff/login` — the BFF does ROPC against Keycloak server-side, stores
428
+ * the tokens in its Redis vault, and sets the httpOnly session cookie.
429
+ * Returns the sanitised user. Throws on a non-2xx response.
430
+ */
431
+ login(request: BffLoginRequest): Promise<BffUser>;
432
+ /**
433
+ * `POST /bff/logout` — the BFF calls KC end-session, deletes the Redis
434
+ * session, and clears the cookie. Non-fatal: a failed logout still leaves
435
+ * the SPA logged out client-side. Throws only on a non-2xx response.
436
+ */
437
+ logout(): Promise<void>;
438
+ /**
439
+ * `GET /bff/me` — the live session's sanitised user, or `null` when there is
440
+ * no session (the BFF answers `401`). Used at app load to bootstrap auth
441
+ * state in place of the old token-in-storage check.
442
+ */
443
+ getCurrentUser(): Promise<BffUser | null>;
444
+ /**
445
+ * `POST /bff/register` — the BFF proxies registration to TenantService and,
446
+ * on success, establishes a session exactly like `login`. Returns the user.
447
+ */
448
+ register(request: BffRegisterRequest): Promise<BffUser>;
449
+ /**
450
+ * `POST /bff/forgot-password` — proxied to TenantService. The backend
451
+ * returns 200 unconditionally (no email enumeration); anything else throws.
452
+ */
453
+ forgotPassword(request: BffForgotPasswordRequest): Promise<void>;
454
+ /**
455
+ * `POST /bff/reset-password` — proxied to TenantService. Throws on a non-2xx
456
+ * response (e.g. `400` for an invalid / expired token).
457
+ */
458
+ resetPassword(request: BffResetPasswordRequest): Promise<void>;
459
+ /**
460
+ * `POST /bff/otp/request` — the BFF proxies to TenantService, which generates
461
+ * a short-TTL code and emails it.
462
+ *
463
+ * The endpoint is anti-enumeration: a `200` is the normal path whether or not
464
+ * the identifier is registered. This method therefore **returns** the relayed
465
+ * `{ success, expiresIn, code }` body (so the UI can show the expiry) rather
466
+ * than treating a 200 as opaque. It still throws on a non-2xx — a `501`
467
+ * (OTP not enabled) or `502` (upstream down) is a real failure to surface.
468
+ */
469
+ requestOtp(request: BffOtpRequestRequest): Promise<BffOtpRequestResult>;
470
+ /**
471
+ * `POST /bff/otp/verify` — the BFF runs the OTP direct-grant against Keycloak
472
+ * server-side, stores the tokens in its Redis vault, and sets the httpOnly
473
+ * session cookie. Returns the sanitised user, exactly like `login`. Throws on
474
+ * a non-2xx (e.g. `401` for a bad / expired code).
475
+ */
476
+ verifyOtp(request: BffOtpVerifyRequest): Promise<BffUser>;
477
+ /**
478
+ * `POST /bff/pin/login` — the BFF runs the event-scoped PIN direct-grant
479
+ * against Keycloak server-side (the `(event, pin)` pair resolves to the
480
+ * staff member's KC account + event-scoped role), stores the tokens in its
481
+ * Redis vault, and sets the httpOnly session cookie. Returns the sanitised
482
+ * user, exactly like `login` / `verifyOtp`. Throws on a non-2xx — `401` for
483
+ * a bad / expired / locked-out PIN or an unknown event, `501` when PIN login
484
+ * is not an enabled method for this BFF.
485
+ */
486
+ pinLogin(request: BffPinLoginRequest): Promise<BffUser>;
487
+ /**
488
+ * Shared POST for every state-changing `/bff/*` call: same-origin, cookie
489
+ * included, `X-BFF-Csrf` header attached. Throws a labelled error on non-2xx.
490
+ */
491
+ private postState;
492
+ }
493
+
318
494
  /**
319
495
  * Convert a Keycloak `/userinfo` payload into a flat, app-friendly user object.
320
496
  *
@@ -488,4 +664,4 @@ declare function normalizeTokenResponse(raw: RawTokenResponse): TokenResponse;
488
664
  */
489
665
  declare function tokenResponseToAuthTokens(response: TokenResponse, now?: number): AuthTokens;
490
666
 
491
- export { AuthTokens, type AuthorizationCodeBodyInput, type AuthorizationResponseLike, type AuthorizationUrlInput, type BiometricFlagStore, BiometricGate, type BiometricGateLike, type BiometricGateOptions, BrowserStorageTokenStorage, type BrowserStorageTokenStorageOptions, CookieTokenStorage, 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 };
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 };
package/dist/index.d.ts CHANGED
@@ -1,8 +1,8 @@
1
- import { T as TokenStorage, c as AuthTokens } from './AuthClient-D95OMajD.js';
2
- export { A as AuthApiClient, d as AuthApiClientOptions, b as AuthClient, e as AuthClientCollaborators, f as AuthClientConfig, g as AuthClientFromIssuerInput, h as AuthEventEmitter, i as AuthEventListener, j as AuthEventName, k as AuthEventUnsubscribe, a 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, o as RawAuthLoginResponse, p as RefreshFn, q as RefreshInterceptor, r as RefreshInterceptorOptions, R as ResetPasswordRequest } from './AuthClient-D95OMajD.js';
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';
3
3
  export { ExchangeAuthorizationCodeInput, FetchDiscoveryDocumentInput, OidcDiscoveryDocument, PkcePair, RefreshAccessTokenInput, clearDiscoveryCache, deriveCodeChallenge, exchangeAuthorizationCode, fetchDiscoveryDocument, generateCodeVerifier, generatePkcePair, refreshAccessToken } from './oidc/index.js';
4
- import { R as RawTokenResponse, T as TokenResponse } from './TokenResponse-CY1CaU2l.js';
5
- export { H as HttpClient, 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-CY1CaU2l.js';
5
+ export { a as HttpRequest, b as HttpResponse, c as createFetchHttpClient } from './TokenResponse-CY1CaU2l.js';
6
6
 
7
7
  /**
8
8
  * Roles emitted by Keycloak realms in the dloizides.com portfolio.
@@ -315,6 +315,182 @@ declare class BiometricGate {
315
315
  unlock(): Promise<void>;
316
316
  }
317
317
 
318
+ /** Credentials posted to `POST /bff/login`. */
319
+ interface BffLoginRequest {
320
+ username: string;
321
+ password: string;
322
+ }
323
+ /** Payload for `POST /bff/register` — proxied by the BFF to TenantService. */
324
+ interface BffRegisterRequest {
325
+ firstName: string;
326
+ lastName: string;
327
+ username: string;
328
+ email: string;
329
+ password: string;
330
+ tenantName: string;
331
+ [key: string]: unknown;
332
+ }
333
+ /** Payload for `POST /bff/forgot-password` — proxied to TenantService. */
334
+ interface BffForgotPasswordRequest {
335
+ email: string;
336
+ /** Full URL with a `{token}` placeholder; the backend substitutes the token. */
337
+ resetUrlTemplate?: string;
338
+ [key: string]: unknown;
339
+ }
340
+ /** Payload for `POST /bff/reset-password` — proxied to TenantService. */
341
+ interface BffResetPasswordRequest {
342
+ token: string;
343
+ newPassword: string;
344
+ }
345
+ /**
346
+ * Payload for `POST /bff/otp/request` — the BFF proxies it to TenantService,
347
+ * which generates a short-TTL code and emails it.
348
+ */
349
+ interface BffOtpRequestRequest {
350
+ /** The email address (or username) the one-time code is sent to. */
351
+ identifier: string;
352
+ }
353
+ /** Payload for `POST /bff/otp/verify` — the BFF exchanges it for a session. */
354
+ interface BffOtpVerifyRequest {
355
+ /** The email / username the code was requested for. */
356
+ username: string;
357
+ /** The one-time code the user entered. */
358
+ otp: string;
359
+ }
360
+ /**
361
+ * Payload for `POST /bff/pin/login` — the BFF exchanges an event-scoped PIN
362
+ * for a session.
363
+ *
364
+ * The `(event, pin)` pair alone identifies the staff member: no `username` /
365
+ * `password` ever leaves the browser. A PIN entered in an event's context
366
+ * grants that staff member their event-scoped role for that event only
367
+ * (the unified-auth plan §4.4 — event-scoped, per-individual PINs).
368
+ */
369
+ interface BffPinLoginRequest {
370
+ /** The numeric PIN the staff member entered. */
371
+ pin: string;
372
+ /** External id of the event the PIN is scoped to (supplied by the page/route). */
373
+ eventExternalId: string;
374
+ }
375
+ /**
376
+ * The body `POST /bff/otp/request` relays from TenantService.
377
+ *
378
+ * Anti-enumeration: the shape is identical whether or not the identifier is
379
+ * registered. `code` is non-null only outside production (a dev convenience);
380
+ * the UI must never depend on it being present.
381
+ */
382
+ interface BffOtpRequestResult {
383
+ /** Always `true` on a relayed 200 — the request was accepted. */
384
+ success: boolean;
385
+ /** Seconds until the emitted code expires — drives a countdown in the UI. */
386
+ expiresIn: number;
387
+ /** The code itself, non-production only; `null` (or absent) in production. */
388
+ code: string | null;
389
+ }
390
+ /**
391
+ * The user object returned by `GET /bff/me` and `POST /bff/login`. The BFF
392
+ * returns the sanitised KC claims under a `user` envelope and **never** a
393
+ * token. Kept permissive so server-added claims flow through without a bump.
394
+ */
395
+ interface BffUser {
396
+ sub?: string;
397
+ email?: string;
398
+ email_verified?: boolean;
399
+ name?: string;
400
+ preferred_username?: string;
401
+ given_name?: string;
402
+ family_name?: string;
403
+ tenantId?: string;
404
+ roles?: string[];
405
+ [key: string]: unknown;
406
+ }
407
+ interface BffAuthClientOptions {
408
+ /** Runtime-agnostic HTTP transport (wrap native `fetch` with `createFetchHttpClient`). */
409
+ http: HttpClient;
410
+ /**
411
+ * BFF origin. Defaults to `''` (same-origin) — the production wiring. An
412
+ * explicit origin is only useful for tests or a non-same-origin BFF.
413
+ */
414
+ baseUrl?: string;
415
+ }
416
+ /**
417
+ * Same-origin client for a per-app BFF.
418
+ *
419
+ * No token storage, no refresh logic, no realm awareness — the BFF owns all of
420
+ * that server-side. The browser's only auth artefact is the httpOnly cookie.
421
+ */
422
+ declare class BffAuthClient {
423
+ private readonly http;
424
+ private readonly baseUrl;
425
+ constructor(options: BffAuthClientOptions);
426
+ /**
427
+ * `POST /bff/login` — the BFF does ROPC against Keycloak server-side, stores
428
+ * the tokens in its Redis vault, and sets the httpOnly session cookie.
429
+ * Returns the sanitised user. Throws on a non-2xx response.
430
+ */
431
+ login(request: BffLoginRequest): Promise<BffUser>;
432
+ /**
433
+ * `POST /bff/logout` — the BFF calls KC end-session, deletes the Redis
434
+ * session, and clears the cookie. Non-fatal: a failed logout still leaves
435
+ * the SPA logged out client-side. Throws only on a non-2xx response.
436
+ */
437
+ logout(): Promise<void>;
438
+ /**
439
+ * `GET /bff/me` — the live session's sanitised user, or `null` when there is
440
+ * no session (the BFF answers `401`). Used at app load to bootstrap auth
441
+ * state in place of the old token-in-storage check.
442
+ */
443
+ getCurrentUser(): Promise<BffUser | null>;
444
+ /**
445
+ * `POST /bff/register` — the BFF proxies registration to TenantService and,
446
+ * on success, establishes a session exactly like `login`. Returns the user.
447
+ */
448
+ register(request: BffRegisterRequest): Promise<BffUser>;
449
+ /**
450
+ * `POST /bff/forgot-password` — proxied to TenantService. The backend
451
+ * returns 200 unconditionally (no email enumeration); anything else throws.
452
+ */
453
+ forgotPassword(request: BffForgotPasswordRequest): Promise<void>;
454
+ /**
455
+ * `POST /bff/reset-password` — proxied to TenantService. Throws on a non-2xx
456
+ * response (e.g. `400` for an invalid / expired token).
457
+ */
458
+ resetPassword(request: BffResetPasswordRequest): Promise<void>;
459
+ /**
460
+ * `POST /bff/otp/request` — the BFF proxies to TenantService, which generates
461
+ * a short-TTL code and emails it.
462
+ *
463
+ * The endpoint is anti-enumeration: a `200` is the normal path whether or not
464
+ * the identifier is registered. This method therefore **returns** the relayed
465
+ * `{ success, expiresIn, code }` body (so the UI can show the expiry) rather
466
+ * than treating a 200 as opaque. It still throws on a non-2xx — a `501`
467
+ * (OTP not enabled) or `502` (upstream down) is a real failure to surface.
468
+ */
469
+ requestOtp(request: BffOtpRequestRequest): Promise<BffOtpRequestResult>;
470
+ /**
471
+ * `POST /bff/otp/verify` — the BFF runs the OTP direct-grant against Keycloak
472
+ * server-side, stores the tokens in its Redis vault, and sets the httpOnly
473
+ * session cookie. Returns the sanitised user, exactly like `login`. Throws on
474
+ * a non-2xx (e.g. `401` for a bad / expired code).
475
+ */
476
+ verifyOtp(request: BffOtpVerifyRequest): Promise<BffUser>;
477
+ /**
478
+ * `POST /bff/pin/login` — the BFF runs the event-scoped PIN direct-grant
479
+ * against Keycloak server-side (the `(event, pin)` pair resolves to the
480
+ * staff member's KC account + event-scoped role), stores the tokens in its
481
+ * Redis vault, and sets the httpOnly session cookie. Returns the sanitised
482
+ * user, exactly like `login` / `verifyOtp`. Throws on a non-2xx — `401` for
483
+ * a bad / expired / locked-out PIN or an unknown event, `501` when PIN login
484
+ * is not an enabled method for this BFF.
485
+ */
486
+ pinLogin(request: BffPinLoginRequest): Promise<BffUser>;
487
+ /**
488
+ * Shared POST for every state-changing `/bff/*` call: same-origin, cookie
489
+ * included, `X-BFF-Csrf` header attached. Throws a labelled error on non-2xx.
490
+ */
491
+ private postState;
492
+ }
493
+
318
494
  /**
319
495
  * Convert a Keycloak `/userinfo` payload into a flat, app-friendly user object.
320
496
  *
@@ -488,4 +664,4 @@ declare function normalizeTokenResponse(raw: RawTokenResponse): TokenResponse;
488
664
  */
489
665
  declare function tokenResponseToAuthTokens(response: TokenResponse, now?: number): AuthTokens;
490
666
 
491
- export { AuthTokens, type AuthorizationCodeBodyInput, type AuthorizationResponseLike, type AuthorizationUrlInput, type BiometricFlagStore, BiometricGate, type BiometricGateLike, type BiometricGateOptions, BrowserStorageTokenStorage, type BrowserStorageTokenStorageOptions, CookieTokenStorage, 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 };
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 };