@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
@@ -10,13 +10,13 @@
10
10
  import type { OxyServices } from './OxyServices';
11
11
  import type { HttpService } from './HttpService';
12
12
  import type { SessionLoginResponse, MinimalUserData } from './models/session';
13
+ import { retryAsync } from './utils/asyncUtils';
13
14
 
14
15
  /**
15
- * OxyServices with optional FedCM methods (provided by FedCM mixin).
16
+ * OxyServices already declares revokeFedCMCredential via mixin type augmentation.
17
+ * This alias is kept for readability in the signOut method.
16
18
  */
17
- interface OxyServicesWithFedCM extends OxyServices {
18
- revokeFedCMCredential?: () => Promise<void>;
19
- }
19
+ type OxyServicesWithFedCM = OxyServices;
20
20
 
21
21
  /**
22
22
  * Storage adapter interface for platform-agnostic storage.
@@ -142,6 +142,7 @@ export class AuthManager {
142
142
  private listeners: Set<AuthStateChangeCallback> = new Set();
143
143
  private currentUser: MinimalUserData | null = null;
144
144
  private refreshTimer: ReturnType<typeof setTimeout> | null = null;
145
+ private refreshPromise: Promise<boolean> | null = null;
145
146
  private config: Required<AuthManagerConfig>;
146
147
 
147
148
  constructor(oxyServices: OxyServices, config: AuthManagerConfig = {}) {
@@ -261,28 +262,53 @@ export class AuthManager {
261
262
  }
262
263
 
263
264
  /**
264
- * Refresh the access token.
265
+ * Refresh the access token. Deduplicates concurrent calls so only one
266
+ * refresh request is in-flight at a time.
265
267
  */
