stitchkit 0.2.0 → 0.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.
Files changed (136) hide show
  1. package/README.md +11 -10
  2. package/dist/browser/client.d.ts.map +1 -1
  3. package/dist/browser/http.d.ts +5 -0
  4. package/dist/browser/http.d.ts.map +1 -1
  5. package/dist/browser/socket-io.d.ts +17 -0
  6. package/dist/browser/socket-io.d.ts.map +1 -1
  7. package/dist/cli.d.ts +23 -0
  8. package/dist/cli.d.ts.map +1 -0
  9. package/dist/cli.js +21 -0
  10. package/dist/contract/define.d.ts +35 -2
  11. package/dist/contract/define.d.ts.map +1 -1
  12. package/dist/contract/index.d.ts +1 -1
  13. package/dist/contract/index.d.ts.map +1 -1
  14. package/dist/contract/index.js +2 -1
  15. package/dist/index-0ed3bx43.js +33 -0
  16. package/dist/index-0ma1eqv4.js +1007 -0
  17. package/dist/index-1cf8jkhf.js +219 -0
  18. package/dist/index-37x76zdn.js +4 -0
  19. package/dist/{index-v2z2v3mq.js → index-3qmtdfpa.js} +304 -62
  20. package/dist/index-48ffdxgk.js +6 -0
  21. package/dist/{index-5sxnvwb1.js → index-78q1qm7v.js} +6 -3
  22. package/dist/{index-kckky6zw.js → index-afzt3nmx.js} +1 -1
  23. package/dist/{index-n7bmdwmz.js → index-d4rwrjbc.js} +8 -4
  24. package/dist/index-kzfs85xp.js +9 -0
  25. package/dist/index-mwmpw6j1.js +60 -0
  26. package/dist/{index-a35v22fh.js → index-vhdvv00d.js} +13 -5
  27. package/dist/index-x3fcszf8.js +8 -0
  28. package/dist/index.js +17 -4
  29. package/dist/internal/errors.d.ts +13 -1
  30. package/dist/internal/errors.d.ts.map +1 -1
  31. package/dist/internal/http-input.d.ts +13 -0
  32. package/dist/internal/http-input.d.ts.map +1 -0
  33. package/dist/internal/safe-json.d.ts +19 -0
  34. package/dist/internal/safe-json.d.ts.map +1 -0
  35. package/dist/internal/secure-fetch.d.ts +27 -0
  36. package/dist/internal/secure-fetch.d.ts.map +1 -0
  37. package/dist/internal/within-dir.d.ts +7 -0
  38. package/dist/internal/within-dir.d.ts.map +1 -0
  39. package/dist/node.d.ts +10 -1
  40. package/dist/node.d.ts.map +1 -1
  41. package/dist/node.js +41 -7
  42. package/dist/observability/audit.d.ts.map +1 -1
  43. package/dist/observability/context.d.ts +9 -1
  44. package/dist/observability/context.d.ts.map +1 -1
  45. package/dist/observability/event.d.ts +4 -0
  46. package/dist/observability/event.d.ts.map +1 -1
  47. package/dist/observability/index.js +38 -15
  48. package/dist/observability/sanitize.d.ts.map +1 -1
  49. package/dist/react.js +2 -0
  50. package/dist/server/context.d.ts +3 -2
  51. package/dist/server/context.d.ts.map +1 -1
  52. package/dist/server/create.d.ts.map +1 -1
  53. package/dist/server/event-bus.d.ts +10 -1
  54. package/dist/server/event-bus.d.ts.map +1 -1
  55. package/dist/server/implement.d.ts.map +1 -1
  56. package/dist/server/index.d.ts +6 -3
  57. package/dist/server/index.d.ts.map +1 -1
  58. package/dist/server/index.js +203 -193
  59. package/dist/server/logger.d.ts +5 -4
  60. package/dist/server/logger.d.ts.map +1 -1
  61. package/dist/server/middleware/auth.d.ts +41 -1
  62. package/dist/server/middleware/auth.d.ts.map +1 -1
  63. package/dist/server/middleware/cookies.d.ts.map +1 -1
  64. package/dist/server/middleware/cors.d.ts +7 -0
  65. package/dist/server/middleware/cors.d.ts.map +1 -1
  66. package/dist/server/middleware/pkce.d.ts +17 -0
  67. package/dist/server/middleware/pkce.d.ts.map +1 -0
  68. package/dist/server/multipart.d.ts +2 -1
  69. package/dist/server/multipart.d.ts.map +1 -1
  70. package/dist/server/node.d.ts +9 -0
  71. package/dist/server/node.d.ts.map +1 -1
  72. package/dist/server/openapi.d.ts +52 -0
  73. package/dist/server/openapi.d.ts.map +1 -0
  74. package/dist/server/rate-limit.d.ts +7 -2
  75. package/dist/server/rate-limit.d.ts.map +1 -1
  76. package/dist/server/request.d.ts +27 -4
  77. package/dist/server/request.d.ts.map +1 -1
  78. package/dist/server/router.d.ts +4 -2
  79. package/dist/server/router.d.ts.map +1 -1
  80. package/dist/server/socket-io.d.ts +60 -15
  81. package/dist/server/socket-io.d.ts.map +1 -1
  82. package/dist/server/stream.d.ts +3 -1
  83. package/dist/server/stream.d.ts.map +1 -1
  84. package/dist/server/types.d.ts +17 -1
  85. package/dist/server/types.d.ts.map +1 -1
  86. package/dist/server/websocket.d.ts +73 -0
  87. package/dist/server/websocket.d.ts.map +1 -0
  88. package/dist/tools/cli-args.d.ts +68 -0
  89. package/dist/tools/cli-args.d.ts.map +1 -0
  90. package/dist/tools/cli-format.d.ts +39 -0
  91. package/dist/tools/cli-format.d.ts.map +1 -0
  92. package/dist/tools/cli-wait.d.ts +45 -0
  93. package/dist/tools/cli-wait.d.ts.map +1 -0
  94. package/dist/tools/cli.d.ts +77 -0
  95. package/dist/tools/cli.d.ts.map +1 -0
  96. package/dist/tools/coerce.d.ts +8 -4
  97. package/dist/tools/coerce.d.ts.map +1 -1
  98. package/dist/tools/execute.d.ts +1 -1
  99. package/dist/tools/execute.d.ts.map +1 -1
  100. package/dist/tools/json-schema.d.ts +29 -4
  101. package/dist/tools/json-schema.d.ts.map +1 -1
  102. package/dist/tools/manifest.d.ts +4 -0
  103. package/dist/tools/manifest.d.ts.map +1 -1
  104. package/dist/tools/mcp-app.d.ts +59 -0
  105. package/dist/tools/mcp-app.d.ts.map +1 -0
  106. package/dist/tools/mcp-handler.d.ts +18 -0
  107. package/dist/tools/mcp-handler.d.ts.map +1 -1
  108. package/dist/tools/mcp.d.ts +9 -0
  109. package/dist/tools/mcp.d.ts.map +1 -1
  110. package/dist/tools/mount-download.d.ts +30 -0
  111. package/dist/tools/mount-download.d.ts.map +1 -0
  112. package/dist/tools/mount-upload.d.ts +19 -0
  113. package/dist/tools/mount-upload.d.ts.map +1 -0
  114. package/dist/tools/mount-wait.d.ts +36 -0
  115. package/dist/tools/mount-wait.d.ts.map +1 -0
  116. package/dist/tools/mount.d.ts +8 -4
  117. package/dist/tools/mount.d.ts.map +1 -1
  118. package/dist/tools/native-result.d.ts +17 -0
  119. package/dist/tools/native-result.d.ts.map +1 -0
  120. package/dist/tools/oauth-metadata.d.ts +28 -0
  121. package/dist/tools/oauth-metadata.d.ts.map +1 -0
  122. package/dist/tools/oauth-provider.d.ts +85 -0
  123. package/dist/tools/oauth-provider.d.ts.map +1 -0
  124. package/dist/tools/remote.d.ts +2 -2
  125. package/dist/tools/remote.d.ts.map +1 -1
  126. package/dist/tools/toolkit.d.ts +51 -0
  127. package/dist/tools/toolkit.d.ts.map +1 -0
  128. package/dist/tools/view-file.d.ts +12 -0
  129. package/dist/tools/view-file.d.ts.map +1 -1
  130. package/dist/tools/wait-core.d.ts +33 -0
  131. package/dist/tools/wait-core.d.ts.map +1 -0
  132. package/dist/tools.d.ts +13 -2
  133. package/dist/tools.d.ts.map +1 -1
  134. package/dist/tools.js +637 -424
  135. package/package.json +22 -9
  136. package/dist/index-ke4mx4ea.js +0 -38
