@oxyhq/core 1.2.1 → 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
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Utility Methods Mixin
|
|
3
|
-
*
|
|
3
|
+
*
|
|
4
4
|
* Provides utility methods including link metadata fetching
|
|
5
5
|
* and Express.js authentication middleware
|
|
6
6
|
*/
|
|
@@ -17,6 +17,20 @@ interface JwtPayload {
|
|
|
17
17
|
[key: string]: any;
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
+
/**
|
|
21
|
+
* Options for oxyClient.auth() middleware
|
|
22
|
+
*/
|
|
23
|
+
interface AuthMiddlewareOptions {
|
|
24
|
+
/** Enable debug logging (default: false) */
|
|
25
|
+
debug?: boolean;
|
|
26
|
+
/** Custom error handler - receives error object, can return response */
|
|
27
|
+
onError?: (error: ApiError) => any;
|
|
28
|
+
/** Load full user profile from API (default: false for performance) */
|
|
29
|
+
loadUser?: boolean;
|
|
30
|
+
/** Optional auth - attach user if token present but don't block (default: false) */
|
|
31
|
+
optional?: boolean;
|
|
32
|
+
}
|
|
33
|
+
|
|
20
34
|
export function OxyServicesUtilityMixin<T extends typeof OxyServicesBase>(Base: T) {
|
|
21
35
|
return class extends Base {
|
|
22
36
|
constructor(...args: any[]) {
|
|
@@ -47,145 +61,323 @@ export function OxyServicesUtilityMixin<T extends typeof OxyServicesBase>(Base:
|
|
|
47
61
|
}
|
|
48
62
|
|
|
49
63
|
/**
|
|
50
|
-
*
|
|
51
|
-
*
|
|
52
|
-
*
|
|
53
|
-
*
|
|
64
|
+
* Express.js authentication middleware
|
|
65
|
+
*
|
|
66
|
+
* Validates JWT tokens against the Oxy API and attaches user data to requests.
|
|
67
|
+
* Uses server-side session validation for security (not just JWT decode).
|
|
68
|
+
*
|
|
54
69
|
* @example
|
|
55
70
|
* ```typescript
|
|
56
|
-
*
|
|
57
|
-
*
|
|
58
|
-
*
|
|
59
|
-
*
|
|
60
|
-
*
|
|
61
|
-
*
|
|
62
|
-
*
|
|
63
|
-
*
|
|
64
|
-
*
|
|
65
|
-
* })
|
|
66
|
-
*
|
|
67
|
-
*
|
|
68
|
-
*
|
|
71
|
+
* import { OxyServices } from '@oxyhq/core';
|
|
72
|
+
*
|
|
73
|
+
* const oxy = new OxyServices({ baseURL: 'https://api.oxy.so' });
|
|
74
|
+
*
|
|
75
|
+
* // Protect all routes under /api/protected
|
|
76
|
+
* app.use('/api/protected', oxy.auth());
|
|
77
|
+
*
|
|
78
|
+
* // Access user in route handler
|
|
79
|
+
* app.get('/api/protected/me', (req, res) => {
|
|
80
|
+
* res.json({ userId: req.userId, user: req.user });
|
|
81
|
+
* });
|
|
82
|
+
*
|
|
83
|
+
* // Load full user profile from API
|
|
84
|
+
* app.use('/api/admin', oxy.auth({ loadUser: true }));
|
|
85
|
+
*
|
|
86
|
+
* // Optional auth - attach user if present, don't block if absent
|
|
87
|
+
* app.use('/api/public', oxy.auth({ optional: true }));
|
|
69
88
|
* ```
|
|
70
|
-
*
|
|
89
|
+
*
|
|
71
90
|
* @param options Optional configuration
|
|
72
|
-
* @param options.debug Enable debug logging (default: false)
|
|
73
|
-
* @param options.onError Custom error handler
|
|
74
|
-
* @param options.loadUser Load full user data (default: false for performance)
|
|
75
|
-
* @param options.session Use session-based validation (default: false)
|
|
76
91
|
* @returns Express middleware function
|
|
77
92
|
*/
|
|
78
|
-
auth(options: {
|
|
79
|
-
debug
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
// Return a synchronous middleware function
|
|
87
|
-
return (req: any, res: any, next: any) => {
|
|
93
|
+
auth(options: AuthMiddlewareOptions = {}) {
|
|
94
|
+
const { debug = false, onError, loadUser = false, optional = false } = options;
|
|
95
|
+
// Cast to any for cross-mixin method access (Auth mixin methods available at runtime)
|
|
96
|
+
const oxyInstance = this as any;
|
|
97
|
+
|
|
98
|
+
// Return an async middleware function
|
|
99
|
+
return async (req: any, res: any, next: any) => {
|
|
88
100
|
try {
|
|
89
|
-
// Extract token from Authorization header
|
|
101
|
+
// Extract token from Authorization header or query params
|
|
90
102
|
const authHeader = req.headers['authorization'];
|
|
91
|
-
|
|
92
|
-
|
|
103
|
+
let token = authHeader?.startsWith('Bearer ') ? authHeader.substring(7) : null;
|
|
104
|
+
|
|
105
|
+
// Fallback to query params (useful for WebSocket upgrades)
|
|
106
|
+
if (!token) {
|
|
107
|
+
const q = req.query || {};
|
|
108
|
+
if (typeof q.token === 'string' && q.token) token = q.token;
|
|
109
|
+
else if (typeof q.access_token === 'string' && q.access_token) token = q.access_token;
|
|
110
|
+
}
|
|
111
|
+
|
|
93
112
|
if (debug) {
|
|
94
|
-
console.log(
|
|
95
|
-
console.log(`🔐 Auth: Token present: ${!!token}`);
|
|
113
|
+
console.log(`[oxy.auth] ${req.method} ${req.path} | token: ${!!token}`);
|
|
96
114
|
}
|
|
97
|
-
|
|
115
|
+
|
|
98
116
|
if (!token) {
|
|
117
|
+
if (optional) {
|
|
118
|
+
req.userId = null;
|
|
119
|
+
req.user = null;
|
|
120
|
+
return next();
|
|
121
|
+
}
|
|
122
|
+
|
|
99
123
|
const error = {
|
|
100
124
|
message: 'Access token required',
|
|
101
125
|
code: 'MISSING_TOKEN',
|
|
102
126
|
status: 401
|
|
103
127
|
};
|
|
104
|
-
|
|
105
|
-
if (debug) console.log(`❌ Auth: Missing token`);
|
|
106
|
-
|
|
107
128
|
if (onError) return onError(error);
|
|
108
129
|
return res.status(401).json(error);
|
|
109
130
|
}
|
|
110
|
-
|
|
111
|
-
// Decode
|
|
131
|
+
|
|
132
|
+
// Decode token to extract claims
|
|
112
133
|
let decoded: JwtPayload;
|
|
113
134
|
try {
|
|
114
135
|
decoded = jwtDecode<JwtPayload>(token);
|
|
115
|
-
|
|
116
|
-
if (debug) {
|
|
117
|
-
console.log(`🔐 Auth: Token decoded, User ID: ${decoded.userId || decoded.id}`);
|
|
118
|
-
}
|
|
119
136
|
} catch (decodeError) {
|
|
137
|
+
if (optional) {
|
|
138
|
+
req.userId = null;
|
|
139
|
+
req.user = null;
|
|
140
|
+
return next();
|
|
141
|
+
}
|
|
142
|
+
|
|
120
143
|
const error = {
|
|
121
144
|
message: 'Invalid token format',
|
|
122
145
|
code: 'INVALID_TOKEN_FORMAT',
|
|
123
|
-
status:
|
|
146
|
+
status: 401
|
|
124
147
|
};
|
|
125
|
-
|
|
126
|
-
if (debug) console.log(`❌ Auth: Token decode failed`);
|
|
127
|
-
|
|
128
148
|
if (onError) return onError(error);
|
|
129
|
-
return res.status(
|
|
149
|
+
return res.status(401).json(error);
|
|
130
150
|
}
|
|
131
|
-
|
|
151
|
+
|
|
132
152
|
const userId = decoded.userId || decoded.id;
|
|
133
153
|
if (!userId) {
|
|
154
|
+
if (optional) {
|
|
155
|
+
req.userId = null;
|
|
156
|
+
req.user = null;
|
|
157
|
+
return next();
|
|
158
|
+
}
|
|
159
|
+
|
|
134
160
|
const error = {
|
|
135
161
|
message: 'Token missing user ID',
|
|
136
162
|
code: 'INVALID_TOKEN_PAYLOAD',
|
|
137
|
-
status:
|
|
163
|
+
status: 401
|
|
138
164
|
};
|
|
139
|
-
|
|
140
|
-
if (debug) console.log(`❌ Auth: Token missing user ID`);
|
|
141
|
-
|
|
142
165
|
if (onError) return onError(error);
|
|
143
|
-
return res.status(
|
|
166
|
+
return res.status(401).json(error);
|
|
144
167
|
}
|
|
145
|
-
|
|
146
|
-
// Check token expiration
|
|
168
|
+
|
|
169
|
+
// Check token expiration locally first (fast path)
|
|
147
170
|
if (decoded.exp && decoded.exp < Math.floor(Date.now() / 1000)) {
|
|
171
|
+
if (optional) {
|
|
172
|
+
req.userId = null;
|
|
173
|
+
req.user = null;
|
|
174
|
+
return next();
|
|
175
|
+
}
|
|
176
|
+
|
|
148
177
|
const error = {
|
|
149
178
|
message: 'Token expired',
|
|
150
179
|
code: 'TOKEN_EXPIRED',
|
|
151
|
-
status:
|
|
180
|
+
status: 401
|
|
152
181
|
};
|
|
153
|
-
|
|
154
|
-
if (debug) console.log(`❌ Auth: Token expired`);
|
|
155
|
-
|
|
156
182
|
if (onError) return onError(error);
|
|
157
|
-
return res.status(
|
|
183
|
+
return res.status(401).json(error);
|
|
158
184
|
}
|
|
159
|
-
|
|
160
|
-
//
|
|
161
|
-
//
|
|
162
|
-
|
|
163
|
-
|
|
185
|
+
|
|
186
|
+
// Validate token against the Oxy API for session-based verification
|
|
187
|
+
// This ensures the session hasn't been revoked server-side
|
|
188
|
+
if (decoded.sessionId) {
|
|
189
|
+
try {
|
|
190
|
+
const validationResult = await oxyInstance.validateSession(decoded.sessionId, {
|
|
191
|
+
useHeaderValidation: true,
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
if (!validationResult || !validationResult.valid) {
|
|
195
|
+
if (optional) {
|
|
196
|
+
req.userId = null;
|
|
197
|
+
req.user = null;
|
|
198
|
+
return next();
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
const error = {
|
|
202
|
+
message: 'Session invalid or expired',
|
|
203
|
+
code: 'INVALID_SESSION',
|
|
204
|
+
status: 401
|
|
205
|
+
};
|
|
206
|
+
if (onError) return onError(error);
|
|
207
|
+
return res.status(401).json(error);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// Use validated user data from session validation (already has full user)
|
|
211
|
+
req.userId = userId;
|
|
212
|
+
req.accessToken = token;
|
|
213
|
+
req.sessionId = decoded.sessionId;
|
|
214
|
+
|
|
215
|
+
if (loadUser && validationResult.user) {
|
|
216
|
+
// Session validation already returns full user data
|
|
217
|
+
req.user = validationResult.user;
|
|
218
|
+
} else {
|
|
219
|
+
req.user = { id: userId } as User;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
if (debug) {
|
|
223
|
+
console.log(`[oxy.auth] OK user=${userId} session=${decoded.sessionId}`);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
return next();
|
|
227
|
+
} catch (validationError) {
|
|
228
|
+
if (debug) {
|
|
229
|
+
console.log(`[oxy.auth] Session validation failed:`, validationError);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
if (optional) {
|
|
233
|
+
req.userId = null;
|
|
234
|
+
req.user = null;
|
|
235
|
+
return next();
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
const error = {
|
|
239
|
+
message: 'Session validation failed',
|
|
240
|
+
code: 'SESSION_VALIDATION_ERROR',
|
|
241
|
+
status: 401
|
|
242
|
+
};
|
|
243
|
+
if (onError) return onError(error);
|
|
244
|
+
return res.status(401).json(error);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// Non-session token: use local validation only (userId from JWT)
|
|
164
249
|
req.userId = userId;
|
|
165
250
|
req.accessToken = token;
|
|
166
251
|
req.user = { id: userId } as User;
|
|
167
|
-
|
|
168
|
-
//
|
|
169
|
-
|
|
170
|
-
|
|
252
|
+
|
|
253
|
+
// If loadUser requested with non-session token, fetch from API
|
|
254
|
+
if (loadUser) {
|
|
255
|
+
try {
|
|
256
|
+
// Temporarily set token to make the API call
|
|
257
|
+
const prevToken = oxyInstance.getAccessToken();
|
|
258
|
+
oxyInstance.setTokens(token);
|
|
259
|
+
const fullUser = await oxyInstance.getCurrentUser();
|
|
260
|
+
// Restore previous token
|
|
261
|
+
if (prevToken) {
|
|
262
|
+
oxyInstance.setTokens(prevToken);
|
|
263
|
+
} else {
|
|
264
|
+
oxyInstance.clearTokens();
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
if (fullUser) {
|
|
268
|
+
req.user = fullUser;
|
|
269
|
+
}
|
|
270
|
+
} catch {
|
|
271
|
+
// Failed to load user, continue with basic user object
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
171
275
|
if (debug) {
|
|
172
|
-
console.log(
|
|
276
|
+
console.log(`[oxy.auth] OK user=${userId} (no session)`);
|
|
173
277
|
}
|
|
174
|
-
|
|
278
|
+
|
|
175
279
|
next();
|
|
176
280
|
} catch (error) {
|
|
177
|
-
const apiError =
|
|
178
|
-
|
|
281
|
+
const apiError = oxyInstance.handleError(error) as any;
|
|
282
|
+
|
|
179
283
|
if (debug) {
|
|
180
|
-
|
|
181
|
-
console.log(`❌ Auth: Unexpected error:`, apiError);
|
|
284
|
+
console.log(`[oxy.auth] Error:`, apiError);
|
|
182
285
|
}
|
|
183
|
-
|
|
286
|
+
|
|
184
287
|
if (onError) return onError(apiError);
|
|
185
288
|
return res.status((apiError && apiError.status) || 500).json(apiError);
|
|
186
289
|
}
|
|
187
290
|
};
|
|
188
291
|
}
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* Socket.IO authentication middleware factory
|
|
295
|
+
*
|
|
296
|
+
* Returns a middleware function for Socket.IO that validates JWT tokens
|
|
297
|
+
* from the handshake auth object and attaches user data to the socket.
|
|
298
|
+
*
|
|
299
|
+
* @example
|
|
300
|
+
* ```typescript
|
|
301
|
+
* import { OxyServices } from '@oxyhq/core';
|
|
302
|
+
* import { Server } from 'socket.io';
|
|
303
|
+
*
|
|
304
|
+
* const oxy = new OxyServices({ baseURL: 'https://api.oxy.so' });
|
|
305
|
+
* const io = new Server(server);
|
|
306
|
+
*
|
|
307
|
+
* // Authenticate all socket connections
|
|
308
|
+
* io.use(oxy.authSocket());
|
|
309
|
+
*
|
|
310
|
+
* io.on('connection', (socket) => {
|
|
311
|
+
* console.log('Authenticated user:', socket.data.userId);
|
|
312
|
+
* });
|
|
313
|
+
* ```
|
|
314
|
+
*/
|
|
315
|
+
authSocket(options: { debug?: boolean } = {}) {
|
|
316
|
+
const { debug = false } = options;
|
|
317
|
+
// Cast to any for cross-mixin method access (Auth mixin methods available at runtime)
|
|
318
|
+
const oxyInstance = this as any;
|
|
319
|
+
|
|
320
|
+
return async (socket: any, next: (err?: Error) => void) => {
|
|
321
|
+
try {
|
|
322
|
+
const token = socket.handshake?.auth?.token;
|
|
323
|
+
|
|
324
|
+
if (!token) {
|
|
325
|
+
return next(new Error('Authentication required'));
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
let decoded: JwtPayload;
|
|
329
|
+
try {
|
|
330
|
+
decoded = jwtDecode<JwtPayload>(token);
|
|
331
|
+
} catch {
|
|
332
|
+
return next(new Error('Invalid token'));
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
const userId = decoded.userId || decoded.id;
|
|
336
|
+
if (!userId) {
|
|
337
|
+
return next(new Error('Invalid token payload'));
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
// Check expiration
|
|
341
|
+
if (decoded.exp && decoded.exp < Math.floor(Date.now() / 1000)) {
|
|
342
|
+
return next(new Error('Token expired'));
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
// Validate session if available
|
|
346
|
+
if (decoded.sessionId) {
|
|
347
|
+
try {
|
|
348
|
+
const result = await oxyInstance.validateSession(decoded.sessionId, {
|
|
349
|
+
useHeaderValidation: true,
|
|
350
|
+
});
|
|
351
|
+
if (!result || !result.valid) {
|
|
352
|
+
return next(new Error('Session invalid'));
|
|
353
|
+
}
|
|
354
|
+
} catch {
|
|
355
|
+
return next(new Error('Session validation failed'));
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
// Attach user data to socket
|
|
360
|
+
socket.data = socket.data || {};
|
|
361
|
+
socket.data.userId = userId;
|
|
362
|
+
socket.data.sessionId = decoded.sessionId || null;
|
|
363
|
+
socket.data.token = token;
|
|
364
|
+
|
|
365
|
+
// Also set on socket.user for backward compatibility
|
|
366
|
+
socket.user = { id: userId, userId, sessionId: decoded.sessionId };
|
|
367
|
+
|
|
368
|
+
if (debug) {
|
|
369
|
+
console.log(`[oxy.authSocket] OK user=${userId}`);
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
next();
|
|
373
|
+
} catch (err) {
|
|
374
|
+
if (debug) {
|
|
375
|
+
console.log(`[oxy.authSocket] Error:`, err);
|
|
376
|
+
}
|
|
377
|
+
next(new Error('Authentication error'));
|
|
378
|
+
}
|
|
379
|
+
};
|
|
380
|
+
}
|
|
189
381
|
};
|
|
190
382
|
}
|
|
191
383
|
|