stitchkit 0.55.0 → 0.56.1

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 (78) hide show
  1. package/dist/browser/socket-io.d.ts +14 -6
  2. package/dist/browser/socket-io.d.ts.map +1 -1
  3. package/dist/cli.js +4 -4
  4. package/dist/contract/errors.d.ts +7 -0
  5. package/dist/contract/errors.d.ts.map +1 -1
  6. package/dist/contract/index.js +1 -1
  7. package/dist/files/boundary.d.ts +8 -5
  8. package/dist/files/boundary.d.ts.map +1 -1
  9. package/dist/files.js +1 -1
  10. package/dist/{index-rgd18hx3.js → index-28cqssm3.js} +7 -0
  11. package/dist/index-557by2db.js +34 -0
  12. package/dist/index-5r13htq1.js +283 -0
  13. package/dist/index-6y759j86.js +1031 -0
  14. package/dist/{index-npye5kt7.js → index-75njxz7p.js} +69 -7
  15. package/dist/{index-1k16zv57.js → index-bfcpjw20.js} +125 -31
  16. package/dist/{index-0gv7k0ra.js → index-escqg10p.js} +73 -666
  17. package/dist/{index-v4bbq3p4.js → index-j3dem06f.js} +1 -1
  18. package/dist/{index-qx84bqzk.js → index-jcc611vh.js} +1 -1
  19. package/dist/{index-pzc32v75.js → index-sm2tjx06.js} +41 -4
  20. package/dist/{index-js7yexng.js → index-xy8fmh6w.js} +83 -4
  21. package/dist/index.d.ts +2 -2
  22. package/dist/index.d.ts.map +1 -1
  23. package/dist/index.js +27 -6
  24. package/dist/node.js +5 -4
  25. package/dist/observability/audit.d.ts +4 -0
  26. package/dist/observability/audit.d.ts.map +1 -1
  27. package/dist/observability/event.d.ts +5 -0
  28. package/dist/observability/event.d.ts.map +1 -1
  29. package/dist/observability/index.js +17 -11
  30. package/dist/remote.js +1 -1
  31. package/dist/server/context-contribution.d.ts +8 -0
  32. package/dist/server/context-contribution.d.ts.map +1 -1
  33. package/dist/server/create.d.ts.map +1 -1
  34. package/dist/server/error-hook.d.ts +1 -1
  35. package/dist/server/error-hook.d.ts.map +1 -1
  36. package/dist/server/index.d.ts +1 -1
  37. package/dist/server/index.d.ts.map +1 -1
  38. package/dist/server/index.js +10 -6
  39. package/dist/server/logger.d.ts.map +1 -1
  40. package/dist/server/middleware/auth.d.ts +29 -2
  41. package/dist/server/middleware/auth.d.ts.map +1 -1
  42. package/dist/server/request-body.d.ts.map +1 -1
  43. package/dist/testing/surface-conformance.d.ts +143 -10
  44. package/dist/testing/surface-conformance.d.ts.map +1 -1
  45. package/dist/testing/surface-manifest.d.ts +132 -33
  46. package/dist/testing/surface-manifest.d.ts.map +1 -1
  47. package/dist/testing.d.ts +2 -2
  48. package/dist/testing.d.ts.map +1 -1
  49. package/dist/testing.js +470 -122
  50. package/dist/tools/async-operation.d.ts +210 -18
  51. package/dist/tools/async-operation.d.ts.map +1 -1
  52. package/dist/tools/define-download-tool.d.ts.map +1 -1
  53. package/dist/tools/define-upload-tool.d.ts.map +1 -1
  54. package/dist/tools/internal/surface-projector.d.ts +101 -0
  55. package/dist/tools/internal/surface-projector.d.ts.map +1 -0
  56. package/dist/tools/managed-file-error.d.ts +6 -0
  57. package/dist/tools/managed-file-error.d.ts.map +1 -0
  58. package/dist/tools/mcp-prepare.d.ts +3 -21
  59. package/dist/tools/mcp-prepare.d.ts.map +1 -1
  60. package/dist/tools/mcp-round-policy.d.ts +9 -0
  61. package/dist/tools/mcp-round-policy.d.ts.map +1 -0
  62. package/dist/tools/mcp-round.d.ts +0 -4
  63. package/dist/tools/mcp-round.d.ts.map +1 -1
  64. package/dist/tools/mount-download.d.ts.map +1 -1
  65. package/dist/tools/mount-upload.d.ts.map +1 -1
  66. package/dist/tools/mount.d.ts +5 -9
  67. package/dist/tools/mount.d.ts.map +1 -1
  68. package/dist/tools/runtime-tool.d.ts.map +1 -1
  69. package/dist/tools/surface.d.ts +7 -0
  70. package/dist/tools/surface.d.ts.map +1 -1
  71. package/dist/tools/view-file.d.ts.map +1 -1
  72. package/dist/tools.d.ts +1 -1
  73. package/dist/tools.d.ts.map +1 -1
  74. package/dist/tools.js +348 -383
  75. package/llms-full.txt +425 -66
  76. package/package.json +1 -1
  77. package/dist/index-h60df7rj.js +0 -110
  78. package/dist/index-k4dftwf7.js +0 -66
