@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/cjs/AuthManager.js
CHANGED
|
@@ -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
|
-
|
|
192
|
-
|
|
193
|
-
this._lastKnownAccessToken
|
|
194
|
-
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
|
|
454
|
-
|
|
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.
|
|
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
|
-
|
|
523
|
-
|
|
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
|
-
|
|
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
|
|
468
|
+
// Invalidate current cookie-backed sessions on the server (best-effort)
|
|
629
469
|
try {
|
|
630
|
-
|
|
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
|
|
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.
|
|
667
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
724
|
-
return method;
|
|
544
|
+
return this.currentAuthMethod;
|
|
725
545
|
}
|
|
726
546
|
/**
|
|
727
547
|
* Initialize auth state on app startup.
|
|
728
548
|
*
|
|
729
|
-
*
|
|
730
|
-
*
|
|
731
|
-
*
|
|
732
|
-
*
|
|
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
|
|
743
|
-
*
|
|
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
|
-
|
|
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
|
|
801
|
-
//
|
|
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
|
-
//
|
|
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
|
|
863
|
-
*
|
|
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` (
|
|
878
|
-
*
|
|
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
|
-
*
|
|
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
|
-
//
|
|
1034
|
-
//
|
|
1035
|
-
//
|
|
1036
|
-
//
|
|
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
|
|
1199
|
-
*
|
|
1200
|
-
*
|
|
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.
|
|
1227
|
-
*
|
|
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
|
*/
|