@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
@@ -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
-
package/src/i18n/index.ts CHANGED
@@ -1,52 +1,40 @@
1
- // Use JSON locale files (RN Metro supports static requires reliably)
2
- // eslint-disable-next-line @typescript-eslint/no-var-requires
3
- const enUS = require('./locales/en-US.json') as Record<string, any>;
4
- // eslint-disable-next-line @typescript-eslint/no-var-requires
5
- const esES = require('./locales/es-ES.json') as Record<string, any>;
6
- // eslint-disable-next-line @typescript-eslint/no-var-requires
7
- const caES = require('./locales/ca-ES.json') as Record<string, any>;
8
- // eslint-disable-next-line @typescript-eslint/no-var-requires
9
- const frFR = require('./locales/fr-FR.json') as Record<string, any>;
10
- // eslint-disable-next-line @typescript-eslint/no-var-requires
11
- const deDE = require('./locales/de-DE.json') as Record<string, any>;
12
- // eslint-disable-next-line @typescript-eslint/no-var-requires
13
- const itIT = require('./locales/it-IT.json') as Record<string, any>;
14
- // eslint-disable-next-line @typescript-eslint/no-var-requires
15
- const ptPT = require('./locales/pt-PT.json') as Record<string, any>;
16
- // eslint-disable-next-line @typescript-eslint/no-var-requires
17
- const jaJP = require('./locales/ja-JP.json') as Record<string, any>;
18
- // eslint-disable-next-line @typescript-eslint/no-var-requires
19
- const koKR = require('./locales/ko-KR.json') as Record<string, any>;
20
- // eslint-disable-next-line @typescript-eslint/no-var-requires
21
- const zhCN = require('./locales/zh-CN.json') as Record<string, any>;
22
- // eslint-disable-next-line @typescript-eslint/no-var-requires
23
- const arSA = require('./locales/ar-SA.json') as Record<string, any>;
1
+ import enUS from './locales/en-US.json';
2
+ import esES from './locales/es-ES.json';
3
+ import caES from './locales/ca-ES.json';
4
+ import frFR from './locales/fr-FR.json';
5
+ import deDE from './locales/de-DE.json';
6
+ import itIT from './locales/it-IT.json';
7
+ import ptPT from './locales/pt-PT.json';
8
+ import jaJP from './locales/ja-JP.json';
9
+ import koKR from './locales/ko-KR.json';
10
+ import zhCN from './locales/zh-CN.json';
11
+ import arSA from './locales/ar-SA.json';
24
12
 
25
13
  export type LocaleDict = Record<string, any>;
26
14
 
27
15
  const DICTS: Record<string, LocaleDict> = {
28
- 'en': enUS as LocaleDict,
29
- 'en-US': enUS as LocaleDict,
30
- 'es': esES as LocaleDict,
31
- 'es-ES': esES as LocaleDict,
32
- 'ca': caES as LocaleDict,
33
- 'ca-ES': caES as LocaleDict,
34
- 'fr': frFR as LocaleDict,
35
- 'fr-FR': frFR as LocaleDict,
36
- 'de': deDE as LocaleDict,
37
- 'de-DE': deDE as LocaleDict,
38
- 'it': itIT as LocaleDict,
39
- 'it-IT': itIT as LocaleDict,
40
- 'pt': ptPT as LocaleDict,
41
- 'pt-PT': ptPT as LocaleDict,
42
- 'ja': jaJP as LocaleDict,
43
- 'ja-JP': jaJP as LocaleDict,
44
- 'ko': koKR as LocaleDict,
45
- 'ko-KR': koKR as LocaleDict,
46
- 'zh': zhCN as LocaleDict,
47
- 'zh-CN': zhCN as LocaleDict,
48
- 'ar': arSA as LocaleDict,
49
- 'ar-SA': arSA as LocaleDict,
16
+ 'en': enUS,
17
+ 'en-US': enUS,
18
+ 'es': esES,
19
+ 'es-ES': esES,
20
+ 'ca': caES,
21
+ 'ca-ES': caES,
22
+ 'fr': frFR,
23
+ 'fr-FR': frFR,
24
+ 'de': deDE,
25
+ 'de-DE': deDE,
26
+ 'it': itIT,
27
+ 'it-IT': itIT,
28
+ 'pt': ptPT,
29
+ 'pt-PT': ptPT,
30
+ 'ja': jaJP,
31
+ 'ja-JP': jaJP,
32
+ 'ko': koKR,
33
+ 'ko-KR': koKR,
34
+ 'zh': zhCN,
35
+ 'zh-CN': zhCN,
36
+ 'ar': arSA,
37
+ 'ar-SA': arSA,
50
38
  };
