@oxyhq/core 1.6.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.
@@ -300,6 +300,19 @@ class AuthManager {
300
300
  clearTimeout(this.refreshTimer);
301
301
  this.refreshTimer = null;
302
302
  }
303
+ // Invalidate current session on the server (best-effort)
304
+ try {
305
+ const sessionJson = await this.storage.getItem(STORAGE_KEYS.SESSION);
306
+ if (sessionJson) {
307
+ const session = JSON.parse(sessionJson);
308
+ if (session.sessionId && typeof this.oxyServices.logoutSession === 'function') {
309
+ await this.oxyServices.logoutSession(session.sessionId);
310
+ }
311
+ }
312
+ }
313
+ catch {
314
+ // Best-effort: don't block local signout on network failure
315
+ }
303
316
  // Revoke FedCM credential if supported
304
317
  try {
305
318
  const services = this.oxyServices;
package/dist/cjs/index.js CHANGED
@@ -30,7 +30,7 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
30
30
  };
31
31
  Object.defineProperty(exports, "__esModule", { value: true });
32
32
  exports.calculateBackoffInterval = exports.createCircuitBreakerState = exports.DEFAULT_CIRCUIT_BREAKER_CONFIG = exports.isRetryableError = exports.isNetworkError = exports.isServerError = exports.isRateLimitError = exports.isNotFoundError = exports.isForbiddenError = exports.isUnauthorizedError = exports.isAlreadyRegisteredError = exports.getErrorMessage = exports.getErrorStatus = exports.HttpStatus = exports.getSystemColorScheme = exports.systemPrefersDarkMode = exports.getOppositeTheme = exports.normalizeColorScheme = exports.normalizeTheme = exports.getContrastTextColor = exports.isLightColor = exports.withOpacity = exports.rgbToHex = exports.hexToRgb = exports.lightenColor = exports.darkenColor = exports.isAndroid = exports.isIOS = exports.isNative = exports.isWeb = exports.setPlatformOS = exports.getPlatformOS = exports.normalizeLanguageCode = exports.getNativeLanguageName = exports.getLanguageName = exports.getLanguageMetadata = exports.SUPPORTED_LANGUAGES = exports.DeviceManager = exports.RecoveryPhraseService = exports.SignatureService = exports.KeyManager = exports.createCrossDomainAuth = exports.CrossDomainAuth = exports.createAuthManager = exports.AuthManager = exports.oxyClient = exports.OXY_CLOUD_URL = exports.OxyAuthenticationTimeoutError = exports.OxyAuthenticationError = exports.OxyServices = void 0;
33
- exports.logPerformance = exports.logPayment = exports.logDevice = exports.logUser = exports.logSession = exports.logApi = exports.logAuth = exports.LogLevel = exports.logger = exports.retryAsync = exports.validateRequiredFields = exports.handleHttpError = exports.createApiError = exports.ErrorCodes = exports.packageInfo = exports.sessionsArraysEqual = exports.normalizeAndSortSessions = exports.mergeSessions = exports.translate = exports.createDebugLogger = exports.debugError = exports.debugWarn = exports.debugLog = exports.isDev = exports.withRetry = exports.delay = exports.shouldAllowRequest = exports.recordSuccess = exports.recordFailure = void 0;
33
+ exports.logPerformance = exports.logPayment = exports.logDevice = exports.logUser = exports.logSession = exports.logApi = exports.logAuth = exports.LogLevel = exports.logger = exports.retryAsync = exports.validateRequiredFields = exports.handleHttpError = exports.createApiError = exports.ErrorCodes = exports.packageInfo = exports.sessionsArraysEqual = exports.normalizeAndSortSessions = exports.mergeSessions = exports.authenticatedApiCall = exports.withAuthErrorHandling = exports.isAuthenticationError = exports.ensureValidToken = exports.AuthenticationFailedError = exports.SessionSyncRequiredError = exports.translate = exports.createDebugLogger = exports.debugError = exports.debugWarn = exports.debugLog = exports.isDev = exports.withRetry = exports.delay = exports.shouldAllowRequest = exports.recordSuccess = exports.recordFailure = void 0;
34
34
  // Ensure crypto polyfills are loaded before anything else
35
35
  require("./crypto/polyfill");
36
36
  // --- Core API Client ---
