@oxyhq/core 1.5.0 → 1.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (92) hide show
  1. package/dist/cjs/AuthManager.js +14 -1
  2. package/dist/cjs/HttpService.js +87 -69
  3. package/dist/cjs/OxyServices.base.js +5 -4
  4. package/dist/cjs/crypto/keyManager.js +1 -13
  5. package/dist/cjs/crypto/signatureService.js +7 -20
  6. package/dist/cjs/index.js +9 -1
  7. package/dist/cjs/mixins/OxyServices.analytics.js +2 -2
  8. package/dist/cjs/mixins/OxyServices.assets.js +14 -14
  9. package/dist/cjs/mixins/OxyServices.auth.js +19 -19
  10. package/dist/cjs/mixins/OxyServices.developer.js +6 -6
  11. package/dist/cjs/mixins/OxyServices.devices.js +7 -7
  12. package/dist/cjs/mixins/OxyServices.features.js +23 -23
  13. package/dist/cjs/mixins/OxyServices.fedcm.js +1 -1
  14. package/dist/cjs/mixins/OxyServices.karma.js +6 -6
  15. package/dist/cjs/mixins/OxyServices.location.js +2 -2
  16. package/dist/cjs/mixins/OxyServices.payment.js +6 -6
  17. package/dist/cjs/mixins/OxyServices.popup.js +1 -1
  18. package/dist/cjs/mixins/OxyServices.privacy.js +6 -6
  19. package/dist/cjs/mixins/OxyServices.security.js +3 -3
  20. package/dist/cjs/mixins/OxyServices.user.js +22 -22
  21. package/dist/cjs/mixins/OxyServices.utility.js +39 -10
  22. package/dist/cjs/utils/authHelpers.js +114 -0
  23. package/dist/cjs/utils/platform.js +14 -0
  24. package/dist/esm/AuthManager.js +14 -1
  25. package/dist/esm/HttpService.js +87 -69
  26. package/dist/esm/OxyServices.base.js +5 -4
  27. package/dist/esm/crypto/keyManager.js +1 -13
  28. package/dist/esm/crypto/signatureService.js +2 -15
  29. package/dist/esm/index.js +2 -0
  30. package/dist/esm/mixins/OxyServices.analytics.js +2 -2
  31. package/dist/esm/mixins/OxyServices.assets.js +14 -14
  32. package/dist/esm/mixins/OxyServices.auth.js +19 -19
  33. package/dist/esm/mixins/OxyServices.developer.js +6 -6
  34. package/dist/esm/mixins/OxyServices.devices.js +7 -7
  35. package/dist/esm/mixins/OxyServices.features.js +23 -23
  36. package/dist/esm/mixins/OxyServices.fedcm.js +1 -1
  37. package/dist/esm/mixins/OxyServices.karma.js +6 -6
  38. package/dist/esm/mixins/OxyServices.location.js +2 -2
  39. package/dist/esm/mixins/OxyServices.payment.js +6 -6
  40. package/dist/esm/mixins/OxyServices.popup.js +1 -1
  41. package/dist/esm/mixins/OxyServices.privacy.js +6 -6
  42. package/dist/esm/mixins/OxyServices.security.js +3 -3
  43. package/dist/esm/mixins/OxyServices.user.js +22 -22
  44. package/dist/esm/mixins/OxyServices.utility.js +39 -10
  45. package/dist/esm/utils/authHelpers.js +105 -0
  46. package/dist/esm/utils/platform.js +12 -0
  47. package/dist/types/HttpService.d.ts +4 -1
  48. package/dist/types/OxyServices.base.d.ts +1 -1
  49. package/dist/types/index.d.ts +2 -0
  50. package/dist/types/mixins/OxyServices.analytics.d.ts +1 -1
  51. package/dist/types/mixins/OxyServices.assets.d.ts +1 -1
  52. package/dist/types/mixins/OxyServices.auth.d.ts +1 -1
  53. package/dist/types/mixins/OxyServices.developer.d.ts +1 -1
  54. package/dist/types/mixins/OxyServices.devices.d.ts +1 -1
  55. package/dist/types/mixins/OxyServices.features.d.ts +1 -1
  56. package/dist/types/mixins/OxyServices.fedcm.d.ts +1 -1
  57. package/dist/types/mixins/OxyServices.karma.d.ts +1 -1
  58. package/dist/types/mixins/OxyServices.language.d.ts +1 -1
  59. package/dist/types/mixins/OxyServices.location.d.ts +1 -1
  60. package/dist/types/mixins/OxyServices.payment.d.ts +1 -1
  61. package/dist/types/mixins/OxyServices.popup.d.ts +1 -1
  62. package/dist/types/mixins/OxyServices.privacy.d.ts +1 -1
  63. package/dist/types/mixins/OxyServices.redirect.d.ts +1 -1
  64. package/dist/types/mixins/OxyServices.security.d.ts +1 -1
  65. package/dist/types/mixins/OxyServices.user.d.ts +1 -1
  66. package/dist/types/mixins/OxyServices.utility.d.ts +20 -6
  67. package/dist/types/utils/authHelpers.d.ts +57 -0
  68. package/dist/types/utils/platform.d.ts +8 -0
  69. package/package.json +1 -1
  70. package/src/AuthManager.ts +14 -1
  71. package/src/HttpService.ts +85 -67
  72. package/src/OxyServices.base.ts +5 -4
  73. package/src/crypto/keyManager.ts +1 -15
  74. package/src/crypto/signatureService.ts +2 -17
  75. package/src/index.ts +11 -0
  76. package/src/mixins/OxyServices.analytics.ts +2 -2
  77. package/src/mixins/OxyServices.assets.ts +14 -14
  78. package/src/mixins/OxyServices.auth.ts +19 -19
  79. package/src/mixins/OxyServices.developer.ts +6 -6
  80. package/src/mixins/OxyServices.devices.ts +7 -7
  81. package/src/mixins/OxyServices.features.ts +23 -23
  82. package/src/mixins/OxyServices.fedcm.ts +1 -1
  83. package/src/mixins/OxyServices.karma.ts +6 -6
  84. package/src/mixins/OxyServices.location.ts +2 -2
  85. package/src/mixins/OxyServices.payment.ts +6 -6
  86. package/src/mixins/OxyServices.popup.ts +1 -1
  87. package/src/mixins/OxyServices.privacy.ts +6 -6
  88. package/src/mixins/OxyServices.security.ts +3 -3
  89. package/src/mixins/OxyServices.user.ts +22 -22
  90. package/src/mixins/OxyServices.utility.ts +41 -11
  91. package/src/utils/authHelpers.ts +140 -0
  92. package/src/utils/platform.ts +14 -0
