@oxyhq/core 1.0.1 → 1.1.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 (92) hide show
  1. package/dist/cjs/AuthManager.js +19 -9
  2. package/dist/cjs/CrossDomainAuth.js +2 -2
  3. package/dist/cjs/HttpService.js +9 -8
  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 +2 -2
  17. package/dist/cjs/i18n/locales/ko-KR.json +114 -115
  18. package/dist/cjs/i18n/locales/pt-PT.json +114 -115
  19. package/dist/cjs/i18n/locales/zh-CN.json +114 -115
  20. package/dist/cjs/mixins/OxyServices.fedcm.js +13 -41
  21. package/dist/cjs/mixins/OxyServices.language.js +5 -2
  22. package/dist/cjs/mixins/OxyServices.privacy.js +2 -1
  23. package/dist/cjs/mixins/OxyServices.security.js +3 -2
  24. package/dist/cjs/shared/utils/debugUtils.js +8 -1
  25. package/dist/cjs/utils/deviceManager.js +3 -1
  26. package/dist/cjs/utils/platform.js +3 -2
  27. package/dist/esm/AuthManager.js +19 -9
  28. package/dist/esm/CrossDomainAuth.js +2 -2
  29. package/dist/esm/HttpService.js +9 -8
  30. package/dist/esm/OxyServices.base.js +16 -3
  31. package/dist/esm/crypto/keyManager.js +29 -24
  32. package/dist/esm/crypto/polyfill.js +6 -1
  33. package/dist/esm/crypto/signatureService.js +40 -31
  34. package/dist/esm/i18n/index.js +11 -23
  35. package/dist/esm/i18n/locales/ar-SA.json +114 -115
  36. package/dist/esm/i18n/locales/ca-ES.json +114 -115
  37. package/dist/esm/i18n/locales/de-DE.json +114 -115
  38. package/dist/esm/i18n/locales/en-US.json +936 -936
  39. package/dist/esm/i18n/locales/es-ES.json +924 -924
  40. package/dist/esm/i18n/locales/fr-FR.json +114 -115
  41. package/dist/esm/i18n/locales/it-IT.json +114 -115
  42. package/dist/esm/i18n/locales/ja-JP.json +2 -2
  43. package/dist/esm/i18n/locales/ko-KR.json +114 -115
  44. package/dist/esm/i18n/locales/pt-PT.json +114 -115
  45. package/dist/esm/i18n/locales/zh-CN.json +114 -115
  46. package/dist/esm/mixins/OxyServices.fedcm.js +13 -41
  47. package/dist/esm/mixins/OxyServices.language.js +5 -2
  48. package/dist/esm/mixins/OxyServices.privacy.js +2 -1
  49. package/dist/esm/mixins/OxyServices.security.js +3 -2
  50. package/dist/esm/shared/utils/debugUtils.js +8 -1
  51. package/dist/esm/utils/deviceManager.js +3 -1
  52. package/dist/esm/utils/platform.js +3 -2
  53. package/dist/types/CrossDomainAuth.d.ts +2 -2
  54. package/dist/types/OxyServices.base.d.ts +4 -1
  55. package/dist/types/OxyServices.d.ts +13 -0
  56. package/dist/types/index.d.ts +3 -0
  57. package/dist/types/mixins/OxyServices.analytics.d.ts +2 -0
  58. package/dist/types/mixins/OxyServices.assets.d.ts +2 -0
  59. package/dist/types/mixins/OxyServices.auth.d.ts +2 -0
  60. package/dist/types/mixins/OxyServices.developer.d.ts +2 -0
  61. package/dist/types/mixins/OxyServices.devices.d.ts +2 -0
  62. package/dist/types/mixins/OxyServices.features.d.ts +2 -0
  63. package/dist/types/mixins/OxyServices.fedcm.d.ts +4 -2
  64. package/dist/types/mixins/OxyServices.karma.d.ts +2 -0
  65. package/dist/types/mixins/OxyServices.language.d.ts +2 -0
  66. package/dist/types/mixins/OxyServices.location.d.ts +2 -0
  67. package/dist/types/mixins/OxyServices.payment.d.ts +2 -0
  68. package/dist/types/mixins/OxyServices.popup.d.ts +2 -0
  69. package/dist/types/mixins/OxyServices.privacy.d.ts +2 -0
  70. package/dist/types/mixins/OxyServices.redirect.d.ts +2 -0
  71. package/dist/types/mixins/OxyServices.security.d.ts +2 -0
  72. package/dist/types/mixins/OxyServices.user.d.ts +2 -0
  73. package/dist/types/mixins/OxyServices.utility.d.ts +2 -0
  74. package/package.json +2 -3
  75. package/src/AuthManager.ts +25 -15
  76. package/src/CrossDomainAuth.ts +2 -2
  77. package/src/HttpService.ts +9 -8
  78. package/src/OxyServices.base.ts +21 -4
  79. package/src/OxyServices.ts +23 -2
  80. package/src/crypto/keyManager.ts +30 -25
  81. package/src/crypto/polyfill.ts +6 -1
  82. package/src/crypto/signatureService.ts +43 -37
  83. package/src/i18n/index.ts +33 -45
  84. package/src/index.ts +3 -0
  85. package/src/mixins/OxyServices.fedcm.ts +14 -44
  86. package/src/mixins/OxyServices.language.ts +6 -3
  87. package/src/mixins/OxyServices.privacy.ts +2 -1
  88. package/src/mixins/OxyServices.security.ts +3 -2
  89. package/src/shared/utils/__tests__/debugUtils.test.ts +55 -0
  90. package/src/shared/utils/debugUtils.ts +6 -1
  91. package/src/utils/deviceManager.ts +4 -2
  92. package/src/utils/platform.ts +3 -2
