@thunderid/browser 1.3.1 → 1.4.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/dist/index.d.ts CHANGED
@@ -3,6 +3,8 @@
3
3
  */
4
4
  export { default as ThunderIDBrowserClient } from './ThunderIDBrowserClient';
5
5
  export { default as FetchHttpClient } from './FetchHttpClient';
6
+ export { default as BackendClient, BACKEND_REQUEST_HEADER } from './BackendClient';
7
+ export { default as BackendHttpClient } from './BackendHttpClient';
6
8
  export { default as Hooks, Hooks as BrowserHooks } from './constants/Hooks';
7
9
  export * from './constants/SPAConstants';
8
10
  export { default as BrowserStorage } from './models/BrowserStorage';
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
package/dist/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { Buffer } from 'buffer/index.js';
2
2
  if (typeof window !== 'undefined' && !window.Buffer) { window.Buffer = Buffer; }
3
- import { DEFAULT_THEME, HttpClient, OIDCRequestConstants, ThunderIDAuthException, ThunderIDJavaScriptClient, ThunderIDRuntimeError, TokenConstants, arrayBufferToBase64url, base64urlToArrayBuffer, createPackageComponentLogger, extractEmojiFromUri, extractPkceStorageKeyFromState, generateFlattenedUserProfile, getUsersMe, isEmojiUri, resolveResourceEndpoint } from "@thunderid/javascript";
3
+ import { DEFAULT_THEME, HttpClient, OIDCRequestConstants, ThunderIDAuthException, ThunderIDJavaScriptClient, ThunderIDRuntimeError, TokenConstants, arrayBufferToBase64url, base64urlToArrayBuffer, createPackageComponentLogger, extractEmojiFromUri, extractPkceStorageKeyFromState, generateFlattenedUserProfile, getUsersMe, isEmojiUri, resolveBackendEndpoints, resolveResourceEndpoint } from "@thunderid/javascript";
4
4
  import base64url from "base64url";
5
5
  import sha256 from "fast-sha256";
6
6
  import { createLocalJWKSet, jwtVerify } from "jose";
@@ -33,38 +33,226 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
33
33
  }) : target, mod));
34
34
 
35
35
  //#endregion
36
- //#region src/constants/Hooks.ts
36
+ //#region src/BackendClient.ts
37
37
  /**
38
- * Event hook names for attaching callbacks via `ThunderIDBrowserClient.on()`.
38
+ * Header every backend request carries. A cross-origin page cannot set it without a CORS preflight,
39
+ * which the backend never answers, so it keeps the cookie from being used cross-site.
39
40
  */