266
268
  async refreshToken(): Promise<boolean> {
269
+ // If a refresh is already in-flight, return the same promise
270
+ if (this.refreshPromise) {
271
+ return this.refreshPromise;
272
+ }
273
+
274
+ this.refreshPromise = this._doRefreshToken();
275
+ try {
276
+ return await this.refreshPromise;
277
+ } finally {
278
+ this.refreshPromise = null;
279
+ }
280
+ }
281
+
282
+ private async _doRefreshToken(): Promise<boolean> {
267
283
  const refreshToken = await this.storage.getItem(STORAGE_KEYS.REFRESH_TOKEN);
268
284
  if (!refreshToken) {
269
285
  return false;
270
286
  }
271
287
 
272
288
  try {
273
- // Cast httpService to proper type (needed due to mixin composition)
274
- const httpService = this.oxyServices.httpService as HttpService;
275
- const response = await httpService.request<SessionLoginResponse>({
276
- method: 'POST',
277
- url: '/api/auth/refresh',
278
- data: { refreshToken },
279
- cache: false,
280
- });
281
-
282
- await this.handleAuthSuccess(response, 'credentials');
289
+ await retryAsync(
290
+ async () => {
291
+ const httpService = this.oxyServices.httpService as HttpService;
292
+ const response = await httpService.request<SessionLoginResponse>({
293
+ method: 'POST',
294
+ url: '/api/auth/refresh',
295
+ data: { refreshToken },
296
+ cache: false,
297
+ });
298
+ await this.handleAuthSuccess(response, 'credentials');
299
+ },
300
+ 2, // 2 retries = 3 total attempts
301
+ 1000, // 1s base delay with exponential backoff + jitter
302
+ (error: any) => {
303
+ // Don't retry on 4xx client errors (invalid/revoked token)
304
+ const status = error?.status ?? error?.response?.status;
305
+ if (status && status >= 400 && status < 500) return false;
306
+ return true;
307
+ }
308
+ );
283
309
  return true;
284
310
  } catch {
285
- // Refresh failed, clear session and update state
311
+ // All retry attempts exhausted, clear session
286
312
  await this.clearSession();
287
313
  this.currentUser = null;
288
314
  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
  *
@@ -287,7 +287,7 @@ export class CrossDomainAuth {
287
287
  *
288
288
  * @example
289
289
  * ```typescript
290
- * import { createCrossDomainAuth } from '@oxyhq/services';
290
+ * import { createCrossDomainAuth } from '@oxyhq/core';
291
291
  *
292
292
  * const oxyServices = new OxyServices({ baseURL: 'https://api.oxy.so' });
293
293
  * const auth = createCrossDomainAuth(oxyServices);
@@ -17,6 +17,7 @@ import { TTLCache, registerCacheForCleanup } from './utils/cache';
17
17
  import { RequestDeduplicator, RequestQueue, SimpleLogger } from './utils/requestUtils';
18
18
  import { retryAsync } from './utils/asyncUtils';
19
19
  import { handleHttpError } from './utils/errorUtils';
20
+ import { isDev } from './shared/utils/debugUtils';
20
21
  import { jwtDecode } from 'jwt-decode';
21
22
  import { isNative, getPlatformOS } from './utils/platform';
22
23
  import type { OxyConfig } from './models/interfaces';
@@ -130,6 +131,7 @@ export class HttpService {
130
131
  private requestQueue: RequestQueue;
131
132
  private logger: SimpleLogger;
132
133
  private config: OxyConfig;
134
+ private tokenRefreshPromise: Promise<string | null> | null = null;
133
135
 
134
136
  // Performance monitoring
135
137
  private requestMetrics = {
@@ -279,7 +281,7 @@ export class HttpService {
279
281
  }
280
282
 
281
283
  // Debug logging for CSRF issues
282
- if (isStateChangingMethod && __DEV__) {
284
+ if (isStateChangingMethod && isDev()) {
283
285
  console.log('[HttpService] CSRF Debug:', {
284
286
  url,
285
287
  method,
@@ -484,20 +486,20 @@ export class HttpService {
484
486
  // Return cached token if available
485
487
  const cachedToken = this.tokenStore.getCsrfToken();
486
488
  if (cachedToken) {
487
- if (__DEV__) console.log('[HttpService] Using cached CSRF token');
489
+ if (isDev()) console.log('[HttpService] Using cached CSRF token');
488
490
  return cachedToken;
489
491
  }
490
492
 
491
493
  // Deduplicate concurrent CSRF token fetches
492
494
  const existingPromise = this.tokenStore.getCsrfTokenFetchPromise();
493
495
  if (existingPromise) {
494
- if (__DEV__) console.log('[HttpService] Waiting for existing CSRF fetch');
496
+ if (isDev()) console.log('[HttpService] Waiting for existing CSRF fetch');
495
497
  return existingPromise;
496
498
  }
497
499
 
498
500
  const fetchPromise = (async () => {
499
501
  try {
500
- if (__DEV__) console.log('[HttpService] Fetching CSRF token from:', `${this.baseURL}/api/csrf-token`);
502
+ if (isDev()) console.log('[HttpService] Fetching CSRF token from:', `${this.baseURL}/api/csrf-token`);
501
503
 
502
504
  // Use AbortController for timeout (more compatible than AbortSignal.timeout)
503
505
  const controller = new AbortController();
@@ -512,11 +514,11 @@ export class HttpService {
512
514
 
513
515
  clearTimeout(timeoutId);
514
516
 
515
- if (__DEV__) console.log('[HttpService] CSRF fetch response:', response.status, response.ok);
517
+ if (isDev()) console.log('[HttpService] CSRF fetch response:', response.status, response.ok);
516
518
 
517
519
  if (response.ok) {
518
520
  const data = await response.json() as { csrfToken?: string };
519
- if (__DEV__) console.log('[HttpService] CSRF response data:', data);
521
+ if (isDev()) console.log('[HttpService] CSRF response data:', data);
520
522
  const token = data.csrfToken || null;
521
523
  this.tokenStore.setCsrfToken(token);
522
524
  this.logger.debug('CSRF token fetched');
@@ -531,11 +533,11 @@ export class HttpService {
531
533
  return headerToken;
532
534
  }
533
535
 
534
- if (__DEV__) console.log('[HttpService] CSRF fetch failed with status:', response.status);
536
+ if (isDev()) console.log('[HttpService] CSRF fetch failed with status:', response.status);
535
537
  this.logger.warn('Failed to fetch CSRF token:', response.status);
536
538
  return null;
537
539
  } catch (error) {
538
- if (__DEV__) console.log('[HttpService] CSRF fetch error:', error);
540
+ if (isDev()) console.log('[HttpService] CSRF fetch error:', error);
539
541
  this.logger.warn('CSRF token fetch error:', error);
540
542
  return null;
541
543
  } finally {
@@ -562,25 +564,15 @@ export class HttpService {
562
564
 
563
565
  // If token expires in less than 60 seconds, refresh it
564
566
  if (decoded.exp && decoded.exp - currentTime < 60 && decoded.sessionId) {
567
+ // Deduplicate concurrent refresh attempts
568
+ if (!this.tokenRefreshPromise) {
569
+ this.tokenRefreshPromise = this._refreshTokenFromSession(decoded.sessionId);
570
+ }
565
571
  try {
566
- const refreshUrl = `${this.baseURL}/api/session/token/${decoded.sessionId}`;
567
-
568
- // Use AbortSignal.timeout for consistent timeout handling
569
- const response = await fetch(refreshUrl, {
570
- method: 'GET',
571
- headers: { 'Accept': 'application/json' },
572
- signal: AbortSignal.timeout(5000),
573
- credentials: 'include', // Include cookies for cross-origin requests
574
- });
575
-
576
- if (response.ok) {
577
- const { accessToken: newToken } = await response.json();
578
- this.tokenStore.setTokens(newToken);
579
- this.logger.debug('Token refreshed');
580
- return `Bearer ${newToken}`;
581
- }
582
- } catch (refreshError) {
583
- this.logger.warn('Token refresh failed, using current token');
572
+ const result = await this.tokenRefreshPromise;
573
+ if (result) return result;
574
+ } finally {
575
+ this.tokenRefreshPromise = null;
584
576
  }
585
577
  }
586
578
 
@@ -591,6 +583,28 @@ export class HttpService {
591
583
  }
592
584
  }
593
585
 
586
+ private async _refreshTokenFromSession(sessionId: string): Promise<string | null> {
587
+ try {
588
+ const refreshUrl = `${this.baseURL}/api/session/token/${sessionId}`;
589
+ const response = await fetch(refreshUrl, {
590
+ method: 'GET',
591
+ headers: { 'Accept': 'application/json' },
592
+ signal: AbortSignal.timeout(5000),
593
+ credentials: 'include',
594
+ });
595
+
596
+ if (response.ok) {
597
+ const { accessToken: newToken } = await response.json();
598
+ this.tokenStore.setTokens(newToken);
599
+ this.logger.debug('Token refreshed');
600
+ return `Bearer ${newToken}`;
601
+ }
602
+ } catch (refreshError) {
603
+ this.logger.warn('Token refresh failed, using current token');
604
+ }
605
+ return null;
606
+ }
607
+
594
608
  /**
595
609
  * Unwrap standardized API response format
596
610
  */
@@ -131,21 +131,38 @@ export class OxyServicesBase {
131
131
  */
132
132
  public clearTokens(): void {
133
133
  this.httpService.clearTokens();
134
+ this._cachedUserId = undefined;
135
+ this._cachedAccessToken = null;
134
136
  }
135
137
 
138
+ /** @internal */ _cachedUserId: string | null | undefined = undefined;
139
+ /** @internal */ _cachedAccessToken: string | null = null;
140
+
136
141
  /**
137
- * Get the current user ID from the access token
142
+ * Get the current user ID from the access token.
143
+ * Caches the decoded value and invalidates when the token changes.
138
144
  */
139
145
  public getCurrentUserId(): string | null {
140
146
  const accessToken = this.httpService.getAccessToken();
147
+
148
+ // Return cached value if token hasn't changed
149
+ if (accessToken === this._cachedAccessToken && this._cachedUserId !== undefined) {
150
+ return this._cachedUserId;
151
+ }
152
+
153
+ this._cachedAccessToken = accessToken;
154
+
141
155
  if (!accessToken) {
156
+ this._cachedUserId = null;
142
157
  return null;
143
158
  }
144
-
159
+
145
160
  try {
146
161
  const decoded = jwtDecode<JwtPayload>(accessToken);
147
- return decoded.userId || decoded.id || null;
148
- } catch (error) {
162
+ this._cachedUserId = decoded.userId || decoded.id || null;
163
+ return this._cachedUserId;
164
+ } catch {
165
+ this._cachedUserId = null;
149
166
  return null;
150
167
  }
151
168
  }
@@ -58,6 +58,10 @@
58
58
  */
59
59
  import { OxyServicesBase, type OxyConfig } from './OxyServices.base';
60
60
  import { OxyAuthenticationError, OxyAuthenticationTimeoutError } from './OxyServices.errors';
61
+ import type { SessionLoginResponse } from './models/session';
62
+ import type { FedCMAuthOptions, FedCMConfig } from './mixins/OxyServices.fedcm';
63
+ import type { PopupAuthOptions } from './mixins/OxyServices.popup';
64
+ import type { RedirectAuthOptions } from './mixins/OxyServices.redirect';
61
65
 
62
66
  // Import mixin composition helper
63
67
  import { composeOxyServices } from './mixins';
@@ -106,8 +110,25 @@ export class OxyServices extends (OxyServicesComposed as any) {
106
110
  }
107
111
 
108
112
  // Type augmentation to expose mixin methods to TypeScript
109
- // This allows proper type checking while avoiding complex mixin type inference
110
- export interface OxyServices extends InstanceType<ReturnType<typeof composeOxyServices>> {}
113
+ // This allows proper type checking while avoiding complex mixin type inference.
114
+ // Explicit declarations are added for cross-domain auth methods that downstream
115
+ // packages (auth-sdk, services) need without casting to `any`.
116
+ export interface OxyServices extends InstanceType<ReturnType<typeof composeOxyServices>> {
117
+ // FedCM authentication
118
+ isFedCMSupported(): boolean;
119
+ signInWithFedCM(options?: FedCMAuthOptions): Promise<SessionLoginResponse>;
120
+ silentSignInWithFedCM(): Promise<SessionLoginResponse | null>;
121
+ revokeFedCMCredential(): Promise<void>;
122
+ getFedCMConfig(): FedCMConfig;
123
+
124
+ // Popup authentication
125
+ signInWithPopup(options?: PopupAuthOptions): Promise<SessionLoginResponse>;
126
+ signUpWithPopup(options?: PopupAuthOptions): Promise<SessionLoginResponse>;
127
+
128
+ // Redirect authentication
129
+ signInWithRedirect(options?: RedirectAuthOptions): void;
130
+ signUpWithRedirect(options?: RedirectAuthOptions): void;
131
+ }
111
132
 
112
133
  // Re-export error classes for convenience
113
134
  export { OxyAuthenticationError, OxyAuthenticationTimeoutError };
@@ -9,6 +9,7 @@ import { ec as EC } from 'elliptic';
9
9
  import type { ECKeyPair } from 'elliptic';
10
10
  import { isWeb, isIOS, isAndroid } from '../utils/platform';
11
11
  import { logger } from '../utils/loggerUtils';
12
+ import { isDev } from '../shared/utils/debugUtils';
12
13
 
13
14
  // Lazy imports for React Native specific modules
14
15
  let SecureStore: typeof import('expo-secure-store') | null = null;
@@ -49,7 +50,9 @@ const ANDROID_ACCOUNT_TYPE = 'com.oxy.account';
49
50
  async function initSecureStore(): Promise<typeof import('expo-secure-store')> {
50
51
  if (!SecureStore) {
51
52
  try {
52
- SecureStore = await import('expo-secure-store');
53
+ // Variable indirection prevents bundlers (Vite, webpack) from statically resolving this
54
+ const moduleName = 'expo-secure-store';
55
+ SecureStore = await import(moduleName);
53
56
  } catch (error) {
54
57
  const errorMessage = error instanceof Error ? error.message : String(error);
55
58
  throw new Error(`Failed to load expo-secure-store: ${errorMessage}. Make sure expo-secure-store is installed and properly configured.`);
@@ -85,9 +88,11 @@ function isWebPlatform(): boolean {
85
88
 
86
89
  async function initExpoCrypto(): Promise<typeof import('expo-crypto')> {
87
90
  if (!ExpoCrypto) {
88
- ExpoCrypto = await import('expo-crypto');
91
+ // Variable indirection prevents bundlers (Vite, webpack) from statically resolving this
92
+ const moduleName = 'expo-crypto';
93
+ ExpoCrypto = await import(moduleName);
89
94
  }
90
- return ExpoCrypto;
95
+ return ExpoCrypto!;
91
96
  }
92
97
 
93
98
  /**
@@ -238,7 +243,7 @@ export class KeyManager {
238
243
  KeyManager.cachedSharedPublicKey = publicKey;
239
244
  KeyManager.cachedHasSharedIdentity = true;
240
245
 
241
- if (__DEV__) {
246
+ if (isDev()) {
242
247
  logger.debug('Shared identity created successfully', { component: 'KeyManager' });
243
248
  }
244
249
 
@@ -277,7 +282,7 @@ export class KeyManager {
277
282
 
278
283
  return publicKey;
279
284
  } catch (error) {
280
- if (__DEV__) {
285
+ if (isDev()) {
281
286
  logger.warn('Failed to get shared public key', { component: 'KeyManager' }, error);
282
287
  }
283
288
  KeyManager.cachedSharedPublicKey = null;
@@ -312,7 +317,7 @@ export class KeyManager {
312
317
 
313
318
  return privateKey;
314
319
  } catch (error) {
315
- if (__DEV__) {
320
+ if (isDev()) {
316
321
  logger.warn('Failed to get shared private key', { component: 'KeyManager' }, error);
317
322
  }
318
323
  return null;
@@ -343,7 +348,7 @@ export class KeyManager {
343
348
 
344
349
  return hasShared;
345
350
  } catch (error) {
346
- if (__DEV__) {
351
+ if (isDev()) {
347
352
  logger.warn('Failed to check shared identity', { component: 'KeyManager' }, error);
348
353
  }
349
354
  KeyManager.cachedHasSharedIdentity = false;
@@ -392,7 +397,7 @@ export class KeyManager {
392
397
  KeyManager.cachedSharedPublicKey = publicKey;
393
398
  KeyManager.cachedHasSharedIdentity = true;
394
399
 
395
- if (__DEV__) {
400
+ if (isDev()) {
396
401
  logger.debug('Shared identity imported successfully', { component: 'KeyManager' });
397
402
  }
398
403
 
@@ -431,11 +436,11 @@ export class KeyManager {
431
436
  await store.setItemAsync(STORAGE_KEYS.SHARED_SESSION_TOKEN, accessToken);
432
437
  }
433
438
 
434
- if (__DEV__) {
439
+ if (isDev()) {
435
440
  logger.debug('Shared session stored successfully', { component: 'KeyManager' });
436
441
  }
437
442
  } catch (error) {
438
- if (__DEV__) {
443
+ if (isDev()) {
439
444
  logger.error('Failed to store shared session', error, { component: 'KeyManager' });
440
445
  }
441
446
  throw error;
@@ -479,7 +484,7 @@ export class KeyManager {
479
484
 
480
485
  return { sessionId, accessToken };
481
486
  } catch (error) {
482
- if (__DEV__) {
487
+ if (isDev()) {
483
488
  logger.warn('Failed to get shared session', { component: 'KeyManager' }, error);
484
489
  }
485
490
  return null;
@@ -512,11 +517,11 @@ export class KeyManager {
512
517
  await store.deleteItemAsync(STORAGE_KEYS.SHARED_SESSION_TOKEN);
513
518
  }
514
519
 
515
- if (__DEV__) {
520
+ if (isDev()) {
516
521
  logger.debug('Shared session cleared successfully', { component: 'KeyManager' });
517
522
  }
518
523
  } catch (error) {
519
- if (__DEV__) {
524
+ if (isDev()) {
520
525
  logger.error('Failed to clear shared session', error, { component: 'KeyManager' });
521
526
  }
522
527
  }
@@ -540,7 +545,7 @@ export class KeyManager {
540
545
  // Check if we already have a shared identity
541
546
  const hasShared = await KeyManager.hasSharedIdentity();
542
547
  if (hasShared) {
543
- if (__DEV__) {
548
+ if (isDev()) {
544
549
  logger.debug('Shared identity already exists, skipping migration', { component: 'KeyManager' });
545
550
  }
546
551
  return true;
@@ -549,7 +554,7 @@ export class KeyManager {
549
554
  // Get local identity
550
555
  const privateKey = await KeyManager.getPrivateKey();
551
556
  if (!privateKey) {
552
- if (__DEV__) {
557
+ if (isDev()) {
553
558
  logger.debug('No local identity to migrate', { component: 'KeyManager' });
554
559
  }
555
560
  return false;
@@ -558,13 +563,13 @@ export class KeyManager {
558
563
  // Import to shared storage
559
564
  await KeyManager.importSharedIdentity(privateKey);
560
565
 
561
- if (__DEV__) {
566
+ if (isDev()) {
562
567
  logger.debug('Successfully migrated local identity to shared identity', { component: 'KeyManager' });
563
568
  }
564
569
 
565
570
  return true;
566
571
  } catch (error) {
567
- if (__DEV__) {
572
+ if (isDev()) {
568
573
  logger.error('Failed to migrate to shared identity', error, { component: 'KeyManager' });
569
574
  }
570
575
  return false;
@@ -635,7 +640,7 @@ export class KeyManager {
635
640
  } catch (error) {
636
641
  // If secure store is not available, return null (no identity)
637
642
  // This allows the app to continue functioning even if secure store fails to load
638
- if (__DEV__) {
643
+ if (isDev()) {
639
644
  logger.warn('Failed to access secure store', { component: 'KeyManager' }, error);
640
645
  }
641
646
  return null;
@@ -665,7 +670,7 @@ export class KeyManager {
665
670
  // If secure store is not available, return null (no identity)
666
671
  // Cache null to avoid repeated failed attempts
667
672
  KeyManager.cachedPublicKey = null;
668
- if (__DEV__) {
673
+ if (isDev()) {
669
674
  logger.warn('Failed to access secure store', { component: 'KeyManager' }, error);
670
675
  }
671
676
  return null;
@@ -695,7 +700,7 @@ export class KeyManager {
695
700
  // If we can't check, assume no identity (safer default)
696
701
  // Cache false to avoid repeated failed attempts
697
702
  KeyManager.cachedHasIdentity = false;
698
- if (__DEV__) {
703
+ if (isDev()) {
699
704
  logger.warn('Failed to check identity', { component: 'KeyManager' }, error);
700
705
  }
701
706
  return false;
@@ -736,11 +741,11 @@ export class KeyManager {
736
741
  if (!skipBackup) {
737
742
  try {
738
743
  const backupSuccess = await KeyManager.backupIdentity();
739
- if (!backupSuccess && typeof __DEV__ !== 'undefined' && __DEV__) {
744
+ if (!backupSuccess && isDev()) {
740
745
  logger.warn('Failed to backup identity before deletion - proceeding anyway', { component: 'KeyManager' });
741
746
  }
742
747
  } catch (backupError) {
743
- if (typeof __DEV__ !== 'undefined' && __DEV__) {
748
+ if (isDev()) {
744
749
  logger.warn('Failed to backup identity before deletion', { component: 'KeyManager' }, backupError);
745
750
  }
746
751
  }
@@ -790,7 +795,7 @@ export class KeyManager {
790
795
 
791
796
  return true;
792
797
  } catch (error) {
793
- if (typeof __DEV__ !== 'undefined' && __DEV__) {
798
+ if (isDev()) {
794
799
  logger.error('Failed to backup identity', error, { component: 'KeyManager' });
795
800
  }
796
801
  return false;
@@ -836,7 +841,7 @@ export class KeyManager {
836
841
 
837
842
  return true;
838
843
  } catch (error) {
839
- if (typeof __DEV__ !== 'undefined' && __DEV__) {
844
+ if (isDev()) {
840
845
  logger.error('Identity integrity check failed', error, { component: 'KeyManager' });
841
846
  }
842
847
  return false;
@@ -893,7 +898,7 @@ export class KeyManager {
893
898
 
894
899
  return false;
895
900
  } catch (error) {
896
- if (typeof __DEV__ !== 'undefined' && __DEV__) {
901
+ if (isDev()) {
897
902
  logger.error('Failed to restore identity from backup', error, { component: 'KeyManager' });
898
903
  }
899
904
  return false;
@@ -37,7 +37,12 @@ function getRandomBytesSync(byteCount: number): Uint8Array {
37
37
  if (!expoCryptoLoadAttempted) {
38
38
  expoCryptoLoadAttempted = true;
39
39
  try {
40
- expoCryptoModule = require('expo-crypto');
40
+ // Only use require() in CJS environments (Metro/Node). In ESM (Vite/browser),
41
+ // crypto.getRandomValues exists natively so this code path is never reached.
42
+ if (typeof require !== 'undefined') {
43
+ const moduleName = 'expo-crypto';
44
+ expoCryptoModule = require(moduleName);
45
+ }
41
46
  } catch {
42
47
  // expo-crypto not available — expected in non-RN environments
43
48
  }