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