40
- let Hooks = /* @__PURE__ */ function(Hooks$1) {
41
- Hooks$1["SignIn"] = "sign-in";
42
- Hooks$1["SignOut"] = "sign-out";
43
- Hooks$1["Initialize"] = "initialize";
44
- Hooks$1["HttpRequestStart"] = "http-request-start";
45
- Hooks$1["HttpRequestFinish"] = "http-request-finish";
46
- Hooks$1["HttpRequestError"] = "http-request-error";
47
- Hooks$1["HttpRequestSuccess"] = "http-request-success";
48
- Hooks$1["RevokeAccessToken"] = "revoke-access-token";
49
- Hooks$1["CustomGrant"] = "custom-grant";
50
- Hooks$1["SignOutFailed"] = "sign-out-failed";
51
- return Hooks$1;
52
- }({});
53
- var Hooks_default = Hooks;
54
-
55
- //#endregion
56
- //#region src/constants/SPAConstants.ts
57
- const ERROR = "error";
58
- const ERROR_DESCRIPTION = "error_description";
59
- const CUSTOM_GRANT_CONFIG = "custom_grant_config";
60
- const STATE_QUERY = "state";
61
- const SILENT_SIGN_IN_STATE = "sign-in-silently";
62
- const SILENT_SIGN_IN_TIMEOUT = 1e4;
63
- const ACCESS_TOKEN_INVALID = "Access token is invalid";
64
- const REFRESH_ACCESS_TOKEN_ERR0R = "refresh-access-token-error";
65
- const TOKEN_REQUEST_CONFIG_KEY = "token_request_config";
66
- const SILENT_SIGN_IN_SIGNED_IN = "silent_sign_in_signed_in";
67
- const SILENT_SIGN_IN_SIGNED_OUT = "silent_sign_in_signed_out";
41
+ const BACKEND_REQUEST_HEADER = "X-Requested-With";
42
+ const BACKEND_REQUEST_HEADER_VALUE = "XMLHttpRequest";
43
+ /** A check on focus is skipped when the last one is more recent than this, in milliseconds. */
44
+ const FOCUS_CHECK_THROTTLE = 5e3;
45
+ /**
46
+ * Talks to the app's backend when sign-in runs there: reads the session, refreshes it, and starts and ends it. The
47
+ * browser never holds a token; the backend keeps them behind an `HttpOnly` cookie.
48
+ */
49
+ var BackendClient = class BackendClient {
50
+ session;
51
+ refreshing;
52
+ hasLoadedSession = false;
53
+ /** Whether the session last seen was signed in, so a later read can tell it has ended. */
54
+ wasSignedIn = false;
55
+ lastCheck = 0;
56
+ signedOutListeners = [];
57
+ constructor(endpoints) {
58
+ this.endpoints = endpoints;
59
+ }
60
+ /**
61
+ * Calls the listener when a signed-in session turns out to have ended without this client signing
62
+ * out: another app signed the user out, an administrator ended the session, or it timed out.
63
+ */
64
+ onSignedOut(listener) {
65
+ this.signedOutListeners.push(listener);
66
+ }
67
+ /**
68
+ * Records that the session has ended, and tells the listeners when it had been signed in.
69
+ */
70
+ markSignedOut() {
71
+ this.session = Promise.resolve({ isSignedIn: false });
72
+ this.hasLoadedSession = true;
73
+ if (this.wasSignedIn) {
74
+ this.wasSignedIn = false;
75
+ this.signedOutListeners.forEach((listener) => listener());
76
+ }
77
+ }
78
+ /**
79
+ * Asks the backend whether the session is still live. The check neither refreshes the session nor
80
+ * counts as activity, so it can be polled without keeping an idle session alive.
81
+ *
82
+ * @returns Whether the session is live. A failed check reports the last known state.
83
+ */
84
+ async checkSession() {
85
+ this.lastCheck = Date.now();
86
+ try {
87
+ const response = await this.send(this.endpoints.checkSession, "GET");
88
+ if (!response.ok) return this.wasSignedIn;
89
+ const { isSignedIn } = await response.json();
90
+ if (!isSignedIn) this.markSignedOut();
91
+ return isSignedIn;
92
+ } catch {
93
+ return this.wasSignedIn;
94
+ }
95
+ }
96
+ /**
97
+ * Watches for the session ending while the page is open: checks it whenever the page gets focus or
98
+ * becomes visible again, and every `pollSeconds` while it is visible when that is positive.
99
+ *
100
+ * @returns A function that stops watching.
101
+ */
102
+ watch(pollSeconds) {
103
+ const checkIfSignedIn = () => {
104
+ if (this.wasSignedIn && document.visibilityState === "visible") this.checkSession();
105
+ };
106
+ const onFocus = () => {
107
+ if (Date.now() - this.lastCheck >= FOCUS_CHECK_THROTTLE) checkIfSignedIn();
108
+ };
109
+ document.addEventListener("visibilitychange", onFocus);
110
+ window.addEventListener("focus", onFocus);
111
+ const timer = pollSeconds > 0 ? setInterval(checkIfSignedIn, pollSeconds * 1e3) : void 0;
112
+ return () => {
113
+ document.removeEventListener("visibilitychange", onFocus);
114
+ window.removeEventListener("focus", onFocus);
115
+ if (timer) clearInterval(timer);
116
+ };
117
+ }
118
+ /**
119
+ * Returns the session. The result is cached until a sign-in, sign-out, or refresh changes it, so
120
+ * repeated sign-in checks do not reach the server each time.
121
+ *
122
+ * @param reload - Read the session from the server even when one is cached.
123
+ */
124
+ getSession(reload = false) {
125
+ if (reload) this.session = void 0;
126
+ this.session ??= this.loadSession().catch((error) => {
127
+ this.session = void 0;
128
+ throw error;
129
+ }).finally(() => {
130
+ this.hasLoadedSession = true;
131
+ });
132
+ return this.session;
133
+ }
134
+ /**
135
+ * Whether this client talks to exactly these endpoints.
136
+ */
137
+ hasEndpoints(endpoints) {
138
+ return Object.keys(endpoints).every((key) => key === "bypass" ? JSON.stringify(this.endpoints.bypass) === JSON.stringify(endpoints.bypass) : this.endpoints[key] === endpoints[key]);
139
+ }
140
+ /**
141
+ * Whether the first session read is still in flight, so it is not yet known whether the user is
142
+ * signed in.
143
+ */
144
+ isLoading() {
145
+ return !this.hasLoadedSession;
146
+ }
147
+ /**
148
+ * Refreshes the session once, sharing one request between concurrent callers.
149
+ *
150
+ * @returns Whether the session was refreshed. When it was not, the session is now signed out.
151
+ */
152
+ refresh() {
153
+ this.refreshing ??= this.send(this.endpoints.refresh, "POST").then((response) => {
154
+ if (response.ok) this.session = void 0;
155
+ else this.markSignedOut();
156
+ return response.ok;
157
+ }).finally(() => {
158
+ this.refreshing = void 0;
159
+ });
160
+ return this.refreshing;
161
+ }
162
+ /**
163
+ * The URL that starts sign-in at the backend and comes back to `returnTo`.
164
+ */
165
+ getSignInUrl(returnTo) {
166
+ return `${this.endpoints.signIn}?${new URLSearchParams({ returnTo }).toString()}`;
167
+ }
168
+ /**
169
+ * Ends the backend session.
170
+ *
171
+ * @returns The URL the browser visits to end the session at the authorization server.
172
+ */
173
+ async signOut() {
174
+ const response = await this.send(this.endpoints.signOut, "POST");
175
+ this.wasSignedIn = false;
176
+ this.session = Promise.resolve({ isSignedIn: false });
177
+ if (!response.ok) throw new ThunderIDAuthException("SPA-BACKEND_CLIENT-SO-SE01", "Sign out failed.", `The backend sign-out endpoint returned ${response.status}.`);
178
+ return (await response.json()).redirectUrl;
179
+ }
180
+ /**
181
+ * Maps an API URL to its backend proxy URL.
182
+ *
183
+ * @returns The proxy URL, or `undefined` when the URL is not under the API base and so cannot be
184
+ * sent with the session's credentials.
185
+ */
186
+ toProxyUrl(url) {
187
+ const { apiBaseUrl, proxy } = this.endpoints;
188
+ let rest;
189
+ if (url === apiBaseUrl || url.startsWith(`${apiBaseUrl}/`) || url.startsWith(`${apiBaseUrl}?`)) rest = url.slice(apiBaseUrl.length);
190
+ else if (url.startsWith("/") && !url.startsWith("//")) rest = url;
191
+ else return;
192
+ return `${proxy}${rest.startsWith("/") ? rest : `/${rest}`}`;
193
+ }
194
+ /**
195
+ * Whether a request to this URL skips the proxy and goes straight from the browser.
196
+ */
197
+ isBypassed(url) {
198
+ return this.endpoints.bypass.urls.some((base) => url === base || url.startsWith(`${base}/`) || url.startsWith(`${base}?`));
199
+ }
200
+ /**
201
+ * Whether requests that skip the proxy carry the access token.
202
+ */
203
+ attachesTokenOnBypass() {
204
+ return this.endpoints.bypass.attachToken;
205
+ }
206
+ /**
207
+ * Gets an access token for the requests that skip the proxy from the backend's token endpoint. It comes without a
208
+ * refresh token: when it expires, the caller asks again.
209
+ *
210
+ * @returns The token response, as the backend sent it.
211
+ */
212
+ async requestAccessToken() {
213
+ let response = await this.send(this.endpoints.token, "POST");
214
+ if (response.status === 401) {
215
+ const { error } = await response.json().catch(() => ({}));
216
+ if (error === "session_expired" && await this.refresh()) response = await this.send(this.endpoints.token, "POST");
217
+ else if (error === "login_required") this.markSignedOut();
218
+ }
219
+ if (!response.ok) throw new ThunderIDAuthException("SPA-BACKEND_CLIENT-RAT-SE01", "Failed to get an access token.", `The backend token endpoint returned ${response.status}.`);
220
+ return await response.json();
221
+ }
222
+ /**
223
+ * The headers and credentials mode every backend request is sent with.
224
+ */
225
+ static requestInit(headers = {}) {
226
+ return {
227
+ credentials: "same-origin",
228
+ headers: {
229
+ ...headers,
230
+ [BACKEND_REQUEST_HEADER]: BACKEND_REQUEST_HEADER_VALUE
231
+ }
232
+ };
233
+ }
234
+ async loadSession() {
235
+ let response = await this.send(this.endpoints.session, "GET");
236
+ if (response.status === 401 && await this.refresh()) response = await this.send(this.endpoints.session, "GET");
237
+ if (response.status === 401) return this.signedOutSession();
238
+ if (!response.ok) throw new ThunderIDAuthException("SPA-BACKEND_CLIENT-GS-SE01", "Failed to read the session.", `The backend session endpoint returned ${response.status}.`);
239
+ const session = await response.json();
240
+ if (!session.isSignedIn) return this.signedOutSession();
241
+ this.wasSignedIn = true;
242
+ return session;
243
+ }
244
+ signedOutSession() {
245
+ if (this.wasSignedIn) queueMicrotask(() => this.markSignedOut());
246
+ return { isSignedIn: false };
247
+ }
248
+ send(url, method) {
249
+ return fetch(url, {
250
+ method,
251
+ ...BackendClient.requestInit({ Accept: "application/json" })
252
+ });
253
+ }
254
+ };
255
+ var BackendClient_default = BackendClient;
68
256
 
