@oxyhq/core 1.0.2 → 1.2.0
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/AuthManager.js +35 -10
- package/dist/cjs/CrossDomainAuth.js +2 -2
- package/dist/cjs/HttpService.js +40 -24
- package/dist/cjs/OxyServices.base.js +16 -3
- package/dist/cjs/crypto/keyManager.js +29 -24
- package/dist/cjs/crypto/polyfill.js +6 -1
- package/dist/cjs/crypto/signatureService.js +40 -31
- package/dist/cjs/i18n/index.js +36 -45
- package/dist/cjs/i18n/locales/ar-SA.json +114 -115
- package/dist/cjs/i18n/locales/ca-ES.json +114 -115
- package/dist/cjs/i18n/locales/de-DE.json +114 -115
- package/dist/cjs/i18n/locales/en-US.json +936 -936
- package/dist/cjs/i18n/locales/es-ES.json +924 -924
- package/dist/cjs/i18n/locales/fr-FR.json +114 -115
- package/dist/cjs/i18n/locales/it-IT.json +114 -115
- package/dist/cjs/i18n/locales/ja-JP.json +1 -1
- package/dist/cjs/i18n/locales/ko-KR.json +114 -115
- package/dist/cjs/i18n/locales/locales/ar-SA.json +120 -0
- package/dist/cjs/i18n/locales/locales/ca-ES.json +120 -0
- package/dist/cjs/i18n/locales/locales/de-DE.json +120 -0
- package/dist/cjs/i18n/locales/locales/en-US.json +956 -0
- package/dist/cjs/i18n/locales/locales/es-ES.json +944 -0
- package/dist/cjs/i18n/locales/locales/fr-FR.json +120 -0
- package/dist/cjs/i18n/locales/locales/it-IT.json +120 -0
- package/dist/cjs/i18n/locales/locales/ja-JP.json +119 -0
- package/dist/cjs/i18n/locales/locales/ko-KR.json +120 -0
- package/dist/cjs/i18n/locales/locales/pt-PT.json +120 -0
- package/dist/cjs/i18n/locales/locales/zh-CN.json +120 -0
- package/dist/cjs/i18n/locales/pt-PT.json +114 -115
- package/dist/cjs/i18n/locales/zh-CN.json +114 -115
- package/dist/cjs/mixins/OxyServices.fedcm.js +21 -45
- package/dist/cjs/mixins/OxyServices.language.js +5 -2
- package/dist/cjs/mixins/OxyServices.popup.js +16 -6
- package/dist/cjs/mixins/OxyServices.privacy.js +2 -1
- package/dist/cjs/mixins/OxyServices.redirect.js +16 -6
- package/dist/cjs/mixins/OxyServices.security.js +3 -2
- package/dist/cjs/shared/utils/debugUtils.js +8 -1
- package/dist/cjs/utils/deviceManager.js +4 -6
- package/dist/cjs/utils/platform.js +3 -2
- package/dist/esm/AuthManager.js +35 -10
- package/dist/esm/CrossDomainAuth.js +2 -2
- package/dist/esm/HttpService.js +40 -24
- package/dist/esm/OxyServices.base.js +16 -3
- package/dist/esm/crypto/keyManager.js +29 -24
- package/dist/esm/crypto/polyfill.js +6 -1
- package/dist/esm/crypto/signatureService.js +40 -31
- package/dist/esm/i18n/index.js +11 -23
- package/dist/esm/i18n/locales/ar-SA.json +114 -115
- package/dist/esm/i18n/locales/ca-ES.json +114 -115
- package/dist/esm/i18n/locales/de-DE.json +114 -115
- package/dist/esm/i18n/locales/en-US.json +936 -936
- package/dist/esm/i18n/locales/es-ES.json +924 -924
- package/dist/esm/i18n/locales/fr-FR.json +114 -115
- package/dist/esm/i18n/locales/it-IT.json +114 -115
- package/dist/esm/i18n/locales/ja-JP.json +1 -1
- package/dist/esm/i18n/locales/ko-KR.json +114 -115
- package/dist/esm/i18n/locales/locales/ar-SA.json +120 -0
- package/dist/esm/i18n/locales/locales/ca-ES.json +120 -0
- package/dist/esm/i18n/locales/locales/de-DE.json +120 -0
- package/dist/esm/i18n/locales/locales/en-US.json +956 -0
- package/dist/esm/i18n/locales/locales/es-ES.json +944 -0
- package/dist/esm/i18n/locales/locales/fr-FR.json +120 -0
- package/dist/esm/i18n/locales/locales/it-IT.json +120 -0
- package/dist/esm/i18n/locales/locales/ja-JP.json +119 -0
- package/dist/esm/i18n/locales/locales/ko-KR.json +120 -0
- package/dist/esm/i18n/locales/locales/pt-PT.json +120 -0
- package/dist/esm/i18n/locales/locales/zh-CN.json +120 -0
- package/dist/esm/i18n/locales/pt-PT.json +114 -115
- package/dist/esm/i18n/locales/zh-CN.json +114 -115
- package/dist/esm/mixins/OxyServices.fedcm.js +21 -45
- package/dist/esm/mixins/OxyServices.language.js +5 -2
- package/dist/esm/mixins/OxyServices.popup.js +16 -6
- package/dist/esm/mixins/OxyServices.privacy.js +2 -1
- package/dist/esm/mixins/OxyServices.redirect.js +16 -6
- package/dist/esm/mixins/OxyServices.security.js +3 -2
- package/dist/esm/shared/utils/debugUtils.js +8 -1
- package/dist/esm/utils/deviceManager.js +4 -6
- package/dist/esm/utils/platform.js +3 -2
- package/dist/types/AuthManager.d.ts +4 -1
- package/dist/types/CrossDomainAuth.d.ts +2 -2
- package/dist/types/HttpService.d.ts +2 -0
- package/dist/types/OxyServices.base.d.ts +4 -1
- package/dist/types/OxyServices.d.ts +13 -0
- package/dist/types/index.d.ts +3 -0
- package/dist/types/mixins/OxyServices.analytics.d.ts +2 -0
- package/dist/types/mixins/OxyServices.assets.d.ts +2 -0
- package/dist/types/mixins/OxyServices.auth.d.ts +2 -0
- package/dist/types/mixins/OxyServices.developer.d.ts +2 -0
- package/dist/types/mixins/OxyServices.devices.d.ts +2 -0
- package/dist/types/mixins/OxyServices.features.d.ts +2 -0
- package/dist/types/mixins/OxyServices.fedcm.d.ts +4 -2
- package/dist/types/mixins/OxyServices.karma.d.ts +2 -0
- package/dist/types/mixins/OxyServices.language.d.ts +2 -0
- package/dist/types/mixins/OxyServices.location.d.ts +2 -0
- package/dist/types/mixins/OxyServices.payment.d.ts +2 -0
- package/dist/types/mixins/OxyServices.popup.d.ts +2 -0
- package/dist/types/mixins/OxyServices.privacy.d.ts +2 -0
- package/dist/types/mixins/OxyServices.redirect.d.ts +2 -0
- package/dist/types/mixins/OxyServices.security.d.ts +2 -0
- package/dist/types/mixins/OxyServices.user.d.ts +2 -0
- package/dist/types/mixins/OxyServices.utility.d.ts +2 -0
- package/package.json +1 -2
- package/src/AuthManager.ts +42 -16
- package/src/CrossDomainAuth.ts +2 -2
- package/src/HttpService.ts +40 -26
- package/src/OxyServices.base.ts +21 -4
- package/src/OxyServices.ts +23 -2
- package/src/crypto/keyManager.ts +30 -25
- package/src/crypto/polyfill.ts +6 -1
- package/src/crypto/signatureService.ts +43 -37
- package/src/i18n/index.ts +33 -45
- package/src/index.ts +3 -0
- package/src/mixins/OxyServices.fedcm.ts +22 -48
- package/src/mixins/OxyServices.language.ts +6 -3
- package/src/mixins/OxyServices.popup.ts +16 -6
- package/src/mixins/OxyServices.privacy.ts +2 -1
- package/src/mixins/OxyServices.redirect.ts +16 -6
- package/src/mixins/OxyServices.security.ts +3 -2
- package/src/shared/utils/__tests__/debugUtils.test.ts +55 -0
- package/src/shared/utils/debugUtils.ts +6 -1
- package/src/utils/deviceManager.ts +5 -6
- package/src/utils/platform.ts +3 -2
|
@@ -151,7 +151,10 @@ function OxyServicesFedCMMixin(Base) {
|
|
|
151
151
|
}
|
|
152
152
|
const clientId = this.getClientId();
|
|
153
153
|
debug.log('Silent SSO: Starting for', clientId);
|
|
154
|
-
//
|
|
154
|
+
// Only try silent mediation (no UI) - works if user previously consented.
|
|
155
|
+
// We intentionally do NOT fall back to optional mediation here because
|
|
156
|
+
// this runs on app startup — showing browser UI without user action is bad UX.
|
|
157
|
+
// Optional/interactive mediation should only happen when the user clicks "Sign In".
|
|
155
158
|
let credential = null;
|
|
156
159
|
try {
|
|
157
160
|
const nonce = this.generateNonce();
|
|
@@ -165,33 +168,13 @@ function OxyServicesFedCMMixin(Base) {
|
|
|
165
168
|
debug.log('Silent SSO: Silent mediation result:', { hasCredential: !!credential, hasToken: !!credential?.token });
|
|
166
169
|
}
|
|
167
170
|
catch (silentError) {
|
|
168
|
-
// Silent mediation failed - this is expected if user hasn't consented before or is in quiet period
|
|
169
171
|
const errorName = silentError instanceof Error ? silentError.name : 'Unknown';
|
|
170
172
|
const errorMessage = silentError instanceof Error ? silentError.message : String(silentError);
|
|
171
|
-
debug.log('Silent SSO: Silent mediation
|
|
172
|
-
|
|
173
|
-
// If silent failed, try optional mediation which shows browser UI if needed
|
|
174
|
-
if (!credential || !credential.token) {
|
|
175
|
-
try {
|
|
176
|
-
const nonce = this.generateNonce();
|
|
177
|
-
debug.log('Silent SSO: Trying optional mediation (may show browser UI)...');
|
|
178
|
-
credential = await this.requestIdentityCredential({
|
|
179
|
-
configURL: this.constructor.DEFAULT_CONFIG_URL,
|
|
180
|
-
clientId,
|
|
181
|
-
nonce,
|
|
182
|
-
mediation: 'optional',
|
|
183
|
-
});
|
|
184
|
-
debug.log('Silent SSO: Optional mediation result:', { hasCredential: !!credential, hasToken: !!credential?.token });
|
|
185
|
-
}
|
|
186
|
-
catch (optionalError) {
|
|
187
|
-
const errorName = optionalError instanceof Error ? optionalError.name : 'Unknown';
|
|
188
|
-
const errorMessage = optionalError instanceof Error ? optionalError.message : String(optionalError);
|
|
189
|
-
debug.log('Silent SSO: Optional mediation also failed:', { name: errorName, message: errorMessage });
|
|
190
|
-
return null;
|
|
191
|
-
}
|
|
173
|
+
debug.log('Silent SSO: Silent mediation failed:', { name: errorName, message: errorMessage });
|
|
174
|
+
return null;
|
|
192
175
|
}
|
|
193
176
|
if (!credential || !credential.token) {
|
|
194
|
-
debug.log('Silent SSO: No credential returned (user
|
|
177
|
+
debug.log('Silent SSO: No credential returned (user not logged in at IdP or hasn\'t consented)');
|
|
195
178
|
return null;
|
|
196
179
|
}
|
|
197
180
|
debug.log('Silent SSO: Got credential, exchanging for session...');
|
|
@@ -341,28 +324,17 @@ function OxyServicesFedCMMixin(Base) {
|
|
|
341
324
|
* @private
|
|
342
325
|
*/
|
|
343
326
|
async exchangeIdTokenForSession(idToken) {
|
|
344
|
-
debug.log('
|
|
345
|
-
debug.log('exchangeIdTokenForSession: Token length:', idToken?.length);
|
|
346
|
-
debug.log('exchangeIdTokenForSession: Token preview:', idToken?.substring(0, 50) + '...');
|
|
327
|
+
debug.log('Exchanging ID token for session...');
|
|
347
328
|
try {
|
|
348
329
|
const response = await this.makeRequest('POST', '/api/fedcm/exchange', { id_token: idToken }, { cache: false });
|
|
349
|
-
debug.log('
|
|
350
|
-
|
|
351
|
-
hasSessionId: !!response?.sessionId,
|
|
330
|
+
debug.log('Token exchange complete:', {
|
|
331
|
+
hasSession: !!response?.sessionId,
|
|
352
332
|
hasUser: !!response?.user,
|
|
353
|
-
hasAccessToken: !!response?.accessToken,
|
|
354
|
-
userId: response?.user?.id,
|
|
355
|
-
username: response?.user?.username,
|
|
356
|
-
responseKeys: response ? Object.keys(response) : [],
|
|
357
333
|
});
|
|
358
334
|
return response;
|
|
359
335
|
}
|
|
360
336
|
catch (error) {
|
|
361
|
-
debug.error('
|
|
362
|
-
name: error instanceof Error ? error.name : 'Unknown',
|
|
363
|
-
message: error instanceof Error ? error.message : String(error),
|
|
364
|
-
stack: error instanceof Error ? error.stack : undefined,
|
|
365
|
-
});
|
|
337
|
+
debug.error('Token exchange failed:', error instanceof Error ? error.message : String(error));
|
|
366
338
|
throw error;
|
|
367
339
|
}
|
|
368
340
|
}
|
|
@@ -408,11 +380,15 @@ function OxyServicesFedCMMixin(Base) {
|
|
|
408
380
|
* @private
|
|
409
381
|
*/
|
|
410
382
|
generateNonce() {
|
|
411
|
-
if (typeof
|
|
412
|
-
return
|
|
383
|
+
if (typeof crypto !== 'undefined' && crypto.randomUUID) {
|
|
384
|
+
return crypto.randomUUID();
|
|
385
|
+
}
|
|
386
|
+
if (typeof crypto !== 'undefined' && crypto.getRandomValues) {
|
|
387
|
+
const bytes = new Uint8Array(16);
|
|
388
|
+
crypto.getRandomValues(bytes);
|
|
389
|
+
return Array.from(bytes, b => b.toString(16).padStart(2, '0')).join('');
|
|
413
390
|
}
|
|
414
|
-
|
|
415
|
-
return `${Date.now()}-${Math.random().toString(36).substring(2, 15)}`;
|
|
391
|
+
throw new Error('No secure random source available for nonce generation');
|
|
416
392
|
}
|
|
417
393
|
/**
|
|
418
394
|
* Get the client ID for this origin
|
|
@@ -427,9 +403,9 @@ function OxyServicesFedCMMixin(Base) {
|
|
|
427
403
|
}
|
|
428
404
|
},
|
|
429
405
|
_a.DEFAULT_CONFIG_URL = 'https://auth.oxy.so/fedcm.json',
|
|
430
|
-
_a.FEDCM_TIMEOUT =
|
|
406
|
+
_a.FEDCM_TIMEOUT = 15000 // 15 seconds for interactive
|
|
431
407
|
,
|
|
432
|
-
_a.FEDCM_SILENT_TIMEOUT =
|
|
408
|
+
_a.FEDCM_SILENT_TIMEOUT = 3000 // 3 seconds for silent mediation
|
|
433
409
|
,
|
|
434
410
|
_a;
|
|
435
411
|
}
|
|
@@ -38,6 +38,7 @@ exports.OxyServicesLanguageMixin = OxyServicesLanguageMixin;
|
|
|
38
38
|
* Language Methods Mixin
|
|
39
39
|
*/
|
|
40
40
|
const languageUtils_1 = require("../utils/languageUtils");
|
|
41
|
+
const debugUtils_1 = require("../shared/utils/debugUtils");
|
|
41
42
|
function OxyServicesLanguageMixin(Base) {
|
|
42
43
|
return class extends Base {
|
|
43
44
|
constructor(...args) {
|
|
@@ -50,7 +51,9 @@ function OxyServicesLanguageMixin(Base) {
|
|
|
50
51
|
const isReactNative = typeof navigator !== 'undefined' && navigator.product === 'ReactNative';
|
|
51
52
|
if (isReactNative) {
|
|
52
53
|
try {
|
|
53
|
-
|
|
54
|
+
// Variable indirection prevents bundlers (Vite, webpack) from statically resolving this
|
|
55
|
+
const moduleName = '@react-native-async-storage/async-storage';
|
|
56
|
+
const asyncStorageModule = await Promise.resolve(`${moduleName}`).then(s => __importStar(require(s)));
|
|
54
57
|
const storage = asyncStorageModule.default;
|
|
55
58
|
return {
|
|
56
59
|
getItem: storage.getItem.bind(storage),
|
|
@@ -113,7 +116,7 @@ function OxyServicesLanguageMixin(Base) {
|
|
|
113
116
|
return null;
|
|
114
117
|
}
|
|
115
118
|
catch (error) {
|
|
116
|
-
if (
|
|
119
|
+
if ((0, debugUtils_1.isDev)()) {
|
|
117
120
|
console.warn('Failed to get current language:', error);
|
|
118
121
|
}
|
|
119
122
|
return null;
|
|
@@ -323,10 +323,15 @@ function OxyServicesPopupAuthMixin(Base) {
|
|
|
323
323
|
* @private
|
|
324
324
|
*/
|
|
325
325
|
generateState() {
|
|
326
|
-
if (typeof
|
|
327
|
-
return
|
|
326
|
+
if (typeof crypto !== 'undefined' && crypto.randomUUID) {
|
|
327
|
+
return crypto.randomUUID();
|
|
328
328
|
}
|
|
329
|
-
|
|
329
|
+
if (typeof crypto !== 'undefined' && crypto.getRandomValues) {
|
|
330
|
+
const bytes = new Uint8Array(16);
|
|
331
|
+
crypto.getRandomValues(bytes);
|
|
332
|
+
return Array.from(bytes, b => b.toString(16).padStart(2, '0')).join('');
|
|
333
|
+
}
|
|
334
|
+
throw new Error('No secure random source available for state generation');
|
|
330
335
|
}
|
|
331
336
|
/**
|
|
332
337
|
* Generate nonce for replay attack prevention
|
|
@@ -334,10 +339,15 @@ function OxyServicesPopupAuthMixin(Base) {
|
|
|
334
339
|
* @private
|
|
335
340
|
*/
|
|
336
341
|
generateNonce() {
|
|
337
|
-
if (typeof
|
|
338
|
-
return
|
|
342
|
+
if (typeof crypto !== 'undefined' && crypto.randomUUID) {
|
|
343
|
+
return crypto.randomUUID();
|
|
344
|
+
}
|
|
345
|
+
if (typeof crypto !== 'undefined' && crypto.getRandomValues) {
|
|
346
|
+
const bytes = new Uint8Array(16);
|
|
347
|
+
crypto.getRandomValues(bytes);
|
|
348
|
+
return Array.from(bytes, b => b.toString(16).padStart(2, '0')).join('');
|
|
339
349
|
}
|
|
340
|
-
|
|
350
|
+
throw new Error('No secure random source available for nonce generation');
|
|
341
351
|
}
|
|
342
352
|
/**
|
|
343
353
|
* Store auth state in session storage
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.OxyServicesPrivacyMixin = OxyServicesPrivacyMixin;
|
|
4
|
+
const debugUtils_1 = require("../shared/utils/debugUtils");
|
|
4
5
|
function OxyServicesPrivacyMixin(Base) {
|
|
5
6
|
return class extends Base {
|
|
6
7
|
constructor(...args) {
|
|
@@ -28,7 +29,7 @@ function OxyServicesPrivacyMixin(Base) {
|
|
|
28
29
|
}
|
|
29
30
|
catch (error) {
|
|
30
31
|
// If there's an error, assume not in list to avoid breaking functionality
|
|
31
|
-
if (
|
|
32
|
+
if ((0, debugUtils_1.isDev)()) {
|
|
32
33
|
console.warn('Error checking user list:', error);
|
|
33
34
|
}
|
|
34
35
|
return false;
|
|
@@ -254,10 +254,15 @@ function OxyServicesRedirectAuthMixin(Base) {
|
|
|
254
254
|
* @private
|
|
255
255
|
*/
|
|
256
256
|
generateState() {
|
|
257
|
-
if (typeof
|
|
258
|
-
return
|
|
257
|
+
if (typeof crypto !== 'undefined' && crypto.randomUUID) {
|
|
258
|
+
return crypto.randomUUID();
|
|
259
259
|
}
|
|
260
|
-
|
|
260
|
+
if (typeof crypto !== 'undefined' && crypto.getRandomValues) {
|
|
261
|
+
const bytes = new Uint8Array(16);
|
|
262
|
+
crypto.getRandomValues(bytes);
|
|
263
|
+
return Array.from(bytes, b => b.toString(16).padStart(2, '0')).join('');
|
|
264
|
+
}
|
|
265
|
+
throw new Error('No secure random source available for state generation');
|
|
261
266
|
}
|
|
262
267
|
/**
|
|
263
268
|
* Generate nonce for replay attack prevention
|
|
@@ -265,10 +270,15 @@ function OxyServicesRedirectAuthMixin(Base) {
|
|
|
265
270
|
* @private
|
|
266
271
|
*/
|
|
267
272
|
generateNonce() {
|
|
268
|
-
if (typeof
|
|
269
|
-
return
|
|
273
|
+
if (typeof crypto !== 'undefined' && crypto.randomUUID) {
|
|
274
|
+
return crypto.randomUUID();
|
|
275
|
+
}
|
|
276
|
+
if (typeof crypto !== 'undefined' && crypto.getRandomValues) {
|
|
277
|
+
const bytes = new Uint8Array(16);
|
|
278
|
+
crypto.getRandomValues(bytes);
|
|
279
|
+
return Array.from(bytes, b => b.toString(16).padStart(2, '0')).join('');
|
|
270
280
|
}
|
|
271
|
-
|
|
281
|
+
throw new Error('No secure random source available for nonce generation');
|
|
272
282
|
}
|
|
273
283
|
/**
|
|
274
284
|
* Store auth state in session storage
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.OxyServicesSecurityMixin = OxyServicesSecurityMixin;
|
|
4
|
+
const debugUtils_1 = require("../shared/utils/debugUtils");
|
|
4
5
|
function OxyServicesSecurityMixin(Base) {
|
|
5
6
|
return class extends Base {
|
|
6
7
|
constructor(...args) {
|
|
@@ -55,7 +56,7 @@ function OxyServicesSecurityMixin(Base) {
|
|
|
55
56
|
catch (error) {
|
|
56
57
|
// Don't throw - logging failures shouldn't break user flow
|
|
57
58
|
// But log for monitoring
|
|
58
|
-
if (
|
|
59
|
+
if ((0, debugUtils_1.isDev)()) {
|
|
59
60
|
console.warn('[OxyServices] Failed to log private key exported event:', error);
|
|
60
61
|
}
|
|
61
62
|
}
|
|
@@ -72,7 +73,7 @@ function OxyServicesSecurityMixin(Base) {
|
|
|
72
73
|
catch (error) {
|
|
73
74
|
// Don't throw - logging failures shouldn't break user flow
|
|
74
75
|
// But log for monitoring
|
|
75
|
-
if (
|
|
76
|
+
if ((0, debugUtils_1.isDev)()) {
|
|
76
77
|
console.warn('[OxyServices] Failed to log backup created event:', error);
|
|
77
78
|
}
|
|
78
79
|
}
|
|
@@ -13,7 +13,14 @@ exports.createDebugLogger = exports.debugError = exports.debugWarn = exports.deb
|
|
|
13
13
|
* Check if running in development mode
|
|
14
14
|
*/
|
|
15
15
|
const isDev = () => {
|
|
16
|
-
|
|
16
|
+
if (typeof __DEV__ !== 'undefined')
|
|
17
|
+
return __DEV__;
|
|
18
|
+
try {
|
|
19
|
+
return typeof process !== 'undefined' && process.env?.NODE_ENV === 'development';
|
|
20
|
+
}
|
|
21
|
+
catch {
|
|
22
|
+
return false;
|
|
23
|
+
}
|
|
17
24
|
};
|
|
18
25
|
exports.isDev = isDev;
|
|
19
26
|
/**
|
|
@@ -51,7 +51,9 @@ class DeviceManager {
|
|
|
51
51
|
static async getStorage() {
|
|
52
52
|
if (this.isReactNative()) {
|
|
53
53
|
try {
|
|
54
|
-
|
|
54
|
+
// Variable indirection prevents bundlers (Vite, webpack) from statically resolving this
|
|
55
|
+
const moduleName = '@react-native-async-storage/async-storage';
|
|
56
|
+
const asyncStorageModule = await Promise.resolve(`${moduleName}`).then(s => __importStar(require(s)));
|
|
55
57
|
const storage = asyncStorageModule.default;
|
|
56
58
|
return {
|
|
57
59
|
getItem: storage.getItem.bind(storage),
|
|
@@ -169,16 +171,12 @@ class DeviceManager {
|
|
|
169
171
|
* Generate a unique device ID
|
|
170
172
|
*/
|
|
171
173
|
static generateDeviceId() {
|
|
172
|
-
// Use crypto.getRandomValues if available, otherwise fallback to Math.random
|
|
173
174
|
if (typeof crypto !== 'undefined' && crypto.getRandomValues) {
|
|
174
175
|
const array = new Uint8Array(32);
|
|
175
176
|
crypto.getRandomValues(array);
|
|
176
177
|
return Array.from(array, byte => byte.toString(16).padStart(2, '0')).join('');
|
|
177
178
|
}
|
|
178
|
-
|
|
179
|
-
// Fallback for environments without crypto.getRandomValues
|
|
180
|
-
return 'device_' + Date.now().toString(36) + Math.random().toString(36).substr(2);
|
|
181
|
-
}
|
|
179
|
+
throw new Error('No secure random source available for device ID generation');
|
|
182
180
|
}
|
|
183
181
|
/**
|
|
184
182
|
* Get a user-friendly device name based on platform
|
|
@@ -134,8 +134,9 @@ async function initPlatformFromReactNative() {
|
|
|
134
134
|
return; // Already initialized
|
|
135
135
|
}
|
|
136
136
|
try {
|
|
137
|
-
//
|
|
138
|
-
const
|
|
137
|
+
// Variable indirection prevents bundlers (Vite, webpack) from statically resolving this
|
|
138
|
+
const moduleName = 'react-native';
|
|
139
|
+
const { Platform } = await Promise.resolve(`${moduleName}`).then(s => __importStar(require(s)));
|
|
139
140
|
setPlatformOS(Platform.OS);
|
|
140
141
|
}
|
|
141
142
|
catch {
|
package/dist/esm/AuthManager.js
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
*
|
|
7
7
|
* @module core/AuthManager
|
|
8
8
|
*/
|
|
9
|
+
import { retryAsync } from './utils/asyncUtils';
|
|
9
10
|
/**
|
|
10
11
|
* Storage keys used by AuthManager.
|
|
11
12
|
*/
|
|
@@ -98,6 +99,7 @@ export class AuthManager {
|
|
|
98
99
|
this.listeners = new Set();
|
|
99
100
|
this.currentUser = null;
|
|
100
101
|
this.refreshTimer = null;
|
|
102
|
+
this.refreshPromise = null;
|
|
101
103
|
this.oxyServices = oxyServices;
|
|
102
104
|
this.config = {
|
|
103
105
|
storage: config.storage ?? this.getDefaultStorage(),
|
|
@@ -198,27 +200,50 @@ export class AuthManager {
|
|
|
198
200
|
}
|
|
199
201
|
}
|
|
200
202
|
/**
|
|
201
|
-
* Refresh the access token.
|
|
203
|
+
* Refresh the access token. Deduplicates concurrent calls so only one
|
|
204
|
+
* refresh request is in-flight at a time.
|
|
202
205
|
*/
|
|
203
206
|
async refreshToken() {
|
|
207
|
+
// If a refresh is already in-flight, return the same promise
|
|
208
|
+
if (this.refreshPromise) {
|
|
209
|
+
return this.refreshPromise;
|
|
210
|
+
}
|
|
211
|
+
this.refreshPromise = this._doRefreshToken();
|
|
212
|
+
try {
|
|
213
|
+
return await this.refreshPromise;
|
|
214
|
+
}
|
|
215
|
+
finally {
|
|
216
|
+
this.refreshPromise = null;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
async _doRefreshToken() {
|
|
204
220
|
const refreshToken = await this.storage.getItem(STORAGE_KEYS.REFRESH_TOKEN);
|
|
205
221
|
if (!refreshToken) {
|
|
206
222
|
return false;
|
|
207
223
|
}
|
|
208
224
|
try {
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
225
|
+
await retryAsync(async () => {
|
|
226
|
+
const httpService = this.oxyServices.httpService;
|
|
227
|
+
const response = await httpService.request({
|
|
228
|
+
method: 'POST',
|
|
229
|
+
url: '/api/auth/refresh',
|
|
230
|
+
data: { refreshToken },
|
|
231
|
+
cache: false,
|
|
232
|
+
});
|
|
233
|
+
await this.handleAuthSuccess(response, 'credentials');
|
|
234
|
+
}, 2, // 2 retries = 3 total attempts
|
|
235
|
+
1000, // 1s base delay with exponential backoff + jitter
|
|
236
|
+
(error) => {
|
|
237
|
+
// Don't retry on 4xx client errors (invalid/revoked token)
|
|
238
|
+
const status = error?.status ?? error?.response?.status;
|
|
239
|
+
if (status && status >= 400 && status < 500)
|
|
240
|
+
return false;
|
|
241
|
+
return true;
|
|
216
242
|
});
|
|
217
|
-
await this.handleAuthSuccess(response, 'credentials');
|
|
218
243
|
return true;
|
|
219
244
|
}
|
|
220
245
|
catch {
|
|
221
|
-
//
|
|
246
|
+
// All retry attempts exhausted, clear session
|
|
222
247
|
await this.clearSession();
|
|
223
248
|
this.currentUser = null;
|
|
224
249
|
this.notifyListeners();
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
*
|
|
11
11
|
* Usage:
|
|
12
12
|
* ```typescript
|
|
13
|
-
* import { CrossDomainAuth } from '@oxyhq/
|
|
13
|
+
* import { CrossDomainAuth } from '@oxyhq/core';
|
|
14
14
|
*
|
|
15
15
|
* const auth = new CrossDomainAuth(oxyServices);
|
|
16
16
|
*
|
|
@@ -233,7 +233,7 @@ export class CrossDomainAuth {
|
|
|
233
233
|
*
|
|
234
234
|
* @example
|
|
235
235
|
* ```typescript
|
|
236
|
-
* import { createCrossDomainAuth } from '@oxyhq/
|
|
236
|
+
* import { createCrossDomainAuth } from '@oxyhq/core';
|
|
237
237
|
*
|
|
238
238
|
* const oxyServices = new OxyServices({ baseURL: 'https://api.oxy.so' });
|
|
239
239
|
* const auth = createCrossDomainAuth(oxyServices);
|
package/dist/esm/HttpService.js
CHANGED
|
@@ -16,6 +16,7 @@ import { TTLCache, registerCacheForCleanup } from './utils/cache';
|
|
|
16
16
|
import { RequestDeduplicator, RequestQueue, SimpleLogger } from './utils/requestUtils';
|
|
17
17
|
import { retryAsync } from './utils/asyncUtils';
|
|
18
18
|
import { handleHttpError } from './utils/errorUtils';
|
|
19
|
+
import { isDev } from './shared/utils/debugUtils';
|
|
19
20
|
import { jwtDecode } from 'jwt-decode';
|
|
20
21
|
import { isNative, getPlatformOS } from './utils/platform';
|
|
21
22
|
/**
|
|
@@ -81,6 +82,7 @@ class TokenStore {
|
|
|
81
82
|
*/
|
|
82
83
|
export class HttpService {
|
|
83
84
|
constructor(config) {
|
|
85
|
+
this.tokenRefreshPromise = null;
|
|
84
86
|
// Performance monitoring
|
|
85
87
|
this.requestMetrics = {
|
|
86
88
|
totalRequests: 0,
|
|
@@ -186,7 +188,7 @@ export class HttpService {
|
|
|
186
188
|
headers['X-Native-App'] = 'true';
|
|
187
189
|
}
|
|
188
190
|
// Debug logging for CSRF issues
|
|
189
|
-
if (isStateChangingMethod &&
|
|
191
|
+
if (isStateChangingMethod && isDev()) {
|
|
190
192
|
console.log('[HttpService] CSRF Debug:', {
|
|
191
193
|
url,
|
|
192
194
|
method,
|
|
@@ -370,20 +372,20 @@ export class HttpService {
|
|
|
370
372
|
// Return cached token if available
|
|
371
373
|
const cachedToken = this.tokenStore.getCsrfToken();
|
|
372
374
|
if (cachedToken) {
|
|
373
|
-
if (
|
|
375
|
+
if (isDev())
|
|
374
376
|
console.log('[HttpService] Using cached CSRF token');
|
|
375
377
|
return cachedToken;
|
|
376
378
|
}
|
|
377
379
|
// Deduplicate concurrent CSRF token fetches
|
|
378
380
|
const existingPromise = this.tokenStore.getCsrfTokenFetchPromise();
|
|
379
381
|
if (existingPromise) {
|
|
380
|
-
if (
|
|
382
|
+
if (isDev())
|
|
381
383
|
console.log('[HttpService] Waiting for existing CSRF fetch');
|
|
382
384
|
return existingPromise;
|
|
383
385
|
}
|
|
384
386
|
const fetchPromise = (async () => {
|
|
385
387
|
try {
|
|
386
|
-
if (
|
|
388
|
+
if (isDev())
|
|
387
389
|
console.log('[HttpService] Fetching CSRF token from:', `${this.baseURL}/api/csrf-token`);
|
|
388
390
|
// Use AbortController for timeout (more compatible than AbortSignal.timeout)
|
|
389
391
|
const controller = new AbortController();
|
|
@@ -395,11 +397,11 @@ export class HttpService {
|
|
|
395
397
|
signal: controller.signal,
|
|
396
398
|
});
|
|
397
399
|
clearTimeout(timeoutId);
|
|
398
|
-
if (
|
|
400
|
+
if (isDev())
|
|
399
401
|
console.log('[HttpService] CSRF fetch response:', response.status, response.ok);
|
|
400
402
|
if (response.ok) {
|
|
401
403
|
const data = await response.json();
|
|
402
|
-
if (
|
|
404
|
+
if (isDev())
|
|
403
405
|
console.log('[HttpService] CSRF response data:', data);
|
|
404
406
|
const token = data.csrfToken || null;
|
|
405
407
|
this.tokenStore.setCsrfToken(token);
|
|
@@ -413,13 +415,13 @@ export class HttpService {
|
|
|
413
415
|
this.logger.debug('CSRF token from header');
|
|
414
416
|
return headerToken;
|
|
415
417
|
}
|
|
416
|
-
if (
|
|
418
|
+
if (isDev())
|
|
417
419
|
console.log('[HttpService] CSRF fetch failed with status:', response.status);
|
|
418
420
|
this.logger.warn('Failed to fetch CSRF token:', response.status);
|
|
419
421
|
return null;
|
|
420
422
|
}
|
|
421
423
|
catch (error) {
|
|
422
|
-
if (
|
|
424
|
+
if (isDev())
|
|
423
425
|
console.log('[HttpService] CSRF fetch error:', error);
|
|
424
426
|
this.logger.warn('CSRF token fetch error:', error);
|
|
425
427
|
return null;
|
|
@@ -444,24 +446,17 @@ export class HttpService {
|
|
|
444
446
|
const currentTime = Math.floor(Date.now() / 1000);
|
|
445
447
|
// If token expires in less than 60 seconds, refresh it
|
|
446
448
|
if (decoded.exp && decoded.exp - currentTime < 60 && decoded.sessionId) {
|
|
449
|
+
// Deduplicate concurrent refresh attempts
|
|
450
|
+
if (!this.tokenRefreshPromise) {
|
|
451
|
+
this.tokenRefreshPromise = this._refreshTokenFromSession(decoded.sessionId);
|
|
452
|
+
}
|
|
447
453
|
try {
|
|
448
|
-
const
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
method: 'GET',
|
|
452
|
-
headers: { 'Accept': 'application/json' },
|
|
453
|
-
signal: AbortSignal.timeout(5000),
|
|
454
|
-
credentials: 'include', // Include cookies for cross-origin requests
|
|
455
|
-
});
|
|
456
|
-
if (response.ok) {
|
|
457
|
-
const { accessToken: newToken } = await response.json();
|
|
458
|
-
this.tokenStore.setTokens(newToken);
|
|
459
|
-
this.logger.debug('Token refreshed');
|
|
460
|
-
return `Bearer ${newToken}`;
|
|
461
|
-
}
|
|
454
|
+
const result = await this.tokenRefreshPromise;
|
|
455
|
+
if (result)
|
|
456
|
+
return result;
|
|
462
457
|
}
|
|
463
|
-
|
|
464
|
-
this.
|
|
458
|
+
finally {
|
|
459
|
+
this.tokenRefreshPromise = null;
|
|
465
460
|
}
|
|
466
461
|
}
|
|
467
462
|
return `Bearer ${accessToken}`;
|
|
@@ -471,6 +466,27 @@ export class HttpService {
|
|
|
471
466
|
return `Bearer ${accessToken}`;
|
|
472
467
|
}
|
|
473
468
|
}
|
|
469
|
+
async _refreshTokenFromSession(sessionId) {
|
|
470
|
+
try {
|
|
471
|
+
const refreshUrl = `${this.baseURL}/api/session/token/${sessionId}`;
|
|
472
|
+
const response = await fetch(refreshUrl, {
|
|
473
|
+
method: 'GET',
|
|
474
|
+
headers: { 'Accept': 'application/json' },
|
|
475
|
+
signal: AbortSignal.timeout(5000),
|
|
476
|
+
credentials: 'include',
|
|
477
|
+
});
|
|
478
|
+
if (response.ok) {
|
|
479
|
+
const { accessToken: newToken } = await response.json();
|
|
480
|
+
this.tokenStore.setTokens(newToken);
|
|
481
|
+
this.logger.debug('Token refreshed');
|
|
482
|
+
return `Bearer ${newToken}`;
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
catch (refreshError) {
|
|
486
|
+
this.logger.warn('Token refresh failed, using current token');
|
|
487
|
+
}
|
|
488
|
+
return null;
|
|
489
|
+
}
|
|
474
490
|
/**
|
|
475
491
|
* Unwrap standardized API response format
|
|
476
492
|
*/
|
|
@@ -12,6 +12,8 @@ import { OxyAuthenticationError, OxyAuthenticationTimeoutError } from './OxyServ
|
|
|
12
12
|
*/
|
|
13
13
|
export class OxyServicesBase {
|
|
14
14
|
constructor(...args) {
|
|
15
|
+
/** @internal */ this._cachedUserId = undefined;
|
|
16
|
+
/** @internal */ this._cachedAccessToken = null;
|
|
15
17
|
const config = args[0];
|
|
16
18
|
if (!config || typeof config !== 'object') {
|
|
17
19
|
throw new Error('OxyConfig is required');
|
|
@@ -95,20 +97,31 @@ export class OxyServicesBase {
|
|
|
95
97
|
*/
|
|
96
98
|
clearTokens() {
|
|
97
99
|
this.httpService.clearTokens();
|
|
100
|
+
this._cachedUserId = undefined;
|
|
101
|
+
this._cachedAccessToken = null;
|
|
98
102
|
}
|
|
99
103
|
/**
|
|
100
|
-
* Get the current user ID from the access token
|
|
104
|
+
* Get the current user ID from the access token.
|
|
105
|
+
* Caches the decoded value and invalidates when the token changes.
|
|
101
106
|
*/
|
|
102
107
|
getCurrentUserId() {
|
|
103
108
|
const accessToken = this.httpService.getAccessToken();
|
|
109
|
+
// Return cached value if token hasn't changed
|
|
110
|
+
if (accessToken === this._cachedAccessToken && this._cachedUserId !== undefined) {
|
|
111
|
+
return this._cachedUserId;
|
|
112
|
+
}
|
|
113
|
+
this._cachedAccessToken = accessToken;
|
|
104
114
|
if (!accessToken) {
|
|
115
|
+
this._cachedUserId = null;
|
|
105
116
|
return null;
|
|
106
117
|
}
|
|
107
118
|
try {
|
|
108
119
|
const decoded = jwtDecode(accessToken);
|
|
109
|
-
|
|
120
|
+
this._cachedUserId = decoded.userId || decoded.id || null;
|
|
121
|
+
return this._cachedUserId;
|
|
110
122
|
}
|
|
111
|
-
catch
|
|
123
|
+
catch {
|
|
124
|
+
this._cachedUserId = null;
|
|
112
125
|
return null;
|
|
113
126
|
}
|
|
114
127
|
}
|