@oxyhq/core 1.0.1 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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 +2 -2
- package/dist/cjs/i18n/locales/ko-KR.json +114 -115
- 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 +2 -2
- package/dist/esm/i18n/locales/ko-KR.json +114 -115
- 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 +2 -3
- 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
|
@@ -100,6 +100,8 @@ export declare function OxyServicesPrivacyMixin<T extends typeof OxyServicesBase
|
|
|
100
100
|
getCloudURL(): string;
|
|
101
101
|
setTokens(accessToken: string, refreshToken?: string): void;
|
|
102
102
|
clearTokens(): void;
|
|
103
|
+
_cachedUserId: string | null | undefined;
|
|
104
|
+
_cachedAccessToken: string | null;
|
|
103
105
|
getCurrentUserId(): string | null;
|
|
104
106
|
hasValidToken(): boolean;
|
|
105
107
|
getAccessToken(): string | null;
|
|
@@ -216,6 +216,8 @@ export declare function OxyServicesRedirectAuthMixin<T extends typeof OxyService
|
|
|
216
216
|
getCloudURL(): string;
|
|
217
217
|
setTokens(accessToken: string, refreshToken?: string): void;
|
|
218
218
|
clearTokens(): void;
|
|
219
|
+
_cachedUserId: string | null | undefined;
|
|
220
|
+
_cachedAccessToken: string | null;
|
|
219
221
|
getCurrentUserId(): string | null;
|
|
220
222
|
hasValidToken(): boolean;
|
|
221
223
|
getAccessToken(): string | null;
|
|
@@ -56,6 +56,8 @@ export declare function OxyServicesSecurityMixin<T extends typeof OxyServicesBas
|
|
|
56
56
|
getCloudURL(): string;
|
|
57
57
|
setTokens(accessToken: string, refreshToken?: string): void;
|
|
58
58
|
clearTokens(): void;
|
|
59
|
+
_cachedUserId: string | null | undefined;
|
|
60
|
+
_cachedAccessToken: string | null;
|
|
59
61
|
getCurrentUserId(): string | null;
|
|
60
62
|
hasValidToken(): boolean;
|
|
61
63
|
getAccessToken(): string | null;
|
|
@@ -160,6 +160,8 @@ export declare function OxyServicesUserMixin<T extends typeof OxyServicesBase>(B
|
|
|
160
160
|
getCloudURL(): string;
|
|
161
161
|
setTokens(accessToken: string, refreshToken?: string): void;
|
|
162
162
|
clearTokens(): void;
|
|
163
|
+
_cachedUserId: string | null | undefined;
|
|
164
|
+
_cachedAccessToken: string | null;
|
|
163
165
|
getCurrentUserId(): string | null;
|
|
164
166
|
hasValidToken(): boolean;
|
|
165
167
|
getAccessToken(): string | null;
|
|
@@ -71,6 +71,8 @@ export declare function OxyServicesUtilityMixin<T extends typeof OxyServicesBase
|
|
|
71
71
|
getCloudURL(): string;
|
|
72
72
|
setTokens(accessToken: string, refreshToken?: string): void;
|
|
73
73
|
clearTokens(): void;
|
|
74
|
+
_cachedUserId: string | null | undefined;
|
|
75
|
+
_cachedAccessToken: string | null;
|
|
74
76
|
getCurrentUserId(): string | null;
|
|
75
77
|
hasValidToken(): boolean;
|
|
76
78
|
getAccessToken(): string | null;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@oxyhq/core",
|
|
3
|
-
"version": "1.0
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"description": "OxyHQ SDK Foundation — API client, authentication, cryptographic identity, and shared utilities",
|
|
5
5
|
"main": "dist/cjs/index.js",
|
|
6
6
|
"module": "dist/esm/index.js",
|
|
@@ -65,7 +65,6 @@
|
|
|
65
65
|
"lint": "biome lint --error-on-warnings ./src"
|
|
66
66
|
},
|
|
67
67
|
"dependencies": {
|
|
68
|
-
"axios": "^1.9.0",
|
|
69
68
|
"bip39": "^3.1.0",
|
|
70
69
|
"buffer": "^6.0.3",
|
|
71
70
|
"elliptic": "^6.6.1",
|
|
@@ -81,4 +80,4 @@
|
|
|
81
80
|
"@types/node": "^20.19.9",
|
|
82
81
|
"typescript": "^5.9.2"
|
|
83
82
|
}
|
|
84
|
-
}
|
|
83
|
+
}
|
package/src/AuthManager.ts
CHANGED
|
@@ -10,13 +10,13 @@
|
|
|
10
10
|
import type { OxyServices } from './OxyServices';
|
|
11
11
|
import type { HttpService } from './HttpService';
|
|
12
12
|
import type { SessionLoginResponse, MinimalUserData } from './models/session';
|
|
13
|
+
import { retryAsync } from './utils/asyncUtils';
|
|
13
14
|
|
|
14
15
|
/**
|
|
15
|
-
* OxyServices
|
|
16
|
+
* OxyServices already declares revokeFedCMCredential via mixin type augmentation.
|
|
17
|
+
* This alias is kept for readability in the signOut method.
|
|
16
18
|
*/
|
|
17
|
-
|
|
18
|
-
revokeFedCMCredential?: () => Promise<void>;
|
|
19
|
-
}
|
|
19
|
+
type OxyServicesWithFedCM = OxyServices;
|
|
20
20
|
|
|
21
21
|
/**
|
|
22
22
|
* Storage adapter interface for platform-agnostic storage.
|
|
@@ -270,19 +270,29 @@ export class AuthManager {
|
|
|
270
270
|
}
|
|
271
271
|
|
|
272
272
|
try {
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
273
|
+
await retryAsync(
|
|
274
|
+
async () => {
|
|
275
|
+
const httpService = this.oxyServices.httpService as HttpService;
|
|
276
|
+
const response = await httpService.request<SessionLoginResponse>({
|
|
277
|
+
method: 'POST',
|
|
278
|
+
url: '/api/auth/refresh',
|
|
279
|
+
data: { refreshToken },
|
|
280
|
+
cache: false,
|
|
281
|
+
});
|
|
282
|
+
await this.handleAuthSuccess(response, 'credentials');
|
|
283
|
+
},
|
|
284
|
+
2, // 2 retries = 3 total attempts
|
|
285
|
+
1000, // 1s base delay with exponential backoff + jitter
|
|
286
|
+
(error: any) => {
|
|
287
|
+
// Don't retry on 4xx client errors (invalid/revoked token)
|
|
288
|
+
const status = error?.status ?? error?.response?.status;
|
|
289
|
+
if (status && status >= 400 && status < 500) return false;
|
|
290
|
+
return true;
|
|
291
|
+
}
|
|
292
|
+
);
|
|
283
293
|
return true;
|
|
284
294
|
} catch {
|
|
285
|
-
//
|
|
295
|
+
// All retry attempts exhausted, clear session
|
|
286
296
|
await this.clearSession();
|
|
287
297
|
this.currentUser = null;
|
|
288
298
|
this.notifyListeners();
|
package/src/CrossDomainAuth.ts
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
*
|
|
11
11
|
* Usage:
|
|
12
12
|
* ```typescript
|
|
13
|
-
* import { CrossDomainAuth } from '@oxyhq/
|
|
13
|
+
* import { CrossDomainAuth } from '@oxyhq/core';
|
|
14
14
|
*
|
|
15
15
|
* const auth = new CrossDomainAuth(oxyServices);
|
|
16
16
|
*
|
|
@@ -287,7 +287,7 @@ export class CrossDomainAuth {
|
|
|
287
287
|
*
|
|
288
288
|
* @example
|
|
289
289
|
* ```typescript
|
|
290
|
-
* import { createCrossDomainAuth } from '@oxyhq/
|
|
290
|
+
* import { createCrossDomainAuth } from '@oxyhq/core';
|
|
291
291
|
*
|
|
292
292
|
* const oxyServices = new OxyServices({ baseURL: 'https://api.oxy.so' });
|
|
293
293
|
* const auth = createCrossDomainAuth(oxyServices);
|
package/src/HttpService.ts
CHANGED
|
@@ -17,6 +17,7 @@ import { TTLCache, registerCacheForCleanup } from './utils/cache';
|
|
|
17
17
|
import { RequestDeduplicator, RequestQueue, SimpleLogger } from './utils/requestUtils';
|
|
18
18
|
import { retryAsync } from './utils/asyncUtils';
|
|
19
19
|
import { handleHttpError } from './utils/errorUtils';
|
|
20
|
+
import { isDev } from './shared/utils/debugUtils';
|
|
20
21
|
import { jwtDecode } from 'jwt-decode';
|
|
21
22
|
import { isNative, getPlatformOS } from './utils/platform';
|
|
22
23
|
import type { OxyConfig } from './models/interfaces';
|
|
@@ -279,7 +280,7 @@ export class HttpService {
|
|
|
279
280
|
}
|
|
280
281
|
|
|
281
282
|
// Debug logging for CSRF issues
|
|
282
|
-
if (isStateChangingMethod &&
|
|
283
|
+
if (isStateChangingMethod && isDev()) {
|
|
283
284
|
console.log('[HttpService] CSRF Debug:', {
|
|
284
285
|
url,
|
|
285
286
|
method,
|
|
@@ -484,20 +485,20 @@ export class HttpService {
|
|
|
484
485
|
// Return cached token if available
|
|
485
486
|
const cachedToken = this.tokenStore.getCsrfToken();
|
|
486
487
|
if (cachedToken) {
|
|
487
|
-
if (
|
|
488
|
+
if (isDev()) console.log('[HttpService] Using cached CSRF token');
|
|
488
489
|
return cachedToken;
|
|
489
490
|
}
|
|
490
491
|
|
|
491
492
|
// Deduplicate concurrent CSRF token fetches
|
|
492
493
|
const existingPromise = this.tokenStore.getCsrfTokenFetchPromise();
|
|
493
494
|
if (existingPromise) {
|
|
494
|
-
if (
|
|
495
|
+
if (isDev()) console.log('[HttpService] Waiting for existing CSRF fetch');
|
|
495
496
|
return existingPromise;
|
|
496
497
|
}
|
|
497
498
|
|
|
498
499
|
const fetchPromise = (async () => {
|
|
499
500
|
try {
|
|
500
|
-
if (
|
|
501
|
+
if (isDev()) console.log('[HttpService] Fetching CSRF token from:', `${this.baseURL}/api/csrf-token`);
|
|
501
502
|
|
|
502
503
|
// Use AbortController for timeout (more compatible than AbortSignal.timeout)
|
|
503
504
|
const controller = new AbortController();
|
|
@@ -512,11 +513,11 @@ export class HttpService {
|
|
|
512
513
|
|
|
513
514
|
clearTimeout(timeoutId);
|
|
514
515
|
|
|
515
|
-
if (
|
|
516
|
+
if (isDev()) console.log('[HttpService] CSRF fetch response:', response.status, response.ok);
|
|
516
517
|
|
|
517
518
|
if (response.ok) {
|
|
518
519
|
const data = await response.json() as { csrfToken?: string };
|
|
519
|
-
if (
|
|
520
|
+
if (isDev()) console.log('[HttpService] CSRF response data:', data);
|
|
520
521
|
const token = data.csrfToken || null;
|
|
521
522
|
this.tokenStore.setCsrfToken(token);
|
|
522
523
|
this.logger.debug('CSRF token fetched');
|
|
@@ -531,11 +532,11 @@ export class HttpService {
|
|
|
531
532
|
return headerToken;
|
|
532
533
|
}
|
|
533
534
|
|
|
534
|
-
if (
|
|
535
|
+
if (isDev()) console.log('[HttpService] CSRF fetch failed with status:', response.status);
|
|
535
536
|
this.logger.warn('Failed to fetch CSRF token:', response.status);
|
|
536
537
|
return null;
|
|
537
538
|
} catch (error) {
|
|
538
|
-
if (
|
|
539
|
+
if (isDev()) console.log('[HttpService] CSRF fetch error:', error);
|
|
539
540
|
this.logger.warn('CSRF token fetch error:', error);
|
|
540
541
|
return null;
|
|
541
542
|
} finally {
|
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
|
-
|