stitchkit 0.55.0 → 0.56.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 (69) 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-1k16zv57.js → index-bfcpjw20.js} +125 -31
  15. package/dist/{index-0gv7k0ra.js → index-escqg10p.js} +73 -666
  16. package/dist/{index-v4bbq3p4.js → index-j3dem06f.js} +1 -1
  17. package/dist/{index-qx84bqzk.js → index-jcc611vh.js} +1 -1
  18. package/dist/{index-npye5kt7.js → index-n4nfa7gh.js} +3 -3
  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/index.js +3 -3
  26. package/dist/remote.js +1 -1
  27. package/dist/server/context-contribution.d.ts +8 -0
  28. package/dist/server/context-contribution.d.ts.map +1 -1
  29. package/dist/server/index.d.ts +1 -1
  30. package/dist/server/index.d.ts.map +1 -1
  31. package/dist/server/index.js +8 -5
  32. package/dist/server/middleware/auth.d.ts +29 -2
  33. package/dist/server/middleware/auth.d.ts.map +1 -1
  34. package/dist/testing/surface-conformance.d.ts +143 -10
  35. package/dist/testing/surface-conformance.d.ts.map +1 -1
  36. package/dist/testing/surface-manifest.d.ts +132 -33
  37. package/dist/testing/surface-manifest.d.ts.map +1 -1
  38. package/dist/testing.d.ts +2 -2
  39. package/dist/testing.d.ts.map +1 -1
  40. package/dist/testing.js +470 -122
  41. package/dist/tools/async-operation.d.ts +210 -18
  42. package/dist/tools/async-operation.d.ts.map +1 -1
  43. package/dist/tools/define-download-tool.d.ts.map +1 -1
  44. package/dist/tools/define-upload-tool.d.ts.map +1 -1
  45. package/dist/tools/internal/surface-projector.d.ts +101 -0
  46. package/dist/tools/internal/surface-projector.d.ts.map +1 -0
  47. package/dist/tools/managed-file-error.d.ts +6 -0
  48. package/dist/tools/managed-file-error.d.ts.map +1 -0
  49. package/dist/tools/mcp-prepare.d.ts +3 -21
  50. package/dist/tools/mcp-prepare.d.ts.map +1 -1
  51. package/dist/tools/mcp-round-policy.d.ts +9 -0
  52. package/dist/tools/mcp-round-policy.d.ts.map +1 -0
  53. package/dist/tools/mcp-round.d.ts +0 -4
  54. package/dist/tools/mcp-round.d.ts.map +1 -1
  55. package/dist/tools/mount-download.d.ts.map +1 -1
  56. package/dist/tools/mount-upload.d.ts.map +1 -1
  57. package/dist/tools/mount.d.ts +5 -9
  58. package/dist/tools/mount.d.ts.map +1 -1
  59. package/dist/tools/runtime-tool.d.ts.map +1 -1
  60. package/dist/tools/surface.d.ts +7 -0
  61. package/dist/tools/surface.d.ts.map +1 -1
  62. package/dist/tools/view-file.d.ts.map +1 -1
  63. package/dist/tools.d.ts +1 -1
  64. package/dist/tools.d.ts.map +1 -1
  65. package/dist/tools.js +348 -383
  66. package/llms-full.txt +361 -58
  67. package/package.json +1 -1
  68. package/dist/index-h60df7rj.js +0 -110
  69. package/dist/index-k4dftwf7.js +0 -66
@@ -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;