@mosano-product-framework/sdk 0.2.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.
Files changed (74) hide show
  1. package/README.md +827 -0
  2. package/README.react.md +348 -0
  3. package/dist/auth/claims-types.d.ts +89 -0
  4. package/dist/auth/claims.d.ts +125 -0
  5. package/dist/auth/cross-tab.d.ts +114 -0
  6. package/dist/auth/errors.d.ts +40 -0
  7. package/dist/auth/index.d.ts +18 -0
  8. package/dist/auth/index.js +5 -0
  9. package/dist/auth/index.js.map +1 -0
  10. package/dist/auth/oauth-state.d.ts +93 -0
  11. package/dist/auth/session-manager.d.ts +253 -0
  12. package/dist/auth/storage.d.ts +36 -0
  13. package/dist/auth/tenant-directory.d.ts +59 -0
  14. package/dist/auth/tenant-selection.d.ts +92 -0
  15. package/dist/chunk-7WAV52EO.js +621 -0
  16. package/dist/chunk-7WAV52EO.js.map +1 -0
  17. package/dist/chunk-AJWM5MDZ.js +410 -0
  18. package/dist/chunk-AJWM5MDZ.js.map +1 -0
  19. package/dist/chunk-EXPYHNPV.js +212 -0
  20. package/dist/chunk-EXPYHNPV.js.map +1 -0
  21. package/dist/chunk-GPWGOYCA.js +85 -0
  22. package/dist/chunk-GPWGOYCA.js.map +1 -0
  23. package/dist/chunk-GQJ3QQPH.js +339 -0
  24. package/dist/chunk-GQJ3QQPH.js.map +1 -0
  25. package/dist/chunk-K2ELAI2X.js +64 -0
  26. package/dist/chunk-K2ELAI2X.js.map +1 -0
  27. package/dist/chunk-LRM6JJ63.js +616 -0
  28. package/dist/chunk-LRM6JJ63.js.map +1 -0
  29. package/dist/chunk-XAXFIIRT.js +959 -0
  30. package/dist/chunk-XAXFIIRT.js.map +1 -0
  31. package/dist/client/core/client-factory.d.ts +61 -0
  32. package/dist/client/core/client.d.ts +144 -0
  33. package/dist/client/core/errors.d.ts +105 -0
  34. package/dist/client/core/index.d.ts +9 -0
  35. package/dist/client/core/middleware.d.ts +67 -0
  36. package/dist/client/core/types.d.ts +99 -0
  37. package/dist/client/graphql/client.d.ts +66 -0
  38. package/dist/client/graphql/factory.d.ts +84 -0
  39. package/dist/client/graphql/operation.d.ts +24 -0
  40. package/dist/client/graphql/types.d.ts +60 -0
  41. package/dist/client/graphql/ws-client.d.ts +116 -0
  42. package/dist/client/index.d.ts +17 -0
  43. package/dist/client/index.js +227 -0
  44. package/dist/client/index.js.map +1 -0
  45. package/dist/client/middlewares/admin-auth.d.ts +90 -0
  46. package/dist/client/middlewares/auth.d.ts +81 -0
  47. package/dist/client/middlewares/index.d.ts +12 -0
  48. package/dist/client/middlewares/logging.d.ts +102 -0
  49. package/dist/client/middlewares/retry.d.ts +138 -0
  50. package/dist/client/middlewares/tenant.d.ts +60 -0
  51. package/dist/client/middlewares/turnstile.d.ts +41 -0
  52. package/dist/client/peer-free.d.ts +25 -0
  53. package/dist/client/utils/url.d.ts +19 -0
  54. package/dist/identity/index.d.ts +85 -0
  55. package/dist/identity/index.js +6 -0
  56. package/dist/identity/index.js.map +1 -0
  57. package/dist/identity/types.d.ts +690 -0
  58. package/dist/identity/v0.d.ts +594 -0
  59. package/dist/index.d.ts +50 -0
  60. package/dist/index.js +24 -0
  61. package/dist/index.js.map +1 -0
  62. package/dist/react/context.d.ts +47 -0
  63. package/dist/react/hooks.d.ts +120 -0
  64. package/dist/react/index.d.ts +19 -0
  65. package/dist/react/index.js +308 -0
  66. package/dist/react/index.js.map +1 -0
  67. package/dist/react/provider.d.ts +68 -0
  68. package/dist/react/store.d.ts +85 -0
  69. package/dist/storage/index.d.ts +31 -0
  70. package/dist/storage/index.js +5 -0
  71. package/dist/storage/index.js.map +1 -0
  72. package/dist/storage/types.d.ts +107 -0
  73. package/dist/storage/v0.d.ts +120 -0
  74. package/package.json +99 -0
