@oxyhq/core 3.4.1 → 3.4.2

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
@@ -6,18 +6,8 @@
6
6
  *
7
7
  * @module core/AuthManager
8
8
  */
9
- import { retryAsync } from './utils/asyncUtils.js';
10
9
  import { jwtDecode } from 'jwt-decode';
11
- /**
12
- * Storage keys used by AuthManager.
13
- */
14
10
  const STORAGE_KEYS = {
15
- ACCESS_TOKEN: 'oxy_access_token',
16
- REFRESH_TOKEN: 'oxy_refresh_token',
17
- SESSION: 'oxy_session',
18
- USER: 'oxy_user',
19
- AUTH_METHOD: 'oxy_auth_method',
20
- FEDCM_LOGIN_HINT: 'oxy_fedcm_login_hint',
21
11
  /**
22
12
  * Persisted active `authuser` slot index for the cookie path. Stores ONLY
23
13
  * the integer slot index (e.g. `"0"`, `"1"`), never a token or session
@@ -108,14 +98,13 @@ export class AuthManager {
108
98
  constructor(oxyServices, config = {}) {
109
99
  this.listeners = new Set();
110
100
  this.currentUser = null;
101
+ this.currentAuthMethod = null;
111
102
  this.refreshTimer = null;
112
103
  this.refreshPromise = null;
113
104
  /** Tracks the access token this instance last knew about, for cross-tab adoption. */
114
105
  this._lastKnownAccessToken = null;
115
106
  /** BroadcastChannel for coordinating token refreshes across browser tabs. */
116
107
  this._broadcastChannel = null;
117
- /** Set to true when another tab broadcasts a successful refresh, so this tab can skip its own. */
118
- this._otherTabRefreshed = false;
119
108
  /**
120
109
  * Identifier for this AuthManager instance (≈ "this tab"). Random hex
121
110
  * generated at construction; advertised in every outgoing broadcast and
@@ -181,14 +170,12 @@ export class AuthManager {
181
170
  autoRefresh: config.autoRefresh ?? true,
182
171
  refreshBuffer: config.refreshBuffer ?? 5 * 60 * 1000, // 5 minutes
183
172
  crossTabSync,
184
- cookieOnly: config.cookieOnly ?? false,
185
173
  };
186
174
  this.storage = this.config.storage;
187
- // Persist tokens to storage when HttpService refreshes them automatically
188
- this.oxyServices.httpService.onTokenRefreshed = (accessToken) => {
189
- this._lastKnownAccessToken = accessToken;
190
- this.storage.setItem(STORAGE_KEYS.ACCESS_TOKEN, accessToken);
191
- };
175
+ this.oxyServices.httpService.setAuthRefreshHandler(async () => {
176
+ const refreshed = await this.refreshToken();
177
+ return refreshed ? this._lastKnownAccessToken : null;
178
+ });
192
179
  // Setup cross-tab coordination in browser environments
193
180
  if (this.config.crossTabSync) {
194
181
  this._initBroadcastChannel();
@@ -221,43 +208,6 @@ export class AuthManager {
221
208
  if (!this._acceptBroadcast(message))
222
209
  return;
223
210
  switch (message.type) {
224
- case 'tokens_refreshed': {
225
- // Another tab successfully refreshed. Signal to cancel our pending refresh.
226
- this._otherTabRefreshed = true;
227
- // Adopt the new tokens from shared storage
228
- const newToken = await this.storage.getItem(STORAGE_KEYS.ACCESS_TOKEN);
229
- if (newToken && newToken !== this._lastKnownAccessToken) {
230
- this._lastKnownAccessToken = newToken;
231
- this.oxyServices.httpService.setTokens(newToken);
232
- // Re-read session for updated expiry and schedule next refresh
233
- const sessionJson = await this.storage.getItem(STORAGE_KEYS.SESSION);
234
- if (sessionJson) {
235
- try {
236
- const session = JSON.parse(sessionJson);
237
- if (session.expiresAt && this.config.autoRefresh) {
238
- this.setupTokenRefresh(session.expiresAt);
239
- }
240
- }
241
- catch {
242
- // Ignore parse errors
243
- }
244
- }
245
- }
246
- break;
247
- }
248
- case 'signed_out': {
249
- // Another tab signed out. Clear our local state to stay consistent.
250
- if (this.refreshTimer) {
251
- clearTimeout(this.refreshTimer);
252
- this.refreshTimer = null;
253
- }
254
- this.refreshPromise = null;
255
- this._lastKnownAccessToken = null;
256
- this.oxyServices.httpService.setTokens('');
257
- this.currentUser = null;
258
- this.notifyListeners();
259
- break;
260
- }
261
211
  case 'accounts_restored':
262
212
  case 'authuser_switched':
263
213
  case 'authuser_signed_out': {
@@ -279,7 +229,7 @@ export class AuthManager {
279
229
  break;
280
230
  }
281
231
  case 'all_signed_out': {
282
- // Mirror `signed_out` but also wipe the cookie-path registry.
232
+ // Wipe the cookie-path registry after another tab signed every slot out.
283
233
  if (this.refreshTimer) {
284
234
  clearTimeout(this.refreshTimer);
285
235
  this.refreshTimer = null;
@@ -290,10 +240,10 @@ export class AuthManager {
290
240
  this._lastKnownAccessToken = null;
291
241
  this.oxyServices.httpService.setTokens('');
292
242
  this.currentUser = null;
243
+ this.currentAuthMethod = null;
293
244
  this.notifyListeners();
294
245
  break;
295
246
  }
296
- // 'refresh_starting' is informational; we don't need to act on it currently
297
247
  }
298
248
  }
299
249
  /**
@@ -425,58 +375,48 @@ export class AuthManager {
425
375
  * @param method - Auth method used
426
376
  */
427
377
  async handleAuthSuccess(session, method = 'credentials') {
428
- // Store tokens
378
+ // Access tokens are memory-only. Fresh login responses plant the token on
379
+ // the HTTP client and the AuthManager registry, but never write it to JS
380
+ // storage. Durable web refresh lives in the httpOnly cookie set by the API.
429
381
  if (session.accessToken) {
430
382
  this._lastKnownAccessToken = session.accessToken;
431
- await this.storage.setItem(STORAGE_KEYS.ACCESS_TOKEN, session.accessToken);
432
383
  this.oxyServices.httpService.setTokens(session.accessToken);
433
384
  }
434
- // Store refresh token if available
435
- if (session.refreshToken) {
436
- await this.storage.setItem(STORAGE_KEYS.REFRESH_TOKEN, session.refreshToken);
437
- }
438
- // Store session info
439
- await this.storage.setItem(STORAGE_KEYS.SESSION, JSON.stringify({
440
- sessionId: session.sessionId,
441
- deviceId: session.deviceId,
442
- expiresAt: session.expiresAt,
443
- }));
444
- // Store user only if it has valid required fields (not an empty placeholder)
445
385
  if (session.user && typeof session.user.id === 'string' && session.user.id.length > 0) {
446
- await this.storage.setItem(STORAGE_KEYS.USER, JSON.stringify(session.user));
447
386
  this.currentUser = session.user;
448
387
  }
449
- // Store auth method
450
- await this.storage.setItem(STORAGE_KEYS.AUTH_METHOD, method);
388
+ this.currentAuthMethod = method;
389
+ const decodedAuthuser = session.accessToken
390
+ ? AuthManager.decodeAuthuserFromAccessToken(session.accessToken)
391
+ : null;
392
+ const authuser = decodedAuthuser ?? 0;
393
+ if (session.accessToken && session.sessionId) {
394
+ this.accounts.set(authuser, {
395
+ authuser,
396
+ sessionId: session.sessionId,
397
+ user: {
398
+ id: session.user.id,
399
+ username: session.user.username,
400
+ avatar: session.user.avatar ?? null,
401
+ },
402
+ accessToken: session.accessToken,
403
+ expiresAt: session.expiresAt,
404
+ });
405
+ this.activeAuthuser = authuser;
406
+ await this.writeActiveAuthuser(authuser);
407
+ }
451
408
  // Setup auto-refresh if enabled
452
409
  if (this.config.autoRefresh && session.expiresAt) {
453
- this.setupTokenRefresh(session.expiresAt);
410
+ this.setupCookieRefresh(session.expiresAt, authuser);
454
411
  }
455
412
  // Notify listeners
456
413
  this.notifyListeners();
457
414
  }
458
- /**
459
- * Setup automatic token refresh.
460
- */
461
- setupTokenRefresh(expiresAt) {
462
- if (this.refreshTimer) {
463
- clearTimeout(this.refreshTimer);
464
- }
465
- const expiresAtMs = new Date(expiresAt).getTime();
466
- const now = Date.now();
467
- const refreshAt = expiresAtMs - this.config.refreshBuffer;
468
- const delay = Math.max(0, refreshAt - now);
469
- if (delay > 0) {
470
- this.refreshTimer = setTimeout(() => {
471
- this.refreshToken().catch(() => {
472
- // Refresh failed, user will need to re-auth
473
- });
474
- }, delay);
475
- }
476
- }
477
415
  /**
478
416
  * Refresh the access token. Deduplicates concurrent calls so only one
479
- * refresh request is in-flight at a time.
417
+ * refresh request is in-flight at a time. The only refresh authority is the
418
+ * active httpOnly refresh-cookie slot; this method never reads access tokens
419
+ * from storage.
480
420
  */
481
421
  async refreshToken() {
482
422
  // If a refresh is already in-flight, return the same promise
@@ -492,121 +432,21 @@ export class AuthManager {
492
432
  }
493
433
  }
494
434
  async _doRefreshToken() {
495
- // Reset the cross-tab flag before starting
496
- this._otherTabRefreshed = false;
497
- // Get session info to find sessionId for token refresh
498
- const sessionJson = await this.storage.getItem(STORAGE_KEYS.SESSION);
499
- if (!sessionJson) {
500
- return false;
501
- }
502
- let sessionId;
503
- try {
504
- const session = JSON.parse(sessionJson);
505
- sessionId = session.sessionId;
506
- if (!sessionId)
507
- return false;
508
- }
509
- catch (err) {
510
- console.error('AuthManager: Failed to parse session from storage.', err);
511
- return false;
512
- }
513
- // Record the token we know about before attempting refresh
514
- const tokenBeforeRefresh = this._lastKnownAccessToken;
515
- // Broadcast that we're starting a refresh (informational for other tabs)
516
- this._broadcast({ type: 'refresh_starting', sessionId, timestamp: Date.now() });
517
435
  try {
518
- await retryAsync(async () => {
519
- // Before each attempt, check if another tab already refreshed
520
- if (this._otherTabRefreshed) {
521
- const adoptedToken = await this.storage.getItem(STORAGE_KEYS.ACCESS_TOKEN);
522
- if (adoptedToken && adoptedToken !== tokenBeforeRefresh) {
523
- // Another tab succeeded. Adopt its tokens and short-circuit.
524
- this._lastKnownAccessToken = adoptedToken;
525
- this.oxyServices.httpService.setTokens(adoptedToken);
526
- return;
527
- }
528
- }
529
- const httpService = this.oxyServices.httpService;
530
- // Use session-based token endpoint which handles auto-refresh server-side
531
- const response = await httpService.request({
532
- method: 'GET',
533
- url: `/session/token/${sessionId}`,
534
- cache: false,
535
- retry: false,
536
- });
537
- if (!response.accessToken) {
538
- throw new Error('No access token in refresh response');
539
- }
540
- // Update access token in storage and HTTP client
541
- this._lastKnownAccessToken = response.accessToken;
542
- await this.storage.setItem(STORAGE_KEYS.ACCESS_TOKEN, response.accessToken);
543
- this.oxyServices.httpService.setTokens(response.accessToken);
544
- // Update session expiry and schedule next refresh
545
- if (response.expiresAt) {
546
- try {
547
- const session = JSON.parse(sessionJson);
548
- session.expiresAt = response.expiresAt;
549
- await this.storage.setItem(STORAGE_KEYS.SESSION, JSON.stringify(session));
550
- }
551
- catch (err) {
552
- // Ignore parse errors for session update, but log for debugging.
553
- console.error('AuthManager: Failed to re-save session after token refresh.', err);
554
- }
555
- if (this.config.autoRefresh) {
556
- this.setupTokenRefresh(response.expiresAt);
557
- }
558
- }
559
- // Broadcast success so other tabs can adopt these tokens
560
- this._broadcast({ type: 'tokens_refreshed', sessionId, timestamp: Date.now() });
561
- }, 2, // 2 retries = 3 total attempts
562
- 1000, // 1s base delay with exponential backoff + jitter
563
- (error) => {
564
- // Don't retry on 4xx client errors (invalid/revoked token)
565
- const status = error?.status ?? error?.response?.status;
566
- if (status && status >= 400 && status < 500)
567
- return false;
436
+ if (this.activeAuthuser !== null) {
437
+ await this.switchAuthuser(this.activeAuthuser);
568
438
  return true;
569
- });
570
- return true;
439
+ }
440
+ const restored = await this.restoreFromCookies();
441
+ return restored.accounts.length > 0;
571
442
  }
572
443
  catch {
573
- // All retry attempts exhausted. Before clearing the session, check if
574
- // another tab managed to refresh successfully while we were retrying.
575
- // Since all tabs share the same storage (localStorage), a successful
576
- // refresh from another tab will have written a different access token.
577
- const currentStoredToken = await this.storage.getItem(STORAGE_KEYS.ACCESS_TOKEN);
578
- if (currentStoredToken && currentStoredToken !== tokenBeforeRefresh) {
579
- // Another tab refreshed successfully. Adopt its tokens instead of logging out.
580
- this._lastKnownAccessToken = currentStoredToken;
581
- this.oxyServices.httpService.setTokens(currentStoredToken);
582
- // Restore user from storage in case it was updated
583
- const userJson = await this.storage.getItem(STORAGE_KEYS.USER);
584
- if (userJson) {
585
- try {
586
- this.currentUser = JSON.parse(userJson);
587
- }
588
- catch {
589
- // Ignore parse errors
590
- }
591
- }
592
- // Re-read session expiry and schedule next refresh
593
- const updatedSessionJson = await this.storage.getItem(STORAGE_KEYS.SESSION);
594
- if (updatedSessionJson) {
595
- try {
596
- const session = JSON.parse(updatedSessionJson);
597
- if (session.expiresAt && this.config.autoRefresh) {
598
- this.setupTokenRefresh(session.expiresAt);
599
- }
600
- }
601
- catch {
602
- // Ignore parse errors
603
- }
604
- }
605
- return true;
606
- }
607
- // No other tab rescued us -- truly clear the session
608
444
  await this.clearSession();
609
445
  this.currentUser = null;
446
+ this.accounts.clear();
447
+ this.activeAuthuser = null;
448
+ this._lastKnownAccessToken = null;
449
+ this.oxyServices.httpService.setTokens('');
610
450
  this.notifyListeners();
611
451
  return false;
612
452
  }
@@ -621,15 +461,9 @@ export class AuthManager {
621
461
  this.refreshTimer = null;
622
462
  }
623
463
  this.refreshPromise = null;
624
- // Invalidate current session on the server (best-effort)
464
+ // Invalidate current cookie-backed sessions on the server (best-effort)
625
465
  try {
626
- const sessionJson = await this.storage.getItem(STORAGE_KEYS.SESSION);
627
- if (sessionJson) {
628
- const session = JSON.parse(sessionJson);
629
- if (session.sessionId && typeof this.oxyServices.logoutSession === 'function') {
630
- await this.oxyServices.logoutSession(session.sessionId);
631
- }
632
- }
466
+ await this.signOutAllViaCookies();
633
467
  }
634
468
  catch {
635
469
  // Best-effort: don't block local signout on network failure
@@ -649,22 +483,17 @@ export class AuthManager {
649
483
  this._lastKnownAccessToken = null;
650
484
  // Clear storage
651
485
  await this.clearSession();
652
- // Notify other tabs so they also sign out
653
- this._broadcast({ type: 'signed_out', timestamp: Date.now() });
654
486
  // Update state and notify
655
487
  this.currentUser = null;
656
488
  this.notifyListeners();
657
489
  }
658
490
  /**
659
- * Clear session data from storage.
491
+ * Clear local cookie-path state. The only persisted AuthManager value is the
492
+ * active numeric slot; tokens and user objects are intentionally memory-only.
660
493
  */
661
494
  async clearSession() {
662
- await this.storage.removeItem(STORAGE_KEYS.ACCESS_TOKEN);
663
- await this.storage.removeItem(STORAGE_KEYS.REFRESH_TOKEN);
664
- await this.storage.removeItem(STORAGE_KEYS.SESSION);
665
- await this.storage.removeItem(STORAGE_KEYS.USER);
666
- await this.storage.removeItem(STORAGE_KEYS.AUTH_METHOD);
667
- await this.storage.removeItem(STORAGE_KEYS.FEDCM_LOGIN_HINT);
495
+ await this.clearActiveAuthuser();
496
+ this.currentAuthMethod = null;
668
497
  }
669
498
  /**
670
499
  * Get current user.
@@ -679,17 +508,11 @@ export class AuthManager {
679
508
  return this.currentUser !== null;
680
509
  }
681
510
  /**
682
- * Get a valid access token, refreshing automatically if expired or expiring soon.
511
+ * Get a valid access token, refreshing automatically if expired or expiring
512
+ * soon. The token is read from memory only.
683
513
  */
684
514
  async getAccessToken() {
685
- // In cookieOnly / cookie-restore flows the active access token lives only in
686
- // memory (`_lastKnownAccessToken` + httpService) and is intentionally never
687
- // written to JS storage — the cookieOnly contract forbids persisting tokens
688
- // in JS-accessible storage. Fall back to the in-memory token when storage has
689
- // none, otherwise getAccessToken returns null after every cold-boot/reload and
690
- // standalone API clients (e.g. the Console axios client) send no Authorization
691
- // header → 401 on every authed endpoint while `isAuthenticated` is still true.
692
- const token = (await this.storage.getItem(STORAGE_KEYS.ACCESS_TOKEN)) ?? this._lastKnownAccessToken;
515
+ const token = this._lastKnownAccessToken;
693
516
  if (!token)
694
517
  return null;
695
518
  try {
@@ -700,9 +523,7 @@ export class AuthManager {
700
523
  if (decoded.exp - now < buffer) {
701
524
  const refreshed = await this.refreshToken();
702
525
  if (refreshed) {
703
- // refreshToken() updates both storage and `_lastKnownAccessToken`;
704
- // prefer storage but fall back to memory for the cookieOnly path.
705
- return (await this.storage.getItem(STORAGE_KEYS.ACCESS_TOKEN)) ?? this._lastKnownAccessToken;
526
+ return this._lastKnownAccessToken;
706
527
  }
707
528
  }
708
529
  }
@@ -716,95 +537,38 @@ export class AuthManager {
716
537
  * Get the auth method used for current session.
717
538
  */
718
539
  async getAuthMethod() {
719
- const method = await this.storage.getItem(STORAGE_KEYS.AUTH_METHOD);
720
- return method;
540
+ return this.currentAuthMethod;
721
541
  }
722
542
  /**
723
543
  * Initialize auth state on app startup.
724
544
  *
725
- * Order of operations:
726
- * 1. Try the cookie path via `restoreFromCookies()`. This is the
727
- * preferred path because the httpOnly refresh cookies are
728
- * cross-tab, persist across hard reloads, and don't expose any
729
- * refresh-token material to JS.
730
- * 2. If the cookie path yielded zero accounts AND `cookieOnly` is
731
- * `false`, fall back to the legacy localStorage path
732
- * (`oxy_access_token` / `oxy_session`) for backwards compatibility
733
- * with apps that haven't migrated to the cookie endpoint yet.
734
- * 3. If `cookieOnly` is `true`, skip the legacy fallback entirely.
735
- * This guarantees no tokens or refresh tokens are ever read from
736
- * or written to JS-accessible storage.
545
+ * Only the cookie path is authoritative. `restoreFromCookies()` refreshes
546
+ * the httpOnly `oxy_rt_${authuser}` slots through `/auth/refresh-all`,
547
+ * plants the active access token in memory, and returns the active user.
548
+ * No access token, refresh token, or session JSON is read from localStorage.
737
549
  *
738
- * Returns the active user on success, or `null` when neither path
739
- * restored a session.
550
+ * Returns the active user on success, or `null` when no cookie-backed
551
+ * account was restored.
740
552
  */
741
553
  async initialize(options = {}) {
742
- // 1. Cookie path (preferred). Forward the optional cold-boot fail-fast
743
- // timeout so a cross-domain stall cannot hang provider init.
744
554
  const cookieResult = await this.restoreFromCookies(options);
745
555
  if (cookieResult.accounts.length > 0) {
746
556
  return this.currentUser;
747
557
  }
748
- // 2. Legacy localStorage path (opt-out via `cookieOnly`).
749
- if (this.config.cookieOnly) {
750
- return null;
751
- }
752
- try {
753
- // Try to restore user from storage
754
- const userJson = await this.storage.getItem(STORAGE_KEYS.USER);
755
- if (userJson) {
756
- this.currentUser = JSON.parse(userJson);
757
- }
758
- // Restore token to HTTP client
759
- const token = await this.storage.getItem(STORAGE_KEYS.ACCESS_TOKEN);
760
- if (token) {
761
- this._lastKnownAccessToken = token;
762
- this.oxyServices.httpService.setTokens(token);
763
- }
764
- // Check session expiry
765
- const sessionJson = await this.storage.getItem(STORAGE_KEYS.SESSION);
766
- if (sessionJson) {
767
- const session = JSON.parse(sessionJson);
768
- if (session.expiresAt) {
769
- const expiresAt = new Date(session.expiresAt).getTime();
770
- if (expiresAt <= Date.now()) {
771
- // Session expired, try refresh
772
- const refreshed = await this.refreshToken();
773
- if (!refreshed) {
774
- await this.clearSession();
775
- this.currentUser = null;
776
- }
777
- }
778
- else if (this.config.autoRefresh) {
779
- // Setup refresh timer
780
- this.setupTokenRefresh(session.expiresAt);
781
- }
782
- }
783
- }
784
- return this.currentUser;
785
- }
786
- catch {
787
- // Failed to restore, start fresh
788
- await this.clearSession();
789
- this.currentUser = null;
790
- return null;
791
- }
558
+ return null;
792
559
  }
793
560
  // -------------------------------------------------------------------------
794
561
  // Multi-account cookie path (Google-style multi-sign-in).
795
562
  // -------------------------------------------------------------------------
796
- // The cookie path is web-only and orthogonal to the legacy bearer path
797
- // above: it never touches the `oxy_access_token` / `oxy_refresh_token` /
563
+ // The cookie path is web-only. It never touches the retired
564
+ // `oxy_access_token` / `oxy_refresh_token` /
798
565
  // `oxy_session` localStorage keys, because the refresh token lives in the
799
566
  // httpOnly `oxy_rt_${authuser}` cookies and access tokens live in
800
567
  // `this.accounts` (in-memory only). The only localStorage key the cookie
801
568
  // path writes is `STORAGE_KEYS.ACTIVE_AUTHUSER` — a small integer that is
802
569
  // explicitly NOT a secret.
803
570
  //
804
- // Apps that want to opt out of the legacy localStorage path entirely
805
- // (recommended for new web apps) pass `cookieOnly: true` to the
806
- // AuthManager config; in that mode `initialize()` ONLY uses the cookie
807
- // path.
571
+ // `initialize()` only uses the cookie path.
808
572
  // -------------------------------------------------------------------------
809
573
  /**
810
574
  * Read the persisted active `authuser` slot index. Returns `null` when
@@ -855,9 +619,8 @@ export class AuthManager {
855
619
  }
856
620
  /**
857
621
  * Build a `MinimalUserData` from a `RefreshAllAccount`. Returns `null` when
858
- * the wire entry has no user shape (legacy `/auth/refresh` fallback) the
859
- * AuthManager's caller is expected to hydrate via `/users/me` in that
860
- * case.
622
+ * the wire entry has no user shape; the AuthManager's caller is expected to
623
+ * hydrate via `/users/me` in that case.
861
624
  */
862
625
  static toMinimalUser(account) {
863
626
  if (!account.user)
@@ -870,8 +633,8 @@ export class AuthManager {
870
633
  }
871
634
  /**
872
635
  * Hydrate the user shape for a slot whose AuthManagerAccount currently has
873
- * `user: null` (legacy refresh fallback, or a switch onto a previously
874
- * unknown slot). Calls `/users/me` with the slot's freshly-planted access
636
+ * `user: null` (for example, a switch onto a previously unknown slot). Calls
637
+ * `/users/me` with the slot's freshly-planted access
875
638
  * token already on the HTTP client; merges the result back into the
876
639
  * registry entry. Network failures are non-fatal — the slot remains with
877
640
  * `user: null` and the UI is expected to render the public-key fallback
@@ -943,9 +706,7 @@ export class AuthManager {
943
706
  * Calls `oxyServices.refreshAllSessions()` (`POST /auth/refresh-all` with
944
707
  * `credentials: 'include'`). The server rotates every presented
945
708
  * `oxy_rt_${authuser}` cookie in parallel and returns one entry per
946
- * VALID slot. The SDK transparently falls back to the legacy single-slot
947
- * `/auth/refresh` against older servers (handled inside
948
- * `refreshAllSessions`).
709
+ * valid slot.
949
710
  *
950
711
  * Plants the active account's access token on the shared HTTP client;
951
712
  * sibling slots' tokens stay in the in-memory registry so a later
@@ -1026,10 +787,10 @@ export class AuthManager {
1026
787
  if (this.config.autoRefresh) {
1027
788
  this.setupCookieRefresh(activeAccount.expiresAt, active);
1028
789
  }
1029
- // The legacy /auth/refresh fallback yields user=null for the active
1030
- // slot. Schedule a /users/me hydration so the chooser isn't stuck on
1031
- // the public-key handle. Hydration is fire-and-forget — the snapshot
1032
- // is already considered "restored" once the access token is planted.
790
+ // If the active slot has no user shape, schedule a /users/me hydration so
791
+ // the chooser isn't stuck on the public-key handle. Hydration is
792
+ // fire-and-forget — the snapshot is already considered "restored" once
793
+ // the access token is planted.
1033
794
  if (activeAccount.user === null) {
1034
795
  slotsNeedingHydration.push(activeAccount.authuser);
1035
796
  }
@@ -1180,6 +941,7 @@ export class AuthManager {
1180
941
  this._lastKnownAccessToken = null;
1181
942
  this.oxyServices.httpService.setTokens('');
1182
943
  this.currentUser = null;
944
+ this.currentAuthMethod = null;
1183
945
  await this.clearActiveAuthuser();
1184
946
  }
1185
947
  }
@@ -1191,10 +953,9 @@ export class AuthManager {
1191
953
  *
1192
954
  * Calls `oxyServices.logoutAllSessionsViaCookie()`: server-side revokes
1193
955
  * every presented family and `Set-Cookie`s an immediate expiry for every
1194
- * recognised `oxy_rt_${n}` slot AND the legacy `oxy_rt` cookie. The
1195
- * in-memory registry is wiped, the active slot is cleared, and the
1196
- * persisted `oxy_active_authuser` is removed so the next cold boot
1197
- * starts fresh.
956
+ * recognised `oxy_rt_${n}` slot. The in-memory registry is wiped, the active
957
+ * slot is cleared, and the persisted `oxy_active_authuser` is removed so the
958
+ * next cold boot starts fresh.
1198
959
  */
1199
960
  async signOutAllViaCookies() {
1200
961
  try {
@@ -1208,6 +969,7 @@ export class AuthManager {
1208
969
  this._lastKnownAccessToken = null;
1209
970
  this.oxyServices.httpService.setTokens('');
1210
971
  this.currentUser = null;
972
+ this.currentAuthMethod = null;
1211
973
  this._lastRestoreAt.clear();
1212
974
  await this.clearActiveAuthuser();
1213
975
  // Also clear the refresh timer that the cookie path may have scheduled.
@@ -1219,9 +981,8 @@ export class AuthManager {
1219
981
  this.notifyListeners();
1220
982
  }
1221
983
  /**
1222
- * Schedule an auto-refresh for the cookie path on the active slot. Reuses
1223
- * the same single `refreshTimer` as the legacy path (the AuthManager has
1224
- * exactly ONE active slot at a time, so one timer suffices).
984
+ * Schedule an auto-refresh for the cookie path on the active slot. The
985
+ * AuthManager has exactly one active slot at a time, so one timer suffices.
1225
986
  */
1226
987
  setupCookieRefresh(expiresAt, authuser) {
1227
988
  if (this.refreshTimer) {
@@ -1260,6 +1021,17 @@ export class AuthManager {
1260
1021
  return null;
1261
1022
  }
1262
1023
  }
1024
+ static decodeAuthuserFromAccessToken(token) {
1025
+ try {
1026
+ const decoded = jwtDecode(token);
1027
+ return typeof decoded.authuser === 'number' && Number.isFinite(decoded.authuser) && decoded.authuser >= 0
1028
+ ? decoded.authuser
1029
+ : null;
1030
+ }
1031
+ catch {
1032
+ return null;
1033
+ }
1034
+ }
1263
1035
  /**
1264
1036
  * Destroy the auth manager and clean up resources.
1265
1037
  */