@oxyhq/core 1.3.0 → 1.5.0
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/mixins/OxyServices.auth.js +2 -0
- package/dist/cjs/mixins/OxyServices.utility.js +95 -2
- package/dist/cjs/utils/asyncUtils.js +0 -1
- package/dist/esm/mixins/OxyServices.auth.js +2 -0
- package/dist/esm/mixins/OxyServices.utility.js +62 -2
- package/dist/esm/utils/asyncUtils.js +0 -1
- package/dist/types/mixins/OxyServices.utility.d.ts +6 -0
- package/package.json +1 -1
- package/src/mixins/OxyServices.auth.ts +8 -6
- package/src/mixins/OxyServices.utility.ts +68 -2
- package/src/utils/asyncUtils.ts +1 -2
|
@@ -251,6 +251,8 @@ function OxyServicesAuthMixin(Base) {
|
|
|
251
251
|
return await this.makeRequest('GET', `/api/session/validate/${sessionId}`, urlParams, { cache: false });
|
|
252
252
|
}
|
|
253
253
|
catch (error) {
|
|
254
|
+
// Session is invalid — clear any cached user data for this session (#196)
|
|
255
|
+
this.clearCacheEntry(`GET:/api/session/user/${sessionId}`);
|
|
254
256
|
throw this.handleError(error);
|
|
255
257
|
}
|
|
256
258
|
}
|
|
@@ -1,4 +1,37 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
2
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
36
|
exports.OxyServicesUtilityMixin = OxyServicesUtilityMixin;
|
|
4
37
|
/**
|
|
@@ -59,7 +92,7 @@ function OxyServicesUtilityMixin(Base) {
|
|
|
59
92
|
* @returns Express middleware function
|
|
60
93
|
*/
|
|
61
94
|
auth(options = {}) {
|
|
62
|
-
const { debug = false, onError, loadUser = false, optional = false } = options;
|
|
95
|
+
const { debug = false, onError, loadUser = false, optional = false, jwtSecret } = options;
|
|
63
96
|
// Cast to any for cross-mixin method access (Auth mixin methods available at runtime)
|
|
64
97
|
const oxyInstance = this;
|
|
65
98
|
// Return an async middleware function
|
|
@@ -115,8 +148,56 @@ function OxyServicesUtilityMixin(Base) {
|
|
|
115
148
|
return res.status(401).json(error);
|
|
116
149
|
}
|
|
117
150
|
// Handle service tokens (internal service-to-service auth)
|
|
118
|
-
// Service tokens are stateless JWTs with type: 'service' —
|
|
151
|
+
// Service tokens are stateless JWTs with type: 'service' — requires signature verification
|
|
119
152
|
if (decoded.type === 'service') {
|
|
153
|
+
// Service tokens MUST be cryptographically verified — reject if no secret provided
|
|
154
|
+
if (!jwtSecret) {
|
|
155
|
+
if (optional) {
|
|
156
|
+
req.userId = null;
|
|
157
|
+
req.user = null;
|
|
158
|
+
return next();
|
|
159
|
+
}
|
|
160
|
+
const error = {
|
|
161
|
+
message: 'Service token verification not configured',
|
|
162
|
+
code: 'SERVICE_TOKEN_NOT_CONFIGURED',
|
|
163
|
+
status: 403
|
|
164
|
+
};
|
|
165
|
+
if (onError)
|
|
166
|
+
return onError(error);
|
|
167
|
+
return res.status(403).json(error);
|
|
168
|
+
}
|
|
169
|
+
// Verify JWT signature (not just decode)
|
|
170
|
+
try {
|
|
171
|
+
const { createHmac } = await Promise.resolve().then(() => __importStar(require('crypto')));
|
|
172
|
+
const [headerB64, payloadB64, signatureB64] = token.split('.');
|
|
173
|
+
if (!headerB64 || !payloadB64 || !signatureB64) {
|
|
174
|
+
throw new Error('Invalid token structure');
|
|
175
|
+
}
|
|
176
|
+
const expectedSig = createHmac('sha256', jwtSecret)
|
|
177
|
+
.update(`${headerB64}.${payloadB64}`)
|
|
178
|
+
.digest('base64')
|
|
179
|
+
.replace(/\+/g, '-')
|
|
180
|
+
.replace(/\//g, '_')
|
|
181
|
+
.replace(/=/g, '');
|
|
182
|
+
// Timing-safe comparison
|
|
183
|
+
const sigBuf = Buffer.from(signatureB64);
|
|
184
|
+
const expectedBuf = Buffer.from(expectedSig);
|
|
185
|
+
const { timingSafeEqual } = await Promise.resolve().then(() => __importStar(require('crypto')));
|
|
186
|
+
if (sigBuf.length !== expectedBuf.length || !timingSafeEqual(sigBuf, expectedBuf)) {
|
|
187
|
+
throw new Error('Invalid signature');
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
catch {
|
|
191
|
+
if (optional) {
|
|
192
|
+
req.userId = null;
|
|
193
|
+
req.user = null;
|
|
194
|
+
return next();
|
|
195
|
+
}
|
|
196
|
+
const error = { message: 'Invalid service token signature', code: 'INVALID_SERVICE_TOKEN', status: 401 };
|
|
197
|
+
if (onError)
|
|
198
|
+
return onError(error);
|
|
199
|
+
return res.status(401).json(error);
|
|
200
|
+
}
|
|
120
201
|
// Check expiration
|
|
121
202
|
if (decoded.exp && decoded.exp < Math.floor(Date.now() / 1000)) {
|
|
122
203
|
if (optional) {
|
|
@@ -129,6 +210,18 @@ function OxyServicesUtilityMixin(Base) {
|
|
|
129
210
|
return onError(error);
|
|
130
211
|
return res.status(401).json(error);
|
|
131
212
|
}
|
|
213
|
+
// Validate required service token fields
|
|
214
|
+
if (!decoded.appId) {
|
|
215
|
+
if (optional) {
|
|
216
|
+
req.userId = null;
|
|
217
|
+
req.user = null;
|
|
218
|
+
return next();
|
|
219
|
+
}
|
|
220
|
+
const error = { message: 'Invalid service token: missing appId', code: 'INVALID_SERVICE_TOKEN', status: 401 };
|
|
221
|
+
if (onError)
|
|
222
|
+
return onError(error);
|
|
223
|
+
return res.status(401).json(error);
|
|
224
|
+
}
|
|
132
225
|
// Read delegated user ID from header
|
|
133
226
|
const oxyUserId = req.headers['x-oxy-user-id'];
|
|
134
227
|
req.userId = oxyUserId || null;
|
|
@@ -85,7 +85,6 @@ async function retryAsync(operation, maxRetries = 3, baseDelay = 1000, shouldRet
|
|
|
85
85
|
*/
|
|
86
86
|
function debounceAsync(func, delay) {
|
|
87
87
|
let timeoutId;
|
|
88
|
-
const lastPromise = null;
|
|
89
88
|
return (...args) => {
|
|
90
89
|
return new Promise((resolve, reject) => {
|
|
91
90
|
clearTimeout(timeoutId);
|
|
@@ -248,6 +248,8 @@ export function OxyServicesAuthMixin(Base) {
|
|
|
248
248
|
return await this.makeRequest('GET', `/api/session/validate/${sessionId}`, urlParams, { cache: false });
|
|
249
249
|
}
|
|
250
250
|
catch (error) {
|
|
251
|
+
// Session is invalid — clear any cached user data for this session (#196)
|
|
252
|
+
this.clearCacheEntry(`GET:/api/session/user/${sessionId}`);
|
|
251
253
|
throw this.handleError(error);
|
|
252
254
|
}
|
|
253
255
|
}
|
|
@@ -56,7 +56,7 @@ export function OxyServicesUtilityMixin(Base) {
|
|
|
56
56
|
* @returns Express middleware function
|
|
57
57
|
*/
|
|
58
58
|
auth(options = {}) {
|
|
59
|
-
const { debug = false, onError, loadUser = false, optional = false } = options;
|
|
59
|
+
const { debug = false, onError, loadUser = false, optional = false, jwtSecret } = options;
|
|
60
60
|
// Cast to any for cross-mixin method access (Auth mixin methods available at runtime)
|
|
61
61
|
const oxyInstance = this;
|
|
62
62
|
// Return an async middleware function
|
|
@@ -112,8 +112,56 @@ export function OxyServicesUtilityMixin(Base) {
|
|
|
112
112
|
return res.status(401).json(error);
|
|
113
113
|
}
|
|
114
114
|
// Handle service tokens (internal service-to-service auth)
|
|
115
|
-
// Service tokens are stateless JWTs with type: 'service' —
|
|
115
|
+
// Service tokens are stateless JWTs with type: 'service' — requires signature verification
|
|
116
116
|
if (decoded.type === 'service') {
|
|
117
|
+
// Service tokens MUST be cryptographically verified — reject if no secret provided
|
|
118
|
+
if (!jwtSecret) {
|
|
119
|
+
if (optional) {
|
|
120
|
+
req.userId = null;
|
|
121
|
+
req.user = null;
|
|
122
|
+
return next();
|
|
123
|
+
}
|
|
124
|
+
const error = {
|
|
125
|
+
message: 'Service token verification not configured',
|
|
126
|
+
code: 'SERVICE_TOKEN_NOT_CONFIGURED',
|
|
127
|
+
status: 403
|
|
128
|
+
};
|
|
129
|
+
if (onError)
|
|
130
|
+
return onError(error);
|
|
131
|
+
return res.status(403).json(error);
|
|
132
|
+
}
|
|
133
|
+
// Verify JWT signature (not just decode)
|
|
134
|
+
try {
|
|
135
|
+
const { createHmac } = await import('crypto');
|
|
136
|
+
const [headerB64, payloadB64, signatureB64] = token.split('.');
|
|
137
|
+
if (!headerB64 || !payloadB64 || !signatureB64) {
|
|
138
|
+
throw new Error('Invalid token structure');
|
|
139
|
+
}
|
|
140
|
+
const expectedSig = createHmac('sha256', jwtSecret)
|
|
141
|
+
.update(`${headerB64}.${payloadB64}`)
|
|
142
|
+
.digest('base64')
|
|
143
|
+
.replace(/\+/g, '-')
|
|
144
|
+
.replace(/\//g, '_')
|
|
145
|
+
.replace(/=/g, '');
|
|
146
|
+
// Timing-safe comparison
|
|
147
|
+
const sigBuf = Buffer.from(signatureB64);
|
|
148
|
+
const expectedBuf = Buffer.from(expectedSig);
|
|
149
|
+
const { timingSafeEqual } = await import('crypto');
|
|
150
|
+
if (sigBuf.length !== expectedBuf.length || !timingSafeEqual(sigBuf, expectedBuf)) {
|
|
151
|
+
throw new Error('Invalid signature');
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
catch {
|
|
155
|
+
if (optional) {
|
|
156
|
+
req.userId = null;
|
|
157
|
+
req.user = null;
|
|
158
|
+
return next();
|
|
159
|
+
}
|
|
160
|
+
const error = { message: 'Invalid service token signature', code: 'INVALID_SERVICE_TOKEN', status: 401 };
|
|
161
|
+
if (onError)
|
|
162
|
+
return onError(error);
|
|
163
|
+
return res.status(401).json(error);
|
|
164
|
+
}
|
|
117
165
|
// Check expiration
|
|
118
166
|
if (decoded.exp && decoded.exp < Math.floor(Date.now() / 1000)) {
|
|
119
167
|
if (optional) {
|
|
@@ -126,6 +174,18 @@ export function OxyServicesUtilityMixin(Base) {
|
|
|
126
174
|
return onError(error);
|
|
127
175
|
return res.status(401).json(error);
|
|
128
176
|
}
|
|
177
|
+
// Validate required service token fields
|
|
178
|
+
if (!decoded.appId) {
|
|
179
|
+
if (optional) {
|
|
180
|
+
req.userId = null;
|
|
181
|
+
req.user = null;
|
|
182
|
+
return next();
|
|
183
|
+
}
|
|
184
|
+
const error = { message: 'Invalid service token: missing appId', code: 'INVALID_SERVICE_TOKEN', status: 401 };
|
|
185
|
+
if (onError)
|
|
186
|
+
return onError(error);
|
|
187
|
+
return res.status(401).json(error);
|
|
188
|
+
}
|
|
129
189
|
// Read delegated user ID from header
|
|
130
190
|
const oxyUserId = req.headers['x-oxy-user-id'];
|
|
131
191
|
req.userId = oxyUserId || null;
|
|
@@ -71,7 +71,6 @@ export async function retryAsync(operation, maxRetries = 3, baseDelay = 1000, sh
|
|
|
71
71
|
*/
|
|
72
72
|
export function debounceAsync(func, delay) {
|
|
73
73
|
let timeoutId;
|
|
74
|
-
const lastPromise = null;
|
|
75
74
|
return (...args) => {
|
|
76
75
|
return new Promise((resolve, reject) => {
|
|
77
76
|
clearTimeout(timeoutId);
|
|
@@ -19,6 +19,12 @@ interface AuthMiddlewareOptions {
|
|
|
19
19
|
loadUser?: boolean;
|
|
20
20
|
/** Optional auth - attach user if token present but don't block (default: false) */
|
|
21
21
|
optional?: boolean;
|
|
22
|
+
/**
|
|
23
|
+
* JWT secret for verifying service token signatures locally.
|
|
24
|
+
* When provided, service tokens will be cryptographically verified.
|
|
25
|
+
* When omitted, service tokens will be rejected (secure default).
|
|
26
|
+
*/
|
|
27
|
+
jwtSecret?: string;
|
|
22
28
|
}
|
|
23
29
|
export declare function OxyServicesUtilityMixin<T extends typeof OxyServicesBase>(Base: T): {
|
|
24
30
|
new (...args: any[]): {
|
package/package.json
CHANGED
|
@@ -341,25 +341,27 @@ export function OxyServicesAuthMixin<T extends typeof OxyServicesBase>(Base: T)
|
|
|
341
341
|
* Validate session
|
|
342
342
|
*/
|
|
343
343
|
async validateSession(
|
|
344
|
-
sessionId: string,
|
|
344
|
+
sessionId: string,
|
|
345
345
|
options: {
|
|
346
346
|
deviceFingerprint?: string;
|
|
347
347
|
useHeaderValidation?: boolean;
|
|
348
348
|
} = {}
|
|
349
|
-
): Promise<{
|
|
350
|
-
valid: boolean;
|
|
351
|
-
expiresAt: string;
|
|
352
|
-
lastActivity: string;
|
|
349
|
+
): Promise<{
|
|
350
|
+
valid: boolean;
|
|
351
|
+
expiresAt: string;
|
|
352
|
+
lastActivity: string;
|
|
353
353
|
user: User;
|
|
354
354
|
sessionId?: string;
|
|
355
355
|
source?: string;
|
|
356
356
|
}> {
|
|
357
357
|
try {
|
|
358
|
-
const urlParams:
|
|
358
|
+
const urlParams: Record<string, string> = {};
|
|
359
359
|
if (options.deviceFingerprint) urlParams.deviceFingerprint = options.deviceFingerprint;
|
|
360
360
|
if (options.useHeaderValidation) urlParams.useHeaderValidation = 'true';
|
|
361
361
|
return await this.makeRequest('GET', `/api/session/validate/${sessionId}`, urlParams, { cache: false });
|
|
362
362
|
} catch (error) {
|
|
363
|
+
// Session is invalid — clear any cached user data for this session (#196)
|
|
364
|
+
this.clearCacheEntry(`GET:/api/session/user/${sessionId}`);
|
|
363
365
|
throw this.handleError(error);
|
|
364
366
|
}
|
|
365
367
|
}
|
|
@@ -40,6 +40,12 @@ interface AuthMiddlewareOptions {
|
|
|
40
40
|
loadUser?: boolean;
|
|
41
41
|
/** Optional auth - attach user if token present but don't block (default: false) */
|
|
42
42
|
optional?: boolean;
|
|
43
|
+
/**
|
|
44
|
+
* JWT secret for verifying service token signatures locally.
|
|
45
|
+
* When provided, service tokens will be cryptographically verified.
|
|
46
|
+
* When omitted, service tokens will be rejected (secure default).
|
|
47
|
+
*/
|
|
48
|
+
jwtSecret?: string;
|
|
43
49
|
}
|
|
44
50
|
|
|
45
51
|
export function OxyServicesUtilityMixin<T extends typeof OxyServicesBase>(Base: T) {
|
|
@@ -102,7 +108,7 @@ export function OxyServicesUtilityMixin<T extends typeof OxyServicesBase>(Base:
|
|
|
102
108
|
* @returns Express middleware function
|
|
103
109
|
*/
|
|
104
110
|
auth(options: AuthMiddlewareOptions = {}) {
|
|
105
|
-
const { debug = false, onError, loadUser = false, optional = false } = options;
|
|
111
|
+
const { debug = false, onError, loadUser = false, optional = false, jwtSecret } = options;
|
|
106
112
|
// Cast to any for cross-mixin method access (Auth mixin methods available at runtime)
|
|
107
113
|
const oxyInstance = this as any;
|
|
108
114
|
|
|
@@ -161,8 +167,56 @@ export function OxyServicesUtilityMixin<T extends typeof OxyServicesBase>(Base:
|
|
|
161
167
|
}
|
|
162
168
|
|
|
163
169
|
// Handle service tokens (internal service-to-service auth)
|
|
164
|
-
// Service tokens are stateless JWTs with type: 'service' —
|
|
170
|
+
// Service tokens are stateless JWTs with type: 'service' — requires signature verification
|
|
165
171
|
if (decoded.type === 'service') {
|
|
172
|
+
// Service tokens MUST be cryptographically verified — reject if no secret provided
|
|
173
|
+
if (!jwtSecret) {
|
|
174
|
+
if (optional) {
|
|
175
|
+
req.userId = null;
|
|
176
|
+
req.user = null;
|
|
177
|
+
return next();
|
|
178
|
+
}
|
|
179
|
+
const error = {
|
|
180
|
+
message: 'Service token verification not configured',
|
|
181
|
+
code: 'SERVICE_TOKEN_NOT_CONFIGURED',
|
|
182
|
+
status: 403
|
|
183
|
+
};
|
|
184
|
+
if (onError) return onError(error);
|
|
185
|
+
return res.status(403).json(error);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// Verify JWT signature (not just decode)
|
|
189
|
+
try {
|
|
190
|
+
const { createHmac } = await import('crypto');
|
|
191
|
+
const [headerB64, payloadB64, signatureB64] = token.split('.');
|
|
192
|
+
if (!headerB64 || !payloadB64 || !signatureB64) {
|
|
193
|
+
throw new Error('Invalid token structure');
|
|
194
|
+
}
|
|
195
|
+
const expectedSig = createHmac('sha256', jwtSecret)
|
|
196
|
+
.update(`${headerB64}.${payloadB64}`)
|
|
197
|
+
.digest('base64')
|
|
198
|
+
.replace(/\+/g, '-')
|
|
199
|
+
.replace(/\//g, '_')
|
|
200
|
+
.replace(/=/g, '');
|
|
201
|
+
|
|
202
|
+
// Timing-safe comparison
|
|
203
|
+
const sigBuf = Buffer.from(signatureB64);
|
|
204
|
+
const expectedBuf = Buffer.from(expectedSig);
|
|
205
|
+
const { timingSafeEqual } = await import('crypto');
|
|
206
|
+
if (sigBuf.length !== expectedBuf.length || !timingSafeEqual(sigBuf, expectedBuf)) {
|
|
207
|
+
throw new Error('Invalid signature');
|
|
208
|
+
}
|
|
209
|
+
} catch {
|
|
210
|
+
if (optional) {
|
|
211
|
+
req.userId = null;
|
|
212
|
+
req.user = null;
|
|
213
|
+
return next();
|
|
214
|
+
}
|
|
215
|
+
const error = { message: 'Invalid service token signature', code: 'INVALID_SERVICE_TOKEN', status: 401 };
|
|
216
|
+
if (onError) return onError(error);
|
|
217
|
+
return res.status(401).json(error);
|
|
218
|
+
}
|
|
219
|
+
|
|
166
220
|
// Check expiration
|
|
167
221
|
if (decoded.exp && decoded.exp < Math.floor(Date.now() / 1000)) {
|
|
168
222
|
if (optional) {
|
|
@@ -175,6 +229,18 @@ export function OxyServicesUtilityMixin<T extends typeof OxyServicesBase>(Base:
|
|
|
175
229
|
return res.status(401).json(error);
|
|
176
230
|
}
|
|
177
231
|
|
|
232
|
+
// Validate required service token fields
|
|
233
|
+
if (!decoded.appId) {
|
|
234
|
+
if (optional) {
|
|
235
|
+
req.userId = null;
|
|
236
|
+
req.user = null;
|
|
237
|
+
return next();
|
|
238
|
+
}
|
|
239
|
+
const error = { message: 'Invalid service token: missing appId', code: 'INVALID_SERVICE_TOKEN', status: 401 };
|
|
240
|
+
if (onError) return onError(error);
|
|
241
|
+
return res.status(401).json(error);
|
|
242
|
+
}
|
|
243
|
+
|
|
178
244
|
// Read delegated user ID from header
|
|
179
245
|
const oxyUserId = req.headers['x-oxy-user-id'] as string;
|
|
180
246
|
|
package/src/utils/asyncUtils.ts
CHANGED
|
@@ -103,8 +103,7 @@ export function debounceAsync<T extends (...args: any[]) => Promise<any>>(
|
|
|
103
103
|
delay: number
|
|
104
104
|
): (...args: Parameters<T>) => Promise<ReturnType<T>> {
|
|
105
105
|
let timeoutId: ReturnType<typeof setTimeout>;
|
|
106
|
-
|
|
107
|
-
|
|
106
|
+
|
|
108
107
|
return (...args: Parameters<T>): Promise<ReturnType<T>> => {
|
|
109
108
|
return new Promise((resolve, reject) => {
|
|
110
109
|
clearTimeout(timeoutId);
|