@oxyhq/core 1.0.2 → 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 (114) 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 +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 +13 -41
  32. package/dist/cjs/mixins/OxyServices.language.js +5 -2
  33. package/dist/cjs/mixins/OxyServices.privacy.js +2 -1
  34. package/dist/cjs/mixins/OxyServices.security.js +3 -2
  35. package/dist/cjs/shared/utils/debugUtils.js +8 -1
  36. package/dist/cjs/utils/deviceManager.js +3 -1
  37. package/dist/cjs/utils/platform.js +3 -2
  38. package/dist/esm/AuthManager.js +19 -9
  39. package/dist/esm/CrossDomainAuth.js +2 -2
  40. package/dist/esm/HttpService.js +9 -8
  41. package/dist/esm/OxyServices.base.js +16 -3
  42. package/dist/esm/crypto/keyManager.js +29 -24
  43. package/dist/esm/crypto/polyfill.js +6 -1
  44. package/dist/esm/crypto/signatureService.js +40 -31
  45. package/dist/esm/i18n/index.js +11 -23
  46. package/dist/esm/i18n/locales/ar-SA.json +114 -115
  47. package/dist/esm/i18n/locales/ca-ES.json +114 -115
  48. package/dist/esm/i18n/locales/de-DE.json +114 -115
  49. package/dist/esm/i18n/locales/en-US.json +936 -936
  50. package/dist/esm/i18n/locales/es-ES.json +924 -924
  51. package/dist/esm/i18n/locales/fr-FR.json +114 -115
  52. package/dist/esm/i18n/locales/it-IT.json +114 -115
  53. package/dist/esm/i18n/locales/ja-JP.json +1 -1
  54. package/dist/esm/i18n/locales/ko-KR.json +114 -115
  55. package/dist/esm/i18n/locales/locales/ar-SA.json +120 -0
  56. package/dist/esm/i18n/locales/locales/ca-ES.json +120 -0
  57. package/dist/esm/i18n/locales/locales/de-DE.json +120 -0
  58. package/dist/esm/i18n/locales/locales/en-US.json +956 -0
  59. package/dist/esm/i18n/locales/locales/es-ES.json +944 -0
  60. package/dist/esm/i18n/locales/locales/fr-FR.json +120 -0
  61. package/dist/esm/i18n/locales/locales/it-IT.json +120 -0
  62. package/dist/esm/i18n/locales/locales/ja-JP.json +119 -0
  63. package/dist/esm/i18n/locales/locales/ko-KR.json +120 -0
  64. package/dist/esm/i18n/locales/locales/pt-PT.json +120 -0
  65. package/dist/esm/i18n/locales/locales/zh-CN.json +120 -0
  66. package/dist/esm/i18n/locales/pt-PT.json +114 -115
  67. package/dist/esm/i18n/locales/zh-CN.json +114 -115
  68. package/dist/esm/mixins/OxyServices.fedcm.js +13 -41
  69. package/dist/esm/mixins/OxyServices.language.js +5 -2
  70. package/dist/esm/mixins/OxyServices.privacy.js +2 -1
  71. package/dist/esm/mixins/OxyServices.security.js +3 -2
  72. package/dist/esm/shared/utils/debugUtils.js +8 -1
  73. package/dist/esm/utils/deviceManager.js +3 -1
  74. package/dist/esm/utils/platform.js +3 -2
  75. package/dist/types/CrossDomainAuth.d.ts +2 -2
  76. package/dist/types/OxyServices.base.d.ts +4 -1
  77. package/dist/types/OxyServices.d.ts +13 -0
  78. package/dist/types/index.d.ts +3 -0
  79. package/dist/types/mixins/OxyServices.analytics.d.ts +2 -0
  80. package/dist/types/mixins/OxyServices.assets.d.ts +2 -0
  81. package/dist/types/mixins/OxyServices.auth.d.ts +2 -0
  82. package/dist/types/mixins/OxyServices.developer.d.ts +2 -0
  83. package/dist/types/mixins/OxyServices.devices.d.ts +2 -0
  84. package/dist/types/mixins/OxyServices.features.d.ts +2 -0
  85. package/dist/types/mixins/OxyServices.fedcm.d.ts +4 -2
  86. package/dist/types/mixins/OxyServices.karma.d.ts +2 -0
  87. package/dist/types/mixins/OxyServices.language.d.ts +2 -0
  88. package/dist/types/mixins/OxyServices.location.d.ts +2 -0
  89. package/dist/types/mixins/OxyServices.payment.d.ts +2 -0
  90. package/dist/types/mixins/OxyServices.popup.d.ts +2 -0
  91. package/dist/types/mixins/OxyServices.privacy.d.ts +2 -0
  92. package/dist/types/mixins/OxyServices.redirect.d.ts +2 -0
  93. package/dist/types/mixins/OxyServices.security.d.ts +2 -0
  94. package/dist/types/mixins/OxyServices.user.d.ts +2 -0
  95. package/dist/types/mixins/OxyServices.utility.d.ts +2 -0
  96. package/package.json +1 -2
  97. package/src/AuthManager.ts +25 -15
  98. package/src/CrossDomainAuth.ts +2 -2
  99. package/src/HttpService.ts +9 -8
  100. package/src/OxyServices.base.ts +21 -4
  101. package/src/OxyServices.ts +23 -2
  102. package/src/crypto/keyManager.ts +30 -25
  103. package/src/crypto/polyfill.ts +6 -1
  104. package/src/crypto/signatureService.ts +43 -37
  105. package/src/i18n/index.ts +33 -45
  106. package/src/index.ts +3 -0
  107. package/src/mixins/OxyServices.fedcm.ts +14 -44
  108. package/src/mixins/OxyServices.language.ts +6 -3
  109. package/src/mixins/OxyServices.privacy.ts +2 -1
  110. package/src/mixins/OxyServices.security.ts +3 -2
  111. package/src/shared/utils/__tests__/debugUtils.test.ts +55 -0
  112. package/src/shared/utils/debugUtils.ts +6 -1
  113. package/src/utils/deviceManager.ts +4 -2
  114. package/src/utils/platform.ts +3 -2
@@ -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
-
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
  }
@@ -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;