@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
|
@@ -151,7 +151,10 @@ function OxyServicesFedCMMixin(Base) {
|
|
|
151
151
|
}
|
|
152
152
|
const clientId = this.getClientId();
|
|
153
153
|
debug.log('Silent SSO: Starting for', clientId);
|
|
154
|
-
//
|
|
154
|
+
// Only try silent mediation (no UI) - works if user previously consented.
|
|
155
|
+
// We intentionally do NOT fall back to optional mediation here because
|
|
156
|
+
// this runs on app startup — showing browser UI without user action is bad UX.
|
|
157
|
+
// Optional/interactive mediation should only happen when the user clicks "Sign In".
|
|
155
158
|
let credential = null;
|
|
156
159
|
try {
|
|
157
160
|
const nonce = this.generateNonce();
|
|
@@ -165,33 +168,13 @@ function OxyServicesFedCMMixin(Base) {
|
|
|
165
168
|
debug.log('Silent SSO: Silent mediation result:', { hasCredential: !!credential, hasToken: !!credential?.token });
|
|
166
169
|
}
|
|
167
170
|
catch (silentError) {
|
|
168
|
-
// Silent mediation failed - this is expected if user hasn't consented before or is in quiet period
|
|
169
171
|
const errorName = silentError instanceof Error ? silentError.name : 'Unknown';
|
|
170
172
|
const errorMessage = silentError instanceof Error ? silentError.message : String(silentError);
|
|
171
|
-
debug.log('Silent SSO: Silent mediation
|
|
172
|
-
|
|
173
|
-
// If silent failed, try optional mediation which shows browser UI if needed
|
|
174
|
-
if (!credential || !credential.token) {
|
|
175
|
-
try {
|
|
176
|
-
const nonce = this.generateNonce();
|
|
177
|
-
debug.log('Silent SSO: Trying optional mediation (may show browser UI)...');
|
|
178
|
-
credential = await this.requestIdentityCredential({
|
|
179
|
-
configURL: this.constructor.DEFAULT_CONFIG_URL,
|
|
180
|
-
clientId,
|
|
181
|
-
nonce,
|
|
182
|
-
mediation: 'optional',
|
|
183
|
-
});
|
|
184
|
-
debug.log('Silent SSO: Optional mediation result:', { hasCredential: !!credential, hasToken: !!credential?.token });
|
|
185
|
-
}
|
|
186
|
-
catch (optionalError) {
|
|
187
|
-
const errorName = optionalError instanceof Error ? optionalError.name : 'Unknown';
|
|
188
|
-
const errorMessage = optionalError instanceof Error ? optionalError.message : String(optionalError);
|
|
189
|
-
debug.log('Silent SSO: Optional mediation also failed:', { name: errorName, message: errorMessage });
|
|
190
|
-
return null;
|
|
191
|
-
}
|
|
173
|
+
debug.log('Silent SSO: Silent mediation failed:', { name: errorName, message: errorMessage });
|
|
174
|
+
return null;
|
|
192
175
|
}
|
|
193
176
|
if (!credential || !credential.token) {
|
|
194
|
-
debug.log('Silent SSO: No credential returned (user
|
|
177
|
+
debug.log('Silent SSO: No credential returned (user not logged in at IdP or hasn\'t consented)');
|
|
195
178
|
return null;
|
|
196
179
|
}
|
|
197
180
|
debug.log('Silent SSO: Got credential, exchanging for session...');
|
|
@@ -341,28 +324,17 @@ function OxyServicesFedCMMixin(Base) {
|
|
|
341
324
|
* @private
|
|
342
325
|
*/
|
|
343
326
|
async exchangeIdTokenForSession(idToken) {
|
|
344
|
-
debug.log('
|
|
345
|
-
debug.log('exchangeIdTokenForSession: Token length:', idToken?.length);
|
|
346
|
-
debug.log('exchangeIdTokenForSession: Token preview:', idToken?.substring(0, 50) + '...');
|
|
327
|
+
debug.log('Exchanging ID token for session...');
|
|
347
328
|
try {
|
|
348
329
|
const response = await this.makeRequest('POST', '/api/fedcm/exchange', { id_token: idToken }, { cache: false });
|
|
349
|
-
debug.log('
|
|
350
|
-
|
|
351
|
-
hasSessionId: !!response?.sessionId,
|
|
330
|
+
debug.log('Token exchange complete:', {
|
|
331
|
+
hasSession: !!response?.sessionId,
|
|
352
332
|
hasUser: !!response?.user,
|
|
353
|
-
hasAccessToken: !!response?.accessToken,
|
|
354
|
-
userId: response?.user?.id,
|
|
355
|
-
username: response?.user?.username,
|
|
356
|
-
responseKeys: response ? Object.keys(response) : [],
|
|
357
333
|
});
|
|
358
334
|
return response;
|
|
359
335
|
}
|
|
360
336
|
catch (error) {
|
|
361
|
-
debug.error('
|
|
362
|
-
name: error instanceof Error ? error.name : 'Unknown',
|
|
363
|
-
message: error instanceof Error ? error.message : String(error),
|
|
364
|
-
stack: error instanceof Error ? error.stack : undefined,
|
|
365
|
-
});
|
|
337
|
+
debug.error('Token exchange failed:', error instanceof Error ? error.message : String(error));
|
|
366
338
|
throw error;
|
|
367
339
|
}
|
|
368
340
|
}
|
|
@@ -427,9 +399,9 @@ function OxyServicesFedCMMixin(Base) {
|
|
|
427
399
|
}
|
|
428
400
|
},
|
|
429
401
|
_a.DEFAULT_CONFIG_URL = 'https://auth.oxy.so/fedcm.json',
|
|
430
|
-
_a.FEDCM_TIMEOUT =
|
|
402
|
+
_a.FEDCM_TIMEOUT = 15000 // 15 seconds for interactive
|
|
431
403
|
,
|
|
432
|
-
_a.FEDCM_SILENT_TIMEOUT =
|
|
404
|
+
_a.FEDCM_SILENT_TIMEOUT = 3000 // 3 seconds for silent mediation
|
|
433
405
|
,
|
|
434
406
|
_a;
|
|
435
407
|
}
|
|
@@ -38,6 +38,7 @@ exports.OxyServicesLanguageMixin = OxyServicesLanguageMixin;
|
|
|
38
38
|
* Language Methods Mixin
|
|
39
39
|
*/
|
|
40
40
|
const languageUtils_1 = require("../utils/languageUtils");
|
|
41
|
+
const debugUtils_1 = require("../shared/utils/debugUtils");
|
|
41
42
|
function OxyServicesLanguageMixin(Base) {
|
|
42
43
|
return class extends Base {
|
|
43
44
|
constructor(...args) {
|
|
@@ -50,7 +51,9 @@ function OxyServicesLanguageMixin(Base) {
|
|
|
50
51
|
const isReactNative = typeof navigator !== 'undefined' && navigator.product === 'ReactNative';
|
|
51
52
|
if (isReactNative) {
|
|
52
53
|
try {
|
|
53
|
-
|
|
54
|
+
// Variable indirection prevents bundlers (Vite, webpack) from statically resolving this
|
|
55
|
+
const moduleName = '@react-native-async-storage/async-storage';
|
|
56
|
+
const asyncStorageModule = await Promise.resolve(`${moduleName}`).then(s => __importStar(require(s)));
|
|
54
57
|
const storage = asyncStorageModule.default;
|
|
55
58
|
return {
|
|
56
59
|
getItem: storage.getItem.bind(storage),
|
|
@@ -113,7 +116,7 @@ function OxyServicesLanguageMixin(Base) {
|
|
|
113
116
|
return null;
|
|
114
117
|
}
|
|
115
118
|
catch (error) {
|
|
116
|
-
if (
|
|
119
|
+
if ((0, debugUtils_1.isDev)()) {
|
|
117
120
|
console.warn('Failed to get current language:', error);
|
|
118
121
|
}
|
|
119
122
|
return null;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.OxyServicesPrivacyMixin = OxyServicesPrivacyMixin;
|
|
4
|
+
const debugUtils_1 = require("../shared/utils/debugUtils");
|
|
4
5
|
function OxyServicesPrivacyMixin(Base) {
|
|
5
6
|
return class extends Base {
|
|
6
7
|
constructor(...args) {
|
|
@@ -28,7 +29,7 @@ function OxyServicesPrivacyMixin(Base) {
|
|
|
28
29
|
}
|
|
29
30
|
catch (error) {
|
|
30
31
|
// If there's an error, assume not in list to avoid breaking functionality
|
|
31
|
-
if (
|
|
32
|
+
if ((0, debugUtils_1.isDev)()) {
|
|
32
33
|
console.warn('Error checking user list:', error);
|
|
33
34
|
}
|
|
34
35
|
return false;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.OxyServicesSecurityMixin = OxyServicesSecurityMixin;
|
|
4
|
+
const debugUtils_1 = require("../shared/utils/debugUtils");
|
|
4
5
|
function OxyServicesSecurityMixin(Base) {
|
|
5
6
|
return class extends Base {
|
|
6
7
|
constructor(...args) {
|
|
@@ -55,7 +56,7 @@ function OxyServicesSecurityMixin(Base) {
|
|
|
55
56
|
catch (error) {
|
|
56
57
|
// Don't throw - logging failures shouldn't break user flow
|
|
57
58
|
// But log for monitoring
|
|
58
|
-
if (
|
|
59
|
+
if ((0, debugUtils_1.isDev)()) {
|
|
59
60
|
console.warn('[OxyServices] Failed to log private key exported event:', error);
|
|
60
61
|
}
|
|
61
62
|
}
|
|
@@ -72,7 +73,7 @@ function OxyServicesSecurityMixin(Base) {
|
|
|
72
73
|
catch (error) {
|
|
73
74
|
// Don't throw - logging failures shouldn't break user flow
|
|
74
75
|
// But log for monitoring
|
|
75
|
-
if (
|
|
76
|
+
if ((0, debugUtils_1.isDev)()) {
|
|
76
77
|
console.warn('[OxyServices] Failed to log backup created event:', error);
|
|
77
78
|
}
|
|
78
79
|
}
|
|
@@ -13,7 +13,14 @@ exports.createDebugLogger = exports.debugError = exports.debugWarn = exports.deb
|
|
|
13
13
|
* Check if running in development mode
|
|
14
14
|
*/
|
|
15
15
|
const isDev = () => {
|
|
16
|
-
|
|
16
|
+
if (typeof __DEV__ !== 'undefined')
|
|
17
|
+
return __DEV__;
|
|
18
|
+
try {
|
|
19
|
+
return typeof process !== 'undefined' && process.env?.NODE_ENV === 'development';
|
|
20
|
+
}
|
|
21
|
+
catch {
|
|
22
|
+
return false;
|
|
23
|
+
}
|
|
17
24
|
};
|
|
18
25
|
exports.isDev = isDev;
|
|
19
26
|
/**
|
|
@@ -51,7 +51,9 @@ class DeviceManager {
|
|
|
51
51
|
static async getStorage() {
|
|
52
52
|
if (this.isReactNative()) {
|
|
53
53
|
try {
|
|
54
|
-
|
|
54
|
+
// Variable indirection prevents bundlers (Vite, webpack) from statically resolving this
|
|
55
|
+
const moduleName = '@react-native-async-storage/async-storage';
|
|
56
|
+
const asyncStorageModule = await Promise.resolve(`${moduleName}`).then(s => __importStar(require(s)));
|
|
55
57
|
const storage = asyncStorageModule.default;
|
|
56
58
|
return {
|
|
57
59
|
getItem: storage.getItem.bind(storage),
|
|
@@ -134,8 +134,9 @@ async function initPlatformFromReactNative() {
|
|
|
134
134
|
return; // Already initialized
|
|
135
135
|
}
|
|
136
136
|
try {
|
|
137
|
-
//
|
|
138
|
-
const
|
|
137
|
+
// Variable indirection prevents bundlers (Vite, webpack) from statically resolving this
|
|
138
|
+
const moduleName = 'react-native';
|
|
139
|
+
const { Platform } = await Promise.resolve(`${moduleName}`).then(s => __importStar(require(s)));
|
|
139
140
|
setPlatformOS(Platform.OS);
|
|
140
141
|
}
|
|
141
142
|
catch {
|
package/dist/esm/AuthManager.js
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
*
|
|
7
7
|
* @module core/AuthManager
|
|
8
8
|
*/
|
|
9
|
+
import { retryAsync } from './utils/asyncUtils';
|
|
9
10
|
/**
|
|
10
11
|
* Storage keys used by AuthManager.
|
|
11
12
|
*/
|
|
@@ -206,19 +207,28 @@ export class AuthManager {
|
|
|
206
207
|
return false;
|
|
207
208
|
}
|
|
208
209
|
try {
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
210
|
+
await retryAsync(async () => {
|
|
211
|
+
const httpService = this.oxyServices.httpService;
|
|
212
|
+
const response = await httpService.request({
|
|
213
|
+
method: 'POST',
|
|
214
|
+
url: '/api/auth/refresh',
|
|
215
|
+
data: { refreshToken },
|
|
216
|
+
cache: false,
|
|
217
|
+
});
|
|
218
|
+
await this.handleAuthSuccess(response, 'credentials');
|
|
219
|
+
}, 2, // 2 retries = 3 total attempts
|
|
220
|
+
1000, // 1s base delay with exponential backoff + jitter
|
|
221
|
+
(error) => {
|
|
222
|
+
// Don't retry on 4xx client errors (invalid/revoked token)
|
|
223
|
+
const status = error?.status ?? error?.response?.status;
|
|
224
|
+
if (status && status >= 400 && status < 500)
|
|
225
|
+
return false;
|
|
226
|
+
return true;
|
|
216
227
|
});
|
|
217
|
-
await this.handleAuthSuccess(response, 'credentials');
|
|
218
228
|
return true;
|
|
219
229
|
}
|
|
220
230
|
catch {
|
|
221
|
-
//
|
|
231
|
+
// All retry attempts exhausted, clear session
|
|
222
232
|
await this.clearSession();
|
|
223
233
|
this.currentUser = null;
|
|
224
234
|
this.notifyListeners();
|
|
@@ -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
|
*
|
|
@@ -233,7 +233,7 @@ export class CrossDomainAuth {
|
|
|
233
233
|
*
|
|
234
234
|
* @example
|
|
235
235
|
* ```typescript
|
|
236
|
-
* import { createCrossDomainAuth } from '@oxyhq/
|
|
236
|
+
* import { createCrossDomainAuth } from '@oxyhq/core';
|
|
237
237
|
*
|
|
238
238
|
* const oxyServices = new OxyServices({ baseURL: 'https://api.oxy.so' });
|
|
239
239
|
* const auth = createCrossDomainAuth(oxyServices);
|
package/dist/esm/HttpService.js
CHANGED
|
@@ -16,6 +16,7 @@ import { TTLCache, registerCacheForCleanup } from './utils/cache';
|
|
|
16
16
|
import { RequestDeduplicator, RequestQueue, SimpleLogger } from './utils/requestUtils';
|
|
17
17
|
import { retryAsync } from './utils/asyncUtils';
|
|
18
18
|
import { handleHttpError } from './utils/errorUtils';
|
|
19
|
+
import { isDev } from './shared/utils/debugUtils';
|
|
19
20
|
import { jwtDecode } from 'jwt-decode';
|
|
20
21
|
import { isNative, getPlatformOS } from './utils/platform';
|
|
21
22
|
/**
|
|
@@ -186,7 +187,7 @@ export class HttpService {
|
|
|
186
187
|
headers['X-Native-App'] = 'true';
|
|
187
188
|
}
|
|
188
189
|
// Debug logging for CSRF issues
|
|
189
|
-
if (isStateChangingMethod &&
|
|
190
|
+
if (isStateChangingMethod && isDev()) {
|
|
190
191
|
console.log('[HttpService] CSRF Debug:', {
|
|
191
192
|
url,
|
|
192
193
|
method,
|
|
@@ -370,20 +371,20 @@ export class HttpService {
|
|
|
370
371
|
// Return cached token if available
|
|
371
372
|
const cachedToken = this.tokenStore.getCsrfToken();
|
|
372
373
|
if (cachedToken) {
|
|
373
|
-
if (
|
|
374
|
+
if (isDev())
|
|
374
375
|
console.log('[HttpService] Using cached CSRF token');
|
|
375
376
|
return cachedToken;
|
|
376
377
|
}
|
|
377
378
|
// Deduplicate concurrent CSRF token fetches
|
|
378
379
|
const existingPromise = this.tokenStore.getCsrfTokenFetchPromise();
|
|
379
380
|
if (existingPromise) {
|
|
380
|
-
if (
|
|
381
|
+
if (isDev())
|
|
381
382
|
console.log('[HttpService] Waiting for existing CSRF fetch');
|
|
382
383
|
return existingPromise;
|
|
383
384
|
}
|
|
384
385
|
const fetchPromise = (async () => {
|
|
385
386
|
try {
|
|
386
|
-
if (
|
|
387
|
+
if (isDev())
|
|
387
388
|
console.log('[HttpService] Fetching CSRF token from:', `${this.baseURL}/api/csrf-token`);
|
|
388
389
|
// Use AbortController for timeout (more compatible than AbortSignal.timeout)
|
|
389
390
|
const controller = new AbortController();
|
|
@@ -395,11 +396,11 @@ export class HttpService {
|
|
|
395
396
|
signal: controller.signal,
|
|
396
397
|
});
|
|
397
398
|
clearTimeout(timeoutId);
|
|
398
|
-
if (
|
|
399
|
+
if (isDev())
|
|
399
400
|
console.log('[HttpService] CSRF fetch response:', response.status, response.ok);
|
|
400
401
|
if (response.ok) {
|
|
401
402
|
const data = await response.json();
|
|
402
|
-
if (
|
|
403
|
+
if (isDev())
|
|
403
404
|
console.log('[HttpService] CSRF response data:', data);
|
|
404
405
|
const token = data.csrfToken || null;
|
|
405
406
|
this.tokenStore.setCsrfToken(token);
|
|
@@ -413,13 +414,13 @@ export class HttpService {
|
|
|
413
414
|
this.logger.debug('CSRF token from header');
|
|
414
415
|
return headerToken;
|
|
415
416
|
}
|
|
416
|
-
if (
|
|
417
|
+
if (isDev())
|
|
417
418
|
console.log('[HttpService] CSRF fetch failed with status:', response.status);
|
|
418
419
|
this.logger.warn('Failed to fetch CSRF token:', response.status);
|
|
419
420
|
return null;
|
|
420
421
|
}
|
|
421
422
|
catch (error) {
|
|
422
|
-
if (
|
|
423
|
+
if (isDev())
|
|
423
424
|
console.log('[HttpService] CSRF fetch error:', error);
|
|
424
425
|
this.logger.warn('CSRF token fetch error:', error);
|
|
425
426
|
return null;
|
|
@@ -12,6 +12,8 @@ import { OxyAuthenticationError, OxyAuthenticationTimeoutError } from './OxyServ
|
|
|
12
12
|
*/
|
|
13
13
|
export class OxyServicesBase {
|
|
14
14
|
constructor(...args) {
|
|
15
|
+
/** @internal */ this._cachedUserId = undefined;
|
|
16
|
+
/** @internal */ this._cachedAccessToken = null;
|
|
15
17
|
const config = args[0];
|
|
16
18
|
if (!config || typeof config !== 'object') {
|
|
17
19
|
throw new Error('OxyConfig is required');
|
|
@@ -95,20 +97,31 @@ export class OxyServicesBase {
|
|
|
95
97
|
*/
|
|
96
98
|
clearTokens() {
|
|
97
99
|
this.httpService.clearTokens();
|
|
100
|
+
this._cachedUserId = undefined;
|
|
101
|
+
this._cachedAccessToken = null;
|
|
98
102
|
}
|
|
99
103
|
/**
|
|
100
|
-
* Get the current user ID from the access token
|
|
104
|
+
* Get the current user ID from the access token.
|
|
105
|
+
* Caches the decoded value and invalidates when the token changes.
|
|
101
106
|
*/
|
|
102
107
|
getCurrentUserId() {
|
|
103
108
|
const accessToken = this.httpService.getAccessToken();
|
|
109
|
+
// Return cached value if token hasn't changed
|
|
110
|
+
if (accessToken === this._cachedAccessToken && this._cachedUserId !== undefined) {
|
|
111
|
+
return this._cachedUserId;
|
|
112
|
+
}
|
|
113
|
+
this._cachedAccessToken = accessToken;
|
|
104
114
|
if (!accessToken) {
|
|
115
|
+
this._cachedUserId = null;
|
|
105
116
|
return null;
|
|
106
117
|
}
|
|
107
118
|
try {
|
|
108
119
|
const decoded = jwtDecode(accessToken);
|
|
109
|
-
|
|
120
|
+
this._cachedUserId = decoded.userId || decoded.id || null;
|
|
121
|
+
return this._cachedUserId;
|
|
110
122
|
}
|
|
111
|
-
catch
|
|
123
|
+
catch {
|
|
124
|
+
this._cachedUserId = null;
|
|
112
125
|
return null;
|
|
113
126
|
}
|
|
114
127
|
}
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
import { ec as EC } from 'elliptic';
|
|
8
8
|
import { isWeb, isIOS, isAndroid } from '../utils/platform';
|
|
9
9
|
import { logger } from '../utils/loggerUtils';
|
|
10
|
+
import { isDev } from '../shared/utils/debugUtils';
|
|
10
11
|
// Lazy imports for React Native specific modules
|
|
11
12
|
let SecureStore = null;
|
|
12
13
|
let ExpoCrypto = null;
|
|
@@ -41,7 +42,9 @@ const ANDROID_ACCOUNT_TYPE = 'com.oxy.account';
|
|
|
41
42
|
async function initSecureStore() {
|
|
42
43
|
if (!SecureStore) {
|
|
43
44
|
try {
|
|
44
|
-
|
|
45
|
+
// Variable indirection prevents bundlers (Vite, webpack) from statically resolving this
|
|
46
|
+
const moduleName = 'expo-secure-store';
|
|
47
|
+
SecureStore = await import(moduleName);
|
|
45
48
|
}
|
|
46
49
|
catch (error) {
|
|
47
50
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
@@ -74,7 +77,9 @@ function isWebPlatform() {
|
|
|
74
77
|
}
|
|
75
78
|
async function initExpoCrypto() {
|
|
76
79
|
if (!ExpoCrypto) {
|
|
77
|
-
|
|
80
|
+
// Variable indirection prevents bundlers (Vite, webpack) from statically resolving this
|
|
81
|
+
const moduleName = 'expo-crypto';
|
|
82
|
+
ExpoCrypto = await import(moduleName);
|
|
78
83
|
}
|
|
79
84
|
return ExpoCrypto;
|
|
80
85
|
}
|
|
@@ -201,7 +206,7 @@ export class KeyManager {
|
|
|
201
206
|
// Update cache
|
|
202
207
|
KeyManager.cachedSharedPublicKey = publicKey;
|
|
203
208
|
KeyManager.cachedHasSharedIdentity = true;
|
|
204
|
-
if (
|
|
209
|
+
if (isDev()) {
|
|
205
210
|
logger.debug('Shared identity created successfully', { component: 'KeyManager' });
|
|
206
211
|
}
|
|
207
212
|
return publicKey;
|
|
@@ -235,7 +240,7 @@ export class KeyManager {
|
|
|
235
240
|
return publicKey;
|
|
236
241
|
}
|
|
237
242
|
catch (error) {
|
|
238
|
-
if (
|
|
243
|
+
if (isDev()) {
|
|
239
244
|
logger.warn('Failed to get shared public key', { component: 'KeyManager' }, error);
|
|
240
245
|
}
|
|
241
246
|
KeyManager.cachedSharedPublicKey = null;
|
|
@@ -268,7 +273,7 @@ export class KeyManager {
|
|
|
268
273
|
return privateKey;
|
|
269
274
|
}
|
|
270
275
|
catch (error) {
|
|
271
|
-
if (
|
|
276
|
+
if (isDev()) {
|
|
272
277
|
logger.warn('Failed to get shared private key', { component: 'KeyManager' }, error);
|
|
273
278
|
}
|
|
274
279
|
return null;
|
|
@@ -295,7 +300,7 @@ export class KeyManager {
|
|
|
295
300
|
return hasShared;
|
|
296
301
|
}
|
|
297
302
|
catch (error) {
|
|
298
|
-
if (
|
|
303
|
+
if (isDev()) {
|
|
299
304
|
logger.warn('Failed to check shared identity', { component: 'KeyManager' }, error);
|
|
300
305
|
}
|
|
301
306
|
KeyManager.cachedHasSharedIdentity = false;
|
|
@@ -338,7 +343,7 @@ export class KeyManager {
|
|
|
338
343
|
// Update cache
|
|
339
344
|
KeyManager.cachedSharedPublicKey = publicKey;
|
|
340
345
|
KeyManager.cachedHasSharedIdentity = true;
|
|
341
|
-
if (
|
|
346
|
+
if (isDev()) {
|
|
342
347
|
logger.debug('Shared identity imported successfully', { component: 'KeyManager' });
|
|
343
348
|
}
|
|
344
349
|
return publicKey;
|
|
@@ -372,12 +377,12 @@ export class KeyManager {
|
|
|
372
377
|
await store.setItemAsync(STORAGE_KEYS.SHARED_SESSION_ID, sessionId);
|
|
373
378
|
await store.setItemAsync(STORAGE_KEYS.SHARED_SESSION_TOKEN, accessToken);
|
|
374
379
|
}
|
|
375
|
-
if (
|
|
380
|
+
if (isDev()) {
|
|
376
381
|
logger.debug('Shared session stored successfully', { component: 'KeyManager' });
|
|
377
382
|
}
|
|
378
383
|
}
|
|
379
384
|
catch (error) {
|
|
380
|
-
if (
|
|
385
|
+
if (isDev()) {
|
|
381
386
|
logger.error('Failed to store shared session', error, { component: 'KeyManager' });
|
|
382
387
|
}
|
|
383
388
|
throw error;
|
|
@@ -417,7 +422,7 @@ export class KeyManager {
|
|
|
417
422
|
return { sessionId, accessToken };
|
|
418
423
|
}
|
|
419
424
|
catch (error) {
|
|
420
|
-
if (
|
|
425
|
+
if (isDev()) {
|
|
421
426
|
logger.warn('Failed to get shared session', { component: 'KeyManager' }, error);
|
|
422
427
|
}
|
|
423
428
|
return null;
|
|
@@ -447,12 +452,12 @@ export class KeyManager {
|
|
|
447
452
|
await store.deleteItemAsync(STORAGE_KEYS.SHARED_SESSION_ID);
|
|
448
453
|
await store.deleteItemAsync(STORAGE_KEYS.SHARED_SESSION_TOKEN);
|
|
449
454
|
}
|
|
450
|
-
if (
|
|
455
|
+
if (isDev()) {
|
|
451
456
|
logger.debug('Shared session cleared successfully', { component: 'KeyManager' });
|
|
452
457
|
}
|
|
453
458
|
}
|
|
454
459
|
catch (error) {
|
|
455
|
-
if (
|
|
460
|
+
if (isDev()) {
|
|
456
461
|
logger.error('Failed to clear shared session', error, { component: 'KeyManager' });
|
|
457
462
|
}
|
|
458
463
|
}
|
|
@@ -474,7 +479,7 @@ export class KeyManager {
|
|
|
474
479
|
// Check if we already have a shared identity
|
|
475
480
|
const hasShared = await KeyManager.hasSharedIdentity();
|
|
476
481
|
if (hasShared) {
|
|
477
|
-
if (
|
|
482
|
+
if (isDev()) {
|
|
478
483
|
logger.debug('Shared identity already exists, skipping migration', { component: 'KeyManager' });
|
|
479
484
|
}
|
|
480
485
|
return true;
|
|
@@ -482,20 +487,20 @@ export class KeyManager {
|
|
|
482
487
|
// Get local identity
|
|
483
488
|
const privateKey = await KeyManager.getPrivateKey();
|
|
484
489
|
if (!privateKey) {
|
|
485
|
-
if (
|
|
490
|
+
if (isDev()) {
|
|
486
491
|
logger.debug('No local identity to migrate', { component: 'KeyManager' });
|
|
487
492
|
}
|
|
488
493
|
return false;
|
|
489
494
|
}
|
|
490
495
|
// Import to shared storage
|
|
491
496
|
await KeyManager.importSharedIdentity(privateKey);
|
|
492
|
-
if (
|
|
497
|
+
if (isDev()) {
|
|
493
498
|
logger.debug('Successfully migrated local identity to shared identity', { component: 'KeyManager' });
|
|
494
499
|
}
|
|
495
500
|
return true;
|
|
496
501
|
}
|
|
497
502
|
catch (error) {
|
|
498
|
-
if (
|
|
503
|
+
if (isDev()) {
|
|
499
504
|
logger.error('Failed to migrate to shared identity', error, { component: 'KeyManager' });
|
|
500
505
|
}
|
|
501
506
|
return false;
|
|
@@ -555,7 +560,7 @@ export class KeyManager {
|
|
|
555
560
|
catch (error) {
|
|
556
561
|
// If secure store is not available, return null (no identity)
|
|
557
562
|
// This allows the app to continue functioning even if secure store fails to load
|
|
558
|
-
if (
|
|
563
|
+
if (isDev()) {
|
|
559
564
|
logger.warn('Failed to access secure store', { component: 'KeyManager' }, error);
|
|
560
565
|
}
|
|
561
566
|
return null;
|
|
@@ -582,7 +587,7 @@ export class KeyManager {
|
|
|
582
587
|
// If secure store is not available, return null (no identity)
|
|
583
588
|
// Cache null to avoid repeated failed attempts
|
|
584
589
|
KeyManager.cachedPublicKey = null;
|
|
585
|
-
if (
|
|
590
|
+
if (isDev()) {
|
|
586
591
|
logger.warn('Failed to access secure store', { component: 'KeyManager' }, error);
|
|
587
592
|
}
|
|
588
593
|
return null;
|
|
@@ -609,7 +614,7 @@ export class KeyManager {
|
|
|
609
614
|
// If we can't check, assume no identity (safer default)
|
|
610
615
|
// Cache false to avoid repeated failed attempts
|
|
611
616
|
KeyManager.cachedHasIdentity = false;
|
|
612
|
-
if (
|
|
617
|
+
if (isDev()) {
|
|
613
618
|
logger.warn('Failed to check identity', { component: 'KeyManager' }, error);
|
|
614
619
|
}
|
|
615
620
|
return false;
|
|
@@ -642,12 +647,12 @@ export class KeyManager {
|
|
|
642
647
|
if (!skipBackup) {
|
|
643
648
|
try {
|
|
644
649
|
const backupSuccess = await KeyManager.backupIdentity();
|
|
645
|
-
if (!backupSuccess &&
|
|
650
|
+
if (!backupSuccess && isDev()) {
|
|
646
651
|
logger.warn('Failed to backup identity before deletion - proceeding anyway', { component: 'KeyManager' });
|
|
647
652
|
}
|
|
648
653
|
}
|
|
649
654
|
catch (backupError) {
|
|
650
|
-
if (
|
|
655
|
+
if (isDev()) {
|
|
651
656
|
logger.warn('Failed to backup identity before deletion', { component: 'KeyManager' }, backupError);
|
|
652
657
|
}
|
|
653
658
|
}
|
|
@@ -692,7 +697,7 @@ export class KeyManager {
|
|
|
692
697
|
return true;
|
|
693
698
|
}
|
|
694
699
|
catch (error) {
|
|
695
|
-
if (
|
|
700
|
+
if (isDev()) {
|
|
696
701
|
logger.error('Failed to backup identity', error, { component: 'KeyManager' });
|
|
697
702
|
}
|
|
698
703
|
return false;
|
|
@@ -732,7 +737,7 @@ export class KeyManager {
|
|
|
732
737
|
return true;
|
|
733
738
|
}
|
|
734
739
|
catch (error) {
|
|
735
|
-
if (
|
|
740
|
+
if (isDev()) {
|
|
736
741
|
logger.error('Identity integrity check failed', error, { component: 'KeyManager' });
|
|
737
742
|
}
|
|
738
743
|
return false;
|
|
@@ -780,7 +785,7 @@ export class KeyManager {
|
|
|
780
785
|
return false;
|
|
781
786
|
}
|
|
782
787
|
catch (error) {
|
|
783
|
-
if (
|
|
788
|
+
if (isDev()) {
|
|
784
789
|
logger.error('Failed to restore identity from backup', error, { component: 'KeyManager' });
|
|
785
790
|
}
|
|
786
791
|
return false;
|
|
@@ -31,7 +31,12 @@ function getRandomBytesSync(byteCount) {
|
|
|
31
31
|
if (!expoCryptoLoadAttempted) {
|
|
32
32
|
expoCryptoLoadAttempted = true;
|
|
33
33
|
try {
|
|
34
|
-
|
|
34
|
+
// Only use require() in CJS environments (Metro/Node). In ESM (Vite/browser),
|
|
35
|
+
// crypto.getRandomValues exists natively so this code path is never reached.
|
|
36
|
+
if (typeof require !== 'undefined') {
|
|
37
|
+
const moduleName = 'expo-crypto';
|
|
38
|
+
expoCryptoModule = require(moduleName);
|
|
39
|
+
}
|
|
35
40
|
}
|
|
36
41
|
catch {
|
|
37
42
|
// expo-crypto not available — expected in non-RN environments
|