@oxyhq/core 3.4.1 → 3.4.3

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 (174) hide show
  1. package/dist/cjs/.tsbuildinfo +1 -1
  2. package/dist/cjs/AuthManager.js +91 -319
  3. package/dist/cjs/CrossDomainAuth.js +19 -106
  4. package/dist/cjs/HttpService.js +49 -73
  5. package/dist/cjs/OxyServices.base.js +2 -2
  6. package/dist/cjs/i18n/index.js +7 -1
  7. package/dist/cjs/i18n/locales/ar-SA.json +18 -2
  8. package/dist/cjs/i18n/locales/ca-ES.json +18 -2
  9. package/dist/cjs/i18n/locales/de-DE.json +18 -2
  10. package/dist/cjs/i18n/locales/en-US.json +16 -2
  11. package/dist/cjs/i18n/locales/es-ES.json +16 -2
  12. package/dist/cjs/i18n/locales/fr-FR.json +18 -2
  13. package/dist/cjs/i18n/locales/it-IT.json +18 -2
  14. package/dist/cjs/i18n/locales/ja-JP.json +18 -2
  15. package/dist/cjs/i18n/locales/ko-KR.json +18 -2
  16. package/dist/cjs/i18n/locales/locales/ar-SA.json +18 -2
  17. package/dist/cjs/i18n/locales/locales/ca-ES.json +18 -2
  18. package/dist/cjs/i18n/locales/locales/de-DE.json +18 -2
  19. package/dist/cjs/i18n/locales/locales/en-US.json +17 -3
  20. package/dist/cjs/i18n/locales/locales/es-ES.json +16 -2
  21. package/dist/cjs/i18n/locales/locales/fr-FR.json +18 -2
  22. package/dist/cjs/i18n/locales/locales/it-IT.json +18 -2
  23. package/dist/cjs/i18n/locales/locales/ja-JP.json +18 -2
  24. package/dist/cjs/i18n/locales/locales/ko-KR.json +18 -2
  25. package/dist/cjs/i18n/locales/locales/pt-PT.json +18 -2
  26. package/dist/cjs/i18n/locales/locales/zh-CN.json +18 -2
  27. package/dist/cjs/i18n/locales/pt-PT.json +18 -2
  28. package/dist/cjs/i18n/locales/zh-CN.json +18 -2
  29. package/dist/cjs/mixins/OxyServices.auth.js +20 -63
  30. package/dist/cjs/mixins/OxyServices.fedcm.js +10 -12
  31. package/dist/cjs/mixins/OxyServices.popup.js +50 -299
  32. package/dist/cjs/mixins/OxyServices.redirect.js +84 -348
  33. package/dist/cjs/mixins/OxyServices.silent.js +204 -0
  34. package/dist/cjs/mixins/OxyServices.sso.js +4 -5
  35. package/dist/cjs/mixins/OxyServices.utility.js +6 -15
  36. package/dist/cjs/mixins/index.js +5 -6
  37. package/dist/cjs/server/index.js +21 -0
  38. package/dist/cjs/server/rateLimit.js +77 -0
  39. package/dist/cjs/shared/utils/debugUtils.js +1 -1
  40. package/dist/cjs/utils/accountUtils.js +4 -4
  41. package/dist/cjs/utils/authHelpers.js +21 -15
  42. package/dist/cjs/utils/coldBoot.js +3 -3
  43. package/dist/cjs/utils/fapiAutoDetect.js +1 -1
  44. package/dist/esm/.tsbuildinfo +1 -1
  45. package/dist/esm/AuthManager.js +91 -319
  46. package/dist/esm/CrossDomainAuth.js +19 -106
  47. package/dist/esm/HttpService.js +49 -73
  48. package/dist/esm/OxyServices.base.js +2 -2
  49. package/dist/esm/i18n/index.js +7 -1
  50. package/dist/esm/i18n/locales/ar-SA.json +18 -2
  51. package/dist/esm/i18n/locales/ca-ES.json +18 -2
  52. package/dist/esm/i18n/locales/de-DE.json +18 -2
  53. package/dist/esm/i18n/locales/en-US.json +16 -2
  54. package/dist/esm/i18n/locales/es-ES.json +16 -2
  55. package/dist/esm/i18n/locales/fr-FR.json +18 -2
  56. package/dist/esm/i18n/locales/it-IT.json +18 -2
  57. package/dist/esm/i18n/locales/ja-JP.json +18 -2
  58. package/dist/esm/i18n/locales/ko-KR.json +18 -2
  59. package/dist/esm/i18n/locales/locales/ar-SA.json +18 -2
  60. package/dist/esm/i18n/locales/locales/ca-ES.json +18 -2
  61. package/dist/esm/i18n/locales/locales/de-DE.json +18 -2
  62. package/dist/esm/i18n/locales/locales/en-US.json +17 -3
  63. package/dist/esm/i18n/locales/locales/es-ES.json +16 -2
  64. package/dist/esm/i18n/locales/locales/fr-FR.json +18 -2
  65. package/dist/esm/i18n/locales/locales/it-IT.json +18 -2
  66. package/dist/esm/i18n/locales/locales/ja-JP.json +18 -2
  67. package/dist/esm/i18n/locales/locales/ko-KR.json +18 -2
  68. package/dist/esm/i18n/locales/locales/pt-PT.json +18 -2
  69. package/dist/esm/i18n/locales/locales/zh-CN.json +18 -2
  70. package/dist/esm/i18n/locales/pt-PT.json +18 -2
  71. package/dist/esm/i18n/locales/zh-CN.json +18 -2
  72. package/dist/esm/mixins/OxyServices.auth.js +20 -63
  73. package/dist/esm/mixins/OxyServices.fedcm.js +10 -12
  74. package/dist/esm/mixins/OxyServices.popup.js +52 -301
  75. package/dist/esm/mixins/OxyServices.redirect.js +84 -349
  76. package/dist/esm/mixins/OxyServices.silent.js +202 -0
  77. package/dist/esm/mixins/OxyServices.sso.js +4 -5
  78. package/dist/esm/mixins/OxyServices.utility.js +6 -15
  79. package/dist/esm/mixins/index.js +5 -6
  80. package/dist/esm/server/index.js +17 -0
  81. package/dist/esm/server/rateLimit.js +71 -0
  82. package/dist/esm/shared/utils/debugUtils.js +1 -1
  83. package/dist/esm/utils/accountUtils.js +4 -4
  84. package/dist/esm/utils/authHelpers.js +21 -15
  85. package/dist/esm/utils/coldBoot.js +3 -3
  86. package/dist/esm/utils/fapiAutoDetect.js +1 -1
  87. package/dist/types/.tsbuildinfo +1 -1
  88. package/dist/types/AuthManager.d.ts +26 -53
  89. package/dist/types/AuthManagerTypes.d.ts +5 -9
  90. package/dist/types/CrossDomainAuth.d.ts +13 -52
  91. package/dist/types/HttpService.d.ts +9 -8
  92. package/dist/types/OxyServices.base.d.ts +1 -1
  93. package/dist/types/OxyServices.d.ts +4 -10
  94. package/dist/types/index.d.ts +1 -1
  95. package/dist/types/mixins/OxyServices.analytics.d.ts +1 -1
  96. package/dist/types/mixins/OxyServices.appData.d.ts +1 -1
  97. package/dist/types/mixins/OxyServices.applications.d.ts +1 -1
  98. package/dist/types/mixins/OxyServices.assets.d.ts +1 -1
  99. package/dist/types/mixins/OxyServices.auth.d.ts +10 -31
  100. package/dist/types/mixins/OxyServices.contacts.d.ts +1 -1
  101. package/dist/types/mixins/OxyServices.devices.d.ts +1 -1
  102. package/dist/types/mixins/OxyServices.features.d.ts +1 -1
  103. package/dist/types/mixins/OxyServices.fedcm.d.ts +5 -5
  104. package/dist/types/mixins/OxyServices.language.d.ts +1 -1
  105. package/dist/types/mixins/OxyServices.location.d.ts +1 -1
  106. package/dist/types/mixins/OxyServices.managedAccounts.d.ts +1 -1
  107. package/dist/types/mixins/OxyServices.payment.d.ts +1 -1
  108. package/dist/types/mixins/OxyServices.popup.d.ts +18 -120
  109. package/dist/types/mixins/OxyServices.privacy.d.ts +1 -1
  110. package/dist/types/mixins/OxyServices.redirect.d.ts +13 -174
  111. package/dist/types/mixins/OxyServices.reputation.d.ts +1 -1
  112. package/dist/types/mixins/OxyServices.security.d.ts +1 -1
  113. package/dist/types/mixins/OxyServices.silent.d.ts +131 -0
  114. package/dist/types/mixins/OxyServices.sso.d.ts +4 -5
  115. package/dist/types/mixins/OxyServices.topics.d.ts +1 -1
  116. package/dist/types/mixins/OxyServices.user.d.ts +1 -1
  117. package/dist/types/mixins/OxyServices.utility.d.ts +3 -8
  118. package/dist/types/mixins/OxyServices.workspaces.d.ts +1 -1
  119. package/dist/types/mixins/index.d.ts +3 -3
  120. package/dist/types/models/interfaces.d.ts +5 -16
  121. package/dist/types/models/session.d.ts +0 -2
  122. package/dist/types/server/index.d.ts +18 -0
  123. package/dist/types/server/rateLimit.d.ts +40 -0
  124. package/dist/types/shared/utils/debugUtils.d.ts +1 -1
  125. package/dist/types/utils/authHelpers.d.ts +4 -3
  126. package/dist/types/utils/coldBoot.d.ts +2 -2
  127. package/dist/types/utils/fapiAutoDetect.d.ts +1 -1
  128. package/package.json +24 -2
  129. package/src/AuthManager.ts +100 -370
  130. package/src/AuthManagerTypes.ts +5 -9
  131. package/src/CrossDomainAuth.ts +22 -129
  132. package/src/HttpService.ts +55 -73
  133. package/src/OxyServices.base.ts +2 -3
  134. package/src/OxyServices.ts +9 -11
  135. package/src/__tests__/authManager.cookiePath.test.ts +19 -17
  136. package/src/__tests__/authManager.security.test.ts +7 -3
  137. package/src/__tests__/crossDomainAuth.test.ts +26 -118
  138. package/src/i18n/index.ts +7 -1
  139. package/src/i18n/locales/ar-SA.json +18 -2
  140. package/src/i18n/locales/ca-ES.json +18 -2
  141. package/src/i18n/locales/de-DE.json +18 -2
  142. package/src/i18n/locales/en-US.json +17 -3
  143. package/src/i18n/locales/es-ES.json +16 -2
  144. package/src/i18n/locales/fr-FR.json +18 -2
  145. package/src/i18n/locales/it-IT.json +18 -2
  146. package/src/i18n/locales/ja-JP.json +18 -2
  147. package/src/i18n/locales/ko-KR.json +18 -2
  148. package/src/i18n/locales/pt-PT.json +18 -2
  149. package/src/i18n/locales/zh-CN.json +18 -2
  150. package/src/index.ts +1 -1
  151. package/src/mixins/OxyServices.auth.ts +23 -75
  152. package/src/mixins/OxyServices.fedcm.ts +10 -12
  153. package/src/mixins/OxyServices.redirect.ts +82 -371
  154. package/src/mixins/OxyServices.silent.ts +272 -0
  155. package/src/mixins/OxyServices.sso.ts +5 -6
  156. package/src/mixins/OxyServices.utility.ts +9 -22
  157. package/src/mixins/__tests__/appData.test.ts +1 -1
  158. package/src/mixins/__tests__/onTokensChanged.test.ts +1 -1
  159. package/src/mixins/__tests__/reputation.test.ts +1 -1
  160. package/src/mixins/__tests__/serviceAuth.test.ts +7 -5
  161. package/src/mixins/__tests__/silent.test.ts +102 -0
  162. package/src/mixins/__tests__/verifyChallenge.test.ts +9 -14
  163. package/src/mixins/index.ts +6 -8
  164. package/src/models/interfaces.ts +5 -16
  165. package/src/models/session.ts +1 -3
  166. package/src/server/index.ts +19 -0
  167. package/src/server/rateLimit.ts +170 -0
  168. package/src/shared/utils/debugUtils.ts +1 -1
  169. package/src/utils/accountUtils.ts +4 -4
  170. package/src/utils/authHelpers.ts +23 -15
  171. package/src/utils/coldBoot.ts +4 -4
  172. package/src/utils/fapiAutoDetect.ts +1 -1
  173. package/src/mixins/OxyServices.popup.ts +0 -631
  174. package/src/mixins/__tests__/popup.test.ts +0 -374
