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