@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.
@@ -221,20 +221,53 @@ class AuthManager {
221
221
  }
222
222
  }
223
223
  async _doRefreshToken() {
224
- const refreshToken = await this.storage.getItem(STORAGE_KEYS.REFRESH_TOKEN);
225
- if (!refreshToken) {
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: 'POST',
233
- url: '/api/auth/refresh',
234
- data: { refreshToken },
245
+ method: 'GET',
246
+ url: `/api/session/token/${sessionId}`,
235
247
  cache: false,
248
+ retry: false,
236
249
  });
237
- await this.handleAuthSuccess(response, 'credentials');
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
- * Simple Express.js authentication middleware
32
+ * Express.js authentication middleware
33
33
  *
34
- * Built-in authentication middleware that validates JWT tokens and adds user data to requests.
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
- * // Basic usage - just add it to your routes
39
- * app.use('/api/protected', oxyServices.auth());
39
+ * import { OxyServices } from '@oxyhq/core';
40
40
  *
41
- * // With debug logging
42
- * app.use('/api/protected', oxyServices.auth({ debug: true }));
41
+ * const oxy = new OxyServices({ baseURL: 'https://api.oxy.so' });
43
42
  *
44
- * // With custom error handling
45
- * app.use('/api/protected', oxyServices.auth({
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
- * // Load full user data
50
- * app.use('/api/protected', oxyServices.auth({ loadUser: true }));
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, session = false } = options;
62
- // Return a synchronous middleware function
63
- return (req, res, next) => {
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
- const token = authHeader?.startsWith('Bearer ') ? authHeader.substring(7) : null;
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(`🔐 Auth: Processing ${req.method} ${req.path}`);
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 and validate token
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: 403
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(403).json(error);
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: 403
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(403).json(error);
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: 403
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(403).json(error);
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
- // For now, skip session validation to keep it simple
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
- // Automatically authenticate the OxyServices instance so all subsequent API calls are authenticated
137
- this.setTokens(token);
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(`✅ Auth: Authentication successful for user ${userId}`);
233
+ console.log(`[oxy.auth] OK user=${userId} (no session)`);
140
234
  }
141
235
  next();
142
236
  }
143
237
  catch (error) {
144
- const apiError = this.handleError(error);
238
+ const apiError = oxyInstance.handleError(error);
145
239
  if (debug) {
146
- // Debug logging - using console.log is acceptable here for development
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
  }
@@ -217,20 +217,53 @@ export class AuthManager {
217
217
  }
218
218
  }
219
219
  async _doRefreshToken() {
220
- const refreshToken = await this.storage.getItem(STORAGE_KEYS.REFRESH_TOKEN);
221
- if (!refreshToken) {
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: 'POST',
229
- url: '/api/auth/refresh',
230
- data: { refreshToken },
241
+ method: 'GET',
242
+ url: `/api/session/token/${sessionId}`,
231
243
  cache: false,
244
+ retry: false,
232
245
  });
233
- await this.handleAuthSuccess(response, 'credentials');
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) => {