@dloizides/auth-client 2.1.0 → 3.0.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,32 @@
1
1
  # Changelog
2
2
 
3
+ ## 3.0.0 (2026-05-19)
4
+
5
+ Major release for Phase 2 of the identity-hardening initiative. Adds the
6
+ shared `BffAuthClient` — the same-origin client for a per-app
7
+ **Backend-For-Frontend** (`bff-katalogos`, `bff-erevna`). The BFF terminates
8
+ authentication server-side: the browser holds only an opaque httpOnly session
9
+ cookie, never a token.
10
+
11
+ This is the **new recommended auth surface**. The major bump signals that
12
+ recommendation — it is **not** a breaking change: every v2.x export
13
+ (`AuthClient`, the direct-KC ROPC adapters, `useDirectKcAuth`, the OIDC
14
+ primitives, storage adapters, hooks) remains and is unchanged. BaseClient
15
+ still consumes the direct-KC path; it is removed in a later phase.
16
+
17
+ ### Added
18
+
19
+ - `BffAuthClient` — same-origin client for a per-app BFF. Methods:
20
+ `login({username,password})` → `POST /bff/login`; `logout()` →
21
+ `POST /bff/logout`; `getCurrentUser()` → `GET /bff/me`; `register(...)`,
22
+ `forgotPassword(...)`, `resetPassword(...)` → the matching `/bff/*`
23
+ endpoints. Every call is a same-origin `fetch` with
24
+ `credentials: 'include'`; state-changing calls carry the `X-BFF-Csrf: 1`
25
+ header the `Bff.AspNetCore` anti-forgery middleware requires. Does **no
26
+ token handling** — the BFF owns tokens, the browser owns only the cookie.
27
+ - Types: `BffAuthClientOptions`, `BffLoginRequest`, `BffRegisterRequest`,
28
+ `BffForgotPasswordRequest`, `BffResetPasswordRequest`, `BffUser`.
29
+
3
30
  ## 2.1.0 (2026-05-17)
4
31
 
5
32
  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.
package/dist/index.d.mts CHANGED
@@ -1,8 +1,8 @@
1
1
  import { T as TokenStorage, c as AuthTokens } from './AuthClient-BGr8L03W.mjs';
2
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';
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,109 @@ 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
+ * The user object returned by `GET /bff/me` and `POST /bff/login`. The BFF
347
+ * returns the sanitised KC claims under a `user` envelope and **never** a
348
+ * token. Kept permissive so server-added claims flow through without a bump.
349
+ */
350
+ interface BffUser {
351
+ sub?: string;
352
+ email?: string;
353
+ email_verified?: boolean;
354
+ name?: string;
355
+ preferred_username?: string;
356
+ given_name?: string;
357
+ family_name?: string;
358
+ tenantId?: string;
359
+ roles?: string[];
360
+ [key: string]: unknown;
361
+ }
362
+ interface BffAuthClientOptions {
363
+ /** Runtime-agnostic HTTP transport (wrap native `fetch` with `createFetchHttpClient`). */
364
+ http: HttpClient;
365
+ /**
366
+ * BFF origin. Defaults to `''` (same-origin) — the production wiring. An
367
+ * explicit origin is only useful for tests or a non-same-origin BFF.
368
+ */
369
+ baseUrl?: string;
370
+ }
371
+ /**
372
+ * Same-origin client for a per-app BFF.
373
+ *
374
+ * No token storage, no refresh logic, no realm awareness — the BFF owns all of
375
+ * that server-side. The browser's only auth artefact is the httpOnly cookie.
376
+ */
377
+ declare class BffAuthClient {
378
+ private readonly http;
379
+ private readonly baseUrl;
380
+ constructor(options: BffAuthClientOptions);
381
+ /**
382
+ * `POST /bff/login` — the BFF does ROPC against Keycloak server-side, stores
383
+ * the tokens in its Redis vault, and sets the httpOnly session cookie.
384
+ * Returns the sanitised user. Throws on a non-2xx response.
385
+ */
386
+ login(request: BffLoginRequest): Promise<BffUser>;
387
+ /**
388
+ * `POST /bff/logout` — the BFF calls KC end-session, deletes the Redis
389
+ * session, and clears the cookie. Non-fatal: a failed logout still leaves
390
+ * the SPA logged out client-side. Throws only on a non-2xx response.
391
+ */
392
+ logout(): Promise<void>;
393
+ /**
394
+ * `GET /bff/me` — the live session's sanitised user, or `null` when there is
395
+ * no session (the BFF answers `401`). Used at app load to bootstrap auth
396
+ * state in place of the old token-in-storage check.
397
+ */
398
+ getCurrentUser(): Promise<BffUser | null>;
399
+ /**
400
+ * `POST /bff/register` — the BFF proxies registration to TenantService and,
401
+ * on success, establishes a session exactly like `login`. Returns the user.
402
+ */
403
+ register(request: BffRegisterRequest): Promise<BffUser>;
404
+ /**
405
+ * `POST /bff/forgot-password` — proxied to TenantService. The backend
406
+ * returns 200 unconditionally (no email enumeration); anything else throws.
407
+ */
408
+ forgotPassword(request: BffForgotPasswordRequest): Promise<void>;
409
+ /**
410
+ * `POST /bff/reset-password` — proxied to TenantService. Throws on a non-2xx
411
+ * response (e.g. `400` for an invalid / expired token).
412
+ */
413
+ resetPassword(request: BffResetPasswordRequest): Promise<void>;
414
+ /**
415
+ * Shared POST for every state-changing `/bff/*` call: same-origin, cookie
416
+ * included, `X-BFF-Csrf` header attached. Throws a labelled error on non-2xx.
417
+ */
418
+ private postState;
419
+ }
420
+
318
421
  /**
319
422
  * Convert a Keycloak `/userinfo` payload into a flat, app-friendly user object.
320
423
  *
@@ -488,4 +591,4 @@ declare function normalizeTokenResponse(raw: RawTokenResponse): TokenResponse;
488
591
  */