@@ -7,7 +7,7 @@ import {
7
7
  defaultSignalSource,
8
8
  guardSignalCallback,
9
9
  reportSignalError
10
- } from "./index-pzc32v75.js";
10
+ } from "./index-sm2tjx06.js";
11
11
  import {
12
12
  errorCode,
13
13
  extractIp,
@@ -23,13 +23,13 @@ import {
23
23
  setRequestError,
24
24
  validateDeclaredOutput,
25
25
  zodIssues
26
- } from "./index-v4bbq3p4.js";
26
+ } from "./index-j3dem06f.js";
27
27
  import {
28
28
  AppError,
29
29
  badRequest,
30
30
  isUnsafeKey,
31
31
  safeJsonParse
32
- } from "./index-h60df7rj.js";
32
+ } from "./index-5r13htq1.js";
33
33
  import {
34
34
  isRecord,
35
35
  parseTrailingWildcard,
@@ -466,12 +466,20 @@ async function parseMultipart(req, descriptor, fieldsSchema, receivers) {
466
466
  }
467
467
 
468
468
  // src/server/request-body.ts
469
+ function requestAbortReason(req) {
470
+ return req.signal.reason ?? new DOMException("The connection was closed", "AbortError");
471
+ }
472
+ function throwIfRequestAborted(req) {
473
+ if (req.signal.aborted)
474
+ throw requestAbortReason(req);
475
+ }
469
476
  function assertJsonBodyLimit(maxBytes, owner) {
470
477
  if (maxBytes !== undefined && (!Number.isSafeInteger(maxBytes) || maxBytes <= 0)) {
471
478
  throw new Error(`${owner} must be a positive safe integer, received ${maxBytes}`);
472
479
  }
473
480
  }
474
481
  async function readRequestText(req, maxBytes) {
482
+ throwIfRequestAborted(req);
475
483
  if (maxBytes === undefined)
476
484
  return req.text();
477
485
  assertJsonBodyLimit(maxBytes, "maxJsonBodyBytes");
@@ -480,9 +488,25 @@ async function readRequestText(req, maxBytes) {
480
488
  return "";
481
489
  const chunks = [];
482
490
  let total = 0;
491
+ let rejectAbortedRead = (_reason) => {
492
+ return;
493
+ };
494
+ const abortedRead = new Promise((_resolve, reject) => {
495
+ rejectAbortedRead = reject;
496
+ });
497
+ const onAbort = () => {
498
+ const reason = requestAbortReason(req);
499
+ rejectAbortedRead(reason);
500
+ reader.cancel(reason).catch(() => {
501
+ return;
502
+ });
503
+ };
504
+ req.signal.addEventListener("abort", onAbort, { once: true });
505
+ if (req.signal.aborted)
506
+ onAbort();
483
507
  try {
484
508
  while (true) {
485
- const { done, value } = await reader.read();
509
+ const { done, value } = await Promise.race([reader.read(), abortedRead]);
486
510
  if (done)
487
511
  break;
488
512
  total += value.byteLength;
@@ -493,7 +517,9 @@ async function readRequestText(req, maxBytes) {
493
517
  chunks.push(value);
494
518
  }
495
519
  } finally {
496
- reader.releaseLock();
520
+ req.signal.removeEventListener("abort", onAbort);
521
+ if (!req.signal.aborted)
522
+ reader.releaseLock();
497
523
  }
498
524
  const bytes = new Uint8Array(total);
499
525
  let offset = 0;
@@ -630,6 +656,8 @@ function safePath(pathname) {
630
656
  return out;
631
657
  }
632
658
  function levelForStatus(status) {
659
+ if (status === 499)
660
+ return "info";
633
661
  if (status >= 500)
634
662
  return "error";
635
663
  if (status >= 400)
@@ -1037,6 +1065,29 @@ function validateRawRoutes(rawRoutes) {
1037
1065
 
1038
1066
  // src/server/create.ts
1039
1067
  var BODY_METHODS = new Set(["POST", "PUT", "PATCH", "DELETE"]);
1068
+ var MAX_REQUEST_ABORT_CAUSE_DEPTH = 8;
1069
+ function containsRequestAbortReason(error, reason) {
1070
+ let current = error;
1071
+ const visited = new Set;
1072
+ for (let depth = 0;depth <= MAX_REQUEST_ABORT_CAUSE_DEPTH; depth += 1) {
1073
+ if (current === reason)
1074
+ return true;
1075
+ if (depth === MAX_REQUEST_ABORT_CAUSE_DEPTH || !isRecord(current))
1076
+ return false;
1077
+ if (visited.has(current))
1078
+ return false;
1079
+ visited.add(current);
1080
+ current = current.cause;
1081
+ }
1082
+ return false;
1083
+ }
1084
+ function isClientClosedRequest(req, error) {
1085
+ if (!req.signal.aborted)
1086
+ return false;
1087
+ if (isRecord(error) && error.name === "AbortError")
1088
+ return true;
1089
+ return req.signal.reason !== undefined && containsRequestAbortReason(error, req.signal.reason);
1090
+ }
1040
1091
  function createHandler(config) {
1041
1092
  const { cors, hooks, logging = false, observability, trustProxy = false } = config;
1042
1093
  if (cors)
@@ -1100,7 +1151,7 @@ function createHandler(config) {
1100
1151
  } catch {}
1101
1152
  }
1102
1153
  let completed = false;
1103
- const complete = (status, errorCode2) => {
1154
+ const complete = (status, errorCode2, outcome) => {
1104
1155
  if (completed)
1105
1156
  return;
1106
1157
  completed = true;
@@ -1148,11 +1199,22 @@ function createHandler(config) {
1148
1199
  const context = getRequestContext();
1149
1200
  if (observability && context) {
1150
1201
  try {
1151
- observability.complete({ context, statusCode: status, durationMs, payload });
1202
+ observability.complete({
1203
+ context,
1204
+ statusCode: status,
1205
+ durationMs,
1206
+ payload,
1207
+ ...outcome !== undefined && { outcome }
1208
+ });
1152
1209
  } catch {}
1153
1210
  }
1154
1211
  };
1155
1212
  const respondError = async (err, errCtx, endpoint) => {
1213
+ if (isClientClosedRequest(req, err)) {
1214
+ const response = applyCors(new Response(null, { status: 499, statusText: "Client Closed Request" }), cors, req);
1215
+ complete(response.status, undefined, "cancelled");
1216
+ return response;
1217
+ }
1156
1218
  const recordFailure = (normalized) => {
1157
1219
  if (getRequestContext()?.error !== undefined)
1158
1220
  return;
@@ -9,11 +9,16 @@ import {
9
9
  // src/files/boundary.ts
10
10
  import { randomUUID } from "node:crypto";
11
11
  import { constants } from "node:fs";
12
- import { link, open, realpath, rename, stat, unlink } from "node:fs/promises";
12
+ import { link, mkdir, open, realpath, rename, stat, unlink } from "node:fs/promises";
13
13
  import { basename, dirname, resolve } from "node:path";
14
14
  var DEFAULT_MAX_BYTES = 100 * 1024 * 1024;
15
15
  var DEFAULT_INSPECTION_BYTES = 64 * 1024;
16
+ var DEFAULT_INSPECTION_TIMEOUT_MS = 15000;
16
17
  var NOFOLLOW_FLAG = constants.O_NOFOLLOW ?? 0;
18
+ var ManagedFileInspectionSchema = ManagedFileRefSchema.pick({
19
+ mediaType: true,
20
+ name: true
21
+ });
17
22
 
18
23
  class ManagedFileError extends Error {
19
24
  code;
@@ -47,20 +52,118 @@ function ioError(message, error) {
47
52
  return error;
48
53
  return new ManagedFileError("FILE_IO_ERROR", message, { cause: error });
49
54
  }
55
+ async function realpathOrNull(path, message) {
56
+ try {
57
+ return await realpath(path);
58
+ } catch (error) {
59
+ if (errorCode(error) === "ENOENT")
60
+ return null;
61
+ throw ioError(message, error);
62
+ }
63
+ }
64
+ async function bindRoot(config) {
65
+ const requested = resolve(config.root);
66
+ let canonical = await realpathOrNull(requested, "failed to resolve managed-file root");
67
+ if (canonical === null && config.createRoot) {
68
+ const parent = await realpathOrNull(dirname(requested), "failed to resolve managed-file root parent");
69
+ if (parent === null) {
70
+ throw new ManagedFileError("FILE_NOT_FOUND", "managed-file root parent must exist");
71
+ }
72
+ const parentInfo = await stat(parent).catch((error) => {
73
+ throw ioError("failed to inspect managed-file root parent", error);
74
+ });
75
+ if (!parentInfo.isDirectory()) {
76
+ throw new ManagedFileError("FILE_NOT_REGULAR", "managed-file root parent must be a directory");
77
+ }
78
+ try {
79
+ await mkdir(requested, { mode: 448 });
80
+ } catch (error) {
81
+ if (errorCode(error) !== "EEXIST") {
82
+ if (errorCode(error) === "ENOENT") {
83
+ throw new ManagedFileError("FILE_NOT_FOUND", "managed-file root parent must exist");
84
+ }
85
+ throw ioError("failed to create managed-file root", error);
86
+ }
87
+ }
88
+ canonical = await realpathOrNull(requested, "failed to resolve managed-file root");
89
+ }
90
+ if (canonical === null) {
91
+ throw new ManagedFileError("FILE_NOT_FOUND", "managed-file root must exist");
92
+ }
93
+ const info = await stat(canonical).catch((error) => {
94
+ throw ioError("failed to inspect managed-file root", error);
95
+ });
96
+ if (!info.isDirectory()) {
97
+ throw new ManagedFileError("FILE_NOT_REGULAR", "managed-file root must be a directory");
98
+ }
99
+ return canonical;
100
+ }
50
101
  async function existingParent(root, target) {
51
- const parent = await realpath(dirname(target)).catch(() => null);
102
+ const parent = await realpathOrNull(dirname(target), "failed to resolve managed-file parent directory");
52
103
  if (parent === null) {
53
104
  throw new ManagedFileError("FILE_NOT_FOUND", "managed-file parent directory not found");
54
105
  }
55
106
  if (!isWithinDir(root, parent)) {
56
107
  throw new ManagedFileError("FILE_OUTSIDE_ROOT", "managed-file path escapes its boundary");
57
108
  }
58
- const info = await stat(parent).catch(() => null);
59
- if (!info?.isDirectory()) {
60
- throw new ManagedFileError("FILE_NOT_FOUND", "managed-file parent directory not found");
109
+ const info = await stat(parent).catch((error) => {
110
+ throw ioError("failed to inspect managed-file parent directory", error);
111
+ });
112
+ if (!info.isDirectory()) {
113
+ throw new ManagedFileError("FILE_NOT_REGULAR", "managed-file parent is not a directory");
61
114
  }
62
115
  return parent;
63
116
  }
117
+ function abortReason(signal) {
118
+ return signal.reason ?? new DOMException("The operation was aborted", "AbortError");
119
+ }
120
+ function raceWithSignal(operation, signal) {
121
+ signal.throwIfAborted();
122
+ return new Promise((resolvePromise, rejectPromise) => {
123
+ let settled = false;
124
+ const settle = (complete) => {
125
+ if (settled)
126
+ return;
127
+ settled = true;
128
+ signal.removeEventListener("abort", onAbort);
129
+ complete();
130
+ };
131
+ const onAbort = () => settle(() => rejectPromise(abortReason(signal)));
132
+ signal.addEventListener("abort", onAbort, { once: true });
133
+ if (signal.aborted)
134
+ onAbort();
135
+ operation.then((value) => settle(() => resolvePromise(value)), (error) => settle(() => rejectPromise(error)));
136
+ });
137
+ }
138
+ async function inspectFile(inspector, input, timeoutMs, outerSignal) {
139
+ if (!inspector)
140
+ return {};
141
+ outerSignal?.throwIfAborted();
142
+ const timeoutSignal = AbortSignal.timeout(timeoutMs);
143
+ const signal = outerSignal ? AbortSignal.any([outerSignal, timeoutSignal]) : timeoutSignal;
144
+ try {
145
+ const inspected = await raceWithSignal(Promise.resolve().then(() => inspector({ ...input, signal })), signal);
146
+ return ManagedFileInspectionSchema.parse(inspected);
147
+ } catch (error) {
148
+ if (outerSignal?.aborted)
149
+ throw abortReason(outerSignal);
150
+ throw new ManagedFileError("FILE_INSPECTION_REJECTED", "managed file rejected by inspection", { cause: error });
151
+ }
152
+ }
153
+ function inspectedRef(path, size, inspection, fallback = {}) {
154
+ const mediaType = inspection.mediaType ?? fallback.mediaType;
155
+ const name = inspection.name ?? fallback.name;
156
+ const parsed = ManagedFileRefSchema.safeParse({
157
+ path,
158
+ size,
159
+ ...mediaType ? { mediaType } : {},
160
+ ...name ? { name } : {}
161
+ });
162
+ if (!parsed.success) {
163
+ throw new ManagedFileError("FILE_INSPECTION_REJECTED", "managed file rejected by inspection", { cause: parsed.error });
164
+ }
165
+ return parsed.data;
166
+ }
64
167
  async function readHandle(handle, maxBytes, signal) {
65
168
  const chunks = [];
66
169
  let total = 0;
@@ -143,13 +246,11 @@ async function writeSource(handle, source, maxBytes, inspectionBytes, signal) {
143
246
  return { size, prefix };
144
247
  }
145
248
  async function createManagedFileBoundary(config) {
146
- const root = await realpath(resolve(config.root)).catch(() => null);
147
- if (root === null || !(await stat(root).catch(() => null))?.isDirectory()) {
148
- throw new ManagedFileError("FILE_NOT_FOUND", "managed-file root must exist");
149
- }
249
+ const root = await bindRoot(config);
150
250
  const maxReadBytes = positiveLimit(config.maxReadBytes, DEFAULT_MAX_BYTES, "maxReadBytes");
151
251
  const maxWriteBytes = positiveLimit(config.maxWriteBytes, DEFAULT_MAX_BYTES, "maxWriteBytes");
152
252
  const inspectionBytes = positiveLimit(config.inspectionBytes, DEFAULT_INSPECTION_BYTES, "inspectionBytes");
253
+ const inspectionTimeoutMs = positiveLimit(config.inspectionTimeoutMs, DEFAULT_INSPECTION_TIMEOUT_MS, "inspectionTimeoutMs");
153
254
  const targetFor = (path) => {
154
255
  const relative = filePath(path);
155
256
  const target = resolve(root, ...relative.split("/"));
@@ -161,7 +262,8 @@ async function createManagedFileBoundary(config) {
161
262
  return {
162
263
  async read(path, options = {}) {
163
264
  const resolved = targetFor(path);
164
- const realTarget = await realpath(resolved.target).catch(() => null);
265
+ options.signal?.throwIfAborted();
266
+ const realTarget = await realpathOrNull(resolved.target, "failed to resolve managed file");
165
267
  if (realTarget === null) {
166
268
  throw new ManagedFileError("FILE_NOT_FOUND", "managed file not found");
167
269
  }
@@ -177,8 +279,12 @@ async function createManagedFileBoundary(config) {
177
279
  throw new ManagedFileError("FILE_NOT_REGULAR", "managed path is not a regular file");
178
280
  }
179
281
  const bytes = await readHandle(handle, maxBytes, options.signal);
282
+ const inspection = await inspectFile(config.inspect, {
283
+ prefix: bytes.slice(0, inspectionBytes),
284
+ name: basename(resolved.path)
285
+ }, inspectionTimeoutMs, options.signal);
180
286
  return {
181
- ref: ManagedFileRefSchema.parse({ path: resolved.path, size: bytes.byteLength }),
287
+ ref: inspectedRef(resolved.path, bytes.byteLength, inspection),
182
288
  bytes
183
289
  };
184
290
  } catch (error) {
@@ -211,18 +317,11 @@ async function createManagedFileBoundary(config) {
211
317
  await handle.sync();
212
318
  await handle.close();
213
319
  handle = undefined;
214
- let inspection = {};
215
- if (config.inspect) {
216
- try {
217
- inspection = await config.inspect({
218
- prefix: written.prefix,
219
- declaredMediaType: options.mediaType,
220
- name: options.name ?? basename(resolved.path)
221
- });
222
- } catch (error) {
223
- throw new ManagedFileError("FILE_INSPECTION_REJECTED", "managed file rejected by inspection", { cause: error });
224
- }
225
- }
320
+ const inspection = await inspectFile(config.inspect, {
321
+ prefix: written.prefix,
322
+ ...options.mediaType ? { declaredMediaType: options.mediaType } : {},
323
+ name: options.name ?? basename(resolved.path)
324
+ }, inspectionTimeoutMs, options.signal);
226
325
  options.signal?.throwIfAborted();
227
326
  if (options.replace) {
228
327
  await rename(temporary, resolved.target);
@@ -238,15 +337,10 @@ async function createManagedFileBoundary(config) {
238
337
  throw error;
239
338
  }
240
339
  }
241
- const mediaType = inspection.mediaType ?? options.mediaType;
242
- const name = inspection.name ?? options.name;
243
- const ref = ManagedFileRefSchema.parse({
244
- path: resolved.path,
245
- size,
246
- ...mediaType ? { mediaType } : {},
247
- ...name ? { name } : {}
340
+ return inspectedRef(resolved.path, size, inspection, {
341
+ mediaType: options.mediaType,
342
+ name: options.name
248
343
  });
249
- return ref;
250
344
  } catch (error) {
251
345
  if (options.signal?.aborted && error === options.signal.reason)
252
346
  throw error;