@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.
- package/dist/cjs/.tsbuildinfo +1 -1
- package/dist/cjs/AuthManager.js +91 -319
- package/dist/cjs/CrossDomainAuth.js +19 -106
- package/dist/cjs/HttpService.js +49 -73
- package/dist/cjs/OxyServices.base.js +2 -2
- package/dist/cjs/i18n/index.js +7 -1
- package/dist/cjs/i18n/locales/ar-SA.json +18 -2
- package/dist/cjs/i18n/locales/ca-ES.json +18 -2
- package/dist/cjs/i18n/locales/de-DE.json +18 -2
- package/dist/cjs/i18n/locales/en-US.json +16 -2
- package/dist/cjs/i18n/locales/es-ES.json +16 -2
- package/dist/cjs/i18n/locales/fr-FR.json +18 -2
- package/dist/cjs/i18n/locales/it-IT.json +18 -2
- package/dist/cjs/i18n/locales/ja-JP.json +18 -2
- package/dist/cjs/i18n/locales/ko-KR.json +18 -2
- package/dist/cjs/i18n/locales/locales/ar-SA.json +18 -2
- package/dist/cjs/i18n/locales/locales/ca-ES.json +18 -2
- package/dist/cjs/i18n/locales/locales/de-DE.json +18 -2
- package/dist/cjs/i18n/locales/locales/en-US.json +17 -3
- package/dist/cjs/i18n/locales/locales/es-ES.json +16 -2
- package/dist/cjs/i18n/locales/locales/fr-FR.json +18 -2
- package/dist/cjs/i18n/locales/locales/it-IT.json +18 -2
- package/dist/cjs/i18n/locales/locales/ja-JP.json +18 -2
- package/dist/cjs/i18n/locales/locales/ko-KR.json +18 -2
- package/dist/cjs/i18n/locales/locales/pt-PT.json +18 -2
- package/dist/cjs/i18n/locales/locales/zh-CN.json +18 -2
- package/dist/cjs/i18n/locales/pt-PT.json +18 -2
- package/dist/cjs/i18n/locales/zh-CN.json +18 -2
- package/dist/cjs/mixins/OxyServices.auth.js +20 -63
- package/dist/cjs/mixins/OxyServices.fedcm.js +10 -12
- package/dist/cjs/mixins/OxyServices.popup.js +50 -299
- package/dist/cjs/mixins/OxyServices.redirect.js +84 -348
- package/dist/cjs/mixins/OxyServices.silent.js +204 -0
- package/dist/cjs/mixins/OxyServices.sso.js +4 -5
- package/dist/cjs/mixins/OxyServices.utility.js +6 -15
- package/dist/cjs/mixins/index.js +5 -6
- package/dist/cjs/server/index.js +21 -0
- package/dist/cjs/server/rateLimit.js +77 -0
- package/dist/cjs/shared/utils/debugUtils.js +1 -1
- package/dist/cjs/utils/accountUtils.js +4 -4
- package/dist/cjs/utils/authHelpers.js +21 -15
- package/dist/cjs/utils/coldBoot.js +3 -3
- package/dist/cjs/utils/fapiAutoDetect.js +1 -1
- package/dist/esm/.tsbuildinfo +1 -1
- package/dist/esm/AuthManager.js +91 -319
- package/dist/esm/CrossDomainAuth.js +19 -106
- package/dist/esm/HttpService.js +49 -73
- package/dist/esm/OxyServices.base.js +2 -2
- package/dist/esm/i18n/index.js +7 -1
- package/dist/esm/i18n/locales/ar-SA.json +18 -2
- package/dist/esm/i18n/locales/ca-ES.json +18 -2
- package/dist/esm/i18n/locales/de-DE.json +18 -2
- package/dist/esm/i18n/locales/en-US.json +16 -2
- package/dist/esm/i18n/locales/es-ES.json +16 -2
- package/dist/esm/i18n/locales/fr-FR.json +18 -2
- package/dist/esm/i18n/locales/it-IT.json +18 -2
- package/dist/esm/i18n/locales/ja-JP.json +18 -2
- package/dist/esm/i18n/locales/ko-KR.json +18 -2
- package/dist/esm/i18n/locales/locales/ar-SA.json +18 -2
- package/dist/esm/i18n/locales/locales/ca-ES.json +18 -2
- package/dist/esm/i18n/locales/locales/de-DE.json +18 -2
- package/dist/esm/i18n/locales/locales/en-US.json +17 -3
- package/dist/esm/i18n/locales/locales/es-ES.json +16 -2
- package/dist/esm/i18n/locales/locales/fr-FR.json +18 -2
- package/dist/esm/i18n/locales/locales/it-IT.json +18 -2
- package/dist/esm/i18n/locales/locales/ja-JP.json +18 -2
- package/dist/esm/i18n/locales/locales/ko-KR.json +18 -2
- package/dist/esm/i18n/locales/locales/pt-PT.json +18 -2
- package/dist/esm/i18n/locales/locales/zh-CN.json +18 -2
- package/dist/esm/i18n/locales/pt-PT.json +18 -2
- package/dist/esm/i18n/locales/zh-CN.json +18 -2
- package/dist/esm/mixins/OxyServices.auth.js +20 -63
- package/dist/esm/mixins/OxyServices.fedcm.js +10 -12
- package/dist/esm/mixins/OxyServices.popup.js +52 -301
- package/dist/esm/mixins/OxyServices.redirect.js +84 -349
- package/dist/esm/mixins/OxyServices.silent.js +202 -0
- package/dist/esm/mixins/OxyServices.sso.js +4 -5
- package/dist/esm/mixins/OxyServices.utility.js +6 -15
- package/dist/esm/mixins/index.js +5 -6
- package/dist/esm/server/index.js +17 -0
- package/dist/esm/server/rateLimit.js +71 -0
- package/dist/esm/shared/utils/debugUtils.js +1 -1
- package/dist/esm/utils/accountUtils.js +4 -4
- package/dist/esm/utils/authHelpers.js +21 -15
- package/dist/esm/utils/coldBoot.js +3 -3
- package/dist/esm/utils/fapiAutoDetect.js +1 -1
- package/dist/types/.tsbuildinfo +1 -1
- package/dist/types/AuthManager.d.ts +26 -53
- package/dist/types/AuthManagerTypes.d.ts +5 -9
- package/dist/types/CrossDomainAuth.d.ts +13 -52
- package/dist/types/HttpService.d.ts +9 -8
- package/dist/types/OxyServices.base.d.ts +1 -1
- package/dist/types/OxyServices.d.ts +4 -10
- package/dist/types/index.d.ts +1 -1
- package/dist/types/mixins/OxyServices.analytics.d.ts +1 -1
- package/dist/types/mixins/OxyServices.appData.d.ts +1 -1
- package/dist/types/mixins/OxyServices.applications.d.ts +1 -1
- package/dist/types/mixins/OxyServices.assets.d.ts +1 -1
- package/dist/types/mixins/OxyServices.auth.d.ts +10 -31
- package/dist/types/mixins/OxyServices.contacts.d.ts +1 -1
- package/dist/types/mixins/OxyServices.devices.d.ts +1 -1
- package/dist/types/mixins/OxyServices.features.d.ts +1 -1
- package/dist/types/mixins/OxyServices.fedcm.d.ts +5 -5
- package/dist/types/mixins/OxyServices.language.d.ts +1 -1
- package/dist/types/mixins/OxyServices.location.d.ts +1 -1
- package/dist/types/mixins/OxyServices.managedAccounts.d.ts +1 -1
- package/dist/types/mixins/OxyServices.payment.d.ts +1 -1
- package/dist/types/mixins/OxyServices.popup.d.ts +18 -120
- package/dist/types/mixins/OxyServices.privacy.d.ts +1 -1
- package/dist/types/mixins/OxyServices.redirect.d.ts +13 -174
- package/dist/types/mixins/OxyServices.reputation.d.ts +1 -1
- package/dist/types/mixins/OxyServices.security.d.ts +1 -1
- package/dist/types/mixins/OxyServices.silent.d.ts +131 -0
- package/dist/types/mixins/OxyServices.sso.d.ts +4 -5
- package/dist/types/mixins/OxyServices.topics.d.ts +1 -1
- package/dist/types/mixins/OxyServices.user.d.ts +1 -1
- package/dist/types/mixins/OxyServices.utility.d.ts +3 -8
- package/dist/types/mixins/OxyServices.workspaces.d.ts +1 -1
- package/dist/types/mixins/index.d.ts +3 -3
- package/dist/types/models/interfaces.d.ts +5 -16
- package/dist/types/models/session.d.ts +0 -2
- package/dist/types/server/index.d.ts +18 -0
- package/dist/types/server/rateLimit.d.ts +40 -0
- package/dist/types/shared/utils/debugUtils.d.ts +1 -1
- package/dist/types/utils/authHelpers.d.ts +4 -3
- package/dist/types/utils/coldBoot.d.ts +2 -2
- package/dist/types/utils/fapiAutoDetect.d.ts +1 -1
- package/package.json +24 -2
- package/src/AuthManager.ts +100 -370
- package/src/AuthManagerTypes.ts +5 -9
- package/src/CrossDomainAuth.ts +22 -129
- package/src/HttpService.ts +55 -73
- package/src/OxyServices.base.ts +2 -3
- package/src/OxyServices.ts +9 -11
- package/src/__tests__/authManager.cookiePath.test.ts +19 -17
- package/src/__tests__/authManager.security.test.ts +7 -3
- package/src/__tests__/crossDomainAuth.test.ts +26 -118
- package/src/i18n/index.ts +7 -1
- package/src/i18n/locales/ar-SA.json +18 -2
- package/src/i18n/locales/ca-ES.json +18 -2
- package/src/i18n/locales/de-DE.json +18 -2
- package/src/i18n/locales/en-US.json +17 -3
- package/src/i18n/locales/es-ES.json +16 -2
- package/src/i18n/locales/fr-FR.json +18 -2
- package/src/i18n/locales/it-IT.json +18 -2
- package/src/i18n/locales/ja-JP.json +18 -2
- package/src/i18n/locales/ko-KR.json +18 -2
- package/src/i18n/locales/pt-PT.json +18 -2
- package/src/i18n/locales/zh-CN.json +18 -2
- package/src/index.ts +1 -1
- package/src/mixins/OxyServices.auth.ts +23 -75
- package/src/mixins/OxyServices.fedcm.ts +10 -12
- package/src/mixins/OxyServices.redirect.ts +82 -371
- package/src/mixins/OxyServices.silent.ts +272 -0
- package/src/mixins/OxyServices.sso.ts +5 -6
- package/src/mixins/OxyServices.utility.ts +9 -22
- package/src/mixins/__tests__/appData.test.ts +1 -1
- package/src/mixins/__tests__/onTokensChanged.test.ts +1 -1
- package/src/mixins/__tests__/reputation.test.ts +1 -1
- package/src/mixins/__tests__/serviceAuth.test.ts +7 -5
- package/src/mixins/__tests__/silent.test.ts +102 -0
- package/src/mixins/__tests__/verifyChallenge.test.ts +9 -14
- package/src/mixins/index.ts +6 -8
- package/src/models/interfaces.ts +5 -16
- package/src/models/session.ts +1 -3
- package/src/server/index.ts +19 -0
- package/src/server/rateLimit.ts +170 -0
- package/src/shared/utils/debugUtils.ts +1 -1
- package/src/utils/accountUtils.ts +4 -4
- package/src/utils/authHelpers.ts +23 -15
- package/src/utils/coldBoot.ts +4 -4
- package/src/utils/fapiAutoDetect.ts +1 -1
- package/src/mixins/OxyServices.popup.ts +0 -631
- package/src/mixins/__tests__/popup.test.ts +0 -374
package/dist/esm/AuthManager.js
CHANGED
|
@@ -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
|
-
|
|
188
|
-
|
|
189
|
-
this._lastKnownAccessToken
|
|
190
|
-
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
|
|
450
|
-
|
|
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.
|
|
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
|
-
|
|
519
|
-
|
|
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
|
-
|
|
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
|
|
464
|
+
// Invalidate current cookie-backed sessions on the server (best-effort)
|
|
625
465
|
try {
|
|
626
|
-
|
|
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
|
|
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.
|
|
663
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
720
|
-
return method;
|
|
540
|
+
return this.currentAuthMethod;
|
|
721
541
|
}
|
|
722
542
|
/**
|
|
723
543
|
* Initialize auth state on app startup.
|
|
724
544
|
*
|
|
725
|
-
*
|
|
726
|
-
*
|
|
727
|
-
*
|
|
728
|
-
*
|
|
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
|
|
739
|
-
*
|
|
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
|
-
|
|
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
|
|
797
|
-
//
|
|
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
|
-
//
|
|
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
|
|
859
|
-
*
|
|
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` (
|
|
874
|
-
*
|
|
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
|
-
*
|
|
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
|
-
//
|
|
1030
|
-
//
|
|
1031
|
-
//
|
|
1032
|
-
//
|
|
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
|
|
1195
|
-
*
|
|
1196
|
-
*
|
|
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.
|
|
1223
|
-
*
|
|
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
|
*/
|