489
592
  declare function tokenResponseToAuthTokens(response: TokenResponse, now?: number): AuthTokens;
490
593
 
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 };
594
+ export { AuthTokens, type AuthorizationCodeBodyInput, type AuthorizationResponseLike, type AuthorizationUrlInput, BffAuthClient, type BffAuthClientOptions, type BffForgotPasswordRequest, type BffLoginRequest, 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
1
  import { T as TokenStorage, c as AuthTokens } from './AuthClient-D95OMajD.js';
2
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';
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,109 @@ 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
+ * The user object returned by `GET /bff/me` and `POST /bff/login`. The BFF
347
+ * returns the sanitised KC claims under a `user` envelope and **never** a
348
+ * token. Kept permissive so server-added claims flow through without a bump.
349
+ */
350
+ interface BffUser {
351
+ sub?: string;
352
+ email?: string;
353
+ email_verified?: boolean;
354
+ name?: string;
355
+ preferred_username?: string;
356
+ given_name?: string;
357
+ family_name?: string;
358
+ tenantId?: string;
359
+ roles?: string[];
360
+ [key: string]: unknown;
361
+ }
362
+ interface BffAuthClientOptions {
363
+ /** Runtime-agnostic HTTP transport (wrap native `fetch` with `createFetchHttpClient`). */
364
+ http: HttpClient;
365
+ /**
366
+ * BFF origin. Defaults to `''` (same-origin) — the production wiring. An
367
+ * explicit origin is only useful for tests or a non-same-origin BFF.
368
+ */
369
+ baseUrl?: string;
370
+ }
371
+ /**
372
+ * Same-origin client for a per-app BFF.
373
+ *
374
+ * No token storage, no refresh logic, no realm awareness — the BFF owns all of
375
+ * that server-side. The browser's only auth artefact is the httpOnly cookie.
376
+ */
377
+ declare class BffAuthClient {
378
+ private readonly http;
379
+ private readonly baseUrl;
380
+ constructor(options: BffAuthClientOptions);
381
+ /**
382
+ * `POST /bff/login` — the BFF does ROPC against Keycloak server-side, stores
383
+ * the tokens in its Redis vault, and sets the httpOnly session cookie.
384
+ * Returns the sanitised user. Throws on a non-2xx response.
385
+ */
386
+ login(request: BffLoginRequest): Promise<BffUser>;
387
+ /**
388
+ * `POST /bff/logout` — the BFF calls KC end-session, deletes the Redis
389
+ * session, and clears the cookie. Non-fatal: a failed logout still leaves
390
+ * the SPA logged out client-side. Throws only on a non-2xx response.
391
+ */
392
+ logout(): Promise<void>;
393
+ /**
394
+ * `GET /bff/me` — the live session's sanitised user, or `null` when there is
395
+ * no session (the BFF answers `401`). Used at app load to bootstrap auth
396
+ * state in place of the old token-in-storage check.
397
+ */
398
+ getCurrentUser(): Promise<BffUser | null>;
399
+ /**
400
+ * `POST /bff/register` — the BFF proxies registration to TenantService and,
401
+ * on success, establishes a session exactly like `login`. Returns the user.
402
+ */
403
+ register(request: BffRegisterRequest): Promise<BffUser>;
404
+ /**
405
+ * `POST /bff/forgot-password` — proxied to TenantService. The backend
406
+ * returns 200 unconditionally (no email enumeration); anything else throws.
407
+ */
408
+ forgotPassword(request: BffForgotPasswordRequest): Promise<void>;
409
+ /**
410
+ * `POST /bff/reset-password` — proxied to TenantService. Throws on a non-2xx
411
+ * response (e.g. `400` for an invalid / expired token).
412
+ */
413
+ resetPassword(request: BffResetPasswordRequest): Promise<void>;
414
+ /**
415
+ * Shared POST for every state-changing `/bff/*` call: same-origin, cookie
416
+ * included, `X-BFF-Csrf` header attached. Throws a labelled error on non-2xx.
417
+ */
418
+ private postState;
419
+ }
420
+
318
421
  /**
319
422
  * Convert a Keycloak `/userinfo` payload into a flat, app-friendly user object.
320
423
  *
@@ -488,4 +591,4 @@ declare function normalizeTokenResponse(raw: RawTokenResponse): TokenResponse;
488
591
  */
489
592
  declare function tokenResponseToAuthTokens(response: TokenResponse, now?: number): AuthTokens;
490
593
 
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 };
594
+ export { AuthTokens, type AuthorizationCodeBodyInput, type AuthorizationResponseLike, type AuthorizationUrlInput, BffAuthClient, type BffAuthClientOptions, type BffForgotPasswordRequest, type BffLoginRequest, 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.js CHANGED
@@ -1059,6 +1059,121 @@ var AuthApiClient = class {
1059
1059
  }
1060
1060
  };
