@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.
Files changed (122) hide show
  1. package/dist/cjs/AuthManager.js +35 -10
  2. package/dist/cjs/CrossDomainAuth.js +2 -2
  3. package/dist/cjs/HttpService.js +40 -24
  4. package/dist/cjs/OxyServices.base.js +16 -3
  5. package/dist/cjs/crypto/keyManager.js +29 -24
  6. package/dist/cjs/crypto/polyfill.js +6 -1
  7. package/dist/cjs/crypto/signatureService.js +40 -31
  8. package/dist/cjs/i18n/index.js +36 -45
  9. package/dist/cjs/i18n/locales/ar-SA.json +114 -115
  10. package/dist/cjs/i18n/locales/ca-ES.json +114 -115
  11. package/dist/cjs/i18n/locales/de-DE.json +114 -115
  12. package/dist/cjs/i18n/locales/en-US.json +936 -936
  13. package/dist/cjs/i18n/locales/es-ES.json +924 -924
  14. package/dist/cjs/i18n/locales/fr-FR.json +114 -115
  15. package/dist/cjs/i18n/locales/it-IT.json +114 -115
  16. package/dist/cjs/i18n/locales/ja-JP.json +1 -1
  17. package/dist/cjs/i18n/locales/ko-KR.json +114 -115
  18. package/dist/cjs/i18n/locales/locales/ar-SA.json +120 -0
  19. package/dist/cjs/i18n/locales/locales/ca-ES.json +120 -0
  20. package/dist/cjs/i18n/locales/locales/de-DE.json +120 -0
  21. package/dist/cjs/i18n/locales/locales/en-US.json +956 -0
  22. package/dist/cjs/i18n/locales/locales/es-ES.json +944 -0
  23. package/dist/cjs/i18n/locales/locales/fr-FR.json +120 -0
  24. package/dist/cjs/i18n/locales/locales/it-IT.json +120 -0
  25. package/dist/cjs/i18n/locales/locales/ja-JP.json +119 -0
  26. package/dist/cjs/i18n/locales/locales/ko-KR.json +120 -0
  27. package/dist/cjs/i18n/locales/locales/pt-PT.json +120 -0
  28. package/dist/cjs/i18n/locales/locales/zh-CN.json +120 -0
  29. package/dist/cjs/i18n/locales/pt-PT.json +114 -115
  30. package/dist/cjs/i18n/locales/zh-CN.json +114 -115
  31. package/dist/cjs/mixins/OxyServices.fedcm.js +21 -45
  32. package/dist/cjs/mixins/OxyServices.language.js +5 -2
  33. package/dist/cjs/mixins/OxyServices.popup.js +16 -6
  34. package/dist/cjs/mixins/OxyServices.privacy.js +2 -1
  35. package/dist/cjs/mixins/OxyServices.redirect.js +16 -6
  36. package/dist/cjs/mixins/OxyServices.security.js +3 -2
  37. package/dist/cjs/shared/utils/debugUtils.js +8 -1
  38. package/dist/cjs/utils/deviceManager.js +4 -6
  39. package/dist/cjs/utils/platform.js +3 -2
  40. package/dist/esm/AuthManager.js +35 -10
  41. package/dist/esm/CrossDomainAuth.js +2 -2
  42. package/dist/esm/HttpService.js +40 -24
  43. package/dist/esm/OxyServices.base.js +16 -3
  44. package/dist/esm/crypto/keyManager.js +29 -24
  45. package/dist/esm/crypto/polyfill.js +6 -1
  46. package/dist/esm/crypto/signatureService.js +40 -31
  47. package/dist/esm/i18n/index.js +11 -23
  48. package/dist/esm/i18n/locales/ar-SA.json +114 -115
  49. package/dist/esm/i18n/locales/ca-ES.json +114 -115
  50. package/dist/esm/i18n/locales/de-DE.json +114 -115
  51. package/dist/esm/i18n/locales/en-US.json +936 -936
  52. package/dist/esm/i18n/locales/es-ES.json +924 -924
  53. package/dist/esm/i18n/locales/fr-FR.json +114 -115
  54. package/dist/esm/i18n/locales/it-IT.json +114 -115
  55. package/dist/esm/i18n/locales/ja-JP.json +1 -1
  56. package/dist/esm/i18n/locales/ko-KR.json +114 -115
  57. package/dist/esm/i18n/locales/locales/ar-SA.json +120 -0
  58. package/dist/esm/i18n/locales/locales/ca-ES.json +120 -0
  59. package/dist/esm/i18n/locales/locales/de-DE.json +120 -0
  60. package/dist/esm/i18n/locales/locales/en-US.json +956 -0
  61. package/dist/esm/i18n/locales/locales/es-ES.json +944 -0
  62. package/dist/esm/i18n/locales/locales/fr-FR.json +120 -0
  63. package/dist/esm/i18n/locales/locales/it-IT.json +120 -0
  64. package/dist/esm/i18n/locales/locales/ja-JP.json +119 -0
  65. package/dist/esm/i18n/locales/locales/ko-KR.json +120 -0
  66. package/dist/esm/i18n/locales/locales/pt-PT.json +120 -0
  67. package/dist/esm/i18n/locales/locales/zh-CN.json +120 -0
  68. package/dist/esm/i18n/locales/pt-PT.json +114 -115
  69. package/dist/esm/i18n/locales/zh-CN.json +114 -115
  70. package/dist/esm/mixins/OxyServices.fedcm.js +21 -45
  71. package/dist/esm/mixins/OxyServices.language.js +5 -2
  72. package/dist/esm/mixins/OxyServices.popup.js +16 -6
  73. package/dist/esm/mixins/OxyServices.privacy.js +2 -1
  74. package/dist/esm/mixins/OxyServices.redirect.js +16 -6
  75. package/dist/esm/mixins/OxyServices.security.js +3 -2
  76. package/dist/esm/shared/utils/debugUtils.js +8 -1
  77. package/dist/esm/utils/deviceManager.js +4 -6
  78. package/dist/esm/utils/platform.js +3 -2
  79. package/dist/types/AuthManager.d.ts +4 -1
  80. package/dist/types/CrossDomainAuth.d.ts +2 -2
  81. package/dist/types/HttpService.d.ts +2 -0
  82. package/dist/types/OxyServices.base.d.ts +4 -1
  83. package/dist/types/OxyServices.d.ts +13 -0
  84. package/dist/types/index.d.ts +3 -0
  85. package/dist/types/mixins/OxyServices.analytics.d.ts +2 -0
  86. package/dist/types/mixins/OxyServices.assets.d.ts +2 -0
  87. package/dist/types/mixins/OxyServices.auth.d.ts +2 -0
  88. package/dist/types/mixins/OxyServices.developer.d.ts +2 -0
  89. package/dist/types/mixins/OxyServices.devices.d.ts +2 -0
  90. package/dist/types/mixins/OxyServices.features.d.ts +2 -0
  91. package/dist/types/mixins/OxyServices.fedcm.d.ts +4 -2
  92. package/dist/types/mixins/OxyServices.karma.d.ts +2 -0
  93. package/dist/types/mixins/OxyServices.language.d.ts +2 -0
  94. package/dist/types/mixins/OxyServices.location.d.ts +2 -0
  95. package/dist/types/mixins/OxyServices.payment.d.ts +2 -0
  96. package/dist/types/mixins/OxyServices.popup.d.ts +2 -0
  97. package/dist/types/mixins/OxyServices.privacy.d.ts +2 -0
  98. package/dist/types/mixins/OxyServices.redirect.d.ts +2 -0
  99. package/dist/types/mixins/OxyServices.security.d.ts +2 -0
  100. package/dist/types/mixins/OxyServices.user.d.ts +2 -0
  101. package/dist/types/mixins/OxyServices.utility.d.ts +2 -0
  102. package/package.json +1 -2
  103. package/src/AuthManager.ts +42 -16
  104. package/src/CrossDomainAuth.ts +2 -2
  105. package/src/HttpService.ts +40 -26
  106. package/src/OxyServices.base.ts +21 -4
  107. package/src/OxyServices.ts +23 -2
  108. package/src/crypto/keyManager.ts +30 -25
  109. package/src/crypto/polyfill.ts +6 -1
  110. package/src/crypto/signatureService.ts +43 -37
  111. package/src/i18n/index.ts +33 -45
  112. package/src/index.ts +3 -0
  113. package/src/mixins/OxyServices.fedcm.ts +22 -48
  114. package/src/mixins/OxyServices.language.ts +6 -3
  115. package/src/mixins/OxyServices.popup.ts +16 -6
  116. package/src/mixins/OxyServices.privacy.ts +2 -1
  117. package/src/mixins/OxyServices.redirect.ts +16 -6
  118. package/src/mixins/OxyServices.security.ts +3 -2
  119. package/src/shared/utils/__tests__/debugUtils.test.ts +55 -0
  120. package/src/shared/utils/debugUtils.ts +6 -1
  121. package/src/utils/deviceManager.ts +5 -6
  122. 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