@@ -100,6 +100,8 @@ export declare function OxyServicesPrivacyMixin<T extends typeof OxyServicesBase
100
100
  getCloudURL(): string;
101
101
  setTokens(accessToken: string, refreshToken?: string): void;
102
102
  clearTokens(): void;
103
+ _cachedUserId: string | null | undefined;
104
+ _cachedAccessToken: string | null;
103
105
  getCurrentUserId(): string | null;
104
106
  hasValidToken(): boolean;
105
107
  getAccessToken(): string | null;
@@ -216,6 +216,8 @@ export declare function OxyServicesRedirectAuthMixin<T extends typeof OxyService
216
216
  getCloudURL(): string;
217
217
  setTokens(accessToken: string, refreshToken?: string): void;
218
218
  clearTokens(): void;
219
+ _cachedUserId: string | null | undefined;
220
+ _cachedAccessToken: string | null;
219
221
  getCurrentUserId(): string | null;
220
222
  hasValidToken(): boolean;
221
223
  getAccessToken(): string | null;
@@ -56,6 +56,8 @@ export declare function OxyServicesSecurityMixin<T extends typeof OxyServicesBas
56
56
  getCloudURL(): string;
57
57
  setTokens(accessToken: string, refreshToken?: string): void;
58
58
  clearTokens(): void;
59
+ _cachedUserId: string | null | undefined;
60
+ _cachedAccessToken: string | null;
59
61
  getCurrentUserId(): string | null;
60
62
  hasValidToken(): boolean;
61
63
  getAccessToken(): string | null;
@@ -160,6 +160,8 @@ export declare function OxyServicesUserMixin<T extends typeof OxyServicesBase>(B
160
160
  getCloudURL(): string;
161
161
  setTokens(accessToken: string, refreshToken?: string): void;
162
162
  clearTokens(): void;
163
+ _cachedUserId: string | null | undefined;
164
+ _cachedAccessToken: string | null;
163
165
  getCurrentUserId(): string | null;
164
166
  hasValidToken(): boolean;
165
167
  getAccessToken(): string | null;
@@ -71,6 +71,8 @@ export declare function OxyServicesUtilityMixin<T extends typeof OxyServicesBase
71
71
  getCloudURL(): string;
72
72
  setTokens(accessToken: string, refreshToken?: string): void;
73
73
  clearTokens(): void;
74
+ _cachedUserId: string | null | undefined;
75
+ _cachedAccessToken: string | null;
74
76
  getCurrentUserId(): string | null;
75
77
  hasValidToken(): boolean;
76
78
  getAccessToken(): string | null;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oxyhq/core",
3
- "version": "1.0.1",
3
+ "version": "1.1.0",
4
4
  "description": "OxyHQ SDK Foundation — API client, authentication, cryptographic identity, and shared utilities",
5
5
  "main": "dist/cjs/index.js",
6
6
  "module": "dist/esm/index.js",
@@ -65,7 +65,6 @@
65
65
  "lint": "biome lint --error-on-warnings ./src"
66
66
  },
67
67
  "dependencies": {
68
- "axios": "^1.9.0",
69
68
  "bip39": "^3.1.0",
70
69
  "buffer": "^6.0.3",
71
70
  "elliptic": "^6.6.1",
@@ -81,4 +80,4 @@
81
80
  "@types/node": "^20.19.9",
82
81
  "typescript": "^5.9.2"
83
82
  }
84
- }
83
+ }
@@ -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.
@@ -270,19 +270,29 @@ export class AuthManager {
270
270
  }
271
271
 
272
272
  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');
