@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
|
@@ -26,122 +26,215 @@ export function OxyServicesUtilityMixin(Base) {
|
|
|
26
26
|
}
|
|
27
27
|
}
|
|
28
28
|
/**
|
|
29
|
-
*
|
|
29
|
+
* Express.js authentication middleware
|
|
30
30
|
*
|
|
31
|
-
*
|
|
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
|
-
*
|
|
36
|
-
* app.use('/api/protected', oxyServices.auth());
|
|
36
|
+
* import { OxyServices } from '@oxyhq/core';
|
|
37
37
|
*
|
|
38
|
-
*
|
|
39
|
-
* app.use('/api/protected', oxyServices.auth({ debug: true }));
|
|
38
|
+
* const oxy = new OxyServices({ baseURL: 'https://api.oxy.so' });
|
|
40
39
|
*
|
|
41
|
-
* //
|
|
42
|
-
* app.use('/api/protected',
|
|
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
|
-
* //
|
|
47
|
-
* app.
|
|
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,
|
|
59
|
-
//
|
|
60
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
|
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:
|
|
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(
|
|
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:
|
|
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(
|
|
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:
|
|
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(
|
|
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
|
-
//
|
|
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
|
-
//
|
|
134
|
-
|
|
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(
|
|
230
|
+
console.log(`[oxy.auth] OK user=${userId} (no session)`);
|
|
137
231
|
}
|
|
138
232
|
next();
|
|
139
233
|
}
|
|
140
234
|
catch (error) {
|
|
141
|
-
const apiError =
|
|
235
|
+
const apiError = oxyInstance.handleError(error);
|
|
142
236
|
if (debug) {
|
|
143
|
-
|
|
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
|
-
*
|
|
28
|
+
* Express.js authentication middleware
|
|
16
29
|
*
|
|
17
|
-
*
|
|
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
|
-
*
|
|
22
|
-
* app.use('/api/protected', oxyServices.auth());
|
|
35
|
+
* import { OxyServices } from '@oxyhq/core';
|
|
23
36
|
*
|
|
24
|
-
*
|
|
25
|
-
* app.use('/api/protected', oxyServices.auth({ debug: true }));
|
|
37
|
+
* const oxy = new OxyServices({ baseURL: 'https://api.oxy.so' });
|
|
26
38
|
*
|
|
27
|
-
* //
|
|
28
|
-
* app.use('/api/protected',
|
|
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
|
-
* //
|
|
33
|
-
* app.
|
|
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
|
-
|
|
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
package/src/AuthManager.ts
CHANGED
|
@@ -280,8 +280,19 @@ export class AuthManager {
|
|
|
280
280
|
}
|
|
281
281
|
|
|
282
282
|
private async _doRefreshToken(): Promise<boolean> {
|
|
283
|
-
|
|
284
|
-
|
|
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
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
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
|
-
|
|
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
|
package/src/OxyServices.ts
CHANGED
|
@@ -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
|