@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.
- package/dist/cjs/AuthManager.js +13 -0
- package/dist/cjs/index.js +9 -1
- package/dist/cjs/mixins/OxyServices.utility.js +24 -4
- package/dist/cjs/utils/authHelpers.js +114 -0
- package/dist/esm/AuthManager.js +13 -0
- package/dist/esm/index.js +2 -0
- package/dist/esm/mixins/OxyServices.utility.js +24 -4
- package/dist/esm/utils/authHelpers.js +105 -0
- package/dist/types/index.d.ts +2 -0
- package/dist/types/mixins/OxyServices.utility.d.ts +13 -0
- package/dist/types/utils/authHelpers.d.ts +57 -0
- package/package.json +1 -1
- package/src/AuthManager.ts +13 -0
- package/src/index.ts +11 -0
- package/src/mixins/OxyServices.utility.ts +24 -4
- package/src/utils/authHelpers.ts +140 -0
package/dist/cjs/AuthManager.js
CHANGED
|
@@ -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
|
+
}
|
package/dist/esm/AuthManager.js
CHANGED
|
@@ -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
|
+
}
|
package/dist/types/index.d.ts
CHANGED
|
@@ -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
package/src/AuthManager.ts
CHANGED
|
@@ -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
|
+
}
|