- // First try silent mediation (no UI) - works if user previously consented
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 error (will try optional):', { name: errorName, message: errorMessage });
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 may have dismissed prompt or is not logged in at IdP)');
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('exchangeIdTokenForSession: Starting exchange...');
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('exchangeIdTokenForSession: Response received:', {
350
- hasResponse: !!response,
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('exchangeIdTokenForSession: 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 window !== 'undefined' && window.crypto && window.crypto.randomUUID) {
412
- return window.crypto.randomUUID();
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
- // Fallback for older browsers
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 = 60000 // 1 minute for interactive
406
+ _a.FEDCM_TIMEOUT = 15000 // 15 seconds for interactive
431
407
  ,
432
- _a.FEDCM_SILENT_TIMEOUT = 10000 // 10 seconds for silent mediation
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
- const asyncStorageModule = await Promise.resolve().then(() => __importStar(require('@react-native-async-storage/async-storage')));
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 (__DEV__) {
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 window !== 'undefined' && window.crypto && window.crypto.randomUUID) {
327
- return window.crypto.randomUUID();
326
+ if (typeof crypto !== 'undefined' && crypto.randomUUID) {
327
+ return crypto.randomUUID();
328
328
  }
329
- return `${Date.now()}-${Math.random().toString(36).substring(2, 15)}`;
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 window !== 'undefined' && window.crypto && window.crypto.randomUUID) {
338
- return window.crypto.randomUUID();
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
- return `${Date.now()}-${Math.random().toString(36).substring(2, 15)}`;
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 (__DEV__) {
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 window !== 'undefined' && window.crypto && window.crypto.randomUUID) {
258
- return window.crypto.randomUUID();
257
+ if (typeof crypto !== 'undefined' && crypto.randomUUID) {
258
+ return crypto.randomUUID();
259
259
  }
260
- return `${Date.now()}-${Math.random().toString(36).substring(2, 15)}`;
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 window !== 'undefined' && window.crypto && window.crypto.randomUUID) {
269
- return window.crypto.randomUUID();
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
- return `${Date.now()}-${Math.random().toString(36).substring(2, 15)}`;
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 (__DEV__) {
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 (__DEV__) {
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
- return typeof __DEV__ !== 'undefined' && __DEV__;
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
- const asyncStorageModule = await Promise.resolve().then(() => __importStar(require('@react-native-async-storage/async-storage')));
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
- else {
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
- // Dynamic import to avoid bundler issues
138
- const { Platform } = await Promise.resolve().then(() => __importStar(require('react-native')));
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 {
@@ -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
- // Cast httpService to proper type (needed due to mixin composition)
210
- const httpService = this.oxyServices.httpService;
211
- const response = await httpService.request({
212
- method: 'POST',
213
- url: '/api/auth/refresh',
214
- data: { refreshToken },
215
- cache: false,
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
- // Refresh failed, clear session and update state
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/services';
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/services';
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);
@@ -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 && __DEV__) {
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 (__DEV__)
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 (__DEV__)
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 (__DEV__)
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 (__DEV__)
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 (__DEV__)
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 (__DEV__)
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 (__DEV__)
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 refreshUrl = `${this.baseURL}/api/session/token/${decoded.sessionId}`;
449
- // Use AbortSignal.timeout for consistent timeout handling
450
- const response = await fetch(refreshUrl, {
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
- catch (refreshError) {
464
- this.logger.warn('Token refresh failed, using current token');
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
- return decoded.userId || decoded.id || null;
120
+ this._cachedUserId = decoded.userId || decoded.id || null;
121
+ return this._cachedUserId;
110
122
  }
111
- catch (error) {
123
+ catch {
124
+ this._cachedUserId = null;
112
125
  return null;
113
126
  }
114
127
  }