@@ -44,7 +44,7 @@ function OxyServicesPrivacyMixin(Base) {
44
44
  */
45
45
  async getBlockedUsers() {
46
46
  try {
47
- return await this.makeRequest('GET', '/api/privacy/blocked', undefined, {
47
+ return await this.makeRequest('GET', '/privacy/blocked', undefined, {
48
48
  cache: true,
49
49
  cacheTTL: 1 * 60 * 1000, // 1 minute cache
50
50
  });
@@ -63,7 +63,7 @@ function OxyServicesPrivacyMixin(Base) {
63
63
  if (!userId) {
64
64
  throw new Error('User ID is required');
65
65
  }
66
- return await this.makeRequest('POST', `/api/privacy/blocked/${userId}`, undefined, {
66
+ return await this.makeRequest('POST', `/privacy/blocked/${userId}`, undefined, {
67
67
  cache: false,
68
68
  });
69
69
  }
@@ -81,7 +81,7 @@ function OxyServicesPrivacyMixin(Base) {
81
81
  if (!userId) {
82
82
  throw new Error('User ID is required');
83
83
  }
84
- return await this.makeRequest('DELETE', `/api/privacy/blocked/${userId}`, undefined, {
84
+ return await this.makeRequest('DELETE', `/privacy/blocked/${userId}`, undefined, {
85
85
  cache: false,
86
86
  });
87
87
  }
@@ -106,7 +106,7 @@ function OxyServicesPrivacyMixin(Base) {
106
106
  */
107
107
  async getRestrictedUsers() {
108
108
  try {
109
- return await this.makeRequest('GET', '/api/privacy/restricted', undefined, {
109
+ return await this.makeRequest('GET', '/privacy/restricted', undefined, {
110
110
  cache: true,
111
111
  cacheTTL: 1 * 60 * 1000, // 1 minute cache
112
112
  });
@@ -125,7 +125,7 @@ function OxyServicesPrivacyMixin(Base) {
125
125
  if (!userId) {
126
126
  throw new Error('User ID is required');
127
127
  }
128
- return await this.makeRequest('POST', `/api/privacy/restricted/${userId}`, undefined, {
128
+ return await this.makeRequest('POST', `/privacy/restricted/${userId}`, undefined, {
129
129
  cache: false,
130
130
  });
131
131
  }
@@ -143,7 +143,7 @@ function OxyServicesPrivacyMixin(Base) {
143
143
  if (!userId) {
144
144
  throw new Error('User ID is required');
145
145
  }
146
- return await this.makeRequest('DELETE', `/api/privacy/restricted/${userId}`, undefined, {
146
+ return await this.makeRequest('DELETE', `/privacy/restricted/${userId}`, undefined, {
147
147
  cache: false,
148
148
  });
149
149
  }
@@ -23,7 +23,7 @@ function OxyServicesSecurityMixin(Base) {
23
23
  params.offset = offset;
24
24
  if (eventType)
25
25
  params.eventType = eventType;
26
- const response = await this.makeRequest('GET', '/api/security/activity', params, { cache: false });
26
+ const response = await this.makeRequest('GET', '/security/activity', params, { cache: false });
27
27
  return response;
28
28
  }
29
29
  catch (error) {
@@ -51,7 +51,7 @@ function OxyServicesSecurityMixin(Base) {
51
51
  */
52
52
  async logPrivateKeyExported(deviceId) {
53
53
  try {
54
- await this.makeRequest('POST', '/api/security/activity/private-key-exported', { deviceId }, { cache: false });
54
+ await this.makeRequest('POST', '/security/activity/private-key-exported', { deviceId }, { cache: false });
55
55
  }
56
56
  catch (error) {
57
57
  // Don't throw - logging failures shouldn't break user flow
@@ -68,7 +68,7 @@ function OxyServicesSecurityMixin(Base) {
68
68
  */
69
69
  async logBackupCreated(deviceId) {
70
70
  try {
71
- await this.makeRequest('POST', '/api/security/activity/backup-created', { deviceId }, { cache: false });
71
+ await this.makeRequest('POST', '/security/activity/backup-created', { deviceId }, { cache: false });
72
72
  }
73
73
  catch (error) {
74
74
  // Don't throw - logging failures shouldn't break user flow
@@ -12,7 +12,7 @@ function OxyServicesUserMixin(Base) {
12
12
  */
13
13
  async getProfileByUsername(username) {
14
14
  try {
15
- return await this.makeRequest('GET', `/api/profiles/username/${username}`, undefined, {
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', '/api/profiles/search', paramsObj, {
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', '/api/profiles/recommendations', undefined, { cache: true });
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', `/api/users/${userId}`, undefined, {
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', '/api/users/me', undefined, {
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', '/api/users/me', updates, { cache: false });
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', `/api/privacy/${id}/privacy`, undefined, {
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', `/api/privacy/${id}/privacy`, settings, {
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', '/api/users/verify/request', {
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: `/api/users/me/data`,
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', '/api/users/me', {
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', `/api/users/${userId}/follow`, undefined, { cache: false });
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', `/api/users/${userId}/follow`, undefined, { cache: false });
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', `/api/users/${userId}/follow-status`, undefined, {
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', `/api/users/${userId}/followers`, params, {
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', `/api/users/${userId}/following`, params, {
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', '/api/notifications', undefined, {
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', '/api/notifications/unread-count', undefined, {
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', '/api/notifications', data, { cache: false });
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', `/api/notifications/${notificationId}/read`, undefined, { cache: false });
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', '/api/notifications/read-all', undefined, { cache: false });
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', `/api/notifications/${notificationId}`, undefined, { cache: false });
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', '/api/link-metadata', { url }, {
55
+ return await this.makeRequest('GET', '/link-metadata', { url }, {
56
56
  cache: true,
57
57
  cacheTTL: mixinHelpers_1.CACHE_TIMES.EXTRA_LONG,
58
58
  });
@@ -67,25 +67,36 @@ function OxyServicesUtilityMixin(Base) {
67
67
  * Validates JWT tokens against the Oxy API and attaches user data to requests.
68
68
  * Uses server-side session validation for security (not just JWT decode).
69
69
  *
70
+ * **Design note — jwtDecode vs jwt.verify:**
71
+ * This middleware intentionally uses `jwtDecode()` (decode-only, no signature
72
+ * verification) for user tokens. This is by design, NOT a security gap:
73
+ * - Third-party apps using `oxy.auth()` don't have the Oxy JWT secret
74
+ * - Security comes from API-based session validation (`validateSession()`)
75
+ * which checks the session server-side on every request
76
+ * - Service tokens (type: 'service') DO use cryptographic HMAC verification
77
+ * via the `jwtSecret` option, since they are stateless
78
+ * - The backend's own `authMiddleware` uses `jwt.verify()` because it has
79
+ * direct access to `ACCESS_TOKEN_SECRET`
80
+ *
70
81
  * @example
71
82
  * ```typescript
72
83
  * import { OxyServices } from '@oxyhq/core';
73
84
  *
74
85
  * const oxy = new OxyServices({ baseURL: 'https://api.oxy.so' });
75
86
  *
76
- * // Protect all routes under /api/protected
77
- * app.use('/api/protected', oxy.auth());
87
+ * // Protect all routes under /protected
88
+ * app.use('/protected', oxy.auth());
78
89
  *
79
90
  * // Access user in route handler
80
- * app.get('/api/protected/me', (req, res) => {
91
+ * app.get('/protected/me', (req, res) => {
81
92
  * res.json({ userId: req.userId, user: req.user });
82
93
  * });
83
94
  *
84
95
  * // Load full user profile from API
85
- * app.use('/api/admin', oxy.auth({ loadUser: true }));
96
+ * app.use('/admin', oxy.auth({ loadUser: true }));
86
97
  *
87
98
  * // Optional auth - attach user if present, don't block if absent
88
- * app.use('/api/public', oxy.auth({ optional: true }));
99
+ * app.use('/public', oxy.auth({ optional: true }));
89
100
  * ```
90
101
  *
91
102
  * @param options Optional configuration
@@ -119,6 +130,7 @@ function OxyServicesUtilityMixin(Base) {
119
130
  return next();
120
131
  }
121
132
  const error = {
133
+ error: 'MISSING_TOKEN',
122
134
  message: 'Access token required',
123
135
  code: 'MISSING_TOKEN',
124
136
  status: 401
@@ -139,6 +151,7 @@ function OxyServicesUtilityMixin(Base) {
139
151
  return next();
140
152
  }
141
153
  const error = {
154
+ error: 'INVALID_TOKEN_FORMAT',
142
155
  message: 'Invalid token format',
143
156
  code: 'INVALID_TOKEN_FORMAT',
144
157
  status: 401
@@ -158,6 +171,7 @@ function OxyServicesUtilityMixin(Base) {
158
171
  return next();
159
172
  }
160
173
  const error = {
174
+ error: 'SERVICE_TOKEN_NOT_CONFIGURED',
161
175
  message: 'Service token verification not configured',
162
176
  code: 'SERVICE_TOKEN_NOT_CONFIGURED',
163
177
  status: 403
@@ -187,13 +201,22 @@ function OxyServicesUtilityMixin(Base) {
187
201
  throw new Error('Invalid signature');
188
202
  }
189
203
  }
190
- catch {
204
+ catch (verifyError) {
205
+ const isSignatureError = verifyError instanceof Error &&
206
+ (verifyError.message === 'Invalid signature' || verifyError.message === 'Invalid token structure');
207
+ if (!isSignatureError) {
208
+ console.error('[oxy.auth] Unexpected error during service token verification:', verifyError);
209
+ const error = { error: 'AUTH_INTERNAL_ERROR', message: 'Internal authentication error', code: 'AUTH_INTERNAL_ERROR', status: 500 };
210
+ if (onError)
211
+ return onError(error);
212
+ return res.status(500).json(error);
213
+ }
191
214
  if (optional) {
192
215
  req.userId = null;
193
216
  req.user = null;
194
217
  return next();
195
218
  }
196
- const error = { message: 'Invalid service token signature', code: 'INVALID_SERVICE_TOKEN', status: 401 };
219
+ const error = { error: 'INVALID_SERVICE_TOKEN', message: 'Invalid service token signature', code: 'INVALID_SERVICE_TOKEN', status: 401 };
197
220
  if (onError)
198
221
  return onError(error);
199
222
  return res.status(401).json(error);
@@ -205,7 +228,7 @@ function OxyServicesUtilityMixin(Base) {
205
228
  req.user = null;
206
229
  return next();
207
230
  }
208
- const error = { message: 'Service token expired', code: 'TOKEN_EXPIRED', status: 401 };
231
+ const error = { error: 'TOKEN_EXPIRED', message: 'Service token expired', code: 'TOKEN_EXPIRED', status: 401 };
209
232
  if (onError)
210
233
  return onError(error);
211
234
  return res.status(401).json(error);
@@ -217,7 +240,7 @@ function OxyServicesUtilityMixin(Base) {
217
240
  req.user = null;
218
241
  return next();
219
242
  }
220
- const error = { message: 'Invalid service token: missing appId', code: 'INVALID_SERVICE_TOKEN', status: 401 };
243
+ const error = { error: 'INVALID_SERVICE_TOKEN', message: 'Invalid service token: missing appId', code: 'INVALID_SERVICE_TOKEN', status: 401 };
221
244
  if (onError)
222
245
  return onError(error);
223
246
  return res.status(401).json(error);
@@ -244,6 +267,7 @@ function OxyServicesUtilityMixin(Base) {
244
267
  return next();
245
268
  }
246
269
  const error = {
270
+ error: 'INVALID_TOKEN_PAYLOAD',
247
271
  message: 'Token missing user ID',
248
272
  code: 'INVALID_TOKEN_PAYLOAD',
249
273
  status: 401
@@ -260,6 +284,7 @@ function OxyServicesUtilityMixin(Base) {
260
284
  return next();
261
285
  }
262
286
  const error = {
287
+ error: 'TOKEN_EXPIRED',
263
288
  message: 'Token expired',
264
289
  code: 'TOKEN_EXPIRED',
265
290
  status: 401
@@ -282,6 +307,7 @@ function OxyServicesUtilityMixin(Base) {
282
307
  return next();
283
308
  }
284
309
  const error = {
310
+ error: 'INVALID_SESSION',
285
311
  message: 'Session invalid or expired',
286
312
  code: 'INVALID_SESSION',
287
313
  status: 401
@@ -316,6 +342,7 @@ function OxyServicesUtilityMixin(Base) {
316
342
  return next();
317
343
  }
318
344
  const error = {
345
+ error: 'SESSION_VALIDATION_ERROR',
319
346
  message: 'Session validation failed',
320
347
  code: 'SESSION_VALIDATION_ERROR',
321
348
  status: 401
@@ -373,6 +400,8 @@ function OxyServicesUtilityMixin(Base) {
373
400
  * Returns a middleware function for Socket.IO that validates JWT tokens
374
401
  * from the handshake auth object and attaches user data to the socket.
375
402
  *
403
+ * Uses `jwtDecode()` + API session validation (same rationale as `auth()`).
404
+ *
376
405
  * @example
377
406
  * ```typescript
378
407
  * import { OxyServices } from '@oxyhq/core';
@@ -0,0 +1,114 @@
1
+ "use strict";
2
+ /**
3
+ * Authentication helper utilities for common token validation
4
+ * and authentication error handling patterns.
5
+ */
6
+ Object.defineProperty(exports, "__esModule", { value: true });
7
+ exports.AuthenticationFailedError = exports.SessionSyncRequiredError = void 0;
8
+ exports.ensureValidToken = ensureValidToken;
9
+ exports.isAuthenticationError = isAuthenticationError;
10
+ exports.withAuthErrorHandling = withAuthErrorHandling;
11
+ exports.authenticatedApiCall = authenticatedApiCall;
12
+ /**
13
+ * Error thrown when session sync is required
14
+ */
15
+ class SessionSyncRequiredError extends Error {
16
+ constructor(message = 'Session needs to be synced. Please try again.') {
17
+ super(message);
18
+ this.name = 'SessionSyncRequiredError';
19
+ }
20
+ }
21
+ exports.SessionSyncRequiredError = SessionSyncRequiredError;
22
+ /**
23
+ * Error thrown when authentication fails
24
+ */
25
+ class AuthenticationFailedError extends Error {
26
+ constructor(message = 'Authentication failed. Please sign in again.') {
27
+ super(message);
28
+ this.name = 'AuthenticationFailedError';
29
+ }
30
+ }
31
+ exports.AuthenticationFailedError = AuthenticationFailedError;
32
+ /**
33
+ * Ensures a valid token exists before making authenticated API calls.
34
+ * If no valid token exists and an active session ID is available,
35
+ * attempts to refresh the token using the session.
36
+ *
37
+ * @throws {SessionSyncRequiredError} If the session needs to be synced (offline session)
38
+ */
39
+ async function ensureValidToken(oxyServices, activeSessionId) {
40
+ if (oxyServices.hasValidToken() || !activeSessionId) {
41
+ return;
42
+ }
43
+ try {
44
+ await oxyServices.getTokenBySession(activeSessionId);
45
+ }
46
+ catch (tokenError) {
47
+ const errorMessage = tokenError instanceof Error ? tokenError.message : String(tokenError);
48
+ if (errorMessage.includes('AUTH_REQUIRED_OFFLINE_SESSION') || errorMessage.includes('offline')) {
49
+ throw new SessionSyncRequiredError();
50
+ }
51
+ throw tokenError;
52
+ }
53
+ }
54
+ /**
55
+ * Checks if an error is an authentication error (401 or auth-related message)
56
+ */
57
+ function isAuthenticationError(error) {
58
+ if (!error || typeof error !== 'object') {
59
+ return false;
60
+ }
61
+ const errorObj = error;
62
+ const errorMessage = errorObj.message || '';
63
+ const status = errorObj.status || errorObj.response?.status;
64
+ return (status === 401 ||
65
+ errorMessage.includes('Authentication required') ||
66
+ errorMessage.includes('Invalid or missing authorization header'));
67
+ }
68
+ /**
69
+ * Wraps an API call with authentication error handling.
70
+ * On auth failure, optionally attempts to sync the session and retry.
71
+ *
72
+ * @throws {AuthenticationFailedError} If authentication fails and cannot be recovered
73
+ */
74
+ async function withAuthErrorHandling(apiCall, options) {
75
+ try {
76
+ return await apiCall();
77
+ }
78
+ catch (error) {
79
+ if (!isAuthenticationError(error)) {
80
+ throw error;
81
+ }
82
+ if (options?.syncSession && options?.activeSessionId && options?.oxyServices) {
83
+ try {
84
+ await options.syncSession();
85
+ await options.oxyServices.getTokenBySession(options.activeSessionId);
86
+ return await apiCall();
87
+ }
88
+ catch {
89
+ throw new AuthenticationFailedError();
90
+ }
91
+ }
92
+ throw new AuthenticationFailedError();
93
+ }
94
+ }
95
+ /**
96
+ * Combines token validation and auth error handling for a complete authenticated API call.
97
+ *
98
+ * @example
99
+ * ```ts
100
+ * return await authenticatedApiCall(
101
+ * oxyServices,
102
+ * activeSessionId,
103
+ * () => oxyServices.updateProfile(updates)
104
+ * );
105
+ * ```
106
+ */
107
+ async function authenticatedApiCall(oxyServices, activeSessionId, apiCall, syncSession) {
108
+ await ensureValidToken(oxyServices, activeSessionId);
109
+ return withAuthErrorHandling(apiCall, {
110
+ syncSession,
111
+ activeSessionId,
112
+ oxyServices,
113
+ });
114
+ }
@@ -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
@@ -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: `/api/session/token/${sessionId}`,
246
+ url: `/session/token/${sessionId}`,
247
247
  cache: false,
248
248
  retry: false,
249
249
  });
@@ -296,6 +296,19 @@ export class AuthManager {
296
296
  clearTimeout(this.refreshTimer);
297
297
  this.refreshTimer = null;
298
298
  }
299
+ // Invalidate current session on the server (best-effort)
300
+ try {
301
+ const sessionJson = await this.storage.getItem(STORAGE_KEYS.SESSION);
302
+ if (sessionJson) {
303
+ const session = JSON.parse(sessionJson);
304
+ if (session.sessionId && typeof this.oxyServices.logoutSession === 'function') {
305
+ await this.oxyServices.logoutSession(session.sessionId);
306
+ }
307
+ }
308
+ }
309
+ catch {
310
+ // Best-effort: don't block local signout on network failure
311
+ }
299
312
  // Revoke FedCM credential if supported
300
313
  try {
301
314
  const services = this.oxyServices;