69
257
  //#endregion
70
258
  //#region src/FetchHttpClient.ts
@@ -82,6 +270,9 @@ var FetchHttpClient = class FetchHttpClient extends HttpClient {
82
270
  if (!this.instances.has(instanceId)) this.instances.set(instanceId, new FetchHttpClient(isHandlerEnabled, attachToken));
83
271
  return this.instances.get(instanceId);
84
272
  }
273
+ static setInstance(instanceId, client) {
274
+ this.instances.set(instanceId, client);
275
+ }
85
276
  static destroyInstance(instanceId = 0) {
86
277
  this.instances.delete(instanceId);
87
278
  }
@@ -141,6 +332,108 @@ var FetchHttpClient = class FetchHttpClient extends HttpClient {
141
332
  };
142
333
  var FetchHttpClient_default = FetchHttpClient;
143
334
 
335
+ //#endregion
336
+ //#region src/BackendHttpClient.ts
337
+ /**
338
+ * The HTTP client used when sign-in runs on the backend. It sends each request through the backend's
339
+ * API proxy with the session cookie instead of attaching a token, and refreshes the session once
340
+ * when the backend reports
341
+ * it expired. Requests under `bypass` go straight there with the cookies and, unless turned off, the
342
+ * access token the backend issued.
343
+ */
344
+ const hasBackendError = (error, code$1) => error?.response?.status === 401 && error.response.data?.error === code$1;
345
+ const isSessionExpired = (error) => hasBackendError(error, "session_expired");
346
+ var BackendHttpClient = class extends FetchHttpClient_default {
347
+ /**
348
+ * @param backend - The backend client.
349
+ * @param getAccessToken - Returns the access token for bypassed requests; `renew` asks for a new one.
350
+ */
351
+ constructor(backend, getAccessToken) {
352
+ super(true);
353
+ this.backend = backend;
354
+ this.getAccessToken = getAccessToken;
355
+ }
356
+ async transport(config) {
357
+ if (this.backend.isBypassed(config.url ?? "")) return this.sendBypassed(config);
358
+ const proxyUrl = this.backend.toProxyUrl(config.url ?? "");
359
+ if (!proxyUrl) throw Object.assign(/* @__PURE__ */ new Error(`"${config.url}" is not under the backend API base, so it cannot be sent.`), {
360
+ code: "BACKEND_UNSUPPORTED_URL",
361
+ config
362
+ });
363
+ const send = () => super.transport({
364
+ ...config,
365
+ ...BackendClient_default.requestInit(config.headers),
366
+ url: proxyUrl
367
+ });
368
+ try {
369
+ return await send();
370
+ } catch (error) {
371
+ if (isSessionExpired(error) && await this.backend.refresh()) return send();
372
+ if (hasBackendError(error, "login_required")) this.backend.markSignedOut();
373
+ throw error;
374
+ }
375
+ }
376
+ /**
377
+ * Sends a request straight to its URL, skipping the proxy, with the cookies and the access token.
378
+ * When the API rejects the token, it gets a new one and retries once.
379
+ */
380
+ async sendBypassed(config) {
381
+ if (!this.backend.attachesTokenOnBypass()) return super.transport({
382
+ ...config,
383
+ credentials: "include"
384
+ });
385
+ const send = async (renew) => super.transport({
386
+ ...config,
387
+ credentials: "include",
388
+ headers: {
389
+ ...config.headers,
390
+ Authorization: `Bearer ${await this.getAccessToken(renew)}`
391
+ }
392
+ });
393
+ try {
394
+ return await send(false);
395
+ } catch (error) {
396
+ if (error.response?.status === 401) return send(true);
397
+ throw error;
398
+ }
399
+ }
400
+ };
401
+ var BackendHttpClient_default = BackendHttpClient;
402
+
403
+ //#endregion
404
+ //#region src/constants/Hooks.ts
405
+ /**
406
+ * Event hook names for attaching callbacks via `ThunderIDBrowserClient.on()`.
407
+ */
408
+ let Hooks = /* @__PURE__ */ function(Hooks$1) {
409
+ Hooks$1["SignIn"] = "sign-in";
410
+ Hooks$1["SignOut"] = "sign-out";
411
+ Hooks$1["Initialize"] = "initialize";
412
+ Hooks$1["HttpRequestStart"] = "http-request-start";
413
+ Hooks$1["HttpRequestFinish"] = "http-request-finish";
414
+ Hooks$1["HttpRequestError"] = "http-request-error";
415
+ Hooks$1["HttpRequestSuccess"] = "http-request-success";
416
+ Hooks$1["RevokeAccessToken"] = "revoke-access-token";
417
+ Hooks$1["CustomGrant"] = "custom-grant";
418
+ Hooks$1["SignOutFailed"] = "sign-out-failed";
419
+ return Hooks$1;
420
+ }({});
421
+ var Hooks_default = Hooks;
422
+
423
+ //#endregion
424
+ //#region src/constants/SPAConstants.ts
425
+ const ERROR = "error";
426
+ const ERROR_DESCRIPTION = "error_description";
427
+ const CUSTOM_GRANT_CONFIG = "custom_grant_config";
428
+ const STATE_QUERY = "state";
429
+ const SILENT_SIGN_IN_STATE = "sign-in-silently";
430
+ const SILENT_SIGN_IN_TIMEOUT = 1e4;
431
+ const ACCESS_TOKEN_INVALID = "Access token is invalid";
432
+ const REFRESH_ACCESS_TOKEN_ERR0R = "refresh-access-token-error";
433
+ const TOKEN_REQUEST_CONFIG_KEY = "token_request_config";
434
+ const SILENT_SIGN_IN_SIGNED_IN = "silent_sign_in_signed_in";
435
+ const SILENT_SIGN_IN_SIGNED_OUT = "silent_sign_in_signed_out";
436
+
144
437
  //#endregion
