@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/BackendClient.d.ts +109 -0
- package/dist/BackendClient.d.ts.map +1 -0
- package/dist/BackendHttpClient.d.ts +20 -0
- package/dist/BackendHttpClient.d.ts.map +1 -0
- package/dist/FetchHttpClient.d.ts +1 -0
- package/dist/FetchHttpClient.d.ts.map +1 -1
- package/dist/ThunderIDBrowserClient.d.ts +16 -0
- package/dist/ThunderIDBrowserClient.d.ts.map +1 -1
- package/dist/cjs/index.cjs +425 -32
- package/dist/cjs/index.cjs.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +424 -34
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/dist/cjs/index.cjs
CHANGED
|
@@ -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/
|
|
38
|
+
//#region src/BackendClient.ts
|
|
39
39
|
/**
|
|
40
|
-
*
|
|
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
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
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
|
-
|
|
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;
|