@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.
- package/dist/cjs/AuthManager.js +19 -9
- package/dist/cjs/CrossDomainAuth.js +2 -2
- package/dist/cjs/HttpService.js +9 -8
- package/dist/cjs/OxyServices.base.js +16 -3
- package/dist/cjs/crypto/keyManager.js +29 -24
- package/dist/cjs/crypto/polyfill.js +6 -1
- package/dist/cjs/crypto/signatureService.js +40 -31
- package/dist/cjs/i18n/index.js +36 -45
- package/dist/cjs/i18n/locales/ar-SA.json +114 -115
- package/dist/cjs/i18n/locales/ca-ES.json +114 -115
- package/dist/cjs/i18n/locales/de-DE.json +114 -115
- package/dist/cjs/i18n/locales/en-US.json +936 -936
- package/dist/cjs/i18n/locales/es-ES.json +924 -924
- package/dist/cjs/i18n/locales/fr-FR.json +114 -115
- package/dist/cjs/i18n/locales/it-IT.json +114 -115
- package/dist/cjs/i18n/locales/ja-JP.json +1 -1
- package/dist/cjs/i18n/locales/ko-KR.json +114 -115
- package/dist/cjs/i18n/locales/locales/ar-SA.json +120 -0
- package/dist/cjs/i18n/locales/locales/ca-ES.json +120 -0
- package/dist/cjs/i18n/locales/locales/de-DE.json +120 -0
- package/dist/cjs/i18n/locales/locales/en-US.json +956 -0
- package/dist/cjs/i18n/locales/locales/es-ES.json +944 -0
- package/dist/cjs/i18n/locales/locales/fr-FR.json +120 -0
- package/dist/cjs/i18n/locales/locales/it-IT.json +120 -0
- package/dist/cjs/i18n/locales/locales/ja-JP.json +119 -0
- package/dist/cjs/i18n/locales/locales/ko-KR.json +120 -0
- package/dist/cjs/i18n/locales/locales/pt-PT.json +120 -0
- package/dist/cjs/i18n/locales/locales/zh-CN.json +120 -0
- package/dist/cjs/i18n/locales/pt-PT.json +114 -115
- package/dist/cjs/i18n/locales/zh-CN.json +114 -115
- package/dist/cjs/mixins/OxyServices.fedcm.js +13 -41
- package/dist/cjs/mixins/OxyServices.language.js +5 -2
- package/dist/cjs/mixins/OxyServices.privacy.js +2 -1
- package/dist/cjs/mixins/OxyServices.security.js +3 -2
- package/dist/cjs/shared/utils/debugUtils.js +8 -1
- package/dist/cjs/utils/deviceManager.js +3 -1
- package/dist/cjs/utils/platform.js +3 -2
- package/dist/esm/AuthManager.js +19 -9
- package/dist/esm/CrossDomainAuth.js +2 -2
- package/dist/esm/HttpService.js +9 -8
- package/dist/esm/OxyServices.base.js +16 -3
- package/dist/esm/crypto/keyManager.js +29 -24
- package/dist/esm/crypto/polyfill.js +6 -1
- package/dist/esm/crypto/signatureService.js +40 -31
- package/dist/esm/i18n/index.js +11 -23
- package/dist/esm/i18n/locales/ar-SA.json +114 -115
- package/dist/esm/i18n/locales/ca-ES.json +114 -115
- package/dist/esm/i18n/locales/de-DE.json +114 -115
- package/dist/esm/i18n/locales/en-US.json +936 -936
- package/dist/esm/i18n/locales/es-ES.json +924 -924
- package/dist/esm/i18n/locales/fr-FR.json +114 -115
- package/dist/esm/i18n/locales/it-IT.json +114 -115
- package/dist/esm/i18n/locales/ja-JP.json +1 -1
- package/dist/esm/i18n/locales/ko-KR.json +114 -115
- package/dist/esm/i18n/locales/locales/ar-SA.json +120 -0
- package/dist/esm/i18n/locales/locales/ca-ES.json +120 -0
- package/dist/esm/i18n/locales/locales/de-DE.json +120 -0
- package/dist/esm/i18n/locales/locales/en-US.json +956 -0
- package/dist/esm/i18n/locales/locales/es-ES.json +944 -0
- package/dist/esm/i18n/locales/locales/fr-FR.json +120 -0
- package/dist/esm/i18n/locales/locales/it-IT.json +120 -0
- package/dist/esm/i18n/locales/locales/ja-JP.json +119 -0
- package/dist/esm/i18n/locales/locales/ko-KR.json +120 -0
- package/dist/esm/i18n/locales/locales/pt-PT.json +120 -0
- package/dist/esm/i18n/locales/locales/zh-CN.json +120 -0
- package/dist/esm/i18n/locales/pt-PT.json +114 -115
- package/dist/esm/i18n/locales/zh-CN.json +114 -115
- package/dist/esm/mixins/OxyServices.fedcm.js +13 -41
- package/dist/esm/mixins/OxyServices.language.js +5 -2
- package/dist/esm/mixins/OxyServices.privacy.js +2 -1
- package/dist/esm/mixins/OxyServices.security.js +3 -2
- package/dist/esm/shared/utils/debugUtils.js +8 -1
- package/dist/esm/utils/deviceManager.js +3 -1
- package/dist/esm/utils/platform.js +3 -2
- package/dist/types/CrossDomainAuth.d.ts +2 -2
- package/dist/types/OxyServices.base.d.ts +4 -1
- package/dist/types/OxyServices.d.ts +13 -0
- package/dist/types/index.d.ts +3 -0
- package/dist/types/mixins/OxyServices.analytics.d.ts +2 -0
- package/dist/types/mixins/OxyServices.assets.d.ts +2 -0
- package/dist/types/mixins/OxyServices.auth.d.ts +2 -0
- package/dist/types/mixins/OxyServices.developer.d.ts +2 -0
- package/dist/types/mixins/OxyServices.devices.d.ts +2 -0
- package/dist/types/mixins/OxyServices.features.d.ts +2 -0
- package/dist/types/mixins/OxyServices.fedcm.d.ts +4 -2
- package/dist/types/mixins/OxyServices.karma.d.ts +2 -0
- package/dist/types/mixins/OxyServices.language.d.ts +2 -0
- package/dist/types/mixins/OxyServices.location.d.ts +2 -0
- package/dist/types/mixins/OxyServices.payment.d.ts +2 -0
- package/dist/types/mixins/OxyServices.popup.d.ts +2 -0
- package/dist/types/mixins/OxyServices.privacy.d.ts +2 -0
- package/dist/types/mixins/OxyServices.redirect.d.ts +2 -0
- package/dist/types/mixins/OxyServices.security.d.ts +2 -0
- package/dist/types/mixins/OxyServices.user.d.ts +2 -0
- package/dist/types/mixins/OxyServices.utility.d.ts +2 -0
- package/package.json +1 -2
- package/src/AuthManager.ts +25 -15
- package/src/CrossDomainAuth.ts +2 -2
- package/src/HttpService.ts +9 -8
- package/src/OxyServices.base.ts +21 -4
- package/src/OxyServices.ts +23 -2
- package/src/crypto/keyManager.ts +30 -25
- package/src/crypto/polyfill.ts +6 -1
- package/src/crypto/signatureService.ts +43 -37
- package/src/i18n/index.ts +33 -45
- package/src/index.ts +3 -0
- package/src/mixins/OxyServices.fedcm.ts +14 -44
- package/src/mixins/OxyServices.language.ts +6 -3
- package/src/mixins/OxyServices.privacy.ts +2 -1
- package/src/mixins/OxyServices.security.ts +3 -2
- package/src/shared/utils/__tests__/debugUtils.test.ts +55 -0
- package/src/shared/utils/debugUtils.ts +6 -1
- package/src/utils/deviceManager.ts +4 -2
- package/src/utils/platform.ts +3 -2
package/src/OxyServices.base.ts
CHANGED
|
@@ -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
|
-
|
|
148
|
-
|
|
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
|
}
|
package/src/OxyServices.ts
CHANGED
|
@@ -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
|
-
|
|
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 };
|
package/src/crypto/keyManager.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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 (
|
|
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 (
|
|
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 (
|
|
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 (
|
|
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 (
|
|
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 (
|
|
439
|
+
if (isDev()) {
|
|
435
440
|
logger.debug('Shared session stored successfully', { component: 'KeyManager' });
|
|
436
441
|
}
|
|
437
442
|
} catch (error) {
|
|
438
|
-
if (
|
|
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 (
|
|
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 (
|
|
520
|
+
if (isDev()) {
|
|
516
521
|
logger.debug('Shared session cleared successfully', { component: 'KeyManager' });
|
|
517
522
|
}
|
|
518
523
|
} catch (error) {
|
|
519
|
-
if (
|
|
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 (
|
|
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 (
|
|
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 (
|
|
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 (
|
|
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 (
|
|
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 (
|
|
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 (
|
|
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 &&
|
|
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 (
|
|
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 (
|
|
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 (
|
|
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 (
|
|
901
|
+
if (isDev()) {
|
|
897
902
|
logger.error('Failed to restore identity from backup', error, { component: 'KeyManager' });
|
|
898
903
|
}
|
|
899
904
|
return false;
|
package/src/crypto/polyfill.ts
CHANGED
|
@@ -37,7 +37,12 @@ function getRandomBytesSync(byteCount: number): Uint8Array {
|
|
|
37
37
|
if (!expoCryptoLoadAttempted) {
|
|
38
38
|
expoCryptoLoadAttempted = true;
|
|
39
39
|
try {
|
|
40
|
-
|
|
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
|
-
|
|
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,
|
|
45
|
-
if (isReactNative()
|
|
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
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
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
|
-
|
|
91
|
-
|
|
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
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
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
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
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
|
|
29
|
-
'en-US': enUS
|
|
30
|
-
'es': esES
|
|
31
|
-
'es-ES': esES
|
|
32
|
-
'ca': caES
|
|
33
|
-
'ca-ES': caES
|
|
34
|
-
'fr': frFR
|
|
35
|
-
'fr-FR': frFR
|
|
36
|
-
'de': deDE
|
|
37
|
-
'de-DE': deDE
|
|
38
|
-
'it': itIT
|
|
39
|
-
'it-IT': itIT
|
|
40
|
-
'pt': ptPT
|
|
41
|
-
'pt-PT': ptPT
|
|
42
|
-
'ja': jaJP
|
|
43
|
-
'ja-JP': jaJP
|
|
44
|
-
'ko': koKR
|
|
45
|
-
'ko-KR': koKR
|
|
46
|
-
'zh': zhCN
|
|
47
|
-
'zh-CN': zhCN
|
|
48
|
-
'ar': arSA
|
|
49
|
-
'ar-SA': arSA
|
|
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 =
|
|
53
|
-
public static readonly FEDCM_SILENT_TIMEOUT =
|
|
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
|
-
//
|
|
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
|
|
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
|
|
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('
|
|
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('
|
|
408
|
-
|
|
409
|
-
|
|
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('
|
|
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
|
-
|
|
26
|
-
const
|
|
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 (
|
|
90
|
+
if (isDev()) {
|
|
88
91
|
console.warn('Failed to get current language:', error);
|
|
89
92
|
}
|
|
90
93
|
return null;
|