stitchkit 0.2.0 → 0.3.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.
Files changed (63) hide show
  1. package/dist/browser/http.d.ts +5 -0
  2. package/dist/browser/http.d.ts.map +1 -1
  3. package/dist/{index-a35v22fh.js → index-0t460v7k.js} +12 -4
  4. package/dist/{index-n7bmdwmz.js → index-4b1j82gp.js} +8 -4
  5. package/dist/{index-5sxnvwb1.js → index-4kz3aqpx.js} +2 -2
  6. package/dist/{index-v2z2v3mq.js → index-dpj1jcys.js} +150 -60
  7. package/dist/index-kzfs85xp.js +9 -0
  8. package/dist/index-mwmpw6j1.js +60 -0
  9. package/dist/index-q6ja2qwq.js +7 -0
  10. package/dist/index.js +4 -3
  11. package/dist/internal/errors.d.ts +13 -1
  12. package/dist/internal/errors.d.ts.map +1 -1
  13. package/dist/internal/safe-json.d.ts +19 -0
  14. package/dist/internal/safe-json.d.ts.map +1 -0
  15. package/dist/internal/within-dir.d.ts +7 -0
  16. package/dist/internal/within-dir.d.ts.map +1 -0
  17. package/dist/node.js +6 -3
  18. package/dist/observability/context.d.ts +9 -1
  19. package/dist/observability/context.d.ts.map +1 -1
  20. package/dist/observability/index.js +35 -15
  21. package/dist/observability/sanitize.d.ts.map +1 -1
  22. package/dist/server/context.d.ts +3 -2
  23. package/dist/server/context.d.ts.map +1 -1
  24. package/dist/server/create.d.ts.map +1 -1
  25. package/dist/server/event-bus.d.ts +10 -1
  26. package/dist/server/event-bus.d.ts.map +1 -1
  27. package/dist/server/index.d.ts +1 -1
  28. package/dist/server/index.d.ts.map +1 -1
  29. package/dist/server/index.js +86 -32
  30. package/dist/server/logger.d.ts +5 -4
  31. package/dist/server/logger.d.ts.map +1 -1
  32. package/dist/server/middleware/auth.d.ts +24 -1
  33. package/dist/server/middleware/auth.d.ts.map +1 -1
  34. package/dist/server/middleware/cookies.d.ts.map +1 -1
  35. package/dist/server/middleware/cors.d.ts +7 -0
  36. package/dist/server/middleware/cors.d.ts.map +1 -1
  37. package/dist/server/multipart.d.ts +2 -1
  38. package/dist/server/multipart.d.ts.map +1 -1
  39. package/dist/server/rate-limit.d.ts +7 -2
  40. package/dist/server/rate-limit.d.ts.map +1 -1
  41. package/dist/server/request.d.ts +27 -4
  42. package/dist/server/request.d.ts.map +1 -1
  43. package/dist/server/router.d.ts +4 -2
  44. package/dist/server/router.d.ts.map +1 -1
  45. package/dist/server/stream.d.ts +3 -1
  46. package/dist/server/stream.d.ts.map +1 -1
  47. package/dist/server/types.d.ts +12 -0
  48. package/dist/server/types.d.ts.map +1 -1
  49. package/dist/tools/coerce.d.ts +8 -4
  50. package/dist/tools/coerce.d.ts.map +1 -1
  51. package/dist/tools/execute.d.ts +1 -1
  52. package/dist/tools/execute.d.ts.map +1 -1
  53. package/dist/tools/manifest.d.ts +4 -0
  54. package/dist/tools/manifest.d.ts.map +1 -1
  55. package/dist/tools/mcp-handler.d.ts.map +1 -1
  56. package/dist/tools/mount.d.ts +2 -2
  57. package/dist/tools/mount.d.ts.map +1 -1
  58. package/dist/tools/view-file.d.ts.map +1 -1
  59. package/dist/tools.d.ts +1 -1
  60. package/dist/tools.d.ts.map +1 -1
  61. package/dist/tools.js +140 -93
  62. package/package.json +1 -1
  63. package/dist/index-ke4mx4ea.js +0 -38
@@ -39,6 +39,11 @@ export interface HttpClientConfig {
39
39
  statusCodes?: number[];
40
40
  };
41
41
  parseError?: (body: unknown) => ErrorEnvelope['error'] | null;
42
+ /**
43
+ * Path prefixes whose `401` must NOT emit an `unauthorized` event — the
44
+ * login / session endpoints, where a `401` is an expected outcome. Matched
45
+ * against `URL.pathname`, so the prefixes start with `/`.
46
+ */
42
47
  authEndpoints?: string[];
