stitchkit 0.37.0 → 0.39.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 (61) hide show
  1. package/README.md +1 -1
  2. package/dist/browser/client-url.d.ts +13 -0
  3. package/dist/browser/client-url.d.ts.map +1 -0
  4. package/dist/browser/client.d.ts +41 -5
  5. package/dist/browser/client.d.ts.map +1 -1
  6. package/dist/browser/http.d.ts +10 -7
  7. package/dist/browser/http.d.ts.map +1 -1
  8. package/dist/cli.js +2 -2
  9. package/dist/contract/define.d.ts +102 -4
  10. package/dist/contract/define.d.ts.map +1 -1
  11. package/dist/contract/index.d.ts +1 -1
  12. package/dist/contract/index.d.ts.map +1 -1
  13. package/dist/contract/index.js +1 -1
  14. package/dist/{index-czmqks7r.js → index-6jypn22c.js} +1 -1
  15. package/dist/{index-4pvtq6h2.js → index-7rbzbnnf.js} +104 -30
  16. package/dist/{index-xq45akyd.js → index-92gs1m5b.js} +1 -1
  17. package/dist/{index-e497hxcy.js → index-fyfk537k.js} +50 -5
  18. package/dist/{index-g3jrbd0z.js → index-xax049k6.js} +101 -5
  19. package/dist/{index-s6yhmg1k.js → index-y5scd1cr.js} +3 -1
  20. package/dist/index.d.ts +2 -2
  21. package/dist/index.d.ts.map +1 -1
  22. package/dist/index.js +243 -111
  23. package/dist/internal/route-pattern.d.ts +7 -0
  24. package/dist/internal/route-pattern.d.ts.map +1 -0
  25. package/dist/node.js +3 -3
  26. package/dist/observability/index.js +1 -1
  27. package/dist/react/entity-cache.d.ts +56 -42
  28. package/dist/react/entity-cache.d.ts.map +1 -1
  29. package/dist/react.d.ts +1 -1
  30. package/dist/react.d.ts.map +1 -1
  31. package/dist/react.js +98 -46
  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.js +22 -7
  35. package/dist/server/middleware/cors.d.ts.map +1 -1
  36. package/dist/server/openapi.d.ts.map +1 -1
  37. package/dist/server/response-metadata.d.ts +9 -0
  38. package/dist/server/response-metadata.d.ts.map +1 -0
  39. package/dist/server/router.d.ts +2 -0
  40. package/dist/server/router.d.ts.map +1 -1
  41. package/dist/server/socket-io.d.ts +1 -1
  42. package/dist/server/types.d.ts +14 -8
  43. package/dist/server/types.d.ts.map +1 -1
  44. package/dist/tools/agent.d.ts +3 -0
  45. package/dist/tools/agent.d.ts.map +1 -1
  46. package/dist/tools/invoker.d.ts +33 -0
  47. package/dist/tools/invoker.d.ts.map +1 -0
  48. package/dist/tools/mcp.d.ts.map +1 -1
  49. package/dist/tools/mount.d.ts.map +1 -1
  50. package/dist/tools/names.d.ts +1 -1
  51. package/dist/tools/names.d.ts.map +1 -1
  52. package/dist/tools/native-mcp.d.ts +6 -33
  53. package/dist/tools/native-mcp.d.ts.map +1 -1
  54. package/dist/tools/remote.d.ts.map +1 -1
  55. package/dist/tools/runtime-tool.d.ts +58 -0
  56. package/dist/tools/runtime-tool.d.ts.map +1 -0
  57. package/dist/tools.d.ts +3 -1
  58. package/dist/tools.d.ts.map +1 -1
  59. package/dist/tools.js +295 -182
  60. package/llms-full.txt +562 -79
  61. package/package.json +1 -1
@@ -2,7 +2,7 @@ import {
2
2
  assertCorsConfig,
3
3
  corsHeaders,
4
4
  corsPreflightResponse
5
- } from "./index-czmqks7r.js";
5
+ } from "./index-6jypn22c.js";
6
6
  import {
7
7
  isWithinDir
8
8
  } from "./index-x3fcszf8.js";
