@oxyhq/core 1.4.0 → 1.6.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 (87) hide show
  1. package/dist/cjs/AuthManager.js +1 -1
  2. package/dist/cjs/HttpService.js +87 -69
  3. package/dist/cjs/OxyServices.base.js +5 -4
  4. package/dist/cjs/crypto/keyManager.js +1 -13
  5. package/dist/cjs/crypto/signatureService.js +7 -20
  6. package/dist/cjs/mixins/OxyServices.analytics.js +2 -2
  7. package/dist/cjs/mixins/OxyServices.assets.js +14 -14
  8. package/dist/cjs/mixins/OxyServices.auth.js +20 -18
  9. package/dist/cjs/mixins/OxyServices.developer.js +6 -6
  10. package/dist/cjs/mixins/OxyServices.devices.js +7 -7
  11. package/dist/cjs/mixins/OxyServices.features.js +23 -23
  12. package/dist/cjs/mixins/OxyServices.fedcm.js +1 -1
  13. package/dist/cjs/mixins/OxyServices.karma.js +6 -6
  14. package/dist/cjs/mixins/OxyServices.location.js +2 -2
  15. package/dist/cjs/mixins/OxyServices.payment.js +6 -6
  16. package/dist/cjs/mixins/OxyServices.popup.js +1 -1
  17. package/dist/cjs/mixins/OxyServices.privacy.js +6 -6
  18. package/dist/cjs/mixins/OxyServices.security.js +3 -3
  19. package/dist/cjs/mixins/OxyServices.user.js +22 -22
  20. package/dist/cjs/mixins/OxyServices.utility.js +16 -7
  21. package/dist/cjs/utils/asyncUtils.js +0 -1
  22. package/dist/cjs/utils/platform.js +14 -0
  23. package/dist/esm/AuthManager.js +1 -1
  24. package/dist/esm/HttpService.js +87 -69
  25. package/dist/esm/OxyServices.base.js +5 -4
  26. package/dist/esm/crypto/keyManager.js +1 -13
  27. package/dist/esm/crypto/signatureService.js +2 -15
  28. package/dist/esm/mixins/OxyServices.analytics.js +2 -2
  29. package/dist/esm/mixins/OxyServices.assets.js +14 -14
  30. package/dist/esm/mixins/OxyServices.auth.js +20 -18
  31. package/dist/esm/mixins/OxyServices.developer.js +6 -6
  32. package/dist/esm/mixins/OxyServices.devices.js +7 -7
  33. package/dist/esm/mixins/OxyServices.features.js +23 -23
  34. package/dist/esm/mixins/OxyServices.fedcm.js +1 -1
  35. package/dist/esm/mixins/OxyServices.karma.js +6 -6
  36. package/dist/esm/mixins/OxyServices.location.js +2 -2
  37. package/dist/esm/mixins/OxyServices.payment.js +6 -6
  38. package/dist/esm/mixins/OxyServices.popup.js +1 -1
  39. package/dist/esm/mixins/OxyServices.privacy.js +6 -6
  40. package/dist/esm/mixins/OxyServices.security.js +3 -3
  41. package/dist/esm/mixins/OxyServices.user.js +22 -22
  42. package/dist/esm/mixins/OxyServices.utility.js +16 -7
  43. package/dist/esm/utils/asyncUtils.js +0 -1
  44. package/dist/esm/utils/platform.js +12 -0
  45. package/dist/types/HttpService.d.ts +4 -1
  46. package/dist/types/OxyServices.base.d.ts +1 -1
  47. package/dist/types/mixins/OxyServices.analytics.d.ts +1 -1
  48. package/dist/types/mixins/OxyServices.assets.d.ts +1 -1
  49. package/dist/types/mixins/OxyServices.auth.d.ts +1 -1
  50. package/dist/types/mixins/OxyServices.developer.d.ts +1 -1
  51. package/dist/types/mixins/OxyServices.devices.d.ts +1 -1
  52. package/dist/types/mixins/OxyServices.features.d.ts +1 -1
  53. package/dist/types/mixins/OxyServices.fedcm.d.ts +1 -1
  54. package/dist/types/mixins/OxyServices.karma.d.ts +1 -1
  55. package/dist/types/mixins/OxyServices.language.d.ts +1 -1
  56. package/dist/types/mixins/OxyServices.location.d.ts +1 -1
  57. package/dist/types/mixins/OxyServices.payment.d.ts +1 -1
  58. package/dist/types/mixins/OxyServices.popup.d.ts +1 -1
  59. package/dist/types/mixins/OxyServices.privacy.d.ts +1 -1
  60. package/dist/types/mixins/OxyServices.redirect.d.ts +1 -1
  61. package/dist/types/mixins/OxyServices.security.d.ts +1 -1
  62. package/dist/types/mixins/OxyServices.user.d.ts +1 -1
  63. package/dist/types/mixins/OxyServices.utility.d.ts +7 -6
  64. package/dist/types/utils/platform.d.ts +8 -0
  65. package/package.json +1 -1
  66. package/src/AuthManager.ts +1 -1
  67. package/src/HttpService.ts +85 -67
  68. package/src/OxyServices.base.ts +5 -4
  69. package/src/crypto/keyManager.ts +1 -15
  70. package/src/crypto/signatureService.ts +2 -17
  71. package/src/mixins/OxyServices.analytics.ts +2 -2
  72. package/src/mixins/OxyServices.assets.ts +14 -14
  73. package/src/mixins/OxyServices.auth.ts +26 -24
  74. package/src/mixins/OxyServices.developer.ts +6 -6
  75. package/src/mixins/OxyServices.devices.ts +7 -7
  76. package/src/mixins/OxyServices.features.ts +23 -23
  77. package/src/mixins/OxyServices.fedcm.ts +1 -1
  78. package/src/mixins/OxyServices.karma.ts +6 -6
  79. package/src/mixins/OxyServices.location.ts +2 -2
  80. package/src/mixins/OxyServices.payment.ts +6 -6
  81. package/src/mixins/OxyServices.popup.ts +1 -1
  82. package/src/mixins/OxyServices.privacy.ts +6 -6
  83. package/src/mixins/OxyServices.security.ts +3 -3
  84. package/src/mixins/OxyServices.user.ts +22 -22
  85. package/src/mixins/OxyServices.utility.ts +18 -8
  86. package/src/utils/asyncUtils.ts +1 -2
  87. package/src/utils/platform.ts +14 -0
