stitchkit 0.36.1 → 0.38.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 (92) hide show
  1. package/README.md +4 -2
  2. package/dist/browser/client-url.d.ts +11 -0
  3. package/dist/browser/client-url.d.ts.map +1 -0
  4. package/dist/browser/client.d.ts +16 -4
  5. package/dist/browser/client.d.ts.map +1 -1
  6. package/dist/browser/http.d.ts +7 -3
  7. package/dist/browser/http.d.ts.map +1 -1
  8. package/dist/cli.js +3 -3
  9. package/dist/contract/define.d.ts +102 -2
  10. package/dist/contract/define.d.ts.map +1 -1
  11. package/dist/contract/errors.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 +1 -1
  15. package/dist/{index-bx49hskg.js → index-bgdd42pt.js} +1 -1
  16. package/dist/{index-0ed3bx43.js → index-frfyw9fa.js} +2 -2
  17. package/dist/{index-82gncajj.js → index-n5t4gnfz.js} +557 -337
  18. package/dist/{index-dvrn81q4.js → index-p3kwf73n.js} +165 -55
  19. package/dist/{index-pmftwk2a.js → index-x4wbc8sz.js} +54 -0
  20. package/dist/index.d.ts +2 -2
  21. package/dist/index.d.ts.map +1 -1
  22. package/dist/index.js +147 -114
  23. package/dist/node.d.ts +1 -1
  24. package/dist/node.d.ts.map +1 -1
  25. package/dist/node.js +7 -3
  26. package/dist/observability/index.js +3 -3
  27. package/dist/server/bun.d.ts +32 -0
  28. package/dist/server/bun.d.ts.map +1 -0
  29. package/dist/server/context.d.ts +1 -1
  30. package/dist/server/context.d.ts.map +1 -1
  31. package/dist/server/create.d.ts +2 -3
  32. package/dist/server/create.d.ts.map +1 -1
  33. package/dist/server/implement.d.ts.map +1 -1
  34. package/dist/server/index.d.ts +3 -2
  35. package/dist/server/index.d.ts.map +1 -1
  36. package/dist/server/index.js +41 -7
  37. package/dist/server/logger.d.ts +5 -5
  38. package/dist/server/logger.d.ts.map +1 -1
  39. package/dist/server/logging.d.ts +4 -1
  40. package/dist/server/logging.d.ts.map +1 -1
  41. package/dist/server/middleware/auth.d.ts +2 -2
  42. package/dist/server/middleware/auth.d.ts.map +1 -1
  43. package/dist/server/openapi.d.ts.map +1 -1
  44. package/dist/server/request-body.d.ts +4 -0
  45. package/dist/server/request-body.d.ts.map +1 -0
  46. package/dist/server/response-metadata.d.ts +9 -0
  47. package/dist/server/response-metadata.d.ts.map +1 -0
  48. package/dist/server/router.d.ts +3 -3
  49. package/dist/server/router.d.ts.map +1 -1
  50. package/dist/server/socket-io-config.d.ts +20 -0
  51. package/dist/server/socket-io-config.d.ts.map +1 -0
  52. package/dist/server/socket-io-node.d.ts +14 -0
  53. package/dist/server/socket-io-node.d.ts.map +1 -0
  54. package/dist/server/socket-io.d.ts +5 -32
  55. package/dist/server/socket-io.d.ts.map +1 -1
  56. package/dist/server/types.d.ts +63 -50
  57. package/dist/server/types.d.ts.map +1 -1
  58. package/dist/tools/agent.d.ts.map +1 -1
  59. package/dist/tools/execute.d.ts +46 -16
  60. package/dist/tools/execute.d.ts.map +1 -1
  61. package/dist/tools/flatten-join.d.ts +8 -0
  62. package/dist/tools/flatten-join.d.ts.map +1 -0
  63. package/dist/tools/flatten.d.ts +7 -44
  64. package/dist/tools/flatten.d.ts.map +1 -1
  65. package/dist/tools/json-schema.d.ts +1 -1
  66. package/dist/tools/json-schema.d.ts.map +1 -1
  67. package/dist/tools/manifest.d.ts.map +1 -1
  68. package/dist/tools/mcp-handler.d.ts +7 -13
  69. package/dist/tools/mcp-handler.d.ts.map +1 -1
  70. package/dist/tools/mcp.d.ts +61 -24
  71. package/dist/tools/mcp.d.ts.map +1 -1
  72. package/dist/tools/mount.d.ts +9 -6
  73. package/dist/tools/mount.d.ts.map +1 -1
  74. package/dist/tools/names.d.ts +1 -1
  75. package/dist/tools/native-mcp.d.ts +57 -0
  76. package/dist/tools/native-mcp.d.ts.map +1 -0
  77. package/dist/tools/portable-formats.d.ts +15 -0
  78. package/dist/tools/portable-formats.d.ts.map +1 -0
  79. package/dist/tools/presentation.d.ts +15 -0
  80. package/dist/tools/presentation.d.ts.map +1 -0
  81. package/dist/tools/remote.d.ts.map +1 -1
  82. package/dist/tools/schema.d.ts +3 -8
  83. package/dist/tools/schema.d.ts.map +1 -1
  84. package/dist/tools/tool-logger.d.ts.map +1 -1
  85. package/dist/tools/view-file.d.ts +5 -3
  86. package/dist/tools/view-file.d.ts.map +1 -1
  87. package/dist/tools.d.ts +7 -4
  88. package/dist/tools.d.ts.map +1 -1
  89. package/dist/tools.js +481 -205
  90. package/llms-full.txt +536 -87
  91. package/package.json +14 -11
  92. /package/dist/{index-17rdjw68.js → index-mzx0an0s.js} +0 -0