43
48
  /**
44
49
  * Extra headers added to every request. A function is re-evaluated per
@@ -1 +1 @@
1
- {"version":3,"file":"http.d.ts","sourceRoot":"","sources":["../../src/browser/http.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"http.d.ts","sourceRoot":"","sources":["../../src/browser/http.ts"],"names":[],"mappings":"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"}
@@ -21,9 +21,17 @@ function normalizeError(err) {
21
21
  if (err instanceof z.ZodError) {
22
22
  return new AppError("VALIDATION_ERROR", formatZodError(err), 400);
23
23
  }
24
- const msg = err instanceof Error ? err.message : String(err);
25
- const safeMessage = msg.length > 200 ? `${msg.slice(0, 200)}...` : msg;
26
- return new AppError("INTERNAL_SERVER_ERROR", safeMessage, 500);
24
+ console.error("[stitchkit] unhandled error:", err);
25
+ return new AppError("INTERNAL_SERVER_ERROR", "Internal server error", 500);
26
+ }
27
+ function validateHandlerOutput(schema, data) {
28
+ const parsed = schema.safeParse(data);
29
+ if (parsed.success)
30
+ return { ok: true, data: parsed.data };
31
+ return {
32
+ ok: false,
33
+ message: `Handler output does not match the contract: ${formatZodError(parsed.error)}`
34
+ };
27
35
  }
28
36
 
29
- export { formatZodError, normalizeError };
37
+ export { formatZodError, normalizeError, validateHandlerOutput };
@@ -1,3 +1,7 @@
1
+ import {
2
+ normalizeError
3
+ } from "./index-0t460v7k.js";
4
+
1
5
  // src/server/stream.ts
2
6
  function streamSSE(generator) {
3
7
  const encoder = new TextEncoder;
@@ -15,8 +19,8 @@ function streamSSE(generator) {
15
19
  `));
16
20
  controller.close();
17
21
  } catch (err) {
18
- const msg = err instanceof Error ? err.message : String(err);
19
- controller.enqueue(encoder.encode(`data: ${JSON.stringify({ error: msg })}
22
+ const envelope = normalizeError(err).toJSON();
23
+ controller.enqueue(encoder.encode(`data: ${JSON.stringify(envelope)}
20
24
 
21
25
  `));
22
26
  controller.close();
@@ -48,9 +52,9 @@ async function* parseSSE(response, options) {
48
52
  buffer = lines.pop() ?? "";
49
53
  for (const rawLine of lines) {
50
54
  const line = rawLine.endsWith("\r") ? rawLine.slice(0, -1) : rawLine;
51
- if (!line.startsWith("data: "))
55
+ if (!line.startsWith("data:"))
52
56
  continue;
53
- const data = line.slice(6);
57
+ const data = line.slice(5).replace(/^ /, "");
54
58
  if (data === "[DONE]")
55
59
  return;
56
60
  try {
@@ -40,7 +40,7 @@ function createHttpClient(config) {
40
40
  let ssrCookies = null;
41
41
  let isLoggedOut = false;
42
42
  const listeners = new Set;
43
- const authEndpoints = config.authEndpoints ?? ["auth/"];
43
+ const authEndpoints = config.authEndpoints ?? ["/auth/"];
44
44
  const parseError = config.parseError ?? parseApiErrorBody;
45
45
  function emit(event) {
46
46
  for (const fn of listeners) {
@@ -76,7 +76,7 @@ function createHttpClient(config) {
76
76
  async ({ request: request2, response }) => {
77
77
  if (response.status === 401) {
78
78
  const url = new URL(request2.url).pathname;
79
- if (!isLoggedOut && !authEndpoints.some((path) => url.includes(path))) {
79
+ if (!isLoggedOut && !authEndpoints.some((path) => url.startsWith(path))) {
80
80
  isLoggedOut = true;
81
81
  emit({ type: "unauthorized" });
82
82
  }
@@ -1,6 +1,10 @@
1
1
  import {
2
- normalizeError
3
- } from "./index-a35v22fh.js";
2
+ isWithinDir
3
+ } from "./index-q6ja2qwq.js";
4
+ import {
5
+ normalizeError,
6
+ validateHandlerOutput
7
+ } from "./index-0t460v7k.js";
4
8
  import {
5
9
  AppError,
6
10
  badRequest
@@ -9,62 +13,103 @@ import {
9
13
  extractIp,
10
14
  getClientInfo,
11
15
  parseQueryParams,
16
+ resolveSocketIp,
12
17
  resolveTraceId
13
- } from "./index-ke4mx4ea.js";
18
+ } from "./index-mwmpw6j1.js";
19
+ import {
20
+ isUnsafeKey,
21
+ safeJsonParse
22
+ } from "./index-kzfs85xp.js";
14
23
 
15
24
  // src/server/multipart.ts
16
25
  var DEFAULT_MAX_UPLOAD_BYTES = 25 * 1024 * 1024;
17
- async function parseMultipart(req, fileField, fieldsSchema, maxBytes = DEFAULT_MAX_UPLOAD_BYTES) {
18
- const declared = Number(req.headers.get("content-length") ?? 0);
19
- if (declared > maxBytes) {
20
- badRequest(`Upload exceeds the ${Math.round(maxBytes / 1024 / 1024)} MB limit`);
26
+ async function readBodyCapped(req, maxBytes) {
27
+ const reader = req.body?.getReader();
28
+ if (!reader)
29
+ return new Uint8Array(0);
30
+ const chunks = [];
31
+ let total = 0;
32
+ try {
33
+ while (true) {
34
+ const { done, value } = await reader.read();
35
+ if (done)
36
+ break;
37
+ total += value.length;
38
+ if (total > maxBytes) {
39
+ await reader.cancel();
40
+ badRequest(`Upload exceeds the ${Math.round(maxBytes / 1024 / 1024)} MB limit`);
41
+ }
42
+ chunks.push(value);
43
+ }
44
+ } finally {
45
+ reader.releaseLock();
46
+ }
47
+ const buffer = new Uint8Array(total);
48
+ let offset = 0;
49
+ for (const chunk of chunks) {
50
+ buffer.set(chunk, offset);
51
+ offset += chunk.length;
21
52
  }
22
- const formData = await req.formData();
53
+ return buffer;
54
+ }
55
+ async function parseMultipart(req, fileField, fieldsSchema, maxBytes = DEFAULT_MAX_UPLOAD_BYTES) {
56
+ const body = await readBodyCapped(req, maxBytes);
57
+ const contentType = req.headers.get("content-type") ?? "";
58
+ const formData = await new Response(body, {
59
+ headers: { "content-type": contentType }
60
+ }).formData();
23
61
  const file = formData.get(fileField);
24
62
  if (!file || !(file instanceof File)) {
25
63
  badRequest(`Missing file field: ${fileField}`);
26
64
  }
27
- if (file.size > maxBytes) {
28
- badRequest(`Upload exceeds the ${Math.round(maxBytes / 1024 / 1024)} MB limit`);
29
- }
30
65
  const fields = {};
31
66
  for (const [key, value] of formData.entries()) {
32
67
  if (key === fileField)
33
68
  continue;
69
+ if (isUnsafeKey(key))
70
+ continue;
34
71
  if (typeof value === "string") {
35
72
  try {
36
- fields[key] = JSON.parse(value);
73
+ fields[key] = safeJsonParse(value);
37
74
  } catch {
38
75
  fields[key] = value;
39
76
  }
40
77
  }
41
78
  }
42
- const parsed = fieldsSchema ? fieldsSchema.parse(fields) : fields;
43
- return { file, fields: parsed };
79
+ return { file, fields: fieldsSchema ? fieldsSchema.parse(fields) : fields };
44
80
  }
45
81
 
46
82
  // src/server/middleware/cors.ts
47
- function corsHeaders(config, requestOrigin) {
48
- let allowOrigin;
83
+ function assertCorsConfig(config) {
84
+ if (config.credentials && (config.origin === undefined || config.origin === "*")) {
85
+ throw new Error("[stitchkit] cors: `credentials: true` cannot be combined with a wildcard origin. " + "Set `origin` to an explicit string or list.");
86
+ }
87
+ }
88
+ function resolveOrigin(config, requestOrigin) {
49
89
  if (config.origin === undefined || config.origin === "*") {
50
- allowOrigin = config.credentials ? requestOrigin ?? undefined : "*";
51
- } else if (Array.isArray(config.origin)) {
52
- allowOrigin = requestOrigin && config.origin.includes(requestOrigin) ? requestOrigin : undefined;
53
- } else {
54
- allowOrigin = config.origin;
90
+ return "*";
91
+ }
92
+ if (Array.isArray(config.origin)) {
93
+ if (!requestOrigin)
94
+ return;
95
+ const lower = requestOrigin.toLowerCase();
96
+ return config.origin.some((o) => o.toLowerCase() === lower) ? requestOrigin : undefined;
55
97
  }
98
+ return config.origin;
99
+ }
100
+ function corsHeaders(config, requestOrigin) {
101
+ const allowOrigin = resolveOrigin(config, requestOrigin);
56
102
  const headers = {
57
103
  "Access-Control-Allow-Methods": config.methods ?? "GET, POST, PUT, PATCH, DELETE, OPTIONS",
58
104
  "Access-Control-Allow-Headers": config.headers ?? "Content-Type, Authorization, X-Trace-Id"
59
105
  };
60
106
  if (allowOrigin !== undefined) {
61
107
  headers["Access-Control-Allow-Origin"] = allowOrigin;
108
+ if (config.credentials) {
109
+ headers["Access-Control-Allow-Credentials"] = "true";
110
+ }
62
111
  }
63
- if (config.credentials) {
64
- headers["Access-Control-Allow-Credentials"] = "true";
65
- }
66
- const variesByOrigin = Array.isArray(config.origin) || (config.origin === undefined || config.origin === "*") && Boolean(config.credentials);
67
- if (variesByOrigin) {
112
+ if (Array.isArray(config.origin)) {
68
113
  headers.Vary = "Origin";
69
114
  }
70
115
  return headers;
@@ -77,7 +122,8 @@ function corsPreflightResponse(config, req) {
77
122
  }
78
123
 
79
124
  // src/server/router.ts
80
- import { resolve, sep } from "node:path";
125
+ import { readFile, stat } from "node:fs/promises";
126
+ import { extname, resolve } from "node:path";
81
127
  function joinPath(...parts) {
82
128
  const joined = parts.filter(Boolean).map((part) => part.replace(/^\/+|\/+$/g, "")).filter(Boolean).join("/");
83
129
  return `/${joined}`;
@@ -195,25 +241,51 @@ function matchRawRoute(rawRoutes, httpMethod, pathname) {
195
241
  }
196
242
  return null;
197
243
  }
244
+ var STATIC_MIME = {
245
+ ".html": "text/html; charset=utf-8",
246
+ ".css": "text/css; charset=utf-8",
247
+ ".js": "text/javascript; charset=utf-8",
248
+ ".mjs": "text/javascript; charset=utf-8",
249
+ ".json": "application/json; charset=utf-8",
250
+ ".svg": "image/svg+xml",
251
+ ".png": "image/png",
252
+ ".jpg": "image/jpeg",
253
+ ".jpeg": "image/jpeg",
254
+ ".gif": "image/gif",
255
+ ".webp": "image/webp",
256
+ ".ico": "image/x-icon",
257
+ ".woff2": "font/woff2",
258
+ ".txt": "text/plain; charset=utf-8"
259
+ };
198
260
  function staticRoute(prefix, dir) {
199
261
  const cleanPrefix = prefix.replace(/\/+$/, "");
200
- const cleanDir = dir.replace(/\/+$/, "");
262
+ const root = resolve(dir.replace(/\/+$/, ""));
201
263
  return {
202
264
  method: "GET",
203
265
  path: `${cleanPrefix}/*`,
204
266
  handler: async (req) => {
205
267
  const pathname = new URL(req.url).pathname;
206
- const rel = pathname.slice(cleanPrefix.length).replace(/^\/+/, "");
207
- const root = resolve(cleanDir);
268
+ let rel;
269
+ try {
270
+ rel = decodeURIComponent(pathname.slice(cleanPrefix.length)).replace(/^\/+/, "");
271
+ } catch {
272
+ return new Response("Bad request", { status: 400 });
273
+ }
208
274
  const target = resolve(root, rel);
209
- if (target !== root && !target.startsWith(root + sep)) {
275
+ if (!isWithinDir(root, target)) {
210
276
  return new Response("Forbidden", { status: 403 });
211
277
  }
212
- const file = Bun.file(target);
213
- if (!await file.exists()) {
278
+ const info = await stat(target).catch(() => null);
279
+ if (!info?.isFile()) {
214
280
  return new Response("Not found", { status: 404 });
215
281
  }
216
- return new Response(file);
282
+ const body = await readFile(target);
283
+ return new Response(body, {
284
+ headers: {
285
+ "Content-Type": STATIC_MIME[extname(target).toLowerCase()] ?? "application/octet-stream",
286
+ "X-Content-Type-Options": "nosniff"
287
+ }
288
+ });
217
289
  }
218
290
  };
219
291
  }
@@ -235,13 +307,16 @@ async function readJsonBody(req) {
235
307
  const text = await req.text();
236
308
  if (text.trim() === "")
237
309
  return {};
310
+ if (!req.headers.get("content-type")?.includes("application/json")) {
311
+ badRequest("Request body must be application/json");
312
+ }
238
313
  try {
239
- return JSON.parse(text);
314
+ return safeJsonParse(text);
240
315
  } catch {
241
316
  badRequest("Invalid JSON body");
242
317
  }
243
318
  }
244
- async function buildContext(req, url, method, pathParams, traceId) {
319
+ async function buildContext(req, url, method, pathParams, traceId, clientIp) {
245
320
  const parsedParams = method.paramsSchema ? method.paramsSchema.parse(pathParams) : undefined;
246
321
  let parsedInput;
247
322
  let file;
@@ -265,7 +340,7 @@ async function buildContext(req, url, method, pathParams, traceId) {
265
340
  }
266
341
  const safePathParams = {};
267
342
  for (const [k, v] of Object.entries(pathParams)) {
268
- if (!RESERVED_KEYS.has(k))
343
+ if (!RESERVED_KEYS.has(k) && !isUnsafeKey(k))
269
344
  safePathParams[k] = v;
270
345
  }
271
346
  return {
@@ -278,10 +353,10 @@ async function buildContext(req, url, method, pathParams, traceId) {
278
353
  headers: req.headers,
279
354
  ...safePathParams,
280
355
  traceId,
281
- ...getClientInfo(req)
356
+ ...getClientInfo(req, clientIp)
282
357
  };
283
358
  }
284
- function buildErrorContext(req, url, traceId) {
359
+ function buildErrorContext(req, url, traceId, clientIp) {
285
360
  return {
286
361
  params: undefined,
287
362
  input: undefined,
@@ -290,7 +365,7 @@ function buildErrorContext(req, url, traceId) {
290
365
  url,
291
366
  headers: req.headers,
292
367
  traceId,
293
- ...getClientInfo(req)
368
+ ...getClientInfo(req, clientIp)
294
369
  };
295
370
  }
296
371
 
@@ -326,7 +401,10 @@ function timestamp() {
326
401
  return `${t}.${now.getMilliseconds().toString().padStart(3, "0")}`;
327
402
  }
328
403
  function elapsedMs(startTime) {
329
- return Number(process.hrtime.bigint() - startTime) / 1e6;
404
+ return performance.now() - startTime;
405
+ }
406
+ function safePath(pathname) {
407
+ return pathname.replace(/[-]/g, "");
330
408
  }
331
409
  function levelForStatus(status) {
332
410
  if (status >= 500)
@@ -372,15 +450,15 @@ function shouldLog(pathname, method) {
372
450
  return false;
373
451
  return !SKIP_PREFIXES.some((prefix) => pathname.startsWith(prefix));
374
452
  }
375
- function logIncoming(req, pathname, traceId) {
376
- const log = { traceId, startTime: process.hrtime.bigint() };
453
+ function logIncoming(req, pathname, traceId, ipAddress) {
454
+ const log = { traceId, startTime: performance.now() };
377
455
  if (!isProd) {
378
456
  const mc = METHOD_COLOR[req.method] ?? c.dim;
379
- console.log(`${c.gray}[${timestamp()}]${c.reset} ${mc}${req.method}${c.reset} ${c.dim}${traceId}${c.reset} ${c.cyan}→${c.reset} ${pathname} ${ipLabel(extractIp(req))}`);
457
+ console.log(`${c.gray}[${timestamp()}]${c.reset} ${mc}${req.method}${c.reset} ${c.dim}${traceId}${c.reset} ${c.cyan}→${c.reset} ${safePath(pathname)} ${ipLabel(ipAddress ?? "")}`);
380
458
  }
381
459
  return log;
382
460
  }
383
- function logOutgoing(req, pathname, status, log) {
461
+ function logOutgoing(req, pathname, status, log, ipAddress) {
384
462
  const ms = elapsedMs(log.startTime);
385
463
  if (isProd) {
386
464
  console.log(JSON.stringify({
@@ -388,42 +466,45 @@ function logOutgoing(req, pathname, status, log) {
388
466
  level: levelForStatus(status),
389
467
  msg: `${req.method} ${pathname} ${status}`,
390
468
  ...buildLogFields(req.method, pathname, status, Math.round(ms), log.traceId),
391
- ip: extractIp(req) || undefined
469
+ ip: ipAddress
392
470
  }));
393
471
  return;
394
472
  }
395
473
  const mc = METHOD_COLOR[req.method] ?? c.dim;
396
- console.log(`${c.gray}[${timestamp()}]${c.reset} ${mc}${req.method}${c.reset} ${c.dim}${log.traceId}${c.reset} ${c.cyan}←${c.reset} ${pathname} ${statusColor(status)}${status}${c.reset} ${durationColor(ms)}${formatMs(ms)}${c.reset} ${ipLabel(extractIp(req))}`);
474
+ console.log(`${c.gray}[${timestamp()}]${c.reset} ${mc}${req.method}${c.reset} ${c.dim}${log.traceId}${c.reset} ${c.cyan}←${c.reset} ${safePath(pathname)} ${statusColor(status)}${status}${c.reset} ${durationColor(ms)}${formatMs(ms)}${c.reset} ${ipLabel(ipAddress ?? "")}`);
397
475
  }
398
476
 
399
477
  // src/server/create.ts
400
478
  function createHandler(config) {
401
- const { cors, hooks, logging = false } = config;
479
+ const { cors, hooks, logging = false, trustProxy = false } = config;
480
+ if (cors)
481
+ assertCorsConfig(cors);
402
482
  const customLogger = typeof logging === "object" ? logging : null;
403
483
  const useDefaultLog = logging === true;
404
484
  const resolveId = config.traceId ?? resolveTraceId;
405
485
  const routeMap = buildRouteMap(normalizeGroups(config));
406
486
  validateRoutes(routeMap);
407
- async function dispatch(req, url, traceId, server) {
487
+ async function dispatch(req, url, traceId, server, clientIp) {
408
488
  const shouldLogRequest = logging && shouldLog(url.pathname, req.method);
489
+ const ipAddress = extractIp(req, clientIp) || undefined;
409
490
  let reqLog;
410
491
  if (shouldLogRequest && useDefaultLog) {
411
- reqLog = logIncoming(req, url.pathname, traceId);
492
+ reqLog = logIncoming(req, url.pathname, traceId, ipAddress);
412
493
  }
413
494
  if (shouldLogRequest && customLogger) {
414
495
  customLogger.debug(`${req.method} ${url.pathname}`, {
415
496
  traceId,
416
497
  method: req.method,
417
498
  path: url.pathname,
418
- ip: extractIp(req) || undefined
499
+ ip: ipAddress
419
500
  });
420
- reqLog = { traceId, startTime: process.hrtime.bigint() };
501
+ reqLog = { traceId, startTime: performance.now() };
421
502
  }
422
503
  const logDone = (status) => {
423
504
  if (!reqLog)
424
505
  return;
425
506
  if (useDefaultLog)
426
- logOutgoing(req, url.pathname, status, reqLog);
507
+ logOutgoing(req, url.pathname, status, reqLog, ipAddress);
427
508
  if (customLogger) {
428
509
  const durationMs = Math.round(elapsedMs(reqLog.startTime));
429
510
  const level = levelForStatus(status);
@@ -433,7 +514,7 @@ function createHandler(config) {
433
514
  const respondError = async (err, errCtx, endpoint) => {
434
515
  if (hooks?.onError) {
435
516
  try {
436
- const response = await hooks.onError(errCtx ?? buildErrorContext(req, url, traceId), err, endpoint);
517
+ const response = await hooks.onError(errCtx ?? buildErrorContext(req, url, traceId, clientIp), err, endpoint);
437
518
  if (response instanceof Response) {
438
519
  const withCors = applyCors(response, cors, req);
439
520
  logDone(withCors.status);
@@ -463,7 +544,8 @@ function createHandler(config) {
463
544
  try {
464
545
  const res = await rawMatch.route.handler(req, {
465
546
  params: rawMatch.params,
466
- server
547
+ server,
548
+ ipAddress
467
549
  });
468
550
  const withCors = applyCors(res, cors, req);
469
551
  logDone(withCors.status);
@@ -486,7 +568,7 @@ function createHandler(config) {
486
568
  const { method, pathParams, groupHooks } = match;
487
569
  let ctx;
488
570
  try {
489
- ctx = await buildContext(req, url, method, pathParams, traceId);
571
+ ctx = await buildContext(req, url, method, pathParams, traceId, clientIp);
490
572
  if (hooks?.beforeHandle) {
491
573
  await hooks.beforeHandle(ctx, method);
492
574
  }
@@ -505,7 +587,11 @@ function createHandler(config) {
505
587
  result = transformed;
506
588
  }
507
589
  if (method.outputSchema) {
508
- result = method.outputSchema.parse(result);
590
+ const checked = validateHandlerOutput(method.outputSchema, result);
591
+ if (!checked.ok) {
592
+ throw new AppError("INTERNAL_SERVER_ERROR", checked.message, 500);
593
+ }
594
+ result = checked.data;
509
595
  }
510
596
  if (result === undefined || result === null) {
511
597
  logDone(204);
@@ -520,10 +606,14 @@ function createHandler(config) {
520
606
  return async (req, server) => {
521
607
  const url = new URL(req.url, "http://localhost");
522
608
  const traceId = resolveId(req);
523
- const response = await dispatch(req, url, traceId, server);
524
- if (!response.headers.has("x-request-id")) {
609
+ const clientIp = {
610
+ trustProxy,
611
+ socketIp: resolveSocketIp(req, server)
612
+ };
613
+ const response = await dispatch(req, url, traceId, server, clientIp);
614
+ try {
525
615
  response.headers.set("x-request-id", traceId);
526
- }
616
+ } catch {}
527
617
  return response;
528
618
  };
529
619
  }
@@ -0,0 +1,9 @@
1
+ // src/internal/safe-json.ts
2
+ function isUnsafeKey(key) {
3
+ return key === "__proto__";
4
+ }
5
+ function safeJsonParse(text) {
6
+ return JSON.parse(text, (key, value) => isUnsafeKey(key) ? undefined : value);
7
+ }
8
+
9
+ export { isUnsafeKey, safeJsonParse };
@@ -0,0 +1,60 @@
1
+ import {
2
+ isUnsafeKey
3
+ } from "./index-kzfs85xp.js";
4
+ import {
5
+ isRecord
6
+ } from "./index-809wc1tt.js";
7
+
8
+ // src/server/request.ts
9
+ function generateTraceId() {
10
+ return `${Date.now().toString(36)}-${crypto.randomUUID().slice(0, 8)}`;
11
+ }
12
+ function resolveTraceId(req) {
13
+ const header = req.headers.get("x-request-id") ?? req.headers.get("x-trace-id");
14
+ const trimmed = header?.trim();
15
+ if (trimmed && trimmed.length <= 128 && /^[\w.-]+$/.test(trimmed)) {
16
+ return trimmed;
17
+ }
18
+ return generateTraceId();
19
+ }
20
+ function resolveSocketIp(req, server) {
21
+ if (typeof server === "object" && server !== null && "requestIP" in server && typeof server.requestIP === "function") {
22
+ const addr = server.requestIP(req);
23
+ if (isRecord(addr) && typeof addr.address === "string" && addr.address) {
24
+ return addr.address;
25
+ }
26
+ }
27
+ if ("ip" in req && typeof req.ip === "string" && req.ip)
28
+ return req.ip;
29
+ return;
30
+ }
31
+ function extractIp(req, options = {}) {
32
+ if (options.trustProxy) {
33
+ const forwarded = req.headers.get("x-forwarded-for");
34
+ if (forwarded)
35
+ return (forwarded.split(",")[0] ?? "").trim().replace(/^::ffff:/, "");
36
+ const realIp = req.headers.get("x-real-ip");
37
+ if (realIp)
38
+ return realIp.trim().replace(/^::ffff:/, "");
39
+ }
40
+ return (options.socketIp ?? "").replace(/^::ffff:/, "");
41
+ }
42
+ function getClientInfo(req, options = {}) {
43
+ return {
44
+ ipAddress: extractIp(req, options) || undefined,
45
+ userAgent: req.headers.get("user-agent") ?? undefined
46
+ };
47
+ }
48
+ function parseQueryParams(url) {
49
+ const query = {};
50
+ for (const key of new Set(url.searchParams.keys())) {
51
+ if (isUnsafeKey(key))
52
+ continue;
53
+ const values = url.searchParams.getAll(key);
54
+ const [first] = values;
55
+ query[key] = values.length === 1 && first !== undefined ? first : values;
56
+ }
57
+ return query;
58
+ }
59
+
60
+ export { generateTraceId, resolveTraceId, resolveSocketIp, extractIp, getClientInfo, parseQueryParams };
@@ -0,0 +1,7 @@
1
+ // src/internal/within-dir.ts
2
+ import { sep } from "node:path";
3
+ function isWithinDir(root, target) {
4
+ return target === root || target.startsWith(root + sep);
5
+ }
6
+
7
+ export { isWithinDir };
package/dist/index.js CHANGED
@@ -3,11 +3,11 @@ import {
3
3
  createClient,
4
4
  createClients,
5
5
  createHttpClient
6
- } from "./index-5sxnvwb1.js";
6
+ } from "./index-4kz3aqpx.js";
7
7
  import {
8
8
  parseSSE
9
- } from "./index-n7bmdwmz.js";
10
- import"./index-809wc1tt.js";
9
+ } from "./index-4b1j82gp.js";
10
+ import"./index-0t460v7k.js";
11
11
  import {
12
12
  ALL_TRANSPORTS,
13
13
  AppError,
@@ -21,6 +21,7 @@ import {
21
21
  rateLimited,
22
22
  unauthorized
23
23
  } from "./index-kckky6zw.js";
24
+ import"./index-809wc1tt.js";
24
25
  // src/browser/socket-io.ts
25
26
  import { io } from "socket.io-client";
26
27
  function createSocketIOClient(config) {
@@ -1,5 +1,17 @@
1
- import { z } from 'zod';
1
+ import { type ZodType, z } from 'zod';
2
2
  import { AppError } from '../contract';
3
3
  export declare function formatZodError(error: z.ZodError): string;
4
4
  export declare function normalizeError(err: unknown): AppError;
5
+ /**
6
+ * Validate a handler's return value against the contract `output` schema. A
7
+ * mismatch is a **server** fault (the handler broke its own contract) — shared
8
+ * by the HTTP and tool transports so both report it identically.
9
+ */
10
+ export declare function validateHandlerOutput(schema: ZodType, data: unknown): {
11
+ ok: true;
12
+ data: unknown;
13
+ } | {
14
+ ok: false;
15
+ message: string;
16
+ };
5
17
  //# sourceMappingURL=errors.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../../src/internal/errors.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACxB,OAAO,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AAEvC,wBAAgB,cAAc,CAAC,KAAK,EAAE,CAAC,CAAC,QAAQ,GAAG,MAAM,CASxD;AAED,wBAAgB,cAAc,CAAC,GAAG,EAAE,OAAO,GAAG,QAAQ,CAWrD"}
