@pixelume/sdk 0.4.0 → 0.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.
- package/CHANGELOG.md +31 -0
- package/README.md +59 -16
- package/dist/core/PixelumeClient.js +1 -0
- package/dist/core/PixelumeClient.js.map +1 -1
- package/dist/core/config.d.ts +4 -0
- package/dist/core/config.js +18 -0
- package/dist/core/config.js.map +1 -1
- package/dist/core/errors.d.ts +5 -0
- package/dist/core/errors.js.map +1 -1
- package/dist/core/types.d.ts +29 -0
- package/dist/core/types.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js.map +1 -1
- package/dist/modules/auth/AuthModule.d.ts +20 -1
- package/dist/modules/auth/AuthModule.js +305 -21
- package/dist/modules/auth/AuthModule.js.map +1 -1
- package/dist/ui/widget.js +15 -5
- package/dist/ui/widget.js.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/version.js.map +1 -1
- package/package.json +1 -1
|
@@ -3,6 +3,12 @@ import { PixelumeAuthError, PixelumeConfigError, isPixelumeError, } from '../../
|
|
|
3
3
|
import { SessionManager, browserResumeSource, } from './SessionManager.js';
|
|
4
4
|
const AUTHORIZATION_REQUEST_TTL_MS = 10 * 60_000;
|
|
5
5
|
const CALLBACK_PARAMETERS = ['code', 'state', 'error', 'error_description'];
|
|
6
|
+
/** `prompt=none` answers meaning "the central session cannot authorize without a screen". */
|
|
7
|
+
const SESSION_ENDED_ERRORS = new Set([
|
|
8
|
+
'login_required',
|
|
9
|
+
'consent_required',
|
|
10
|
+
'interaction_required',
|
|
11
|
+
]);
|
|
6
12
|
export class AuthModule {
|
|
7
13
|
#http;
|
|
8
14
|
#config;
|
|
@@ -13,16 +19,22 @@ export class AuthModule {
|
|
|
13
19
|
#loginPromise = null;
|
|
14
20
|
#playerPromise = null;
|
|
15
21
|
#logoutPromise = null;
|
|
22
|
+
/** True while a callback of a silent attempt is being completed. */
|
|
23
|
+
#completingRestore = false;
|
|
24
|
+
#resume;
|
|
25
|
+
#stopRestoreWatch = null;
|
|
16
26
|
constructor(http, config) {
|
|
17
27
|
this.#http = http;
|
|
18
28
|
this.#config = config;
|
|
19
29
|
const browser = config.browser;
|
|
30
|
+
this.#resume =
|
|
31
|
+
browser !== undefined && typeof browser.subscribeResume === 'function'
|
|
32
|
+
? (onResume) => browser.subscribeResume?.(onResume) ?? (() => undefined)
|
|
33
|
+
: browserResumeSource();
|
|
20
34
|
this.#session = new SessionManager({
|
|
21
35
|
http,
|
|
22
36
|
clientId: () => this.#requireClientId(),
|
|
23
|
-
resume:
|
|
24
|
-
? (onResume) => browser.subscribeResume?.(onResume) ?? (() => undefined)
|
|
25
|
-
: browserResumeSource(),
|
|
37
|
+
resume: this.#resume,
|
|
26
38
|
onEnded: (error) => {
|
|
27
39
|
this.#setState(anonymous('expired', error));
|
|
28
40
|
},
|
|
@@ -46,6 +58,8 @@ export class AuthModule {
|
|
|
46
58
|
}
|
|
47
59
|
/** @internal Releases timers/listeners and drops credentials from memory (no revocation). */
|
|
48
60
|
dispose() {
|
|
61
|
+
this.#stopRestoreWatch?.();
|
|
62
|
+
this.#stopRestoreWatch = null;
|
|
49
63
|
this.#session.dispose();
|
|
50
64
|
this.#listeners.clear();
|
|
51
65
|
}
|
|
@@ -62,7 +76,7 @@ export class AuthModule {
|
|
|
62
76
|
if (scope !== 'profile' && scope !== 'profile saves') {
|
|
63
77
|
return Promise.reject(new PixelumeConfigError('Pixelume Auth scope must be "profile" or "profile saves"'));
|
|
64
78
|
}
|
|
65
|
-
this.#loginPromise ??= this.#doLogin(scope).finally(() => {
|
|
79
|
+
this.#loginPromise ??= this.#doLogin(scope, false).finally(() => {
|
|
66
80
|
this.#loginPromise = null;
|
|
67
81
|
});
|
|
68
82
|
return this.#loginPromise;
|
|
@@ -88,40 +102,92 @@ export class AuthModule {
|
|
|
88
102
|
if (callback !== null) {
|
|
89
103
|
await this.#handleCallback(browser, callback);
|
|
90
104
|
}
|
|
91
|
-
else
|
|
92
|
-
const
|
|
93
|
-
if (
|
|
94
|
-
this.#
|
|
105
|
+
else {
|
|
106
|
+
const persistent = this.#persistentStorage(browser);
|
|
107
|
+
if (this.#config.crossSessionRestore !== null && persistent !== null) {
|
|
108
|
+
await this.#restoreAcrossSessions(browser, persistent, this.#config.crossSessionRestore);
|
|
95
109
|
}
|
|
96
110
|
else {
|
|
97
|
-
|
|
98
|
-
|
|
111
|
+
// The feature was turned off: do not leave a stale hint behind.
|
|
112
|
+
if (persistent !== null)
|
|
113
|
+
this.#removePersistentHint(persistent);
|
|
114
|
+
await this.#restoreSameTab(browser);
|
|
99
115
|
}
|
|
100
116
|
}
|
|
101
|
-
else {
|
|
102
|
-
this.#setState(anonymous('visitor'));
|
|
103
|
-
}
|
|
104
117
|
}
|
|
105
118
|
catch (error) {
|
|
106
119
|
const failure = isPixelumeError(error)
|
|
107
120
|
? error
|
|
108
121
|
: new PixelumeAuthError('browser_unavailable', { cause: error });
|
|
109
|
-
this.#
|
|
122
|
+
const offline = this.#completingRestore && (failure.kind === 'network' || failure.kind === 'timeout');
|
|
123
|
+
this.#setState(anonymous(offline ? 'offline' : 'error', failure));
|
|
124
|
+
}
|
|
125
|
+
finally {
|
|
126
|
+
this.#completingRestore = false;
|
|
110
127
|
}
|
|
111
128
|
return this.#state;
|
|
112
129
|
}
|
|
113
|
-
|
|
130
|
+
/** Behaviour without `restoreAcrossSessions` (unchanged since 0.2): same-tab reload only. */
|
|
131
|
+
async #restoreSameTab(browser) {
|
|
132
|
+
const scope = this.#config.restoreSession
|
|
133
|
+
? this.#readSessionHint(browser.sessionStorage)
|
|
134
|
+
: null;
|
|
135
|
+
if (scope === null) {
|
|
136
|
+
this.#setState(anonymous('visitor'));
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
this.#removeSessionHint(browser.sessionStorage);
|
|
140
|
+
await this.login({ scope });
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* At most one silent (`prompt=none`) attempt per start, driven by the tab hint (reload) or the
|
|
144
|
+
* persistent hint (new tab, reopened browser). A per-tab marker stops a second attempt while a
|
|
145
|
+
* previous one did not come back (navigation error, back button), so it can never loop.
|
|
146
|
+
*/
|
|
147
|
+
async #restoreAcrossSessions(browser, persistent, options) {
|
|
148
|
+
const tabScope = this.#config.restoreSession
|
|
149
|
+
? this.#readSessionHint(browser.sessionStorage)
|
|
150
|
+
: null;
|
|
151
|
+
const hint = this.#readPersistentHint(persistent, options.maxAgeMs);
|
|
152
|
+
const remembered = tabScope ?? hint?.scope ?? null;
|
|
153
|
+
if (remembered === null) {
|
|
154
|
+
this.#setState(anonymous('visitor'));
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
if (this.#recentRestoreAttempt(browser.sessionStorage)) {
|
|
158
|
+
this.#setState(anonymous('visitor'));
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
if (!isOnline(browser)) {
|
|
162
|
+
this.#setState(anonymous('offline'));
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
// Never ask silently for more than both the game and the previous grant allow.
|
|
166
|
+
const scope = remembered === 'profile saves' && this.#config.scope === 'profile saves'
|
|
167
|
+
? 'profile saves'
|
|
168
|
+
: 'profile';
|
|
169
|
+
this.#removeSessionHint(browser.sessionStorage);
|
|
170
|
+
await this.#doLogin(scope, true);
|
|
171
|
+
}
|
|
172
|
+
async #doLogin(scope, silent) {
|
|
114
173
|
const clientId = this.#requireClientId();
|
|
115
174
|
const redirectUri = this.#requireRedirectUri();
|
|
116
175
|
const browser = this.#browser();
|
|
117
176
|
let pending = this.#readPending(browser.sessionStorage);
|
|
118
|
-
if (
|
|
177
|
+
if (silent ||
|
|
178
|
+
pending === null ||
|
|
179
|
+
pending.redirectUri !== redirectUri ||
|
|
180
|
+
pending.silent === true ||
|
|
181
|
+
this.#isExpired(pending)) {
|
|
182
|
+
const returnTo = silent ? sameOriginPath(browser.currentUrl(), redirectUri) : null;
|
|
119
183
|
pending = {
|
|
120
184
|
version: 1,
|
|
121
185
|
state: randomBase64Url(browser.crypto),
|
|
122
186
|
verifier: randomBase64Url(browser.crypto),
|
|
123
187
|
redirectUri,
|
|
124
188
|
createdAt: Date.now(),
|
|
189
|
+
...(silent ? { silent: true, scope } : {}),
|
|
190
|
+
...(returnTo === null ? {} : { returnTo }),
|
|
125
191
|
};
|
|
126
192
|
this.#writePending(browser.sessionStorage, pending);
|
|
127
193
|
}
|
|
@@ -135,8 +201,13 @@ export class AuthModule {
|
|
|
135
201
|
code_challenge: challenge,
|
|
136
202
|
code_challenge_method: 'S256',
|
|
137
203
|
scope,
|
|
204
|
+
...(silent ? { prompt: 'none' } : {}),
|
|
138
205
|
}).toString();
|
|
139
|
-
|
|
206
|
+
if (silent)
|
|
207
|
+
this.#writeRestoreAttempt(browser.sessionStorage);
|
|
208
|
+
this.#setState(Object.freeze(silent
|
|
209
|
+
? { status: 'redirecting', player: null, restoring: true }
|
|
210
|
+
: { status: 'redirecting', player: null }));
|
|
140
211
|
try {
|
|
141
212
|
browser.navigate(authorization.toString());
|
|
142
213
|
}
|
|
@@ -144,11 +215,36 @@ export class AuthModule {
|
|
|
144
215
|
this.#removePending(browser.sessionStorage);
|
|
145
216
|
throw new PixelumeAuthError('browser_unavailable', { cause });
|
|
146
217
|
}
|
|
218
|
+
if (silent)
|
|
219
|
+
this.#watchInterruptedRestore();
|
|
220
|
+
}
|
|
221
|
+
/**
|
|
222
|
+
* If the page comes back without leaving (back/forward cache, navigation error then "back"),
|
|
223
|
+
* the silent attempt is over: become a visitor instead of staying in `redirecting`. The per-tab
|
|
224
|
+
* marker stays, so the next start does not try again right away.
|
|
225
|
+
*/
|
|
226
|
+
#watchInterruptedRestore() {
|
|
227
|
+
this.#stopRestoreWatch?.();
|
|
228
|
+
if (this.#resume === null)
|
|
229
|
+
return;
|
|
230
|
+
const stop = this.#resume(() => {
|
|
231
|
+
const current = this.#state;
|
|
232
|
+
if (current.status === 'redirecting' && current.restoring === true) {
|
|
233
|
+
this.#setState(anonymous('visitor'));
|
|
234
|
+
}
|
|
235
|
+
stop();
|
|
236
|
+
if (this.#stopRestoreWatch === stop)
|
|
237
|
+
this.#stopRestoreWatch = null;
|
|
238
|
+
});
|
|
239
|
+
this.#stopRestoreWatch = stop;
|
|
147
240
|
}
|
|
148
241
|
async #handleCallback(browser, callback) {
|
|
149
242
|
const pending = this.#readPending(browser.sessionStorage);
|
|
150
243
|
this.#removePending(browser.sessionStorage);
|
|
151
|
-
this.#
|
|
244
|
+
this.#removeRestoreAttempt(browser.sessionStorage);
|
|
245
|
+
const silent = pending?.silent === true && !this.#isExpired(pending);
|
|
246
|
+
this.#cleanCallback(browser, callback.url, silent ? (pending.returnTo ?? null) : null);
|
|
247
|
+
this.#completingRestore = silent;
|
|
152
248
|
if (callback.invalid)
|
|
153
249
|
throw new PixelumeAuthError('invalid_callback');
|
|
154
250
|
if (pending === null || this.#isExpired(pending)) {
|
|
@@ -166,6 +262,27 @@ export class AuthModule {
|
|
|
166
262
|
}
|
|
167
263
|
if (callback.error !== null) {
|
|
168
264
|
this.#removeSessionHint(browser.sessionStorage);
|
|
265
|
+
const persistent = this.#persistentStorage(browser);
|
|
266
|
+
if (silent) {
|
|
267
|
+
this.#completingRestore = false;
|
|
268
|
+
if (SESSION_ENDED_ERRORS.has(callback.error)) {
|
|
269
|
+
if (persistent !== null)
|
|
270
|
+
this.#removePersistentHint(persistent);
|
|
271
|
+
this.#setState(anonymous('session_ended'));
|
|
272
|
+
return;
|
|
273
|
+
}
|
|
274
|
+
if (callback.error === 'invalid_scope' && pending.scope === 'profile saves') {
|
|
275
|
+
// The game no longer allows `saves`: the next start tries `profile` only.
|
|
276
|
+
if (persistent !== null)
|
|
277
|
+
this.#downgradePersistentHint(persistent);
|
|
278
|
+
}
|
|
279
|
+
else if (persistent !== null) {
|
|
280
|
+
this.#removePersistentHint(persistent);
|
|
281
|
+
}
|
|
282
|
+
throw new PixelumeAuthError('restore_rejected');
|
|
283
|
+
}
|
|
284
|
+
if (persistent !== null)
|
|
285
|
+
this.#removePersistentHint(persistent);
|
|
169
286
|
if (callback.error === 'access_denied') {
|
|
170
287
|
this.#setState(anonymous('cancelled'));
|
|
171
288
|
return;
|
|
@@ -174,7 +291,9 @@ export class AuthModule {
|
|
|
174
291
|
}
|
|
175
292
|
if (callback.code === null)
|
|
176
293
|
throw new PixelumeAuthError('invalid_callback');
|
|
177
|
-
this.#setState(Object.freeze(
|
|
294
|
+
this.#setState(Object.freeze(silent
|
|
295
|
+
? { status: 'exchanging', player: null, restoring: true }
|
|
296
|
+
: { status: 'exchanging', player: null }));
|
|
178
297
|
const response = await this.#http.request({
|
|
179
298
|
method: 'POST',
|
|
180
299
|
path: '/v1/oauth/token',
|
|
@@ -193,6 +312,10 @@ export class AuthModule {
|
|
|
193
312
|
if (player !== null) {
|
|
194
313
|
const scope = response.data.scopes.includes('saves') ? 'profile saves' : 'profile';
|
|
195
314
|
this.#writeSessionHint(browser.sessionStorage, scope);
|
|
315
|
+
const persistent = this.#persistentStorage(browser);
|
|
316
|
+
if (this.#config.crossSessionRestore !== null && persistent !== null) {
|
|
317
|
+
this.#writePersistentHint(persistent, scope);
|
|
318
|
+
}
|
|
196
319
|
}
|
|
197
320
|
}
|
|
198
321
|
async #fetchPlayer(options) {
|
|
@@ -226,6 +349,9 @@ export class AuthModule {
|
|
|
226
349
|
if (browser) {
|
|
227
350
|
this.#removePending(browser.sessionStorage);
|
|
228
351
|
this.#removeSessionHint(browser.sessionStorage);
|
|
352
|
+
const persistent = this.#persistentStorage(browser);
|
|
353
|
+
if (persistent !== null)
|
|
354
|
+
this.#removePersistentHint(persistent);
|
|
229
355
|
}
|
|
230
356
|
this.#setState(anonymous('logged_out'));
|
|
231
357
|
if (token === null)
|
|
@@ -265,7 +391,12 @@ export class AuthModule {
|
|
|
265
391
|
invalid: !sameCallbackBase(current, expected) || (code === null) === (error === null),
|
|
266
392
|
};
|
|
267
393
|
}
|
|
268
|
-
|
|
394
|
+
/** Removes the callback fields; a silent attempt returns to the page that started it. */
|
|
395
|
+
#cleanCallback(browser, url, returnTo) {
|
|
396
|
+
if (returnTo !== null && isSafeReturnPath(returnTo)) {
|
|
397
|
+
browser.replaceUrl(returnTo);
|
|
398
|
+
return;
|
|
399
|
+
}
|
|
269
400
|
const clean = new URL(url.toString());
|
|
270
401
|
for (const key of CALLBACK_PARAMETERS)
|
|
271
402
|
clean.searchParams.delete(key);
|
|
@@ -300,6 +431,15 @@ export class AuthModule {
|
|
|
300
431
|
globalThis.history.replaceState(globalThis.history.state, '', url);
|
|
301
432
|
},
|
|
302
433
|
sessionStorage: globalThis.sessionStorage,
|
|
434
|
+
get persistentStorage() {
|
|
435
|
+
try {
|
|
436
|
+
return typeof globalThis.localStorage === 'undefined' ? null : globalThis.localStorage;
|
|
437
|
+
}
|
|
438
|
+
catch {
|
|
439
|
+
// Blocked storage (privacy settings, sandboxed frames).
|
|
440
|
+
return null;
|
|
441
|
+
}
|
|
442
|
+
},
|
|
303
443
|
crypto: globalThis.crypto,
|
|
304
444
|
};
|
|
305
445
|
}
|
|
@@ -325,6 +465,99 @@ export class AuthModule {
|
|
|
325
465
|
#hintKey() {
|
|
326
466
|
return `pixelume.oauth.session.${this.#config.clientId ?? 'unconfigured'}`;
|
|
327
467
|
}
|
|
468
|
+
#persistentHintKey() {
|
|
469
|
+
return `pixelume.oauth.restore.${this.#config.clientId ?? 'unconfigured'}`;
|
|
470
|
+
}
|
|
471
|
+
#restoreAttemptKey() {
|
|
472
|
+
return `pixelume.oauth.restore-attempt.${this.#config.clientId ?? 'unconfigured'}`;
|
|
473
|
+
}
|
|
474
|
+
#persistentStorage(browser) {
|
|
475
|
+
try {
|
|
476
|
+
return browser.persistentStorage ?? null;
|
|
477
|
+
}
|
|
478
|
+
catch {
|
|
479
|
+
return null;
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
/**
|
|
483
|
+
* Persistent-hint helpers never throw: an unavailable `localStorage` only disables
|
|
484
|
+
* cross-session restoration, it must not break the login itself.
|
|
485
|
+
*/
|
|
486
|
+
#readPersistentHint(storage, maxAgeMs) {
|
|
487
|
+
try {
|
|
488
|
+
const raw = storage.getItem(this.#persistentHintKey());
|
|
489
|
+
if (raw === null)
|
|
490
|
+
return null;
|
|
491
|
+
const value = JSON.parse(raw);
|
|
492
|
+
const now = Date.now();
|
|
493
|
+
if (!isPersistentHint(value) || value.at > now + 60_000 || value.at + maxAgeMs < now) {
|
|
494
|
+
storage.removeItem(this.#persistentHintKey());
|
|
495
|
+
return null;
|
|
496
|
+
}
|
|
497
|
+
return value;
|
|
498
|
+
}
|
|
499
|
+
catch {
|
|
500
|
+
this.#removePersistentHint(storage);
|
|
501
|
+
return null;
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
#writePersistentHint(storage, scope) {
|
|
505
|
+
const hint = { version: 1, scope, at: Date.now() };
|
|
506
|
+
try {
|
|
507
|
+
storage.setItem(this.#persistentHintKey(), JSON.stringify(hint));
|
|
508
|
+
}
|
|
509
|
+
catch {
|
|
510
|
+
// Quota or blocked storage: the next new tab simply starts as a visitor.
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
#downgradePersistentHint(storage) {
|
|
514
|
+
try {
|
|
515
|
+
const raw = storage.getItem(this.#persistentHintKey());
|
|
516
|
+
const value = raw === null ? null : JSON.parse(raw);
|
|
517
|
+
if (!isPersistentHint(value))
|
|
518
|
+
return;
|
|
519
|
+
storage.setItem(this.#persistentHintKey(), JSON.stringify({ ...value, scope: 'profile' }));
|
|
520
|
+
}
|
|
521
|
+
catch {
|
|
522
|
+
this.#removePersistentHint(storage);
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
#removePersistentHint(storage) {
|
|
526
|
+
try {
|
|
527
|
+
storage.removeItem(this.#persistentHintKey());
|
|
528
|
+
}
|
|
529
|
+
catch {
|
|
530
|
+
// Nothing else to do.
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
#recentRestoreAttempt(storage) {
|
|
534
|
+
const raw = storage.getItem(this.#restoreAttemptKey());
|
|
535
|
+
if (raw === null)
|
|
536
|
+
return false;
|
|
537
|
+
const at = Number(raw);
|
|
538
|
+
const now = Date.now();
|
|
539
|
+
if (!Number.isFinite(at) || at > now + 60_000 || at + AUTHORIZATION_REQUEST_TTL_MS < now) {
|
|
540
|
+
storage.removeItem(this.#restoreAttemptKey());
|
|
541
|
+
return false;
|
|
542
|
+
}
|
|
543
|
+
return true;
|
|
544
|
+
}
|
|
545
|
+
#writeRestoreAttempt(storage) {
|
|
546
|
+
try {
|
|
547
|
+
storage.setItem(this.#restoreAttemptKey(), String(Date.now()));
|
|
548
|
+
}
|
|
549
|
+
catch (cause) {
|
|
550
|
+
throw new PixelumeAuthError('storage_unavailable', { cause });
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
#removeRestoreAttempt(storage) {
|
|
554
|
+
try {
|
|
555
|
+
storage.removeItem(this.#restoreAttemptKey());
|
|
556
|
+
}
|
|
557
|
+
catch (cause) {
|
|
558
|
+
throw new PixelumeAuthError('storage_unavailable', { cause });
|
|
559
|
+
}
|
|
560
|
+
}
|
|
328
561
|
#readPending(storage) {
|
|
329
562
|
try {
|
|
330
563
|
const raw = storage.getItem(this.#pendingKey());
|
|
@@ -408,11 +641,58 @@ function samePlayer(left, right) {
|
|
|
408
641
|
left.displayName === right.displayName &&
|
|
409
642
|
left.avatarUrl === right.avatarUrl);
|
|
410
643
|
}
|
|
644
|
+
function isPersistentHint(value) {
|
|
645
|
+
if (typeof value !== 'object' || value === null || Array.isArray(value))
|
|
646
|
+
return false;
|
|
647
|
+
const hint = value;
|
|
648
|
+
return (hint.version === 1 &&
|
|
649
|
+
(hint.scope === 'profile' || hint.scope === 'profile saves') &&
|
|
650
|
+
typeof hint.at === 'number' &&
|
|
651
|
+
Number.isFinite(hint.at));
|
|
652
|
+
}
|
|
653
|
+
function isOnline(browser) {
|
|
654
|
+
try {
|
|
655
|
+
if (typeof browser.isOnline === 'function')
|
|
656
|
+
return browser.isOnline();
|
|
657
|
+
const navigator = Reflect.get(globalThis, 'navigator');
|
|
658
|
+
return navigator?.onLine !== false;
|
|
659
|
+
}
|
|
660
|
+
catch {
|
|
661
|
+
return true;
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
/** Path, query and hash of `current` when it is on the redirect URI's origin; null otherwise. */
|
|
665
|
+
function sameOriginPath(current, redirectUri) {
|
|
666
|
+
try {
|
|
667
|
+
const url = new URL(current);
|
|
668
|
+
if (url.origin !== new URL(redirectUri).origin)
|
|
669
|
+
return null;
|
|
670
|
+
for (const key of CALLBACK_PARAMETERS)
|
|
671
|
+
url.searchParams.delete(key);
|
|
672
|
+
const path = `${url.pathname}${url.search}${url.hash}`;
|
|
673
|
+
return isSafeReturnPath(path) ? path : null;
|
|
674
|
+
}
|
|
675
|
+
catch {
|
|
676
|
+
return null;
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
function isSafeReturnPath(value) {
|
|
680
|
+
return (value.startsWith('/') &&
|
|
681
|
+
!value.startsWith('//') &&
|
|
682
|
+
!value.includes('\\') &&
|
|
683
|
+
value.length <= 2048);
|
|
684
|
+
}
|
|
411
685
|
function isPendingAuthorization(value) {
|
|
412
686
|
if (typeof value !== 'object' || value === null || Array.isArray(value))
|
|
413
687
|
return false;
|
|
414
688
|
const pending = value;
|
|
415
|
-
return (pending.
|
|
689
|
+
return ((pending.silent === undefined || pending.silent === true) &&
|
|
690
|
+
(pending.scope === undefined ||
|
|
691
|
+
pending.scope === 'profile' ||
|
|
692
|
+
pending.scope === 'profile saves') &&
|
|
693
|
+
(pending.returnTo === undefined ||
|
|
694
|
+
(typeof pending.returnTo === 'string' && isSafeReturnPath(pending.returnTo))) &&
|
|
695
|
+
pending.version === 1 &&
|
|
416
696
|
typeof pending.state === 'string' &&
|
|
417
697
|
/^[A-Za-z0-9_-]{43}$/u.test(pending.state) &&
|
|
418
698
|
typeof pending.verifier === 'string' &&
|
|
@@ -461,6 +741,10 @@ function sameState(left, right) {
|
|
|
461
741
|
if (left.status === 'anonymous' && right.status === 'anonymous') {
|
|
462
742
|
return left.reason === right.reason && left.error === right.error;
|
|
463
743
|
}
|
|
744
|
+
if ((left.status === 'redirecting' && right.status === 'redirecting') ||
|
|
745
|
+
(left.status === 'exchanging' && right.status === 'exchanging')) {
|
|
746
|
+
return left.restoring === right.restoring;
|
|
747
|
+
}
|
|
464
748
|
return true;
|
|
465
749
|
}
|
|
466
750
|
//# sourceMappingURL=AuthModule.js.map
|