145
438
  //#region src/models/BrowserStorage.ts
146
439
  /**
@@ -2513,6 +2806,9 @@ var ThunderIDBrowserClient = class ThunderIDBrowserClient extends ThunderIDJavaS
2513
2806
  _hookCallbacks = /* @__PURE__ */ new Map();
2514
2807
  _initialized = false;
2515
2808
  _startedInitialize = false;
2809
+ _backend;
2810
+ _stopBackendWatch;
2811
+ _backendTokenRequest;
2516
2812
  constructor(instanceId = 0) {
2517
2813
  super(void 0, new SPACryptoUtils_default());
2518
2814
  this._browserInstanceId = instanceId;
@@ -2536,7 +2832,29 @@ var ThunderIDBrowserClient = class ThunderIDBrowserClient extends ThunderIDJavaS
2536
2832
  const attachToken = async (request) => {
2537
2833
  await this._authHelper?.attachTokenToRequestConfig(request);
2538
2834
  };
2539
- this._httpClient = FetchHttpClient_default.getInstance(this._browserInstanceId, true, attachToken);
2835
+ const backendEndpoints = resolveBackendEndpoints(merged, window.location.origin);
2836
+ if (backendEndpoints) {
2837
+ if (!this._backend?.hasEndpoints(backendEndpoints)) {
2838
+ this._backend = new BackendClient_default(backendEndpoints);
2839
+ this._backend.onSignedOut(() => {
2840
+ this.getStorageManager().removeSessionData();
2841
+ this._emitHook(Hooks_default.SignOut);
2842
+ });
2843
+ }
2844
+ this._stopBackendWatch?.();
2845
+ const { session } = merged;
2846
+ this._stopBackendWatch = this._backend.watch(Number(session?.checkInterval ?? 0));
2847
+ this._httpClient = new BackendHttpClient_default(this._backend, (renew) => this._getBackendAccessToken(renew));
2848
+ FetchHttpClient_default.setInstance(this._browserInstanceId, this._httpClient);
2849
+ this._backend.getSession().catch((error) => {
2850
+ logger$2.debug("Could not read the backend session.", error);
2851
+ });
2852
+ } else {
2853
+ this._stopBackendWatch?.();
2854
+ this._stopBackendWatch = void 0;
2855
+ this._backend = void 0;
2856
+ this._httpClient = FetchHttpClient_default.getInstance(this._browserInstanceId, true, attachToken);
2857
+ }
2540
2858
  this._httpClient.setHttpRequestStartCallback((request) => {
2541
2859
  this._emitHook(Hooks_default.HttpRequestStart, request);
2542
2860
  });
@@ -2581,10 +2899,25 @@ var ThunderIDBrowserClient = class ThunderIDBrowserClient extends ThunderIDJavaS
2581
2899
  return true;
2582
2900
  }
