stitchkit 0.24.0 → 0.26.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 (53) hide show
  1. package/dist/browser/client-multipart.d.ts +22 -0
  2. package/dist/browser/client-multipart.d.ts.map +1 -0
  3. package/dist/browser/client.d.ts.map +1 -1
  4. package/dist/cli.js +2 -2
  5. package/dist/contract/define.d.ts +20 -0
  6. package/dist/contract/define.d.ts.map +1 -1
  7. package/dist/contract/factory.d.ts +1 -0
  8. package/dist/contract/factory.d.ts.map +1 -1
  9. package/dist/contract/index.js +1 -1
  10. package/dist/{index-4gawbm74.js → index-0d0rb85d.js} +41 -3
  11. package/dist/{index-g8kyab85.js → index-bkccbx64.js} +10 -3
  12. package/dist/{index-x62gnfsk.js → index-h4y2wg3n.js} +1 -1
  13. package/dist/{index-a9n8m4ec.js → index-p6fge9a5.js} +1 -1
  14. package/dist/{index-x8m8e7dc.js → index-q5w3cvvp.js} +176 -87
  15. package/dist/index.js +20 -18
  16. package/dist/internal/errors.d.ts +6 -1
  17. package/dist/internal/errors.d.ts.map +1 -1
  18. package/dist/internal/write-download.d.ts +17 -0
  19. package/dist/internal/write-download.d.ts.map +1 -0
  20. package/dist/node.js +2 -2
  21. package/dist/server/create.d.ts.map +1 -1
  22. package/dist/server/error-hook.d.ts +14 -3
  23. package/dist/server/error-hook.d.ts.map +1 -1
  24. package/dist/server/implement.d.ts.map +1 -1
  25. package/dist/server/index.js +6 -6
  26. package/dist/server/types.d.ts +13 -0
  27. package/dist/server/types.d.ts.map +1 -1
  28. package/dist/tools/agent.d.ts +2 -0
  29. package/dist/tools/agent.d.ts.map +1 -1
  30. package/dist/tools/cli.d.ts.map +1 -1
  31. package/dist/tools/coerce.d.ts +2 -1
  32. package/dist/tools/coerce.d.ts.map +1 -1
  33. package/dist/tools/execute.d.ts +1 -1
  34. package/dist/tools/execute.d.ts.map +1 -1
  35. package/dist/tools/flatten.d.ts +16 -6
  36. package/dist/tools/flatten.d.ts.map +1 -1
  37. package/dist/tools/list-names.d.ts.map +1 -1
  38. package/dist/tools/mcp.d.ts +10 -3
  39. package/dist/tools/mcp.d.ts.map +1 -1
  40. package/dist/tools/mount-download.d.ts.map +1 -1
  41. package/dist/tools/mount-upload.d.ts.map +1 -1
  42. package/dist/tools/mount-wait.d.ts.map +1 -1
  43. package/dist/tools/mount.d.ts +13 -0
  44. package/dist/tools/mount.d.ts.map +1 -1
  45. package/dist/tools/names.d.ts +34 -0
  46. package/dist/tools/names.d.ts.map +1 -1
  47. package/dist/tools/remote.d.ts.map +1 -1
  48. package/dist/tools/schema.d.ts +43 -1
  49. package/dist/tools/schema.d.ts.map +1 -1
  50. package/dist/tools/transports.d.ts.map +1 -1
  51. package/dist/tools.js +48 -40
  52. package/llms-full.txt +113 -11
  53. package/package.json +1 -1
