@sylphx/lens-server 2.3.2 → 2.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.
- package/dist/index.d.ts +241 -23
- package/dist/index.js +363 -23
- package/package.json +1 -1
- package/src/handlers/framework.ts +17 -4
- package/src/handlers/http.test.ts +227 -2
- package/src/handlers/http.ts +223 -22
- package/src/handlers/index.ts +2 -0
- package/src/handlers/ws-types.ts +39 -0
- package/src/handlers/ws.test.ts +559 -0
- package/src/handlers/ws.ts +99 -0
- package/src/index.ts +21 -0
- package/src/logging/index.ts +20 -0
- package/src/logging/structured-logger.test.ts +367 -0
- package/src/logging/structured-logger.ts +335 -0
- package/src/server/create.test.ts +198 -0
- package/src/server/create.ts +78 -9
- package/src/server/types.ts +1 -1
|
@@ -139,12 +139,12 @@ describe("createHTTPHandler", () => {
|
|
|
139
139
|
expect(data.data.id).toBe("456");
|
|
140
140
|
});
|
|
141
141
|
|
|
142
|
-
it("handles CORS preflight", async () => {
|
|
142
|
+
it("handles CORS preflight in development mode", async () => {
|
|
143
143
|
const app = createApp({
|
|
144
144
|
queries: { getUser },
|
|
145
145
|
mutations: { createUser },
|
|
146
146
|
});
|
|
147
|
-
const handler = createHTTPHandler(app);
|
|
147
|
+
const handler = createHTTPHandler(app, { errors: { development: true } });
|
|
148
148
|
|
|
149
149
|
const request = new Request("http://localhost/", {
|
|
150
150
|
method: "OPTIONS",
|
|
@@ -155,6 +155,39 @@ describe("createHTTPHandler", () => {
|
|
|
155
155
|
expect(response.headers.get("Access-Control-Allow-Origin")).toBe("*");
|
|
156
156
|
});
|
|
157
157
|
|
|
158
|
+
it("does not allow CORS by default in production mode", async () => {
|
|
159
|
+
const app = createApp({
|
|
160
|
+
queries: { getUser },
|
|
161
|
+
mutations: { createUser },
|
|
162
|
+
});
|
|
163
|
+
const handler = createHTTPHandler(app); // Production mode is default
|
|
164
|
+
|
|
165
|
+
const request = new Request("http://localhost/", {
|
|
166
|
+
method: "OPTIONS",
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
const response = await handler(request);
|
|
170
|
+
expect(response.status).toBe(204);
|
|
171
|
+
// No Access-Control-Allow-Origin header in production without explicit config
|
|
172
|
+
expect(response.headers.get("Access-Control-Allow-Origin")).toBeNull();
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
it("allows CORS with explicit origin configuration", async () => {
|
|
176
|
+
const app = createApp({
|
|
177
|
+
queries: { getUser },
|
|
178
|
+
mutations: { createUser },
|
|
179
|
+
});
|
|
180
|
+
const handler = createHTTPHandler(app, { cors: { origin: "https://example.com" } });
|
|
181
|
+
|
|
182
|
+
const request = new Request("http://localhost/", {
|
|
183
|
+
method: "OPTIONS",
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
const response = await handler(request);
|
|
187
|
+
expect(response.status).toBe(204);
|
|
188
|
+
expect(response.headers.get("Access-Control-Allow-Origin")).toBe("https://example.com");
|
|
189
|
+
});
|
|
190
|
+
|
|
158
191
|
it("returns 404 for unknown paths", async () => {
|
|
159
192
|
const app = createApp({
|
|
160
193
|
queries: { getUser },
|
|
@@ -212,4 +245,196 @@ describe("createHTTPHandler", () => {
|
|
|
212
245
|
const data = await response.json();
|
|
213
246
|
expect(data.error).toContain("not found");
|
|
214
247
|
});
|
|
248
|
+
|
|
249
|
+
describe("health check", () => {
|
|
250
|
+
it("returns health status on GET /__lens/health", async () => {
|
|
251
|
+
const app = createApp({
|
|
252
|
+
queries: { getUser },
|
|
253
|
+
mutations: { createUser },
|
|
254
|
+
});
|
|
255
|
+
const handler = createHTTPHandler(app);
|
|
256
|
+
|
|
257
|
+
const request = new Request("http://localhost/__lens/health", {
|
|
258
|
+
method: "GET",
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
const response = await handler(request);
|
|
262
|
+
expect(response.status).toBe(200);
|
|
263
|
+
|
|
264
|
+
const data = await response.json();
|
|
265
|
+
expect(data.status).toBe("healthy");
|
|
266
|
+
expect(data.service).toBe("lens-server");
|
|
267
|
+
expect(data.version).toBeDefined();
|
|
268
|
+
expect(data.uptime).toBeGreaterThanOrEqual(0);
|
|
269
|
+
expect(data.timestamp).toBeDefined();
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
it("supports path prefix for health endpoint", async () => {
|
|
273
|
+
const app = createApp({
|
|
274
|
+
queries: { getUser },
|
|
275
|
+
mutations: { createUser },
|
|
276
|
+
});
|
|
277
|
+
const handler = createHTTPHandler(app, { pathPrefix: "/api" });
|
|
278
|
+
|
|
279
|
+
const request = new Request("http://localhost/api/__lens/health", {
|
|
280
|
+
method: "GET",
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
const response = await handler(request);
|
|
284
|
+
expect(response.status).toBe(200);
|
|
285
|
+
|
|
286
|
+
const data = await response.json();
|
|
287
|
+
expect(data.status).toBe("healthy");
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
it("supports custom health check path", async () => {
|
|
291
|
+
const app = createApp({
|
|
292
|
+
queries: { getUser },
|
|
293
|
+
mutations: { createUser },
|
|
294
|
+
});
|
|
295
|
+
const handler = createHTTPHandler(app, {
|
|
296
|
+
health: { path: "/healthz" },
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
const request = new Request("http://localhost/healthz", {
|
|
300
|
+
method: "GET",
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
const response = await handler(request);
|
|
304
|
+
expect(response.status).toBe(200);
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
it("can be disabled", async () => {
|
|
308
|
+
const app = createApp({
|
|
309
|
+
queries: { getUser },
|
|
310
|
+
mutations: { createUser },
|
|
311
|
+
});
|
|
312
|
+
const handler = createHTTPHandler(app, {
|
|
313
|
+
health: { enabled: false },
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
const request = new Request("http://localhost/__lens/health", {
|
|
317
|
+
method: "GET",
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
const response = await handler(request);
|
|
321
|
+
expect(response.status).toBe(404);
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
it("includes custom health checks", async () => {
|
|
325
|
+
const app = createApp({
|
|
326
|
+
queries: { getUser },
|
|
327
|
+
mutations: { createUser },
|
|
328
|
+
});
|
|
329
|
+
const handler = createHTTPHandler(app, {
|
|
330
|
+
health: {
|
|
331
|
+
checks: () => ({
|
|
332
|
+
database: { status: "pass", message: "Connected" },
|
|
333
|
+
cache: { status: "pass" },
|
|
334
|
+
}),
|
|
335
|
+
},
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
const request = new Request("http://localhost/__lens/health", {
|
|
339
|
+
method: "GET",
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
const response = await handler(request);
|
|
343
|
+
expect(response.status).toBe(200);
|
|
344
|
+
|
|
345
|
+
const data = await response.json();
|
|
346
|
+
expect(data.checks?.database.status).toBe("pass");
|
|
347
|
+
expect(data.checks?.cache.status).toBe("pass");
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
it("returns 503 when custom health check fails", async () => {
|
|
351
|
+
const app = createApp({
|
|
352
|
+
queries: { getUser },
|
|
353
|
+
mutations: { createUser },
|
|
354
|
+
});
|
|
355
|
+
const handler = createHTTPHandler(app, {
|
|
356
|
+
health: {
|
|
357
|
+
checks: () => ({
|
|
358
|
+
database: { status: "fail", message: "Connection refused" },
|
|
359
|
+
}),
|
|
360
|
+
},
|
|
361
|
+
});
|
|
362
|
+
|
|
363
|
+
const request = new Request("http://localhost/__lens/health", {
|
|
364
|
+
method: "GET",
|
|
365
|
+
});
|
|
366
|
+
|
|
367
|
+
const response = await handler(request);
|
|
368
|
+
expect(response.status).toBe(503);
|
|
369
|
+
|
|
370
|
+
const data = await response.json();
|
|
371
|
+
expect(data.status).toBe("degraded");
|
|
372
|
+
expect(data.checks?.database.status).toBe("fail");
|
|
373
|
+
});
|
|
374
|
+
|
|
375
|
+
it("handles async health checks", async () => {
|
|
376
|
+
const app = createApp({
|
|
377
|
+
queries: { getUser },
|
|
378
|
+
mutations: { createUser },
|
|
379
|
+
});
|
|
380
|
+
const handler = createHTTPHandler(app, {
|
|
381
|
+
health: {
|
|
382
|
+
checks: async () => {
|
|
383
|
+
await new Promise((r) => setTimeout(r, 10));
|
|
384
|
+
return { async: { status: "pass" } };
|
|
385
|
+
},
|
|
386
|
+
},
|
|
387
|
+
});
|
|
388
|
+
|
|
389
|
+
const request = new Request("http://localhost/__lens/health", {
|
|
390
|
+
method: "GET",
|
|
391
|
+
});
|
|
392
|
+
|
|
393
|
+
const response = await handler(request);
|
|
394
|
+
expect(response.status).toBe(200);
|
|
395
|
+
|
|
396
|
+
const data = await response.json();
|
|
397
|
+
expect(data.checks?.async.status).toBe("pass");
|
|
398
|
+
});
|
|
399
|
+
|
|
400
|
+
it("handles health check errors gracefully", async () => {
|
|
401
|
+
const app = createApp({
|
|
402
|
+
queries: { getUser },
|
|
403
|
+
mutations: { createUser },
|
|
404
|
+
});
|
|
405
|
+
const handler = createHTTPHandler(app, {
|
|
406
|
+
health: {
|
|
407
|
+
checks: () => {
|
|
408
|
+
throw new Error("Check failed");
|
|
409
|
+
},
|
|
410
|
+
},
|
|
411
|
+
});
|
|
412
|
+
|
|
413
|
+
const request = new Request("http://localhost/__lens/health", {
|
|
414
|
+
method: "GET",
|
|
415
|
+
});
|
|
416
|
+
|
|
417
|
+
const response = await handler(request);
|
|
418
|
+
expect(response.status).toBe(503);
|
|
419
|
+
|
|
420
|
+
const data = await response.json();
|
|
421
|
+
expect(data.status).toBe("degraded");
|
|
422
|
+
expect(data.checks?.healthCheck.status).toBe("fail");
|
|
423
|
+
});
|
|
424
|
+
|
|
425
|
+
it("includes Cache-Control header", async () => {
|
|
426
|
+
const app = createApp({
|
|
427
|
+
queries: { getUser },
|
|
428
|
+
mutations: { createUser },
|
|
429
|
+
});
|
|
430
|
+
const handler = createHTTPHandler(app);
|
|
431
|
+
|
|
432
|
+
const request = new Request("http://localhost/__lens/health", {
|
|
433
|
+
method: "GET",
|
|
434
|
+
});
|
|
435
|
+
|
|
436
|
+
const response = await handler(request);
|
|
437
|
+
expect(response.headers.get("Cache-Control")).toBe("no-cache, no-store, must-revalidate");
|
|
438
|
+
});
|
|
439
|
+
});
|
|
215
440
|
});
|
package/src/handlers/http.ts
CHANGED
|
@@ -12,6 +12,64 @@ import type { LensServer } from "../server/create.js";
|
|
|
12
12
|
// Types
|
|
13
13
|
// =============================================================================
|
|
14
14
|
|
|
15
|
+
/** Error sanitization options */
|
|
16
|
+
export interface ErrorSanitizationOptions {
|
|
17
|
+
/**
|
|
18
|
+
* Enable development mode - shows full error messages.
|
|
19
|
+
* Default: false (production mode - sanitized errors only)
|
|
20
|
+
*/
|
|
21
|
+
development?: boolean;
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Custom error sanitizer function.
|
|
25
|
+
* Return a safe error message to send to the client.
|
|
26
|
+
*/
|
|
27
|
+
sanitize?: (error: Error) => string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Health check response data
|
|
32
|
+
*/
|
|
33
|
+
export interface HealthCheckResponse {
|
|
34
|
+
/** Overall status */
|
|
35
|
+
status: "healthy" | "degraded" | "unhealthy";
|
|
36
|
+
/** Service name */
|
|
37
|
+
service: string;
|
|
38
|
+
/** Server version */
|
|
39
|
+
version: string;
|
|
40
|
+
/** Uptime in seconds */
|
|
41
|
+
uptime: number;
|
|
42
|
+
/** Timestamp of health check */
|
|
43
|
+
timestamp: string;
|
|
44
|
+
/** Optional checks with their status */
|
|
45
|
+
checks?: Record<string, { status: "pass" | "fail"; message?: string }>;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Health check options
|
|
50
|
+
*/
|
|
51
|
+
export interface HealthCheckOptions {
|
|
52
|
+
/**
|
|
53
|
+
* Enable health check endpoint.
|
|
54
|
+
* Default: true
|
|
55
|
+
*/
|
|
56
|
+
enabled?: boolean;
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Custom health check path.
|
|
60
|
+
* Default: "/__lens/health"
|
|
61
|
+
*/
|
|
62
|
+
path?: string;
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Custom health check function.
|
|
66
|
+
* Return additional checks to include in the response.
|
|
67
|
+
*/
|
|
68
|
+
checks?: () =>
|
|
69
|
+
| Promise<Record<string, { status: "pass" | "fail"; message?: string }>>
|
|
70
|
+
| Record<string, { status: "pass" | "fail"; message?: string }>;
|
|
71
|
+
}
|
|
72
|
+
|
|
15
73
|
export interface HTTPHandlerOptions {
|
|
16
74
|
/**
|
|
17
75
|
* Path prefix for Lens endpoints.
|
|
@@ -29,13 +87,25 @@ export interface HTTPHandlerOptions {
|
|
|
29
87
|
|
|
30
88
|
/**
|
|
31
89
|
* Custom CORS headers.
|
|
32
|
-
* Default: Allow all origins
|
|
90
|
+
* Default: Allow all origins in development, strict in production
|
|
33
91
|
*/
|
|
34
92
|
cors?: {
|
|
35
93
|
origin?: string | string[];
|
|
36
94
|
methods?: string[];
|
|
37
95
|
headers?: string[];
|
|
38
96
|
};
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Error sanitization options.
|
|
100
|
+
* Controls what error information is exposed to clients.
|
|
101
|
+
*/
|
|
102
|
+
errors?: ErrorSanitizationOptions;
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Health check endpoint configuration.
|
|
106
|
+
* Enabled by default at /__lens/health
|
|
107
|
+
*/
|
|
108
|
+
health?: HealthCheckOptions;
|
|
39
109
|
}
|
|
40
110
|
|
|
41
111
|
export interface HTTPHandler {
|
|
@@ -75,23 +145,100 @@ export interface HTTPHandler {
|
|
|
75
145
|
* export default { fetch: handler }
|
|
76
146
|
* ```
|
|
77
147
|
*/
|
|
148
|
+
/**
|
|
149
|
+
* Default error sanitizer - removes sensitive information from errors.
|
|
150
|
+
* Safe error messages are preserved, internal details are hidden.
|
|
151
|
+
*/
|
|
152
|
+
function sanitizeError(error: Error, isDevelopment: boolean): string {
|
|
153
|
+
if (isDevelopment) {
|
|
154
|
+
return error.message;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const message = error.message;
|
|
158
|
+
|
|
159
|
+
// Known safe error patterns (validation errors, business logic errors)
|
|
160
|
+
const safePatterns = [
|
|
161
|
+
/^Invalid input:/,
|
|
162
|
+
/^Missing operation/,
|
|
163
|
+
/^Not found/,
|
|
164
|
+
/^Unauthorized/,
|
|
165
|
+
/^Forbidden/,
|
|
166
|
+
/^Bad request/,
|
|
167
|
+
/^Validation failed/,
|
|
168
|
+
];
|
|
169
|
+
|
|
170
|
+
if (safePatterns.some((pattern) => pattern.test(message))) {
|
|
171
|
+
return message;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// Check for sensitive patterns
|
|
175
|
+
const sensitivePatterns = [
|
|
176
|
+
/\/[^\s]+\.(ts|js|json)/, // file paths
|
|
177
|
+
/at\s+[^\s]+\s+\(/, // stack traces
|
|
178
|
+
/ENOENT|EACCES|ECONNREFUSED/, // system errors
|
|
179
|
+
/SELECT|INSERT|UPDATE|DELETE|FROM|WHERE/i, // SQL
|
|
180
|
+
/password|secret|token|key|auth/i, // credentials
|
|
181
|
+
];
|
|
182
|
+
|
|
183
|
+
if (sensitivePatterns.some((pattern) => pattern.test(message))) {
|
|
184
|
+
return "An internal error occurred";
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// Allow short, simple messages through
|
|
188
|
+
if (message.length < 100 && !message.includes("\n")) {
|
|
189
|
+
return message;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
return "An internal error occurred";
|
|
193
|
+
}
|
|
194
|
+
|
|
78
195
|
export function createHTTPHandler(
|
|
79
196
|
server: LensServer,
|
|
80
197
|
options: HTTPHandlerOptions = {},
|
|
81
198
|
): HTTPHandler {
|
|
82
|
-
const { pathPrefix = "", cors } = options;
|
|
199
|
+
const { pathPrefix = "", cors, errors, health } = options;
|
|
200
|
+
const isDevelopment = errors?.development ?? false;
|
|
201
|
+
|
|
202
|
+
// Health check configuration
|
|
203
|
+
const healthEnabled = health?.enabled !== false; // Enabled by default
|
|
204
|
+
const healthPath = health?.path ?? "/__lens/health";
|
|
205
|
+
const startTime = Date.now();
|
|
206
|
+
|
|
207
|
+
// Error sanitization function
|
|
208
|
+
const sanitize = (error: Error): string => {
|
|
209
|
+
if (errors?.sanitize) {
|
|
210
|
+
return errors.sanitize(error);
|
|
211
|
+
}
|
|
212
|
+
return sanitizeError(error, isDevelopment);
|
|
213
|
+
};
|
|
83
214
|
|
|
84
215
|
// Build CORS headers
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
:
|
|
216
|
+
// In production, require explicit origin configuration for security
|
|
217
|
+
// In development, allow all origins for convenience
|
|
218
|
+
const allowedOrigin = cors?.origin
|
|
219
|
+
? Array.isArray(cors.origin)
|
|
220
|
+
? cors.origin.join(", ")
|
|
221
|
+
: cors.origin
|
|
222
|
+
: isDevelopment
|
|
223
|
+
? "*"
|
|
224
|
+
: ""; // No cross-origin allowed by default in production
|
|
225
|
+
|
|
226
|
+
// Base headers including security headers
|
|
227
|
+
const baseHeaders: Record<string, string> = {
|
|
228
|
+
"Content-Type": "application/json",
|
|
229
|
+
// Security headers
|
|
230
|
+
"X-Content-Type-Options": "nosniff",
|
|
231
|
+
"X-Frame-Options": "DENY",
|
|
232
|
+
// CORS headers
|
|
91
233
|
"Access-Control-Allow-Methods": cors?.methods?.join(", ") ?? "GET, POST, OPTIONS",
|
|
92
234
|
"Access-Control-Allow-Headers": cors?.headers?.join(", ") ?? "Content-Type, Authorization",
|
|
93
235
|
};
|
|
94
236
|
|
|
237
|
+
// Only add Access-Control-Allow-Origin if there's an allowed origin
|
|
238
|
+
if (allowedOrigin) {
|
|
239
|
+
baseHeaders["Access-Control-Allow-Origin"] = allowedOrigin;
|
|
240
|
+
}
|
|
241
|
+
|
|
95
242
|
const handler = async (request: Request): Promise<Response> => {
|
|
96
243
|
const url = new URL(request.url);
|
|
97
244
|
const pathname = url.pathname;
|
|
@@ -100,7 +247,52 @@ export function createHTTPHandler(
|
|
|
100
247
|
if (request.method === "OPTIONS") {
|
|
101
248
|
return new Response(null, {
|
|
102
249
|
status: 204,
|
|
103
|
-
headers:
|
|
250
|
+
headers: baseHeaders,
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// Health check endpoint: GET /__lens/health
|
|
255
|
+
const fullHealthPath = `${pathPrefix}${healthPath}`;
|
|
256
|
+
if (healthEnabled && request.method === "GET" && pathname === fullHealthPath) {
|
|
257
|
+
const metadata = server.getMetadata();
|
|
258
|
+
const uptimeSeconds = Math.floor((Date.now() - startTime) / 1000);
|
|
259
|
+
|
|
260
|
+
// Run custom health checks if provided
|
|
261
|
+
let customChecks: Record<string, { status: "pass" | "fail"; message?: string }> = {};
|
|
262
|
+
let hasFailure = false;
|
|
263
|
+
|
|
264
|
+
if (health?.checks) {
|
|
265
|
+
try {
|
|
266
|
+
customChecks = await health.checks();
|
|
267
|
+
hasFailure = Object.values(customChecks).some((c) => c.status === "fail");
|
|
268
|
+
} catch (error) {
|
|
269
|
+
customChecks.healthCheck = {
|
|
270
|
+
status: "fail",
|
|
271
|
+
message: error instanceof Error ? error.message : "Health check failed",
|
|
272
|
+
};
|
|
273
|
+
hasFailure = true;
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
const response: HealthCheckResponse = {
|
|
278
|
+
status: hasFailure ? "degraded" : "healthy",
|
|
279
|
+
service: "lens-server",
|
|
280
|
+
version: metadata.version,
|
|
281
|
+
uptime: uptimeSeconds,
|
|
282
|
+
timestamp: new Date().toISOString(),
|
|
283
|
+
};
|
|
284
|
+
|
|
285
|
+
if (Object.keys(customChecks).length > 0) {
|
|
286
|
+
response.checks = customChecks;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
return new Response(JSON.stringify(response), {
|
|
290
|
+
status: hasFailure ? 503 : 200,
|
|
291
|
+
headers: {
|
|
292
|
+
"Content-Type": "application/json",
|
|
293
|
+
"Cache-Control": "no-cache, no-store, must-revalidate",
|
|
294
|
+
...baseHeaders,
|
|
295
|
+
},
|
|
104
296
|
});
|
|
105
297
|
}
|
|
106
298
|
|
|
@@ -110,7 +302,7 @@ export function createHTTPHandler(
|
|
|
110
302
|
return new Response(JSON.stringify(server.getMetadata()), {
|
|
111
303
|
headers: {
|
|
112
304
|
"Content-Type": "application/json",
|
|
113
|
-
...
|
|
305
|
+
...baseHeaders,
|
|
114
306
|
},
|
|
115
307
|
});
|
|
116
308
|
}
|
|
@@ -121,13 +313,21 @@ export function createHTTPHandler(
|
|
|
121
313
|
request.method === "POST" &&
|
|
122
314
|
(pathname === operationPath || pathname === `${pathPrefix}/`)
|
|
123
315
|
) {
|
|
316
|
+
// Parse JSON body with proper error handling
|
|
317
|
+
let body: { operation?: string; path?: string; input?: unknown };
|
|
124
318
|
try {
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
319
|
+
body = (await request.json()) as typeof body;
|
|
320
|
+
} catch {
|
|
321
|
+
return new Response(JSON.stringify({ error: "Invalid JSON in request body" }), {
|
|
322
|
+
status: 400,
|
|
323
|
+
headers: {
|
|
324
|
+
"Content-Type": "application/json",
|
|
325
|
+
...baseHeaders,
|
|
326
|
+
},
|
|
327
|
+
});
|
|
328
|
+
}
|
|
130
329
|
|
|
330
|
+
try {
|
|
131
331
|
// Support both 'operation' and 'path' for backwards compatibility
|
|
132
332
|
const operationPath = body.operation ?? body.path;
|
|
133
333
|
if (!operationPath) {
|
|
@@ -135,7 +335,7 @@ export function createHTTPHandler(
|
|
|
135
335
|
status: 400,
|
|
136
336
|
headers: {
|
|
137
337
|
"Content-Type": "application/json",
|
|
138
|
-
...
|
|
338
|
+
...baseHeaders,
|
|
139
339
|
},
|
|
140
340
|
});
|
|
141
341
|
}
|
|
@@ -148,11 +348,11 @@ export function createHTTPHandler(
|
|
|
148
348
|
);
|
|
149
349
|
|
|
150
350
|
if (result.error) {
|
|
151
|
-
return new Response(JSON.stringify({ error: result.error
|
|
351
|
+
return new Response(JSON.stringify({ error: sanitize(result.error) }), {
|
|
152
352
|
status: 500,
|
|
153
353
|
headers: {
|
|
154
354
|
"Content-Type": "application/json",
|
|
155
|
-
...
|
|
355
|
+
...baseHeaders,
|
|
156
356
|
},
|
|
157
357
|
});
|
|
158
358
|
}
|
|
@@ -160,15 +360,16 @@ export function createHTTPHandler(
|
|
|
160
360
|
return new Response(JSON.stringify({ data: result.data }), {
|
|
161
361
|
headers: {
|
|
162
362
|
"Content-Type": "application/json",
|
|
163
|
-
...
|
|
363
|
+
...baseHeaders,
|
|
164
364
|
},
|
|
165
365
|
});
|
|
166
366
|
} catch (error) {
|
|
167
|
-
|
|
367
|
+
const err = error instanceof Error ? error : new Error(String(error));
|
|
368
|
+
return new Response(JSON.stringify({ error: sanitize(err) }), {
|
|
168
369
|
status: 500,
|
|
169
370
|
headers: {
|
|
170
371
|
"Content-Type": "application/json",
|
|
171
|
-
...
|
|
372
|
+
...baseHeaders,
|
|
172
373
|
},
|
|
173
374
|
});
|
|
174
375
|
}
|
|
@@ -179,7 +380,7 @@ export function createHTTPHandler(
|
|
|
179
380
|
status: 404,
|
|
180
381
|
headers: {
|
|
181
382
|
"Content-Type": "application/json",
|
|
182
|
-
...
|
|
383
|
+
...baseHeaders,
|
|
183
384
|
},
|
|
184
385
|
});
|
|
185
386
|
};
|
package/src/handlers/index.ts
CHANGED
package/src/handlers/ws-types.ts
CHANGED
|
@@ -20,6 +20,45 @@ export interface WSHandlerOptions {
|
|
|
20
20
|
warn?: (message: string, ...args: unknown[]) => void;
|
|
21
21
|
error?: (message: string, ...args: unknown[]) => void;
|
|
22
22
|
};
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Maximum message size in bytes.
|
|
26
|
+
* Messages larger than this will be rejected.
|
|
27
|
+
* Default: 1MB (1024 * 1024)
|
|
28
|
+
*/
|
|
29
|
+
maxMessageSize?: number;
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Maximum subscriptions per client.
|
|
33
|
+
* Prevents resource exhaustion from malicious clients.
|
|
34
|
+
* Default: 100
|
|
35
|
+
*/
|
|
36
|
+
maxSubscriptionsPerClient?: number;
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Maximum connections total.
|
|
40
|
+
* Prevents server overload.
|
|
41
|
+
* Default: 10000
|
|
42
|
+
*/
|
|
43
|
+
maxConnections?: number;
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Rate limiting configuration.
|
|
47
|
+
* Uses token bucket algorithm per client.
|
|
48
|
+
*/
|
|
49
|
+
rateLimit?: {
|
|
50
|
+
/**
|
|
51
|
+
* Maximum messages per window.
|
|
52
|
+
* Default: 100
|
|
53
|
+
*/
|
|
54
|
+
maxMessages?: number;
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Time window in milliseconds.
|
|
58
|
+
* Default: 1000 (1 second)
|
|
59
|
+
*/
|
|
60
|
+
windowMs?: number;
|
|
61
|
+
};
|
|
23
62
|
}
|
|
24
63
|
|
|
25
64
|
/**
|