1061
1061
 
1062
+ // src/bff/BffAuthClient.ts
1063
+ var CSRF_HEADER = "X-BFF-Csrf";
1064
+ var CSRF_HEADER_VALUE = "1";
1065
+ var JSON_CONTENT_TYPE = "application/json";
1066
+ var ENDPOINTS = {
1067
+ login: "/bff/login",
1068
+ logout: "/bff/logout",
1069
+ me: "/bff/me",
1070
+ register: "/bff/register",
1071
+ forgotPassword: "/bff/forgot-password",
1072
+ resetPassword: "/bff/reset-password"
1073
+ };
1074
+ function isRecord(value) {
1075
+ return typeof value === "object" && value !== null;
1076
+ }
1077
+ function extractUser(data) {
1078
+ if (!isRecord(data)) {
1079
+ return null;
1080
+ }
1081
+ const envelope = data;
1082
+ return isRecord(envelope.user) ? envelope.user : null;
1083
+ }
1084
+ var BffAuthClient = class {
1085
+ constructor(options) {
1086
+ this.http = options.http;
1087
+ this.baseUrl = (options.baseUrl ?? "").replace(/\/$/, "");
1088
+ }
1089
+ /**
1090
+ * `POST /bff/login` — the BFF does ROPC against Keycloak server-side, stores
1091
+ * the tokens in its Redis vault, and sets the httpOnly session cookie.
1092
+ * Returns the sanitised user. Throws on a non-2xx response.
1093
+ */
1094
+ async login(request) {
1095
+ const data = await this.postState(ENDPOINTS.login, request, "login");
1096
+ const user = extractUser(data);
1097
+ if (user === null) {
1098
+ throw new Error("login: BFF response missing user");
1099
+ }
1100
+ return user;
1101
+ }
1102
+ /**
1103
+ * `POST /bff/logout` — the BFF calls KC end-session, deletes the Redis
1104
+ * session, and clears the cookie. Non-fatal: a failed logout still leaves
1105
+ * the SPA logged out client-side. Throws only on a non-2xx response.
1106
+ */
1107
+ async logout() {
1108
+ await this.postState(ENDPOINTS.logout, void 0, "logout");
1109
+ }
1110
+ /**
1111
+ * `GET /bff/me` — the live session's sanitised user, or `null` when there is
1112
+ * no session (the BFF answers `401`). Used at app load to bootstrap auth
1113
+ * state in place of the old token-in-storage check.
1114
+ */
1115
+ async getCurrentUser() {
1116
+ const response = await this.http({
1117
+ url: `${this.baseUrl}${ENDPOINTS.me}`,
1118
+ method: "GET",
1119
+ headers: { Accept: JSON_CONTENT_TYPE },
1120
+ credentials: "include"
1121
+ });
1122
+ if (!response.ok) {
1123
+ return null;
1124
+ }
1125
+ return extractUser(response.data);
1126
+ }
1127
+ /**
1128
+ * `POST /bff/register` — the BFF proxies registration to TenantService and,
1129
+ * on success, establishes a session exactly like `login`. Returns the user.
1130
+ */
1131
+ async register(request) {
1132
+ const data = await this.postState(ENDPOINTS.register, request, "register");
1133
+ const user = extractUser(data);
1134
+ if (user === null) {
1135
+ throw new Error("register: BFF response missing user");
1136
+ }
1137
+ return user;
1138
+ }
1139
+ /**
1140
+ * `POST /bff/forgot-password` — proxied to TenantService. The backend
1141
+ * returns 200 unconditionally (no email enumeration); anything else throws.
1142
+ */
1143
+ async forgotPassword(request) {
1144
+ await this.postState(ENDPOINTS.forgotPassword, request, "forgot-password");
1145
+ }
1146
+ /**
1147
+ * `POST /bff/reset-password` — proxied to TenantService. Throws on a non-2xx
1148
+ * response (e.g. `400` for an invalid / expired token).
1149
+ */
1150
+ async resetPassword(request) {
1151
+ await this.postState(ENDPOINTS.resetPassword, request, "reset-password");
1152
+ }
1153
+ /**
1154
+ * Shared POST for every state-changing `/bff/*` call: same-origin, cookie
1155
+ * included, `X-BFF-Csrf` header attached. Throws a labelled error on non-2xx.
1156
+ */
1157
+ async postState(path, body, label) {
1158
+ const headers = {
1159
+ "Content-Type": JSON_CONTENT_TYPE,
1160
+ Accept: JSON_CONTENT_TYPE,
1161
+ [CSRF_HEADER]: CSRF_HEADER_VALUE
1162
+ };
1163
+ const response = await this.http({
1164
+ url: `${this.baseUrl}${path}`,
1165
+ method: "POST",
1166
+ headers,
1167
+ body: body === void 0 ? void 0 : JSON.stringify(body),
1168
+ credentials: "include"
1169
+ });
1170
+ if (!response.ok) {
1171
+ throw new Error(`${label} failed with status ${String(response.status)}`);
1172
+ }
1173
+ return response.data;
1174
+ }
1175
+ };
1176
+
1062
1177
  // src/utils/normalizeKeycloakUser.ts
1063
1178
  function isNonEmptyString(value) {
1064
1179
  return typeof value === "string" && value !== "";
@@ -1172,6 +1287,7 @@ function decodeUtf8(binary) {
1172
1287
  exports.AuthApiClient = AuthApiClient;
1173
1288
  exports.AuthClient = AuthClient;
1174
1289
  exports.AuthEventEmitter = AuthEventEmitter;
1290
+ exports.BffAuthClient = BffAuthClient;
1175
1291
  exports.BiometricGate = BiometricGate;
1176
1292
  exports.BrowserStorageTokenStorage = BrowserStorageTokenStorage;
1177
1293
  exports.CookieTokenStorage = CookieTokenStorage;