@@ -12,7 +12,7 @@ function OxyServicesUserMixin(Base) {
12
12
  */
13
13
  async getProfileByUsername(username) {
14
14
  try {
15
- return await this.makeRequest('GET', `/api/profiles/username/${username}`, undefined, {
15
+ return await this.makeRequest('GET', `/profiles/username/${username}`, undefined, {
16
16
  cache: true,
17
17
  cacheTTL: 5 * 60 * 1000, // 5 minutes cache for profiles
18
18
  });
@@ -29,7 +29,7 @@ function OxyServicesUserMixin(Base) {
29
29
  const params = { query, ...pagination };
30
30
  const searchParams = (0, apiUtils_1.buildSearchParams)(params);
31
31
  const paramsObj = Object.fromEntries(searchParams.entries());
32
- const response = await this.makeRequest('GET', '/api/profiles/search', paramsObj, {
32
+ const response = await this.makeRequest('GET', '/profiles/search', paramsObj, {
33
33
  cache: true,
34
34
  cacheTTL: 2 * 60 * 1000, // 2 minutes cache
35
35
  });
@@ -74,7 +74,7 @@ function OxyServicesUserMixin(Base) {
74
74
  */
75
75
  async getProfileRecommendations() {
76
76
  return this.withAuthRetry(async () => {
77
- return await this.makeRequest('GET', '/api/profiles/recommendations', undefined, { cache: true });
77
+ return await this.makeRequest('GET', '/profiles/recommendations', undefined, { cache: true });
78
78
  }, 'getProfileRecommendations');
79
79
  }
80
80
  /**
@@ -82,7 +82,7 @@ function OxyServicesUserMixin(Base) {
82
82
  */
83
83
  async getUserById(userId) {
84
84
  try {
85
- return await this.makeRequest('GET', `/api/users/${userId}`, undefined, {
85
+ return await this.makeRequest('GET', `/users/${userId}`, undefined, {
86
86
  cache: true,
87
87
  cacheTTL: 5 * 60 * 1000, // 5 minutes cache
88
88
  });
@@ -96,7 +96,7 @@ function OxyServicesUserMixin(Base) {
96
96
  */
97
97
  async getCurrentUser() {
98
98
  return this.withAuthRetry(async () => {
99
- return await this.makeRequest('GET', '/api/users/me', undefined, {
99
+ return await this.makeRequest('GET', '/users/me', undefined, {
100
100
  cache: true,
101
101
  cacheTTL: 1 * 60 * 1000, // 1 minute cache for current user
102
102
  });
@@ -108,7 +108,7 @@ function OxyServicesUserMixin(Base) {
108
108
  */
109
109
  async updateProfile(updates) {
110
110
  try {
111
- return await this.makeRequest('PUT', '/api/users/me', updates, { cache: false });
111
+ return await this.makeRequest('PUT', '/users/me', updates, { cache: false });
112
112
  }
113
113
  catch (error) {
114
114
  const errorAny = error;
@@ -134,7 +134,7 @@ function OxyServicesUserMixin(Base) {
134
134
  async getPrivacySettings(userId) {
135
135
  try {
136
136
  const id = userId || (await this.getCurrentUser()).id;
137
- return await this.makeRequest('GET', `/api/privacy/${id}/privacy`, undefined, {
137
+ return await this.makeRequest('GET', `/privacy/${id}/privacy`, undefined, {
138
138
  cache: true,
139
139
  cacheTTL: 2 * 60 * 1000, // 2 minutes cache
140
140
  });
@@ -151,7 +151,7 @@ function OxyServicesUserMixin(Base) {
151
151
  async updatePrivacySettings(settings, userId) {
152
152
  try {
153
153
  const id = userId || (await this.getCurrentUser()).id;
154
- return await this.makeRequest('PATCH', `/api/privacy/${id}/privacy`, settings, {
154
+ return await this.makeRequest('PATCH', `/privacy/${id}/privacy`, settings, {
155
155
  cache: false,
156
156
  });
157
157
  }
@@ -164,7 +164,7 @@ function OxyServicesUserMixin(Base) {
164
164
  */
165
165
  async requestAccountVerification(reason, evidence) {
166
166
  try {
167
- return await this.makeRequest('POST', '/api/users/verify/request', {
167
+ return await this.makeRequest('POST', '/users/verify/request', {
168
168
  reason,
169
169
  evidence,
170
170
  }, { cache: false });
@@ -181,7 +181,7 @@ function OxyServicesUserMixin(Base) {
181
181
  // Use httpService for blob responses (it handles blob responses automatically)
182
182
  const result = await this.getClient().request({
183
183
  method: 'GET',
184
- url: `/api/users/me/data`,
184
+ url: `/users/me/data`,
185
185
  params: { format },
186
186
  cache: false,
187
187
  });
@@ -198,7 +198,7 @@ function OxyServicesUserMixin(Base) {
198
198
  */
199
199
  async deleteAccount(password, confirmText) {
200
200
  try {
201
- return await this.makeRequest('DELETE', '/api/users/me', {
201
+ return await this.makeRequest('DELETE', '/users/me', {
202
202
  password,
203
203
  confirmText,
204
204
  }, { cache: false });
@@ -212,7 +212,7 @@ function OxyServicesUserMixin(Base) {
212
212
  */
213
213
  async followUser(userId) {
214
214
  try {
215
- return await this.makeRequest('POST', `/api/users/${userId}/follow`, undefined, { cache: false });
215
+ return await this.makeRequest('POST', `/users/${userId}/follow`, undefined, { cache: false });
216
216
  }
217
217
  catch (error) {
218
218
  throw this.handleError(error);
@@ -223,7 +223,7 @@ function OxyServicesUserMixin(Base) {
223
223
  */
224
224
  async unfollowUser(userId) {
225
225
  try {
226
- return await this.makeRequest('DELETE', `/api/users/${userId}/follow`, undefined, { cache: false });
226
+ return await this.makeRequest('DELETE', `/users/${userId}/follow`, undefined, { cache: false });
227
227
  }
228
228
  catch (error) {
229
229
  throw this.handleError(error);
@@ -234,7 +234,7 @@ function OxyServicesUserMixin(Base) {
234
234
  */
235
235
  async getFollowStatus(userId) {
236
236
  try {
237
- return await this.makeRequest('GET', `/api/users/${userId}/follow-status`, undefined, {
237
+ return await this.makeRequest('GET', `/users/${userId}/follow-status`, undefined, {
238
238
  cache: true,
239
239
  cacheTTL: 1 * 60 * 1000, // 1 minute cache
240
240
  });
@@ -249,7 +249,7 @@ function OxyServicesUserMixin(Base) {
249
249
  async getUserFollowers(userId, pagination) {
250
250
  try {
251
251
  const params = (0, apiUtils_1.buildPaginationParams)(pagination || {});
252
- const response = await this.makeRequest('GET', `/api/users/${userId}/followers`, params, {
252
+ const response = await this.makeRequest('GET', `/users/${userId}/followers`, params, {
253
253
  cache: true,
254
254
  cacheTTL: 2 * 60 * 1000, // 2 minutes cache
255
255
  });
@@ -269,7 +269,7 @@ function OxyServicesUserMixin(Base) {
269
269
  async getUserFollowing(userId, pagination) {
270
270
  try {
271
271
  const params = (0, apiUtils_1.buildPaginationParams)(pagination || {});
272
- const response = await this.makeRequest('GET', `/api/users/${userId}/following`, params, {
272
+ const response = await this.makeRequest('GET', `/users/${userId}/following`, params, {
273
273
  cache: true,
274
274
  cacheTTL: 2 * 60 * 1000, // 2 minutes cache
275
275
  });
@@ -288,7 +288,7 @@ function OxyServicesUserMixin(Base) {
288
288
  */
289
289
  async getNotifications() {
290
290
  return this.withAuthRetry(async () => {
291
- return await this.makeRequest('GET', '/api/notifications', undefined, {
291
+ return await this.makeRequest('GET', '/notifications', undefined, {
292
292
  cache: false, // Don't cache notifications - always get fresh data
293
293
  });
294
294
  }, 'getNotifications');
@@ -298,7 +298,7 @@ function OxyServicesUserMixin(Base) {
298
298
  */
299
299
  async getUnreadCount() {
300
300
  try {
301
- const res = await this.makeRequest('GET', '/api/notifications/unread-count', undefined, {
301
+ const res = await this.makeRequest('GET', '/notifications/unread-count', undefined, {
302
302
  cache: false, // Don't cache unread count - always get fresh data
303
303
  });
304
304
  return res.count;
@@ -312,7 +312,7 @@ function OxyServicesUserMixin(Base) {
312
312
  */
313
313
  async createNotification(data) {
314
314
  try {
315
- return await this.makeRequest('POST', '/api/notifications', data, { cache: false });
315
+ return await this.makeRequest('POST', '/notifications', data, { cache: false });
316
316
  }
317
317
  catch (error) {
318
318
  throw this.handleError(error);
@@ -323,7 +323,7 @@ function OxyServicesUserMixin(Base) {
323
323
  */
324
324
  async markNotificationAsRead(notificationId) {
325
325
  try {
326
- await this.makeRequest('PUT', `/api/notifications/${notificationId}/read`, undefined, { cache: false });
326
+ await this.makeRequest('PUT', `/notifications/${notificationId}/read`, undefined, { cache: false });
327
327
  }
328
328
  catch (error) {
329
329
  throw this.handleError(error);
@@ -334,7 +334,7 @@ function OxyServicesUserMixin(Base) {
334
334
  */
335
335
  async markAllNotificationsAsRead() {
336
336
  try {
337
- await this.makeRequest('PUT', '/api/notifications/read-all', undefined, { cache: false });
337
+ await this.makeRequest('PUT', '/notifications/read-all', undefined, { cache: false });
338
338
  }
339
339
  catch (error) {
340
340
  throw this.handleError(error);
@@ -345,7 +345,7 @@ function OxyServicesUserMixin(Base) {
345
345
  */
346
346
  async deleteNotification(notificationId) {
347
347
  try {
348
- await this.makeRequest('DELETE', `/api/notifications/${notificationId}`, undefined, { cache: false });
348
+ await this.makeRequest('DELETE', `/notifications/${notificationId}`, undefined, { cache: false });
349
349
  }
350
350
  catch (error) {
351
351
  throw this.handleError(error);
@@ -52,7 +52,7 @@ function OxyServicesUtilityMixin(Base) {
52
52
  */
53
53
  async fetchLinkMetadata(url) {
54
54
  try {
55
- return await this.makeRequest('GET', '/api/link-metadata', { url }, {
55
+ return await this.makeRequest('GET', '/link-metadata', { url }, {
56
56
  cache: true,
57
57
  cacheTTL: mixinHelpers_1.CACHE_TIMES.EXTRA_LONG,
58
58
  });
@@ -73,19 +73,19 @@ function OxyServicesUtilityMixin(Base) {
73
73
  *
74
74
  * const oxy = new OxyServices({ baseURL: 'https://api.oxy.so' });
75
75
  *
76
- * // Protect all routes under /api/protected
77
- * app.use('/api/protected', oxy.auth());
76
+ * // Protect all routes under /protected
77
+ * app.use('/protected', oxy.auth());
78
78
  *
79
79
  * // Access user in route handler
80
- * app.get('/api/protected/me', (req, res) => {
80
+ * app.get('/protected/me', (req, res) => {
81
81
  * res.json({ userId: req.userId, user: req.user });
82
82
  * });
83
83
  *
84
84
  * // Load full user profile from API
85
- * app.use('/api/admin', oxy.auth({ loadUser: true }));
85
+ * app.use('/admin', oxy.auth({ loadUser: true }));
86
86
  *
87
87
  * // Optional auth - attach user if present, don't block if absent
88
- * app.use('/api/public', oxy.auth({ optional: true }));
88
+ * app.use('/public', oxy.auth({ optional: true }));
89
89
  * ```
90
90
  *
91
91
  * @param options Optional configuration
@@ -187,7 +187,16 @@ function OxyServicesUtilityMixin(Base) {
187
187
  throw new Error('Invalid signature');
188
188
  }
189
189
  }
190
- catch {
190
+ catch (verifyError) {
191
+ const isSignatureError = verifyError instanceof Error &&
192
+ (verifyError.message === 'Invalid signature' || verifyError.message === 'Invalid token structure');
193
+ if (!isSignatureError) {
194
+ console.error('[oxy.auth] Unexpected error during service token verification:', verifyError);
195
+ const error = { message: 'Internal authentication error', code: 'AUTH_INTERNAL_ERROR', status: 500 };
196
+ if (onError)
197
+ return onError(error);
198
+ return res.status(500).json(error);
199
+ }
191
200
  if (optional) {
192
201
  req.userId = null;
193
202
  req.user = null;
@@ -85,7 +85,6 @@ async function retryAsync(operation, maxRetries = 3, baseDelay = 1000, shouldRet
85
85
  */
86
86
  function debounceAsync(func, delay) {
87
87
  let timeoutId;
88
- const lastPromise = null;
89
88
  return (...args) => {
90
89
  return new Promise((resolve, reject) => {
91
90
  clearTimeout(timeoutId);
@@ -12,6 +12,8 @@ exports.isWeb = isWeb;
12
12
  exports.isNative = isNative;
13
13
  exports.isIOS = isIOS;
14
14
  exports.isAndroid = isAndroid;
15
+ exports.isReactNative = isReactNative;
16
+ exports.isNodeJS = isNodeJS;
15
17
  exports.setPlatformOS = setPlatformOS;
16
18
  /**
17
19
  * Detect the current platform without importing react-native
@@ -82,6 +84,18 @@ function isIOS() {
82
84
  function isAndroid() {
83
85
  return getPlatformOS() === 'android';
84
86
  }
87
+ /**
88
+ * Check if running in React Native
89
+ */
90
+ function isReactNative() {
91
+ return typeof navigator !== 'undefined' && navigator.product === 'ReactNative';
92
+ }
93
+ /**
94
+ * Check if running in Node.js
95
+ */
96
+ function isNodeJS() {
97
+ return typeof process !== 'undefined' && process.versions != null && process.versions.node != null;
98
+ }
85
99
  /**
86
100
  * Set the platform OS explicitly
87
101
  * Called by React Native entry point to register the platform
@@ -243,7 +243,7 @@ export class AuthManager {
243
243
  // Use session-based token endpoint which handles auto-refresh server-side
244
244
  const response = await httpService.request({
245
245
  method: 'GET',
246
- url: `/api/session/token/${sessionId}`,
246
+ url: `/session/token/${sessionId}`,
247
247
  cache: false,
248
248
  retry: false,
249
249
  });
@@ -25,7 +25,9 @@ import { isNative, getPlatformOS } from './utils/platform';
25
25
  */
26
26
  const isNativeApp = isNative();
27
27
  /**
28
- * Token store for authentication (singleton)
28
+ * Token store for authentication (instance-based)
29
+ * Each HttpService gets its own TokenStore to prevent conflicts
30
+ * when multiple OxyServices instances coexist server-side.
29
31
  */
30
32
  class TokenStore {
31
33
  constructor() {
@@ -34,12 +36,6 @@ class TokenStore {
34
36
  this.csrfToken = null;
35
37
  this.csrfTokenFetchPromise = null;
36
38
  }
37
- static getInstance() {
38
- if (!TokenStore.instance) {
39
- TokenStore.instance = new TokenStore();
40
- }
41
- return TokenStore.instance;
42
- }
43
39
  setTokens(accessToken, refreshToken = '') {
44
40
  this.accessToken = accessToken;
45
41
  this.refreshToken = refreshToken;
@@ -83,6 +79,7 @@ class TokenStore {
83
79
  export class HttpService {
84
80
  constructor(config) {
85
81
  this.tokenRefreshPromise = null;
82
+ this.tokenRefreshCooldownUntil = 0;
86
83
  this._onTokenRefreshed = null;
87
84
  // Performance monitoring
88
85
  this.requestMetrics = {
@@ -95,7 +92,7 @@ export class HttpService {
95
92
  };
96
93
  this.config = config;
97
94
  this.baseURL = config.baseURL;
98
- this.tokenStore = TokenStore.getInstance();
95
+ this.tokenStore = new TokenStore();
99
96
  this.logger = new SimpleLogger(config.enableLogging || false, config.logLevel || 'error', 'HttpService');
100
97
  // Initialize performance infrastructure
101
98
  this.cache = new TTLCache(config.cacheTTL || 5 * 60 * 1000);
@@ -247,6 +244,20 @@ export class HttpService {
247
244
  // Refresh failed or no token — clear tokens
248
245
  this.tokenStore.clearTokens();
249
246
  }
247
+ // On 403 with CSRF error, clear cached token and retry once
248
+ if (response.status === 403 && !config._isCsrfRetry) {
249
+ try {
250
+ const clonedResponse = response.clone();
251
+ const errBody = await clonedResponse.json();
252
+ if (errBody?.code === 'CSRF_TOKEN_INVALID' || errBody?.code === 'CSRF_TOKEN_MISSING') {
253
+ this.tokenStore.clearCsrfToken();
254
+ return this.request({ ...config, _isCsrfRetry: true, retry: false });
255
+ }
256
+ }
257
+ catch {
258
+ // Failed to parse error body — not a CSRF error
259
+ }
260
+ }
250
261
  // Try to parse error response (handle empty/malformed JSON)
251
262
  let errorMessage = `HTTP ${response.status}: ${response.statusText}`;
252
263
  const contentType = response.headers.get('content-type');
@@ -403,52 +414,57 @@ export class HttpService {
403
414
  return existingPromise;
404
415
  }
405
416
  const fetchPromise = (async () => {
406
- try {
407
- if (isDev())
408
- console.log('[HttpService] Fetching CSRF token from:', `${this.baseURL}/api/csrf-token`);
409
- // Use AbortController for timeout (more compatible than AbortSignal.timeout)
410
- const controller = new AbortController();
411
- const timeoutId = setTimeout(() => controller.abort(), 5000);
412
- const response = await fetch(`${this.baseURL}/api/csrf-token`, {
413
- method: 'GET',
414
- headers: { 'Accept': 'application/json' },
415
- credentials: 'include', // Required to receive and send cookies
416
- signal: controller.signal,
417
- });
418
- clearTimeout(timeoutId);
419
- if (isDev())
420
- console.log('[HttpService] CSRF fetch response:', response.status, response.ok);
421
- if (response.ok) {
422
- const data = await response.json();
417
+ const maxAttempts = 2;
418
+ for (let attempt = 1; attempt <= maxAttempts; attempt++) {
419
+ try {
420
+ if (isDev())
421
+ console.log('[HttpService] Fetching CSRF token from:', `${this.baseURL}/csrf-token`, `(attempt ${attempt})`);
422
+ // Use AbortController for timeout (more compatible than AbortSignal.timeout)
423
+ const controller = new AbortController();
424
+ const timeoutId = setTimeout(() => controller.abort(), 5000);
425
+ const response = await fetch(`${this.baseURL}/csrf-token`, {
426
+ method: 'GET',
427
+ headers: { 'Accept': 'application/json' },
428
+ credentials: 'include', // Required to receive and send cookies
429
+ signal: controller.signal,
430
+ });
431
+ clearTimeout(timeoutId);
432
+ if (isDev())
433
+ console.log('[HttpService] CSRF fetch response:', response.status, response.ok);
434
+ if (response.ok) {
435
+ const data = await response.json();
436
+ if (isDev())
437
+ console.log('[HttpService] CSRF response data:', data);
438
+ const token = data.csrfToken || null;
439
+ this.tokenStore.setCsrfToken(token);
440
+ this.logger.debug('CSRF token fetched');
441
+ return token;
442
+ }
443
+ // Also check response header for CSRF token
444
+ const headerToken = response.headers.get('X-CSRF-Token');
445
+ if (headerToken) {
446
+ this.tokenStore.setCsrfToken(headerToken);
447
+ this.logger.debug('CSRF token from header');
448
+ return headerToken;
449
+ }
423
450
  if (isDev())
424
- console.log('[HttpService] CSRF response data:', data);
425
- const token = data.csrfToken || null;
426
- this.tokenStore.setCsrfToken(token);
427
- this.logger.debug('CSRF token fetched');
428
- return token;
451
+ console.log('[HttpService] CSRF fetch failed with status:', response.status);
452
+ this.logger.warn('Failed to fetch CSRF token:', response.status);
429
453
  }
430
- // Also check response header for CSRF token
431
- const headerToken = response.headers.get('X-CSRF-Token');
432
- if (headerToken) {
433
- this.tokenStore.setCsrfToken(headerToken);
434
- this.logger.debug('CSRF token from header');
435
- return headerToken;
454
+ catch (error) {
455
+ if (isDev())
456
+ console.log('[HttpService] CSRF fetch error:', error);
457
+ this.logger.warn('CSRF token fetch error:', error);
458
+ }
459
+ // Wait before retry (500ms)
460
+ if (attempt < maxAttempts) {
461
+ await new Promise(resolve => setTimeout(resolve, 500));
436
462
  }
437
- if (isDev())
438
- console.log('[HttpService] CSRF fetch failed with status:', response.status);
439
- this.logger.warn('Failed to fetch CSRF token:', response.status);
440
- return null;
441
- }
442
- catch (error) {
443
- if (isDev())
444
- console.log('[HttpService] CSRF fetch error:', error);
445
- this.logger.warn('CSRF token fetch error:', error);
446
- return null;
447
- }
448
- finally {
449
- this.tokenStore.setCsrfTokenFetchPromise(null);
450
463
  }
451
- })();
464
+ return null;
465
+ })().finally(() => {
466
+ this.tokenStore.setCsrfTokenFetchPromise(null);
467
+ });
452
468
  this.tokenStore.setCsrfTokenFetchPromise(fetchPromise);
453
469
  return fetchPromise;
454
470
  }
@@ -465,18 +481,25 @@ export class HttpService {
465
481
  const currentTime = Math.floor(Date.now() / 1000);
466
482
  // If token expires in less than 60 seconds, refresh it
467
483
  if (decoded.exp && decoded.exp - currentTime < 60 && decoded.sessionId) {
468
- // Deduplicate concurrent refresh attempts
469
- if (!this.tokenRefreshPromise) {
470
- this.tokenRefreshPromise = this._refreshTokenFromSession(decoded.sessionId);
484
+ // Skip if we recently failed a refresh (5s cooldown to prevent storms)
485
+ if (Date.now() < this.tokenRefreshCooldownUntil) {
486
+ return `Bearer ${accessToken}`;
471
487
  }
472
- try {
473
- const result = await this.tokenRefreshPromise;
474
- if (result)
488
+ // Deduplicate concurrent refresh attempts. The promise is shared
489
+ // across all concurrent callers and cleared only after it settles,
490
+ // so every awaiter receives the same result.
491
+ if (!this.tokenRefreshPromise) {
492
+ this.tokenRefreshPromise = this._refreshTokenFromSession(decoded.sessionId)
493
+ .then((result) => {
494
+ if (!result)
495
+ this.tokenRefreshCooldownUntil = Date.now() + 5000;
475
496
  return result;
497
+ })
498
+ .finally(() => { this.tokenRefreshPromise = null; });
476
499
  }
477
- finally {
478
- this.tokenRefreshPromise = null;
479
- }
500
+ const result = await this.tokenRefreshPromise;
501
+ if (result)
502
+ return result;
480
503
  }
481
504
  return `Bearer ${accessToken}`;
482
505
  }
@@ -487,7 +510,7 @@ export class HttpService {
487
510
  }
488
511
  async _refreshTokenFromSession(sessionId) {
489
512
  try {
490
- const refreshUrl = `${this.baseURL}/api/session/token/${sessionId}`;
513
+ const refreshUrl = `${this.baseURL}/session/token/${sessionId}`;
491
514
  const response = await fetch(refreshUrl, {
492
515
  method: 'GET',
493
516
  headers: { 'Accept': 'application/json' },
@@ -640,14 +663,9 @@ export class HttpService {
640
663
  getMetrics() {
641
664
  return { ...this.requestMetrics };
642
665
  }
643
- // Test-only utility
644
- static __resetTokensForTests() {
645
- try {
646
- TokenStore.getInstance().clearTokens();
647
- }
648
- catch (error) {
649
- // Silently fail in test cleanup - this is expected behavior
650
- // TokenStore might not be initialized in some test scenarios
651
- }
666
+ // Test-only utility — clears tokens on this instance
667
+ __resetTokensForTests() {
668
+ this.tokenStore.clearTokens();
669
+ this.tokenStore.clearCsrfToken();
652
670
  }
653
671
  }
@@ -23,9 +23,10 @@ export class OxyServicesBase {
23
23
  // Initialize unified HTTP service (handles auth, caching, deduplication, queuing, retry)
24
24
  this.httpService = new HttpService(config);
25
25
  }
26
- // Test-only utility to reset global tokens between jest tests
27
- static __resetTokensForTests() {
28
- HttpService.__resetTokensForTests();
26
+ // Test-only utility to reset tokens on this instance between jest tests
27
+ // Note: tokens are now per-instance, so create new instances in tests for isolation
28
+ __resetTokensForTests() {
29
+ this.httpService.__resetTokensForTests();
29
30
  }
30
31
  /**
31
32
  * Make a request with all performance optimizations
@@ -231,7 +232,7 @@ export class OxyServicesBase {
231
232
  return false;
232
233
  }
233
234
  try {
234
- const res = await this.makeRequest('GET', '/api/auth/validate', undefined, {
235
+ const res = await this.makeRequest('GET', '/auth/validate', undefined, {
235
236
  cache: false,
236
237
  retry: false,
237
238
  });
@@ -5,7 +5,7 @@
5
5
  * Private keys are stored securely using expo-secure-store and never leave the device.
6
6
  */
7
7
  import { ec as EC } from 'elliptic';
8
- import { isWeb, isIOS, isAndroid } from '../utils/platform';
8
+ import { isWeb, isIOS, isAndroid, isReactNative, isNodeJS } from '../utils/platform';
9
9
  import { logger } from '../utils/loggerUtils';
10
10
  import { isDev } from '../shared/utils/debugUtils';
11
11
  // Lazy imports for React Native specific modules
@@ -56,18 +56,6 @@ async function initSecureStore() {
56
56
  }
57
57
  return SecureStore;
58
58
  }
59
- /**
60
- * Check if we're in a React Native environment
61
- */
62
- function isReactNative() {
63
- return typeof navigator !== 'undefined' && navigator.product === 'ReactNative';
64
- }
65
- /**
66
- * Check if we're in a Node.js environment
67
- */
68
- function isNodeJS() {
69
- return typeof process !== 'undefined' && process.versions != null && process.versions.node != null;
70
- }
71
59
  /**
72
60
  * Check if we're on web platform
73
61
  * Identity storage is only available on native platforms (iOS/Android)
@@ -6,21 +6,10 @@
6
6
  */
7
7
  import { ec as EC } from 'elliptic';
8
8
  import { KeyManager } from './keyManager';
9
+ import { isReactNative, isNodeJS } from '../utils/platform';
9
10
  // Lazy import for expo-crypto
10
11
  let ExpoCrypto = null;
11
12
  const ec = new EC('secp256k1');
12
- /**
13
- * Check if we're in a React Native environment
14
- */
15
- function isReactNative() {
16
- return typeof navigator !== 'undefined' && navigator.product === 'ReactNative';
17
- }
18
- /**
19
- * Check if we're in a Node.js environment
20
- */
21
- function isNodeJS() {
22
- return typeof process !== 'undefined' && process.versions != null && process.versions.node != null;
23
- }
24
13
  /**
25
14
  * Initialize expo-crypto module
26
15
  */
@@ -44,9 +33,7 @@ async function sha256(message) {
44
33
  // In Node.js, use Node's crypto module
45
34
  if (isNodeJS()) {
46
35
  try {
47
- // eslint-disable-next-line @typescript-eslint/no-implied-eval
48
- const getCrypto = new Function('return require("crypto")');
49
- const nodeCrypto = getCrypto();
36
+ const nodeCrypto = await import('crypto');
50
37
  return nodeCrypto.createHash('sha256').update(message).digest('hex');
51
38
  }
52
39
  catch {
@@ -11,7 +11,7 @@ export function OxyServicesAnalyticsMixin(Base) {
11
11
  */
12
12
  async trackEvent(eventName, properties) {
13
13
  try {
14
- await this.makeRequest('POST', '/api/analytics/events', {
14
+ await this.makeRequest('POST', '/analytics/events', {
15
15
  event: eventName,
16
16
  properties
17
17
  }, { cache: false, retry: false }); // Don't retry analytics events
@@ -33,7 +33,7 @@ export function OxyServicesAnalyticsMixin(Base) {
33
33
  params.startDate = startDate;
34
34
  if (endDate)
35
35
  params.endDate = endDate;
36
- return await this.makeRequest('GET', '/api/analytics', params, {
36
+ return await this.makeRequest('GET', '/analytics', params, {
37
37
  cache: true,
38
38
  cacheTTL: CACHE_TIMES.LONG,
39
39
  });