@oxyhq/core 1.2.2 → 1.2.3
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 +39 -6
- package/dist/cjs/mixins/OxyServices.utility.js +225 -51
- package/dist/esm/AuthManager.js +39 -6
- package/dist/esm/mixins/OxyServices.utility.js +225 -51
- package/dist/types/OxyServices.d.ts +9 -0
- package/dist/types/mixins/OxyServices.utility.d.ts +56 -21
- package/package.json +1 -1
- package/src/AuthManager.ts +42 -7
- package/src/OxyServices.ts +13 -0
- package/src/mixins/OxyServices.utility.ts +274 -82
package/dist/cjs/AuthManager.js
CHANGED
|
@@ -221,20 +221,53 @@ class AuthManager {
|
|
|
221
221
|
}
|
|
222
222
|
}
|
|
223
223
|
async _doRefreshToken() {
|
|
224
|
-
|
|
225
|
-
|
|
224
|
+
// Get session info to find sessionId for token refresh
|
|
225
|
+
const sessionJson = await this.storage.getItem(STORAGE_KEYS.SESSION);
|
|
226
|
+
if (!sessionJson) {
|
|
227
|
+
return false;
|
|
228
|
+
}
|
|
229
|
+
let sessionId;
|
|
230
|
+
try {
|
|
231
|
+
const session = JSON.parse(sessionJson);
|
|
232
|
+
sessionId = session.sessionId;
|
|
233
|
+
if (!sessionId)
|
|
234
|
+
return false;
|
|
235
|
+
}
|
|
236
|
+
catch (err) {
|
|
237
|
+
console.error('AuthManager: Failed to parse session from storage.', err);
|
|
226
238
|
return false;
|
|
227
239
|
}
|
|
228
240
|
try {
|
|
229
241
|
await (0, asyncUtils_1.retryAsync)(async () => {
|
|
230
242
|
const httpService = this.oxyServices.httpService;
|
|
243
|
+
// Use session-based token endpoint which handles auto-refresh server-side
|
|
231
244
|
const response = await httpService.request({
|
|
232
|
-
method: '
|
|
233
|
-
url:
|
|
234
|
-
data: { refreshToken },
|
|
245
|
+
method: 'GET',
|
|
246
|
+
url: `/api/session/token/${sessionId}`,
|
|
235
247
|
cache: false,
|
|
248
|
+
retry: false,
|
|
236
249
|
});
|
|
237
|
-
|
|
250
|
+
if (!response.accessToken) {
|
|
251
|
+
throw new Error('No access token in refresh response');
|
|
252
|
+
}
|
|
253
|
+
// Update access token in storage and HTTP client
|
|
254
|
+
await this.storage.setItem(STORAGE_KEYS.ACCESS_TOKEN, response.accessToken);
|
|
255
|
+
this.oxyServices.httpService.setTokens(response.accessToken);
|
|
256
|
+
// Update session expiry and schedule next refresh
|
|
257
|
+
if (response.expiresAt) {
|
|
258
|
+
try {
|
|
259
|
+
const session = JSON.parse(sessionJson);
|
|
260
|
+
session.expiresAt = response.expiresAt;
|
|
261
|
+
await this.storage.setItem(STORAGE_KEYS.SESSION, JSON.stringify(session));
|
|
262
|
+
}
|
|
263
|
+
catch (err) {
|
|
264
|
+
// Ignore parse errors for session update, but log for debugging.
|
|
265
|
+
console.error('AuthManager: Failed to re-save session after token refresh.', err);
|
|
266
|
+
}
|
|
267
|
+
if (this.config.autoRefresh) {
|
|
268
|
+
this.setupTokenRefresh(response.expiresAt);
|
|
269
|
+
}
|
|
270
|
+
}
|
|
238
271
|
}, 2, // 2 retries = 3 total attempts
|
|
239
272
|
1000, // 1s base delay with exponential backoff + jitter
|
|
240
273
|
(error) => {
|
|
@@ -29,122 +29,215 @@ function OxyServicesUtilityMixin(Base) {
|
|
|
29
29
|
}
|
|
30
30
|
}
|
|
31
31
|
/**
|
|
32
|
-
*
|
|
32
|
+
* Express.js authentication middleware
|
|
33
33
|
*
|
|
34
|
-
*
|
|
34
|
+
* Validates JWT tokens against the Oxy API and attaches user data to requests.
|
|
35
|
+
* Uses server-side session validation for security (not just JWT decode).
|
|
35
36
|
*
|
|
36
37
|
* @example
|
|
37
38
|
* ```typescript
|
|
38
|
-
*
|
|
39
|
-
* app.use('/api/protected', oxyServices.auth());
|
|
39
|
+
* import { OxyServices } from '@oxyhq/core';
|
|
40
40
|
*
|
|
41
|
-
*
|
|
42
|
-
* app.use('/api/protected', oxyServices.auth({ debug: true }));
|
|
41
|
+
* const oxy = new OxyServices({ baseURL: 'https://api.oxy.so' });
|
|
43
42
|
*
|
|
44
|
-
* //
|
|
45
|
-
* app.use('/api/protected',
|
|
46
|
-
* onError: (error) => console.error('Auth failed:', error)
|
|
47
|
-
* }));
|
|
43
|
+
* // Protect all routes under /api/protected
|
|
44
|
+
* app.use('/api/protected', oxy.auth());
|
|
48
45
|
*
|
|
49
|
-
* //
|
|
50
|
-
* app.
|
|
46
|
+
* // Access user in route handler
|
|
47
|
+
* app.get('/api/protected/me', (req, res) => {
|
|
48
|
+
* res.json({ userId: req.userId, user: req.user });
|
|
49
|
+
* });
|
|
50
|
+
*
|
|
51
|
+
* // Load full user profile from API
|
|
52
|
+
* app.use('/api/admin', oxy.auth({ loadUser: true }));
|
|
53
|
+
*
|
|
54
|
+
* // Optional auth - attach user if present, don't block if absent
|
|
55
|
+
* app.use('/api/public', oxy.auth({ optional: true }));
|
|
51
56
|
* ```
|
|
52
57
|
*
|
|
53
58
|
* @param options Optional configuration
|
|
54
|
-
* @param options.debug Enable debug logging (default: false)
|
|
55
|
-
* @param options.onError Custom error handler
|
|
56
|
-
* @param options.loadUser Load full user data (default: false for performance)
|
|
57
|
-
* @param options.session Use session-based validation (default: false)
|
|
58
59
|
* @returns Express middleware function
|
|
59
60
|
*/
|
|
60
61
|
auth(options = {}) {
|
|
61
|
-
const { debug = false, onError, loadUser = false,
|
|
62
|
-
//
|
|
63
|
-
|
|
62
|
+
const { debug = false, onError, loadUser = false, optional = false } = options;
|
|
63
|
+
// Cast to any for cross-mixin method access (Auth mixin methods available at runtime)
|
|
64
|
+
const oxyInstance = this;
|
|
65
|
+
// Return an async middleware function
|
|
66
|
+
return async (req, res, next) => {
|
|
64
67
|
try {
|
|
65
|
-
// Extract token from Authorization header
|
|
68
|
+
// Extract token from Authorization header or query params
|
|
66
69
|
const authHeader = req.headers['authorization'];
|
|
67
|
-
|
|
70
|
+
let token = authHeader?.startsWith('Bearer ') ? authHeader.substring(7) : null;
|
|
71
|
+
// Fallback to query params (useful for WebSocket upgrades)
|
|
72
|
+
if (!token) {
|
|
73
|
+
const q = req.query || {};
|
|
74
|
+
if (typeof q.token === 'string' && q.token)
|
|
75
|
+
token = q.token;
|
|
76
|
+
else if (typeof q.access_token === 'string' && q.access_token)
|
|
77
|
+
token = q.access_token;
|
|
78
|
+
}
|
|
68
79
|
if (debug) {
|
|
69
|
-
console.log(
|
|
70
|
-
console.log(`🔐 Auth: Token present: ${!!token}`);
|
|
80
|
+
console.log(`[oxy.auth] ${req.method} ${req.path} | token: ${!!token}`);
|
|
71
81
|
}
|
|
72
82
|
if (!token) {
|
|
83
|
+
if (optional) {
|
|
84
|
+
req.userId = null;
|
|
85
|
+
req.user = null;
|
|
86
|
+
return next();
|
|
87
|
+
}
|
|
73
88
|
const error = {
|
|
74
89
|
message: 'Access token required',
|
|
75
90
|
code: 'MISSING_TOKEN',
|
|
76
91
|
status: 401
|
|
77
92
|
};
|
|
78
|
-
if (debug)
|
|
79
|
-
console.log(`❌ Auth: Missing token`);
|
|
80
93
|
if (onError)
|
|
81
94
|
return onError(error);
|
|
82
95
|
return res.status(401).json(error);
|
|
83
96
|
}
|
|
84
|
-
// Decode
|
|
97
|
+
// Decode token to extract claims
|
|
85
98
|
let decoded;
|
|
86
99
|
try {
|
|
87
100
|
decoded = (0, jwt_decode_1.jwtDecode)(token);
|
|
88
|
-
if (debug) {
|
|
89
|
-
console.log(`🔐 Auth: Token decoded, User ID: ${decoded.userId || decoded.id}`);
|
|
90
|
-
}
|
|
91
101
|
}
|
|
92
102
|
catch (decodeError) {
|
|
103
|
+
if (optional) {
|
|
104
|
+
req.userId = null;
|
|
105
|
+
req.user = null;
|
|
106
|
+
return next();
|
|
107
|
+
}
|
|
93
108
|
const error = {
|
|
94
109
|
message: 'Invalid token format',
|
|
95
110
|
code: 'INVALID_TOKEN_FORMAT',
|
|
96
|
-
status:
|
|
111
|
+
status: 401
|
|
97
112
|
};
|
|
98
|
-
if (debug)
|
|
99
|
-
console.log(`❌ Auth: Token decode failed`);
|
|
100
113
|
if (onError)
|
|
101
114
|
return onError(error);
|
|
102
|
-
return res.status(
|
|
115
|
+
return res.status(401).json(error);
|
|
103
116
|
}
|
|
104
117
|
const userId = decoded.userId || decoded.id;
|
|
105
118
|
if (!userId) {
|
|
119
|
+
if (optional) {
|
|
120
|
+
req.userId = null;
|
|
121
|
+
req.user = null;
|
|
122
|
+
return next();
|
|
123
|
+
}
|
|
106
124
|
const error = {
|
|
107
125
|
message: 'Token missing user ID',
|
|
108
126
|
code: 'INVALID_TOKEN_PAYLOAD',
|
|
109
|
-
status:
|
|
127
|
+
status: 401
|
|
110
128
|
};
|
|
111
|
-
if (debug)
|
|
112
|
-
console.log(`❌ Auth: Token missing user ID`);
|
|
113
129
|
if (onError)
|
|
114
130
|
return onError(error);
|
|
115
|
-
return res.status(
|
|
131
|
+
return res.status(401).json(error);
|
|
116
132
|
}
|
|
117
|
-
// Check token expiration
|
|
133
|
+
// Check token expiration locally first (fast path)
|
|
118
134
|
if (decoded.exp && decoded.exp < Math.floor(Date.now() / 1000)) {
|
|
135
|
+
if (optional) {
|
|
136
|
+
req.userId = null;
|
|
137
|
+
req.user = null;
|
|
138
|
+
return next();
|
|
139
|
+
}
|
|
119
140
|
const error = {
|
|
120
141
|
message: 'Token expired',
|
|
121
142
|
code: 'TOKEN_EXPIRED',
|
|
122
|
-
status:
|
|
143
|
+
status: 401
|
|
123
144
|
};
|
|
124
|
-
if (debug)
|
|
125
|
-
console.log(`❌ Auth: Token expired`);
|
|
126
145
|
if (onError)
|
|
127
146
|
return onError(error);
|
|
128
|
-
return res.status(
|
|
147
|
+
return res.status(401).json(error);
|
|
148
|
+
}
|
|
149
|
+
// Validate token against the Oxy API for session-based verification
|
|
150
|
+
// This ensures the session hasn't been revoked server-side
|
|
151
|
+
if (decoded.sessionId) {
|
|
152
|
+
try {
|
|
153
|
+
const validationResult = await oxyInstance.validateSession(decoded.sessionId, {
|
|
154
|
+
useHeaderValidation: true,
|
|
155
|
+
});
|
|
156
|
+
if (!validationResult || !validationResult.valid) {
|
|
157
|
+
if (optional) {
|
|
158
|
+
req.userId = null;
|
|
159
|
+
req.user = null;
|
|
160
|
+
return next();
|
|
161
|
+
}
|
|
162
|
+
const error = {
|
|
163
|
+
message: 'Session invalid or expired',
|
|
164
|
+
code: 'INVALID_SESSION',
|
|
165
|
+
status: 401
|
|
166
|
+
};
|
|
167
|
+
if (onError)
|
|
168
|
+
return onError(error);
|
|
169
|
+
return res.status(401).json(error);
|
|
170
|
+
}
|
|
171
|
+
// Use validated user data from session validation (already has full user)
|
|
172
|
+
req.userId = userId;
|
|
173
|
+
req.accessToken = token;
|
|
174
|
+
req.sessionId = decoded.sessionId;
|
|
175
|
+
if (loadUser && validationResult.user) {
|
|
176
|
+
// Session validation already returns full user data
|
|
177
|
+
req.user = validationResult.user;
|
|
178
|
+
}
|
|
179
|
+
else {
|
|
180
|
+
req.user = { id: userId };
|
|
181
|
+
}
|
|
182
|
+
if (debug) {
|
|
183
|
+
console.log(`[oxy.auth] OK user=${userId} session=${decoded.sessionId}`);
|
|
184
|
+
}
|
|
185
|
+
return next();
|
|
186
|
+
}
|
|
187
|
+
catch (validationError) {
|
|
188
|
+
if (debug) {
|
|
189
|
+
console.log(`[oxy.auth] Session validation failed:`, validationError);
|
|
190
|
+
}
|
|
191
|
+
if (optional) {
|
|
192
|
+
req.userId = null;
|
|
193
|
+
req.user = null;
|
|
194
|
+
return next();
|
|
195
|
+
}
|
|
196
|
+
const error = {
|
|
197
|
+
message: 'Session validation failed',
|
|
198
|
+
code: 'SESSION_VALIDATION_ERROR',
|
|
199
|
+
status: 401
|
|
200
|
+
};
|
|
201
|
+
if (onError)
|
|
202
|
+
return onError(error);
|
|
203
|
+
return res.status(401).json(error);
|
|
204
|
+
}
|
|
129
205
|
}
|
|
130
|
-
//
|
|
131
|
-
// Session validation can be added later if needed
|
|
132
|
-
// Set request properties immediately
|
|
206
|
+
// Non-session token: use local validation only (userId from JWT)
|
|
133
207
|
req.userId = userId;
|
|
134
208
|
req.accessToken = token;
|
|
135
209
|
req.user = { id: userId };
|
|
136
|
-
//
|
|
137
|
-
|
|
210
|
+
// If loadUser requested with non-session token, fetch from API
|
|
211
|
+
if (loadUser) {
|
|
212
|
+
try {
|
|
213
|
+
// Temporarily set token to make the API call
|
|
214
|
+
const prevToken = oxyInstance.getAccessToken();
|
|
215
|
+
oxyInstance.setTokens(token);
|
|
216
|
+
const fullUser = await oxyInstance.getCurrentUser();
|
|
217
|
+
// Restore previous token
|
|
218
|
+
if (prevToken) {
|
|
219
|
+
oxyInstance.setTokens(prevToken);
|
|
220
|
+
}
|
|
221
|
+
else {
|
|
222
|
+
oxyInstance.clearTokens();
|
|
223
|
+
}
|
|
224
|
+
if (fullUser) {
|
|
225
|
+
req.user = fullUser;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
catch {
|
|
229
|
+
// Failed to load user, continue with basic user object
|
|
230
|
+
}
|
|
231
|
+
}
|
|
138
232
|
if (debug) {
|
|
139
|
-
console.log(
|
|
233
|
+
console.log(`[oxy.auth] OK user=${userId} (no session)`);
|
|
140
234
|
}
|
|
141
235
|
next();
|
|
142
236
|
}
|
|
143
237
|
catch (error) {
|
|
144
|
-
const apiError =
|
|
238
|
+
const apiError = oxyInstance.handleError(error);
|
|
145
239
|
if (debug) {
|
|
146
|
-
|
|
147
|
-
console.log(`❌ Auth: Unexpected error:`, apiError);
|
|
240
|
+
console.log(`[oxy.auth] Error:`, apiError);
|
|
148
241
|
}
|
|
149
242
|
if (onError)
|
|
150
243
|
return onError(apiError);
|
|
@@ -152,5 +245,86 @@ function OxyServicesUtilityMixin(Base) {
|
|
|
152
245
|
}
|
|
153
246
|
};
|
|
154
247
|
}
|
|
248
|
+
/**
|
|
249
|
+
* Socket.IO authentication middleware factory
|
|
250
|
+
*
|
|
251
|
+
* Returns a middleware function for Socket.IO that validates JWT tokens
|
|
252
|
+
* from the handshake auth object and attaches user data to the socket.
|
|
253
|
+
*
|
|
254
|
+
* @example
|
|
255
|
+
* ```typescript
|
|
256
|
+
* import { OxyServices } from '@oxyhq/core';
|
|
257
|
+
* import { Server } from 'socket.io';
|
|
258
|
+
*
|
|
259
|
+
* const oxy = new OxyServices({ baseURL: 'https://api.oxy.so' });
|
|
260
|
+
* const io = new Server(server);
|
|
261
|
+
*
|
|
262
|
+
* // Authenticate all socket connections
|
|
263
|
+
* io.use(oxy.authSocket());
|
|
264
|
+
*
|
|
265
|
+
* io.on('connection', (socket) => {
|
|
266
|
+
* console.log('Authenticated user:', socket.data.userId);
|
|
267
|
+
* });
|
|
268
|
+
* ```
|
|
269
|
+
*/
|
|
270
|
+
authSocket(options = {}) {
|
|
271
|
+
const { debug = false } = options;
|
|
272
|
+
// Cast to any for cross-mixin method access (Auth mixin methods available at runtime)
|
|
273
|
+
const oxyInstance = this;
|
|
274
|
+
return async (socket, next) => {
|
|
275
|
+
try {
|
|
276
|
+
const token = socket.handshake?.auth?.token;
|
|
277
|
+
if (!token) {
|
|
278
|
+
return next(new Error('Authentication required'));
|
|
279
|
+
}
|
|
280
|
+
let decoded;
|
|
281
|
+
try {
|
|
282
|
+
decoded = (0, jwt_decode_1.jwtDecode)(token);
|
|
283
|
+
}
|
|
284
|
+
catch {
|
|
285
|
+
return next(new Error('Invalid token'));
|
|
286
|
+
}
|
|
287
|
+
const userId = decoded.userId || decoded.id;
|
|
288
|
+
if (!userId) {
|
|
289
|
+
return next(new Error('Invalid token payload'));
|
|
290
|
+
}
|
|
291
|
+
// Check expiration
|
|
292
|
+
if (decoded.exp && decoded.exp < Math.floor(Date.now() / 1000)) {
|
|
293
|
+
return next(new Error('Token expired'));
|
|
294
|
+
}
|
|
295
|
+
// Validate session if available
|
|
296
|
+
if (decoded.sessionId) {
|
|
297
|
+
try {
|
|
298
|
+
const result = await oxyInstance.validateSession(decoded.sessionId, {
|
|
299
|
+
useHeaderValidation: true,
|
|
300
|
+
});
|
|
301
|
+
if (!result || !result.valid) {
|
|
302
|
+
return next(new Error('Session invalid'));
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
catch {
|
|
306
|
+
return next(new Error('Session validation failed'));
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
// Attach user data to socket
|
|
310
|
+
socket.data = socket.data || {};
|
|
311
|
+
socket.data.userId = userId;
|
|
312
|
+
socket.data.sessionId = decoded.sessionId || null;
|
|
313
|
+
socket.data.token = token;
|
|
314
|
+
// Also set on socket.user for backward compatibility
|
|
315
|
+
socket.user = { id: userId, userId, sessionId: decoded.sessionId };
|
|
316
|
+
if (debug) {
|
|
317
|
+
console.log(`[oxy.authSocket] OK user=${userId}`);
|
|
318
|
+
}
|
|
319
|
+
next();
|
|
320
|
+
}
|
|
321
|
+
catch (err) {
|
|
322
|
+
if (debug) {
|
|
323
|
+
console.log(`[oxy.authSocket] Error:`, err);
|
|
324
|
+
}
|
|
325
|
+
next(new Error('Authentication error'));
|
|
326
|
+
}
|
|
327
|
+
};
|
|
328
|
+
}
|
|
155
329
|
};
|
|
156
330
|
}
|
package/dist/esm/AuthManager.js
CHANGED
|
@@ -217,20 +217,53 @@ export class AuthManager {
|
|
|
217
217
|
}
|
|
218
218
|
}
|
|
219
219
|
async _doRefreshToken() {
|
|
220
|
-
|
|
221
|
-
|
|
220
|
+
// Get session info to find sessionId for token refresh
|
|
221
|
+
const sessionJson = await this.storage.getItem(STORAGE_KEYS.SESSION);
|
|
222
|
+
if (!sessionJson) {
|
|
223
|
+
return false;
|
|
224
|
+
}
|
|
225
|
+
let sessionId;
|
|
226
|
+
try {
|
|
227
|
+
const session = JSON.parse(sessionJson);
|
|
228
|
+
sessionId = session.sessionId;
|
|
229
|
+
if (!sessionId)
|
|
230
|
+
return false;
|
|
231
|
+
}
|
|
232
|
+
catch (err) {
|
|
233
|
+
console.error('AuthManager: Failed to parse session from storage.', err);
|
|
222
234
|
return false;
|
|
223
235
|
}
|
|
224
236
|
try {
|
|
225
237
|
await retryAsync(async () => {
|
|
226
238
|
const httpService = this.oxyServices.httpService;
|
|
239
|
+
// Use session-based token endpoint which handles auto-refresh server-side
|
|
227
240
|
const response = await httpService.request({
|
|
228
|
-
method: '
|
|
229
|
-
url:
|
|
230
|
-
data: { refreshToken },
|
|
241
|
+
method: 'GET',
|
|
242
|
+
url: `/api/session/token/${sessionId}`,
|
|
231
243
|
cache: false,
|
|
244
|
+
retry: false,
|
|
232
245
|
});
|
|
233
|
-
|
|
246
|
+
if (!response.accessToken) {
|
|
247
|
+
throw new Error('No access token in refresh response');
|
|
248
|
+
}
|
|
249
|
+
// Update access token in storage and HTTP client
|
|
250
|
+
await this.storage.setItem(STORAGE_KEYS.ACCESS_TOKEN, response.accessToken);
|
|
251
|
+
this.oxyServices.httpService.setTokens(response.accessToken);
|
|
252
|
+
// Update session expiry and schedule next refresh
|
|
253
|
+
if (response.expiresAt) {
|
|
254
|
+
try {
|
|
255
|
+
const session = JSON.parse(sessionJson);
|
|
256
|
+
session.expiresAt = response.expiresAt;
|
|
257
|
+
await this.storage.setItem(STORAGE_KEYS.SESSION, JSON.stringify(session));
|
|
258
|
+
}
|
|
259
|
+
catch (err) {
|
|
260
|
+
// Ignore parse errors for session update, but log for debugging.
|
|
261
|
+
console.error('AuthManager: Failed to re-save session after token refresh.', err);
|
|
262
|
+
}
|
|
263
|
+
if (this.config.autoRefresh) {
|
|
264
|
+
this.setupTokenRefresh(response.expiresAt);
|
|
265
|
+
}
|
|
266
|
+
}
|
|
234
267
|
}, 2, // 2 retries = 3 total attempts
|
|
235
268
|
1000, // 1s base delay with exponential backoff + jitter
|
|
236
269
|
(error) => {
|