@oxyhq/core 3.4.1 → 3.4.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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/src/AuthManager.ts
CHANGED
|
@@ -8,7 +8,6 @@
|
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
import type { OxyServices } from './OxyServices';
|
|
11
|
-
import type { HttpService } from './HttpService';
|
|
12
11
|
import type { SessionLoginResponse, MinimalUserData } from './models/session';
|
|
13
12
|
import type {
|
|
14
13
|
RefreshAllAccount,
|
|
@@ -23,7 +22,6 @@ import type {
|
|
|
23
22
|
RestoreFromCookiesOptions,
|
|
24
23
|
SwitchAuthuserResult,
|
|
25
24
|
} from './AuthManagerTypes';
|
|
26
|
-
import { retryAsync } from './utils/asyncUtils';
|
|
27
25
|
import { jwtDecode } from 'jwt-decode';
|
|
28
26
|
|
|
29
27
|
/**
|
|
@@ -49,7 +47,7 @@ export type AuthStateChangeCallback = (user: MinimalUserData | null) => void;
|
|
|
49
47
|
/**
|
|
50
48
|
* Auth method types.
|
|
51
49
|
*/
|
|
52
|
-
export type AuthMethod = 'fedcm' | '
|
|
50
|
+
export type AuthMethod = 'fedcm' | 'redirect' | 'credentials' | 'identity';
|
|
53
51
|
|
|
54
52
|
/**
|
|
55
53
|
* Auth manager configuration.
|
|
@@ -63,30 +61,14 @@ export interface AuthManagerConfig {
|
|
|
63
61
|
refreshBuffer?: number;
|
|
64
62
|
/** Enable cross-tab coordination via BroadcastChannel (default: true in browsers) */
|
|
65
63
|
crossTabSync?: boolean;
|
|
66
|
-
/**
|
|
67
|
-
* "Cookie-only" mode for web apps that rely exclusively on the
|
|
68
|
-
* `oxy_rt_${authuser}` httpOnly refresh cookies and refuse to fall back
|
|
69
|
-
* to the legacy localStorage token/refresh-token path.
|
|
70
|
-
*
|
|
71
|
-
* - `false` (default): `initialize()` tries `restoreFromCookies()` first;
|
|
72
|
-
* if no accounts are restored it falls back to the legacy localStorage
|
|
73
|
-
* path (`oxy_access_token` / `oxy_session`).
|
|
74
|
-
* - `true`: `initialize()` ONLY uses `restoreFromCookies()`. No token /
|
|
75
|
-
* refresh-token / session JSON is read from or written to localStorage.
|
|
76
|
-
* This is the secure default for apps that ship the cookie path end-to-
|
|
77
|
-
* end and want to guarantee no tokens leak to JS-accessible storage.
|
|
78
|
-
*/
|
|
79
|
-
cookieOnly?: boolean;
|
|
80
64
|
}
|
|
81
65
|
|
|
82
66
|
/**
|
|
83
67
|
* Messages sent between tabs via BroadcastChannel for token refresh coordination.
|
|
84
68
|
*
|
|
85
|
-
*
|
|
86
|
-
*
|
|
87
|
-
*
|
|
88
|
-
* `all_signed_out`). The handler ignores unknown types defensively so a
|
|
89
|
-
* mismatched-version sibling tab can't crash this one.
|
|
69
|
+
* Multi-account cookie-path messages keep same-origin tabs aligned while the
|
|
70
|
+
* httpOnly refresh cookies remain the authority. The handler ignores unknown
|
|
71
|
+
* types defensively so a mismatched-version sibling tab can't crash this one.
|
|
90
72
|
*
|
|
91
73
|
* Every outgoing message also carries the sender tab's `tabId` and `nonce`
|
|
92
74
|
* (see `_broadcastNonce` / `_tabId` on AuthManager). The receiver records the
|
|
@@ -96,15 +78,12 @@ export interface AuthManagerConfig {
|
|
|
96
78
|
*/
|
|
97
79
|
interface CrossTabMessage {
|
|
98
80
|
type:
|
|
99
|
-
| 'refresh_starting'
|
|
100
|
-
| 'tokens_refreshed'
|
|
101
|
-
| 'signed_out'
|
|
102
81
|
| 'accounts_restored'
|
|
103
82
|
| 'authuser_switched'
|
|
104
83
|
| 'authuser_signed_out'
|
|
105
84
|
| 'all_signed_out';
|
|
106
85
|
sessionId?: string;
|
|
107
|
-
/** Slot index for `authuser_*` events
|
|
86
|
+
/** Slot index for `authuser_*` events. */
|
|
108
87
|
authuser?: number;
|
|
109
88
|
timestamp: number;
|
|
110
89
|
/** Sender-tab identifier (random hex, generated at AuthManager construction). */
|
|
@@ -113,16 +92,7 @@ interface CrossTabMessage {
|
|
|
113
92
|
nonce: string;
|
|
114
93
|
}
|
|
115
94
|
|
|
116
|
-
/**
|
|
117
|
-
* Storage keys used by AuthManager.
|
|
118
|
-
*/
|
|
119
95
|
const STORAGE_KEYS = {
|
|
120
|
-
ACCESS_TOKEN: 'oxy_access_token',
|
|
121
|
-
REFRESH_TOKEN: 'oxy_refresh_token',
|
|
122
|
-
SESSION: 'oxy_session',
|
|
123
|
-
USER: 'oxy_user',
|
|
124
|
-
AUTH_METHOD: 'oxy_auth_method',
|
|
125
|
-
FEDCM_LOGIN_HINT: 'oxy_fedcm_login_hint',
|
|
126
96
|
/**
|
|
127
97
|
* Persisted active `authuser` slot index for the cookie path. Stores ONLY
|
|
128
98
|
* the integer slot index (e.g. `"0"`, `"1"`), never a token or session
|
|
@@ -214,11 +184,11 @@ export class AuthManager {
|
|
|
214
184
|
private storage: StorageAdapter;
|
|
215
185
|
private listeners: Set<AuthStateChangeCallback> = new Set();
|
|
216
186
|
private currentUser: MinimalUserData | null = null;
|
|
187
|
+
private currentAuthMethod: AuthMethod | null = null;
|
|
217
188
|
private refreshTimer: ReturnType<typeof setTimeout> | null = null;
|
|
218
189
|
private refreshPromise: Promise<boolean> | null = null;
|
|
219
|
-
private config: Required<Omit<AuthManagerConfig, 'crossTabSync'
|
|
190
|
+
private config: Required<Omit<AuthManagerConfig, 'crossTabSync'>> & {
|
|
220
191
|
crossTabSync: boolean;
|
|
221
|
-
cookieOnly: boolean;
|
|
222
192
|
};
|
|
223
193
|
|
|
224
194
|
/** Tracks the access token this instance last knew about, for cross-tab adoption. */
|
|
@@ -227,9 +197,6 @@ export class AuthManager {
|
|
|
227
197
|
/** BroadcastChannel for coordinating token refreshes across browser tabs. */
|
|
228
198
|
private _broadcastChannel: BroadcastChannel | null = null;
|
|
229
199
|
|
|
230
|
-
/** Set to true when another tab broadcasts a successful refresh, so this tab can skip its own. */
|
|
231
|
-
private _otherTabRefreshed = false;
|
|
232
|
-
|
|
233
200
|
/**
|
|
234
201
|
* Identifier for this AuthManager instance (≈ "this tab"). Random hex
|
|
235
202
|
* generated at construction; advertised in every outgoing broadcast and
|
|
@@ -305,15 +272,13 @@ export class AuthManager {
|
|
|
305
272
|
autoRefresh: config.autoRefresh ?? true,
|
|
306
273
|
refreshBuffer: config.refreshBuffer ?? 5 * 60 * 1000, // 5 minutes
|
|
307
274
|
crossTabSync,
|
|
308
|
-
cookieOnly: config.cookieOnly ?? false,
|
|
309
275
|
};
|
|
310
276
|
this.storage = this.config.storage;
|
|
311
277
|
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
this._lastKnownAccessToken
|
|
315
|
-
|
|
316
|
-
};
|
|
278
|
+
this.oxyServices.httpService.setAuthRefreshHandler(async () => {
|
|
279
|
+
const refreshed = await this.refreshToken();
|
|
280
|
+
return refreshed ? this._lastKnownAccessToken : null;
|
|
281
|
+
});
|
|
317
282
|
|
|
318
283
|
// Setup cross-tab coordination in browser environments
|
|
319
284
|
if (this.config.crossTabSync) {
|
|
@@ -347,46 +312,6 @@ export class AuthManager {
|
|
|
347
312
|
if (!this._acceptBroadcast(message)) return;
|
|
348
313
|
|
|
349
314
|
switch (message.type) {
|
|
350
|
-
case 'tokens_refreshed': {
|
|
351
|
-
// Another tab successfully refreshed. Signal to cancel our pending refresh.
|
|
352
|
-
this._otherTabRefreshed = true;
|
|
353
|
-
|
|
354
|
-
// Adopt the new tokens from shared storage
|
|
355
|
-
const newToken = await this.storage.getItem(STORAGE_KEYS.ACCESS_TOKEN);
|
|
356
|
-
if (newToken && newToken !== this._lastKnownAccessToken) {
|
|
357
|
-
this._lastKnownAccessToken = newToken;
|
|
358
|
-
this.oxyServices.httpService.setTokens(newToken);
|
|
359
|
-
|
|
360
|
-
// Re-read session for updated expiry and schedule next refresh
|
|
361
|
-
const sessionJson = await this.storage.getItem(STORAGE_KEYS.SESSION);
|
|
362
|
-
if (sessionJson) {
|
|
363
|
-
try {
|
|
364
|
-
const session = JSON.parse(sessionJson);
|
|
365
|
-
if (session.expiresAt && this.config.autoRefresh) {
|
|
366
|
-
this.setupTokenRefresh(session.expiresAt);
|
|
367
|
-
}
|
|
368
|
-
} catch {
|
|
369
|
-
// Ignore parse errors
|
|
370
|
-
}
|
|
371
|
-
}
|
|
372
|
-
}
|
|
373
|
-
break;
|
|
374
|
-
}
|
|
375
|
-
|
|
376
|
-
case 'signed_out': {
|
|
377
|
-
// Another tab signed out. Clear our local state to stay consistent.
|
|
378
|
-
if (this.refreshTimer) {
|
|
379
|
-
clearTimeout(this.refreshTimer);
|
|
380
|
-
this.refreshTimer = null;
|
|
381
|
-
}
|
|
382
|
-
this.refreshPromise = null;
|
|
383
|
-
this._lastKnownAccessToken = null;
|
|
384
|
-
this.oxyServices.httpService.setTokens('');
|
|
385
|
-
this.currentUser = null;
|
|
386
|
-
this.notifyListeners();
|
|
387
|
-
break;
|
|
388
|
-
}
|
|
389
|
-
|
|
390
315
|
case 'accounts_restored':
|
|
391
316
|
case 'authuser_switched':
|
|
392
317
|
case 'authuser_signed_out': {
|
|
@@ -409,7 +334,7 @@ export class AuthManager {
|
|
|
409
334
|
}
|
|
410
335
|
|
|
411
336
|
case 'all_signed_out': {
|
|
412
|
-
//
|
|
337
|
+
// Wipe the cookie-path registry after another tab signed every slot out.
|
|
413
338
|
if (this.refreshTimer) {
|
|
414
339
|
clearTimeout(this.refreshTimer);
|
|
415
340
|
this.refreshTimer = null;
|
|
@@ -420,11 +345,11 @@ export class AuthManager {
|
|
|
420
345
|
this._lastKnownAccessToken = null;
|
|
421
346
|
this.oxyServices.httpService.setTokens('');
|
|
422
347
|
this.currentUser = null;
|
|
348
|
+
this.currentAuthMethod = null;
|
|
423
349
|
this.notifyListeners();
|
|
424
350
|
break;
|
|
425
351
|
}
|
|
426
352
|
|
|
427
|
-
// 'refresh_starting' is informational; we don't need to act on it currently
|
|
428
353
|
}
|
|
429
354
|
}
|
|
430
355
|
|
|
@@ -564,68 +489,54 @@ export class AuthManager {
|
|
|
564
489
|
session: SessionLoginResponse,
|
|
565
490
|
method: AuthMethod = 'credentials'
|
|
566
491
|
): Promise<void> {
|
|
567
|
-
//
|
|
492
|
+
// Access tokens are memory-only. Fresh login responses plant the token on
|
|
493
|
+
// the HTTP client and the AuthManager registry, but never write it to JS
|
|
494
|
+
// storage. Durable web refresh lives in the httpOnly cookie set by the API.
|
|
568
495
|
if (session.accessToken) {
|
|
569
496
|
this._lastKnownAccessToken = session.accessToken;
|
|
570
|
-
await this.storage.setItem(STORAGE_KEYS.ACCESS_TOKEN, session.accessToken);
|
|
571
497
|
this.oxyServices.httpService.setTokens(session.accessToken);
|
|
572
498
|
}
|
|
573
499
|
|
|
574
|
-
// Store refresh token if available
|
|
575
|
-
if (session.refreshToken) {
|
|
576
|
-
await this.storage.setItem(STORAGE_KEYS.REFRESH_TOKEN, session.refreshToken);
|
|
577
|
-
}
|
|
578
|
-
|
|
579
|
-
// Store session info
|
|
580
|
-
await this.storage.setItem(STORAGE_KEYS.SESSION, JSON.stringify({
|
|
581
|
-
sessionId: session.sessionId,
|
|
582
|
-
deviceId: session.deviceId,
|
|
583
|
-
expiresAt: session.expiresAt,
|
|
584
|
-
}));
|
|
585
|
-
|
|
586
|
-
// Store user only if it has valid required fields (not an empty placeholder)
|
|
587
500
|
if (session.user && typeof (session.user as any).id === 'string' && (session.user as any).id.length > 0) {
|
|
588
|
-
await this.storage.setItem(STORAGE_KEYS.USER, JSON.stringify(session.user));
|
|
589
501
|
this.currentUser = session.user;
|
|
590
502
|
}
|
|
591
503
|
|
|
592
|
-
|
|
593
|
-
|
|
504
|
+
this.currentAuthMethod = method;
|
|
505
|
+
|
|
506
|
+
const decodedAuthuser = session.accessToken
|
|
507
|
+
? AuthManager.decodeAuthuserFromAccessToken(session.accessToken)
|
|
508
|
+
: null;
|
|
509
|
+
const authuser = decodedAuthuser ?? 0;
|
|
510
|
+
if (session.accessToken && session.sessionId) {
|
|
511
|
+
this.accounts.set(authuser, {
|
|
512
|
+
authuser,
|
|
513
|
+
sessionId: session.sessionId,
|
|
514
|
+
user: {
|
|
515
|
+
id: session.user.id,
|
|
516
|
+
username: session.user.username,
|
|
517
|
+
avatar: session.user.avatar ?? null,
|
|
518
|
+
},
|
|
519
|
+
accessToken: session.accessToken,
|
|
520
|
+
expiresAt: session.expiresAt,
|
|
521
|
+
});
|
|
522
|
+
this.activeAuthuser = authuser;
|
|
523
|
+
await this.writeActiveAuthuser(authuser);
|
|
524
|
+
}
|
|
594
525
|
|
|
595
526
|
// Setup auto-refresh if enabled
|
|
596
527
|
if (this.config.autoRefresh && session.expiresAt) {
|
|
597
|
-
this.
|
|
528
|
+
this.setupCookieRefresh(session.expiresAt, authuser);
|
|
598
529
|
}
|
|
599
530
|
|
|
600
531
|
// Notify listeners
|
|
601
532
|
this.notifyListeners();
|
|
602
533
|
}
|
|
603
534
|
|
|
604
|
-
/**
|
|
605
|
-
* Setup automatic token refresh.
|
|
606
|
-
*/
|
|
607
|
-
private setupTokenRefresh(expiresAt: string): void {
|
|
608
|
-
if (this.refreshTimer) {
|
|
609
|
-
clearTimeout(this.refreshTimer);
|
|
610
|
-
}
|
|
611
|
-
|
|
612
|
-
const expiresAtMs = new Date(expiresAt).getTime();
|
|
613
|
-
const now = Date.now();
|
|
614
|
-
const refreshAt = expiresAtMs - this.config.refreshBuffer;
|
|
615
|
-
const delay = Math.max(0, refreshAt - now);
|
|
616
|
-
|
|
617
|
-
if (delay > 0) {
|
|
618
|
-
this.refreshTimer = setTimeout(() => {
|
|
619
|
-
this.refreshToken().catch(() => {
|
|
620
|
-
// Refresh failed, user will need to re-auth
|
|
621
|
-
});
|
|
622
|
-
}, delay);
|
|
623
|
-
}
|
|
624
|
-
}
|
|
625
|
-
|
|
626
535
|
/**
|
|
627
536
|
* Refresh the access token. Deduplicates concurrent calls so only one
|
|
628
|
-
* refresh request is in-flight at a time.
|
|
537
|
+
* refresh request is in-flight at a time. The only refresh authority is the
|
|
538
|
+
* active httpOnly refresh-cookie slot; this method never reads access tokens
|
|
539
|
+
* from storage.
|
|
629
540
|
*/
|
|
630
541
|
async refreshToken(): Promise<boolean> {
|
|
631
542
|
// If a refresh is already in-flight, return the same promise
|
|
@@ -642,131 +553,21 @@ export class AuthManager {
|
|
|
642
553
|
}
|
|
643
554
|
|
|
644
555
|
private async _doRefreshToken(): Promise<boolean> {
|
|
645
|
-
// Reset the cross-tab flag before starting
|
|
646
|
-
this._otherTabRefreshed = false;
|
|
647
|
-
|
|
648
|
-
// Get session info to find sessionId for token refresh
|
|
649
|
-
const sessionJson = await this.storage.getItem(STORAGE_KEYS.SESSION);
|
|
650
|
-
if (!sessionJson) {
|
|
651
|
-
return false;
|
|
652
|
-
}
|
|
653
|
-
|
|
654
|
-
let sessionId: string;
|
|
655
556
|
try {
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
if (!sessionId) return false;
|
|
659
|
-
} catch (err) {
|
|
660
|
-
console.error('AuthManager: Failed to parse session from storage.', err);
|
|
661
|
-
return false;
|
|
662
|
-
}
|
|
663
|
-
|
|
664
|
-
// Record the token we know about before attempting refresh
|
|
665
|
-
const tokenBeforeRefresh = this._lastKnownAccessToken;
|
|
666
|
-
|
|
667
|
-
// Broadcast that we're starting a refresh (informational for other tabs)
|
|
668
|
-
this._broadcast({ type: 'refresh_starting', sessionId, timestamp: Date.now() });
|
|
669
|
-
|
|
670
|
-
try {
|
|
671
|
-
await retryAsync(
|
|
672
|
-
async () => {
|
|
673
|
-
// Before each attempt, check if another tab already refreshed
|
|
674
|
-
if (this._otherTabRefreshed) {
|
|
675
|
-
const adoptedToken = await this.storage.getItem(STORAGE_KEYS.ACCESS_TOKEN);
|
|
676
|
-
if (adoptedToken && adoptedToken !== tokenBeforeRefresh) {
|
|
677
|
-
// Another tab succeeded. Adopt its tokens and short-circuit.
|
|
678
|
-
this._lastKnownAccessToken = adoptedToken;
|
|
679
|
-
this.oxyServices.httpService.setTokens(adoptedToken);
|
|
680
|
-
return;
|
|
681
|
-
}
|
|
682
|
-
}
|
|
683
|
-
|
|
684
|
-
const httpService = this.oxyServices.httpService as HttpService;
|
|
685
|
-
// Use session-based token endpoint which handles auto-refresh server-side
|
|
686
|
-
const response = await httpService.request<{ accessToken: string; expiresAt: string }>({
|
|
687
|
-
method: 'GET',
|
|
688
|
-
url: `/session/token/${sessionId}`,
|
|
689
|
-
cache: false,
|
|
690
|
-
retry: false,
|
|
691
|
-
});
|
|
692
|
-
|
|
693
|
-
if (!response.accessToken) {
|
|
694
|
-
throw new Error('No access token in refresh response');
|
|
695
|
-
}
|
|
696
|
-
|
|
697
|
-
// Update access token in storage and HTTP client
|
|
698
|
-
this._lastKnownAccessToken = response.accessToken;
|
|
699
|
-
await this.storage.setItem(STORAGE_KEYS.ACCESS_TOKEN, response.accessToken);
|
|
700
|
-
this.oxyServices.httpService.setTokens(response.accessToken);
|
|
701
|
-
|
|
702
|
-
// Update session expiry and schedule next refresh
|
|
703
|
-
if (response.expiresAt) {
|
|
704
|
-
try {
|
|
705
|
-
const session = JSON.parse(sessionJson);
|
|
706
|
-
session.expiresAt = response.expiresAt;
|
|
707
|
-
await this.storage.setItem(STORAGE_KEYS.SESSION, JSON.stringify(session));
|
|
708
|
-
} catch (err) {
|
|
709
|
-
// Ignore parse errors for session update, but log for debugging.
|
|
710
|
-
console.error('AuthManager: Failed to re-save session after token refresh.', err);
|
|
711
|
-
}
|
|
712
|
-
|
|
713
|
-
if (this.config.autoRefresh) {
|
|
714
|
-
this.setupTokenRefresh(response.expiresAt);
|
|
715
|
-
}
|
|
716
|
-
}
|
|
717
|
-
|
|
718
|
-
// Broadcast success so other tabs can adopt these tokens
|
|
719
|
-
this._broadcast({ type: 'tokens_refreshed', sessionId, timestamp: Date.now() });
|
|
720
|
-
},
|
|
721
|
-
2, // 2 retries = 3 total attempts
|
|
722
|
-
1000, // 1s base delay with exponential backoff + jitter
|
|
723
|
-
(error: any) => {
|
|
724
|
-
// Don't retry on 4xx client errors (invalid/revoked token)
|
|
725
|
-
const status = error?.status ?? error?.response?.status;
|
|
726
|
-
if (status && status >= 400 && status < 500) return false;
|
|
727
|
-
return true;
|
|
728
|
-
}
|
|
729
|
-
);
|
|
730
|
-
return true;
|
|
731
|
-
} catch {
|
|
732
|
-
// All retry attempts exhausted. Before clearing the session, check if
|
|
733
|
-
// another tab managed to refresh successfully while we were retrying.
|
|
734
|
-
// Since all tabs share the same storage (localStorage), a successful
|
|
735
|
-
// refresh from another tab will have written a different access token.
|
|
736
|
-
const currentStoredToken = await this.storage.getItem(STORAGE_KEYS.ACCESS_TOKEN);
|
|
737
|
-
if (currentStoredToken && currentStoredToken !== tokenBeforeRefresh) {
|
|
738
|
-
// Another tab refreshed successfully. Adopt its tokens instead of logging out.
|
|
739
|
-
this._lastKnownAccessToken = currentStoredToken;
|
|
740
|
-
this.oxyServices.httpService.setTokens(currentStoredToken);
|
|
741
|
-
|
|
742
|
-
// Restore user from storage in case it was updated
|
|
743
|
-
const userJson = await this.storage.getItem(STORAGE_KEYS.USER);
|
|
744
|
-
if (userJson) {
|
|
745
|
-
try {
|
|
746
|
-
this.currentUser = JSON.parse(userJson);
|
|
747
|
-
} catch {
|
|
748
|
-
// Ignore parse errors
|
|
749
|
-
}
|
|
750
|
-
}
|
|
751
|
-
|
|
752
|
-
// Re-read session expiry and schedule next refresh
|
|
753
|
-
const updatedSessionJson = await this.storage.getItem(STORAGE_KEYS.SESSION);
|
|
754
|
-
if (updatedSessionJson) {
|
|
755
|
-
try {
|
|
756
|
-
const session = JSON.parse(updatedSessionJson);
|
|
757
|
-
if (session.expiresAt && this.config.autoRefresh) {
|
|
758
|
-
this.setupTokenRefresh(session.expiresAt);
|
|
759
|
-
}
|
|
760
|
-
} catch {
|
|
761
|
-
// Ignore parse errors
|
|
762
|
-
}
|
|
763
|
-
}
|
|
557
|
+
if (this.activeAuthuser !== null) {
|
|
558
|
+
await this.switchAuthuser(this.activeAuthuser);
|
|
764
559
|
return true;
|
|
765
560
|
}
|
|
766
561
|
|
|
767
|
-
|
|
562
|
+
const restored = await this.restoreFromCookies();
|
|
563
|
+
return restored.accounts.length > 0;
|
|
564
|
+
} catch {
|
|
768
565
|
await this.clearSession();
|
|
769
566
|
this.currentUser = null;
|
|
567
|
+
this.accounts.clear();
|
|
568
|
+
this.activeAuthuser = null;
|
|
569
|
+
this._lastKnownAccessToken = null;
|
|
570
|
+
this.oxyServices.httpService.setTokens('');
|
|
770
571
|
this.notifyListeners();
|
|
771
572
|
return false;
|
|
772
573
|
}
|
|
@@ -783,16 +584,10 @@ export class AuthManager {
|
|
|
783
584
|
}
|
|
784
585
|
this.refreshPromise = null;
|
|
785
586
|
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
const session = JSON.parse(sessionJson);
|
|
791
|
-
if (session.sessionId && typeof (this.oxyServices as any).logoutSession === 'function') {
|
|
792
|
-
await (this.oxyServices as any).logoutSession(session.sessionId);
|
|
793
|
-
}
|
|
794
|
-
}
|
|
795
|
-
} catch {
|
|
587
|
+
// Invalidate current cookie-backed sessions on the server (best-effort)
|
|
588
|
+
try {
|
|
589
|
+
await this.signOutAllViaCookies();
|
|
590
|
+
} catch {
|
|
796
591
|
// Best-effort: don't block local signout on network failure
|
|
797
592
|
}
|
|
798
593
|
|
|
@@ -813,24 +608,18 @@ export class AuthManager {
|
|
|
813
608
|
// Clear storage
|
|
814
609
|
await this.clearSession();
|
|
815
610
|
|
|
816
|
-
// Notify other tabs so they also sign out
|
|
817
|
-
this._broadcast({ type: 'signed_out', timestamp: Date.now() });
|
|
818
|
-
|
|
819
611
|
// Update state and notify
|
|
820
612
|
this.currentUser = null;
|
|
821
613
|
this.notifyListeners();
|
|
822
614
|
}
|
|
823
615
|
|
|
824
616
|
/**
|
|
825
|
-
* Clear
|
|
617
|
+
* Clear local cookie-path state. The only persisted AuthManager value is the
|
|
618
|
+
* active numeric slot; tokens and user objects are intentionally memory-only.
|
|
826
619
|
*/
|
|
827
620
|
private async clearSession(): Promise<void> {
|
|
828
|
-
await this.
|
|
829
|
-
|
|
830
|
-
await this.storage.removeItem(STORAGE_KEYS.SESSION);
|
|
831
|
-
await this.storage.removeItem(STORAGE_KEYS.USER);
|
|
832
|
-
await this.storage.removeItem(STORAGE_KEYS.AUTH_METHOD);
|
|
833
|
-
await this.storage.removeItem(STORAGE_KEYS.FEDCM_LOGIN_HINT);
|
|
621
|
+
await this.clearActiveAuthuser();
|
|
622
|
+
this.currentAuthMethod = null;
|
|
834
623
|
}
|
|
835
624
|
|
|
836
625
|
/**
|
|
@@ -848,17 +637,11 @@ export class AuthManager {
|
|
|
848
637
|
}
|
|
849
638
|
|
|
850
639
|
/**
|
|
851
|
-
* Get a valid access token, refreshing automatically if expired or expiring
|
|
640
|
+
* Get a valid access token, refreshing automatically if expired or expiring
|
|
641
|
+
* soon. The token is read from memory only.
|
|
852
642
|
*/
|
|
853
643
|
async getAccessToken(): Promise<string | null> {
|
|
854
|
-
|
|
855
|
-
// memory (`_lastKnownAccessToken` + httpService) and is intentionally never
|
|
856
|
-
// written to JS storage — the cookieOnly contract forbids persisting tokens
|
|
857
|
-
// in JS-accessible storage. Fall back to the in-memory token when storage has
|
|
858
|
-
// none, otherwise getAccessToken returns null after every cold-boot/reload and
|
|
859
|
-
// standalone API clients (e.g. the Console axios client) send no Authorization
|
|
860
|
-
// header → 401 on every authed endpoint while `isAuthenticated` is still true.
|
|
861
|
-
const token = (await this.storage.getItem(STORAGE_KEYS.ACCESS_TOKEN)) ?? this._lastKnownAccessToken;
|
|
644
|
+
const token = this._lastKnownAccessToken;
|
|
862
645
|
if (!token) return null;
|
|
863
646
|
|
|
864
647
|
try {
|
|
@@ -869,9 +652,7 @@ export class AuthManager {
|
|
|
869
652
|
if (decoded.exp - now < buffer) {
|
|
870
653
|
const refreshed = await this.refreshToken();
|
|
871
654
|
if (refreshed) {
|
|
872
|
-
|
|
873
|
-
// prefer storage but fall back to memory for the cookieOnly path.
|
|
874
|
-
return (await this.storage.getItem(STORAGE_KEYS.ACCESS_TOKEN)) ?? this._lastKnownAccessToken;
|
|
655
|
+
return this._lastKnownAccessToken;
|
|
875
656
|
}
|
|
876
657
|
}
|
|
877
658
|
}
|
|
@@ -886,100 +667,41 @@ export class AuthManager {
|
|
|
886
667
|
* Get the auth method used for current session.
|
|
887
668
|
*/
|
|
888
669
|
async getAuthMethod(): Promise<AuthMethod | null> {
|
|
889
|
-
|
|
890
|
-
return method as AuthMethod | null;
|
|
670
|
+
return this.currentAuthMethod;
|
|
891
671
|
}
|
|
892
672
|
|
|
893
673
|
/**
|
|
894
674
|
* Initialize auth state on app startup.
|
|
895
675
|
*
|
|
896
|
-
*
|
|
897
|
-
*
|
|
898
|
-
*
|
|
899
|
-
*
|
|
900
|
-
* refresh-token material to JS.
|
|
901
|
-
* 2. If the cookie path yielded zero accounts AND `cookieOnly` is
|
|
902
|
-
* `false`, fall back to the legacy localStorage path
|
|
903
|
-
* (`oxy_access_token` / `oxy_session`) for backwards compatibility
|
|
904
|
-
* with apps that haven't migrated to the cookie endpoint yet.
|
|
905
|
-
* 3. If `cookieOnly` is `true`, skip the legacy fallback entirely.
|
|
906
|
-
* This guarantees no tokens or refresh tokens are ever read from
|
|
907
|
-
* or written to JS-accessible storage.
|
|
676
|
+
* Only the cookie path is authoritative. `restoreFromCookies()` refreshes
|
|
677
|
+
* the httpOnly `oxy_rt_${authuser}` slots through `/auth/refresh-all`,
|
|
678
|
+
* plants the active access token in memory, and returns the active user.
|
|
679
|
+
* No access token, refresh token, or session JSON is read from localStorage.
|
|
908
680
|
*
|
|
909
|
-
* Returns the active user on success, or `null` when
|
|
910
|
-
*
|
|
681
|
+
* Returns the active user on success, or `null` when no cookie-backed
|
|
682
|
+
* account was restored.
|
|
911
683
|
*/
|
|
912
684
|
async initialize(options: RestoreFromCookiesOptions = {}): Promise<MinimalUserData | null> {
|
|
913
|
-
// 1. Cookie path (preferred). Forward the optional cold-boot fail-fast
|
|
914
|
-
// timeout so a cross-domain stall cannot hang provider init.
|
|
915
685
|
const cookieResult = await this.restoreFromCookies(options);
|
|
916
686
|
if (cookieResult.accounts.length > 0) {
|
|
917
687
|
return this.currentUser;
|
|
918
688
|
}
|
|
919
689
|
|
|
920
|
-
|
|
921
|
-
if (this.config.cookieOnly) {
|
|
922
|
-
return null;
|
|
923
|
-
}
|
|
924
|
-
|
|
925
|
-
try {
|
|
926
|
-
// Try to restore user from storage
|
|
927
|
-
const userJson = await this.storage.getItem(STORAGE_KEYS.USER);
|
|
928
|
-
if (userJson) {
|
|
929
|
-
this.currentUser = JSON.parse(userJson);
|
|
930
|
-
}
|
|
931
|
-
|
|
932
|
-
// Restore token to HTTP client
|
|
933
|
-
const token = await this.storage.getItem(STORAGE_KEYS.ACCESS_TOKEN);
|
|
934
|
-
if (token) {
|
|
935
|
-
this._lastKnownAccessToken = token;
|
|
936
|
-
this.oxyServices.httpService.setTokens(token);
|
|
937
|
-
}
|
|
938
|
-
|
|
939
|
-
// Check session expiry
|
|
940
|
-
const sessionJson = await this.storage.getItem(STORAGE_KEYS.SESSION);
|
|
941
|
-
if (sessionJson) {
|
|
942
|
-
const session = JSON.parse(sessionJson);
|
|
943
|
-
if (session.expiresAt) {
|
|
944
|
-
const expiresAt = new Date(session.expiresAt).getTime();
|
|
945
|
-
if (expiresAt <= Date.now()) {
|
|
946
|
-
// Session expired, try refresh
|
|
947
|
-
const refreshed = await this.refreshToken();
|
|
948
|
-
if (!refreshed) {
|
|
949
|
-
await this.clearSession();
|
|
950
|
-
this.currentUser = null;
|
|
951
|
-
}
|
|
952
|
-
} else if (this.config.autoRefresh) {
|
|
953
|
-
// Setup refresh timer
|
|
954
|
-
this.setupTokenRefresh(session.expiresAt);
|
|
955
|
-
}
|
|
956
|
-
}
|
|
957
|
-
}
|
|
958
|
-
|
|
959
|
-
return this.currentUser;
|
|
960
|
-
} catch {
|
|
961
|
-
// Failed to restore, start fresh
|
|
962
|
-
await this.clearSession();
|
|
963
|
-
this.currentUser = null;
|
|
964
|
-
return null;
|
|
965
|
-
}
|
|
690
|
+
return null;
|
|
966
691
|
}
|
|
967
692
|
|
|
968
693
|
// -------------------------------------------------------------------------
|
|
969
694
|
// Multi-account cookie path (Google-style multi-sign-in).
|
|
970
695
|
// -------------------------------------------------------------------------
|
|
971
|
-
// The cookie path is web-only
|
|
972
|
-
//
|
|
696
|
+
// The cookie path is web-only. It never touches the retired
|
|
697
|
+
// `oxy_access_token` / `oxy_refresh_token` /
|
|
973
698
|
// `oxy_session` localStorage keys, because the refresh token lives in the
|
|
974
699
|
// httpOnly `oxy_rt_${authuser}` cookies and access tokens live in
|
|
975
700
|
// `this.accounts` (in-memory only). The only localStorage key the cookie
|
|
976
701
|
// path writes is `STORAGE_KEYS.ACTIVE_AUTHUSER` — a small integer that is
|
|
977
702
|
// explicitly NOT a secret.
|
|
978
703
|
//
|
|
979
|
-
//
|
|
980
|
-
// (recommended for new web apps) pass `cookieOnly: true` to the
|
|
981
|
-
// AuthManager config; in that mode `initialize()` ONLY uses the cookie
|
|
982
|
-
// path.
|
|
704
|
+
// `initialize()` only uses the cookie path.
|
|
983
705
|
// -------------------------------------------------------------------------
|
|
984
706
|
|
|
985
707
|
/**
|
|
@@ -1028,9 +750,8 @@ export class AuthManager {
|
|
|
1028
750
|
|
|
1029
751
|
/**
|
|
1030
752
|
* Build a `MinimalUserData` from a `RefreshAllAccount`. Returns `null` when
|
|
1031
|
-
* the wire entry has no user shape
|
|
1032
|
-
*
|
|
1033
|
-
* case.
|
|
753
|
+
* the wire entry has no user shape; the AuthManager's caller is expected to
|
|
754
|
+
* hydrate via `/users/me` in that case.
|
|
1034
755
|
*/
|
|
1035
756
|
private static toMinimalUser(account: RefreshAllAccount): MinimalUserData | null {
|
|
1036
757
|
if (!account.user) return null;
|
|
@@ -1043,8 +764,8 @@ export class AuthManager {
|
|
|
1043
764
|
|
|
1044
765
|
/**
|
|
1045
766
|
* Hydrate the user shape for a slot whose AuthManagerAccount currently has
|
|
1046
|
-
* `user: null` (
|
|
1047
|
-
*
|
|
767
|
+
* `user: null` (for example, a switch onto a previously unknown slot). Calls
|
|
768
|
+
* `/users/me` with the slot's freshly-planted access
|
|
1048
769
|
* token already on the HTTP client; merges the result back into the
|
|
1049
770
|
* registry entry. Network failures are non-fatal — the slot remains with
|
|
1050
771
|
* `user: null` and the UI is expected to render the public-key fallback
|
|
@@ -1122,9 +843,7 @@ export class AuthManager {
|
|
|
1122
843
|
* Calls `oxyServices.refreshAllSessions()` (`POST /auth/refresh-all` with
|
|
1123
844
|
* `credentials: 'include'`). The server rotates every presented
|
|
1124
845
|
* `oxy_rt_${authuser}` cookie in parallel and returns one entry per
|
|
1125
|
-
*
|
|
1126
|
-
* `/auth/refresh` against older servers (handled inside
|
|
1127
|
-
* `refreshAllSessions`).
|
|
846
|
+
* valid slot.
|
|
1128
847
|
*
|
|
1129
848
|
* Plants the active account's access token on the shared HTTP client;
|
|
1130
849
|
* sibling slots' tokens stay in the in-memory registry so a later
|
|
@@ -1211,10 +930,10 @@ export class AuthManager {
|
|
|
1211
930
|
this.setupCookieRefresh(activeAccount.expiresAt, active);
|
|
1212
931
|
}
|
|
1213
932
|
|
|
1214
|
-
//
|
|
1215
|
-
//
|
|
1216
|
-
//
|
|
1217
|
-
//
|
|
933
|
+
// If the active slot has no user shape, schedule a /users/me hydration so
|
|
934
|
+
// the chooser isn't stuck on the public-key handle. Hydration is
|
|
935
|
+
// fire-and-forget — the snapshot is already considered "restored" once
|
|
936
|
+
// the access token is planted.
|
|
1218
937
|
if (activeAccount.user === null) {
|
|
1219
938
|
slotsNeedingHydration.push(activeAccount.authuser);
|
|
1220
939
|
}
|
|
@@ -1376,6 +1095,7 @@ export class AuthManager {
|
|
|
1376
1095
|
this._lastKnownAccessToken = null;
|
|
1377
1096
|
this.oxyServices.httpService.setTokens('');
|
|
1378
1097
|
this.currentUser = null;
|
|
1098
|
+
this.currentAuthMethod = null;
|
|
1379
1099
|
await this.clearActiveAuthuser();
|
|
1380
1100
|
}
|
|
1381
1101
|
}
|
|
@@ -1389,10 +1109,9 @@ export class AuthManager {
|
|
|
1389
1109
|
*
|
|
1390
1110
|
* Calls `oxyServices.logoutAllSessionsViaCookie()`: server-side revokes
|
|
1391
1111
|
* every presented family and `Set-Cookie`s an immediate expiry for every
|
|
1392
|
-
* recognised `oxy_rt_${n}` slot
|
|
1393
|
-
*
|
|
1394
|
-
*
|
|
1395
|
-
* starts fresh.
|
|
1112
|
+
* recognised `oxy_rt_${n}` slot. The in-memory registry is wiped, the active
|
|
1113
|
+
* slot is cleared, and the persisted `oxy_active_authuser` is removed so the
|
|
1114
|
+
* next cold boot starts fresh.
|
|
1396
1115
|
*/
|
|
1397
1116
|
async signOutAllViaCookies(): Promise<void> {
|
|
1398
1117
|
try {
|
|
@@ -1406,6 +1125,7 @@ export class AuthManager {
|
|
|
1406
1125
|
this._lastKnownAccessToken = null;
|
|
1407
1126
|
this.oxyServices.httpService.setTokens('');
|
|
1408
1127
|
this.currentUser = null;
|
|
1128
|
+
this.currentAuthMethod = null;
|
|
1409
1129
|
this._lastRestoreAt.clear();
|
|
1410
1130
|
await this.clearActiveAuthuser();
|
|
1411
1131
|
|
|
@@ -1420,9 +1140,8 @@ export class AuthManager {
|
|
|
1420
1140
|
}
|
|
1421
1141
|
|
|
1422
1142
|
/**
|
|
1423
|
-
* Schedule an auto-refresh for the cookie path on the active slot.
|
|
1424
|
-
*
|
|
1425
|
-
* exactly ONE active slot at a time, so one timer suffices).
|
|
1143
|
+
* Schedule an auto-refresh for the cookie path on the active slot. The
|
|
1144
|
+
* AuthManager has exactly one active slot at a time, so one timer suffices.
|
|
1426
1145
|
*/
|
|
1427
1146
|
private setupCookieRefresh(expiresAt: string, authuser: number): void {
|
|
1428
1147
|
if (this.refreshTimer) {
|
|
@@ -1463,6 +1182,17 @@ export class AuthManager {
|
|
|
1463
1182
|
}
|
|
1464
1183
|
}
|
|
1465
1184
|
|
|
1185
|
+
private static decodeAuthuserFromAccessToken(token: string): number | null {
|
|
1186
|
+
try {
|
|
1187
|
+
const decoded = jwtDecode<{ authuser?: number }>(token);
|
|
1188
|
+
return typeof decoded.authuser === 'number' && Number.isFinite(decoded.authuser) && decoded.authuser >= 0
|
|
1189
|
+
? decoded.authuser
|
|
1190
|
+
: null;
|
|
1191
|
+
} catch {
|
|
1192
|
+
return null;
|
|
1193
|
+
}
|
|
1194
|
+
}
|
|
1195
|
+
|
|
1466
1196
|
/**
|
|
1467
1197
|
* Destroy the auth manager and clean up resources.
|
|
1468
1198
|
*/
|