51
39
 
52
40
  const FALLBACK = 'en-US';
package/src/index.ts CHANGED
@@ -27,6 +27,9 @@ export type { StorageAdapter, AuthStateChangeCallback, AuthMethod, AuthManagerCo
27
27
 
28
28
  export { CrossDomainAuth, createCrossDomainAuth } from './CrossDomainAuth';
29
29
  export type { CrossDomainAuthOptions } from './CrossDomainAuth';
30
+ export type { FedCMAuthOptions, FedCMConfig } from './mixins/OxyServices.fedcm';
31
+ export type { PopupAuthOptions } from './mixins/OxyServices.popup';
32
+ export type { RedirectAuthOptions } from './mixins/OxyServices.redirect';
30
33
 
31
34
  // --- Crypto / Identity ---
32
35
  export { KeyManager, SignatureService, RecoveryPhraseService } from './crypto';
@@ -49,8 +49,8 @@ export function OxyServicesFedCMMixin<T extends typeof OxyServicesBase>(Base: T)
49
49
  super(...(args as [any]));
50
50
  }
51
51
  public static readonly DEFAULT_CONFIG_URL = 'https://auth.oxy.so/fedcm.json';
52
- public static readonly FEDCM_TIMEOUT = 60000; // 1 minute for interactive
53
- public static readonly FEDCM_SILENT_TIMEOUT = 10000; // 10 seconds for silent mediation
52
+ public static readonly FEDCM_TIMEOUT = 15000; // 15 seconds for interactive
53
+ public static readonly FEDCM_SILENT_TIMEOUT = 3000; // 3 seconds for silent mediation
54
54
 
55
55
  /**
56
56
  * Check if FedCM is supported in the current browser
@@ -180,7 +180,10 @@ export function OxyServicesFedCMMixin<T extends typeof OxyServicesBase>(Base: T)
180
180
  const clientId = this.getClientId();
181
181
  debug.log('Silent SSO: Starting for', clientId);
182
182
 
183
- // First try silent mediation (no UI) - works if user previously consented
183
+ // Only try silent mediation (no UI) - works if user previously consented.
184
+ // We intentionally do NOT fall back to optional mediation here because
185
+ // this runs on app startup — showing browser UI without user action is bad UX.
186
+ // Optional/interactive mediation should only happen when the user clicks "Sign In".
184
187
  let credential: { token: string } | null = null;
185
188
 
186
189
  try {
@@ -196,36 +199,14 @@ export function OxyServicesFedCMMixin<T extends typeof OxyServicesBase>(Base: T)
196
199
 
197
200
  debug.log('Silent SSO: Silent mediation result:', { hasCredential: !!credential, hasToken: !!credential?.token });
198
201
  } catch (silentError) {
199
- // Silent mediation failed - this is expected if user hasn't consented before or is in quiet period
200
202
  const errorName = silentError instanceof Error ? silentError.name : 'Unknown';
201
203
  const errorMessage = silentError instanceof Error ? silentError.message : String(silentError);
202
- debug.log('Silent SSO: Silent mediation error (will try optional):', { name: errorName, message: errorMessage });
203
- }
204
-
205
- // If silent failed, try optional mediation which shows browser UI if needed
206
- if (!credential || !credential.token) {
207
- try {
208
- const nonce = this.generateNonce();
209
- debug.log('Silent SSO: Trying optional mediation (may show browser UI)...');
210
-
211
- credential = await this.requestIdentityCredential({
212
- configURL: (this.constructor as any).DEFAULT_CONFIG_URL,
213
- clientId,
214
- nonce,
215
- mediation: 'optional',
216
- });
217
-
218
- debug.log('Silent SSO: Optional mediation result:', { hasCredential: !!credential, hasToken: !!credential?.token });
219
- } catch (optionalError) {
220
- const errorName = optionalError instanceof Error ? optionalError.name : 'Unknown';
221
- const errorMessage = optionalError instanceof Error ? optionalError.message : String(optionalError);
222
- debug.log('Silent SSO: Optional mediation also failed:', { name: errorName, message: errorMessage });
223
- return null;
224
- }
204
+ debug.log('Silent SSO: Silent mediation failed:', { name: errorName, message: errorMessage });
205
+ return null;
225
206
  }
226
207
 
227
208
  if (!credential || !credential.token) {
228
- debug.log('Silent SSO: No credential returned (user may have dismissed prompt or is not logged in at IdP)');
209
+ debug.log('Silent SSO: No credential returned (user not logged in at IdP or hasn\'t consented)');
229
210
  return null;
230
211
  }
231
212
 
@@ -392,9 +373,7 @@ export function OxyServicesFedCMMixin<T extends typeof OxyServicesBase>(Base: T)
392
373
  * @private
393
374
  */