1
+ {"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../../src/internal/errors.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACtC,OAAO,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AAEvC,wBAAgB,cAAc,CAAC,KAAK,EAAE,CAAC,CAAC,QAAQ,GAAG,MAAM,CASxD;AAED,wBAAgB,cAAc,CAAC,GAAG,EAAE,OAAO,GAAG,QAAQ,CAYrD;AAED;;;;GAIG;AACH,wBAAgB,qBAAqB,CACnC,MAAM,EAAE,OAAO,EACf,IAAI,EAAE,OAAO,GACZ;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,IAAI,EAAE,OAAO,CAAA;CAAE,GAAG;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CAO9D"}
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Prototype-pollution defence for untrusted input. Assigning a `__proto__` key
3
+ * onto a plain object (`obj[key] = value`, a JSON reviver, a merge) invokes the
4
+ * `__proto__` setter and rewires the prototype chain — the single real
5
+ * pollution vector at an assignment boundary. `constructor` / `prototype` as
6
+ * plain own keys are inert (they shadow, they do not poison), so they are NOT
7
+ * stripped — dropping them would silently corrupt legitimate data fields.
8
+ *
9
+ * Every boundary that ingests client-controlled keys (JSON bodies, query
10
+ * strings, cookies, multipart fields, tool arguments) routes through here.
11
+ */
12
+ /** True for a key that must never be copied onto a plain object from untrusted input. */
13
+ export declare function isUnsafeKey(key: string): boolean;
14
+ /**
15
+ * `JSON.parse` that strips the `__proto__` key from the result. Throws on
16
+ * invalid JSON, exactly like `JSON.parse` — callers handle that as a 400.
17
+ */
18
+ export declare function safeJsonParse(text: string): unknown;
19
+ //# sourceMappingURL=safe-json.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"safe-json.d.ts","sourceRoot":"","sources":["../../src/internal/safe-json.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,yFAAyF;AACzF,wBAAgB,WAAW,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAEhD;AAED;;;GAGG;AACH,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAEnD"}
@@ -0,0 +1,7 @@
1
+ /**
2
+ * True when `target` (an already-`resolve`d absolute path) stays inside `root`.
3
+ * Blocks `..`, absolute and otherwise-escaping paths — the shared containment
4
+ * check behind `staticRoute` and `view_file`'s local-file branch.
5
+ */
6
+ export declare function isWithinDir(root: string, target: string): boolean;
7
+ //# sourceMappingURL=within-dir.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"within-dir.d.ts","sourceRoot":"","sources":["../../src/internal/within-dir.ts"],"names":[],"mappings":"AAEA;;;;GAIG;AACH,wBAAgB,WAAW,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAEjE"}