2583
2901
  isLoading() {
2584
- return false;
2902
+ return this._backend?.isLoading() ?? false;
2903
+ }
2904
+ /**
2905
+ * Whether sign-in runs on the app's backend, which holds the tokens so the browser never does.
2906
+ */
2907
+ isBackendMode() {
2908
+ return this._backend !== void 0;
2909
+ }
2910
+ _rejectInBackendMode(operation) {
2911
+ return Promise.reject(new ThunderIDAuthException("SPA-AUTH_CLIENT-BACKEND-NS01", "Not supported when sign-in runs on the backend.", `${operation} is not available when sign-in runs on the backend, because the browser holds no tokens.`));
2585
2912
  }
2586
2913
  async signIn(config, authorizationCode, state, tokenRequestConfig) {
2587
2914
  await this.isInitialized();
2915
+ if (this._backend) {
2916
+ if (config?.callOnlyOnRedirect) return;
2917
+ location.href = this._backend.getSignInUrl(`${window.location.pathname}${window.location.search}`);
2918
+ await SPAUtils_default.waitTillPageRedirect();
2919
+ return;
2920
+ }
2588
2921
  if (!SPAUtils_default.canContinueSignIn(Boolean(config?.callOnlyOnRedirect), authorizationCode)) return;
2589
2922
  delete config?.callOnlyOnRedirect;
2590
2923
  const user = await this._signInInternal(config, authorizationCode, state, tokenRequestConfig);
@@ -2655,6 +2988,7 @@ var ThunderIDBrowserClient = class ThunderIDBrowserClient extends ThunderIDJavaS
2655
2988
  }