@@ -7,7 +7,7 @@ import {
7
7
  isWithinDir,
8
8
  normalizeError,
9
9
  validateHandlerOutput
10
- } from "./index-4gawbm74.js";
10
+ } from "./index-0d0rb85d.js";
11
11
  import {
12
12
  isRecord,
13
13
  isUnsafeKey,
@@ -76,13 +76,66 @@ function coerceJsonArgs(args, schema) {
76
76
  }
77
77
 
78
78
  // src/tools/flatten.ts
79
+ import { z as z3 } from "zod";
80
+
81
+ // src/tools/schema.ts
79
82
  import { z as z2 } from "zod";
83
+ function objectShapeKeys(schema) {
84
+ return schema instanceof z2.ZodObject ? Object.keys(schema.shape) : [];
85
+ }
86
+ function keyPolicyOf(schema) {
87
+ return schema.def.catchall;
88
+ }
89
+ function rebuildObject(source, shape) {
90
+ return withKeyPolicy(z2.object(shape), keyPolicyOf(source));
91
+ }
92
+ function withKeyPolicy(object, policy) {
93
+ if (policy === undefined)
94
+ return object;
95
+ return object.catchall(representable(policy));
96
+ }
97
+ function representable(policy) {
98
+ if (policy instanceof z2.ZodNever || policy instanceof z2.ZodUnknown)
99
+ return policy;
100
+ if (!(policy instanceof z2.ZodType))
101
+ return z2.unknown();
102
+ try {
103
+ toJsonSchema(policy, "input");
104
+ return policy;
105
+ } catch {
106
+ return z2.unknown();
107
+ }
108
+ }
109
+ function mergeSchemas(paramsSchema, inputSchema) {
110
+ if (paramsSchema && !(paramsSchema instanceof z2.ZodObject)) {
111
+ throw new Error("Tool params schema must be a z.object()");
112
+ }
113
+ const paramsObject = paramsSchema instanceof z2.ZodObject ? paramsSchema : undefined;
114
+ if (!inputSchema) {
115
+ return paramsObject ?? z2.object({});
116
+ }
117
+ if (inputSchema instanceof z2.ZodObject) {
118
+ if (paramsObject) {
119
+ const conflicts = Object.keys(paramsObject.shape).filter((key) => (key in inputSchema.shape));
120
+ if (conflicts.length > 0) {
121
+ throw new Error(`Schema merge conflict: ${conflicts.join(", ")} appear in both params and input`);
122
+ }
123
+ }
124
+ return rebuildObject(inputSchema, {
125
+ ...paramsObject?.shape ?? {},
126
+ ...inputSchema.shape
127
+ });
128
+ }
129
+ return paramsObject ? z2.intersection(paramsObject, inputSchema) : inputSchema;
130
+ }
131
+
132
+ // src/tools/flatten.ts
80
133
  function flattenDiscriminatedUnion(union) {
81
134
  const discriminator = union.def.discriminator;
82
135
  const allDiscValues = [];
83
136
  const perKey = new Map;
84
137
  for (const opt of union.def.options) {
85
- if (!(opt instanceof z2.ZodObject)) {
138
+ if (!(opt instanceof z3.ZodObject)) {
86
139
  throw new Error(`flattenDiscriminatedUnion: variant for discriminator '${discriminator}' is not a ZodObject`);
87
140
  }
88
141
  const discValues = stringLiteralOrEnumValues(opt.shape[discriminator]);
@@ -95,7 +148,7 @@ function flattenDiscriminatedUnion(union) {
95
148
  if (key === discriminator)
96
149
  continue;
97
150
  const entry = perKey.get(key) ?? { schemas: [], variants: [] };
98
- entry.schemas.push(field instanceof z2.ZodOptional ? field.unwrap() : field);
151
+ entry.schemas.push(unwrapField(field));
99
152
  entry.variants.push(label);
100
153
  perKey.set(key, entry);
101
154
  }
@@ -106,23 +159,44 @@ function flattenDiscriminatedUnion(union) {
106
159
  throw new Error("flattenDiscriminatedUnion: union has no options");
107
160
  }
108
161
  const shape = {
109
- [discriminator]: z2.enum([firstLiteral, ...restLiterals])
162
+ [discriminator]: z3.enum([firstLiteral, ...restLiterals])
110
163
  };
111
164
  for (const [key, entry] of perKey) {
112
165
  const advertised = mergeCollidingFields(entry.schemas);
113
166
  const hint = `Required if ${discriminator} = ${[...new Set(entry.variants)].join(" | ")}`;
114
- shape[key] = z2.optional(advertised).describe(hint);
167
+ shape[key] = z3.optional(advertised).describe(hint);
168
+ }
169
+ return withKeyPolicy(z3.object(shape), mergeVariantKeyPolicies(union.def.options));
170
+ }
171
+ function mergeVariantKeyPolicies(options) {
172
+ let strict = 0;
173
+ let counted = 0;
174
+ for (const option of options) {
175
+ const policy = option instanceof z3.ZodObject ? keyPolicyOf(option) : undefined;
176
+ if (policy !== undefined && !(policy instanceof z3.ZodNever))
177
+ return z3.unknown();
178
+ counted++;
179
+ if (policy instanceof z3.ZodNever)
180
+ strict++;
181
+ }
182
+ if (counted === 0 || strict === 0)
183
+ return;
184
+ return strict === counted ? z3.never() : z3.unknown();
185
+ }
186
+ function unwrapField(field) {
187
+ if (field instanceof z3.ZodOptional || field instanceof z3.ZodDefault) {
188
+ return unwrapField(field.unwrap());
115
189
  }
116
- return z2.object(shape);
190
+ return field;
117
191
  }
118
192
  function preserveDescription(from, to) {
119
193
  return from.description === undefined ? to : to.describe(from.description);
120
194
  }
121
195
  function stringLiteralOrEnumValues(field) {
122
196
  let raw = null;
123
- if (field instanceof z2.ZodLiteral)
197
+ if (field instanceof z3.ZodLiteral)
124
198
  raw = field.def.values;
125
- else if (field instanceof z2.ZodEnum)
199
+ else if (field instanceof z3.ZodEnum)
126
200
  raw = field.options;
127
201
  if (raw === null)
128
202
  return null;
@@ -134,7 +208,7 @@ function isFlattenableDU(union) {
134
208
  if (union.def.options.length === 0)
135
209
  return false;
136
210
  for (const opt of union.def.options) {
137
- if (!(opt instanceof z2.ZodObject))
211
+ if (!(opt instanceof z3.ZodObject))
138
212
  return false;
139
213
  if (stringLiteralOrEnumValues(opt.shape[disc]) === null)
140
214
  return false;
@@ -156,31 +230,31 @@ function stripAnnotations(node) {
156
230
  }
157
231
  }
158
232
  function hasChecks(schema) {
159
- if (!(schema instanceof z2.ZodType))
233
+ if (!(schema instanceof z3.ZodType))
160
234
  return false;
161
235
  const def = schema.def;
162
236
  if (isRecord(def) && Array.isArray(def.checks) && def.checks.length > 0)
163
237
  return true;
164
- if (schema instanceof z2.ZodOptional || schema instanceof z2.ZodNullable || schema instanceof z2.ZodDefault) {
238
+ if (schema instanceof z3.ZodOptional || schema instanceof z3.ZodNullable || schema instanceof z3.ZodDefault) {
165
239
  return hasChecks(schema.unwrap());
166
240
  }
167
- if (schema instanceof z2.ZodPipe)
241
+ if (schema instanceof z3.ZodPipe)
168
242
  return hasChecks(schema.def.in) || hasChecks(schema.def.out);
169
- if (schema instanceof z2.ZodObject)
243
+ if (schema instanceof z3.ZodObject)
170
244
  return Object.values(schema.shape).some(hasChecks);
171
- if (schema instanceof z2.ZodArray)
245
+ if (schema instanceof z3.ZodArray)
172
246
  return hasChecks(schema.element);
173
- if (schema instanceof z2.ZodUnion)
247
+ if (schema instanceof z3.ZodUnion)
174
248
  return schema.def.options.some(hasChecks);
175
- if (schema instanceof z2.ZodRecord)
249
+ if (schema instanceof z3.ZodRecord)
176
250
  return hasChecks(schema.valueType);
177
- if (schema instanceof z2.ZodIntersection) {
251
+ if (schema instanceof z3.ZodIntersection) {
178
252
  return hasChecks(schema.def.left) || hasChecks(schema.def.right);
179
253
  }
180
254
  return false;
181
255
  }
182
256
  function normalizedJson(schema) {
183
- if (!(schema instanceof z2.ZodType))
257
+ if (!(schema instanceof z3.ZodType))
184
258
  return "{}";
185
259
  const json = toJsonSchema(schema, "input", "any");
186
260
  stripAnnotations(json);
@@ -197,8 +271,8 @@ function mergeCollidingFields(schemas) {
197
271
  const only = values[0];
198
272
  if (values.length <= 1) {
199
273
  if (only === undefined)
200
- return z2.unknown();
201
- return schemas.length > 1 && hasChecks(only) ? z2.unknown() : only;
274
+ return z3.unknown();
275
+ return schemas.length > 1 && hasChecks(only) ? z3.unknown() : only;
202
276
  }
203
277
  const merged = [];
204
278
  let allEnum = true;
@@ -214,47 +288,47 @@ function mergeCollidingFields(schemas) {
214
288
  const uniq = [...new Set(merged)];
215
289
  const [first, ...rest] = uniq;
216
290
  if (first !== undefined)
217
- return z2.enum([first, ...rest]);
291
+ return z3.enum([first, ...rest]);
218
292
  }
219
- return z2.unknown();
293
+ return z3.unknown();
220
294
  }
221
295
  function flattenUnionsDeep(schema) {
222
- if (!(schema instanceof z2.ZodType))
223
- return z2.unknown();
224
- if (schema instanceof z2.ZodDiscriminatedUnion) {
296
+ if (!(schema instanceof z3.ZodType))
297
+ return z3.unknown();
298
+ if (schema instanceof z3.ZodDiscriminatedUnion) {
225
299
  if (!isFlattenableDU(schema))
226
300
  return schema;
227
301
  return preserveDescription(schema, flattenUnionsDeep(flattenDiscriminatedUnion(schema)));
228
302
  }
229
- if (schema instanceof z2.ZodObject) {
303
+ if (schema instanceof z3.ZodObject) {
230
304
  const shape = {};
231
305
  for (const [key, field] of Object.entries(schema.shape)) {
232
306
  shape[key] = flattenUnionsDeep(field);
233
307
  }
234
- return preserveDescription(schema, z2.object(shape));
308
+ return preserveDescription(schema, rebuildObject(schema, shape));
235
309
  }
236
- if (schema instanceof z2.ZodArray) {
237
- return preserveDescription(schema, z2.array(flattenUnionsDeep(schema.element)));
310
+ if (schema instanceof z3.ZodArray) {
311
+ return preserveDescription(schema, z3.array(flattenUnionsDeep(schema.element)));
238
312
  }
239
- if (schema instanceof z2.ZodUnion) {
313
+ if (schema instanceof z3.ZodUnion) {
240
314
  const opts = schema.def.options.map((option) => flattenUnionsDeep(option));
241
315
  const [a, b, ...rest] = opts;
242
- return a && b ? preserveDescription(schema, z2.union([a, b, ...rest])) : schema;
316
+ return a && b ? preserveDescription(schema, z3.union([a, b, ...rest])) : schema;
243
317
  }
244
- if (schema instanceof z2.ZodRecord) {
245
- return preserveDescription(schema, z2.record(schema.keyType, flattenUnionsDeep(schema.valueType)));
318
+ if (schema instanceof z3.ZodRecord) {
319
+ return preserveDescription(schema, z3.record(schema.keyType, flattenUnionsDeep(schema.valueType)));
246
320
  }
247
- if (schema instanceof z2.ZodOptional) {
248
- return preserveDescription(schema, z2.optional(flattenUnionsDeep(schema.unwrap())));
321
+ if (schema instanceof z3.ZodOptional) {
322
+ return preserveDescription(schema, z3.optional(flattenUnionsDeep(schema.unwrap())));
249
323
  }
250
- if (schema instanceof z2.ZodNullable) {
251
- return preserveDescription(schema, z2.nullable(flattenUnionsDeep(schema.unwrap())));
324
+ if (schema instanceof z3.ZodNullable) {
325
+ return preserveDescription(schema, z3.nullable(flattenUnionsDeep(schema.unwrap())));
252
326
  }
253
- if (schema instanceof z2.ZodDefault) {
327
+ if (schema instanceof z3.ZodDefault) {
254
328
  return preserveDescription(schema, flattenUnionsDeep(schema.unwrap()).default(schema.def.defaultValue));
255
329
  }
256
- if (schema instanceof z2.ZodIntersection) {
257
- return preserveDescription(schema, z2.intersection(flattenUnionsDeep(schema.def.left), flattenUnionsDeep(schema.def.right)));
330
+ if (schema instanceof z3.ZodIntersection) {
331
+ return preserveDescription(schema, z3.intersection(flattenUnionsDeep(schema.def.left), flattenUnionsDeep(schema.def.right)));
258
332
  }
259
333
  return schema;
260
334
  }
@@ -262,31 +336,6 @@ function flattenUnionsDeep(schema) {
262
336
  // src/tools/mount.ts
263
337
  import { z as z4 } from "zod";
264
338
 
265
- // src/tools/schema.ts
266
- import { z as z3 } from "zod";
267
- function objectShapeKeys(schema) {
268
- return schema instanceof z3.ZodObject ? Object.keys(schema.shape) : [];
269
- }
270
- function mergeSchemas(paramsSchema, inputSchema) {
271
- if (paramsSchema && !(paramsSchema instanceof z3.ZodObject)) {
272
- throw new Error("Tool params schema must be a z.object()");
273
- }
274
- const paramsObject = paramsSchema instanceof z3.ZodObject ? paramsSchema : undefined;
275
- if (!inputSchema) {
276
- return paramsObject ?? z3.object({});
277
- }
278
- if (inputSchema instanceof z3.ZodObject) {
279
- if (paramsObject) {
280
- const conflicts = Object.keys(paramsObject.shape).filter((key) => (key in inputSchema.shape));
281
- if (conflicts.length > 0) {
282
- throw new Error(`Schema merge conflict: ${conflicts.join(", ")} appear in both params and input`);
283
- }
284
- }
285
- return z3.object({ ...paramsObject?.shape ?? {}, ...inputSchema.shape });
286
- }
287
- return paramsObject ? z3.intersection(paramsObject, inputSchema) : inputSchema;
288
- }
289
-
290
339
  // src/tools/execute.ts
291
340
  function toolResultFromError(err) {
292
341
  const appErr = normalizeError(err);
@@ -297,7 +346,7 @@ function toolResultFromError(err) {
297
346
  ...appErr.hint && { hint: appErr.hint }
298
347
  };
299
348
  }
300
- async function executeToolMethod(method, toolName, rawArgs, context, hooks, lifecycle, coerceJson = false) {
349
+ async function executeToolMethod(method, toolName, rawArgs, context, hooks, lifecycle, coerceJson = false, onOutputStrip) {
301
350
  const startedAt = Date.now();
302
351
  const finish = async (result) => {
303
352
  await hooks?.afterToolCall?.(toolName, rawArgs, result, Date.now() - startedAt, context, method);
@@ -361,7 +410,7 @@ async function executeToolMethod(method, toolName, rawArgs, context, hooks, life
361
410
  data = transformed;
362
411
  }
363
412
  if (method.outputSchema) {
364
- const checked = validateHandlerOutput(method.outputSchema, data);
413
+ const checked = validateHandlerOutput(method.outputSchema, data, onOutputStrip);
365
414
  if (!checked.ok) {
366
415
  return finish({
367
416
  ok: false,
@@ -388,6 +437,13 @@ var SINGULAR_EXCEPTIONS = new Set([
388
437
  "progress",
389
438
  "news"
390
439
  ]);
440
+ var TOOL_NAME_RE = /^[a-zA-Z0-9_-]{1,64}$/;
441
+ function normalizeService(value) {
442
+ return value.replace(/[^a-zA-Z0-9_]/g, "_");
443
+ }
444
+ function normalizeMethod(value) {
445
+ return value.replace(/[^a-zA-Z0-9_-]/g, "_");
446
+ }
391
447
  function singularize(name) {
392
448
  if (SINGULAR_EXCEPTIONS.has(name))
393
449
  return name;
@@ -397,22 +453,44 @@ function singularize(name) {
397
453
  return name.slice(0, -1);
398
454
  return name;
399
455
  }
456
+ function singularizeTail(name) {
457
+ const cut = name.lastIndexOf("_");
458
+ if (cut === -1)
459
+ return singularize(name);
460
+ return `${name.slice(0, cut + 1)}${singularize(name.slice(cut + 1))}`;
461
+ }
462
+ function hasUsableChars(value) {
463
+ return /[a-zA-Z0-9]/.test(value);
464
+ }
465
+ function assertToolName(name, serviceName, key) {
466
+ const where = `service "${serviceName}", method "${key}"`;
467
+ if (!TOOL_NAME_RE.test(name)) {
468
+ const why = name.length > 64 ? `is ${name.length} characters (max 64) — set an explicit \`toolName\`` : "must match [a-zA-Z0-9_-]";
469
+ throw new Error(`Tool name "${name}" (${where}) ${why}`);
470
+ }
471
+ }
400
472
  function toToolName(serviceName, methodName) {
401
- const normalized = serviceName.replace(/-/g, "_");
402
- const singular = singularize(normalized);
403
- if (methodName === "list")
473
+ const normalized = normalizeService(serviceName);
474
+ const singular = singularizeTail(normalized);
475
+ const method = normalizeMethod(methodName);
476
+ if (method === "list")
404
477
  return `list_${normalized}`;
405
- if (methodName === "get")
478
+ if (method === "get")
406
479
  return `get_${singular}`;
407
- if (methodName === "create")
480
+ if (method === "create")
408
481
  return `create_${singular}`;
409
- if (methodName === "update")
482
+ if (method === "update")
410
483
  return `update_${singular}`;
411
- if (methodName === "delete")
484
+ if (method === "delete")
412
485
  return `delete_${singular}`;
413
- const snake = methodName.replace(/([A-Z])/g, "_$1").toLowerCase();
486
+ const snake = method.replace(/([A-Z])/g, "_$1").toLowerCase();
414
487
  return `${snake}_${singular}`.replace(/^_/, "");
415
488
  }
489
+ function assertUniqueToolName(name, taken, surface) {
490
+ if (taken) {
491
+ throw new Error(`Duplicate ${surface} "${name}" across mounted services`);
492
+ }
493
+ }
416
494
 
417
495
  // src/tools/mount.ts
418
496
  function applyExtend(base, extra) {
@@ -421,12 +499,12 @@ function applyExtend(base, extra) {
421
499
  if (conflicts.length > 0) {
422
500
  throw new Error(`Tool extend conflict: ${conflicts.join(", ")} already declared by the contract`);
423
501
  }
424
- return z4.object({ ...extra, ...base.shape });
502
+ return rebuildObject(base, { ...extra, ...base.shape });
425
503
  }
426
504
  return z4.intersection(z4.object(extra), base);
427
505
  }
428
506
  function collectTools(service, transport, config = {}) {
429
- const { extend, flattenUnionInput = false } = config;
507
+ const { extend, flattenUnionInput = false, assertNames = true } = config;
430
508
  const tools = [];
431
509
  for (const [methodName, method] of Object.entries(service.methods)) {
432
510
  if (transport === "CLI") {
@@ -438,6 +516,12 @@ function collectTools(service, transport, config = {}) {
438
516
  if (method.multipart)
439
517
  continue;
440
518
  const name = method.toolName ?? toToolName(service.name, methodName);
519
+ if (assertNames && transport !== "CLI") {
520
+ if (!method.toolName && !hasUsableChars(service.name)) {
521
+ throw new Error(`Service prefix "${service.name}" (method "${methodName}") has no characters usable in a tool name — set an explicit \`toolName\` or rename the prefix`);
522
+ }
523
+ assertToolName(name, service.name, methodName);
524
+ }
441
525
  const baseSchema = flattenUnionInput ? mergeSchemas(method.paramsSchema ? flattenUnionsDeep(method.paramsSchema) : undefined, method.inputSchema ? flattenUnionsDeep(method.inputSchema) : undefined) : mergeSchemas(method.paramsSchema, method.inputSchema);
442
526
  const shouldExtend = !!extend && (!extend.filter || extend.filter(service, method));
443
527
  const schema = shouldExtend && extend ? applyExtend(baseSchema, extend.schema) : baseSchema;
@@ -453,7 +537,7 @@ function createToolRunner(config) {
453
537
  extraContext = await config.extend.resolve(rawArgs);
454
538
  }
455
539
  const cleanArgs = tool.shouldExtend && extendKeys ? Object.fromEntries(Object.entries(rawArgs).filter(([key]) => !extendKeys.has(key))) : rawArgs;
456
- return executeToolMethod(tool.method, tool.name, cleanArgs, { ...config.context, ...extraContext, source: config.source }, config.hooks, config.lifecycle, config.coerceJsonArgs ?? true);
540
+ return executeToolMethod(tool.method, tool.name, cleanArgs, { ...config.context, ...extraContext, source: config.source }, config.hooks, config.lifecycle, config.coerceJsonArgs ?? true, config.onOutputStrip ? (paths) => config.onOutputStrip?.(tool.name, paths) : undefined);
457
541
  };
458
542
  }
459
543
  function formatToolError(result, toolName, errorHint) {
@@ -796,7 +880,6 @@ async function pollUntilDone(params) {
796
880
  }
797
881
 
798
882
  // src/tools/cli.ts
799
- import { mkdir, writeFile } from "node:fs/promises";
800
883
  import { basename, resolve } from "node:path";
801
884
  import process from "node:process";
802
885
 
@@ -913,6 +996,17 @@ async function readCapped(res, max) {
913
996
  return Buffer.concat(chunks);
914
997
  }
915
998
 
999
+ // src/internal/write-download.ts
1000
+ import { mkdir, writeFile } from "node:fs/promises";
1001
+ import { dirname } from "node:path";
1002
+ async function writeDownload(root, target, data) {
1003
+ if (!isWithinDir(root, target)) {
1004
+ throw new Error("download name escapes the output dir");
1005
+ }
1006
+ await mkdir(dirname(target), { recursive: true });
1007
+ await writeFile(target, data);
1008
+ }
1009
+
916
1010
  // src/tools/cli.ts
917
1011
  var GLOBAL_OPTIONS = [
918
1012
  ["--json", "Emit raw JSON on stdout (for piping / scripts)"],
@@ -1036,7 +1130,6 @@ function renderCommandHelp(name, command, tool) {
1036
1130
  var DEFAULT_DOWNLOAD_MAX_BYTES = 100 * 1024 * 1024;
1037
1131
  async function downloadResults(files, dir, stderr, quiet, allowPrivate, maxBytes) {
1038
1132
  const root = resolve(dir);
1039
- await mkdir(root, { recursive: true });
1040
1133
  for (const file of files) {
1041
1134
  try {
1042
1135
  const res = await fetchGuarded(new URL(file.url), allowPrivate);
@@ -1046,9 +1139,7 @@ async function downloadResults(files, dir, stderr, quiet, allowPrivate, maxBytes
1046
1139
  if (!buffer)
1047
1140
  throw new Error(`file exceeds the ${maxBytes}-byte cap`);
1048
1141
  const target = resolve(root, basename(file.name));
1049
- if (!isWithinDir(root, target))
1050
- throw new Error("download name escapes the output dir");
1051
- await writeFile(target, buffer);
1142
+ await writeDownload(root, target, buffer);
1052
1143
  if (!quiet)
1053
1144
  stderr(`saved ${target} (${(buffer.length / 1024).toFixed(0)}KB)
1054
1145
  `);
@@ -1070,9 +1161,7 @@ async function createCli(config) {
1070
1161
  const tools = new Map;
1071
1162
  for (const service of services) {
1072
1163
  for (const mountable of collectTools(service, "CLI")) {
1073
- if (tools.has(mountable.name)) {
1074
- throw new Error(`Duplicate CLI command "${mountable.name}" across mounted services`);
1075
- }
1164
+ assertUniqueToolName(mountable.name, tools.has(mountable.name), "CLI command");
1076
1165
  tools.set(mountable.name, mountable);
1077
1166
  }
1078
1167
  }
@@ -1167,4 +1256,4 @@ function safeInputSchema(tool) {
1167
1256
  }
1168
1257
  }
1169
1258
 
1170
- export { coerceJsonArgs, toolResultFromError, flattenDiscriminatedUnion, flattenUnionsDeep, collectTools, createToolRunner, formatToolError, fetchGuarded, readCapped, parseCliArgs, DEFAULT_EXIT_CODES, emitResult, pollUntil, pollUntilDone, createCli };
1259
+ export { coerceJsonArgs, toolResultFromError, flattenDiscriminatedUnion, flattenUnionsDeep, assertToolName, assertUniqueToolName, collectTools, createToolRunner, formatToolError, fetchGuarded, readCapped, writeDownload, parseCliArgs, DEFAULT_EXIT_CODES, emitResult, pollUntil, pollUntilDone, createCli };
package/dist/index.js CHANGED
@@ -16,7 +16,7 @@ import {
16
16
  paginatedSchema,
17
17
  rateLimited,
18
18
  unauthorized
19
- } from "./index-x62gnfsk.js";
19
+ } from "./index-h4y2wg3n.js";
20
20
  import {
21
21
  isRecord,
22
22
  mapObject,
@@ -28,6 +28,25 @@ function inputIsQuery(method) {
28
28
  return method === "GET" || method === "DELETE";
29
29
  }
30
30
 
31
+ // src/browser/client-multipart.ts
32
+ function isFileDescriptor(value) {
33
+ return typeof value === "object" && value !== null && !(value instanceof Blob) && "uri" in value && typeof value.uri === "string" && "name" in value && typeof value.name === "string" && "type" in value && typeof value.type === "string";
34
+ }
35
+ function isMultipartFile(value) {
36
+ return value instanceof Blob || isFileDescriptor(value);
37
+ }
38
+ function appendMultipartFile(form, field, file) {
39
+ const sink = form;
40
+ sink.append(field, file);
41
+ }
42
+ function appendFormFields(formData, values, skipKeys) {
43
+ for (const [key, value] of Object.entries(values)) {
44
+ if (skipKeys.has(key) || value === undefined || value === null)
45
+ continue;
46
+ formData.append(key, typeof value === "string" ? value : JSON.stringify(value));
47
+ }
48
+ }
49
+
31
50
  // src/browser/http.ts
32
51
  import ky, { isHTTPError } from "ky";
33
52
 
@@ -407,23 +426,6 @@ function createFetchMethod(endpoint, prefix, config, contractConfig) {
407
426
  return endpoint.output ? endpoint.output.parse(json) : json;
408
427
  };
409
428
  }
410
- function isFileDescriptor(value) {
411
- return typeof value === "object" && value !== null && !(value instanceof Blob) && "uri" in value && typeof value.uri === "string" && "name" in value && typeof value.name === "string" && "type" in value && typeof value.type === "string";
412
- }
413
- function isMultipartFile(value) {
414
- return value instanceof Blob || isFileDescriptor(value);
415
- }
416
- function appendMultipartFile(form, field, file) {
417
- const sink = form;
418
- sink.append(field, file);
419
- }
420
- function appendFormFields(formData, values, skipKeys) {
421
- for (const [key, value] of Object.entries(values)) {
422
- if (skipKeys.has(key) || value === undefined || value === null)
423
- continue;
424
- formData.append(key, typeof value === "string" ? value : JSON.stringify(value));
425
- }
426
- }
427
429
  async function throwForErrorResponse(res, config, fallbackBody) {
428
430
  const body = await res.json().catch(() => fallbackBody);
429
431
  config.onError?.(res.status, body);
@@ -29,8 +29,13 @@ export declare function normalizeError(err: unknown): AppError;
29
29
  * Validate a handler's return value against the contract `output` schema. A
30
30
  * mismatch is a **server** fault (the handler broke its own contract) — shared
31
31
  * by the HTTP and tool transports so both report it identically.
32
+ *
33
+ * `onStripped` is the migration diagnostic: a handler returning more than its
34
+ * contract declares has the extra fields **deleted**, correctly but invisibly —
35
+ * types cannot catch it (structural typing does not reject excess properties) and
36
+ * nothing logs it. Pass a reporter to find out; omit it and nothing is computed.
32
37
  */
33
- export declare function validateHandlerOutput(schema: ZodType, data: unknown): {
38
+ export declare function validateHandlerOutput(schema: ZodType, data: unknown, onStripped?: (paths: string[]) => void): {
34
39
  ok: true;
35
40
  data: unknown;
36
41
  } | {
@@ -1 +1 @@
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;AAOvC,wBAAgB,cAAc,CAAC,KAAK,EAAE,CAAC,CAAC,QAAQ,GAAG,MAAM,CAMxD;AAED,qFAAqF;AACrF,MAAM,WAAW,eAAe;IAC9B,2EAA2E;IAC3E,IAAI,EAAE,MAAM,CAAC;IACb,yDAAyD;IACzD,IAAI,EAAE,MAAM,CAAC;IACb,6CAA6C;IAC7C,OAAO,EAAE,MAAM,CAAC;CACjB;AAED;;;;;GAKG;AACH,wBAAgB,SAAS,CAAC,KAAK,EAAE,CAAC,CAAC,QAAQ,GAAG,eAAe,EAAE,CAM9D;AAKD;;;;;GAKG;AACH,wBAAgB,SAAS,CAAC,GAAG,EAAE,OAAO,GAAG,MAAM,GAAG,SAAS,CAI1D;AAED,wBAAgB,cAAc,CAAC,GAAG,EAAE,OAAO,GAAG,QAAQ,CAgBrD;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"}
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;AAQvC,wBAAgB,cAAc,CAAC,KAAK,EAAE,CAAC,CAAC,QAAQ,GAAG,MAAM,CAMxD;AAED,qFAAqF;AACrF,MAAM,WAAW,eAAe;IAC9B,2EAA2E;IAC3E,IAAI,EAAE,MAAM,CAAC;IACb,yDAAyD;IACzD,IAAI,EAAE,MAAM,CAAC;IACb,6CAA6C;IAC7C,OAAO,EAAE,MAAM,CAAC;CACjB;AAED;;;;;GAKG;AACH,wBAAgB,SAAS,CAAC,KAAK,EAAE,CAAC,CAAC,QAAQ,GAAG,eAAe,EAAE,CAM9D;AAKD;;;;;GAKG;AACH,wBAAgB,SAAS,CAAC,GAAG,EAAE,OAAO,GAAG,MAAM,GAAG,SAAS,CAI1D;AAED,wBAAgB,cAAc,CAAC,GAAG,EAAE,OAAO,GAAG,QAAQ,CAgBrD;AA4BD;;;;;;;;;GASG;AACH,wBAAgB,qBAAqB,CACnC,MAAM,EAAE,OAAO,EACf,IAAI,EAAE,OAAO,EACb,UAAU,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,IAAI,GACrC;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,IAAI,EAAE,OAAO,CAAA;CAAE,GAAG;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CAe9D"}
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Write a downloaded body to disk, with the containment check both download
3
+ * paths need.
4
+ *
5
+ * Shared by the CLI's `--output-dir` and the `mountDownload` native tool. It
6
+ * deliberately takes an **already-resolved** target rather than building one:
7
+ * the two callers derive the filename differently (an untrusted `file.name` vs a
8
+ * name derived from the URL) and, more importantly, report the path back to the
9
+ * user — so composing it here would silently change one of those outputs from
10
+ * relative to absolute.
11
+ *
12
+ * The containment re-check is cheap and belongs on this side: both callers reduce
13
+ * the name to a basename first, but that is an invariant of *their* code, and
14
+ * this is where the write actually happens.
15
+ */
16
+ export declare function writeDownload(root: string, target: string, data: Uint8Array): Promise<void>;
17
+ //# sourceMappingURL=write-download.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"write-download.d.ts","sourceRoot":"","sources":["../../src/internal/write-download.ts"],"names":[],"mappings":"AAIA;;;;;;;;;;;;;;GAcG;AACH,wBAAsB,aAAa,CACjC,IAAI,EAAE,MAAM,EACZ,MAAM,EAAE,MAAM,EACd,IAAI,EAAE,UAAU,GACf,OAAO,CAAC,IAAI,CAAC,CAMf"}
package/dist/node.js CHANGED
@@ -3,7 +3,7 @@ import {
3
3
  createImplement,
4
4
  createSocketIOServer,
5
5
  implement
6
- } from "./index-g8kyab85.js";
6
+ } from "./index-bkccbx64.js";
7
7
  import"./index-tje0q6gp.js";
8
8
  import {
9
9
  AppError,
@@ -14,7 +14,7 @@ import {
14
14
  notFound,
15
15
  rateLimited,
16
16
  unauthorized
17
- } from "./index-4gawbm74.js";
17
+ } from "./index-0d0rb85d.js";
18
18
  import"./index-dzx781tm.js";
19
19
  import"./index-khwedj16.js";
20
20
  import"./index-c7nyw0yt.js";
@@ -1 +1 @@
1
- {"version":3,"file":"create.d.ts","sourceRoot":"","sources":["../../src/server/create.ts"],"names":[],"mappings":"AA+BA,OAAO,KAAK,EAEV,eAAe,EACf,aAAa,EAGd,MAAM,SAAS,CAAC;AAEjB,wBAAgB,aAAa,CAAC,MAAM,EAAE,aAAa,GAAG,CAAC,GAAG,EAAE,OAAO,KAAK,OAAO,CAAC,QAAQ,CAAC,CA8NxF;AAED,wBAAgB,YAAY,CAAC,MAAM,EAAE,eAAe,uBAsBnD"}
1
+ {"version":3,"file":"create.d.ts","sourceRoot":"","sources":["../../src/server/create.ts"],"names":[],"mappings":"AA+BA,OAAO,KAAK,EAEV,eAAe,EACf,aAAa,EAGd,MAAM,SAAS,CAAC;AAEjB,wBAAgB,aAAa,CAAC,MAAM,EAAE,aAAa,GAAG,CAAC,GAAG,EAAE,OAAO,KAAK,OAAO,CAAC,QAAQ,CAAC,CA0OxF;AAED,wBAAgB,YAAY,CAAC,MAAM,EAAE,eAAe,uBAsBnD"}
@@ -22,11 +22,16 @@
22
22
  * CONFLICT: 'conflict', RATE_LIMITED: 'rate_limited',
23
23
  * INTERNAL_SERVER_ERROR: 'internal',
24
24
  * } satisfies Record<StitchErrorCode, string>,
25
- * render: (info) => ({ ok: false, error: { code: info.code, message: info.message } }),
25
+ * render: (info, ctx) => ({
26
+ * ok: false,
27
+ * error: { code: info.code, message: info.message },
28
+ * traceId: ctx.traceId,
29
+ * }),
26
30
  * });
27
31
  * createServer({ services, hooks: { onError } });
28
32
  * ```
29
33
  */
34
+ import type { RuntimeContext } from '../contract';
30
35
  import { type StitchErrorCode } from '../contract';
31
36
  import type { LifecycleHooks } from './types';
32
37
  /** The normalised error handed to `render` — code already remapped. */
@@ -51,9 +56,15 @@ export interface ErrorHookConfig<TWireCode extends string = string> {
51
56
  */
52
57
  codeMap?: Record<StitchErrorCode, TWireCode>;
53
58
  /** Build the response body from the resolved error. */
54
- render: (info: ResolvedError) => unknown;
59
+ /**
60
+ * Build the response body from the resolved error. `ctx` is the request's
61
+ * `RuntimeContext` — read `ctx.traceId` / `ctx.spanId` to put a correlation id
62
+ * in the envelope, which is the ordinary reason to have one. Declaring the
63
+ * parameter is optional: a one-argument `render` stays assignable.
64
+ */
65
+ render: (info: ResolvedError, ctx: RuntimeContext) => unknown;
55
66
  /** Observe the raw thrown value before rendering — logging / metrics. */
56
- onError?: (error: unknown, info: ResolvedError) => void;
67
+ onError?: (error: unknown, info: ResolvedError, ctx: RuntimeContext) => void;
57
68
  }
58
69
  /** Build an `onError` hook from a code map + envelope renderer. */
59
70
  export declare function createErrorHook<TWireCode extends string = string>(config: ErrorHookConfig<TWireCode>): NonNullable<LifecycleHooks['onError']>;
@@ -1 +1 @@
1
- {"version":3,"file":"error-hook.d.ts","sourceRoot":"","sources":["../../src/server/error-hook.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AACH,OAAO,EAAqB,KAAK,eAAe,EAAE,MAAM,aAAa,CAAC;AAEtE,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,SAAS,CAAC;AAE9C,uEAAuE;AACvE,MAAM,WAAW,aAAa;IAC5B,wFAAwF;IACxF,IAAI,EAAE,MAAM,CAAC;IACb,mBAAmB;IACnB,MAAM,EAAE,MAAM,CAAC;IACf,+FAA+F;IAC/F,OAAO,EAAE,MAAM,CAAC;IAChB,mEAAmE;IACnE,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAClC,6DAA6D;IAC7D,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAED,oCAAoC;AACpC,MAAM,WAAW,eAAe,CAAC,SAAS,SAAS,MAAM,GAAG,MAAM;IAChE;;;;OAIG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC,eAAe,EAAE,SAAS,CAAC,CAAC;IAC7C,uDAAuD;IACvD,MAAM,EAAE,CAAC,IAAI,EAAE,aAAa,KAAK,OAAO,CAAC;IACzC,yEAAyE;IACzE,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,OAAO,EAAE,IAAI,EAAE,aAAa,KAAK,IAAI,CAAC;CACzD;AAED,mEAAmE;AACnE,wBAAgB,eAAe,CAAC,SAAS,SAAS,MAAM,GAAG,MAAM,EAC/D,MAAM,EAAE,eAAe,CAAC,SAAS,CAAC,GACjC,WAAW,CAAC,cAAc,CAAC,SAAS,CAAC,CAAC,CAyBxC"}
1
+ {"version":3,"file":"error-hook.d.ts","sourceRoot":"","sources":["../../src/server/error-hook.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AAEH,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAClD,OAAO,EAAqB,KAAK,eAAe,EAAE,MAAM,aAAa,CAAC;AAEtE,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,SAAS,CAAC;AAE9C,uEAAuE;AACvE,MAAM,WAAW,aAAa;IAC5B,wFAAwF;IACxF,IAAI,EAAE,MAAM,CAAC;IACb,mBAAmB;IACnB,MAAM,EAAE,MAAM,CAAC;IACf,+FAA+F;IAC/F,OAAO,EAAE,MAAM,CAAC;IAChB,mEAAmE;IACnE,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAClC,6DAA6D;IAC7D,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAED,oCAAoC;AACpC,MAAM,WAAW,eAAe,CAAC,SAAS,SAAS,MAAM,GAAG,MAAM;IAChE;;;;OAIG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC,eAAe,EAAE,SAAS,CAAC,CAAC;IAC7C,uDAAuD;IACvD;;;;;OAKG;IACH,MAAM,EAAE,CAAC,IAAI,EAAE,aAAa,EAAE,GAAG,EAAE,cAAc,KAAK,OAAO,CAAC;IAC9D,yEAAyE;IACzE,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,OAAO,EAAE,IAAI,EAAE,aAAa,EAAE,GAAG,EAAE,cAAc,KAAK,IAAI,CAAC;CAC9E;AAED,mEAAmE;AACnE,wBAAgB,eAAe,CAAC,SAAS,SAAS,MAAM,GAAG,MAAM,EAC/D,MAAM,EAAE,eAAe,CAAC,SAAS,CAAC,GACjC,WAAW,CAAC,cAAc,CAAC,SAAS,CAAC,CAAC,CAyBxC"}
@@ -1 +1 @@
1
- {"version":3,"file":"implement.d.ts","sourceRoot":"","sources":["../../src/server/implement.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,WAAW,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAE5E,OAAO,KAAK,EAAE,QAAQ,EAAa,UAAU,EAAE,MAAM,SAAS,CAAC;AAE/D;;;;;GAKG;AACH,wBAAgB,SAAS,CACvB,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,WAAW,CAAC,EACrC,IAAI,SAAS,cAAc,GAAG,cAAc,EAC5C,QAAQ,EAAE,WAAW,CAAC,CAAC,EAAE,MAAM,CAAC,EAAE,QAAQ,EAAE,QAAQ,CAAC,CAAC,EAAE,IAAI,CAAC,GAAG,UAAU,CA+C3E;AAED;;;;GAIG;AACH,wBAAgB,eAAe,CAAC,IAAI,SAAS,cAAc,MACjD,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,WAAW,CAAC,EAC3C,UAAU,WAAW,CAAC,CAAC,EAAE,MAAM,CAAC,EAChC,UAAU,QAAQ,CAAC,CAAC,EAAE,IAAI,CAAC,KAC1B,UAAU,CACd"}
1
+ {"version":3,"file":"implement.d.ts","sourceRoot":"","sources":["../../src/server/implement.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,WAAW,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAG5E,OAAO,KAAK,EAAE,QAAQ,EAAa,UAAU,EAAE,MAAM,SAAS,CAAC;AAE/D;;;;;GAKG;AACH,wBAAgB,SAAS,CACvB,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,WAAW,CAAC,EACrC,IAAI,SAAS,cAAc,GAAG,cAAc,EAC5C,QAAQ,EAAE,WAAW,CAAC,CAAC,EAAE,MAAM,CAAC,EAAE,QAAQ,EAAE,QAAQ,CAAC,CAAC,EAAE,IAAI,CAAC,GAAG,UAAU,CAgD3E;AAED;;;;GAIG;AACH,wBAAgB,eAAe,CAAC,IAAI,SAAS,cAAc,MACjD,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,WAAW,CAAC,EAC3C,UAAU,WAAW,CAAC,CAAC,EAAE,MAAM,CAAC,EAChC,UAAU,QAAQ,CAAC,CAAC,EAAE,IAAI,CAAC,KAC1B,UAAU,CACd"}