273
+ await retryAsync(
274
+ async () => {
275
+ const httpService = this.oxyServices.httpService as HttpService;
276
+ const response = await httpService.request<SessionLoginResponse>({
277
+ method: 'POST',
278
+ url: '/api/auth/refresh',
279
+ data: { refreshToken },
280
+ cache: false,
281
+ });
282
+ await this.handleAuthSuccess(response, 'credentials');
283
+ },
284
+ 2, // 2 retries = 3 total attempts
285
+ 1000, // 1s base delay with exponential backoff + jitter
286
+ (error: any) => {
287
+ // Don't retry on 4xx client errors (invalid/revoked token)
288
+ const status = error?.status ?? error?.response?.status;
289
+ if (status && status >= 400 && status < 500) return false;
290
+ return true;
291
+ }
292
+ );
283
293
  return true;
284
294
  } catch {
285
- // Refresh failed, clear session and update state
295
+ // All retry attempts exhausted, clear session
286
296
  await this.clearSession();
287
297
  this.currentUser = null;
288
298
  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';
@@ -279,7 +280,7 @@ export class HttpService {
279
280
  }
280
281
 
281
282
  // Debug logging for CSRF issues
282
- if (isStateChangingMethod && __DEV__) {
283
+ if (isStateChangingMethod && isDev()) {
283
284
  console.log('[HttpService] CSRF Debug:', {
284
285
  url,
285
286
  method,
@@ -484,20 +485,20 @@ export class HttpService {
484
485
  // Return cached token if available
485
486
  const cachedToken = this.tokenStore.getCsrfToken();
486
487
  if (cachedToken) {
487
- if (__DEV__) console.log('[HttpService] Using cached CSRF token');
488
+ if (isDev()) console.log('[HttpService] Using cached CSRF token');
488
489
  return cachedToken;
489
490
  }
490
491
 
491
492
  // Deduplicate concurrent CSRF token fetches
492
493
  const existingPromise = this.tokenStore.getCsrfTokenFetchPromise();
493
494
  if (existingPromise) {
494
- if (__DEV__) console.log('[HttpService] Waiting for existing CSRF fetch');
495
+ if (isDev()) console.log('[HttpService] Waiting for existing CSRF fetch');
495
496
  return existingPromise;
496
497
  }
497
498
 
498
499
  const fetchPromise = (async () => {
499
500
  try {
500
- if (__DEV__) console.log('[HttpService] Fetching CSRF token from:', `${this.baseURL}/api/csrf-token`);
501
+ if (isDev()) console.log('[HttpService] Fetching CSRF token from:', `${this.baseURL}/api/csrf-token`);
501
502
 
502
503
  // Use AbortController for timeout (more compatible than AbortSignal.timeout)
503
504
  const controller = new AbortController();
@@ -512,11 +513,11 @@ export class HttpService {
512
513
 
513
514
  clearTimeout(timeoutId);
514
515
 
515
- if (__DEV__) console.log('[HttpService] CSRF fetch response:', response.status, response.ok);
516
+ if (isDev()) console.log('[HttpService] CSRF fetch response:', response.status, response.ok);
516
517
 
517
518
  if (response.ok) {
518
519
  const data = await response.json() as { csrfToken?: string };
519
- if (__DEV__) console.log('[HttpService] CSRF response data:', data);
520
+ if (isDev()) console.log('[HttpService] CSRF response data:', data);
520
521
  const token = data.csrfToken || null;
521
522
  this.tokenStore.setCsrfToken(token);
522
523
  this.logger.debug('CSRF token fetched');
@@ -531,11 +532,11 @@ export class HttpService {
531
532
  return headerToken;
532
533
  }
533
534
 
534
- if (__DEV__) console.log('[HttpService] CSRF fetch failed with status:', response.status);
535
+ if (isDev()) console.log('[HttpService] CSRF fetch failed with status:', response.status);
535
536
  this.logger.warn('Failed to fetch CSRF token:', response.status);
536
537
  return null;
537
538
  } catch (error) {
538
- if (__DEV__) console.log('[HttpService] CSRF fetch error:', error);
539
+ if (isDev()) console.log('[HttpService] CSRF fetch error:', error);
539
540
  this.logger.warn('CSRF token fetch error:', error);
540
541
  return null;
541
542
  } finally {
@@ -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
  }
@@ -32,40 +32,44 @@ function isNodeJS(): boolean {
32
32
  */
33
33
  async function initExpoCrypto(): Promise<typeof import('expo-crypto')> {
34
34
  if (!ExpoCrypto) {
35
- ExpoCrypto = await import('expo-crypto');
35
+ // Variable indirection prevents bundlers (Vite, webpack) from statically resolving this
36
+ const moduleName = 'expo-crypto';
37
+ ExpoCrypto = await import(moduleName);
36
38
  }
37
- return ExpoCrypto;
39
+ return ExpoCrypto!;
38
40
  }
39
41
 
40
42
  /**
41
43
  * Compute SHA-256 hash of a string
42
44
  */
43
45
  async function sha256(message: string): Promise<string> {
44
- // In React Native, always use expo-crypto
45
- if (isReactNative() || !isNodeJS()) {
46
+ // In React Native, use expo-crypto
47
+ if (isReactNative()) {
46
48
  const Crypto = await initExpoCrypto();
47
49
  return Crypto.digestStringAsync(
48
50
  Crypto.CryptoDigestAlgorithm.SHA256,
49
51
  message
50
52
  );
51
53
  }
52
-
54
+
53
55
  // In Node.js, use Node's crypto module
54
- // Use Function constructor to prevent Metro bundler from statically analyzing this require
55
- // This ensures the require is only evaluated in Node.js runtime, not during Metro bundling
56
- try {
57
- // eslint-disable-next-line @typescript-eslint/no-implied-eval
58
- const getCrypto = new Function('return require("crypto")');
59
- const crypto = getCrypto();
60
- return crypto.createHash('sha256').update(message).digest('hex');
61
- } catch (error) {
62
- // Fallback to expo-crypto if Node crypto fails
63
- const Crypto = await initExpoCrypto();
64
- return Crypto.digestStringAsync(
65
- Crypto.CryptoDigestAlgorithm.SHA256,
66
- message
67
- );
56
+ if (isNodeJS()) {
57
+ try {
58
+ // eslint-disable-next-line @typescript-eslint/no-implied-eval
59
+ const getCrypto = new Function('return require("crypto")');
60
+ const nodeCrypto = getCrypto();
61
+ return nodeCrypto.createHash('sha256').update(message).digest('hex');
62
+ } catch {
63
+ // Fall through to Web Crypto API
64
+ }
68
65
  }
66
+
67
+ // Browser: use Web Crypto API
68
+ const encoder = new TextEncoder();
69
+ const data = encoder.encode(message);
70
+ const hashBuffer = await globalThis.crypto.subtle.digest('SHA-256', data);
71
+ const hashArray = Array.from(new Uint8Array(hashBuffer));
72
+ return hashArray.map(b => b.toString(16).padStart(2, '0')).join('');
69
73
  }
70
74
 
71
75
  export interface SignedMessage {
@@ -87,29 +91,33 @@ export class SignatureService {
87
91
  * Uses expo-crypto in React Native, crypto.randomBytes in Node.js
88
92
  */
89
93
  static async generateChallenge(): Promise<string> {
90
- if (isReactNative() || !isNodeJS()) {
91
- // Use expo-crypto for React Native (expo-random is deprecated)
94
+ // In React Native, use expo-crypto
95
+ if (isReactNative()) {
92
96
  const Crypto = await initExpoCrypto();
93
97
  const randomBytes = await Crypto.getRandomBytesAsync(32);
94
98
  return Array.from(randomBytes)
95
99
  .map((b: number) => b.toString(16).padStart(2, '0'))
96
100
  .join('');
97
101
  }
98
-
99
- // Node.js fallback
100
- try {
101
- // eslint-disable-next-line @typescript-eslint/no-implied-eval
102
- const getCrypto = new Function('return require("crypto")');
103
- const crypto = getCrypto();
104
- return crypto.randomBytes(32).toString('hex');
105
- } catch (error) {
106
- // Fallback to expo-crypto if Node crypto fails
107
- const Crypto = await initExpoCrypto();
108
- const randomBytes = await Crypto.getRandomBytesAsync(32);
109
- return Array.from(randomBytes)
110
- .map((b: number) => b.toString(16).padStart(2, '0'))
111
- .join('');
102
+
103
+ // In Node.js, use Node's crypto module
104
+ if (isNodeJS()) {
105
+ try {
106
+ // eslint-disable-next-line @typescript-eslint/no-implied-eval
107
+ const getCrypto = new Function('return require("crypto")');
108
+ const nodeCrypto = getCrypto();
109
+ return nodeCrypto.randomBytes(32).toString('hex');
110
+ } catch {
111
+ // Fall through to Web Crypto API
112
+ }
112
113
  }
114
+
115
+ // Browser: use Web Crypto API
116
+ const bytes = new Uint8Array(32);
117
+ globalThis.crypto.getRandomValues(bytes);
118
+ return Array.from(bytes)
119
+ .map(b => b.toString(16).padStart(2, '0'))
120
+ .join('');
113
121
  }
114
122
 
115
123
  /**
@@ -319,5 +327,3 @@ export class SignatureService {
319
327
  }
320
328
 
321
329
  export default SignatureService;
322
-
323
-