2656
2989
  async signInSilently(additionalParams, tokenRequestConfig) {
2657
2990
  await this.isInitialized();
2991
+ if (this._backend) return await this.isSignedIn() ? this.getUser() : false;
2658
2992
  if (SPAUtils_default.wasSignInCalled()) return;
2659
2993
  const response = await this._signInSilentlyInternal(additionalParams, tokenRequestConfig);
2660
2994
  if (response) this._emitHook(Hooks_default.SignIn, response);
@@ -2672,6 +3006,14 @@ var ThunderIDBrowserClient = class ThunderIDBrowserClient extends ThunderIDJavaS
2672
3006
  sessionId = sessionIdOrAfterSignOut;
2673
3007
  afterSignOut = afterSignOutParam;
2674
3008
  }
3009
+ if (this._backend) {
3010
+ await this.getStorageManager().removeSessionData();
3011
+ const backendSignOutUrl = await this._backend.signOut();
3012
+ afterSignOut?.(backendSignOutUrl);
3013
+ location.href = backendSignOutUrl;
3014
+ await SPAUtils_default.waitTillPageRedirect();
3015
+ return backendSignOutUrl;
3016
+ }
2675
3017
  const config = await this.getStorageManager().getConfigData();
2676
3018
  let signOutUrl = "";
2677
3019
  if (config?.rpInitiatedLogout !== false) {
@@ -2703,18 +3045,31 @@ var ThunderIDBrowserClient = class ThunderIDBrowserClient extends ThunderIDJavaS
2703
3045
  }
2704
3046
  async httpRequest(requestConfig) {
2705
3047
  await this._validateMethod(false);
3048
+ if (this._backend) return this._backendRequest(requestConfig);
2706
3049
  const response = await this._authHelper.httpRequest(this._httpClient, requestConfig, this._isHttpHandlerEnabled, this._emitHttpError, this._emitHttpFinish);
2707
3050
  if (this._isHttpHandlerEnabled) this._emitHttpFinish();
2708
3051
  return response;
2709
3052
  }
2710
3053
  async httpRequestAll(configs) {
2711
3054
  await this._validateMethod(false);
3055
+ if (this._backend) return Promise.all(configs.map((requestConfig) => this._backendRequest(requestConfig)));
2712
3056
  const responses = await this._authHelper.httpRequestAll(configs, this._httpClient, this._isHttpHandlerEnabled, this._emitHttpError, this._emitHttpFinish);
2713
3057
  if (this._isHttpHandlerEnabled) this._emitHttpFinish();
2714
3058
  return responses;
2715
3059
  }
3060
+ async _backendRequest(requestConfig) {
3061
+ try {
3062
+ return await this._httpClient.request(requestConfig);
3063
+ } catch (error) {
3064
+ if (this._isHttpHandlerEnabled) await this._emitHttpError(error);
3065
+ throw error;
3066
+ } finally {
3067
+ if (this._isHttpHandlerEnabled) this._emitHttpFinish();
3068
+ }
3069
+ }
2716
3070
  async getUser(userId) {
2717
3071
  await this._validateMethod();
3072
+ if (this._backend) return (await this._backend.getSession()).user ?? {};
2718
3073
  return super.getUser(userId);
2719
3074
  }
2720
3075
  /**
@@ -2738,13 +3093,38 @@ var ThunderIDBrowserClient = class ThunderIDBrowserClient extends ThunderIDJavaS
2738
3093
  };
2739
3094
  }
2740
3095
  async getAccessToken(sessionId) {
3096
+ if (this._backend) return this._getBackendAccessToken();
2741
3097
  return super.getAccessToken(sessionId);
2742
3098
  }
3099
+ /**
3100
+ * The access token for requests under `endpoints.backend.bypass`. Like a client credentials token it
3101
+ * has no refresh token: it is kept in the session storage until it nears expiry, then requested again.
3102
+ *
3103
+ * @param renew - Request a new token even when the stored one is still valid.
3104
+ */
3105
+ async _getBackendAccessToken(renew = false) {
3106
+ const storageManager = this.getStorageManager();
3107
+ const session = await storageManager.getSessionData();
3108
+ const expiresAt = (session?.created_at ?? 0) + Number(session?.expires_in ?? 0) * 1e3;
3109
+ if (!renew && session?.access_token && expiresAt - TokenConstants.Lifecycle.CLIENT_CREDENTIALS_REFRESH_MARGIN_MS > Date.now()) return session.access_token;
3110
+ this._backendTokenRequest ??= this._backend.requestAccessToken().then(async (response) => {
3111
+ await storageManager.setSessionData({
3112
+ ...response,
3113
+ created_at: Date.now()
3114
+ });
3115
+ return response.access_token ?? "";
3116
+ }).finally(() => {
3117
+ this._backendTokenRequest = void 0;
3118
+ });
3119
+ return this._backendTokenRequest;
3120
+ }
2743
3121
  async getDecodedIdToken(userId, idToken) {
2744
3122
  await this._validateMethod();
3123
+ if (this._backend) return (await this._backend.getSession()).user ?? {};
2745
3124
  return super.getDecodedIdToken(userId, idToken);
2746
3125
  }