394
375
  public async exchangeIdTokenForSession(idToken: string): Promise<SessionLoginResponse> {
395
- debug.log('exchangeIdTokenForSession: Starting exchange...');
396
- debug.log('exchangeIdTokenForSession: Token length:', idToken?.length);
397
- debug.log('exchangeIdTokenForSession: Token preview:', idToken?.substring(0, 50) + '...');
376
+ debug.log('Exchanging ID token for session...');
398
377
 
399
378
  try {
400
379
  const response = await this.makeRequest<SessionLoginResponse>(
@@ -404,23 +383,14 @@ export function OxyServicesFedCMMixin<T extends typeof OxyServicesBase>(Base: T)
404
383
  { cache: false }
405
384
  );
406
385
 
407
- debug.log('exchangeIdTokenForSession: Response received:', {
408
- hasResponse: !!response,
409
- hasSessionId: !!(response as any)?.sessionId,
410
- hasUser: !!(response as any)?.user,
411
- hasAccessToken: !!(response as any)?.accessToken,
412
- userId: (response as any)?.user?.id,
413
- username: (response as any)?.user?.username,
414
- responseKeys: response ? Object.keys(response) : [],
386
+ debug.log('Token exchange complete:', {
387
+ hasSession: !!response?.sessionId,
388
+ hasUser: !!response?.user,
415
389
  });
416
390
 
417
391
  return response;
418
392
  } catch (error) {
419
- debug.error('exchangeIdTokenForSession: Error:', {
420
- name: error instanceof Error ? error.name : 'Unknown',
421
- message: error instanceof Error ? error.message : String(error),
422
- stack: error instanceof Error ? error.stack : undefined,
423
- });
393
+ debug.error('Token exchange failed:', error instanceof Error ? error.message : String(error));
424
394
  throw error;
425
395
  }
426
396
  }
@@ -469,11 +439,15 @@ export function OxyServicesFedCMMixin<T extends typeof OxyServicesBase>(Base: T)
469
439
  * @private
470
440
  */
471
441
  public generateNonce(): string {
472
- if (typeof window !== 'undefined' && window.crypto && window.crypto.randomUUID) {
473
- return window.crypto.randomUUID();
442
+ if (typeof crypto !== 'undefined' && crypto.randomUUID) {
443
+ return crypto.randomUUID();
444
+ }
445
+ if (typeof crypto !== 'undefined' && crypto.getRandomValues) {
446
+ const bytes = new Uint8Array(16);
447
+ crypto.getRandomValues(bytes);
448
+ return Array.from(bytes, b => b.toString(16).padStart(2, '0')).join('');
474
449
  }
475
- // Fallback for older browsers
476
- return `${Date.now()}-${Math.random().toString(36).substring(2, 15)}`;
450
+ throw new Error('No secure random source available for nonce generation');
477
451
  }
478
452
 
479
453
  /**
@@ -4,6 +4,7 @@
4
4
  import { normalizeLanguageCode, getLanguageMetadata, getLanguageName, getNativeLanguageName } from '../utils/languageUtils';
5
5
  import type { LanguageMetadata } from '../utils/languageUtils';
6
6
  import type { OxyServicesBase } from '../OxyServices.base';
7
+ import { isDev } from '../shared/utils/debugUtils';
7
8
 
8
9
  export function OxyServicesLanguageMixin<T extends typeof OxyServicesBase>(Base: T) {
9
10
  return class extends Base {
@@ -22,8 +23,10 @@ export function OxyServicesLanguageMixin<T extends typeof OxyServicesBase>(Base:
22
23
 
23
24
  if (isReactNative) {
24
25
  try {
25
- const asyncStorageModule = await import('@react-native-async-storage/async-storage');
26
- const storage = (asyncStorageModule.default as unknown) as import('@react-native-async-storage/async-storage').AsyncStorageStatic;
26
+ // Variable indirection prevents bundlers (Vite, webpack) from statically resolving this
27
+ const moduleName = '@react-native-async-storage/async-storage';
28
+ const asyncStorageModule = await import(moduleName);
29
+ const storage = asyncStorageModule.default as unknown as { getItem: (key: string) => Promise<string | null>; setItem: (key: string, value: string) => Promise<void>; removeItem: (key: string) => Promise<void> };
27
30
  return {
28
31
  getItem: storage.getItem.bind(storage),
29
32
  setItem: storage.setItem.bind(storage),
@@ -84,7 +87,7 @@ export function OxyServicesLanguageMixin<T extends typeof OxyServicesBase>(Base:
84
87
 
85
88
  return null;
86
89
  } catch (error) {
87
- if (__DEV__) {
90
+ if (isDev()) {
88
91
  console.warn('Failed to get current language:', error);
89
92
  }
90
93
  return null;
@@ -397,10 +397,15 @@ export function OxyServicesPopupAuthMixin<T extends typeof OxyServicesBase>(Base
397
397
  * @private
398
398
  */
399
399
  public generateState(): string {
400
- if (typeof window !== 'undefined' && window.crypto && window.crypto.randomUUID) {
401
- return window.crypto.randomUUID();
400
+ if (typeof crypto !== 'undefined' && crypto.randomUUID) {
401
+ return crypto.randomUUID();
402
402
  }
403
- return `${Date.now()}-${Math.random().toString(36).substring(2, 15)}`;
403
+ if (typeof crypto !== 'undefined' && crypto.getRandomValues) {
404
+ const bytes = new Uint8Array(16);
405
+ crypto.getRandomValues(bytes);
406
+ return Array.from(bytes, b => b.toString(16).padStart(2, '0')).join('');
407
+ }
408
+ throw new Error('No secure random source available for state generation');
404
409
  }
405
410
 
406
411
  /**
@@ -409,10 +414,15 @@ export function OxyServicesPopupAuthMixin<T extends typeof OxyServicesBase>(Base
409
414
  * @private
410
415
  */
411
416
  public generateNonce(): string {
412
- if (typeof window !== 'undefined' && window.crypto && window.crypto.randomUUID) {
413
- return window.crypto.randomUUID();
417
+ if (typeof crypto !== 'undefined' && crypto.randomUUID) {
418
+ return crypto.randomUUID();
419
+ }
420
+ if (typeof crypto !== 'undefined' && crypto.getRandomValues) {
421
+ const bytes = new Uint8Array(16);
422
+ crypto.getRandomValues(bytes);
423
+ return Array.from(bytes, b => b.toString(16).padStart(2, '0')).join('');
414
424
  }
415
- return `${Date.now()}-${Math.random().toString(36).substring(2, 15)}`;
425
+ throw new Error('No secure random source available for nonce generation');
416
426
  }
417
427
 
418
428
  /**
@@ -3,6 +3,7 @@
3
3
  */
4
4
  import type { BlockedUser, RestrictedUser } from '../models/interfaces';
5
5
  import type { OxyServicesBase } from '../OxyServices.base';
6
+ import { isDev } from '../shared/utils/debugUtils';
6
7
 
7
8
  export function OxyServicesPrivacyMixin<T extends typeof OxyServicesBase>(Base: T) {
8
9
  return class extends Base {
@@ -35,7 +36,7 @@ export function OxyServicesPrivacyMixin<T extends typeof OxyServicesBase>(Base:
35
36
  });
36
37
  } catch (error) {
37
38
  // If there's an error, assume not in list to avoid breaking functionality
38
- if (__DEV__) {
39
+ if (isDev()) {
39
40
  console.warn('Error checking user list:', error);
40
41
  }
41
42
  return false;
@@ -299,10 +299,15 @@ export function OxyServicesRedirectAuthMixin<T extends typeof OxyServicesBase>(B
299
299
  * @private
300
300
  */
301
301
  public generateState(): string {
302
- if (typeof window !== 'undefined' && window.crypto && window.crypto.randomUUID) {
303
- return window.crypto.randomUUID();
302
+ if (typeof crypto !== 'undefined' && crypto.randomUUID) {
303
+ return crypto.randomUUID();
304
304
  }
305
- return `${Date.now()}-${Math.random().toString(36).substring(2, 15)}`;
305
+ if (typeof crypto !== 'undefined' && crypto.getRandomValues) {
306
+ const bytes = new Uint8Array(16);
307
+ crypto.getRandomValues(bytes);
308
+ return Array.from(bytes, b => b.toString(16).padStart(2, '0')).join('');
309
+ }
310
+ throw new Error('No secure random source available for state generation');
306
311
  }
307
312
 
308
313
  /**
@@ -311,10 +316,15 @@ export function OxyServicesRedirectAuthMixin<T extends typeof OxyServicesBase>(B
311
316
  * @private
312
317
  */
313
318
  public generateNonce(): string {
314
- if (typeof window !== 'undefined' && window.crypto && window.crypto.randomUUID) {
315
- return window.crypto.randomUUID();
319
+ if (typeof crypto !== 'undefined' && crypto.randomUUID) {
320
+ return crypto.randomUUID();
321
+ }
322
+ if (typeof crypto !== 'undefined' && crypto.getRandomValues) {
323
+ const bytes = new Uint8Array(16);
324
+ crypto.getRandomValues(bytes);
325
+ return Array.from(bytes, b => b.toString(16).padStart(2, '0')).join('');
316
326
  }
317
- return `${Date.now()}-${Math.random().toString(36).substring(2, 15)}`;
327
+ throw new Error('No secure random source available for nonce generation');
318
328
  }
319
329
 
320
330
  /**
@@ -3,6 +3,7 @@
3
3
  */
4
4
  import type { OxyServicesBase } from '../OxyServices.base';
5
5
  import type { SecurityActivity, SecurityActivityResponse, SecurityEventType } from '../models/interfaces';
6
+ import { isDev } from '../shared/utils/debugUtils';
6
7
 
7
8
  export function OxyServicesSecurityMixin<T extends typeof OxyServicesBase>(Base: T) {
8
9
  return class extends Base {
@@ -71,7 +72,7 @@ export function OxyServicesSecurityMixin<T extends typeof OxyServicesBase>(Base:
71
72
  } catch (error) {
72
73
  // Don't throw - logging failures shouldn't break user flow
73
74
  // But log for monitoring
74
- if (__DEV__) {
75
+ if (isDev()) {
75
76
  console.warn('[OxyServices] Failed to log private key exported event:', error);
76
77
  }
77
78
  }
@@ -93,7 +94,7 @@ export function OxyServicesSecurityMixin<T extends typeof OxyServicesBase>(Base:
93
94
  } catch (error) {
94
95
  // Don't throw - logging failures shouldn't break user flow
95
96
  // But log for monitoring
96
- if (__DEV__) {
97
+ if (isDev()) {
97
98
  console.warn('[OxyServices] Failed to log backup created event:', error);
98
99
  }
99
100
  }
@@ -0,0 +1,55 @@
1
+ /**
2
+ * Tests for isDev() utility.
3
+ *
4
+ * These tests manipulate the global __DEV__ and process.env.NODE_ENV
5
+ * to verify isDev() works across RN, Node, and browser-like environments.
6
+ */
7
+
8
+ describe('isDev', () => {
9
+ const originalDev = (globalThis as any).__DEV__;
10
+ const originalNodeEnv = process.env.NODE_ENV;
11
+
12
+ afterEach(() => {
13
+ // Restore globals
14
+ if (originalDev === undefined) {
15
+ delete (globalThis as any).__DEV__;
16
+ } else {
17
+ (globalThis as any).__DEV__ = originalDev;
18
+ }
19
+ process.env.NODE_ENV = originalNodeEnv;
20
+
21
+ // Clear module cache so isDev re-evaluates
22
+ jest.resetModules();
23
+ });
24
+
25
+ async function loadIsDev() {
26
+ const mod = await import('../debugUtils');
27
+ return mod.isDev;
28
+ }
29
+
30
+ it('returns true when __DEV__ is true (React Native)', async () => {
31
+ (globalThis as any).__DEV__ = true;
32
+ const isDev = await loadIsDev();
33
+ expect(isDev()).toBe(true);
34
+ });
35
+
36
+ it('returns false when __DEV__ is false', async () => {
37
+ (globalThis as any).__DEV__ = false;
38
+ const isDev = await loadIsDev();
39
+ expect(isDev()).toBe(false);
40
+ });
41
+
42
+ it('falls back to NODE_ENV when __DEV__ is undefined', async () => {
43
+ delete (globalThis as any).__DEV__;
44
+ process.env.NODE_ENV = 'development';
45
+ const isDev = await loadIsDev();
46
+ expect(isDev()).toBe(true);
47
+ });
48
+
49
+ it('returns false when NODE_ENV is production and __DEV__ is undefined', async () => {
50
+ delete (globalThis as any).__DEV__;
51
+ process.env.NODE_ENV = 'production';
52
+ const isDev = await loadIsDev();
53
+ expect(isDev()).toBe(false);
54
+ });
55
+ });
@@ -14,7 +14,12 @@ declare const __DEV__: boolean | undefined;
14
14
  * Check if running in development mode
15
15
  */
16
16
  export const isDev = (): boolean => {
17
- return typeof __DEV__ !== 'undefined' && __DEV__;
17
+ if (typeof __DEV__ !== 'undefined') return __DEV__;
18
+ try {
19
+ return typeof process !== 'undefined' && process.env?.NODE_ENV === 'development';
20
+ } catch {
21
+ return false;
22
+ }
18
23
  };
19
24
 
20
25
  /**
@@ -42,8 +42,10 @@ export class DeviceManager {
42
42
  }> {
43
43
  if (this.isReactNative()) {
44
44
  try {
45
- const asyncStorageModule = await import('@react-native-async-storage/async-storage');
46
- const storage = (asyncStorageModule.default as unknown) as import('@react-native-async-storage/async-storage').AsyncStorageStatic;
45
+ // Variable indirection prevents bundlers (Vite, webpack) from statically resolving this
46
+ const moduleName = '@react-native-async-storage/async-storage';
47
+ const asyncStorageModule = await import(moduleName);
48
+ const storage = asyncStorageModule.default as unknown as { getItem: (key: string) => Promise<string | null>; setItem: (key: string, value: string) => Promise<void>; removeItem: (key: string) => Promise<void> };
47
49
  return {
48
50
  getItem: storage.getItem.bind(storage),
49
51
  setItem: storage.setItem.bind(storage),
@@ -168,15 +170,12 @@ export class DeviceManager {
168
170
  * Generate a unique device ID
169
171
  */
170
172
  private static generateDeviceId(): string {
171
- // Use crypto.getRandomValues if available, otherwise fallback to Math.random
172
173
  if (typeof crypto !== 'undefined' && crypto.getRandomValues) {
173
174
  const array = new Uint8Array(32);
174
175
  crypto.getRandomValues(array);
175
176
  return Array.from(array, byte => byte.toString(16).padStart(2, '0')).join('');
176
- } else {
177
- // Fallback for environments without crypto.getRandomValues
178
- return 'device_' + Date.now().toString(36) + Math.random().toString(36).substr(2);
179
177
  }
178
+ throw new Error('No secure random source available for device ID generation');
180
179
  }
181
180
 
182
181
  /**
@@ -108,8 +108,9 @@ export async function initPlatformFromReactNative(): Promise<void> {
108
108
  }
109
109
 
110
110
  try {
111
- // Dynamic import to avoid bundler issues
112
- const { Platform } = await import('react-native');
111
+ // Variable indirection prevents bundlers (Vite, webpack) from statically resolving this
112
+ const moduleName = 'react-native';
113
+ const { Platform } = await import(moduleName);
113
114
  setPlatformOS(Platform.OS as PlatformOS);
114
115
  } catch {
115
116
  // react-native not available, use detected platform