@@ -1,70 +1,121 @@
1
1
  import {
2
- normalizeError
3
- } from "./index-a35v22fh.js";
2
+ isWithinDir
3
+ } from "./index-x3fcszf8.js";
4
+ import {
5
+ normalizeError,
6
+ validateHandlerOutput
7
+ } from "./index-vhdvv00d.js";
4
8
  import {
5
9
  AppError,
6
10
  badRequest
7
- } from "./index-kckky6zw.js";
11
+ } from "./index-afzt3nmx.js";
8
12
  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";
23
+ import {
24
+ typedEntries
25
+ } from "./index-809wc1tt.js";
26
+ import {
27
+ __require
28
+ } from "./index-37x76zdn.js";
14
29
 
15
30
  // src/server/multipart.ts
16
31
  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`);
32
+ async function readBodyCapped(req, maxBytes) {
33
+ const reader = req.body?.getReader();
34
+ if (!reader)
35
+ return new Uint8Array(0);
36
+ const chunks = [];
37
+ let total = 0;
38
+ try {
39
+ while (true) {
40
+ const { done, value } = await reader.read();
41
+ if (done)
42
+ break;
43
+ total += value.length;
44
+ if (total > maxBytes) {
45
+ await reader.cancel();
46
+ badRequest(`Upload exceeds the ${Math.round(maxBytes / 1024 / 1024)} MB limit`);
47
+ }
48
+ chunks.push(value);
49
+ }
50
+ } finally {
51
+ reader.releaseLock();
52
+ }
53
+ const buffer = new Uint8Array(total);
54
+ let offset = 0;
55
+ for (const chunk of chunks) {
56
+ buffer.set(chunk, offset);
57
+ offset += chunk.length;
21
58
  }
22
- const formData = await req.formData();
59
+ return buffer;
60
+ }
61
+ async function parseMultipart(req, fileField, fieldsSchema, maxBytes = DEFAULT_MAX_UPLOAD_BYTES) {
62
+ const body = await readBodyCapped(req, maxBytes);
63
+ const contentType = req.headers.get("content-type") ?? "";
64
+ const formData = await new Response(body, {
65
+ headers: { "content-type": contentType }
66
+ }).formData();
23
67
  const file = formData.get(fileField);
24
68
  if (!file || !(file instanceof File)) {
25
69
  badRequest(`Missing file field: ${fileField}`);
26
70
  }
27
- if (file.size > maxBytes) {
28
- badRequest(`Upload exceeds the ${Math.round(maxBytes / 1024 / 1024)} MB limit`);
29
- }
30
71
  const fields = {};
31
72
  for (const [key, value] of formData.entries()) {
32
73
  if (key === fileField)
33
74
  continue;
75
+ if (isUnsafeKey(key))
76
+ continue;
34
77
  if (typeof value === "string") {
35
78
  try {
36
- fields[key] = JSON.parse(value);
79
+ fields[key] = safeJsonParse(value);
37
80
  } catch {
38
81
  fields[key] = value;
39
82
  }
40
83
  }
41
84
  }
42
- const parsed = fieldsSchema ? fieldsSchema.parse(fields) : fields;
43
- return { file, fields: parsed };
85
+ return { file, fields: fieldsSchema ? fieldsSchema.parse(fields) : fields };
44
86
  }
45
87
 
46
88
  // src/server/middleware/cors.ts
47
- function corsHeaders(config, requestOrigin) {
48
- let allowOrigin;
89
+ function assertCorsConfig(config) {
90
+ if (config.credentials && (config.origin === undefined || config.origin === "*")) {
91
+ throw new Error("[stitchkit] cors: `credentials: true` cannot be combined with a wildcard origin. " + "Set `origin` to an explicit string or list.");
92
+ }
93
+ }
94
+ function resolveOrigin(config, requestOrigin) {
49
95
  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;
96
+ return "*";
97
+ }
98
+ if (Array.isArray(config.origin)) {
99
+ if (!requestOrigin)
100
+ return;
101
+ const lower = requestOrigin.toLowerCase();
102
+ return config.origin.some((o) => o.toLowerCase() === lower) ? requestOrigin : undefined;
55
103
  }
104
+ return config.origin;
105
+ }
106
+ function corsHeaders(config, requestOrigin) {
107
+ const allowOrigin = resolveOrigin(config, requestOrigin);
56
108
  const headers = {
57
109
  "Access-Control-Allow-Methods": config.methods ?? "GET, POST, PUT, PATCH, DELETE, OPTIONS",
58
110
  "Access-Control-Allow-Headers": config.headers ?? "Content-Type, Authorization, X-Trace-Id"
59
111
  };
60
112
  if (allowOrigin !== undefined) {
61
113
  headers["Access-Control-Allow-Origin"] = allowOrigin;
114
+ if (config.credentials) {
115
+ headers["Access-Control-Allow-Credentials"] = "true";
116
+ }
62
117
  }
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) {
118
+ if (Array.isArray(config.origin)) {
68
119
  headers.Vary = "Origin";
69
120
  }
70
121
  return headers;
@@ -77,7 +128,8 @@ function corsPreflightResponse(config, req) {
77
128
  }
78
129
 
79
130
  // src/server/router.ts
80
- import { resolve, sep } from "node:path";
131
+ import { readFile, stat } from "node:fs/promises";
132
+ import { extname, resolve } from "node:path";
81
133
  function joinPath(...parts) {
82
134
  const joined = parts.filter(Boolean).map((part) => part.replace(/^\/+|\/+$/g, "")).filter(Boolean).join("/");
83
135
  return `/${joined}`;
@@ -195,25 +247,51 @@ function matchRawRoute(rawRoutes, httpMethod, pathname) {
195
247
  }
196
248
  return null;
197
249
  }
250
+ var STATIC_MIME = {
251
+ ".html": "text/html; charset=utf-8",
252
+ ".css": "text/css; charset=utf-8",
253
+ ".js": "text/javascript; charset=utf-8",
254
+ ".mjs": "text/javascript; charset=utf-8",
255
+ ".json": "application/json; charset=utf-8",
256
+ ".svg": "image/svg+xml",
257
+ ".png": "image/png",
258
+ ".jpg": "image/jpeg",
259
+ ".jpeg": "image/jpeg",
260
+ ".gif": "image/gif",
261
+ ".webp": "image/webp",
262
+ ".ico": "image/x-icon",
263
+ ".woff2": "font/woff2",
264
+ ".txt": "text/plain; charset=utf-8"
265
+ };
198
266
  function staticRoute(prefix, dir) {
199
267
  const cleanPrefix = prefix.replace(/\/+$/, "");
200
- const cleanDir = dir.replace(/\/+$/, "");
268
+ const root = resolve(dir.replace(/\/+$/, ""));
201
269
  return {
202
270
  method: "GET",
203
271
  path: `${cleanPrefix}/*`,
204
272
  handler: async (req) => {
205
273
  const pathname = new URL(req.url).pathname;
206
- const rel = pathname.slice(cleanPrefix.length).replace(/^\/+/, "");
207
- const root = resolve(cleanDir);
274
+ let rel;
275
+ try {
276
+ rel = decodeURIComponent(pathname.slice(cleanPrefix.length)).replace(/^\/+/, "");
277
+ } catch {
278
+ return new Response("Bad request", { status: 400 });
279
+ }
208
280
  const target = resolve(root, rel);
209
- if (target !== root && !target.startsWith(root + sep)) {
281
+ if (!isWithinDir(root, target)) {
210
282
  return new Response("Forbidden", { status: 403 });
211
283
  }
212
- const file = Bun.file(target);
213
- if (!await file.exists()) {
284
+ const info = await stat(target).catch(() => null);
285
+ if (!info?.isFile()) {
214
286
  return new Response("Not found", { status: 404 });
215
287
  }
216
- return new Response(file);
288
+ const body = await readFile(target);
289
+ return new Response(body, {
290
+ headers: {
291
+ "Content-Type": STATIC_MIME[extname(target).toLowerCase()] ?? "application/octet-stream",
292
+ "X-Content-Type-Options": "nosniff"
293
+ }
294
+ });
217
295
  }
218
296
  };
219
297
  }
@@ -235,13 +313,16 @@ async function readJsonBody(req) {
235
313
  const text = await req.text();
236
314
  if (text.trim() === "")
237
315
  return {};
316
+ if (!req.headers.get("content-type")?.includes("application/json")) {
317
+ badRequest("Request body must be application/json");
318
+ }
238
319
  try {
239
- return JSON.parse(text);
320
+ return safeJsonParse(text);
240
321
  } catch {
241
322
  badRequest("Invalid JSON body");
242
323
  }
243
324
  }
244
- async function buildContext(req, url, method, pathParams, traceId) {
325
+ async function buildContext(req, url, method, pathParams, traceId, clientIp) {
245
326
  const parsedParams = method.paramsSchema ? method.paramsSchema.parse(pathParams) : undefined;
246
327
  let parsedInput;
247
328
  let file;
@@ -265,7 +346,7 @@ async function buildContext(req, url, method, pathParams, traceId) {
265
346
  }
266
347
  const safePathParams = {};
267
348
  for (const [k, v] of Object.entries(pathParams)) {
268
- if (!RESERVED_KEYS.has(k))
349
+ if (!RESERVED_KEYS.has(k) && !isUnsafeKey(k))
269
350
  safePathParams[k] = v;
270
351
  }
271
352
  return {
@@ -278,10 +359,10 @@ async function buildContext(req, url, method, pathParams, traceId) {
278
359
  headers: req.headers,
279
360
  ...safePathParams,
280
361
  traceId,
281
- ...getClientInfo(req)
362
+ ...getClientInfo(req, clientIp)
282
363
  };
283
364
  }
284
- function buildErrorContext(req, url, traceId) {
365
+ function buildErrorContext(req, url, traceId, clientIp) {
285
366
  return {
286
367
  params: undefined,
287
368
  input: undefined,
@@ -290,7 +371,7 @@ function buildErrorContext(req, url, traceId) {
290
371
  url,
291
372
  headers: req.headers,
292
373
  traceId,
293
- ...getClientInfo(req)
374
+ ...getClientInfo(req, clientIp)
294
375
  };
295
376
  }
296
377
 
@@ -326,7 +407,10 @@ function timestamp() {
326
407
  return `${t}.${now.getMilliseconds().toString().padStart(3, "0")}`;
327
408
  }
328
409
  function elapsedMs(startTime) {
329
- return Number(process.hrtime.bigint() - startTime) / 1e6;
410
+ return performance.now() - startTime;
411
+ }
412
+ function safePath(pathname) {
413
+ return pathname.replace(/[-]/g, "");
330
414
  }
331
415
  function levelForStatus(status) {
332
416
  if (status >= 500)
@@ -372,15 +456,15 @@ function shouldLog(pathname, method) {
372
456
  return false;
373
457
  return !SKIP_PREFIXES.some((prefix) => pathname.startsWith(prefix));
374
458
  }
375
- function logIncoming(req, pathname, traceId) {
376
- const log = { traceId, startTime: process.hrtime.bigint() };
459
+ function logIncoming(req, pathname, traceId, ipAddress) {
460
+ const log = { traceId, startTime: performance.now() };
377
461
  if (!isProd) {
378
462
  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))}`);
463
+ 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
464
  }
381
465
  return log;
382
466
  }
383
- function logOutgoing(req, pathname, status, log) {
467
+ function logOutgoing(req, pathname, status, log, ipAddress) {
384
468
  const ms = elapsedMs(log.startTime);
385
469
  if (isProd) {
386
470
  console.log(JSON.stringify({
@@ -388,42 +472,45 @@ function logOutgoing(req, pathname, status, log) {
388
472
  level: levelForStatus(status),
389
473
  msg: `${req.method} ${pathname} ${status}`,
390
474
  ...buildLogFields(req.method, pathname, status, Math.round(ms), log.traceId),
391
- ip: extractIp(req) || undefined
475
+ ip: ipAddress
392
476
  }));
393
477
  return;
394
478
  }
395
479
  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))}`);
480
+ 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
481
  }
398
482
 
399
483
  // src/server/create.ts
400
484
  function createHandler(config) {
401
- const { cors, hooks, logging = false } = config;
485
+ const { cors, hooks, logging = false, trustProxy = false } = config;
486
+ if (cors)
487
+ assertCorsConfig(cors);
402
488
  const customLogger = typeof logging === "object" ? logging : null;
403
489
  const useDefaultLog = logging === true;
404
490
  const resolveId = config.traceId ?? resolveTraceId;
405
491
  const routeMap = buildRouteMap(normalizeGroups(config));
406
492
  validateRoutes(routeMap);
407
- async function dispatch(req, url, traceId, server) {
493
+ async function dispatch(req, url, traceId, server, clientIp) {
408
494
  const shouldLogRequest = logging && shouldLog(url.pathname, req.method);
495
+ const ipAddress = extractIp(req, clientIp) || undefined;
409
496
  let reqLog;
410
497
  if (shouldLogRequest && useDefaultLog) {
411
- reqLog = logIncoming(req, url.pathname, traceId);
498
+ reqLog = logIncoming(req, url.pathname, traceId, ipAddress);
412
499
  }
413
500
  if (shouldLogRequest && customLogger) {
414
501
  customLogger.debug(`${req.method} ${url.pathname}`, {
415
502
  traceId,
416
503
  method: req.method,
417
504
  path: url.pathname,
418
- ip: extractIp(req) || undefined
505
+ ip: ipAddress
419
506
  });
420
- reqLog = { traceId, startTime: process.hrtime.bigint() };
507
+ reqLog = { traceId, startTime: performance.now() };
421
508
  }
422
509
  const logDone = (status) => {
423
510
  if (!reqLog)
424
511
  return;
425
512
  if (useDefaultLog)
426
- logOutgoing(req, url.pathname, status, reqLog);
513
+ logOutgoing(req, url.pathname, status, reqLog, ipAddress);
427
514
  if (customLogger) {
428
515
  const durationMs = Math.round(elapsedMs(reqLog.startTime));
429
516
  const level = levelForStatus(status);
@@ -433,7 +520,7 @@ function createHandler(config) {
433
520
  const respondError = async (err, errCtx, endpoint) => {
434
521
  if (hooks?.onError) {
435
522
  try {
436
- const response = await hooks.onError(errCtx ?? buildErrorContext(req, url, traceId), err, endpoint);
523
+ const response = await hooks.onError(errCtx ?? buildErrorContext(req, url, traceId, clientIp), err, endpoint);
437
524
  if (response instanceof Response) {
438
525
  const withCors = applyCors(response, cors, req);
439
526
  logDone(withCors.status);
@@ -463,7 +550,8 @@ function createHandler(config) {
463
550
  try {
464
551
  const res = await rawMatch.route.handler(req, {
465
552
  params: rawMatch.params,
466
- server
553
+ server,
554
+ ipAddress
467
555
  });
468
556
  const withCors = applyCors(res, cors, req);
469
557
  logDone(withCors.status);
@@ -486,7 +574,7 @@ function createHandler(config) {
486
574
  const { method, pathParams, groupHooks } = match;
487
575
  let ctx;
488
576
  try {
489
- ctx = await buildContext(req, url, method, pathParams, traceId);
577
+ ctx = await buildContext(req, url, method, pathParams, traceId, clientIp);
490
578
  if (hooks?.beforeHandle) {
491
579
  await hooks.beforeHandle(ctx, method);
492
580
  }
@@ -505,7 +593,11 @@ function createHandler(config) {
505
593
  result = transformed;
506
594
  }
507
595
  if (method.outputSchema) {
508
- result = method.outputSchema.parse(result);
596
+ const checked = validateHandlerOutput(method.outputSchema, result);
597
+ if (!checked.ok) {
598
+ throw new AppError("INTERNAL_SERVER_ERROR", checked.message, 500);
599
+ }
600
+ result = checked.data;
509
601
  }
510
602
  if (result === undefined || result === null) {
511
603
  logDone(204);
@@ -520,10 +612,14 @@ function createHandler(config) {
520
612
  return async (req, server) => {
521
613
  const url = new URL(req.url, "http://localhost");
522
614
  const traceId = resolveId(req);
523
- const response = await dispatch(req, url, traceId, server);
524
- if (!response.headers.has("x-request-id")) {
615
+ const clientIp = {
616
+ trustProxy,
617
+ socketIp: resolveSocketIp(req, server)
618
+ };
619
+ const response = await dispatch(req, url, traceId, server, clientIp);
620
+ try {
525
621
  response.headers.set("x-request-id", traceId);
526
- }
622
+ } catch {}
527
623
  return response;
528
624
  };
529
625
  }
@@ -584,4 +680,150 @@ function json(data, status, cors, req) {
584
680
  return Response.json(data, { status, headers: corsHeaders2(cors, req) });
585
681
  }
586
682
 
587
- export { parseMultipart, corsHeaders, corsPreflightResponse, staticRoute, createHandler, createServer };
683
+ // src/server/implement.ts
684
+ function implement(contract, handlers) {
685
+ const methods = {};
686
+ const groupScope = contract.meta.scope ?? "public";
687
+ for (const [key, endpoint] of typedEntries(contract.endpoints)) {
688
+ const typedHandler = handlers[key];
689
+ methods[String(key)] = {
690
+ method: endpoint.method,
691
+ path: endpoint.path,
692
+ desc: endpoint.desc,
693
+ toolName: "toolName" in endpoint ? endpoint.toolName : undefined,
694
+ expose: endpoint.expose,
695
+ scope: endpoint.scope ?? groupScope,
696
+ paramsSchema: endpoint.params,
697
+ inputSchema: endpoint.input,
698
+ outputSchema: endpoint.output,
699
+ multipart: endpoint.multipart,
700
+ ui: "ui" in endpoint ? endpoint.ui : undefined,
701
+ annotations: "annotations" in endpoint ? endpoint.annotations : undefined,
702
+ handler: (ctx) => typedHandler(ctx)
703
+ };
704
+ }
705
+ return {
706
+ name: contract.meta.prefix,
707
+ prefix: contract.meta.prefix,
708
+ scope: groupScope,
709
+ methods
710
+ };
711
+ }
712
+ function createImplement() {
713
+ return (contract, handlers) => implement(contract, handlers);
714
+ }
715
+
716
+ // src/server/websocket.ts
717
+ function webSocketLane(lane) {
718
+ const { match, handlers } = lane;
719
+ return {
720
+ match,
721
+ handlers: {
722
+ message(ws, message) {
723
+ if (match(ws))
724
+ handlers.message(ws, message);
725
+ },
726
+ open(ws) {
727
+ if (match(ws))
728
+ handlers.open?.(ws);
729
+ },
730
+ close(ws, code, reason) {
731
+ if (match(ws))
732
+ handlers.close?.(ws, code, reason);
733
+ },
734
+ drain(ws) {
735
+ if (match(ws))
736
+ handlers.drain?.(ws);
737
+ },
738
+ ping(ws, data) {
739
+ if (match(ws))
740
+ handlers.ping?.(ws, data);
741
+ },
742
+ pong(ws, data) {
743
+ if (match(ws))
744
+ handlers.pong?.(ws, data);
745
+ }
746
+ }
747
+ };
748
+ }
749
+ function composeWebSocketHandlers(lanes, config) {
750
+ const pick = (ws) => lanes.find((lane) => lane.match(ws));
751
+ return {
752
+ ...config,
753
+ message(ws, message) {
754
+ pick(ws)?.handlers.message(ws, message);
755
+ },
756
+ open(ws) {
757
+ pick(ws)?.handlers.open?.(ws);
758
+ },
759
+ close(ws, code, reason) {
760
+ pick(ws)?.handlers.close?.(ws, code, reason);
761
+ },
762
+ drain(ws) {
763
+ pick(ws)?.handlers.drain?.(ws);
764
+ },
765
+ ping(ws, data) {
766
+ pick(ws)?.handlers.ping?.(ws, data);
767
+ },
768
+ pong(ws, data) {
769
+ pick(ws)?.handlers.pong?.(ws, data);
770
+ }
771
+ };
772
+ }
773
+
774
+ // src/server/socket-io.ts
775
+ var noop = () => {};
776
+ async function createSocketIOServer(config) {
777
+ const path = config.path ?? "/socket.io/";
778
+ const onBun = "Bun" in globalThis;
779
+ const transports = config.transports ?? (onBun ? ["websocket", "polling"] : ["websocket"]);
780
+ const { Server } = await import("socket.io");
781
+ const io = new Server({
782
+ path,
783
+ cors: {
784
+ origin: config.cors.origin,
785
+ credentials: config.cors.credentials ?? true,
786
+ methods: ["GET", "POST"]
787
+ },
788
+ transports,
789
+ pingTimeout: config.pingTimeout ?? 20000,
790
+ pingInterval: config.pingInterval ?? 1e4
791
+ });
792
+ if (onBun) {
793
+ const { Server: Engine } = await import("@socket.io/bun-engine");
794
+ const engine = new Engine({ path });
795
+ io.bind(engine);
796
+ const { websocket } = engine.handler();
797
+ const route = {
798
+ method: "ALL",
799
+ path: `${path.replace(/\/+$/, "")}/*`,
800
+ handler: (req, ctx) => {
801
+ if (!ctx.server) {
802
+ throw new Error("[stitchkit] createSocketIOServer route needs a running Bun server — mount it via createServer.");
803
+ }
804
+ return engine.handleRequest(req, ctx.server);
805
+ }
806
+ };
807
+ return { io, websocket, route, attach: noop };
808
+ }
809
+ return {
810
+ io,
811
+ attach: (server) => io.attach(server),
812
+ websocket: { open: noop, message: noop, close: noop, maxPayloadLength: 0 },
813
+ route: {
814
+ method: "ALL",
815
+ path: `${path.replace(/\/+$/, "")}/*`,
816
+ handler: () => {
817
+ throw new Error("[stitchkit] On Node, Socket.IO attaches via serveNode({ socket }) — this route is not mounted.");
818
+ }
819
+ }
820
+ };
821
+ }
822
+ function socketIoLane(websocket) {
823
+ return webSocketLane({
824
+ match: (_ws) => true,
825
+ handlers: websocket
826
+ });
827
+ }
828
+
829
+ export { parseMultipart, corsHeaders, corsPreflightResponse, staticRoute, createHandler, createServer, implement, createImplement, webSocketLane, composeWebSocketHandlers, createSocketIOServer, socketIoLane };
@@ -0,0 +1,6 @@
1
+ // src/internal/http-input.ts
2
+ function inputIsQuery(method) {
3
+ return method === "GET" || method === "DELETE";
4
+ }
5
+
6
+ export { inputIsQuery };
@@ -1,3 +1,6 @@
1
+ import {
2
+ inputIsQuery
3
+ } from "./index-48ffdxgk.js";
1
4
  import {
2
5
  isRecord,
3
6
  mapObject,
@@ -40,7 +43,7 @@ function createHttpClient(config) {
40
43
  let ssrCookies = null;
41
44
  let isLoggedOut = false;
42
45
  const listeners = new Set;
43
- const authEndpoints = config.authEndpoints ?? ["auth/"];
46
+ const authEndpoints = config.authEndpoints ?? ["/auth/"];
44
47
  const parseError = config.parseError ?? parseApiErrorBody;
45
48
  function emit(event) {
46
49
  for (const fn of listeners) {
@@ -76,7 +79,7 @@ function createHttpClient(config) {
76
79
  async ({ request: request2, response }) => {
77
80
  if (response.status === 401) {
78
81
  const url = new URL(request2.url).pathname;
79
- if (!isLoggedOut && !authEndpoints.some((path) => url.includes(path))) {
82
+ if (!isLoggedOut && !authEndpoints.some((path) => url.startsWith(path))) {
80
83
  isLoggedOut = true;
81
84
  emit({ type: "unauthorized" });
82
85
  }
@@ -261,7 +264,7 @@ function createFetchMethod(endpoint, prefix, config) {
261
264
  Accept: "application/json",
262
265
  ...typeof config.headers === "function" ? config.headers() : config.headers
263
266
  };
264
- const isQuery = endpoint.method === "GET" || endpoint.method === "DELETE";
267
+ const isQuery = inputIsQuery(endpoint.method);
265
268
  const hasBody = !isQuery && !endpoint.multipart && endpoint.input && args;
266
269
  if (isQuery && args) {
267
270
  const remaining = stripParams(args, endpoint.path);
@@ -1,5 +1,5 @@
1
1
  // src/contract/define.ts
2
- var ALL_TRANSPORTS = ["HTTP", "MCP", "AGENT"];
2
+ var ALL_TRANSPORTS = ["HTTP", "MCP", "AGENT", "CLI"];
3
3
  function defineContract(meta, endpoints) {
4
4
  const toolTransports = new Map;
5
5
  for (const [key, ep] of Object.entries(endpoints)) {
@@ -1,3 +1,7 @@
1
+ import {
2
+ normalizeError
3
+ } from "./index-vhdvv00d.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 {
@@ -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 };