@oxyhq/core 1.5.0 → 1.6.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 +1 -1
- package/dist/cjs/HttpService.js +87 -69
- package/dist/cjs/OxyServices.base.js +5 -4
- package/dist/cjs/crypto/keyManager.js +1 -13
- package/dist/cjs/crypto/signatureService.js +7 -20
- package/dist/cjs/mixins/OxyServices.analytics.js +2 -2
- package/dist/cjs/mixins/OxyServices.assets.js +14 -14
- package/dist/cjs/mixins/OxyServices.auth.js +19 -19
- package/dist/cjs/mixins/OxyServices.developer.js +6 -6
- package/dist/cjs/mixins/OxyServices.devices.js +7 -7
- package/dist/cjs/mixins/OxyServices.features.js +23 -23
- package/dist/cjs/mixins/OxyServices.fedcm.js +1 -1
- package/dist/cjs/mixins/OxyServices.karma.js +6 -6
- package/dist/cjs/mixins/OxyServices.location.js +2 -2
- package/dist/cjs/mixins/OxyServices.payment.js +6 -6
- package/dist/cjs/mixins/OxyServices.popup.js +1 -1
- package/dist/cjs/mixins/OxyServices.privacy.js +6 -6
- package/dist/cjs/mixins/OxyServices.security.js +3 -3
- package/dist/cjs/mixins/OxyServices.user.js +22 -22
- package/dist/cjs/mixins/OxyServices.utility.js +16 -7
- package/dist/cjs/utils/platform.js +14 -0
- package/dist/esm/AuthManager.js +1 -1
- package/dist/esm/HttpService.js +87 -69
- package/dist/esm/OxyServices.base.js +5 -4
- package/dist/esm/crypto/keyManager.js +1 -13
- package/dist/esm/crypto/signatureService.js +2 -15
- package/dist/esm/mixins/OxyServices.analytics.js +2 -2
- package/dist/esm/mixins/OxyServices.assets.js +14 -14
- package/dist/esm/mixins/OxyServices.auth.js +19 -19
- package/dist/esm/mixins/OxyServices.developer.js +6 -6
- package/dist/esm/mixins/OxyServices.devices.js +7 -7
- package/dist/esm/mixins/OxyServices.features.js +23 -23
- package/dist/esm/mixins/OxyServices.fedcm.js +1 -1
- package/dist/esm/mixins/OxyServices.karma.js +6 -6
- package/dist/esm/mixins/OxyServices.location.js +2 -2
- package/dist/esm/mixins/OxyServices.payment.js +6 -6
- package/dist/esm/mixins/OxyServices.popup.js +1 -1
- package/dist/esm/mixins/OxyServices.privacy.js +6 -6
- package/dist/esm/mixins/OxyServices.security.js +3 -3
- package/dist/esm/mixins/OxyServices.user.js +22 -22
- package/dist/esm/mixins/OxyServices.utility.js +16 -7
- package/dist/esm/utils/platform.js +12 -0
- package/dist/types/HttpService.d.ts +4 -1
- package/dist/types/OxyServices.base.d.ts +1 -1
- package/dist/types/mixins/OxyServices.analytics.d.ts +1 -1
- package/dist/types/mixins/OxyServices.assets.d.ts +1 -1
- package/dist/types/mixins/OxyServices.auth.d.ts +1 -1
- package/dist/types/mixins/OxyServices.developer.d.ts +1 -1
- package/dist/types/mixins/OxyServices.devices.d.ts +1 -1
- package/dist/types/mixins/OxyServices.features.d.ts +1 -1
- package/dist/types/mixins/OxyServices.fedcm.d.ts +1 -1
- package/dist/types/mixins/OxyServices.karma.d.ts +1 -1
- package/dist/types/mixins/OxyServices.language.d.ts +1 -1
- package/dist/types/mixins/OxyServices.location.d.ts +1 -1
- package/dist/types/mixins/OxyServices.payment.d.ts +1 -1
- package/dist/types/mixins/OxyServices.popup.d.ts +1 -1
- package/dist/types/mixins/OxyServices.privacy.d.ts +1 -1
- package/dist/types/mixins/OxyServices.redirect.d.ts +1 -1
- package/dist/types/mixins/OxyServices.security.d.ts +1 -1
- package/dist/types/mixins/OxyServices.user.d.ts +1 -1
- package/dist/types/mixins/OxyServices.utility.d.ts +7 -6
- package/dist/types/utils/platform.d.ts +8 -0
- package/package.json +1 -1
- package/src/AuthManager.ts +1 -1
- package/src/HttpService.ts +85 -67
- package/src/OxyServices.base.ts +5 -4
- package/src/crypto/keyManager.ts +1 -15
- package/src/crypto/signatureService.ts +2 -17
- package/src/mixins/OxyServices.analytics.ts +2 -2
- package/src/mixins/OxyServices.assets.ts +14 -14
- package/src/mixins/OxyServices.auth.ts +19 -19
- package/src/mixins/OxyServices.developer.ts +6 -6
- package/src/mixins/OxyServices.devices.ts +7 -7
- package/src/mixins/OxyServices.features.ts +23 -23
- package/src/mixins/OxyServices.fedcm.ts +1 -1
- package/src/mixins/OxyServices.karma.ts +6 -6
- package/src/mixins/OxyServices.location.ts +2 -2
- package/src/mixins/OxyServices.payment.ts +6 -6
- package/src/mixins/OxyServices.popup.ts +1 -1
- package/src/mixins/OxyServices.privacy.ts +6 -6
- package/src/mixins/OxyServices.security.ts +3 -3
- package/src/mixins/OxyServices.user.ts +22 -22
- package/src/mixins/OxyServices.utility.ts +18 -8
- package/src/utils/platform.ts +14 -0
|
@@ -12,7 +12,7 @@ function OxyServicesUserMixin(Base) {
|
|
|
12
12
|
*/
|
|
13
13
|
async getProfileByUsername(username) {
|
|
14
14
|
try {
|
|
15
|
-
return await this.makeRequest('GET', `/
|
|
15
|
+
return await this.makeRequest('GET', `/profiles/username/${username}`, undefined, {
|
|
16
16
|
cache: true,
|
|
17
17
|
cacheTTL: 5 * 60 * 1000, // 5 minutes cache for profiles
|
|
18
18
|
});
|
|
@@ -29,7 +29,7 @@ function OxyServicesUserMixin(Base) {
|
|
|
29
29
|
const params = { query, ...pagination };
|
|
30
30
|
const searchParams = (0, apiUtils_1.buildSearchParams)(params);
|
|
31
31
|
const paramsObj = Object.fromEntries(searchParams.entries());
|
|
32
|
-
const response = await this.makeRequest('GET', '/
|
|
32
|
+
const response = await this.makeRequest('GET', '/profiles/search', paramsObj, {
|
|
33
33
|
cache: true,
|
|
34
34
|
cacheTTL: 2 * 60 * 1000, // 2 minutes cache
|
|
35
35
|
});
|
|
@@ -74,7 +74,7 @@ function OxyServicesUserMixin(Base) {
|
|
|
74
74
|
*/
|
|
75
75
|
async getProfileRecommendations() {
|
|
76
76
|
return this.withAuthRetry(async () => {
|
|
77
|
-
return await this.makeRequest('GET', '/
|
|
77
|
+
return await this.makeRequest('GET', '/profiles/recommendations', undefined, { cache: true });
|
|
78
78
|
}, 'getProfileRecommendations');
|
|
79
79
|
}
|
|
80
80
|
/**
|
|
@@ -82,7 +82,7 @@ function OxyServicesUserMixin(Base) {
|
|
|
82
82
|
*/
|
|
83
83
|
async getUserById(userId) {
|
|
84
84
|
try {
|
|
85
|
-
return await this.makeRequest('GET', `/
|
|
85
|
+
return await this.makeRequest('GET', `/users/${userId}`, undefined, {
|
|
86
86
|
cache: true,
|
|
87
87
|
cacheTTL: 5 * 60 * 1000, // 5 minutes cache
|
|
88
88
|
});
|
|
@@ -96,7 +96,7 @@ function OxyServicesUserMixin(Base) {
|
|
|
96
96
|
*/
|
|
97
97
|
async getCurrentUser() {
|
|
98
98
|
return this.withAuthRetry(async () => {
|
|
99
|
-
return await this.makeRequest('GET', '/
|
|
99
|
+
return await this.makeRequest('GET', '/users/me', undefined, {
|
|
100
100
|
cache: true,
|
|
101
101
|
cacheTTL: 1 * 60 * 1000, // 1 minute cache for current user
|
|
102
102
|
});
|
|
@@ -108,7 +108,7 @@ function OxyServicesUserMixin(Base) {
|
|
|
108
108
|
*/
|
|
109
109
|
async updateProfile(updates) {
|
|
110
110
|
try {
|
|
111
|
-
return await this.makeRequest('PUT', '/
|
|
111
|
+
return await this.makeRequest('PUT', '/users/me', updates, { cache: false });
|
|
112
112
|
}
|
|
113
113
|
catch (error) {
|
|
114
114
|
const errorAny = error;
|
|
@@ -134,7 +134,7 @@ function OxyServicesUserMixin(Base) {
|
|
|
134
134
|
async getPrivacySettings(userId) {
|
|
135
135
|
try {
|
|
136
136
|
const id = userId || (await this.getCurrentUser()).id;
|
|
137
|
-
return await this.makeRequest('GET', `/
|
|
137
|
+
return await this.makeRequest('GET', `/privacy/${id}/privacy`, undefined, {
|
|
138
138
|
cache: true,
|
|
139
139
|
cacheTTL: 2 * 60 * 1000, // 2 minutes cache
|
|
140
140
|
});
|
|
@@ -151,7 +151,7 @@ function OxyServicesUserMixin(Base) {
|
|
|
151
151
|
async updatePrivacySettings(settings, userId) {
|
|
152
152
|
try {
|
|
153
153
|
const id = userId || (await this.getCurrentUser()).id;
|
|
154
|
-
return await this.makeRequest('PATCH', `/
|
|
154
|
+
return await this.makeRequest('PATCH', `/privacy/${id}/privacy`, settings, {
|
|
155
155
|
cache: false,
|
|
156
156
|
});
|
|
157
157
|
}
|
|
@@ -164,7 +164,7 @@ function OxyServicesUserMixin(Base) {
|
|
|
164
164
|
*/
|
|
165
165
|
async requestAccountVerification(reason, evidence) {
|
|
166
166
|
try {
|
|
167
|
-
return await this.makeRequest('POST', '/
|
|
167
|
+
return await this.makeRequest('POST', '/users/verify/request', {
|
|
168
168
|
reason,
|
|
169
169
|
evidence,
|
|
170
170
|
}, { cache: false });
|
|
@@ -181,7 +181,7 @@ function OxyServicesUserMixin(Base) {
|
|
|
181
181
|
// Use httpService for blob responses (it handles blob responses automatically)
|
|
182
182
|
const result = await this.getClient().request({
|
|
183
183
|
method: 'GET',
|
|
184
|
-
url: `/
|
|
184
|
+
url: `/users/me/data`,
|
|
185
185
|
params: { format },
|
|
186
186
|
cache: false,
|
|
187
187
|
});
|
|
@@ -198,7 +198,7 @@ function OxyServicesUserMixin(Base) {
|
|
|
198
198
|
*/
|
|
199
199
|
async deleteAccount(password, confirmText) {
|
|
200
200
|
try {
|
|
201
|
-
return await this.makeRequest('DELETE', '/
|
|
201
|
+
return await this.makeRequest('DELETE', '/users/me', {
|
|
202
202
|
password,
|
|
203
203
|
confirmText,
|
|
204
204
|
}, { cache: false });
|
|
@@ -212,7 +212,7 @@ function OxyServicesUserMixin(Base) {
|
|
|
212
212
|
*/
|
|
213
213
|
async followUser(userId) {
|
|
214
214
|
try {
|
|
215
|
-
return await this.makeRequest('POST', `/
|
|
215
|
+
return await this.makeRequest('POST', `/users/${userId}/follow`, undefined, { cache: false });
|
|
216
216
|
}
|
|
217
217
|
catch (error) {
|
|
218
218
|
throw this.handleError(error);
|
|
@@ -223,7 +223,7 @@ function OxyServicesUserMixin(Base) {
|
|
|
223
223
|
*/
|
|
224
224
|
async unfollowUser(userId) {
|
|
225
225
|
try {
|
|
226
|
-
return await this.makeRequest('DELETE', `/
|
|
226
|
+
return await this.makeRequest('DELETE', `/users/${userId}/follow`, undefined, { cache: false });
|
|
227
227
|
}
|
|
228
228
|
catch (error) {
|
|
229
229
|
throw this.handleError(error);
|
|
@@ -234,7 +234,7 @@ function OxyServicesUserMixin(Base) {
|
|
|
234
234
|
*/
|
|
235
235
|
async getFollowStatus(userId) {
|
|
236
236
|
try {
|
|
237
|
-
return await this.makeRequest('GET', `/
|
|
237
|
+
return await this.makeRequest('GET', `/users/${userId}/follow-status`, undefined, {
|
|
238
238
|
cache: true,
|
|
239
239
|
cacheTTL: 1 * 60 * 1000, // 1 minute cache
|
|
240
240
|
});
|
|
@@ -249,7 +249,7 @@ function OxyServicesUserMixin(Base) {
|
|
|
249
249
|
async getUserFollowers(userId, pagination) {
|
|
250
250
|
try {
|
|
251
251
|
const params = (0, apiUtils_1.buildPaginationParams)(pagination || {});
|
|
252
|
-
const response = await this.makeRequest('GET', `/
|
|
252
|
+
const response = await this.makeRequest('GET', `/users/${userId}/followers`, params, {
|
|
253
253
|
cache: true,
|
|
254
254
|
cacheTTL: 2 * 60 * 1000, // 2 minutes cache
|
|
255
255
|
});
|
|
@@ -269,7 +269,7 @@ function OxyServicesUserMixin(Base) {
|
|
|
269
269
|
async getUserFollowing(userId, pagination) {
|
|
270
270
|
try {
|
|
271
271
|
const params = (0, apiUtils_1.buildPaginationParams)(pagination || {});
|
|
272
|
-
const response = await this.makeRequest('GET', `/
|
|
272
|
+
const response = await this.makeRequest('GET', `/users/${userId}/following`, params, {
|
|
273
273
|
cache: true,
|
|
274
274
|
cacheTTL: 2 * 60 * 1000, // 2 minutes cache
|
|
275
275
|
});
|
|
@@ -288,7 +288,7 @@ function OxyServicesUserMixin(Base) {
|
|
|
288
288
|
*/
|
|
289
289
|
async getNotifications() {
|
|
290
290
|
return this.withAuthRetry(async () => {
|
|
291
|
-
return await this.makeRequest('GET', '/
|
|
291
|
+
return await this.makeRequest('GET', '/notifications', undefined, {
|
|
292
292
|
cache: false, // Don't cache notifications - always get fresh data
|
|
293
293
|
});
|
|
294
294
|
}, 'getNotifications');
|
|
@@ -298,7 +298,7 @@ function OxyServicesUserMixin(Base) {
|
|
|
298
298
|
*/
|
|
299
299
|
async getUnreadCount() {
|
|
300
300
|
try {
|
|
301
|
-
const res = await this.makeRequest('GET', '/
|
|
301
|
+
const res = await this.makeRequest('GET', '/notifications/unread-count', undefined, {
|
|
302
302
|
cache: false, // Don't cache unread count - always get fresh data
|
|
303
303
|
});
|
|
304
304
|
return res.count;
|
|
@@ -312,7 +312,7 @@ function OxyServicesUserMixin(Base) {
|
|
|
312
312
|
*/
|
|
313
313
|
async createNotification(data) {
|
|
314
314
|
try {
|
|
315
|
-
return await this.makeRequest('POST', '/
|
|
315
|
+
return await this.makeRequest('POST', '/notifications', data, { cache: false });
|
|
316
316
|
}
|
|
317
317
|
catch (error) {
|
|
318
318
|
throw this.handleError(error);
|
|
@@ -323,7 +323,7 @@ function OxyServicesUserMixin(Base) {
|
|
|
323
323
|
*/
|
|
324
324
|
async markNotificationAsRead(notificationId) {
|
|
325
325
|
try {
|
|
326
|
-
await this.makeRequest('PUT', `/
|
|
326
|
+
await this.makeRequest('PUT', `/notifications/${notificationId}/read`, undefined, { cache: false });
|
|
327
327
|
}
|
|
328
328
|
catch (error) {
|
|
329
329
|
throw this.handleError(error);
|
|
@@ -334,7 +334,7 @@ function OxyServicesUserMixin(Base) {
|
|
|
334
334
|
*/
|
|
335
335
|
async markAllNotificationsAsRead() {
|
|
336
336
|
try {
|
|
337
|
-
await this.makeRequest('PUT', '/
|
|
337
|
+
await this.makeRequest('PUT', '/notifications/read-all', undefined, { cache: false });
|
|
338
338
|
}
|
|
339
339
|
catch (error) {
|
|
340
340
|
throw this.handleError(error);
|
|
@@ -345,7 +345,7 @@ function OxyServicesUserMixin(Base) {
|
|
|
345
345
|
*/
|
|
346
346
|
async deleteNotification(notificationId) {
|
|
347
347
|
try {
|
|
348
|
-
await this.makeRequest('DELETE', `/
|
|
348
|
+
await this.makeRequest('DELETE', `/notifications/${notificationId}`, undefined, { cache: false });
|
|
349
349
|
}
|
|
350
350
|
catch (error) {
|
|
351
351
|
throw this.handleError(error);
|
|
@@ -52,7 +52,7 @@ function OxyServicesUtilityMixin(Base) {
|
|
|
52
52
|
*/
|
|
53
53
|
async fetchLinkMetadata(url) {
|
|
54
54
|
try {
|
|
55
|
-
return await this.makeRequest('GET', '/
|
|
55
|
+
return await this.makeRequest('GET', '/link-metadata', { url }, {
|
|
56
56
|
cache: true,
|
|
57
57
|
cacheTTL: mixinHelpers_1.CACHE_TIMES.EXTRA_LONG,
|
|
58
58
|
});
|
|
@@ -73,19 +73,19 @@ function OxyServicesUtilityMixin(Base) {
|
|
|
73
73
|
*
|
|
74
74
|
* const oxy = new OxyServices({ baseURL: 'https://api.oxy.so' });
|
|
75
75
|
*
|
|
76
|
-
* // Protect all routes under /
|
|
77
|
-
* app.use('/
|
|
76
|
+
* // Protect all routes under /protected
|
|
77
|
+
* app.use('/protected', oxy.auth());
|
|
78
78
|
*
|
|
79
79
|
* // Access user in route handler
|
|
80
|
-
* app.get('/
|
|
80
|
+
* app.get('/protected/me', (req, res) => {
|
|
81
81
|
* res.json({ userId: req.userId, user: req.user });
|
|
82
82
|
* });
|
|
83
83
|
*
|
|
84
84
|
* // Load full user profile from API
|
|
85
|
-
* app.use('/
|
|
85
|
+
* app.use('/admin', oxy.auth({ loadUser: true }));
|
|
86
86
|
*
|
|
87
87
|
* // Optional auth - attach user if present, don't block if absent
|
|
88
|
-
* app.use('/
|
|
88
|
+
* app.use('/public', oxy.auth({ optional: true }));
|
|
89
89
|
* ```
|
|
90
90
|
*
|
|
91
91
|
* @param options Optional configuration
|
|
@@ -187,7 +187,16 @@ function OxyServicesUtilityMixin(Base) {
|
|
|
187
187
|
throw new Error('Invalid signature');
|
|
188
188
|
}
|
|
189
189
|
}
|
|
190
|
-
catch {
|
|
190
|
+
catch (verifyError) {
|
|
191
|
+
const isSignatureError = verifyError instanceof Error &&
|
|
192
|
+
(verifyError.message === 'Invalid signature' || verifyError.message === 'Invalid token structure');
|
|
193
|
+
if (!isSignatureError) {
|
|
194
|
+
console.error('[oxy.auth] Unexpected error during service token verification:', verifyError);
|
|
195
|
+
const error = { message: 'Internal authentication error', code: 'AUTH_INTERNAL_ERROR', status: 500 };
|
|
196
|
+
if (onError)
|
|
197
|
+
return onError(error);
|
|
198
|
+
return res.status(500).json(error);
|
|
199
|
+
}
|
|
191
200
|
if (optional) {
|
|
192
201
|
req.userId = null;
|
|
193
202
|
req.user = null;
|
|
@@ -12,6 +12,8 @@ exports.isWeb = isWeb;
|
|
|
12
12
|
exports.isNative = isNative;
|
|
13
13
|
exports.isIOS = isIOS;
|
|
14
14
|
exports.isAndroid = isAndroid;
|
|
15
|
+
exports.isReactNative = isReactNative;
|
|
16
|
+
exports.isNodeJS = isNodeJS;
|
|
15
17
|
exports.setPlatformOS = setPlatformOS;
|
|
16
18
|
/**
|
|
17
19
|
* Detect the current platform without importing react-native
|
|
@@ -82,6 +84,18 @@ function isIOS() {
|
|
|
82
84
|
function isAndroid() {
|
|
83
85
|
return getPlatformOS() === 'android';
|
|
84
86
|
}
|
|
87
|
+
/**
|
|
88
|
+
* Check if running in React Native
|
|
89
|
+
*/
|
|
90
|
+
function isReactNative() {
|
|
91
|
+
return typeof navigator !== 'undefined' && navigator.product === 'ReactNative';
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Check if running in Node.js
|
|
95
|
+
*/
|
|
96
|
+
function isNodeJS() {
|
|
97
|
+
return typeof process !== 'undefined' && process.versions != null && process.versions.node != null;
|
|
98
|
+
}
|
|
85
99
|
/**
|
|
86
100
|
* Set the platform OS explicitly
|
|
87
101
|
* Called by React Native entry point to register the platform
|
package/dist/esm/AuthManager.js
CHANGED
|
@@ -243,7 +243,7 @@ export class AuthManager {
|
|
|
243
243
|
// Use session-based token endpoint which handles auto-refresh server-side
|
|
244
244
|
const response = await httpService.request({
|
|
245
245
|
method: 'GET',
|
|
246
|
-
url: `/
|
|
246
|
+
url: `/session/token/${sessionId}`,
|
|
247
247
|
cache: false,
|
|
248
248
|
retry: false,
|
|
249
249
|
});
|
package/dist/esm/HttpService.js
CHANGED
|
@@ -25,7 +25,9 @@ import { isNative, getPlatformOS } from './utils/platform';
|
|
|
25
25
|
*/
|
|
26
26
|
const isNativeApp = isNative();
|
|
27
27
|
/**
|
|
28
|
-
* Token store for authentication (
|
|
28
|
+
* Token store for authentication (instance-based)
|
|
29
|
+
* Each HttpService gets its own TokenStore to prevent conflicts
|
|
30
|
+
* when multiple OxyServices instances coexist server-side.
|
|
29
31
|
*/
|
|
30
32
|
class TokenStore {
|
|
31
33
|
constructor() {
|
|
@@ -34,12 +36,6 @@ class TokenStore {
|
|
|
34
36
|
this.csrfToken = null;
|
|
35
37
|
this.csrfTokenFetchPromise = null;
|
|
36
38
|
}
|
|
37
|
-
static getInstance() {
|
|
38
|
-
if (!TokenStore.instance) {
|
|
39
|
-
TokenStore.instance = new TokenStore();
|
|
40
|
-
}
|
|
41
|
-
return TokenStore.instance;
|
|
42
|
-
}
|
|
43
39
|
setTokens(accessToken, refreshToken = '') {
|
|
44
40
|
this.accessToken = accessToken;
|
|
45
41
|
this.refreshToken = refreshToken;
|
|
@@ -83,6 +79,7 @@ class TokenStore {
|
|
|
83
79
|
export class HttpService {
|
|
84
80
|
constructor(config) {
|
|
85
81
|
this.tokenRefreshPromise = null;
|
|
82
|
+
this.tokenRefreshCooldownUntil = 0;
|
|
86
83
|
this._onTokenRefreshed = null;
|
|
87
84
|
// Performance monitoring
|
|
88
85
|
this.requestMetrics = {
|
|
@@ -95,7 +92,7 @@ export class HttpService {
|
|
|
95
92
|
};
|
|
96
93
|
this.config = config;
|
|
97
94
|
this.baseURL = config.baseURL;
|
|
98
|
-
this.tokenStore = TokenStore
|
|
95
|
+
this.tokenStore = new TokenStore();
|
|
99
96
|
this.logger = new SimpleLogger(config.enableLogging || false, config.logLevel || 'error', 'HttpService');
|
|
100
97
|
// Initialize performance infrastructure
|
|
101
98
|
this.cache = new TTLCache(config.cacheTTL || 5 * 60 * 1000);
|
|
@@ -247,6 +244,20 @@ export class HttpService {
|
|
|
247
244
|
// Refresh failed or no token — clear tokens
|
|
248
245
|
this.tokenStore.clearTokens();
|
|
249
246
|
}
|
|
247
|
+
// On 403 with CSRF error, clear cached token and retry once
|
|
248
|
+
if (response.status === 403 && !config._isCsrfRetry) {
|
|
249
|
+
try {
|
|
250
|
+
const clonedResponse = response.clone();
|
|
251
|
+
const errBody = await clonedResponse.json();
|
|
252
|
+
if (errBody?.code === 'CSRF_TOKEN_INVALID' || errBody?.code === 'CSRF_TOKEN_MISSING') {
|
|
253
|
+
this.tokenStore.clearCsrfToken();
|
|
254
|
+
return this.request({ ...config, _isCsrfRetry: true, retry: false });
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
catch {
|
|
258
|
+
// Failed to parse error body — not a CSRF error
|
|
259
|
+
}
|
|
260
|
+
}
|
|
250
261
|
// Try to parse error response (handle empty/malformed JSON)
|
|
251
262
|
let errorMessage = `HTTP ${response.status}: ${response.statusText}`;
|
|
252
263
|
const contentType = response.headers.get('content-type');
|
|
@@ -403,52 +414,57 @@ export class HttpService {
|
|
|
403
414
|
return existingPromise;
|
|
404
415
|
}
|
|
405
416
|
const fetchPromise = (async () => {
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
417
|
+
const maxAttempts = 2;
|
|
418
|
+
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
419
|
+
try {
|
|
420
|
+
if (isDev())
|
|
421
|
+
console.log('[HttpService] Fetching CSRF token from:', `${this.baseURL}/csrf-token`, `(attempt ${attempt})`);
|
|
422
|
+
// Use AbortController for timeout (more compatible than AbortSignal.timeout)
|
|
423
|
+
const controller = new AbortController();
|
|
424
|
+
const timeoutId = setTimeout(() => controller.abort(), 5000);
|
|
425
|
+
const response = await fetch(`${this.baseURL}/csrf-token`, {
|
|
426
|
+
method: 'GET',
|
|
427
|
+
headers: { 'Accept': 'application/json' },
|
|
428
|
+
credentials: 'include', // Required to receive and send cookies
|
|
429
|
+
signal: controller.signal,
|
|
430
|
+
});
|
|
431
|
+
clearTimeout(timeoutId);
|
|
432
|
+
if (isDev())
|
|
433
|
+
console.log('[HttpService] CSRF fetch response:', response.status, response.ok);
|
|
434
|
+
if (response.ok) {
|
|
435
|
+
const data = await response.json();
|
|
436
|
+
if (isDev())
|
|
437
|
+
console.log('[HttpService] CSRF response data:', data);
|
|
438
|
+
const token = data.csrfToken || null;
|
|
439
|
+
this.tokenStore.setCsrfToken(token);
|
|
440
|
+
this.logger.debug('CSRF token fetched');
|
|
441
|
+
return token;
|
|
442
|
+
}
|
|
443
|
+
// Also check response header for CSRF token
|
|
444
|
+
const headerToken = response.headers.get('X-CSRF-Token');
|
|
445
|
+
if (headerToken) {
|
|
446
|
+
this.tokenStore.setCsrfToken(headerToken);
|
|
447
|
+
this.logger.debug('CSRF token from header');
|
|
448
|
+
return headerToken;
|
|
449
|
+
}
|
|
423
450
|
if (isDev())
|
|
424
|
-
console.log('[HttpService] CSRF
|
|
425
|
-
|
|
426
|
-
this.tokenStore.setCsrfToken(token);
|
|
427
|
-
this.logger.debug('CSRF token fetched');
|
|
428
|
-
return token;
|
|
451
|
+
console.log('[HttpService] CSRF fetch failed with status:', response.status);
|
|
452
|
+
this.logger.warn('Failed to fetch CSRF token:', response.status);
|
|
429
453
|
}
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
this.
|
|
434
|
-
|
|
435
|
-
|
|
454
|
+
catch (error) {
|
|
455
|
+
if (isDev())
|
|
456
|
+
console.log('[HttpService] CSRF fetch error:', error);
|
|
457
|
+
this.logger.warn('CSRF token fetch error:', error);
|
|
458
|
+
}
|
|
459
|
+
// Wait before retry (500ms)
|
|
460
|
+
if (attempt < maxAttempts) {
|
|
461
|
+
await new Promise(resolve => setTimeout(resolve, 500));
|
|
436
462
|
}
|
|
437
|
-
if (isDev())
|
|
438
|
-
console.log('[HttpService] CSRF fetch failed with status:', response.status);
|
|
439
|
-
this.logger.warn('Failed to fetch CSRF token:', response.status);
|
|
440
|
-
return null;
|
|
441
|
-
}
|
|
442
|
-
catch (error) {
|
|
443
|
-
if (isDev())
|
|
444
|
-
console.log('[HttpService] CSRF fetch error:', error);
|
|
445
|
-
this.logger.warn('CSRF token fetch error:', error);
|
|
446
|
-
return null;
|
|
447
|
-
}
|
|
448
|
-
finally {
|
|
449
|
-
this.tokenStore.setCsrfTokenFetchPromise(null);
|
|
450
463
|
}
|
|
451
|
-
|
|
464
|
+
return null;
|
|
465
|
+
})().finally(() => {
|
|
466
|
+
this.tokenStore.setCsrfTokenFetchPromise(null);
|
|
467
|
+
});
|
|
452
468
|
this.tokenStore.setCsrfTokenFetchPromise(fetchPromise);
|
|
453
469
|
return fetchPromise;
|
|
454
470
|
}
|
|
@@ -465,18 +481,25 @@ export class HttpService {
|
|
|
465
481
|
const currentTime = Math.floor(Date.now() / 1000);
|
|
466
482
|
// If token expires in less than 60 seconds, refresh it
|
|
467
483
|
if (decoded.exp && decoded.exp - currentTime < 60 && decoded.sessionId) {
|
|
468
|
-
//
|
|
469
|
-
if (
|
|
470
|
-
|
|
484
|
+
// Skip if we recently failed a refresh (5s cooldown to prevent storms)
|
|
485
|
+
if (Date.now() < this.tokenRefreshCooldownUntil) {
|
|
486
|
+
return `Bearer ${accessToken}`;
|
|
471
487
|
}
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
488
|
+
// Deduplicate concurrent refresh attempts. The promise is shared
|
|
489
|
+
// across all concurrent callers and cleared only after it settles,
|
|
490
|
+
// so every awaiter receives the same result.
|
|
491
|
+
if (!this.tokenRefreshPromise) {
|
|
492
|
+
this.tokenRefreshPromise = this._refreshTokenFromSession(decoded.sessionId)
|
|
493
|
+
.then((result) => {
|
|
494
|
+
if (!result)
|
|
495
|
+
this.tokenRefreshCooldownUntil = Date.now() + 5000;
|
|
475
496
|
return result;
|
|
497
|
+
})
|
|
498
|
+
.finally(() => { this.tokenRefreshPromise = null; });
|
|
476
499
|
}
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
500
|
+
const result = await this.tokenRefreshPromise;
|
|
501
|
+
if (result)
|
|
502
|
+
return result;
|
|
480
503
|
}
|
|
481
504
|
return `Bearer ${accessToken}`;
|
|
482
505
|
}
|
|
@@ -487,7 +510,7 @@ export class HttpService {
|
|
|
487
510
|
}
|
|
488
511
|
async _refreshTokenFromSession(sessionId) {
|
|
489
512
|
try {
|
|
490
|
-
const refreshUrl = `${this.baseURL}/
|
|
513
|
+
const refreshUrl = `${this.baseURL}/session/token/${sessionId}`;
|
|
491
514
|
const response = await fetch(refreshUrl, {
|
|
492
515
|
method: 'GET',
|
|
493
516
|
headers: { 'Accept': 'application/json' },
|
|
@@ -640,14 +663,9 @@ export class HttpService {
|
|
|
640
663
|
getMetrics() {
|
|
641
664
|
return { ...this.requestMetrics };
|
|
642
665
|
}
|
|
643
|
-
// Test-only utility
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
}
|
|
648
|
-
catch (error) {
|
|
649
|
-
// Silently fail in test cleanup - this is expected behavior
|
|
650
|
-
// TokenStore might not be initialized in some test scenarios
|
|
651
|
-
}
|
|
666
|
+
// Test-only utility — clears tokens on this instance
|
|
667
|
+
__resetTokensForTests() {
|
|
668
|
+
this.tokenStore.clearTokens();
|
|
669
|
+
this.tokenStore.clearCsrfToken();
|
|
652
670
|
}
|
|
653
671
|
}
|
|
@@ -23,9 +23,10 @@ export class OxyServicesBase {
|
|
|
23
23
|
// Initialize unified HTTP service (handles auth, caching, deduplication, queuing, retry)
|
|
24
24
|
this.httpService = new HttpService(config);
|
|
25
25
|
}
|
|
26
|
-
// Test-only utility to reset
|
|
27
|
-
|
|
28
|
-
|
|
26
|
+
// Test-only utility to reset tokens on this instance between jest tests
|
|
27
|
+
// Note: tokens are now per-instance, so create new instances in tests for isolation
|
|
28
|
+
__resetTokensForTests() {
|
|
29
|
+
this.httpService.__resetTokensForTests();
|
|
29
30
|
}
|
|
30
31
|
/**
|
|
31
32
|
* Make a request with all performance optimizations
|
|
@@ -231,7 +232,7 @@ export class OxyServicesBase {
|
|
|
231
232
|
return false;
|
|
232
233
|
}
|
|
233
234
|
try {
|
|
234
|
-
const res = await this.makeRequest('GET', '/
|
|
235
|
+
const res = await this.makeRequest('GET', '/auth/validate', undefined, {
|
|
235
236
|
cache: false,
|
|
236
237
|
retry: false,
|
|
237
238
|
});
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* Private keys are stored securely using expo-secure-store and never leave the device.
|
|
6
6
|
*/
|
|
7
7
|
import { ec as EC } from 'elliptic';
|
|
8
|
-
import { isWeb, isIOS, isAndroid } from '../utils/platform';
|
|
8
|
+
import { isWeb, isIOS, isAndroid, isReactNative, isNodeJS } from '../utils/platform';
|
|
9
9
|
import { logger } from '../utils/loggerUtils';
|
|
10
10
|
import { isDev } from '../shared/utils/debugUtils';
|
|
11
11
|
// Lazy imports for React Native specific modules
|
|
@@ -56,18 +56,6 @@ async function initSecureStore() {
|
|
|
56
56
|
}
|
|
57
57
|
return SecureStore;
|
|
58
58
|
}
|
|
59
|
-
/**
|
|
60
|
-
* Check if we're in a React Native environment
|
|
61
|
-
*/
|
|
62
|
-
function isReactNative() {
|
|
63
|
-
return typeof navigator !== 'undefined' && navigator.product === 'ReactNative';
|
|
64
|
-
}
|
|
65
|
-
/**
|
|
66
|
-
* Check if we're in a Node.js environment
|
|
67
|
-
*/
|
|
68
|
-
function isNodeJS() {
|
|
69
|
-
return typeof process !== 'undefined' && process.versions != null && process.versions.node != null;
|
|
70
|
-
}
|
|
71
59
|
/**
|
|
72
60
|
* Check if we're on web platform
|
|
73
61
|
* Identity storage is only available on native platforms (iOS/Android)
|
|
@@ -6,21 +6,10 @@
|
|
|
6
6
|
*/
|
|
7
7
|
import { ec as EC } from 'elliptic';
|
|
8
8
|
import { KeyManager } from './keyManager';
|
|
9
|
+
import { isReactNative, isNodeJS } from '../utils/platform';
|
|
9
10
|
// Lazy import for expo-crypto
|
|
10
11
|
let ExpoCrypto = null;
|
|
11
12
|
const ec = new EC('secp256k1');
|
|
12
|
-
/**
|
|
13
|
-
* Check if we're in a React Native environment
|
|
14
|
-
*/
|
|
15
|
-
function isReactNative() {
|
|
16
|
-
return typeof navigator !== 'undefined' && navigator.product === 'ReactNative';
|
|
17
|
-
}
|
|
18
|
-
/**
|
|
19
|
-
* Check if we're in a Node.js environment
|
|
20
|
-
*/
|
|
21
|
-
function isNodeJS() {
|
|
22
|
-
return typeof process !== 'undefined' && process.versions != null && process.versions.node != null;
|
|
23
|
-
}
|
|
24
13
|
/**
|
|
25
14
|
* Initialize expo-crypto module
|
|
26
15
|
*/
|
|
@@ -44,9 +33,7 @@ async function sha256(message) {
|
|
|
44
33
|
// In Node.js, use Node's crypto module
|
|
45
34
|
if (isNodeJS()) {
|
|
46
35
|
try {
|
|
47
|
-
|
|
48
|
-
const getCrypto = new Function('return require("crypto")');
|
|
49
|
-
const nodeCrypto = getCrypto();
|
|
36
|
+
const nodeCrypto = await import('crypto');
|
|
50
37
|
return nodeCrypto.createHash('sha256').update(message).digest('hex');
|
|
51
38
|
}
|
|
52
39
|
catch {
|
|
@@ -11,7 +11,7 @@ export function OxyServicesAnalyticsMixin(Base) {
|
|
|
11
11
|
*/
|
|
12
12
|
async trackEvent(eventName, properties) {
|
|
13
13
|
try {
|
|
14
|
-
await this.makeRequest('POST', '/
|
|
14
|
+
await this.makeRequest('POST', '/analytics/events', {
|
|
15
15
|
event: eventName,
|
|
16
16
|
properties
|
|
17
17
|
}, { cache: false, retry: false }); // Don't retry analytics events
|
|
@@ -33,7 +33,7 @@ export function OxyServicesAnalyticsMixin(Base) {
|
|
|
33
33
|
params.startDate = startDate;
|
|
34
34
|
if (endDate)
|
|
35
35
|
params.endDate = endDate;
|
|
36
|
-
return await this.makeRequest('GET', '/
|
|
36
|
+
return await this.makeRequest('GET', '/analytics', params, {
|
|
37
37
|
cache: true,
|
|
38
38
|
cacheTTL: CACHE_TIMES.LONG,
|
|
39
39
|
});
|