@oxyhq/core 1.3.0 → 1.4.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.
@@ -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' — no session validation needed
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;
@@ -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' — no session validation needed
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;
@@ -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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oxyhq/core",
3
- "version": "1.3.0",
3
+ "version": "1.4.0",
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",
@@ -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' — no session validation needed
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