2747
3126
  async getIdToken(userId) {
3127
+ if (this._backend) return this._rejectInBackendMode("getIdToken");
2748
3128
  await this._validateMethod();
2749
3129
  return super.getIdToken(userId);
2750
3130
  }
@@ -2762,6 +3142,7 @@ var ThunderIDBrowserClient = class ThunderIDBrowserClient extends ThunderIDJavaS
2762
3142
  }
2763
3143
  async isSignedIn(userId) {
2764
3144
  await this.isInitialized();
3145
+ if (this._backend) return (await this._backend.getSession()).isSignedIn;
2765
3146
  return super.isSignedIn(userId);
2766
3147
  }
2767
3148
  notifySignIn(user) {
@@ -2769,10 +3150,15 @@ var ThunderIDBrowserClient = class ThunderIDBrowserClient extends ThunderIDJavaS
2769
3150
  }
2770
3151
  async isSessionActive() {
2771
3152
  await this.isInitialized();
3153
+ if (this._backend) return this.isSignedIn();
2772
3154
  return await this.getStorageManager().getSessionStatus() === "true";
2773
3155
  }
2774
3156
  async refreshAccessToken(userId) {
2775
3157
  await this._validateMethod(false);
3158
+ if (this._backend) {
3159
+ if (!await this._backend.refresh()) return Promise.reject(new ThunderIDAuthException("SPA-AUTH_CLIENT-BACKEND-RF01", "Refresh failed.", "The backend could not refresh the session."));
3160
+ return this.getUser();
3161
+ }
2776
3162
  try {
2777
3163
  return await this._authHelper.refreshAccessToken((cfg) => this._enableRetrievingSignOutURLFromSession(cfg));
2778
3164
  } catch (error) {
@@ -2780,6 +3166,7 @@ var ThunderIDBrowserClient = class ThunderIDBrowserClient extends ThunderIDJavaS
2780
3166
  }
2781
3167
  }
2782
3168
  async revokeAccessToken(userId) {
3169
+ if (this._backend) return this._rejectInBackendMode("revokeAccessToken");
2783
3170
  await this._validateMethod();
2784
3171
  const timer = await this._spaHelper.getRefreshTimeoutTimer();
2785
3172
  await super.revokeAccessToken(userId);
@@ -2788,6 +3175,7 @@ var ThunderIDBrowserClient = class ThunderIDBrowserClient extends ThunderIDJavaS
2788
3175
  return true;
2789
3176
  }
2790
3177
  async exchangeToken(config) {
3178
+ if (this._backend) return this._rejectInBackendMode("exchangeToken");
2791
3179
  if (config.signInRequired) await this._validateMethod();
2792
3180
  if (!config.id) return Promise.reject(new ThunderIDAuthException("SPA-AUTH_CLIENT-RCG-NF01", "The custom grant request id not found.", "Set the `id` attribute on the token exchange config."));
2793
3181
  const response = await this._authHelper.exchangeToken(config, (cfg) => this._enableRetrievingSignOutURLFromSession(cfg));
@@ -2800,6 +3188,7 @@ var ThunderIDBrowserClient = class ThunderIDBrowserClient extends ThunderIDJavaS
2800
3188
  }
2801
3189
  async startAutoRefreshToken() {
2802
3190
  await this.isInitialized();
3191
+ if (this._backend) return;
2803
3192
  await this._spaHelper.clearRefreshTokenTimeout();
2804
3193
  await this._spaHelper.refreshAccessTokenAutomatically(() => this.refreshAccessToken());
2805
3194
  }
@@ -2841,6 +3230,7 @@ var ThunderIDBrowserClient = class ThunderIDBrowserClient extends ThunderIDJavaS
2841
3230
  this._addHookCallback(hook, callback);
2842
3231
  break;
2843
3232
  case Hooks_default.SignOut:
3233
+ this._addHookCallback(hook, callback);
2844
3234
  if (await SPAUtils_default.isSignOutSuccessful(ThunderIDBrowserClient.isSignOutSuccessful.bind(ThunderIDBrowserClient), () => ThunderIDBrowserClient.clearSession())) callback();
2845
3235
  break;
2846
3236
  case Hooks_default.CustomGrant:
@@ -3817,5 +4207,5 @@ const normalizeBorderRadius = (raw) => {
3817
4207
  var normalizeBorderRadius_default = normalizeBorderRadius;
3818
4208
 
3819
4209
  //#endregion
3820
- export { ACCESS_TOKEN_INVALID, ANONYMOUS_ANIMAL_ICONS, ANONYMOUS_ENTITY_ICONS, AVATAR_GRADIENT_COUNT, AVATAR_URI_SCHEME, AuthenticationHelper_default as AuthenticationHelper, Hooks as BrowserHooks, BrowserStorage_default as BrowserStorage, CUSTOM_GRANT_CONFIG, ERROR, ERROR_DESCRIPTION, FetchHttpClient_default as FetchHttpClient, Hooks_default as Hooks, LocalStore_default as LocalStore, MemoryStore_default as MemoryStore, REFRESH_ACCESS_TOKEN_ERR0R, SILENT_SIGN_IN_SIGNED_IN, SILENT_SIGN_IN_SIGNED_OUT, SILENT_SIGN_IN_STATE, SILENT_SIGN_IN_TIMEOUT, SPACryptoUtils_default as SPACryptoUtils, SPAHelper_default as SPAHelper, SPAUtils_default as SPAUtils, STATE_QUERY, SessionStore_default as SessionStore, TOKEN_REQUEST_CONFIG_KEY, ThunderIDBrowserClient_default as ThunderIDBrowserClient, buildAvatarSpec_default as buildAvatarSpec, copyTextToClipboard_default as copyTextToClipboard, createClassObserver, createHttpClientFetcher_default as createHttpClientFetcher, createMediaQueryListener, deriveAvatarContent, detectThemeMode, extractAvatarParamsFromUri_default as extractAvatarParamsFromUri, generateAvatarDataUri_default as generateAvatarDataUri, getActiveTheme_default as getActiveTheme, handleWebAuthnAuthentication_default as handleWebAuthnAuthentication, hasAuthParamsInUrl_default as hasAuthParamsInUrl, hasCalledForThisInstanceInUrl_default as hasCalledForThisInstanceInUrl, http_default as http, isAvatarUri_default as isAvatarUri, navigate_default as navigate, normalizeBorderRadius_default as normalizeBorderRadius, pickAnonymousAvatarName_default as pickAnonymousAvatarName, pickAnonymousEntityName_default as pickAnonymousEntityName, resolveEmojiUrisInHtml_default as resolveEmojiUrisInHtml, resolveLogoUri_default as resolveLogoUri };
4210
+ export { ACCESS_TOKEN_INVALID, ANONYMOUS_ANIMAL_ICONS, ANONYMOUS_ENTITY_ICONS, AVATAR_GRADIENT_COUNT, AVATAR_URI_SCHEME, AuthenticationHelper_default as AuthenticationHelper, BACKEND_REQUEST_HEADER, BackendClient_default as BackendClient, BackendHttpClient_default as BackendHttpClient, Hooks as BrowserHooks, BrowserStorage_default as BrowserStorage, CUSTOM_GRANT_CONFIG, ERROR, ERROR_DESCRIPTION, FetchHttpClient_default as FetchHttpClient, Hooks_default as Hooks, LocalStore_default as LocalStore, MemoryStore_default as MemoryStore, REFRESH_ACCESS_TOKEN_ERR0R, SILENT_SIGN_IN_SIGNED_IN, SILENT_SIGN_IN_SIGNED_OUT, SILENT_SIGN_IN_STATE, SILENT_SIGN_IN_TIMEOUT, SPACryptoUtils_default as SPACryptoUtils, SPAHelper_default as SPAHelper, SPAUtils_default as SPAUtils, STATE_QUERY, SessionStore_default as SessionStore, TOKEN_REQUEST_CONFIG_KEY, ThunderIDBrowserClient_default as ThunderIDBrowserClient, buildAvatarSpec_default as buildAvatarSpec, copyTextToClipboard_default as copyTextToClipboard, createClassObserver, createHttpClientFetcher_default as createHttpClientFetcher, createMediaQueryListener, deriveAvatarContent, detectThemeMode, extractAvatarParamsFromUri_default as extractAvatarParamsFromUri, generateAvatarDataUri_default as generateAvatarDataUri, getActiveTheme_default as getActiveTheme, handleWebAuthnAuthentication_default as handleWebAuthnAuthentication, hasAuthParamsInUrl_default as hasAuthParamsInUrl, hasCalledForThisInstanceInUrl_default as hasCalledForThisInstanceInUrl, http_default as http, isAvatarUri_default as isAvatarUri, navigate_default as navigate, normalizeBorderRadius_default as normalizeBorderRadius, pickAnonymousAvatarName_default as pickAnonymousAvatarName, pickAnonymousEntityName_default as pickAnonymousEntityName, resolveEmojiUrisInHtml_default as resolveEmojiUrisInHtml, resolveLogoUri_default as resolveLogoUri };
3821
4211
  //# sourceMappingURL=index.js.map