@@ -19,6 +19,7 @@ import {
19
19
  mergeMeta,
20
20
  normalizeError,
21
21
  parseQueryParams,
22
+ parseTrailingWildcard,
22
23
  recordedErrorMessage,
23
24
  resolveSocketIp,
24
25
  resolveTraceId,
@@ -27,7 +28,7 @@ import {
27
28
  setRequestError,
28
29
  typedEntries,
29
30
  validateHandlerOutput
30
- } from "./index-e497hxcy.js";
31
+ } from "./index-fyfk537k.js";
31
32
 
32
33
  // src/server/multipart.ts
33
34
  var DEFAULT_MAX_UPLOAD_BYTES = 25 * 1024 * 1024;
@@ -128,10 +129,17 @@ function joinPath(...parts) {
128
129
  return `/${joined}`;
129
130
  }
130
131
  function matchSegments(patternSegments, requestSegments) {
131
- if (patternSegments.length !== requestSegments.length)
132
+ const wildcardSegment = patternSegments.at(-1);
133
+ const wildcardName = wildcardSegment?.startsWith("*") ? wildcardSegment.slice(1) : null;
134
+ const prefixLength = wildcardName ? patternSegments.length - 1 : patternSegments.length;
135
+ if (wildcardName) {
136
+ if (requestSegments.length < prefixLength)
137
+ return null;
138
+ } else if (patternSegments.length !== requestSegments.length) {
132
139
  return null;
140
+ }
133
141
  const params = {};
134
- for (const [i, pattern] of patternSegments.entries()) {
142
+ for (const [i, pattern] of patternSegments.slice(0, prefixLength).entries()) {
135
143
  const actual = requestSegments[i];
136
144
  if (actual === undefined)
137
145
  return null;
@@ -141,8 +149,18 @@ function matchSegments(patternSegments, requestSegments) {
141
149
  return null;
142
150
  }
143
151
  }
152
+ if (wildcardName) {
153
+ params[wildcardName] = requestSegments.slice(prefixLength).map(decodeURIComponent).join("/");
154
+ }
144
155
  return params;
145
156
  }
157
+ function segmentRank(segment) {
158
+ if (segment?.startsWith("*"))
159
+ return 2;
160
+ if (segment?.startsWith(":"))
161
+ return 1;
162
+ return 0;
163
+ }
146
164
  function buildRouteMap(groups) {
147
165
  const map = new Map;
148
166
  for (const { prefix, service, hooks } of groups) {
@@ -161,10 +179,9 @@ function buildRouteMap(groups) {
161
179
  entries.sort((a, b) => {
162
180
  const len = Math.min(a.segments.length, b.segments.length);
163
181
  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;
182
+ const rankDifference = segmentRank(a.segments[i]) - segmentRank(b.segments[i]);
183
+ if (rankDifference !== 0)
184
+ return rankDifference;
168
185
  }
169
186
  return a.segments.length - b.segments.length;
170
187
  });
@@ -205,7 +222,8 @@ function validateRoutes(routeMap) {
205
222
  for (const [method, entries] of routeMap) {
206
223
  const seen = new Map;
207
224
  for (const entry of entries) {
208
- const normalized = entry.segments.map((s) => s.startsWith(":") ? ":param" : s).join("/");
225
+ parseTrailingWildcard(entry.pattern);
226
+ const normalized = entry.segments.map((s) => s.startsWith(":") ? ":param" : s.startsWith("*") ? "*wildcard" : s).join("/");
209
227
  const key = `${method} /${normalized}`;
210
228
  const existing = seen.get(key);
211
229
  if (existing) {
@@ -221,7 +239,13 @@ function findShadowedRoutes(routeMap, rawRoutes) {
221
239
  const shadowed = [];
222
240
  for (const [httpMethod, entries] of routeMap) {
223
241
  for (const entry of entries) {
224
- const probe = `/${entry.segments.map((s) => s.startsWith(":") ? "__param__" : s).join("/")}`;
242
+ const probe = `/${entry.segments.map((segment) => {
243
+ if (segment.startsWith(":"))
244
+ return "__param__";
245
+ if (segment.startsWith("*"))
246
+ return "__wildcard__";
247
+ return segment;
248
+ }).join("/")}`;
225
249
  const match = matchRawRoute(rawRoutes, httpMethod, probe);
226
250
  if (!match)
227
251
  continue;
@@ -239,18 +263,12 @@ function matchRawRoute(rawRoutes, httpMethod, pathname) {
239
263
  for (const route of rawRoutes) {
240
264
  if (route.method !== "ALL" && route.method !== httpMethod)
241
265
  continue;
242
- if (route.path.endsWith("/*")) {
243
- const prefixSegs = route.path.slice(0, -2).split("/").filter(Boolean);
266
+ if (parseTrailingWildcard(route.path)) {
267
+ const routeSegs = route.path.split("/").filter(Boolean);
244
268
  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
- }
269
+ const params = matchSegments(routeSegs, pathSegs);
270
+ if (params)
271
+ return { route, params };
254
272
  continue;
255
273
  }
256
274
  if (route.path.includes("/:")) {
@@ -266,12 +284,16 @@ function matchRawRoute(rawRoutes, httpMethod, pathname) {
266
284
  }
267
285
  return null;
268
286
  }
287
+ function validateRawRoutes(rawRoutes) {
288
+ for (const route of rawRoutes ?? [])
289
+ parseTrailingWildcard(route.path);
290
+ }
269
291
  function staticRoute(prefix, dir) {
270
292
  const cleanPrefix = prefix.replace(/\/+$/, "");
271
293
  const root = resolve(dir.replace(/\/+$/, ""));
272
294
  return {
273
295
  method: "GET",
274
- path: `${cleanPrefix}/*`,
296
+ path: `${cleanPrefix}/*filePath`,
275
297
  handler: async (req) => {
276
298
  const pathname = new URL(req.url).pathname;
277
299
  let rel;
@@ -608,6 +630,39 @@ function collectExtraLogFields(config, req, url, outcome) {
608
630
  return { fields, enrichKeys };
609
631
  }
610
632
 
633
+ // src/server/response-metadata.ts
634
+ var RESERVED_HEADERS = new Set(["content-type", "content-length", "x-request-id"]);
635
+ function hasGetSetCookie(headers) {
636
+ return "getSetCookie" in headers && typeof headers.getSetCookie === "function";
637
+ }
638
+ function isReservedHeader(name) {
639
+ const normalized = name.toLowerCase();
640
+ return RESERVED_HEADERS.has(normalized) || normalized.startsWith("access-control-");
641
+ }
642
+ function createResponseMetadata() {
643
+ return { headers: new Headers };
644
+ }
645
+ function applyResponseMetadata(target, metadata, endpointIdentity) {
646
+ if (!metadata)
647
+ return;
648
+ for (const [name] of metadata.headers) {
649
+ if (isReservedHeader(name)) {
650
+ throw new Error(`${endpointIdentity} cannot set framework-owned response header "${name}"`);
651
+ }
652
+ }
653
+ const preservesSetCookie = hasGetSetCookie(metadata.headers);
654
+ for (const [name, value] of metadata.headers) {
655
+ if (name.toLowerCase() === "set-cookie" && preservesSetCookie)
656
+ continue;
657
+ target.append(name, value);
658
+ }
659
+ if (preservesSetCookie) {
660
+ for (const cookie of metadata.headers.getSetCookie()) {
661
+ target.append("Set-Cookie", cookie);
662
+ }
663
+ }
664
+ }
665
+
611
666
  // src/server/create.ts
612
667
  function createHandler(config) {
613
668
  const { cors, hooks, logging = false, trustProxy = false } = config;
@@ -643,6 +698,7 @@ function createHandler(config) {
643
698
  };
644
699
  const routeMap = buildRouteMap(normalizeGroups(config));
645
700
  validateRoutes(routeMap);
701
+ validateRawRoutes(config.rawRoutes);
646
702
  for (const shadow of findShadowedRoutes(routeMap, config.rawRoutes)) {
647
703
  const gate = shadow.scope && shadow.scope !== "public" ? ` (scope "${shadow.scope}")` : "";
648
704
  const line = `[stitchkit] raw route ${shadow.rawRoute} shadows contract route ${shadow.pattern}` + ` → ${shadow.endpoint}${gate} will never run, and its hooks never apply`;
@@ -793,12 +849,20 @@ function createHandler(config) {
793
849
  if (groupHooks?.beforeHandle) {
794
850
  await groupHooks.beforeHandle(ctx, method);
795
851
  }
852
+ const responseMetadata = method.responseMeta ? createResponseMetadata() : undefined;
853
+ if (responseMetadata)
854
+ ctx.response = responseMetadata;
796
855
  let result = await method.handler(ctx);
797
856
  if (method.rawResponse) {
798
857
  if (!(result instanceof Response)) {
799
858
  throw new AppError("INTERNAL_SERVER_ERROR", `Raw endpoint ${method.serviceName}.${method.key} must return a Response`, 500);
800
859
  }
801
- const rawRes = applyCors(result, cors, req);
860
+ const response = method.method === "HEAD" ? new Response(null, {
861
+ status: result.status,
862
+ statusText: result.statusText,
863
+ headers: result.headers
864
+ }) : result;
865
+ const rawRes = applyCors(response, cors, req);
802
866
  logDone(rawRes.status);
803
867
  return rawRes;
804
868
  }
@@ -824,13 +888,22 @@ function createHandler(config) {
824
888
  }
825
889
  result = checked.data;
826
890
  }
891
+ const responseStatus = method.responseMeta?.status ?? (result === undefined || result === null ? 204 : 200);
892
+ if (method.outputSchema && (responseStatus === 204 || responseStatus === 205)) {
893
+ throw new AppError("INTERNAL_SERVER_ERROR", `${method.serviceName}.${method.key} cannot combine output with bodyless status ${responseStatus}`, 500);
894
+ }
895
+ if (result !== undefined && result !== null && (responseStatus === 204 || responseStatus === 205)) {
896
+ throw new AppError("INTERNAL_SERVER_ERROR", `${method.serviceName}.${method.key} produced data for bodyless status ${responseStatus}`, 500);
897
+ }
898
+ const responseHeaders = new Headers(corsHeaders2(cors, req));
899
+ applyResponseMetadata(responseHeaders, responseMetadata, `${method.serviceName}.${method.key}`);
827
900
  if (result === undefined || result === null) {
828
- const empty = new Response(null, { status: 204, headers: corsHeaders2(cors, req) });
829
- logDone(204);
901
+ const empty = new Response(null, { status: responseStatus, headers: responseHeaders });
902
+ logDone(responseStatus);
830
903
  return empty;
831
904
  }
832
- const body = json(result, 200, cors, req);
833
- logDone(200);
905
+ const body = Response.json(result, { status: responseStatus, headers: responseHeaders });
906
+ logDone(responseStatus);
834
907
  return body;
835
908
  } catch (err) {
836
909
  return respondError(err, ctx, method);
@@ -923,7 +996,7 @@ function implement(contract, handlers) {
923
996
  serviceName: contract.meta.prefix,
924
997
  key: String(key),
925
998
  toolName: "toolName" in endpoint ? endpoint.toolName : undefined,
926
- expose: endpoint.rawResponse || endpoint.rawBody ? HTTP_ONLY : endpoint.expose,
999
+ expose: endpoint.rawResponse || endpoint.rawBody || endpoint.responseMeta ? HTTP_ONLY : endpoint.expose,
927
1000
  scope: endpoint.scope ?? groupScope,
928
1001
  paramsSchema: endpoint.params,
929
1002
  inputSchema: endpoint.input,
@@ -937,6 +1010,7 @@ function implement(contract, handlers) {
937
1010
  meta: mergeMeta(contract.meta.meta, endpoint.meta),
938
1011
  rawResponse: endpoint.rawResponse,
939
1012
  rawBody: endpoint.rawBody,
1013
+ responseMeta: endpoint.responseMeta,
940
1014
  contentType: "contentType" in endpoint ? endpoint.contentType : undefined,
941
1015
  handler: (ctx) => typedHandler(ctx)
942
1016
  };
@@ -1068,7 +1142,7 @@ async function createSocketIOServer(config) {
1068
1142
  const { websocket } = engine.handler();
1069
1143
  const route = {
1070
1144
  method: "ALL",
1071
- path: `${path.replace(/\/+$/, "")}/*`,
1145
+ path: `${path.replace(/\/+$/, "")}/*socketPath`,
1072
1146
  handler: (req, ctx) => {
1073
1147
  if (!ctx.server) {
1074
1148
  throw new Error("[stitchkit] createSocketIOServer route needs a running Bun server — mount it via createServer.");
@@ -1084,7 +1158,7 @@ async function createSocketIOServer(config) {
1084
1158
  websocket: { open: noop, message: noop, close: noop, maxPayloadLength: 0 },
1085
1159
  route: {
1086
1160
  method: "ALL",
1087
- path: `${path.replace(/\/+$/, "")}/*`,
1161
+ path: `${path.replace(/\/+$/, "")}/*socketPath`,
1088
1162
  handler: () => {
1089
1163
  throw new Error("[stitchkit] On Node, Socket.IO attaches via serveNode({ socket }) — this route is not mounted.");
1090
1164
  }
@@ -6,7 +6,7 @@ import {
6
6
  isUnsafeKey,
7
7
  safeJsonParse,
8
8
  unauthorized
9
- } from "./index-e497hxcy.js";
9
+ } from "./index-fyfk537k.js";
10
10
 
11
11
  // src/server/middleware/cookies.ts
12
12
  function parseCookies(header) {
@@ -68,6 +68,51 @@ function appError(code, message, details) {
68
68
  throw new AppError(code, message, isStitchErrorCode(code) ? STITCH_ERROR_STATUS[code] : 500, details);
69
69
  }
70
70
 
71
+ // src/contract/define.ts
72
+ import { z } from "zod";
73
+
74
+ // src/internal/route-pattern.ts
75
+ var PARAM_IDENTIFIER = /^[A-Za-z_][A-Za-z0-9_]*$/;
76
+ function parseTrailingWildcard(path) {
77
+ const segments = path.split("/").filter(Boolean);
78
+ const paramNames = new Set;
79
+ let wildcard = null;
80
+ for (const [segmentIndex, segment] of segments.entries()) {
81
+ if (segment.startsWith(":")) {
82
+ const name2 = segment.slice(1);
83
+ if (!PARAM_IDENTIFIER.test(name2)) {
84
+ throw new Error(`Invalid route parameter name "${name2}" in path "${path}"`);
85
+ }
86
+ if (paramNames.has(name2)) {
87
+ throw new Error(`Duplicate route parameter name "${name2}" in path "${path}"`);
88
+ }
89
+ paramNames.add(name2);
90
+ continue;
91
+ }
92
+ if (!segment.startsWith("*")) {
93
+ if (segment.includes("*")) {
94
+ throw new Error(`Wildcard must occupy its own segment in path "${path}"`);
95
+ }
96
+ continue;
97
+ }
98
+ const name = segment.slice(1);
99
+ if (!PARAM_IDENTIFIER.test(name)) {
100
+ throw new Error(`Trailing wildcard in path "${path}" must be named, for example "/*filePath"`);
101
+ }
102
+ if (segmentIndex !== segments.length - 1) {
103
+ throw new Error(`Wildcard "*${name}" must be the final segment in path "${path}"`);
104
+ }
105
+ if (wildcard) {
106
+ throw new Error(`Path "${path}" contains more than one wildcard`);
107
+ }
108
+ if (paramNames.has(name)) {
109
+ throw new Error(`Duplicate route parameter name "${name}" in path "${path}"`);
110
+ }
111
+ wildcard = { name, segmentIndex };
112
+ }
113
+ return wildcard;
114
+ }
115
+
71
116
  // src/contract/define.ts
72
117
  function mergeMeta(contractMeta, endpointMeta) {
73
118
  if (!contractMeta)
@@ -84,7 +129,7 @@ function isRecord(value) {
84
129
  return typeof value === "object" && value !== null && !Array.isArray(value);
85
130
  }
86
131
  // src/contract/pagination.ts
87
- import { z } from "zod";
132
+ import { z as z2 } from "zod";
88
133
 
89
134
  // src/internal/base64url.ts
90
135
  function bytesToBase64Url(bytes) {
@@ -102,7 +147,7 @@ function base64UrlToBytes(segment) {
102
147
  return Uint8Array.from(atob(padded), (c) => c.charCodeAt(0));
103
148
  }
104
149
  // src/internal/errors.ts
105
- import { z as z2 } from "zod";
150
+ import { z as z3 } from "zod";
106
151
  function issuePath(path) {
107
152
  return path.length > 0 ? path.map(String).join(".") : "(root)";
108
153
  }
@@ -125,7 +170,7 @@ var MAX_DETAIL_ISSUES = 20;
125
170
  function errorCode(err) {
126
171
  if (AppError.is(err))
127
172
  return err.code;
128
- if (err instanceof z2.ZodError)
173
+ if (err instanceof z3.ZodError)
129
174
  return "VALIDATION_ERROR";
130
175
  return;
131
176
  }
@@ -141,7 +186,7 @@ function recordedErrorMessage(code, envelopeMessage, thrown) {
141
186
  function normalizeError(err) {
142
187
  if (AppError.is(err))
143
188
  return err;
144
- if (err instanceof z2.ZodError) {
189
+ if (err instanceof z3.ZodError) {
145
190
  return new AppError("VALIDATION_ERROR", formatZodError(err), 400, {
146
191
  issues: zodIssues(err).slice(0, MAX_DETAIL_ISSUES)
147
192
  });
@@ -337,4 +382,4 @@ function wrapInRequestContext(handler, options = {}) {
337
382
  };
338
383
  }
339
384
 
340
- export { __require, mergeMeta, AppError, notFound, badRequest, unauthorized, forbidden, conflict, rateLimited, STITCH_ERROR_STATUS, isStitchErrorCode, appError, typedEntries, isRecord, bytesToBase64Url, base64UrlToBytes, formatZodError, zodIssues, errorCode, recordedErrorMessage, normalizeError, validateHandlerOutput, isUnsafeKey, safeJsonParse, generateTraceId, resolveTraceId, resolveSocketIp, extractIp, getClientInfo, parseQueryParams, createTraceContext, parseTraceparent, formatTraceparent, resolveTraceContext, childSpan, runWithRequestContext, getRequestContext, getTraceId, getUserId, setRequestUser, setRequestEndpoint, setRequestDimensions, setRequestError, wrapInRequestContext };
385
+ export { __require, parseTrailingWildcard, mergeMeta, AppError, notFound, badRequest, unauthorized, forbidden, conflict, rateLimited, STITCH_ERROR_STATUS, isStitchErrorCode, appError, typedEntries, isRecord, bytesToBase64Url, base64UrlToBytes, formatZodError, zodIssues, errorCode, recordedErrorMessage, normalizeError, validateHandlerOutput, isUnsafeKey, safeJsonParse, generateTraceId, resolveTraceId, resolveSocketIp, extractIp, getClientInfo, parseQueryParams, createTraceContext, parseTraceparent, formatTraceparent, resolveTraceContext, childSpan, runWithRequestContext, getRequestContext, getTraceId, getUserId, setRequestUser, setRequestEndpoint, setRequestDimensions, setRequestError, wrapInRequestContext };
@@ -2,11 +2,66 @@ import {
2
2
  mapObject
3
3
  } from "./index-809wc1tt.js";
4
4
 
5
+ // src/contract/define.ts
6
+ import { z } from "zod";
7
+
8
+ // src/internal/route-pattern.ts
9
+ var PARAM_IDENTIFIER = /^[A-Za-z_][A-Za-z0-9_]*$/;
10
+ function parseTrailingWildcard(path) {
11
+ const segments = path.split("/").filter(Boolean);
12
+ const paramNames = new Set;
13
+ let wildcard = null;
14
+ for (const [segmentIndex, segment] of segments.entries()) {
15
+ if (segment.startsWith(":")) {
16
+ const name2 = segment.slice(1);
17
+ if (!PARAM_IDENTIFIER.test(name2)) {
18
+ throw new Error(`Invalid route parameter name "${name2}" in path "${path}"`);
19
+ }
20
+ if (paramNames.has(name2)) {
21
+ throw new Error(`Duplicate route parameter name "${name2}" in path "${path}"`);
22
+ }
23
+ paramNames.add(name2);
24
+ continue;
25
+ }
26
+ if (!segment.startsWith("*")) {
27
+ if (segment.includes("*")) {
28
+ throw new Error(`Wildcard must occupy its own segment in path "${path}"`);
29
+ }
30
+ continue;
31
+ }
32
+ const name = segment.slice(1);
33
+ if (!PARAM_IDENTIFIER.test(name)) {
34
+ throw new Error(`Trailing wildcard in path "${path}" must be named, for example "/*filePath"`);
35
+ }
36
+ if (segmentIndex !== segments.length - 1) {
37
+ throw new Error(`Wildcard "*${name}" must be the final segment in path "${path}"`);
38
+ }
39
+ if (wildcard) {
40
+ throw new Error(`Path "${path}" contains more than one wildcard`);
41
+ }
42
+ if (paramNames.has(name)) {
43
+ throw new Error(`Duplicate route parameter name "${name}" in path "${path}"`);
44
+ }
45
+ wildcard = { name, segmentIndex };
46
+ }
47
+ return wildcard;
48
+ }
49
+
5
50
  // src/contract/define.ts
6
51
  var ALL_TRANSPORTS = ["HTTP", "MCP", "AGENT", "CLI"];
7
52
  function defineContract(meta, endpoints) {
8
53
  const toolTransports = new Map;
9
54
  for (const [key, ep] of Object.entries(endpoints)) {
55
+ const wildcard = parseTrailingWildcard(ep.path);
56
+ if (wildcard) {
57
+ if (!ep.params) {
58
+ throw new Error(`Contract "${meta.prefix}": endpoint "${key}" wildcard "${wildcard.name}" requires a params schema field`);
59
+ }
60
+ const paramsJson = z.toJSONSchema(ep.params, { io: "input" });
61
+ if (!paramsJson.properties || !(wildcard.name in paramsJson.properties)) {
62
+ throw new Error(`Contract "${meta.prefix}": endpoint "${key}" params schema is missing wildcard field "${wildcard.name}"`);
63
+ }
64
+ }
10
65
  if (ep.desc.trim() === "") {
11
66
  throw new Error(`Contract "${meta.prefix}": endpoint "${key}" has an empty desc`);
12
67
  }
@@ -15,8 +70,12 @@ function defineContract(meta, endpoints) {
15
70
  }
16
71
  if (ep.rawResponse)
17
72
  assertRawEndpoint(meta.prefix, key, ep);
73
+ if (ep.method === "HEAD")
74
+ assertHeadEndpoint(meta.prefix, key, ep);
18
75
  if (ep.rawBody)
19
76
  assertRawBodyEndpoint(meta.prefix, key, ep);
77
+ if ("responseMeta" in ep)
78
+ assertResponseMetaEndpoint(meta.prefix, key, ep);
20
79
  if (!("toolName" in ep) || !ep.toolName)
21
80
  continue;
22
81
  const transports = new Set(ep.expose ? ep.expose.filter((t) => t !== "HTTP") : ["MCP", "AGENT"]);
@@ -53,6 +112,17 @@ function assertRawEndpoint(prefix, key, ep) {
53
112
  throw new Error(`${where} is HTTP-only — remove ${nonHttp.join(", ")} from expose`);
54
113
  }
55
114
  }
115
+ function assertHeadEndpoint(prefix, key, ep) {
116
+ const where = `Contract "${prefix}": HEAD endpoint "${key}"`;
117
+ if (!ep.rawResponse)
118
+ throw new Error(`${where} must declare rawResponse: true`);
119
+ if (ep.input)
120
+ throw new Error(`${where} cannot declare an input schema`);
121
+ if (ep.multipart)
122
+ throw new Error(`${where} cannot be multipart`);
123
+ if (ep.rawBody)
124
+ throw new Error(`${where} cannot retain a raw body`);
125
+ }
56
126
  function assertRawBodyEndpoint(prefix, key, ep) {
57
127
  const where = `Contract "${prefix}": rawBody endpoint "${key}"`;
58
128
  if (!ep.input)
@@ -74,6 +144,32 @@ function assertRawBodyEndpoint(prefix, key, ep) {
74
144
  throw new Error(`${where} is HTTP-only — remove ${nonHttp.join(", ")} from expose`);
75
145
  }
76
146
  }
147
+ function assertResponseMetaEndpoint(prefix, key, ep) {
148
+ const where = `Contract "${prefix}": responseMeta endpoint "${key}"`;
149
+ if (!ep.responseMeta || typeof ep.responseMeta !== "object") {
150
+ throw new Error(`${where} must declare responseMeta as an object`);
151
+ }
152
+ const status = ep.responseMeta.status;
153
+ if (status !== undefined && (!Number.isSafeInteger(status) || status < 200 || status > 299)) {
154
+ throw new Error(`${where} status must be a successful 2xx integer, received ${status}`);
155
+ }
156
+ if (ep.output && (status === 204 || status === 205)) {
157
+ throw new Error(`${where} cannot combine output with bodyless status ${status}`);
158
+ }
159
+ if (ep.rawResponse)
160
+ throw new Error(`${where} cannot also be a rawResponse endpoint`);
161
+ if ("toolName" in ep && ep.toolName)
162
+ throw new Error(`${where} cannot set a toolName`);
163
+ if ("ui" in ep && ep.ui)
164
+ throw new Error(`${where} cannot set MCP ui metadata`);
165
+ if ("annotations" in ep && ep.annotations) {
166
+ throw new Error(`${where} cannot set MCP annotations`);
167
+ }
168
+ const nonHttp = (ep.expose ?? []).filter((transport) => transport !== "HTTP");
169
+ if (nonHttp.length > 0) {
170
+ throw new Error(`${where} is HTTP-only — remove ${nonHttp.join(", ")} from expose`);
171
+ }
172
+ }
77
173
  // src/contract/errors.ts
78
174
  var APP_ERROR_BRAND = Symbol.for("stitchkit.AppError");
79
175
 
@@ -166,7 +262,7 @@ function createContractFactory() {
166
262
  };
167
263
  }
168
264
  // src/contract/pagination.ts
169
- import { z } from "zod";
265
+ import { z as z2 } from "zod";
170
266
 
171
267
  // src/internal/base64url.ts
172
268
  function bytesToBase64Url(bytes) {
@@ -186,9 +282,9 @@ function base64UrlToBytes(segment) {
186
282
 
187
283
  // src/contract/pagination.ts
188
284
  function paginatedSchema(itemSchema) {
189
- return z.object({
190
- items: z.array(itemSchema),
191
- nextCursor: z.string().nullable()
285
+ return z2.object({
286
+ items: z2.array(itemSchema),
287
+ nextCursor: z2.string().nullable()
192
288
  });
193
289
  }
194
290
  function toBase64Url(str) {
@@ -210,4 +306,4 @@ function decodeCursor(cursor, schema) {
210
306
  return null;
211
307
  }
212
308
  }
213
- export { ALL_TRANSPORTS, defineContract, AppError, notFound, badRequest, unauthorized, forbidden, conflict, rateLimited, STITCH_ERROR_STATUS, isStitchErrorCode, appError, defineErrors, createContractFactory, paginatedSchema, encodeCursor, decodeCursor };
309
+ export { parseTrailingWildcard, ALL_TRANSPORTS, defineContract, AppError, notFound, badRequest, unauthorized, forbidden, conflict, rateLimited, STITCH_ERROR_STATUS, isStitchErrorCode, appError, defineErrors, createContractFactory, paginatedSchema, encodeCursor, decodeCursor };
@@ -14,7 +14,7 @@ import {
14
14
  runWithRequestContext,
15
15
  safeJsonParse,
16
16
  validateHandlerOutput
17
- } from "./index-e497hxcy.js";
17
+ } from "./index-fyfk537k.js";
18
18
 
19
19
  // src/tools/coerce.ts
20
20
  import { z } from "zod";
@@ -848,6 +848,8 @@ function collectTools(service, transport, config = {}) {
848
848
  continue;
849
849
  if (method.rawBody)
850
850
  continue;
851
+ if (method.responseMeta)
852
+ continue;
851
853
  if (method.rawResponse)
852
854
  continue;
853
855
  const name = method.toolName ?? toToolName(service.name, methodName);
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 ClientContract, type ClientRegistryValue, type ContractClientConfig, contractEndpointMatchers, createClient, createClients, createScopedClients, createUrlBuilder, createUrlBuilders, type PathPrefixArgs, type RegistryScope, type ScopeClientConfigs, type ScopedClientRegistry, type UrlBuilderConfig, } from './browser/client';
2
+ export { ApiError, type ApiEvent, type ApiEventListener, type ConfiguredHttpClient, createHttpClient, type HeaderProvider, type HttpClient, type HttpClientConfig, type RequestOptions, type UnauthorizedMatcher, } 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,cAAc,EACnB,KAAK,mBAAmB,EACxB,KAAK,oBAAoB,EACzB,wBAAwB,EACxB,YAAY,EACZ,aAAa,EACb,mBAAmB,EACnB,gBAAgB,EAChB,iBAAiB,EACjB,KAAK,cAAc,EACnB,KAAK,aAAa,EAClB,KAAK,kBAAkB,EACvB,KAAK,oBAAoB,EACzB,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,EACnB,KAAK,mBAAmB,GACzB,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"}