@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.
@@ -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
- * Simple Express.js authentication middleware
51
- *
52
- * Built-in authentication middleware that validates JWT tokens and adds user data to requests.
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
- * // Basic usage - just add it to your routes
57
- * app.use('/api/protected', oxyServices.auth());
58
- *
59
- * // With debug logging
60
- * app.use('/api/protected', oxyServices.auth({ debug: true }));
61
- *
62
- * // With custom error handling
63
- * app.use('/api/protected', oxyServices.auth({
64
- * onError: (error) => console.error('Auth failed:', error)
65
- * }));
66
- *
67
- * // Load full user data
68
- * app.use('/api/protected', oxyServices.auth({ loadUser: true }));
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?: boolean;
80
- onError?: (error: ApiError) => any;
81
- loadUser?: boolean;
82
- session?: boolean;
83
- } = {}) {
84
- const { debug = false, onError, loadUser = false, session = false } = options;
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
- const token = authHeader?.startsWith('Bearer ') ? authHeader.substring(7) : null;
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(`🔐 Auth: Processing ${req.method} ${req.path}`);
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 and validate token
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: 403
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(403).json(error);
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: 403
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(403).json(error);
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: 403
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(403).json(error);
183
+ return res.status(401).json(error);
158
184
  }
159
-
160
- // For now, skip session validation to keep it simple
161
- // Session validation can be added later if needed
162
-
163
- // Set request properties immediately
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
- // Automatically authenticate the OxyServices instance so all subsequent API calls are authenticated
169
- this.setTokens(token);
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(`✅ Auth: Authentication successful for user ${userId}`);
276
+ console.log(`[oxy.auth] OK user=${userId} (no session)`);
173
277
  }
174
-
278
+
175
279
  next();
176
280
  } catch (error) {
177
- const apiError = this.handleError(error) as any;
178
-
281
+ const apiError = oxyInstance.handleError(error) as any;
282
+
179
283
  if (debug) {
180
- // Debug logging - using console.log is acceptable here for development
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