@oxyhq/core 3.4.0 → 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 +25 -3
- 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
|
@@ -5,8 +5,7 @@
|
|
|
5
5
|
* selects the best authentication method based on browser capabilities:
|
|
6
6
|
*
|
|
7
7
|
* 1. FedCM (if supported) - Modern, Google-style browser-native auth
|
|
8
|
-
* 2.
|
|
9
|
-
* 3. Redirect (final fallback) - Traditional full-page redirect
|
|
8
|
+
* 2. Redirect (fallback) - Tokenless central SSO full-page redirect
|
|
10
9
|
*
|
|
11
10
|
* Usage:
|
|
12
11
|
* ```typescript
|
|
@@ -17,8 +16,8 @@
|
|
|
17
16
|
* // Automatic method selection
|
|
18
17
|
* const session = await auth.signIn();
|
|
19
18
|
*
|
|
20
|
-
* // Or use specific method
|
|
21
|
-
*
|
|
19
|
+
* // Or use a specific method
|
|
20
|
+
* auth.signInWithRedirect();
|
|
22
21
|
* ```
|
|
23
22
|
*/
|
|
24
23
|
import { logger } from './utils/loggerUtils.js';
|
|
@@ -31,53 +30,23 @@ export class CrossDomainAuth {
|
|
|
31
30
|
*
|
|
32
31
|
* Tries methods in this order:
|
|
33
32
|
* 1. FedCM (if supported and not in private browsing)
|
|
34
|
-
* 2.
|
|
35
|
-
* 3. Redirect (always works)
|
|
33
|
+
* 2. Redirect (always works)
|
|
36
34
|
*
|
|
37
35
|
* @param options - Authentication options
|
|
38
36
|
* @returns Session with user data and access token
|
|
39
37
|
*/
|
|
40
38
|
async signIn(options = {}) {
|
|
41
39
|
const method = options.method || 'auto';
|
|
42
|
-
// If specific method requested, use it directly. The caller MAY have
|
|
43
|
-
// pre-opened a popup on the raw click (the standard pattern in
|
|
44
|
-
// WebOxyProvider / services useAuth). For the FedCM and redirect paths
|
|
45
|
-
// that popup is unused — close it so it doesn't linger as an orphaned
|
|
46
|
-
// blank window. Close in both success and failure paths.
|
|
47
40
|
if (method === 'fedcm') {
|
|
48
|
-
|
|
49
|
-
const session = await this.signInWithFedCM(options);
|
|
50
|
-
this.closeOrphanPopup(options.popup);
|
|
51
|
-
return session;
|
|
52
|
-
}
|
|
53
|
-
catch (error) {
|
|
54
|
-
this.closeOrphanPopup(options.popup);
|
|
55
|
-
throw error;
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
if (method === 'popup') {
|
|
59
|
-
return this.signInWithPopup(options);
|
|
41
|
+
return this.signInWithFedCM(options);
|
|
60
42
|
}
|
|
61
43
|
if (method === 'redirect') {
|
|
62
|
-
this.closeOrphanPopup(options.popup);
|
|
63
44
|
this.signInWithRedirect(options);
|
|
64
45
|
return null; // Redirect doesn't return immediately
|
|
65
46
|
}
|
|
66
|
-
// Auto mode:
|
|
47
|
+
// Auto mode: try methods in order of preference.
|
|
67
48
|
return this.autoSignIn(options);
|
|
68
49
|
}
|
|
69
|
-
/**
|
|
70
|
-
* Close a caller-supplied popup window that is no longer needed (e.g. the
|
|
71
|
-
* resolved auth method didn't end up using it). Safe against null / already
|
|
72
|
-
* closed handles.
|
|
73
|
-
*
|
|
74
|
-
* @private
|
|
75
|
-
*/
|
|
76
|
-
closeOrphanPopup(popup) {
|
|
77
|
-
if (popup && !popup.closed) {
|
|
78
|
-
popup.close();
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
50
|
/**
|
|
82
51
|
* Automatic sign-in with progressive enhancement
|
|
83
52
|
*
|
|
@@ -88,27 +57,13 @@ export class CrossDomainAuth {
|
|
|
88
57
|
if (this.isFedCMSupported()) {
|
|
89
58
|
try {
|
|
90
59
|
options.onMethodSelected?.('fedcm');
|
|
91
|
-
|
|
92
|
-
// FedCM succeeded — close the pre-opened popup so it doesn't linger
|
|
93
|
-
// as an orphaned blank window.
|
|
94
|
-
this.closeOrphanPopup(options.popup);
|
|
95
|
-
return session;
|
|
60
|
+
return await this.signInWithFedCM(options);
|
|
96
61
|
}
|
|
97
62
|
catch (error) {
|
|
98
|
-
logger.warn('FedCM failed,
|
|
63
|
+
logger.warn('FedCM failed, falling back to redirect', { component: 'CrossDomainAuth', method: 'autoSignIn' }, error);
|
|
99
64
|
}
|
|
100
65
|
}
|
|
101
|
-
// 2.
|
|
102
|
-
try {
|
|
103
|
-
options.onMethodSelected?.('popup');
|
|
104
|
-
return await this.signInWithPopup(options);
|
|
105
|
-
}
|
|
106
|
-
catch (error) {
|
|
107
|
-
logger.warn('Popup failed, falling back to redirect', { component: 'CrossDomainAuth', method: 'autoSignIn' }, error);
|
|
108
|
-
// Popup path failed — close the pre-opened popup before redirecting.
|
|
109
|
-
this.closeOrphanPopup(options.popup);
|
|
110
|
-
}
|
|
111
|
-
// 3. Fallback to redirect (always works)
|
|
66
|
+
// 2. Fallback to redirect (always works)
|
|
112
67
|
options.onMethodSelected?.('redirect');
|
|
113
68
|
this.signInWithRedirect(options);
|
|
114
69
|
return null;
|
|
@@ -116,26 +71,13 @@ export class CrossDomainAuth {
|
|
|
116
71
|
/**
|
|
117
72
|
* Sign in using FedCM (Federated Credential Management)
|
|
118
73
|
*
|
|
119
|
-
* Best method - browser-native,
|
|
74
|
+
* Best method - browser-native, Google-like experience
|
|
120
75
|
*/
|
|
121
76
|
async signInWithFedCM(options = {}) {
|
|
122
77
|
return this.oxyServices.signInWithFedCM({
|
|
123
78
|
context: options.isSignup ? 'signup' : 'signin',
|
|
124
79
|
});
|
|
125
80
|
}
|
|
126
|
-
/**
|
|
127
|
-
* Sign in using popup window
|
|
128
|
-
*
|
|
129
|
-
* Good method - preserves app state, no full page reload
|
|
130
|
-
*/
|
|
131
|
-
async signInWithPopup(options = {}) {
|
|
132
|
-
return this.oxyServices.signInWithPopup({
|
|
133
|
-
mode: options.isSignup ? 'signup' : 'login',
|
|
134
|
-
width: options.popupDimensions?.width,
|
|
135
|
-
height: options.popupDimensions?.height,
|
|
136
|
-
popup: options.popup ?? undefined,
|
|
137
|
-
});
|
|
138
|
-
}
|
|
139
81
|
/**
|
|
140
82
|
* Sign in using full-page redirect
|
|
141
83
|
*
|
|
@@ -159,7 +101,7 @@ export class CrossDomainAuth {
|
|
|
159
101
|
* Silent sign-in (check for existing session)
|
|
160
102
|
*
|
|
161
103
|
* Tries to automatically sign in without user interaction.
|
|
162
|
-
* Works with
|
|
104
|
+
* Works with FedCM and iframe-based silent auth.
|
|
163
105
|
*
|
|
164
106
|
* @returns Session if user is already signed in, null otherwise
|
|
165
107
|
*/
|
|
@@ -186,22 +128,13 @@ export class CrossDomainAuth {
|
|
|
186
128
|
}
|
|
187
129
|
}
|
|
188
130
|
/**
|
|
189
|
-
* Restore session from storage
|
|
131
|
+
* Restore session from storage.
|
|
190
132
|
*
|
|
191
|
-
*
|
|
133
|
+
* Access tokens are no longer persisted in browser storage; providers restore
|
|
134
|
+
* through refresh cookies / SSO code exchange instead.
|
|
192
135
|
*/
|
|
193
136
|
restoreSession() {
|
|
194
|
-
return
|
|
195
|
-
}
|
|
196
|
-
/**
|
|
197
|
-
* Open a blank popup SYNCHRONOUSLY (call from a raw user-gesture handler
|
|
198
|
-
* BEFORE any `await`). Returns `null` if the popup was blocked. Pass the
|
|
199
|
-
* handle into `signIn({ popup })` / `signInWithPopup({ popup })` so the
|
|
200
|
-
* popup is not blocked by Chrome after any prior `await` consumed the
|
|
201
|
-
* transient user activation. Delegates to `OxyServices.openBlankPopup`.
|
|
202
|
-
*/
|
|
203
|
-
openBlankPopup(width, height) {
|
|
204
|
-
return this.oxyServices.openBlankPopup(width, height);
|
|
137
|
+
return false;
|
|
205
138
|
}
|
|
206
139
|
/**
|
|
207
140
|
* Check if FedCM is supported in current browser
|
|
@@ -225,8 +158,8 @@ export class CrossDomainAuth {
|
|
|
225
158
|
}
|
|
226
159
|
if (typeof window !== 'undefined') {
|
|
227
160
|
return {
|
|
228
|
-
method: '
|
|
229
|
-
reason: 'Browser environment -
|
|
161
|
+
method: 'redirect',
|
|
162
|
+
reason: 'Browser environment - redirect SSO works without token callback URLs',
|
|
230
163
|
};
|
|
231
164
|
}
|
|
232
165
|
return {
|
|
@@ -239,8 +172,7 @@ export class CrossDomainAuth {
|
|
|
239
172
|
*
|
|
240
173
|
* This handles:
|
|
241
174
|
* 1. Redirect callback (if returning from auth.oxy.so)
|
|
242
|
-
* 2.
|
|
243
|
-
* 3. Silent sign-in (check for existing SSO session)
|
|
175
|
+
* 2. Silent sign-in (check for existing SSO session)
|
|
244
176
|
*
|
|
245
177
|
* @returns Session if user is authenticated, null otherwise
|
|
246
178
|
*/
|
|
@@ -250,26 +182,7 @@ export class CrossDomainAuth {
|
|
|
250
182
|
if (callbackSession) {
|
|
251
183
|
return callbackSession;
|
|
252
184
|
}
|
|
253
|
-
// 2. Try
|
|
254
|
-
const restored = this.restoreSession();
|
|
255
|
-
if (restored) {
|
|
256
|
-
// Verify session is still valid by fetching user
|
|
257
|
-
try {
|
|
258
|
-
const user = await this.oxyServices.getCurrentUser();
|
|
259
|
-
if (user) {
|
|
260
|
-
return {
|
|
261
|
-
sessionId: this.oxyServices.getStoredSessionId?.() || '',
|
|
262
|
-
deviceId: '',
|
|
263
|
-
expiresAt: new Date(Date.now() + 24 * 60 * 60 * 1000).toISOString(),
|
|
264
|
-
user,
|
|
265
|
-
};
|
|
266
|
-
}
|
|
267
|
-
}
|
|
268
|
-
catch (error) {
|
|
269
|
-
logger.debug('stored session invalid', { component: 'CrossDomainAuth', method: 'initialize' }, error);
|
|
270
|
-
}
|
|
271
|
-
}
|
|
272
|
-
// 3. Try silent sign-in (check for SSO session at auth.oxy.so)
|
|
185
|
+
// 2. Try silent sign-in (check for SSO session at auth.oxy.so)
|
|
273
186
|
return await this.silentSignIn();
|
|
274
187
|
}
|
|
275
188
|
}
|
package/dist/esm/HttpService.js
CHANGED
|
@@ -57,23 +57,17 @@ function fnv1a32(str) {
|
|
|
57
57
|
class TokenStore {
|
|
58
58
|
constructor() {
|
|
59
59
|
this.accessToken = null;
|
|
60
|
-
this.refreshToken = null;
|
|
61
60
|
this.csrfToken = null;
|
|
62
61
|
this.csrfTokenFetchPromise = null;
|
|
63
62
|
}
|
|
64
|
-
setTokens(accessToken
|
|
63
|
+
setTokens(accessToken) {
|
|
65
64
|
this.accessToken = accessToken;
|
|
66
|
-
this.refreshToken = refreshToken;
|
|
67
65
|
}
|
|
68
66
|
getAccessToken() {
|
|
69
67
|
return this.accessToken;
|
|
70
68
|
}
|
|
71
|
-
getRefreshToken() {
|
|
72
|
-
return this.refreshToken;
|
|
73
|
-
}
|
|
74
69
|
clearTokens() {
|
|
75
70
|
this.accessToken = null;
|
|
76
|
-
this.refreshToken = null;
|
|
77
71
|
}
|
|
78
72
|
hasAccessToken() {
|
|
79
73
|
return !!this.accessToken;
|
|
@@ -105,13 +99,12 @@ export class HttpService {
|
|
|
105
99
|
constructor(config) {
|
|
106
100
|
this.tokenRefreshPromise = null;
|
|
107
101
|
this.tokenRefreshCooldownUntil = 0;
|
|
108
|
-
this.
|
|
102
|
+
this.authRefreshHandler = null;
|
|
109
103
|
/**
|
|
110
104
|
* Fan-out listeners notified on EVERY access-token change on this instance:
|
|
111
|
-
* explicit `setTokens`, `clearTokens`,
|
|
112
|
-
* internal 401-driven clear.
|
|
113
|
-
*
|
|
114
|
-
* independent observers can mirror token state without clobbering each other.
|
|
105
|
+
* explicit `setTokens`, `clearTokens`, an AuthManager-owned refresh, and the
|
|
106
|
+
* internal 401-driven clear. This is a Set so multiple independent observers
|
|
107
|
+
* can mirror token state without clobbering each other.
|
|
115
108
|
*
|
|
116
109
|
* Each listener receives the resulting access token, or `null` when cleared.
|
|
117
110
|
*/
|
|
@@ -298,23 +291,13 @@ export class HttpService {
|
|
|
298
291
|
clearTimeout(timeoutId);
|
|
299
292
|
// Handle response
|
|
300
293
|
if (!response.ok) {
|
|
301
|
-
// On 401,
|
|
294
|
+
// On 401, delegate refresh to AuthManager and retry once before
|
|
295
|
+
// giving up. HttpService deliberately does not know any session
|
|
296
|
+
// routes; the AuthManager is the single session authority.
|
|
302
297
|
if (response.status === 401 && !config._isAuthRetry) {
|
|
303
|
-
const
|
|
304
|
-
if (
|
|
305
|
-
|
|
306
|
-
const decoded = jwtDecode(currentToken);
|
|
307
|
-
if (decoded.sessionId) {
|
|
308
|
-
const refreshResult = await this._refreshTokenFromSession(decoded.sessionId);
|
|
309
|
-
if (refreshResult) {
|
|
310
|
-
// Retry the request with the new token
|
|
311
|
-
return this.request({ ...config, _isAuthRetry: true, retry: false });
|
|
312
|
-
}
|
|
313
|
-
}
|
|
314
|
-
}
|
|
315
|
-
catch {
|
|
316
|
-
// Token decode failed, fall through to clear
|
|
317
|
-
}
|
|
298
|
+
const refreshed = await this.refreshAccessToken('response-401');
|
|
299
|
+
if (refreshed) {
|
|
300
|
+
return this.request({ ...config, _isAuthRetry: true, retry: false });
|
|
318
301
|
}
|
|
319
302
|
// Refresh failed or no token — clear tokens and stale CSRF
|
|
320
303
|
this.tokenStore.clearTokens();
|
|
@@ -341,7 +324,7 @@ export class HttpService {
|
|
|
341
324
|
if (contentType && contentType.includes('application/json')) {
|
|
342
325
|
try {
|
|
343
326
|
const errorData = await response.json();
|
|
344
|
-
//
|
|
327
|
+
// Accept either structured error field from API responses.
|
|
345
328
|
if (errorData?.message) {
|
|
346
329
|
errorMessage = errorData.message;
|
|
347
330
|
}
|
|
@@ -708,26 +691,10 @@ export class HttpService {
|
|
|
708
691
|
const decoded = jwtDecode(accessToken);
|
|
709
692
|
const currentTime = Math.floor(Date.now() / 1000);
|
|
710
693
|
// If token expires in less than 60 seconds, refresh it
|
|
711
|
-
if (decoded.exp && decoded.exp - currentTime < 60
|
|
712
|
-
|
|
713
|
-
if (
|
|
714
|
-
return `Bearer ${
|
|
715
|
-
}
|
|
716
|
-
// Deduplicate concurrent refresh attempts. The promise is shared
|
|
717
|
-
// across all concurrent callers and cleared only after it settles,
|
|
718
|
-
// so every awaiter receives the same result.
|
|
719
|
-
if (!this.tokenRefreshPromise) {
|
|
720
|
-
this.tokenRefreshPromise = this._refreshTokenFromSession(decoded.sessionId)
|
|
721
|
-
.then((result) => {
|
|
722
|
-
if (!result)
|
|
723
|
-
this.tokenRefreshCooldownUntil = Date.now() + 15000;
|
|
724
|
-
return result;
|
|
725
|
-
})
|
|
726
|
-
.finally(() => { this.tokenRefreshPromise = null; });
|
|
727
|
-
}
|
|
728
|
-
const result = await this.tokenRefreshPromise;
|
|
729
|
-
if (result)
|
|
730
|
-
return result;
|
|
694
|
+
if (decoded.exp && decoded.exp - currentTime < 60) {
|
|
695
|
+
const refreshed = await this.refreshAccessToken('preflight');
|
|
696
|
+
if (refreshed)
|
|
697
|
+
return `Bearer ${refreshed}`;
|
|
731
698
|
// Refresh failed — don't use the expired token (would cause 401 loop)
|
|
732
699
|
return null;
|
|
733
700
|
}
|
|
@@ -738,28 +705,37 @@ export class HttpService {
|
|
|
738
705
|
return null;
|
|
739
706
|
}
|
|
740
707
|
}
|
|
741
|
-
async
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
signal: AbortSignal.timeout(5000),
|
|
748
|
-
credentials: 'include',
|
|
749
|
-
});
|
|
750
|
-
if (response.ok) {
|
|
751
|
-
const { accessToken: newToken } = await response.json();
|
|
752
|
-
this.tokenStore.setTokens(newToken);
|
|
753
|
-
this._onTokenRefreshed?.(newToken);
|
|
754
|
-
this.notifyTokenChange();
|
|
755
|
-
this.logger.debug('Token refreshed');
|
|
756
|
-
return `Bearer ${newToken}`;
|
|
757
|
-
}
|
|
708
|
+
async refreshAccessToken(reason) {
|
|
709
|
+
if (!this.authRefreshHandler) {
|
|
710
|
+
return null;
|
|
711
|
+
}
|
|
712
|
+
if (Date.now() < this.tokenRefreshCooldownUntil) {
|
|
713
|
+
return null;
|
|
758
714
|
}
|
|
759
|
-
|
|
760
|
-
this.
|
|
715
|
+
if (!this.tokenRefreshPromise) {
|
|
716
|
+
this.tokenRefreshPromise = this.authRefreshHandler(reason)
|
|
717
|
+
.then((newToken) => {
|
|
718
|
+
if (!newToken) {
|
|
719
|
+
this.tokenRefreshCooldownUntil = Date.now() + 15000;
|
|
720
|
+
return null;
|
|
721
|
+
}
|
|
722
|
+
if (this.tokenStore.getAccessToken() !== newToken) {
|
|
723
|
+
this.tokenStore.setTokens(newToken);
|
|
724
|
+
this.notifyTokenChange();
|
|
725
|
+
}
|
|
726
|
+
this.logger.debug('Token refreshed via AuthManager');
|
|
727
|
+
return newToken;
|
|
728
|
+
})
|
|
729
|
+
.catch((error) => {
|
|
730
|
+
this.logger.warn('Token refresh failed:', error);
|
|
731
|
+
this.tokenRefreshCooldownUntil = Date.now() + 15000;
|
|
732
|
+
return null;
|
|
733
|
+
})
|
|
734
|
+
.finally(() => {
|
|
735
|
+
this.tokenRefreshPromise = null;
|
|
736
|
+
});
|
|
761
737
|
}
|
|
762
|
-
return
|
|
738
|
+
return this.tokenRefreshPromise;
|
|
763
739
|
}
|
|
764
740
|
/**
|
|
765
741
|
* Unwrap standardized API response format
|
|
@@ -815,12 +791,12 @@ export class HttpService {
|
|
|
815
791
|
return this._actingAsUserId;
|
|
816
792
|
}
|
|
817
793
|
// Token management
|
|
818
|
-
setTokens(accessToken
|
|
819
|
-
this.tokenStore.setTokens(accessToken
|
|
794
|
+
setTokens(accessToken) {
|
|
795
|
+
this.tokenStore.setTokens(accessToken);
|
|
820
796
|
this.notifyTokenChange();
|
|
821
797
|
}
|
|
822
|
-
|
|
823
|
-
this.
|
|
798
|
+
setAuthRefreshHandler(handler) {
|
|
799
|
+
this.authRefreshHandler = handler;
|
|
824
800
|
}
|
|
825
801
|
clearTokens() {
|
|
826
802
|
this.tokenStore.clearTokens();
|
|
@@ -127,8 +127,8 @@ export class OxyServicesBase {
|
|
|
127
127
|
/**
|
|
128
128
|
* Set authentication tokens
|
|
129
129
|
*/
|
|
130
|
-
setTokens(accessToken
|
|
131
|
-
this.httpService.setTokens(accessToken
|
|
130
|
+
setTokens(accessToken) {
|
|
131
|
+
this.httpService.setTokens(accessToken);
|
|
132
132
|
}
|
|
133
133
|
/**
|
|
134
134
|
* Clear stored authentication tokens
|
package/dist/esm/i18n/index.js
CHANGED
|
@@ -41,8 +41,14 @@ export function translate(locale, key, vars) {
|
|
|
41
41
|
const lang = locale && DICTS[locale] ? locale : FALLBACK;
|
|
42
42
|
const dict = DICTS[lang] || DICTS[FALLBACK];
|
|
43
43
|
let val = getNested(dict, key);
|
|
44
|
+
// Per-key fallback to the English dictionary when a key is missing from the
|
|
45
|
+
// resolved (non-English) locale. Without this, a key present in en-US but not
|
|
46
|
+
// yet translated in e.g. es-ES would render the raw dotted key to users.
|
|
47
|
+
if (typeof val !== 'string' && lang !== FALLBACK) {
|
|
48
|
+
val = getNested(DICTS[FALLBACK], key);
|
|
49
|
+
}
|
|
44
50
|
if (typeof val !== 'string')
|
|
45
|
-
return key; //
|
|
51
|
+
return key; // last resort: echo the key when truly absent everywhere
|
|
46
52
|
if (vars) {
|
|
47
53
|
Object.keys(vars).forEach(k => {
|
|
48
54
|
const token = `{{${k}}}`;
|
|
@@ -103,7 +103,9 @@
|
|
|
103
103
|
"createAccount": "إنشاء حساب",
|
|
104
104
|
"signIn": "تسجيل الدخول",
|
|
105
105
|
"verify": "التحقق",
|
|
106
|
-
"resetPassword": "إعادة تعيين كلمة المرور"
|
|
106
|
+
"resetPassword": "إعادة تعيين كلمة المرور",
|
|
107
|
+
"signedOut": "تم تسجيل الخروج",
|
|
108
|
+
"close": "إغلاق"
|
|
107
109
|
},
|
|
108
110
|
"links": {
|
|
109
111
|
"recoverAccount": "استعادة حسابك",
|
|
@@ -115,7 +117,10 @@
|
|
|
115
117
|
"password": "كلمة المرور",
|
|
116
118
|
"confirmPassword": "تأكيد كلمة المرور"
|
|
117
119
|
},
|
|
118
|
-
"revoke": "Revoke"
|
|
120
|
+
"revoke": "Revoke",
|
|
121
|
+
"errors": {
|
|
122
|
+
"signOutAllFailed": "حدثت مشكلة أثناء تسجيل الخروج من جميع الحسابات. يرجى المحاولة مرة أخرى."
|
|
123
|
+
}
|
|
119
124
|
},
|
|
120
125
|
"notifications": {
|
|
121
126
|
"title": "Notifications",
|
|
@@ -198,5 +203,16 @@
|
|
|
198
203
|
"revoked": "Revoked access for {{name}}",
|
|
199
204
|
"revokeFailed": "Failed to revoke access"
|
|
200
205
|
}
|
|
206
|
+
},
|
|
207
|
+
"accountMenu": {
|
|
208
|
+
"label": "قائمة الحساب",
|
|
209
|
+
"manage": "إدارة حساب Oxy الخاص بك",
|
|
210
|
+
"addAnother": "إضافة حساب آخر",
|
|
211
|
+
"signOutAll": "تسجيل الخروج من جميع الحسابات",
|
|
212
|
+
"open": "قائمة الحساب",
|
|
213
|
+
"openHint": "يفتح قائمة الحساب",
|
|
214
|
+
"openWithUser": "قائمة حساب {{name}}",
|
|
215
|
+
"switching": "جارٍ تبديل الحساب…",
|
|
216
|
+
"signOutAccount": "تسجيل خروج {{name}}"
|
|
201
217
|
}
|
|
202
218
|
}
|
|
@@ -103,7 +103,9 @@
|
|
|
103
103
|
"createAccount": "Crear compte",
|
|
104
104
|
"signIn": "Iniciar sessió",
|
|
105
105
|
"verify": "Verificar",
|
|
106
|
-
"resetPassword": "Restablir contrasenya"
|
|
106
|
+
"resetPassword": "Restablir contrasenya",
|
|
107
|
+
"signedOut": "Sessió tancada",
|
|
108
|
+
"close": "Tanca"
|
|
107
109
|
},
|
|
108
110
|
"links": {
|
|
109
111
|
"recoverAccount": "Recuperar el teu compte",
|
|
@@ -115,7 +117,10 @@
|
|
|
115
117
|
"password": "Contrasenya",
|
|
116
118
|
"confirmPassword": "Confirmar contrasenya"
|
|
117
119
|
},
|
|
118
|
-
"revoke": "Revoke"
|
|
120
|
+
"revoke": "Revoke",
|
|
121
|
+
"errors": {
|
|
122
|
+
"signOutAllFailed": "Hi ha hagut un problema en tancar la sessió de tots els comptes. Torna-ho a provar."
|
|
123
|
+
}
|
|
119
124
|
},
|
|
120
125
|
"notifications": {
|
|
121
126
|
"title": "Notifications",
|
|
@@ -198,5 +203,16 @@
|
|
|
198
203
|
"revoked": "Revoked access for {{name}}",
|
|
199
204
|
"revokeFailed": "Failed to revoke access"
|
|
200
205
|
}
|
|
206
|
+
},
|
|
207
|
+
"accountMenu": {
|
|
208
|
+
"label": "Menú del compte",
|
|
209
|
+
"manage": "Gestiona el teu compte d'Oxy",
|
|
210
|
+
"addAnother": "Afegeix un altre compte",
|
|
211
|
+
"signOutAll": "Tanca la sessió de tots els comptes",
|
|
212
|
+
"open": "Menú del compte",
|
|
213
|
+
"openHint": "Obre el menú del compte",
|
|
214
|
+
"openWithUser": "Menú del compte de {{name}}",
|
|
215
|
+
"switching": "Canviant de compte…",
|
|
216
|
+
"signOutAccount": "Tanca la sessió de {{name}}"
|
|
201
217
|
}
|
|
202
218
|
}
|
|
@@ -103,7 +103,9 @@
|
|
|
103
103
|
"createAccount": "Konto erstellen",
|
|
104
104
|
"signIn": "Anmelden",
|
|
105
105
|
"verify": "Überprüfen",
|
|
106
|
-
"resetPassword": "Passwort zurücksetzen"
|
|
106
|
+
"resetPassword": "Passwort zurücksetzen",
|
|
107
|
+
"signedOut": "Abgemeldet",
|
|
108
|
+
"close": "Schließen"
|
|
107
109
|
},
|
|
108
110
|
"links": {
|
|
109
111
|
"recoverAccount": "Ihr Konto wiederherstellen",
|
|
@@ -115,7 +117,10 @@
|
|
|
115
117
|
"password": "Passwort",
|
|
116
118
|
"confirmPassword": "Passwort bestätigen"
|
|
117
119
|
},
|
|
118
|
-
"revoke": "Revoke"
|
|
120
|
+
"revoke": "Revoke",
|
|
121
|
+
"errors": {
|
|
122
|
+
"signOutAllFailed": "Beim Abmelden von allen Konten ist ein Problem aufgetreten. Bitte versuchen Sie es erneut."
|
|
123
|
+
}
|
|
119
124
|
},
|
|
120
125
|
"notifications": {
|
|
121
126
|
"title": "Notifications",
|
|
@@ -198,5 +203,16 @@
|
|
|
198
203
|
"revoked": "Revoked access for {{name}}",
|
|
199
204
|
"revokeFailed": "Failed to revoke access"
|
|
200
205
|
}
|
|
206
|
+
},
|
|
207
|
+
"accountMenu": {
|
|
208
|
+
"label": "Kontomenü",
|
|
209
|
+
"manage": "Ihr Oxy-Konto verwalten",
|
|
210
|
+
"addAnother": "Weiteres Konto hinzufügen",
|
|
211
|
+
"signOutAll": "Von allen Konten abmelden",
|
|
212
|
+
"open": "Kontomenü",
|
|
213
|
+
"openHint": "Öffnet das Kontomenü",
|
|
214
|
+
"openWithUser": "Kontomenü für {{name}}",
|
|
215
|
+
"switching": "Konto wird gewechselt…",
|
|
216
|
+
"signOutAccount": "{{name}} abmelden"
|
|
201
217
|
}
|
|
202
218
|
}
|
|
@@ -1038,7 +1038,8 @@
|
|
|
1038
1038
|
},
|
|
1039
1039
|
"common": {
|
|
1040
1040
|
"errors": {
|
|
1041
|
-
"signOutFailed": "There was a problem signing you out. Please try again."
|
|
1041
|
+
"signOutFailed": "There was a problem signing you out. Please try again.",
|
|
1042
|
+
"signOutAllFailed": "There was a problem signing out of all accounts. Please try again."
|
|
1042
1043
|
},
|
|
1043
1044
|
"confirms": {
|
|
1044
1045
|
"signOut": "Are you sure you want to sign out?",
|
|
@@ -1057,7 +1058,9 @@
|
|
|
1057
1058
|
"signIn": "Sign In",
|
|
1058
1059
|
"signOut": "Sign Out",
|
|
1059
1060
|
"verify": "Verify",
|
|
1060
|
-
"resetPassword": "Reset Password"
|
|
1061
|
+
"resetPassword": "Reset Password",
|
|
1062
|
+
"signedOut": "Signed out",
|
|
1063
|
+
"close": "Close"
|
|
1061
1064
|
},
|
|
1062
1065
|
"cancel": "Cancel",
|
|
1063
1066
|
"revoke": "Revoke",
|
|
@@ -1495,5 +1498,16 @@
|
|
|
1495
1498
|
"revoked": "Revoked access for {{name}}",
|
|
1496
1499
|
"revokeFailed": "Failed to revoke access"
|
|
1497
1500
|
}
|
|
1501
|
+
},
|
|
1502
|
+
"accountMenu": {
|
|
1503
|
+
"label": "Account menu",
|
|
1504
|
+
"manage": "Manage your Oxy Account",
|
|
1505
|
+
"addAnother": "Add another account",
|
|
1506
|
+
"signOutAll": "Sign out of all accounts",
|
|
1507
|
+
"open": "Account menu",
|
|
1508
|
+
"openHint": "Opens the account menu",
|
|
1509
|
+
"openWithUser": "Account menu for {{name}}",
|
|
1510
|
+
"switching": "Switching account…",
|
|
1511
|
+
"signOutAccount": "Sign out {{name}}"
|
|
1498
1512
|
}
|
|
1499
1513
|
}
|
|
@@ -281,7 +281,9 @@
|
|
|
281
281
|
"signIn": "Entrar",
|
|
282
282
|
"signOut": "Cerrar sesión",
|
|
283
283
|
"verify": "Verificar",
|
|
284
|
-
"resetPassword": "Restablecer contraseña"
|
|
284
|
+
"resetPassword": "Restablecer contraseña",
|
|
285
|
+
"signedOut": "Sesión cerrada",
|
|
286
|
+
"close": "Cerrar"
|
|
285
287
|
},
|
|
286
288
|
"cancel": "Cancelar",
|
|
287
289
|
"revoke": "Revocar",
|
|
@@ -302,7 +304,8 @@
|
|
|
302
304
|
"confirmPassword": "Confirmar contraseña"
|
|
303
305
|
},
|
|
304
306
|
"errors": {
|
|
305
|
-
"signOutFailed": "Hubo un problema al cerrar sesión. Inténtalo de nuevo."
|
|
307
|
+
"signOutFailed": "Hubo un problema al cerrar sesión. Inténtalo de nuevo.",
|
|
308
|
+
"signOutAllFailed": "Hubo un problema al cerrar sesión en todas las cuentas. Inténtalo de nuevo."
|
|
306
309
|
},
|
|
307
310
|
"confirms": {
|
|
308
311
|
"signOut": "¿Seguro que quieres cerrar sesión?",
|
|
@@ -1495,5 +1498,16 @@
|
|
|
1495
1498
|
"revoked": "Acceso revocado para {{name}}",
|
|
1496
1499
|
"revokeFailed": "No se ha podido revocar el acceso"
|
|
1497
1500
|
}
|
|
1501
|
+
},
|
|
1502
|
+
"accountMenu": {
|
|
1503
|
+
"label": "Menú de cuenta",
|
|
1504
|
+
"manage": "Gestiona tu cuenta de Oxy",
|
|
1505
|
+
"addAnother": "Añadir otra cuenta",
|
|
1506
|
+
"signOutAll": "Cerrar sesión en todas las cuentas",
|
|
1507
|
+
"open": "Menú de cuenta",
|
|
1508
|
+
"openHint": "Abre el menú de cuenta",
|
|
1509
|
+
"openWithUser": "Menú de cuenta de {{name}}",
|
|
1510
|
+
"switching": "Cambiando de cuenta…",
|
|
1511
|
+
"signOutAccount": "Cerrar sesión de {{name}}"
|
|
1498
1512
|
}
|
|
1499
1513
|
}
|