@@ -0,0 +1,621 @@
1
+ import { tryDecodeAccessToken, tokenLifetimeSeconds, isExpired, DeadSessionError, MPFAuthError } from './chunk-AJWM5MDZ.js';
2
+
3
+ // src/auth/tenant-selection.ts
4
+ var DEFAULT_SELECTION_KEY = "mpf.tenant_selection";
5
+ function readPersisted(storage, key) {
6
+ if (!storage) {
7
+ return null;
8
+ }
9
+ try {
10
+ const raw = storage.getItem(key);
11
+ if (!raw) {
12
+ return null;
13
+ }
14
+ const parsed = JSON.parse(raw);
15
+ if (typeof parsed.tenant !== "string" || parsed.tenant === "") {
16
+ return null;
17
+ }
18
+ return {
19
+ tenant: parsed.tenant,
20
+ ...typeof parsed.role === "string" && parsed.role !== "" ? { role: parsed.role } : {}
21
+ };
22
+ } catch {
23
+ return null;
24
+ }
25
+ }
26
+ function createTenantSelection(initialOrOptions) {
27
+ const isOptions = initialOrOptions !== void 0 && !("tenant" in initialOrOptions);
28
+ const options = isOptions ? initialOrOptions : { ...initialOrOptions ? { initial: initialOrOptions } : {} };
29
+ const storage = options.storage;
30
+ const storageKey = options.storageKey ?? DEFAULT_SELECTION_KEY;
31
+ const initial = options.initial ?? readPersisted(storage, storageKey) ?? void 0;
32
+ let current = {
33
+ tenant: initial?.tenant ?? null,
34
+ ...initial?.role !== void 0 ? { role: initial.role } : {}
35
+ };
36
+ function persist() {
37
+ if (!storage) {
38
+ return;
39
+ }
40
+ try {
41
+ if (current.tenant === null) {
42
+ storage.removeItem(storageKey);
43
+ } else {
44
+ storage.setItem(storageKey, JSON.stringify(current));
45
+ }
46
+ } catch {
47
+ }
48
+ }
49
+ const listeners = /* @__PURE__ */ new Set();
50
+ function emit() {
51
+ for (const listener of [...listeners]) {
52
+ listener(current);
53
+ }
54
+ }
55
+ return {
56
+ get() {
57
+ return current;
58
+ },
59
+ set(tenant, role) {
60
+ if (current.tenant === tenant && current.role === role) {
61
+ return;
62
+ }
63
+ current = { tenant, ...role !== void 0 ? { role } : {} };
64
+ persist();
65
+ emit();
66
+ },
67
+ clear() {
68
+ if (current.tenant === null && current.role === void 0) {
69
+ return;
70
+ }
71
+ current = { tenant: null };
72
+ persist();
73
+ emit();
74
+ },
75
+ subscribe(listener) {
76
+ listeners.add(listener);
77
+ return () => {
78
+ listeners.delete(listener);
79
+ };
80
+ }
81
+ };
82
+ }
83
+
84
+ // src/auth/storage.ts
85
+ function createMemoryStorage() {
86
+ const map = /* @__PURE__ */ new Map();
87
+ return {
88
+ getItem: (key) => map.get(key) ?? null,
89
+ setItem: (key, value) => {
90
+ map.set(key, value);
91
+ },
92
+ removeItem: (key) => {
93
+ map.delete(key);
94
+ }
95
+ };
96
+ }
97
+ function createSessionStorage() {
98
+ if (typeof window === "undefined") {
99
+ return createMemoryStorage();
100
+ }
101
+ try {
102
+ const probe = "__mpf_probe__";
103
+ window.sessionStorage.setItem(probe, probe);
104
+ window.sessionStorage.removeItem(probe);
105
+ return window.sessionStorage;
106
+ } catch {
107
+ return createMemoryStorage();
108
+ }
109
+ }
110
+ function createDefaultStorage() {
111
+ if (typeof window === "undefined") {
112
+ return createMemoryStorage();
113
+ }
114
+ try {
115
+ const probe = "__mpf_probe__";
116
+ window.localStorage.setItem(probe, probe);
117
+ window.localStorage.removeItem(probe);
118
+ return window.localStorage;
119
+ } catch {
120
+ return createMemoryStorage();
121
+ }
122
+ }
123
+
124
+ // src/auth/cross-tab.ts
125
+ function defaultLocks() {
126
+ if (typeof navigator === "undefined") {
127
+ return null;
128
+ }
129
+ const locks = navigator.locks;
130
+ return locks ?? null;
131
+ }
132
+ function defaultChannel(name) {
133
+ if (typeof BroadcastChannel === "undefined") {
134
+ return null;
135
+ }
136
+ try {
137
+ return new BroadcastChannel(name);
138
+ } catch {
139
+ return null;
140
+ }
141
+ }
142
+ function timeoutSignal(ms) {
143
+ const controller = new AbortController();
144
+ setTimeout(() => controller.abort(new Error(`Lock wait timed out after ${ms}ms`)), ms);
145
+ return controller.signal;
146
+ }
147
+ function applyJitter(baseMs, jitterMs, random = Math.random) {
148
+ if (jitterMs <= 0) {
149
+ return Math.max(0, baseMs);
150
+ }
151
+ const offset = (random() * 2 - 1) * jitterMs;
152
+ return Math.max(0, baseMs + offset);
153
+ }
154
+ function createCrossTabCoordinator(options = {}) {
155
+ const {
156
+ lockName = "mpf.refresh",
157
+ channelName = "mpf.auth",
158
+ lockTimeoutMs = 5e3,
159
+ loadPolyfill = async () => null,
160
+ channelFactory = defaultChannel
161
+ } = options;
162
+ const browser = typeof window !== "undefined";
163
+ const injected = options.locks;
164
+ let locks = injected !== void 0 ? injected : browser ? defaultLocks() : null;
165
+ let mode = locks ? injected !== void 0 ? "native" : "native" : "none";
166
+ let polyfillPromise = null;
167
+ if (!locks && browser && injected === void 0) {
168
+ polyfillPromise = loadPolyfill().then((loaded) => {
169
+ if (loaded) {
170
+ locks = loaded;
171
+ mode = "polyfill";
172
+ }
173
+ }).catch(() => {
174
+ });
175
+ }
176
+ const channel = browser ? channelFactory(channelName) : null;
177
+ const listeners = /* @__PURE__ */ new Set();
178
+ const onMessage = (event) => {
179
+ const data = event.data;
180
+ if (!data || typeof data.accessToken !== "string" || typeof data.refreshToken !== "string") {
181
+ return;
182
+ }
183
+ const message = {
184
+ accessToken: data.accessToken,
185
+ refreshToken: data.refreshToken
186
+ };
187
+ for (const listener of [...listeners]) {
188
+ listener(message);
189
+ }
190
+ };
191
+ channel?.addEventListener("message", onMessage);
192
+ return {
193
+ get mode() {
194
+ return mode;
195
+ },
196
+ async withLock(fn) {
197
+ if (polyfillPromise) {
198
+ await polyfillPromise;
199
+ polyfillPromise = null;
200
+ }
201
+ if (!locks) {
202
+ return fn();
203
+ }
204
+ let result;
205
+ let ran = false;
206
+ try {
207
+ await locks.request(lockName, { signal: timeoutSignal(lockTimeoutMs) }, async () => {
208
+ result = await fn();
209
+ ran = true;
210
+ });
211
+ } catch (error) {
212
+ if (ran) {
213
+ throw error;
214
+ }
215
+ return fn();
216
+ }
217
+ return result;
218
+ },
219
+ publish(message) {
220
+ channel?.postMessage(message);
221
+ },
222
+ subscribe(listener) {
223
+ listeners.add(listener);
224
+ return () => {
225
+ listeners.delete(listener);
226
+ };
227
+ },
228
+ close() {
229
+ listeners.clear();
230
+ channel?.removeEventListener("message", onMessage);
231
+ channel?.close();
232
+ }
233
+ };
234
+ }
235
+
236
+ // src/auth/session-manager.ts
237
+ var REFRESH_TOKEN_SUPERSEDED = "REFRESH_TOKEN_SUPERSEDED";
238
+ var DEFAULT_BUFFER_SECONDS = 120;
239
+ var DEFAULT_MIN_RENEW_INTERVAL_MS = 5e3;
240
+ var DEFAULT_JITTER_MS = 3e4;
241
+ var SessionManager = class {
242
+ constructor(options) {
243
+ this.options = options;
244
+ this.storage = options.storage ?? createDefaultStorage();
245
+ this.storageKey = options.storageKey ?? "mpf.refresh_token";
246
+ this.bufferSeconds = options.bufferSeconds ?? DEFAULT_BUFFER_SECONDS;
247
+ this.minRenewIntervalMs = options.minRenewIntervalMs ?? DEFAULT_MIN_RENEW_INTERVAL_MS;
248
+ this.jitterMs = options.jitterMs ?? DEFAULT_JITTER_MS;
249
+ this.now = options.now ?? (() => Date.now());
250
+ this.random = options.random ?? Math.random;
251
+ this.crossTab = options.crossTab === null ? null : options.crossTab ?? createCrossTabCoordinator();
252
+ if (this.crossTab) {
253
+ this.unsubscribeBroadcast = this.crossTab.subscribe((message) => {
254
+ this.adopt(message, { rebroadcast: false });
255
+ });
256
+ }
257
+ this.refreshToken = this.storage.getItem(this.storageKey);
258
+ this.wakeListening = options.listenToWake ?? typeof window !== "undefined";
259
+ if (this.wakeListening && typeof window !== "undefined") {
260
+ window.addEventListener("visibilitychange", this.onWakeEvent);
261
+ window.addEventListener("online", this.onWakeEvent);
262
+ }
263
+ }
264
+ accessToken = null;
265
+ refreshToken = null;
266
+ /** In-flight renewal, for coalescing. */
267
+ inflight = null;
268
+ /**
269
+ * Gate that `getAccessToken()` awaits. Set by the wake path so that requests
270
+ * queued during a wake renewal block behind it instead of shipping a token
271
+ * that is already dead.
272
+ */
273
+ wakeGate = null;
274
+ timer = null;
275
+ lastRenewAt = 0;
276
+ destroyed = false;
277
+ /**
278
+ * Incremented on every `clear()`. A renewal captures it before its network
279
+ * call and refuses to adopt a result from a superseded generation, so signing
280
+ * out (or clearing) mid-renew cannot be undone by the renewal completing.
281
+ */
282
+ generation = 0;
283
+ storage;
284
+ storageKey;
285
+ bufferSeconds;
286
+ minRenewIntervalMs;
287
+ jitterMs;
288
+ now;
289
+ random;
290
+ crossTab;
291
+ unsubscribeBroadcast = null;
292
+ wakeListening;
293
+ // -------------------------------------------------------------------------
294
+ // Public API
295
+ // -------------------------------------------------------------------------
296
+ /**
297
+ * Adopt a freshly minted token pair (after sign-in, or after any flow that
298
+ * changes memberships).
299
+ */
300
+ setTokens(tokens) {
301
+ this.adopt(
302
+ { accessToken: tokens.access_token, refreshToken: tokens.refresh_token },
303
+ { rebroadcast: true }
304
+ );
305
+ }
306
+ /**
307
+ * A usable access token, renewing first if necessary.
308
+ *
309
+ * Returns `null` only when there is no session at all.
310
+ */
311
+ async getAccessToken() {
312
+ if (this.wakeGate) {
313
+ await this.wakeGate;
314
+ }
315
+ if (!this.accessToken) {
316
+ return this.refreshToken ? this.renew() : null;
317
+ }
318
+ if (this.isStale()) {
319
+ return this.renew();
320
+ }
321
+ return this.accessToken;
322
+ }
323
+ /** The current access token without renewing. */
324
+ peekAccessToken() {
325
+ return this.accessToken;
326
+ }
327
+ /** Decoded (UNVERIFIED — UX only) claims of the current access token. */
328
+ getClaims() {
329
+ return tryDecodeAccessToken(this.accessToken);
330
+ }
331
+ /**
332
+ * Renew the session. Coalesced: concurrent callers share one network call,
333
+ * which is mandatory because the refresh token is single-use.
334
+ */
335
+ async renew() {
336
+ if (this.inflight) {
337
+ return this.inflight;
338
+ }
339
+ this.inflight = this.renewGuarded().finally(() => {
340
+ this.inflight = null;
341
+ });
342
+ return this.inflight;
343
+ }
344
+ /**
345
+ * Sign out: revoke the session server-side, then clear it locally.
346
+ *
347
+ * **Never throws, and always clears.** A user who clicked sign-out must end up
348
+ * signed out locally whatever the network did — a UI that refuses to log out
349
+ * is worse than a token that outlives its client, and the failed revoke leaves
350
+ * that token behind either way. The outcome is reported in the return value so
351
+ * an app can say "signed out, but we couldn't reach the server" rather than
352
+ * having to guess.
353
+ *
354
+ * Order is load-bearing: `revokeSession` is an *authenticated* endpoint, so
355
+ * the revoke must go out **before** the access token is cleared. Reversing it
356
+ * silently sends an unauthenticated request that cannot succeed.
357
+ *
358
+ * Idempotent: with no session, it is a no-op reporting `revoked: false`.
359
+ *
360
+ * This exists so consumers never need the raw refresh token. Reaching into
361
+ * storage for it — `localStorage.getItem('mpf.refresh_token')` — hardcodes the
362
+ * default key and breaks for anyone using a custom storage or key.
363
+ */
364
+ async signOut() {
365
+ const refreshToken = this.refreshToken;
366
+ const revoker = this.options.revoke;
367
+ let revoked = false;
368
+ let error;
369
+ if (refreshToken && revoker) {
370
+ try {
371
+ await revoker(refreshToken);
372
+ revoked = true;
373
+ } catch (caught) {
374
+ error = caught instanceof Error ? caught : new Error(String(caught));
375
+ }
376
+ }
377
+ this.clear();
378
+ return { revoked, ...error ? { error } : {} };
379
+ }
380
+ /**
381
+ * Forget the session locally. Stops timers and invalidates any in-flight
382
+ * renewal so it cannot resurrect the session after it resolves.
383
+ *
384
+ * Prefer {@link signOut} for a user-initiated sign-out: this leaves the
385
+ * refresh token valid server-side.
386
+ */
387
+ clear() {
388
+ this.generation += 1;
389
+ this.accessToken = null;
390
+ this.refreshToken = null;
391
+ this.storage.removeItem(this.storageKey);
392
+ this.cancelTimer();
393
+ this.options.onTokensChanged?.(null);
394
+ }
395
+ /** Detach listeners and release the coordinator. */
396
+ destroy() {
397
+ this.destroyed = true;
398
+ this.cancelTimer();
399
+ this.unsubscribeBroadcast?.();
400
+ if (this.wakeListening && typeof window !== "undefined") {
401
+ window.removeEventListener("visibilitychange", this.onWakeEvent);
402
+ window.removeEventListener("online", this.onWakeEvent);
403
+ }
404
+ this.crossTab?.close();
405
+ }
406
+ /**
407
+ * The buffer actually in force for the current token.
408
+ *
409
+ * The configured buffer is clamped to just under half the token's lifetime:
410
+ * if the buffer were ≥ half the lifetime, a freshly minted token would be
411
+ * "stale" the instant it arrived and the manager would renew-loop, hammering
412
+ * `/sessions/renew` forever. A 60s-TTL token therefore gets a 29s buffer, not
413
+ * the configured 120s.
414
+ *
415
+ * Falls back to the configured value when the token carries no `iat` and the
416
+ * lifetime is unknowable.
417
+ */
418
+ effectiveBufferSeconds() {
419
+ const claims = this.getClaims();
420
+ if (!claims) {
421
+ return this.bufferSeconds;
422
+ }
423
+ const lifetime = tokenLifetimeSeconds(claims);
424
+ if (lifetime === void 0) {
425
+ return this.bufferSeconds;
426
+ }
427
+ const ceiling = Math.max(0, Math.floor(lifetime / 2) - 1);
428
+ return Math.min(this.bufferSeconds, ceiling);
429
+ }
430
+ // -------------------------------------------------------------------------
431
+ // Internals
432
+ // -------------------------------------------------------------------------
433
+ isStale() {
434
+ const claims = this.getClaims();
435
+ if (!claims) {
436
+ return true;
437
+ }
438
+ return isExpired(claims, this.effectiveBufferSeconds(), this.now());
439
+ }
440
+ adopt(tokens, opts) {
441
+ this.accessToken = tokens.accessToken;
442
+ this.refreshToken = tokens.refreshToken;
443
+ this.storage.setItem(this.storageKey, tokens.refreshToken);
444
+ this.scheduleProactiveRenew();
445
+ this.options.onTokensChanged?.({
446
+ access_token: tokens.accessToken,
447
+ refresh_token: tokens.refreshToken
448
+ });
449
+ if (opts.rebroadcast) {
450
+ this.crossTab?.publish(tokens);
451
+ }
452
+ }
453
+ async renewGuarded() {
454
+ if (!this.refreshToken) {
455
+ return this.die(new DeadSessionError("No refresh token available"));
456
+ }
457
+ const sinceLast = this.now() - this.lastRenewAt;
458
+ if (this.lastRenewAt !== 0 && sinceLast < this.minRenewIntervalMs && this.accessToken) {
459
+ return this.accessToken;
460
+ }
461
+ const startedWith = this.refreshToken;
462
+ const run = async () => {
463
+ const persisted = this.storage.getItem(this.storageKey);
464
+ if (persisted && persisted !== startedWith) {
465
+ this.refreshToken = persisted;
466
+ if (this.accessToken && !this.isStale()) {
467
+ return this.accessToken;
468
+ }
469
+ }
470
+ return this.networkRenew();
471
+ };
472
+ return this.crossTab ? this.crossTab.withLock(run) : run();
473
+ }
474
+ async networkRenew() {
475
+ const refreshToken = this.refreshToken;
476
+ if (!refreshToken) {
477
+ return this.die(new DeadSessionError("No refresh token available"));
478
+ }
479
+ this.lastRenewAt = this.now();
480
+ const generation = this.generation;
481
+ let pair;
482
+ try {
483
+ pair = await this.options.renew(refreshToken);
484
+ } catch (error) {
485
+ if (isSupersededError(error)) {
486
+ const adopted = await this.adoptAfterSuperseded();
487
+ if (adopted) {
488
+ return adopted;
489
+ }
490
+ }
491
+ return this.die(
492
+ new DeadSessionError(
493
+ "Session renewal failed",
494
+ error instanceof Error ? error : void 0
495
+ )
496
+ );
497
+ }
498
+ if (generation !== this.generation) {
499
+ return pair.access_token;
500
+ }
501
+ this.adopt(
502
+ { accessToken: pair.access_token, refreshToken: pair.refresh_token },
503
+ { rebroadcast: true }
504
+ );
505
+ return pair.access_token;
506
+ }
507
+ /**
508
+ * After a REFRESH_TOKEN_SUPERSEDED, take the winner's tokens.
509
+ *
510
+ * The winner's access token arrives over the broadcast channel; its refresh
511
+ * token is in storage. If neither is usable there is nothing to adopt and the
512
+ * caller declares the session dead.
513
+ */
514
+ async adoptAfterSuperseded() {
515
+ const persisted = this.storage.getItem(this.storageKey);
516
+ if (persisted) {
517
+ this.refreshToken = persisted;
518
+ }
519
+ if (this.accessToken && !this.isStale()) {
520
+ return this.accessToken;
521
+ }
522
+ return null;
523
+ }
524
+ die(error) {
525
+ this.clear();
526
+ this.options.onDeadSession?.(error);
527
+ throw error;
528
+ }
529
+ cancelTimer() {
530
+ if (this.timer !== null) {
531
+ clearTimeout(this.timer);
532
+ this.timer = null;
533
+ }
534
+ }
535
+ /**
536
+ * Schedule a renewal shortly before the token goes stale.
537
+ *
538
+ * Jittered so that N tabs computing the same deadline from the same token do
539
+ * not all wake on the same millisecond.
540
+ */
541
+ scheduleProactiveRenew() {
542
+ this.cancelTimer();
543
+ if (this.destroyed) {
544
+ return;
545
+ }
546
+ const claims = this.getClaims();
547
+ if (!claims) {
548
+ return;
549
+ }
550
+ const staleAtMs = (claims.exp - this.effectiveBufferSeconds()) * 1e3;
551
+ const base = Math.max(0, staleAtMs - this.now());
552
+ const delay = Math.max(
553
+ this.minRenewIntervalMs,
554
+ applyJitter(base, this.jitterMs, this.random)
555
+ );
556
+ this.timer = setTimeout(() => {
557
+ this.timer = null;
558
+ void this.renew().catch(() => {
559
+ });
560
+ }, delay);
561
+ }
562
+ onWakeEvent = () => {
563
+ if (typeof document !== "undefined" && document.visibilityState === "hidden") {
564
+ return;
565
+ }
566
+ void this.handleWake();
567
+ };
568
+ /**
569
+ * Wake path — the real safety net.
570
+ *
571
+ * `setTimeout` does not fire in frozen or discarded tabs (Chrome tab
572
+ * freezing, iOS Safari), so a tab backgrounded for 20 minutes wakes holding a
573
+ * guaranteed-dead token with its timer never having run.
574
+ *
575
+ * Merely rescheduling here is wrong: the delay computes negative, becomes
576
+ * `setTimeout(0)`, and then races whatever the app fires on focus. Instead
577
+ * renew immediately and publish a gate that `getAccessToken()` awaits, so
578
+ * requests triggered by the same focus event queue behind the renewal rather
579
+ * than each eating a 401.
580
+ */
581
+ async handleWake() {
582
+ if (this.destroyed || !this.refreshToken) {
583
+ return;
584
+ }
585
+ if (this.accessToken && !this.isStale()) {
586
+ this.scheduleProactiveRenew();
587
+ return;
588
+ }
589
+ if (this.wakeGate) {
590
+ return;
591
+ }
592
+ let release;
593
+ this.wakeGate = new Promise((resolve) => {
594
+ release = resolve;
595
+ });
596
+ try {
597
+ await this.renew();
598
+ } catch {
599
+ } finally {
600
+ const gate = release;
601
+ this.wakeGate = null;
602
+ gate();
603
+ }
604
+ }
605
+ };
606
+ function isSupersededError(error) {
607
+ if (error instanceof MPFAuthError) {
608
+ return error.serverCode === REFRESH_TOKEN_SUPERSEDED;
609
+ }
610
+ if (error && typeof error === "object" && "code" in error) {
611
+ return error.code === REFRESH_TOKEN_SUPERSEDED;
612
+ }
613
+ return false;
614
+ }
615
+ function createSessionManager(options) {
616
+ return new SessionManager(options);
617
+ }
618
+
619
+ export { REFRESH_TOKEN_SUPERSEDED, SessionManager, applyJitter, createCrossTabCoordinator, createDefaultStorage, createMemoryStorage, createSessionManager, createSessionStorage, createTenantSelection, isSupersededError };
620
+ //# sourceMappingURL=chunk-7WAV52EO.js.map
621
+ //# sourceMappingURL=chunk-7WAV52EO.js.map