@@ -27,7 +27,7 @@ import {
27
27
  setRequestError,
28
28
  typedEntries,
29
29
  validateHandlerOutput
30
- } from "./index-17rdjw68.js";
30
+ } from "./index-mzx0an0s.js";
31
31
 
32
32
  // src/server/multipart.ts
33
33
  var DEFAULT_MAX_UPLOAD_BYTES = 25 * 1024 * 1024;
@@ -128,10 +128,16 @@ function joinPath(...parts) {
128
128
  return `/${joined}`;
129
129
  }
130
130
  function matchSegments(patternSegments, requestSegments) {
131
- if (patternSegments.length !== requestSegments.length)
131
+ const wildcard = patternSegments.at(-1) === "*";
132
+ const prefixLength = wildcard ? patternSegments.length - 1 : patternSegments.length;
133
+ if (wildcard) {
134
+ if (requestSegments.length < prefixLength)
135
+ return null;
136
+ } else if (patternSegments.length !== requestSegments.length) {
132
137
  return null;
138
+ }
133
139
  const params = {};
134
- for (const [i, pattern] of patternSegments.entries()) {
140
+ for (const [i, pattern] of patternSegments.slice(0, prefixLength).entries()) {
135
141
  const actual = requestSegments[i];
136
142
  if (actual === undefined)
137
143
  return null;
@@ -141,8 +147,18 @@ function matchSegments(patternSegments, requestSegments) {
141
147
  return null;
142
148
  }
143
149
  }
150
+ if (wildcard) {
151
+ params["*"] = requestSegments.slice(prefixLength).map(decodeURIComponent).join("/");
152
+ }
144
153
  return params;
145
154
  }
155
+ function segmentRank(segment) {
156
+ if (segment === "*")
157
+ return 2;
158
+ if (segment?.startsWith(":"))
159
+ return 1;
160
+ return 0;
161
+ }
146
162
  function buildRouteMap(groups) {
147
163
  const map = new Map;
148
164
  for (const { prefix, service, hooks } of groups) {
@@ -161,10 +177,9 @@ function buildRouteMap(groups) {
161
177
  entries.sort((a, b) => {
162
178
  const len = Math.min(a.segments.length, b.segments.length);
163
179
  for (let i = 0;i < len; i++) {
164
- const aIsParam = a.segments[i]?.startsWith(":");
165
- const bIsParam = b.segments[i]?.startsWith(":");
166
- if (aIsParam !== bIsParam)
167
- return aIsParam ? 1 : -1;
180
+ const rankDifference = segmentRank(a.segments[i]) - segmentRank(b.segments[i]);
181
+ if (rankDifference !== 0)
182
+ return rankDifference;
168
183
  }
169
184
  return a.segments.length - b.segments.length;
170
185
  });
@@ -221,7 +236,13 @@ function findShadowedRoutes(routeMap, rawRoutes) {
221
236
  const shadowed = [];
222
237
  for (const [httpMethod, entries] of routeMap) {
223
238
  for (const entry of entries) {
224
- const probe = `/${entry.segments.map((s) => s.startsWith(":") ? "__param__" : s).join("/")}`;
239
+ const probe = `/${entry.segments.map((segment) => {
240
+ if (segment.startsWith(":"))
241
+ return "__param__";
242
+ if (segment === "*")
243
+ return "__wildcard__";
244
+ return segment;
245
+ }).join("/")}`;
225
246
  const match = matchRawRoute(rawRoutes, httpMethod, probe);
226
247
  if (!match)
227
248
  continue;
@@ -240,17 +261,11 @@ function matchRawRoute(rawRoutes, httpMethod, pathname) {
240
261
  if (route.method !== "ALL" && route.method !== httpMethod)
241
262
  continue;
242
263
  if (route.path.endsWith("/*")) {
243
- const prefixSegs = route.path.slice(0, -2).split("/").filter(Boolean);
264
+ const routeSegs = route.path.split("/").filter(Boolean);
244
265
  const pathSegs = pathname.split("/").filter(Boolean);
245
- if (pathSegs.length < prefixSegs.length)
246
- continue;
247
- const params = matchSegments(prefixSegs, pathSegs.slice(0, prefixSegs.length));
248
- if (params) {
249
- return {
250
- route,
251
- params: { ...params, "*": pathSegs.slice(prefixSegs.length).join("/") }
252
- };
253
- }
266
+ const params = matchSegments(routeSegs, pathSegs);
267
+ if (params)
268
+ return { route, params };
254
269
  continue;
255
270
  }
256
271
  if (route.path.includes("/:")) {
@@ -299,6 +314,45 @@ function staticRoute(prefix, dir) {
299
314
  };
300
315
  }
301
316
 
317
+ // src/server/request-body.ts
318
+ function assertJsonBodyLimit(maxBytes, owner) {
319
+ if (maxBytes !== undefined && (!Number.isSafeInteger(maxBytes) || maxBytes <= 0)) {
320
+ throw new Error(`${owner} must be a positive safe integer, received ${maxBytes}`);
321
+ }
322
+ }
323
+ async function readRequestText(req, maxBytes) {
324
+ if (maxBytes === undefined)
325
+ return req.text();
326
+ assertJsonBodyLimit(maxBytes, "maxJsonBodyBytes");
327
+ const reader = req.body?.getReader();
328
+ if (!reader)
329
+ return "";
330
+ const chunks = [];
331
+ let total = 0;
332
+ try {
333
+ while (true) {
334
+ const { done, value } = await reader.read();
335
+ if (done)
336
+ break;
337
+ total += value.byteLength;
338
+ if (total > maxBytes) {
339
+ await reader.cancel();
340
+ badRequest(`JSON body exceeds the ${maxBytes}-byte limit`);
341
+ }
342
+ chunks.push(value);
343
+ }
344
+ } finally {
345
+ reader.releaseLock();
346
+ }
347
+ const bytes = new Uint8Array(total);
348
+ let offset = 0;
349
+ for (const chunk of chunks) {
350
+ bytes.set(chunk, offset);
351
+ offset += chunk.byteLength;
352
+ }
353
+ return new TextDecoder().decode(bytes);
354
+ }
355
+
302
356
  // src/server/context.ts
303
357
  var RESERVED_KEYS = new Set([
304
358
  "params",
@@ -307,13 +361,13 @@ var RESERVED_KEYS = new Set([
307
361
  "req",
308
362
  "url",
309
363
  "headers",
364
+ "rawBody",
310
365
  "traceId",
311
366
  "spanId",
312
367
  "ipAddress",
313
368
  "userAgent"
314
369
  ]);
315
- async function readJsonBody(req) {
316
- const text = await req.text();
370
+ function parseJsonBody(req, text) {
317
371
  if (text.trim() === "")
318
372
  return {};
319
373
  if (!req.headers.get("content-type")?.includes("application/json")) {
@@ -343,7 +397,7 @@ function buildBaseContext(req, url, pathParams, traceId, clientIp) {
343
397
  ...getClientInfo(req, clientIp)
344
398
  };
345
399
  }
346
- async function parseRequestInto(ctx, req, url, method, maxUploadBytes) {
400
+ async function parseRequestInto(ctx, req, url, method, maxUploadBytes, maxJsonBodyBytes) {
347
401
  if (method.paramsSchema) {
348
402
  ctx.params = method.paramsSchema.parse(ctx.params);
349
403
  }
@@ -358,9 +412,19 @@ async function parseRequestInto(ctx, req, url, method, maxUploadBytes) {
358
412
  ctx.input = method.inputSchema.parse(parseQueryParams(url));
359
413
  } else if (req.method === "DELETE") {
360
414
  const ct = req.headers.get("content-type");
361
- ctx.input = ct?.includes("application/json") ? method.inputSchema.parse(await readJsonBody(req)) : method.inputSchema.parse(parseQueryParams(url));
415
+ if (ct?.includes("application/json")) {
416
+ const text = await readRequestText(req, method.maxJsonBodyBytes ?? maxJsonBodyBytes);
417
+ if (method.rawBody)
418
+ ctx.rawBody = text;
419
+ ctx.input = method.inputSchema.parse(parseJsonBody(req, text));
420
+ } else {
421
+ ctx.input = method.inputSchema.parse(parseQueryParams(url));
422
+ }
362
423
  } else {
363
- ctx.input = method.inputSchema.parse(await readJsonBody(req));
424
+ const text = await readRequestText(req, method.maxJsonBodyBytes ?? maxJsonBodyBytes);
425
+ if (method.rawBody)
426
+ ctx.rawBody = text;
427
+ ctx.input = method.inputSchema.parse(parseJsonBody(req, text));
364
428
  }
365
429
  }
366
430
  }
@@ -433,7 +497,10 @@ function levelForStatus(status) {
433
497
  return "info";
434
498
  }
435
499
  function buildLogFields(method, path, status, durationMs, traceId, errorCode2) {
436
- return { traceId, method, path, status, durationMs, errorCode: errorCode2 };
500
+ const fields = { traceId, method, path, status, durationMs };
501
+ if (errorCode2 === undefined && status >= 400)
502
+ return fields;
503
+ return { ...fields, errorCode: errorCode2 };
437
504
  }
438
505
  function formatMs(ms) {
439
506
  if (ms >= 1000)
@@ -532,6 +599,7 @@ function shouldSkipLog(config, req, url) {
532
599
  }
533
600
  function collectExtraLogFields(config, req, url, outcome) {
534
601
  const fields = {};
602
+ const enrichKeys = [];
535
603
  const ctx = getRequestContext();
536
604
  if (ctx) {
537
605
  if (ctx.userId)
@@ -546,11 +614,46 @@ function collectExtraLogFields(config, req, url, outcome) {
546
614
  if (config.enrich) {
547
615
  try {
548
616
  const extra = config.enrich(req, url, outcome);
549
- if (extra)
617
+ if (extra) {
618
+ enrichKeys.push(...Object.keys(extra));
550
619
  Object.assign(fields, extra);
620
+ }
551
621
  } catch {}
552
622
  }
553
- return fields;
623
+ return { fields, enrichKeys };
624
+ }
625
+
626
+ // src/server/response-metadata.ts
627
+ var RESERVED_HEADERS = new Set(["content-type", "content-length", "x-request-id"]);
628
+ function hasGetSetCookie(headers) {
629
+ return "getSetCookie" in headers && typeof headers.getSetCookie === "function";
630
+ }
631
+ function isReservedHeader(name) {
632
+ const normalized = name.toLowerCase();
633
+ return RESERVED_HEADERS.has(normalized) || normalized.startsWith("access-control-");
634
+ }
635
+ function createResponseMetadata() {
636
+ return { headers: new Headers };
637
+ }
638
+ function applyResponseMetadata(target, metadata, endpointIdentity) {
639
+ if (!metadata)
640
+ return;
641
+ for (const [name] of metadata.headers) {
642
+ if (isReservedHeader(name)) {
643
+ throw new Error(`${endpointIdentity} cannot set framework-owned response header "${name}"`);
644
+ }
645
+ }
646
+ const preservesSetCookie = hasGetSetCookie(metadata.headers);
647
+ for (const [name, value] of metadata.headers) {
648
+ if (name.toLowerCase() === "set-cookie" && preservesSetCookie)
649
+ continue;
650
+ target.append(name, value);
651
+ }
652
+ if (preservesSetCookie) {
653
+ for (const cookie of metadata.headers.getSetCookie()) {
654
+ target.append("Set-Cookie", cookie);
655
+ }
656
+ }
554
657
  }
555
658
 
556
659
  // src/server/create.ts
@@ -558,6 +661,7 @@ function createHandler(config) {
558
661
  const { cors, hooks, logging = false, trustProxy = false } = config;
559
662
  if (cors)
560
663
  assertCorsConfig(cors);
664
+ assertJsonBodyLimit(config.maxJsonBodyBytes, "HandlerConfig.maxJsonBodyBytes");
561
665
  const logConfig = resolveLoggingConfig(logging);
562
666
  const customLogger = logConfig?.logger ?? null;
563
667
  const useDefaultLog = logConfig !== null && !logConfig.logger;
@@ -571,6 +675,7 @@ function createHandler(config) {
571
675
  } catch {}
572
676
  };
573
677
  let traceResolverBroken = false;
678
+ const warnedEnrichKeys = new Set;
574
679
  const resolveId = (req) => {
575
680
  if (!customTraceId)
576
681
  return resolveTraceId(req);
@@ -617,11 +722,21 @@ function createHandler(config) {
617
722
  logged = true;
618
723
  try {
619
724
  const durationMs = Math.round(elapsedMs(reqLog.startTime));
620
- const extra = collectExtraLogFields(logConfig, req, url, {
725
+ const collected = collectExtraLogFields(logConfig, req, url, {
621
726
  status,
622
727
  durationMs,
623
728
  errorCode: errorCode2
624
729
  });
730
+ const frameworkFields = buildLogFields(req.method, url.pathname, status, durationMs, reqLog.traceId, errorCode2);
731
+ const ownedFields = { ...frameworkFields, ip: ipAddress };
732
+ for (const key of collected.enrichKeys) {
733
+ const ownedByActiveSink = Object.hasOwn(ownedFields, key) || useDefaultLog && (key === "ts" || key === "level" || key === "msg");
734
+ if (ownedByActiveSink && !warnedEnrichKeys.has(key)) {
735
+ warnedEnrichKeys.add(key);
736
+ warn(`[stitchkit] logging.enrich field "${key}" was discarded because the framework owns it`);
737
+ }
738
+ }
739
+ const extra = collected.fields;
625
740
  if (useDefaultLog) {
626
741
  logOutgoing({
627
742
  req,
@@ -639,7 +754,7 @@ function createHandler(config) {
639
754
  const level = levelForStatus(status);
640
755
  customLogger[level](`${req.method} ${url.pathname} ${status}${errorCode2 ? ` ${errorCode2}` : ""} ${durationMs}ms`, {
641
756
  ...extra,
642
- ...buildLogFields(req.method, url.pathname, status, durationMs, reqLog.traceId, errorCode2),
757
+ ...frameworkFields,
643
758
  ip: ipAddress
644
759
  });
645
760
  }
@@ -719,13 +834,16 @@ function createHandler(config) {
719
834
  const ctx = buildBaseContext(req, url, pathParams, traceId, clientIp);
720
835
  setRequestEndpoint(method.serviceName, method.key);
721
836
  try {
722
- await parseRequestInto(ctx, req, url, method, config.maxUploadBytes);
837
+ await parseRequestInto(ctx, req, url, method, config.maxUploadBytes, config.maxJsonBodyBytes);
723
838
  if (hooks?.beforeHandle) {
724
839
  await hooks.beforeHandle(ctx, method);
725
840
  }
726
841
  if (groupHooks?.beforeHandle) {
727
842
  await groupHooks.beforeHandle(ctx, method);
728
843
  }
844
+ const responseMetadata = method.responseMeta ? createResponseMetadata() : undefined;
845
+ if (responseMetadata)
846
+ ctx.response = responseMetadata;
729
847
  let result = await method.handler(ctx);
730
848
  if (method.rawResponse) {
731
849
  if (!(result instanceof Response)) {
@@ -757,13 +875,22 @@ function createHandler(config) {
757
875
  }
758
876
  result = checked.data;
759
877
  }
878
+ const responseStatus = method.responseMeta?.status ?? (result === undefined || result === null ? 204 : 200);
879
+ if (method.outputSchema && (responseStatus === 204 || responseStatus === 205)) {
880
+ throw new AppError("INTERNAL_SERVER_ERROR", `${method.serviceName}.${method.key} cannot combine output with bodyless status ${responseStatus}`, 500);
881
+ }
882
+ if (result !== undefined && result !== null && (responseStatus === 204 || responseStatus === 205)) {
883
+ throw new AppError("INTERNAL_SERVER_ERROR", `${method.serviceName}.${method.key} produced data for bodyless status ${responseStatus}`, 500);
884
+ }
885
+ const responseHeaders = new Headers(corsHeaders2(cors, req));
886
+ applyResponseMetadata(responseHeaders, responseMetadata, `${method.serviceName}.${method.key}`);
760
887
  if (result === undefined || result === null) {
761
- const empty = new Response(null, { status: 204, headers: corsHeaders2(cors, req) });
762
- logDone(204);
888
+ const empty = new Response(null, { status: responseStatus, headers: responseHeaders });
889
+ logDone(responseStatus);
763
890
  return empty;
764
891
  }
765
- const body = json(result, 200, cors, req);
766
- logDone(200);
892
+ const body = Response.json(result, { status: responseStatus, headers: responseHeaders });
893
+ logDone(responseStatus);
767
894
  return body;
768
895
  } catch (err) {
769
896
  return respondError(err, ctx, method);
@@ -783,26 +910,6 @@ function createHandler(config) {
783
910
  return response;
784
911
  };
785
912
  }
786
- function createServer(config) {
787
- const { routes, websocket, development, bun: bunExtra, port = 3000, hostname } = config;
788
- const handler = createHandler(config);
789
- const fetch = config.wrapFetch ? config.wrapFetch(handler) : handler;
790
- return websocket ? Bun.serve({
791
- ...bunExtra,
792
- ...routes && { routes },
793
- ...development && { development },
794
- port,
795
- hostname,
796
- websocket,
797
- fetch
798
- }) : Bun.serve({
799
- ...bunExtra,
800
- ...development && { development },
801
- port,
802
- hostname,
803
- fetch
804
- });
805
- }
806
913
  function normalizeGroups(config) {
807
914
  const result = [];
808
915
  if (config.services) {
@@ -876,18 +983,21 @@ function implement(contract, handlers) {
876
983
  serviceName: contract.meta.prefix,
877
984
  key: String(key),
878
985
  toolName: "toolName" in endpoint ? endpoint.toolName : undefined,
879
- expose: endpoint.rawResponse ? HTTP_ONLY : endpoint.expose,
986
+ expose: endpoint.rawResponse || endpoint.rawBody || endpoint.responseMeta ? HTTP_ONLY : endpoint.expose,
880
987
  scope: endpoint.scope ?? groupScope,
881
988
  paramsSchema: endpoint.params,
882
989
  inputSchema: endpoint.input,
883
990
  outputSchema: endpoint.output,
884
991
  multipart: endpoint.multipart,
885
992
  maxUploadBytes: endpoint.maxUploadBytes,
993
+ maxJsonBodyBytes: endpoint.maxJsonBodyBytes,
886
994
  idempotent: endpoint.idempotent,
887
995
  ui: "ui" in endpoint ? endpoint.ui : undefined,
888
996
  annotations: "annotations" in endpoint ? endpoint.annotations : undefined,
889
997
  meta: mergeMeta(contract.meta.meta, endpoint.meta),
890
998
  rawResponse: endpoint.rawResponse,
999
+ rawBody: endpoint.rawBody,
1000
+ responseMeta: endpoint.responseMeta,
891
1001
  contentType: "contentType" in endpoint ? endpoint.contentType : undefined,
892
1002
  handler: (ctx) => typedHandler(ctx)
893
1003
  };
@@ -1049,4 +1159,4 @@ function socketIoLane(websocket) {
1049
1159
  });
1050
1160
  }
1051
1161
 
1052
- export { parseMultipart, mimeForPath, staticRoute, createHandler, createServer, implement, createImplement, webSocketLane, composeWebSocketHandlers, createSocketIOServer, socketIoLane };
1162
+ export { parseMultipart, mimeForPath, staticRoute, createHandler, implement, createImplement, webSocketLane, composeWebSocketHandlers, createSocketIOServer, socketIoLane };
@@ -10,8 +10,15 @@ function defineContract(meta, endpoints) {
10
10
  if (ep.desc.trim() === "") {
11
11
  throw new Error(`Contract "${meta.prefix}": endpoint "${key}" has an empty desc`);
12
12
  }
13
+ if (ep.maxJsonBodyBytes !== undefined && (!Number.isSafeInteger(ep.maxJsonBodyBytes) || ep.maxJsonBodyBytes <= 0)) {
14
+ throw new Error(`Contract "${meta.prefix}": endpoint "${key}" maxJsonBodyBytes must be a positive safe integer, received ${ep.maxJsonBodyBytes}`);
15
+ }
13
16
  if (ep.rawResponse)
14
17
  assertRawEndpoint(meta.prefix, key, ep);
18
+ if (ep.rawBody)
19
+ assertRawBodyEndpoint(meta.prefix, key, ep);
20
+ if ("responseMeta" in ep)
21
+ assertResponseMetaEndpoint(meta.prefix, key, ep);
15
22
  if (!("toolName" in ep) || !ep.toolName)
16
23
  continue;
17
24
  const transports = new Set(ep.expose ? ep.expose.filter((t) => t !== "HTTP") : ["MCP", "AGENT"]);
@@ -48,6 +55,53 @@ function assertRawEndpoint(prefix, key, ep) {
48
55
  throw new Error(`${where} is HTTP-only — remove ${nonHttp.join(", ")} from expose`);
49
56
  }
50
57
  }
58
+ function assertRawBodyEndpoint(prefix, key, ep) {
59
+ const where = `Contract "${prefix}": rawBody endpoint "${key}"`;
60
+ if (!ep.input)
61
+ throw new Error(`${where} must declare an input schema`);
62
+ if (ep.multipart)
63
+ throw new Error(`${where} cannot be multipart`);
64
+ if (ep.method !== "POST" && ep.method !== "PUT" && ep.method !== "PATCH") {
65
+ throw new Error(`${where} must use POST, PUT or PATCH`);
66
+ }
67
+ if ("toolName" in ep && ep.toolName)
68
+ throw new Error(`${where} cannot set a toolName`);
69
+ if ("ui" in ep && ep.ui)
70
+ throw new Error(`${where} cannot set MCP ui metadata`);
71
+ if ("annotations" in ep && ep.annotations) {
72
+ throw new Error(`${where} cannot set MCP annotations`);
73
+ }
74
+ const nonHttp = (ep.expose ?? []).filter((transport) => transport !== "HTTP");
75
+ if (nonHttp.length > 0) {
76
+ throw new Error(`${where} is HTTP-only — remove ${nonHttp.join(", ")} from expose`);
77
+ }
78
+ }
79
+ function assertResponseMetaEndpoint(prefix, key, ep) {
80
+ const where = `Contract "${prefix}": responseMeta endpoint "${key}"`;
81
+ if (!ep.responseMeta || typeof ep.responseMeta !== "object") {
82
+ throw new Error(`${where} must declare responseMeta as an object`);
83
+ }
84
+ const status = ep.responseMeta.status;
85
+ if (status !== undefined && (!Number.isSafeInteger(status) || status < 200 || status > 299)) {
86
+ throw new Error(`${where} status must be a successful 2xx integer, received ${status}`);
87
+ }
88
+ if (ep.output && (status === 204 || status === 205)) {
89
+ throw new Error(`${where} cannot combine output with bodyless status ${status}`);
90
+ }
91
+ if (ep.rawResponse)
92
+ throw new Error(`${where} cannot also be a rawResponse endpoint`);
93
+ if ("toolName" in ep && ep.toolName)
94
+ throw new Error(`${where} cannot set a toolName`);
95
+ if ("ui" in ep && ep.ui)
96
+ throw new Error(`${where} cannot set MCP ui metadata`);
97
+ if ("annotations" in ep && ep.annotations) {
98
+ throw new Error(`${where} cannot set MCP annotations`);
99
+ }
100
+ const nonHttp = (ep.expose ?? []).filter((transport) => transport !== "HTTP");
101
+ if (nonHttp.length > 0) {
102
+ throw new Error(`${where} is HTTP-only — remove ${nonHttp.join(", ")} from expose`);
103
+ }
104
+ }
51
105
  // src/contract/errors.ts
52
106
  var APP_ERROR_BRAND = Symbol.for("stitchkit.AppError");
53
107
 
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
- export { type ClientConfig, type ContractClientConfig, createClient, createClients, } from './browser/client';
2
- export { ApiError, type ApiEvent, type ApiEventListener, createHttpClient, type HeaderProvider, type HttpClient, type HttpClientConfig, type RequestOptions, } from './browser/http';
1
+ export { type ClientConfig, type ContractClientConfig, createClient, createClients, createUrlBuilder, createUrlBuilders, type PathPrefixArgs, type UrlBuilderConfig, } from './browser/client';
2
+ export { ApiError, type ApiEvent, type ApiEventListener, type ConfiguredHttpClient, createHttpClient, type HeaderProvider, type HttpClient, type HttpClientConfig, type RequestOptions, } from './browser/http';
3
3
  export type { SocketEventMap, SocketIOClient, SocketIOClientConfig, } from './browser/socket-io';
4
4
  export { createSocketIOClient } from './browser/socket-io';
5
5
  export { type ParseSSEOptions, parseSSE } from './browser/stream';
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,KAAK,YAAY,EACjB,KAAK,oBAAoB,EACzB,YAAY,EACZ,aAAa,GACd,MAAM,kBAAkB,CAAC;AAC1B,OAAO,EACL,QAAQ,EACR,KAAK,QAAQ,EACb,KAAK,gBAAgB,EACrB,gBAAgB,EAChB,KAAK,cAAc,EACnB,KAAK,UAAU,EACf,KAAK,gBAAgB,EACrB,KAAK,cAAc,GACpB,MAAM,gBAAgB,CAAC;AACxB,YAAY,EACV,cAAc,EACd,cAAc,EACd,oBAAoB,GACrB,MAAM,qBAAqB,CAAC;AAC7B,OAAO,EAAE,oBAAoB,EAAE,MAAM,qBAAqB,CAAC;AAC3D,OAAO,EAAE,KAAK,eAAe,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AAClE,cAAc,YAAY,CAAC;AAG3B,OAAO,EACL,SAAS,EACT,kBAAkB,EAClB,iBAAiB,EACjB,gBAAgB,EAChB,KAAK,YAAY,GAClB,MAAM,uBAAuB,CAAC;AAC/B,OAAO,EAAE,oBAAoB,EAAE,KAAK,cAAc,EAAE,MAAM,YAAY,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,KAAK,YAAY,EACjB,KAAK,oBAAoB,EACzB,YAAY,EACZ,aAAa,EACb,gBAAgB,EAChB,iBAAiB,EACjB,KAAK,cAAc,EACnB,KAAK,gBAAgB,GACtB,MAAM,kBAAkB,CAAC;AAC1B,OAAO,EACL,QAAQ,EACR,KAAK,QAAQ,EACb,KAAK,gBAAgB,EACrB,KAAK,oBAAoB,EACzB,gBAAgB,EAChB,KAAK,cAAc,EACnB,KAAK,UAAU,EACf,KAAK,gBAAgB,EACrB,KAAK,cAAc,GACpB,MAAM,gBAAgB,CAAC;AACxB,YAAY,EACV,cAAc,EACd,cAAc,EACd,oBAAoB,GACrB,MAAM,qBAAqB,CAAC;AAC7B,OAAO,EAAE,oBAAoB,EAAE,MAAM,qBAAqB,CAAC;AAC3D,OAAO,EAAE,KAAK,eAAe,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AAClE,cAAc,YAAY,CAAC;AAG3B,OAAO,EACL,SAAS,EACT,kBAAkB,EAClB,iBAAiB,EACjB,gBAAgB,EAChB,KAAK,YAAY,GAClB,MAAM,uBAAuB,CAAC;AAC/B,OAAO,EAAE,oBAAoB,EAAE,KAAK,cAAc,EAAE,MAAM,YAAY,CAAC"}