@@ -119,6 +119,14 @@ Object.defineProperty(exports, "createDebugLogger", { enumerable: true, get: fun
119
119
  // --- i18n ---
120
120
  var i18n_1 = require("./i18n");
121
121
  Object.defineProperty(exports, "translate", { enumerable: true, get: function () { return i18n_1.translate; } });
122
+ // --- Auth Helpers ---
123
+ var authHelpers_1 = require("./utils/authHelpers");
124
+ Object.defineProperty(exports, "SessionSyncRequiredError", { enumerable: true, get: function () { return authHelpers_1.SessionSyncRequiredError; } });
125
+ Object.defineProperty(exports, "AuthenticationFailedError", { enumerable: true, get: function () { return authHelpers_1.AuthenticationFailedError; } });
126
+ Object.defineProperty(exports, "ensureValidToken", { enumerable: true, get: function () { return authHelpers_1.ensureValidToken; } });
127
+ Object.defineProperty(exports, "isAuthenticationError", { enumerable: true, get: function () { return authHelpers_1.isAuthenticationError; } });
128
+ Object.defineProperty(exports, "withAuthErrorHandling", { enumerable: true, get: function () { return authHelpers_1.withAuthErrorHandling; } });
129
+ Object.defineProperty(exports, "authenticatedApiCall", { enumerable: true, get: function () { return authHelpers_1.authenticatedApiCall; } });
122
130
  // --- Session Utilities ---
123
131
  var sessionUtils_1 = require("./utils/sessionUtils");
124
132
  Object.defineProperty(exports, "mergeSessions", { enumerable: true, get: function () { return sessionUtils_1.mergeSessions; } });
@@ -67,6 +67,17 @@ 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';
@@ -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
@@ -192,7 +206,7 @@ function OxyServicesUtilityMixin(Base) {
192
206
  (verifyError.message === 'Invalid signature' || verifyError.message === 'Invalid token structure');
193
207
  if (!isSignatureError) {
194
208
  console.error('[oxy.auth] Unexpected error during service token verification:', verifyError);
195
- const error = { message: 'Internal authentication error', code: 'AUTH_INTERNAL_ERROR', status: 500 };
209
+ const error = { error: 'AUTH_INTERNAL_ERROR', message: 'Internal authentication error', code: 'AUTH_INTERNAL_ERROR', status: 500 };
196
210
  if (onError)
197
211
  return onError(error);
198
212
  return res.status(500).json(error);
@@ -202,7 +216,7 @@ function OxyServicesUtilityMixin(Base) {
202
216
  req.user = null;
203
217
  return next();
204
218
  }
205
- 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 };
206
220
  if (onError)
207
221
  return onError(error);
208
222
  return res.status(401).json(error);
@@ -214,7 +228,7 @@ function OxyServicesUtilityMixin(Base) {
214
228
  req.user = null;
215
229
  return next();
216
230
  }
217
- 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 };
218
232
  if (onError)
219
233
  return onError(error);
220
234
  return res.status(401).json(error);
@@ -226,7 +240,7 @@ function OxyServicesUtilityMixin(Base) {
226
240
  req.user = null;
227
241
  return next();
228
242
  }
229
- 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 };
230
244
  if (onError)
231
245
  return onError(error);
232
246
  return res.status(401).json(error);
@@ -253,6 +267,7 @@ function OxyServicesUtilityMixin(Base) {
253
267
  return next();
254
268
  }
255
269
  const error = {
270
+ error: 'INVALID_TOKEN_PAYLOAD',
256
271
  message: 'Token missing user ID',
257
272
  code: 'INVALID_TOKEN_PAYLOAD',
258
273
  status: 401
@@ -269,6 +284,7 @@ function OxyServicesUtilityMixin(Base) {
269
284
  return next();
270
285
  }
271
286
  const error = {
287
+ error: 'TOKEN_EXPIRED',
272
288
  message: 'Token expired',
273
289
  code: 'TOKEN_EXPIRED',
274
290
  status: 401
@@ -291,6 +307,7 @@ function OxyServicesUtilityMixin(Base) {
291
307
  return next();
292
308
  }
293
309
  const error = {
310
+ error: 'INVALID_SESSION',
294
311
  message: 'Session invalid or expired',
295
312
  code: 'INVALID_SESSION',
296
313
  status: 401
@@ -325,6 +342,7 @@ function OxyServicesUtilityMixin(Base) {
325
342
  return next();
326
343
  }
327
344
  const error = {
345
+ error: 'SESSION_VALIDATION_ERROR',
328
346
  message: 'Session validation failed',
329
347
  code: 'SESSION_VALIDATION_ERROR',
330
348
  status: 401
@@ -382,6 +400,8 @@ function OxyServicesUtilityMixin(Base) {
382
400
  * Returns a middleware function for Socket.IO that validates JWT tokens
383
401
  * from the handshake auth object and attaches user data to the socket.
384
402
  *
403
+ * Uses `jwtDecode()` + API session validation (same rationale as `auth()`).
404
+ *
385
405
  * @example
386
406
  * ```typescript
387
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
+ }
@@ -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;
package/dist/esm/index.js CHANGED
@@ -40,6 +40,8 @@ export { DEFAULT_CIRCUIT_BREAKER_CONFIG, createCircuitBreakerState, calculateBac
40
40
  export { isDev, debugLog, debugWarn, debugError, createDebugLogger, } from './shared/utils/debugUtils';
41
41
  // --- i18n ---
42
42
  export { translate } from './i18n';
43
+ // --- Auth Helpers ---
44
+ export { SessionSyncRequiredError, AuthenticationFailedError, ensureValidToken, isAuthenticationError, withAuthErrorHandling, authenticatedApiCall, } from './utils/authHelpers';
43
45
  // --- Session Utilities ---
44
46
  export { mergeSessions, normalizeAndSortSessions, sessionsArraysEqual } from './utils/sessionUtils';
45
47
  // --- Constants ---
@@ -31,6 +31,17 @@ export function OxyServicesUtilityMixin(Base) {
31
31
  * Validates JWT tokens against the Oxy API and attaches user data to requests.
32
32
  * Uses server-side session validation for security (not just JWT decode).
33
33
  *
34
+ * **Design note — jwtDecode vs jwt.verify:**
35
+ * This middleware intentionally uses `jwtDecode()` (decode-only, no signature
36
+ * verification) for user tokens. This is by design, NOT a security gap:
37
+ * - Third-party apps using `oxy.auth()` don't have the Oxy JWT secret
38
+ * - Security comes from API-based session validation (`validateSession()`)
39
+ * which checks the session server-side on every request
40
+ * - Service tokens (type: 'service') DO use cryptographic HMAC verification
41
+ * via the `jwtSecret` option, since they are stateless
42
+ * - The backend's own `authMiddleware` uses `jwt.verify()` because it has
43
+ * direct access to `ACCESS_TOKEN_SECRET`
44
+ *
34
45
  * @example
35
46
  * ```typescript
36
47
  * import { OxyServices } from '@oxyhq/core';
@@ -83,6 +94,7 @@ export function OxyServicesUtilityMixin(Base) {
83
94
  return next();
84
95
  }
85
96
  const error = {
97
+ error: 'MISSING_TOKEN',
86
98
  message: 'Access token required',
87
99
  code: 'MISSING_TOKEN',
88
100
  status: 401
@@ -103,6 +115,7 @@ export function OxyServicesUtilityMixin(Base) {
103
115
  return next();
104
116
  }
105
117
  const error = {
118
+ error: 'INVALID_TOKEN_FORMAT',
106
119
  message: 'Invalid token format',
107
120
  code: 'INVALID_TOKEN_FORMAT',
108
121
  status: 401
@@ -122,6 +135,7 @@ export function OxyServicesUtilityMixin(Base) {
122
135
  return next();
123
136
  }
124
137
  const error = {
138
+ error: 'SERVICE_TOKEN_NOT_CONFIGURED',
125
139
  message: 'Service token verification not configured',
126
140
  code: 'SERVICE_TOKEN_NOT_CONFIGURED',
127
141
  status: 403
@@ -156,7 +170,7 @@ export function OxyServicesUtilityMixin(Base) {
156
170
  (verifyError.message === 'Invalid signature' || verifyError.message === 'Invalid token structure');
157
171
  if (!isSignatureError) {
158
172
  console.error('[oxy.auth] Unexpected error during service token verification:', verifyError);
159
- const error = { message: 'Internal authentication error', code: 'AUTH_INTERNAL_ERROR', status: 500 };
173
+ const error = { error: 'AUTH_INTERNAL_ERROR', message: 'Internal authentication error', code: 'AUTH_INTERNAL_ERROR', status: 500 };
160
174
  if (onError)
161
175
  return onError(error);
162
176
  return res.status(500).json(error);
@@ -166,7 +180,7 @@ export function OxyServicesUtilityMixin(Base) {
166
180
  req.user = null;
167
181
  return next();
168
182
  }
169
- const error = { message: 'Invalid service token signature', code: 'INVALID_SERVICE_TOKEN', status: 401 };
183
+ const error = { error: 'INVALID_SERVICE_TOKEN', message: 'Invalid service token signature', code: 'INVALID_SERVICE_TOKEN', status: 401 };
170
184
  if (onError)
171
185
  return onError(error);
172
186
  return res.status(401).json(error);
@@ -178,7 +192,7 @@ export function OxyServicesUtilityMixin(Base) {
178
192
  req.user = null;
179
193
  return next();
180
194
  }
181
- const error = { message: 'Service token expired', code: 'TOKEN_EXPIRED', status: 401 };
195
+ const error = { error: 'TOKEN_EXPIRED', message: 'Service token expired', code: 'TOKEN_EXPIRED', status: 401 };
182
196
  if (onError)
183
197
  return onError(error);
184
198
  return res.status(401).json(error);
@@ -190,7 +204,7 @@ export function OxyServicesUtilityMixin(Base) {
190
204
  req.user = null;
191
205
  return next();
192
206
  }
193
- const error = { message: 'Invalid service token: missing appId', code: 'INVALID_SERVICE_TOKEN', status: 401 };
207
+ const error = { error: 'INVALID_SERVICE_TOKEN', message: 'Invalid service token: missing appId', code: 'INVALID_SERVICE_TOKEN', status: 401 };
194
208
  if (onError)
195
209
  return onError(error);
196
210
  return res.status(401).json(error);
@@ -217,6 +231,7 @@ export function OxyServicesUtilityMixin(Base) {
217
231
  return next();
218
232
  }
219
233
  const error = {
234
+ error: 'INVALID_TOKEN_PAYLOAD',
220
235
  message: 'Token missing user ID',
221
236
  code: 'INVALID_TOKEN_PAYLOAD',
222
237
  status: 401
@@ -233,6 +248,7 @@ export function OxyServicesUtilityMixin(Base) {
233
248
  return next();
234
249
  }
235
250
  const error = {
251
+ error: 'TOKEN_EXPIRED',
236
252
  message: 'Token expired',
237
253
  code: 'TOKEN_EXPIRED',
238
254
  status: 401
@@ -255,6 +271,7 @@ export function OxyServicesUtilityMixin(Base) {
255
271
  return next();
256
272
  }
257
273
  const error = {
274
+ error: 'INVALID_SESSION',
258
275
  message: 'Session invalid or expired',
259
276
  code: 'INVALID_SESSION',
260
277
  status: 401
@@ -289,6 +306,7 @@ export function OxyServicesUtilityMixin(Base) {
289
306
  return next();
290
307
  }
291
308
  const error = {
309
+ error: 'SESSION_VALIDATION_ERROR',
292
310
  message: 'Session validation failed',
293
311
  code: 'SESSION_VALIDATION_ERROR',
294
312
  status: 401
@@ -346,6 +364,8 @@ export function OxyServicesUtilityMixin(Base) {
346
364
  * Returns a middleware function for Socket.IO that validates JWT tokens
347
365
  * from the handshake auth object and attaches user data to the socket.
348
366
  *
367
+ * Uses `jwtDecode()` + API session validation (same rationale as `auth()`).
368
+ *
349
369
  * @example
350
370
  * ```typescript
351
371
  * import { OxyServices } from '@oxyhq/core';
@@ -0,0 +1,105 @@
1
+ /**
2
+ * Authentication helper utilities for common token validation
3
+ * and authentication error handling patterns.
4
+ */
5
+ /**
6
+ * Error thrown when session sync is required
7
+ */
8
+ export class SessionSyncRequiredError extends Error {
9
+ constructor(message = 'Session needs to be synced. Please try again.') {
10
+ super(message);
11
+ this.name = 'SessionSyncRequiredError';
12
+ }
13
+ }
14
+ /**
15
+ * Error thrown when authentication fails
16
+ */
17
+ export class AuthenticationFailedError extends Error {
18
+ constructor(message = 'Authentication failed. Please sign in again.') {
19
+ super(message);
20
+ this.name = 'AuthenticationFailedError';
21
+ }
22
+ }
23
+ /**
24
+ * Ensures a valid token exists before making authenticated API calls.
25
+ * If no valid token exists and an active session ID is available,
26
+ * attempts to refresh the token using the session.
27
+ *
28
+ * @throws {SessionSyncRequiredError} If the session needs to be synced (offline session)
29
+ */
30
+ export async function ensureValidToken(oxyServices, activeSessionId) {
31
+ if (oxyServices.hasValidToken() || !activeSessionId) {
32
+ return;
33
+ }
34
+ try {
35
+ await oxyServices.getTokenBySession(activeSessionId);
36
+ }
37
+ catch (tokenError) {
38
+ const errorMessage = tokenError instanceof Error ? tokenError.message : String(tokenError);
39
+ if (errorMessage.includes('AUTH_REQUIRED_OFFLINE_SESSION') || errorMessage.includes('offline')) {
40
+ throw new SessionSyncRequiredError();
41
+ }
42
+ throw tokenError;
43
+ }
44
+ }
45
+ /**
46
+ * Checks if an error is an authentication error (401 or auth-related message)
47
+ */
48
+ export function isAuthenticationError(error) {
49
+ if (!error || typeof error !== 'object') {
50
+ return false;
51
+ }
52
+ const errorObj = error;
53
+ const errorMessage = errorObj.message || '';
54
+ const status = errorObj.status || errorObj.response?.status;
55
+ return (status === 401 ||
56
+ errorMessage.includes('Authentication required') ||
57
+ errorMessage.includes('Invalid or missing authorization header'));
58
+ }
59
+ /**
60
+ * Wraps an API call with authentication error handling.
61
+ * On auth failure, optionally attempts to sync the session and retry.
62
+ *
63
+ * @throws {AuthenticationFailedError} If authentication fails and cannot be recovered
64
+ */
65
+ export async function withAuthErrorHandling(apiCall, options) {
66
+ try {
67
+ return await apiCall();
68
+ }
69
+ catch (error) {
70
+ if (!isAuthenticationError(error)) {
71
+ throw error;
72
+ }
73
+ if (options?.syncSession && options?.activeSessionId && options?.oxyServices) {
74
+ try {
75
+ await options.syncSession();
76
+ await options.oxyServices.getTokenBySession(options.activeSessionId);
77
+ return await apiCall();
78
+ }
79
+ catch {
80
+ throw new AuthenticationFailedError();
81
+ }
82
+ }
83
+ throw new AuthenticationFailedError();
84
+ }
85
+ }
86
+ /**
87
+ * Combines token validation and auth error handling for a complete authenticated API call.
88
+ *
89
+ * @example
90
+ * ```ts
91
+ * return await authenticatedApiCall(
92
+ * oxyServices,
93
+ * activeSessionId,
94
+ * () => oxyServices.updateProfile(updates)
95
+ * );
96
+ * ```
97
+ */
98
+ export async function authenticatedApiCall(oxyServices, activeSessionId, apiCall, syncSession) {
99
+ await ensureValidToken(oxyServices, activeSessionId);
100
+ return withAuthErrorHandling(apiCall, {
101
+ syncSession,
102
+ activeSessionId,
103
+ oxyServices,
104
+ });
105
+ }
@@ -43,6 +43,8 @@ export { DEFAULT_CIRCUIT_BREAKER_CONFIG, createCircuitBreakerState, calculateBac
43
43
  export type { CircuitBreakerState, CircuitBreakerConfig } from './shared/utils/networkUtils';
44
44
  export { isDev, debugLog, debugWarn, debugError, createDebugLogger, } from './shared/utils/debugUtils';
45
45
  export { translate } from './i18n';
46
+ export { SessionSyncRequiredError, AuthenticationFailedError, ensureValidToken, isAuthenticationError, withAuthErrorHandling, authenticatedApiCall, } from './utils/authHelpers';
47
+ export type { HandleApiErrorOptions } from './utils/authHelpers';
46
48
  export { mergeSessions, normalizeAndSortSessions, sessionsArraysEqual } from './utils/sessionUtils';
47
49
  export { packageInfo } from './constants/version';
48
50
  export * from './utils/apiUtils';
@@ -43,6 +43,17 @@ export declare function OxyServicesUtilityMixin<T extends typeof OxyServicesBase
43
43
  * Validates JWT tokens against the Oxy API and attaches user data to requests.
44
44
  * Uses server-side session validation for security (not just JWT decode).
45
45
  *
46
+ * **Design note — jwtDecode vs jwt.verify:**
47
+ * This middleware intentionally uses `jwtDecode()` (decode-only, no signature
48
+ * verification) for user tokens. This is by design, NOT a security gap:
49
+ * - Third-party apps using `oxy.auth()` don't have the Oxy JWT secret
50
+ * - Security comes from API-based session validation (`validateSession()`)
51
+ * which checks the session server-side on every request
52
+ * - Service tokens (type: 'service') DO use cryptographic HMAC verification
53
+ * via the `jwtSecret` option, since they are stateless
54
+ * - The backend's own `authMiddleware` uses `jwt.verify()` because it has
55
+ * direct access to `ACCESS_TOKEN_SECRET`
56
+ *
46
57
  * @example
47
58
  * ```typescript
48
59
  * import { OxyServices } from '@oxyhq/core';
@@ -74,6 +85,8 @@ export declare function OxyServicesUtilityMixin<T extends typeof OxyServicesBase
74
85
  * Returns a middleware function for Socket.IO that validates JWT tokens
75
86
  * from the handshake auth object and attaches user data to the socket.
76
87
  *
88
+ * Uses `jwtDecode()` + API session validation (same rationale as `auth()`).
89
+ *
77
90
  * @example
78
91
  * ```typescript
79
92
  * import { OxyServices } from '@oxyhq/core';
@@ -0,0 +1,57 @@
1
+ /**
2
+ * Authentication helper utilities for common token validation
3
+ * and authentication error handling patterns.
4
+ */
5
+ import type { OxyServices } from '../OxyServices';
6
+ /**
7
+ * Error thrown when session sync is required
8
+ */
9
+ export declare class SessionSyncRequiredError extends Error {
10
+ constructor(message?: string);
11
+ }
12
+ /**
13
+ * Error thrown when authentication fails
14
+ */
15
+ export declare class AuthenticationFailedError extends Error {
16
+ constructor(message?: string);
17
+ }
18
+ /**
19
+ * Ensures a valid token exists before making authenticated API calls.
20
+ * If no valid token exists and an active session ID is available,
21
+ * attempts to refresh the token using the session.
22
+ *
23
+ * @throws {SessionSyncRequiredError} If the session needs to be synced (offline session)
24
+ */
25
+ export declare function ensureValidToken(oxyServices: OxyServices, activeSessionId: string | null | undefined): Promise<void>;
26
+ /**
27
+ * Options for handling API authentication errors
28
+ */
29
+ export interface HandleApiErrorOptions {
30
+ syncSession?: () => Promise<unknown>;
31
+ activeSessionId?: string | null;
32
+ oxyServices?: OxyServices;
33
+ }
34
+ /**
35
+ * Checks if an error is an authentication error (401 or auth-related message)
36
+ */
37
+ export declare function isAuthenticationError(error: unknown): boolean;
38
+ /**
39
+ * Wraps an API call with authentication error handling.
40
+ * On auth failure, optionally attempts to sync the session and retry.
41
+ *
42
+ * @throws {AuthenticationFailedError} If authentication fails and cannot be recovered
43
+ */
44
+ export declare function withAuthErrorHandling<T>(apiCall: () => Promise<T>, options?: HandleApiErrorOptions): Promise<T>;
45
+ /**
46
+ * Combines token validation and auth error handling for a complete authenticated API call.
47
+ *
48
+ * @example
49
+ * ```ts
50
+ * return await authenticatedApiCall(
51
+ * oxyServices,
52
+ * activeSessionId,
53
+ * () => oxyServices.updateProfile(updates)
54
+ * );
55
+ * ```
56
+ */
57
+ export declare function authenticatedApiCall<T>(oxyServices: OxyServices, activeSessionId: string | null | undefined, apiCall: () => Promise<T>, syncSession?: () => Promise<unknown>): Promise<T>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oxyhq/core",
3
- "version": "1.6.0",
3
+ "version": "1.6.1",
4
4
  "description": "OxyHQ SDK Foundation — API client, authentication, cryptographic identity, and shared utilities",
5
5
  "main": "dist/cjs/index.js",
6
6
  "module": "dist/esm/index.js",
@@ -366,6 +366,19 @@ export class AuthManager {
366
366
  this.refreshTimer = null;
367
367
  }
368
368
 
369
+ // Invalidate current session on the server (best-effort)
370
+ try {
371
+ const sessionJson = await this.storage.getItem(STORAGE_KEYS.SESSION);
372
+ if (sessionJson) {
373
+ const session = JSON.parse(sessionJson);
374
+ if (session.sessionId && typeof (this.oxyServices as any).logoutSession === 'function') {
375
+ await (this.oxyServices as any).logoutSession(session.sessionId);
376
+ }
377
+ }
378
+ } catch {
379
+ // Best-effort: don't block local signout on network failure
380
+ }
381
+
369
382
  // Revoke FedCM credential if supported
370
383
  try {
371
384
  const services = this.oxyServices as OxyServicesWithFedCM;
package/src/index.ts CHANGED
@@ -123,6 +123,17 @@ export {
123
123
  // --- i18n ---
124
124
  export { translate } from './i18n';
125
125
 
126
+ // --- Auth Helpers ---
127
+ export {
128
+ SessionSyncRequiredError,
129
+ AuthenticationFailedError,
130
+ ensureValidToken,
131
+ isAuthenticationError,
132
+ withAuthErrorHandling,
133
+ authenticatedApiCall,
134
+ } from './utils/authHelpers';
135
+ export type { HandleApiErrorOptions } from './utils/authHelpers';
136
+
126
137
  // --- Session Utilities ---
127
138
  export { mergeSessions, normalizeAndSortSessions, sessionsArraysEqual } from './utils/sessionUtils';
128
139
 
@@ -83,6 +83,17 @@ export function OxyServicesUtilityMixin<T extends typeof OxyServicesBase>(Base:
83
83
  * Validates JWT tokens against the Oxy API and attaches user data to requests.
84
84
  * Uses server-side session validation for security (not just JWT decode).
85
85
  *
86
+ * **Design note — jwtDecode vs jwt.verify:**
87
+ * This middleware intentionally uses `jwtDecode()` (decode-only, no signature
88
+ * verification) for user tokens. This is by design, NOT a security gap:
89
+ * - Third-party apps using `oxy.auth()` don't have the Oxy JWT secret
90
+ * - Security comes from API-based session validation (`validateSession()`)
91
+ * which checks the session server-side on every request
92
+ * - Service tokens (type: 'service') DO use cryptographic HMAC verification
93
+ * via the `jwtSecret` option, since they are stateless
94
+ * - The backend's own `authMiddleware` uses `jwt.verify()` because it has
95
+ * direct access to `ACCESS_TOKEN_SECRET`
96
+ *
86
97
  * @example
87
98
  * ```typescript
88
99
  * import { OxyServices } from '@oxyhq/core';
@@ -138,6 +149,7 @@ export function OxyServicesUtilityMixin<T extends typeof OxyServicesBase>(Base:
138
149
  }
139
150
 
140
151
  const error = {
152
+ error: 'MISSING_TOKEN',
141
153
  message: 'Access token required',
142
154
  code: 'MISSING_TOKEN',
143
155
  status: 401
@@ -158,6 +170,7 @@ export function OxyServicesUtilityMixin<T extends typeof OxyServicesBase>(Base:
158
170
  }
159
171
 
160
172
  const error = {
173
+ error: 'INVALID_TOKEN_FORMAT',
161
174
  message: 'Invalid token format',
162
175
  code: 'INVALID_TOKEN_FORMAT',
163
176
  status: 401
@@ -177,6 +190,7 @@ export function OxyServicesUtilityMixin<T extends typeof OxyServicesBase>(Base:
177
190
  return next();
178
191
  }
179
192
  const error = {
193
+ error: 'SERVICE_TOKEN_NOT_CONFIGURED',
180
194
  message: 'Service token verification not configured',
181
195
  code: 'SERVICE_TOKEN_NOT_CONFIGURED',
182
196
  status: 403
@@ -212,7 +226,7 @@ export function OxyServicesUtilityMixin<T extends typeof OxyServicesBase>(Base:
212
226
 
213
227
  if (!isSignatureError) {
214
228
  console.error('[oxy.auth] Unexpected error during service token verification:', verifyError);
215
- const error = { message: 'Internal authentication error', code: 'AUTH_INTERNAL_ERROR', status: 500 };
229
+ const error = { error: 'AUTH_INTERNAL_ERROR', message: 'Internal authentication error', code: 'AUTH_INTERNAL_ERROR', status: 500 };
216
230
  if (onError) return onError(error);
217
231
  return res.status(500).json(error);
218
232
  }
@@ -222,7 +236,7 @@ export function OxyServicesUtilityMixin<T extends typeof OxyServicesBase>(Base:
222
236
  req.user = null;
223
237
  return next();
224
238
  }
225
- const error = { message: 'Invalid service token signature', code: 'INVALID_SERVICE_TOKEN', status: 401 };
239
+ const error = { error: 'INVALID_SERVICE_TOKEN', message: 'Invalid service token signature', code: 'INVALID_SERVICE_TOKEN', status: 401 };
226
240
  if (onError) return onError(error);
227
241
  return res.status(401).json(error);
228
242
  }
@@ -234,7 +248,7 @@ export function OxyServicesUtilityMixin<T extends typeof OxyServicesBase>(Base:
234
248
  req.user = null;
235
249
  return next();
236
250
  }
237
- const error = { message: 'Service token expired', code: 'TOKEN_EXPIRED', status: 401 };
251
+ const error = { error: 'TOKEN_EXPIRED', message: 'Service token expired', code: 'TOKEN_EXPIRED', status: 401 };
238
252
  if (onError) return onError(error);
239
253
  return res.status(401).json(error);
240
254
  }
@@ -246,7 +260,7 @@ export function OxyServicesUtilityMixin<T extends typeof OxyServicesBase>(Base:
246
260
  req.user = null;
247
261
  return next();
248
262
  }
249
- const error = { message: 'Invalid service token: missing appId', code: 'INVALID_SERVICE_TOKEN', status: 401 };
263
+ const error = { error: 'INVALID_SERVICE_TOKEN', message: 'Invalid service token: missing appId', code: 'INVALID_SERVICE_TOKEN', status: 401 };
250
264
  if (onError) return onError(error);
251
265
  return res.status(401).json(error);
252
266
  }
@@ -278,6 +292,7 @@ export function OxyServicesUtilityMixin<T extends typeof OxyServicesBase>(Base:
278
292
  }
279
293
 
280
294
  const error = {
295
+ error: 'INVALID_TOKEN_PAYLOAD',
281
296
  message: 'Token missing user ID',
282
297
  code: 'INVALID_TOKEN_PAYLOAD',
283
298
  status: 401
@@ -295,6 +310,7 @@ export function OxyServicesUtilityMixin<T extends typeof OxyServicesBase>(Base:
295
310
  }
296
311
 
297
312
  const error = {
313
+ error: 'TOKEN_EXPIRED',
298
314
  message: 'Token expired',
299
315
  code: 'TOKEN_EXPIRED',
300
316
  status: 401
@@ -319,6 +335,7 @@ export function OxyServicesUtilityMixin<T extends typeof OxyServicesBase>(Base:
319
335
  }
320
336
 
321
337
  const error = {
338
+ error: 'INVALID_SESSION',
322
339
  message: 'Session invalid or expired',
323
340
  code: 'INVALID_SESSION',
324
341
  status: 401
@@ -356,6 +373,7 @@ export function OxyServicesUtilityMixin<T extends typeof OxyServicesBase>(Base:
356
373
  }
357
374
 
358
375
  const error = {
376
+ error: 'SESSION_VALIDATION_ERROR',
359
377
  message: 'Session validation failed',
360
378
  code: 'SESSION_VALIDATION_ERROR',
361
379
  status: 401
@@ -416,6 +434,8 @@ export function OxyServicesUtilityMixin<T extends typeof OxyServicesBase>(Base:
416
434
  * Returns a middleware function for Socket.IO that validates JWT tokens
417
435
  * from the handshake auth object and attaches user data to the socket.
418
436
  *
437
+ * Uses `jwtDecode()` + API session validation (same rationale as `auth()`).
438
+ *
419
439
  * @example
420
440
  * ```typescript
421
441
  * import { OxyServices } from '@oxyhq/core';
@@ -0,0 +1,140 @@
1
+ /**
2
+ * Authentication helper utilities for common token validation
3
+ * and authentication error handling patterns.
4
+ */
5
+
6
+ import type { OxyServices } from '../OxyServices';
7
+
8
+ /**
9
+ * Error thrown when session sync is required
10
+ */
11
+ export class SessionSyncRequiredError extends Error {
12
+ constructor(message = 'Session needs to be synced. Please try again.') {
13
+ super(message);
14
+ this.name = 'SessionSyncRequiredError';
15
+ }
16
+ }
17
+
18
+ /**
19
+ * Error thrown when authentication fails
20
+ */
21
+ export class AuthenticationFailedError extends Error {
22
+ constructor(message = 'Authentication failed. Please sign in again.') {
23
+ super(message);
24
+ this.name = 'AuthenticationFailedError';
25
+ }
26
+ }
27
+
28
+ /**
29
+ * Ensures a valid token exists before making authenticated API calls.
30
+ * If no valid token exists and an active session ID is available,
31
+ * attempts to refresh the token using the session.
32
+ *
33
+ * @throws {SessionSyncRequiredError} If the session needs to be synced (offline session)
34
+ */
35
+ export async function ensureValidToken(
36
+ oxyServices: OxyServices,
37
+ activeSessionId: string | null | undefined
38
+ ): Promise<void> {
39
+ if (oxyServices.hasValidToken() || !activeSessionId) {
40
+ return;
41
+ }
42
+
43
+ try {
44
+ await oxyServices.getTokenBySession(activeSessionId);
45
+ } catch (tokenError) {
46
+ const errorMessage = tokenError instanceof Error ? tokenError.message : String(tokenError);
47
+
48
+ if (errorMessage.includes('AUTH_REQUIRED_OFFLINE_SESSION') || errorMessage.includes('offline')) {
49
+ throw new SessionSyncRequiredError();
50
+ }
51
+
52
+ throw tokenError;
53
+ }
54
+ }
55
+
56
+ /**
57
+ * Options for handling API authentication errors
58
+ */
59
+ export interface HandleApiErrorOptions {
60
+ syncSession?: () => Promise<unknown>;
61
+ activeSessionId?: string | null;
62
+ oxyServices?: OxyServices;
63
+ }
64
+
65
+ /**
66
+ * Checks if an error is an authentication error (401 or auth-related message)
67
+ */
68
+ export function isAuthenticationError(error: unknown): boolean {
69
+ if (!error || typeof error !== 'object') {
70
+ return false;
71
+ }
72
+
73
+ const errorObj = error as { message?: string; status?: number; response?: { status?: number } };
74
+ const errorMessage = errorObj.message || '';
75
+ const status = errorObj.status || errorObj.response?.status;
76
+
77
+ return (
78
+ status === 401 ||
79
+ errorMessage.includes('Authentication required') ||
80
+ errorMessage.includes('Invalid or missing authorization header')
81
+ );
82
+ }
83
+
84
+ /**
85
+ * Wraps an API call with authentication error handling.
86
+ * On auth failure, optionally attempts to sync the session and retry.
87
+ *
88
+ * @throws {AuthenticationFailedError} If authentication fails and cannot be recovered
89
+ */
90
+ export async function withAuthErrorHandling<T>(
91
+ apiCall: () => Promise<T>,
92
+ options?: HandleApiErrorOptions
93
+ ): Promise<T> {
94
+ try {
95
+ return await apiCall();
96
+ } catch (error) {
97
+ if (!isAuthenticationError(error)) {
98
+ throw error;
99
+ }
100
+
101
+ if (options?.syncSession && options?.activeSessionId && options?.oxyServices) {
102
+ try {
103
+ await options.syncSession();
104
+ await options.oxyServices.getTokenBySession(options.activeSessionId);
105
+ return await apiCall();
106
+ } catch {
107
+ throw new AuthenticationFailedError();
108
+ }
109
+ }
110
+
111
+ throw new AuthenticationFailedError();
112
+ }
113
+ }
114
+
115
+ /**
116
+ * Combines token validation and auth error handling for a complete authenticated API call.
117
+ *
118
+ * @example
119
+ * ```ts
120
+ * return await authenticatedApiCall(
121
+ * oxyServices,
122
+ * activeSessionId,
123
+ * () => oxyServices.updateProfile(updates)
124
+ * );
125
+ * ```
126
+ */
127
+ export async function authenticatedApiCall<T>(
128
+ oxyServices: OxyServices,
129
+ activeSessionId: string | null | undefined,
130
+ apiCall: () => Promise<T>,
131
+ syncSession?: () => Promise<unknown>
132
+ ): Promise<T> {
133
+ await ensureValidToken(oxyServices, activeSessionId);
134
+
135
+ return withAuthErrorHandling(apiCall, {
136
+ syncSession,
137
+ activeSessionId,
138
+ oxyServices,
139
+ });
140
+ }