@@ -10,18 +10,8 @@
10
10
  Object.defineProperty(exports, "__esModule", { value: true });
11
11
  exports.AuthManager = void 0;
12
12
  exports.createAuthManager = createAuthManager;
13
- const asyncUtils_1 = require("./utils/asyncUtils");
14
13
  const jwt_decode_1 = require("jwt-decode");
15
- /**
16
- * Storage keys used by AuthManager.
17
- */
18
14
  const STORAGE_KEYS = {
19
- ACCESS_TOKEN: 'oxy_access_token',
20
- REFRESH_TOKEN: 'oxy_refresh_token',
21
- SESSION: 'oxy_session',
22
- USER: 'oxy_user',
23
- AUTH_METHOD: 'oxy_auth_method',
24
- FEDCM_LOGIN_HINT: 'oxy_fedcm_login_hint',
25
15
  /**
26
16
  * Persisted active `authuser` slot index for the cookie path. Stores ONLY
27
17
  * the integer slot index (e.g. `"0"`, `"1"`), never a token or session
@@ -112,14 +102,13 @@ class AuthManager {
112
102
  constructor(oxyServices, config = {}) {
113
103
  this.listeners = new Set();
114
104
  this.currentUser = null;
105
+ this.currentAuthMethod = null;
115
106
  this.refreshTimer = null;
116
107
  this.refreshPromise = null;
117
108
  /** Tracks the access token this instance last knew about, for cross-tab adoption. */
118
109
  this._lastKnownAccessToken = null;
119
110
  /** BroadcastChannel for coordinating token refreshes across browser tabs. */
120
111
  this._broadcastChannel = null;
121
- /** Set to true when another tab broadcasts a successful refresh, so this tab can skip its own. */
122
- this._otherTabRefreshed = false;
123
112
  /**
124
113
  * Identifier for this AuthManager instance (≈ "this tab"). Random hex
125
114
  * generated at construction; advertised in every outgoing broadcast and
@@ -185,14 +174,12 @@ class AuthManager {
185
174
  autoRefresh: config.autoRefresh ?? true,
186
175
  refreshBuffer: config.refreshBuffer ?? 5 * 60 * 1000, // 5 minutes
187
176
  crossTabSync,
188
- cookieOnly: config.cookieOnly ?? false,
189
177
  };
190
178
  this.storage = this.config.storage;
191
- // Persist tokens to storage when HttpService refreshes them automatically
192
- this.oxyServices.httpService.onTokenRefreshed = (accessToken) => {
193
- this._lastKnownAccessToken = accessToken;
194
- this.storage.setItem(STORAGE_KEYS.ACCESS_TOKEN, accessToken);
195
- };
179
+ this.oxyServices.httpService.setAuthRefreshHandler(async () => {
180
+ const refreshed = await this.refreshToken();
181
+ return refreshed ? this._lastKnownAccessToken : null;
182
+ });
196
183
  // Setup cross-tab coordination in browser environments
197
184
  if (this.config.crossTabSync) {
198
185
  this._initBroadcastChannel();
@@ -225,43 +212,6 @@ class AuthManager {
225
212
  if (!this._acceptBroadcast(message))
226
213
  return;
227
214
  switch (message.type) {
228
- case 'tokens_refreshed': {
229
- // Another tab successfully refreshed. Signal to cancel our pending refresh.
230
- this._otherTabRefreshed = true;
231
- // Adopt the new tokens from shared storage
232
- const newToken = await this.storage.getItem(STORAGE_KEYS.ACCESS_TOKEN);
233
- if (newToken && newToken !== this._lastKnownAccessToken) {
234
- this._lastKnownAccessToken = newToken;
235
- this.oxyServices.httpService.setTokens(newToken);
236
- // Re-read session for updated expiry and schedule next refresh
237
- const sessionJson = await this.storage.getItem(STORAGE_KEYS.SESSION);
238
- if (sessionJson) {
239
- try {
240
- const session = JSON.parse(sessionJson);
241
- if (session.expiresAt && this.config.autoRefresh) {
242
- this.setupTokenRefresh(session.expiresAt);
243
- }
244
- }
245
- catch {
246
- // Ignore parse errors
247
- }
248
- }
249
- }
250
- break;
251
- }
252
- case 'signed_out': {
253
- // Another tab signed out. Clear our local state to stay consistent.
254
- if (this.refreshTimer) {
255
- clearTimeout(this.refreshTimer);
256
- this.refreshTimer = null;
257
- }
258
- this.refreshPromise = null;
259
- this._lastKnownAccessToken = null;
260
- this.oxyServices.httpService.setTokens('');
261
- this.currentUser = null;
262
- this.notifyListeners();
263
- break;
264
- }
265
215
  case 'accounts_restored':
266
216
  case 'authuser_switched':
267
217
  case 'authuser_signed_out': {
@@ -283,7 +233,7 @@ class AuthManager {
283
233
  break;
284
234
  }
285
235
  case 'all_signed_out': {
286
- // Mirror `signed_out` but also wipe the cookie-path registry.
236
+ // Wipe the cookie-path registry after another tab signed every slot out.
287
237
  if (this.refreshTimer) {
288
238
  clearTimeout(this.refreshTimer);
289
239
  this.refreshTimer = null;
@@ -294,10 +244,10 @@ class AuthManager {
294
244
  this._lastKnownAccessToken = null;
295
245
  this.oxyServices.httpService.setTokens('');
296
246
  this.currentUser = null;
247
+ this.currentAuthMethod = null;
297
248
  this.notifyListeners();
298
249
  break;
299
250
  }
300
- // 'refresh_starting' is informational; we don't need to act on it currently
301
251
  }
302
252
  }
303
253
  /**
@@ -429,58 +379,48 @@ class AuthManager {
429
379
  * @param method - Auth method used
430
380
  */
431
381
  async handleAuthSuccess(session, method = 'credentials') {
432
- // Store tokens
382
+ // Access tokens are memory-only. Fresh login responses plant the token on
383
+ // the HTTP client and the AuthManager registry, but never write it to JS
384
+ // storage. Durable web refresh lives in the httpOnly cookie set by the API.
433
385
  if (session.accessToken) {
434
386
  this._lastKnownAccessToken = session.accessToken;
435
- await this.storage.setItem(STORAGE_KEYS.ACCESS_TOKEN, session.accessToken);
436
387
  this.oxyServices.httpService.setTokens(session.accessToken);
437
388
  }
438
- // Store refresh token if available
439
- if (session.refreshToken) {
440
- await this.storage.setItem(STORAGE_KEYS.REFRESH_TOKEN, session.refreshToken);
441
- }
442
- // Store session info
443
- await this.storage.setItem(STORAGE_KEYS.SESSION, JSON.stringify({
444
- sessionId: session.sessionId,
445
- deviceId: session.deviceId,
446
- expiresAt: session.expiresAt,
447
- }));
448
- // Store user only if it has valid required fields (not an empty placeholder)
449
389
  if (session.user && typeof session.user.id === 'string' && session.user.id.length > 0) {
450
- await this.storage.setItem(STORAGE_KEYS.USER, JSON.stringify(session.user));
451
390
  this.currentUser = session.user;
452
391
  }
453
- // Store auth method
454
- await this.storage.setItem(STORAGE_KEYS.AUTH_METHOD, method);
392
+ this.currentAuthMethod = method;
393
+ const decodedAuthuser = session.accessToken
394
+ ? AuthManager.decodeAuthuserFromAccessToken(session.accessToken)
395
+ : null;
396
+ const authuser = decodedAuthuser ?? 0;
397
+ if (session.accessToken && session.sessionId) {
398
+ this.accounts.set(authuser, {
399
+ authuser,
400
+ sessionId: session.sessionId,
401
+ user: {
402
+ id: session.user.id,
403
+ username: session.user.username,
404
+ avatar: session.user.avatar ?? null,
405
+ },
406
+ accessToken: session.accessToken,
407
+ expiresAt: session.expiresAt,
408
+ });
409
+ this.activeAuthuser = authuser;
410
+ await this.writeActiveAuthuser(authuser);
411
+ }
455
412
  // Setup auto-refresh if enabled
456
413
  if (this.config.autoRefresh && session.expiresAt) {
457
- this.setupTokenRefresh(session.expiresAt);
414
+ this.setupCookieRefresh(session.expiresAt, authuser);
458
415
  }
459
416
  // Notify listeners
460
417
  this.notifyListeners();
461
418
  }
462
- /**
463
- * Setup automatic token refresh.
464
- */
465
- setupTokenRefresh(expiresAt) {
466
- if (this.refreshTimer) {
467
- clearTimeout(this.refreshTimer);
468
- }
469
- const expiresAtMs = new Date(expiresAt).getTime();
470
- const now = Date.now();
471
- const refreshAt = expiresAtMs - this.config.refreshBuffer;
472
- const delay = Math.max(0, refreshAt - now);
473
- if (delay > 0) {
474
- this.refreshTimer = setTimeout(() => {
475
- this.refreshToken().catch(() => {
476
- // Refresh failed, user will need to re-auth
477
- });
478
- }, delay);
479
- }
480
- }
481
419
  /**
482
420
  * Refresh the access token. Deduplicates concurrent calls so only one
483
- * refresh request is in-flight at a time.
421
+ * refresh request is in-flight at a time. The only refresh authority is the
422
+ * active httpOnly refresh-cookie slot; this method never reads access tokens
423
+ * from storage.
484
424
  */
485
425
  async refreshToken() {
486
426
  // If a refresh is already in-flight, return the same promise
@@ -496,121 +436,21 @@ class AuthManager {
496
436
  }
497
437
  }
498
438
  async _doRefreshToken() {
499
- // Reset the cross-tab flag before starting
500
- this._otherTabRefreshed = false;
501
- // Get session info to find sessionId for token refresh
502
- const sessionJson = await this.storage.getItem(STORAGE_KEYS.SESSION);
503
- if (!sessionJson) {
504
- return false;
505
- }
506
- let sessionId;
507
- try {
508
- const session = JSON.parse(sessionJson);
509
- sessionId = session.sessionId;
510
- if (!sessionId)
511
- return false;
512
- }
513
- catch (err) {
514
- console.error('AuthManager: Failed to parse session from storage.', err);
515
- return false;
516
- }
517
- // Record the token we know about before attempting refresh
518
- const tokenBeforeRefresh = this._lastKnownAccessToken;
519
- // Broadcast that we're starting a refresh (informational for other tabs)
520
- this._broadcast({ type: 'refresh_starting', sessionId, timestamp: Date.now() });
521
439
  try {
522
- await (0, asyncUtils_1.retryAsync)(async () => {
523
- // Before each attempt, check if another tab already refreshed
524
- if (this._otherTabRefreshed) {
525
- const adoptedToken = await this.storage.getItem(STORAGE_KEYS.ACCESS_TOKEN);
526
- if (adoptedToken && adoptedToken !== tokenBeforeRefresh) {
527
- // Another tab succeeded. Adopt its tokens and short-circuit.
528
- this._lastKnownAccessToken = adoptedToken;
529
- this.oxyServices.httpService.setTokens(adoptedToken);
530
- return;
531
- }
532
- }
533
- const httpService = this.oxyServices.httpService;
534
- // Use session-based token endpoint which handles auto-refresh server-side
535
- const response = await httpService.request({
536
- method: 'GET',
537
- url: `/session/token/${sessionId}`,
538
- cache: false,
539
- retry: false,
540
- });
541
- if (!response.accessToken) {
542
- throw new Error('No access token in refresh response');
543
- }
544
- // Update access token in storage and HTTP client
545
- this._lastKnownAccessToken = response.accessToken;
546
- await this.storage.setItem(STORAGE_KEYS.ACCESS_TOKEN, response.accessToken);
547
- this.oxyServices.httpService.setTokens(response.accessToken);
548
- // Update session expiry and schedule next refresh
549
- if (response.expiresAt) {
550
- try {
551
- const session = JSON.parse(sessionJson);
552
- session.expiresAt = response.expiresAt;
553
- await this.storage.setItem(STORAGE_KEYS.SESSION, JSON.stringify(session));
554
- }
555
- catch (err) {
556
- // Ignore parse errors for session update, but log for debugging.
557
- console.error('AuthManager: Failed to re-save session after token refresh.', err);
558
- }
559
- if (this.config.autoRefresh) {
560
- this.setupTokenRefresh(response.expiresAt);
561
- }
562
- }
563
- // Broadcast success so other tabs can adopt these tokens
564
- this._broadcast({ type: 'tokens_refreshed', sessionId, timestamp: Date.now() });
565
- }, 2, // 2 retries = 3 total attempts
566
- 1000, // 1s base delay with exponential backoff + jitter
567
- (error) => {
568
- // Don't retry on 4xx client errors (invalid/revoked token)
569
- const status = error?.status ?? error?.response?.status;
570
- if (status && status >= 400 && status < 500)
571
- return false;
440
+ if (this.activeAuthuser !== null) {
441
+ await this.switchAuthuser(this.activeAuthuser);
572
442
  return true;
573
- });
574
- return true;
443
+ }
444
+ const restored = await this.restoreFromCookies();
445
+ return restored.accounts.length > 0;
575
446
  }
576
447
  catch {
577
- // All retry attempts exhausted. Before clearing the session, check if
578
- // another tab managed to refresh successfully while we were retrying.
579
- // Since all tabs share the same storage (localStorage), a successful
580
- // refresh from another tab will have written a different access token.
581
- const currentStoredToken = await this.storage.getItem(STORAGE_KEYS.ACCESS_TOKEN);
582
- if (currentStoredToken && currentStoredToken !== tokenBeforeRefresh) {
583
- // Another tab refreshed successfully. Adopt its tokens instead of logging out.
584
- this._lastKnownAccessToken = currentStoredToken;
585
- this.oxyServices.httpService.setTokens(currentStoredToken);
586
- // Restore user from storage in case it was updated
587
- const userJson = await this.storage.getItem(STORAGE_KEYS.USER);
588
- if (userJson) {
589
- try {
590
- this.currentUser = JSON.parse(userJson);
591
- }
592
- catch {
593
- // Ignore parse errors
594
- }
595
- }
596
- // Re-read session expiry and schedule next refresh
597
- const updatedSessionJson = await this.storage.getItem(STORAGE_KEYS.SESSION);
598
- if (updatedSessionJson) {
599
- try {
600
- const session = JSON.parse(updatedSessionJson);
601
- if (session.expiresAt && this.config.autoRefresh) {
602
- this.setupTokenRefresh(session.expiresAt);
603
- }
604
- }
605
- catch {
606
- // Ignore parse errors
607
- }
608
- }
609
- return true;
610
- }
611
- // No other tab rescued us -- truly clear the session
612
448
  await this.clearSession();
613
449
  this.currentUser = null;
450
+ this.accounts.clear();
451
+ this.activeAuthuser = null;
452
+ this._lastKnownAccessToken = null;
453
+ this.oxyServices.httpService.setTokens('');
614
454
  this.notifyListeners();
615
455
  return false;
616
456
  }
@@ -625,15 +465,9 @@ class AuthManager {
625
465
  this.refreshTimer = null;
626
466
  }
627
467
  this.refreshPromise = null;
628
- // Invalidate current session on the server (best-effort)
468
+ // Invalidate current cookie-backed sessions on the server (best-effort)
629
469
  try {
630
- const sessionJson = await this.storage.getItem(STORAGE_KEYS.SESSION);
631
- if (sessionJson) {
632
- const session = JSON.parse(sessionJson);
633
- if (session.sessionId && typeof this.oxyServices.logoutSession === 'function') {
634
- await this.oxyServices.logoutSession(session.sessionId);
635
- }
636
- }
470
+ await this.signOutAllViaCookies();
637
471
  }
638
472
  catch {
639
473
  // Best-effort: don't block local signout on network failure
@@ -653,22 +487,17 @@ class AuthManager {
653
487
  this._lastKnownAccessToken = null;
654
488
  // Clear storage
655
489
  await this.clearSession();
656
- // Notify other tabs so they also sign out
657
- this._broadcast({ type: 'signed_out', timestamp: Date.now() });
658
490
  // Update state and notify
659
491
  this.currentUser = null;
660
492
  this.notifyListeners();
661
493
  }
662
494
  /**
663
- * Clear session data from storage.
495
+ * Clear local cookie-path state. The only persisted AuthManager value is the
496
+ * active numeric slot; tokens and user objects are intentionally memory-only.
664
497
  */
665
498
  async clearSession() {
666
- await this.storage.removeItem(STORAGE_KEYS.ACCESS_TOKEN);
667
- await this.storage.removeItem(STORAGE_KEYS.REFRESH_TOKEN);
668
- await this.storage.removeItem(STORAGE_KEYS.SESSION);
669
- await this.storage.removeItem(STORAGE_KEYS.USER);
670
- await this.storage.removeItem(STORAGE_KEYS.AUTH_METHOD);
671
- await this.storage.removeItem(STORAGE_KEYS.FEDCM_LOGIN_HINT);
499
+ await this.clearActiveAuthuser();
500
+ this.currentAuthMethod = null;
672
501
  }
673
502
  /**
674
503
  * Get current user.
@@ -683,17 +512,11 @@ class AuthManager {
683
512
  return this.currentUser !== null;
684
513
  }
685
514
  /**
686
- * Get a valid access token, refreshing automatically if expired or expiring soon.
515
+ * Get a valid access token, refreshing automatically if expired or expiring
516
+ * soon. The token is read from memory only.
687
517
  */
688
518
  async getAccessToken() {
689
- // In cookieOnly / cookie-restore flows the active access token lives only in
690
- // memory (`_lastKnownAccessToken` + httpService) and is intentionally never
691
- // written to JS storage — the cookieOnly contract forbids persisting tokens
692
- // in JS-accessible storage. Fall back to the in-memory token when storage has
693
- // none, otherwise getAccessToken returns null after every cold-boot/reload and
694
- // standalone API clients (e.g. the Console axios client) send no Authorization
695
- // header → 401 on every authed endpoint while `isAuthenticated` is still true.
696
- const token = (await this.storage.getItem(STORAGE_KEYS.ACCESS_TOKEN)) ?? this._lastKnownAccessToken;
519
+ const token = this._lastKnownAccessToken;
697
520
  if (!token)
698
521
  return null;
699
522
  try {
@@ -704,9 +527,7 @@ class AuthManager {
704
527
  if (decoded.exp - now < buffer) {
705
528
  const refreshed = await this.refreshToken();
706
529
  if (refreshed) {
707
- // refreshToken() updates both storage and `_lastKnownAccessToken`;
708
- // prefer storage but fall back to memory for the cookieOnly path.
709
- return (await this.storage.getItem(STORAGE_KEYS.ACCESS_TOKEN)) ?? this._lastKnownAccessToken;
530
+ return this._lastKnownAccessToken;
710
531
  }
711
532
  }
712
533
  }
@@ -720,95 +541,38 @@ class AuthManager {
720
541
  * Get the auth method used for current session.
721
542
  */
722
543
  async getAuthMethod() {
723
- const method = await this.storage.getItem(STORAGE_KEYS.AUTH_METHOD);
724
- return method;
544
+ return this.currentAuthMethod;
725
545
  }
726
546
  /**
727
547
  * Initialize auth state on app startup.
728
548
  *
729
- * Order of operations:
730
- * 1. Try the cookie path via `restoreFromCookies()`. This is the
731
- * preferred path because the httpOnly refresh cookies are
732
- * cross-tab, persist across hard reloads, and don't expose any
733
- * refresh-token material to JS.
734
- * 2. If the cookie path yielded zero accounts AND `cookieOnly` is
735
- * `false`, fall back to the legacy localStorage path
736
- * (`oxy_access_token` / `oxy_session`) for backwards compatibility
737
- * with apps that haven't migrated to the cookie endpoint yet.
738
- * 3. If `cookieOnly` is `true`, skip the legacy fallback entirely.
739
- * This guarantees no tokens or refresh tokens are ever read from
740
- * or written to JS-accessible storage.
549
+ * Only the cookie path is authoritative. `restoreFromCookies()` refreshes
550
+ * the httpOnly `oxy_rt_${authuser}` slots through `/auth/refresh-all`,
551
+ * plants the active access token in memory, and returns the active user.
552
+ * No access token, refresh token, or session JSON is read from localStorage.
741
553
  *
742
- * Returns the active user on success, or `null` when neither path
743
- * restored a session.
554
+ * Returns the active user on success, or `null` when no cookie-backed
555
+ * account was restored.
744
556
  */
745
557
  async initialize(options = {}) {
746
- // 1. Cookie path (preferred). Forward the optional cold-boot fail-fast
747
- // timeout so a cross-domain stall cannot hang provider init.
748
558
  const cookieResult = await this.restoreFromCookies(options);
749
559
  if (cookieResult.accounts.length > 0) {
750
560
  return this.currentUser;
751
561
  }
752
- // 2. Legacy localStorage path (opt-out via `cookieOnly`).
753
- if (this.config.cookieOnly) {
754
- return null;
755
- }
756
- try {
757
- // Try to restore user from storage
758
- const userJson = await this.storage.getItem(STORAGE_KEYS.USER);
759
- if (userJson) {
760
- this.currentUser = JSON.parse(userJson);
761
- }
762
- // Restore token to HTTP client
763
- const token = await this.storage.getItem(STORAGE_KEYS.ACCESS_TOKEN);
764
- if (token) {
765
- this._lastKnownAccessToken = token;
766
- this.oxyServices.httpService.setTokens(token);
767
- }
768
- // Check session expiry
769
- const sessionJson = await this.storage.getItem(STORAGE_KEYS.SESSION);
770
- if (sessionJson) {
771
- const session = JSON.parse(sessionJson);
772
- if (session.expiresAt) {
773
- const expiresAt = new Date(session.expiresAt).getTime();
774
- if (expiresAt <= Date.now()) {
775
- // Session expired, try refresh
776
- const refreshed = await this.refreshToken();
777
- if (!refreshed) {
778
- await this.clearSession();
779
- this.currentUser = null;
780
- }
781
- }
782
- else if (this.config.autoRefresh) {
783
- // Setup refresh timer
784
- this.setupTokenRefresh(session.expiresAt);
785
- }
786
- }
787
- }
788
- return this.currentUser;
789
- }
790
- catch {
791
- // Failed to restore, start fresh
792
- await this.clearSession();
793
- this.currentUser = null;
794
- return null;
795
- }
562
+ return null;
796
563
  }
797
564
  // -------------------------------------------------------------------------
798
565
  // Multi-account cookie path (Google-style multi-sign-in).
799
566
  // -------------------------------------------------------------------------
800
- // The cookie path is web-only and orthogonal to the legacy bearer path
801
- // above: it never touches the `oxy_access_token` / `oxy_refresh_token` /
567
+ // The cookie path is web-only. It never touches the retired
568
+ // `oxy_access_token` / `oxy_refresh_token` /
802
569
  // `oxy_session` localStorage keys, because the refresh token lives in the
803
570
  // httpOnly `oxy_rt_${authuser}` cookies and access tokens live in
804
571
  // `this.accounts` (in-memory only). The only localStorage key the cookie
805
572
  // path writes is `STORAGE_KEYS.ACTIVE_AUTHUSER` — a small integer that is
806
573
  // explicitly NOT a secret.
807
574
  //
808
- // Apps that want to opt out of the legacy localStorage path entirely
809
- // (recommended for new web apps) pass `cookieOnly: true` to the
810
- // AuthManager config; in that mode `initialize()` ONLY uses the cookie
811
- // path.
575
+ // `initialize()` only uses the cookie path.
812
576
  // -------------------------------------------------------------------------
813
577
  /**
814
578
  * Read the persisted active `authuser` slot index. Returns `null` when
@@ -859,9 +623,8 @@ class AuthManager {
859
623
  }
860
624
  /**
861
625
  * Build a `MinimalUserData` from a `RefreshAllAccount`. Returns `null` when
862
- * the wire entry has no user shape (legacy `/auth/refresh` fallback) the
863
- * AuthManager's caller is expected to hydrate via `/users/me` in that
864
- * case.
626
+ * the wire entry has no user shape; the AuthManager's caller is expected to
627
+ * hydrate via `/users/me` in that case.
865
628
  */
866
629
  static toMinimalUser(account) {
867
630
  if (!account.user)
@@ -874,8 +637,8 @@ class AuthManager {
874
637
  }
875
638
  /**
876
639
  * Hydrate the user shape for a slot whose AuthManagerAccount currently has
877
- * `user: null` (legacy refresh fallback, or a switch onto a previously
878
- * unknown slot). Calls `/users/me` with the slot's freshly-planted access
640
+ * `user: null` (for example, a switch onto a previously unknown slot). Calls
641
+ * `/users/me` with the slot's freshly-planted access
879
642
  * token already on the HTTP client; merges the result back into the
880
643
  * registry entry. Network failures are non-fatal — the slot remains with
881
644
  * `user: null` and the UI is expected to render the public-key fallback
@@ -947,9 +710,7 @@ class AuthManager {
947
710
  * Calls `oxyServices.refreshAllSessions()` (`POST /auth/refresh-all` with
948
711
  * `credentials: 'include'`). The server rotates every presented
949
712
  * `oxy_rt_${authuser}` cookie in parallel and returns one entry per
950
- * VALID slot. The SDK transparently falls back to the legacy single-slot
951
- * `/auth/refresh` against older servers (handled inside
952
- * `refreshAllSessions`).
713
+ * valid slot.
953
714
  *
954
715
  * Plants the active account's access token on the shared HTTP client;
955
716
  * sibling slots' tokens stay in the in-memory registry so a later
@@ -1030,10 +791,10 @@ class AuthManager {
1030
791
  if (this.config.autoRefresh) {
1031
792
  this.setupCookieRefresh(activeAccount.expiresAt, active);
1032
793
  }
1033
- // The legacy /auth/refresh fallback yields user=null for the active
1034
- // slot. Schedule a /users/me hydration so the chooser isn't stuck on
1035
- // the public-key handle. Hydration is fire-and-forget — the snapshot
1036
- // is already considered "restored" once the access token is planted.
794
+ // If the active slot has no user shape, schedule a /users/me hydration so
795
+ // the chooser isn't stuck on the public-key handle. Hydration is
796
+ // fire-and-forget — the snapshot is already considered "restored" once
797
+ // the access token is planted.
1037
798
  if (activeAccount.user === null) {
1038
799
  slotsNeedingHydration.push(activeAccount.authuser);
1039
800
  }
@@ -1184,6 +945,7 @@ class AuthManager {
1184
945
  this._lastKnownAccessToken = null;
1185
946
  this.oxyServices.httpService.setTokens('');
1186
947
  this.currentUser = null;
948
+ this.currentAuthMethod = null;
1187
949
  await this.clearActiveAuthuser();
1188
950
  }
1189
951
  }
@@ -1195,10 +957,9 @@ class AuthManager {
1195
957
  *
1196
958
  * Calls `oxyServices.logoutAllSessionsViaCookie()`: server-side revokes
1197
959
  * every presented family and `Set-Cookie`s an immediate expiry for every
1198
- * recognised `oxy_rt_${n}` slot AND the legacy `oxy_rt` cookie. The
1199
- * in-memory registry is wiped, the active slot is cleared, and the
1200
- * persisted `oxy_active_authuser` is removed so the next cold boot
1201
- * starts fresh.
960
+ * recognised `oxy_rt_${n}` slot. The in-memory registry is wiped, the active
961
+ * slot is cleared, and the persisted `oxy_active_authuser` is removed so the
962
+ * next cold boot starts fresh.
1202
963
  */
1203
964
  async signOutAllViaCookies() {
1204
965
  try {
@@ -1212,6 +973,7 @@ class AuthManager {
1212
973
  this._lastKnownAccessToken = null;
1213
974
  this.oxyServices.httpService.setTokens('');
1214
975
  this.currentUser = null;
976
+ this.currentAuthMethod = null;
1215
977
  this._lastRestoreAt.clear();
1216
978
  await this.clearActiveAuthuser();
1217
979
  // Also clear the refresh timer that the cookie path may have scheduled.
@@ -1223,9 +985,8 @@ class AuthManager {
1223
985
  this.notifyListeners();
1224
986
  }
1225
987
  /**
1226
- * Schedule an auto-refresh for the cookie path on the active slot. Reuses
1227
- * the same single `refreshTimer` as the legacy path (the AuthManager has
1228
- * exactly ONE active slot at a time, so one timer suffices).
988
+ * Schedule an auto-refresh for the cookie path on the active slot. The
989
+ * AuthManager has exactly one active slot at a time, so one timer suffices.
1229
990
  */
1230
991
  setupCookieRefresh(expiresAt, authuser) {
1231
992
  if (this.refreshTimer) {
@@ -1264,6 +1025,17 @@ class AuthManager {
1264
1025
  return null;
1265
1026
  }
1266
1027
  }
1028
+ static decodeAuthuserFromAccessToken(token) {
1029
+ try {
1030
+ const decoded = (0, jwt_decode_1.jwtDecode)(token);
1031
+ return typeof decoded.authuser === 'number' && Number.isFinite(decoded.authuser) && decoded.authuser >= 0
1032
+ ? decoded.authuser
1033
+ : null;
1034
+ }
1035
+ catch {
1036
+ return null;
1037
+ }
1038
+ }
1267
1039
  /**
1268
1040
  * Destroy the auth manager and clean up resources.
1269
1041
  */