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.
- package/README.md +1 -1
- package/dist/browser/client-url.d.ts +13 -0
- package/dist/browser/client-url.d.ts.map +1 -0
- package/dist/browser/client.d.ts +41 -5
- package/dist/browser/client.d.ts.map +1 -1
- package/dist/browser/http.d.ts +10 -7
- package/dist/browser/http.d.ts.map +1 -1
- package/dist/cli.js +2 -2
- package/dist/contract/define.d.ts +102 -4
- package/dist/contract/define.d.ts.map +1 -1
- package/dist/contract/index.d.ts +1 -1
- package/dist/contract/index.d.ts.map +1 -1
- package/dist/contract/index.js +1 -1
- package/dist/{index-czmqks7r.js → index-6jypn22c.js} +1 -1
- package/dist/{index-4pvtq6h2.js → index-7rbzbnnf.js} +104 -30
- package/dist/{index-xq45akyd.js → index-92gs1m5b.js} +1 -1
- package/dist/{index-e497hxcy.js → index-fyfk537k.js} +50 -5
- package/dist/{index-g3jrbd0z.js → index-xax049k6.js} +101 -5
- package/dist/{index-s6yhmg1k.js → index-y5scd1cr.js} +3 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +243 -111
- package/dist/internal/route-pattern.d.ts +7 -0
- package/dist/internal/route-pattern.d.ts.map +1 -0
- package/dist/node.js +3 -3
- package/dist/observability/index.js +1 -1
- package/dist/react/entity-cache.d.ts +56 -42
- package/dist/react/entity-cache.d.ts.map +1 -1
- package/dist/react.d.ts +1 -1
- package/dist/react.d.ts.map +1 -1
- package/dist/react.js +98 -46
- package/dist/server/create.d.ts.map +1 -1
- package/dist/server/implement.d.ts.map +1 -1
- package/dist/server/index.js +22 -7
- package/dist/server/middleware/cors.d.ts.map +1 -1
- package/dist/server/openapi.d.ts.map +1 -1
- package/dist/server/response-metadata.d.ts +9 -0
- package/dist/server/response-metadata.d.ts.map +1 -0
- package/dist/server/router.d.ts +2 -0
- package/dist/server/router.d.ts.map +1 -1
- package/dist/server/socket-io.d.ts +1 -1
- package/dist/server/types.d.ts +14 -8
- package/dist/server/types.d.ts.map +1 -1
- package/dist/tools/agent.d.ts +3 -0
- package/dist/tools/agent.d.ts.map +1 -1
- package/dist/tools/invoker.d.ts +33 -0
- package/dist/tools/invoker.d.ts.map +1 -0
- package/dist/tools/mcp.d.ts.map +1 -1
- package/dist/tools/mount.d.ts.map +1 -1
- package/dist/tools/names.d.ts +1 -1
- package/dist/tools/names.d.ts.map +1 -1
- package/dist/tools/native-mcp.d.ts +6 -33
- package/dist/tools/native-mcp.d.ts.map +1 -1
- package/dist/tools/remote.d.ts.map +1 -1
- package/dist/tools/runtime-tool.d.ts +58 -0
- package/dist/tools/runtime-tool.d.ts.map +1 -0
- package/dist/tools.d.ts +3 -1
- package/dist/tools.d.ts.map +1 -1
- package/dist/tools.js +295 -182
- package/llms-full.txt +562 -79
- package/package.json +1 -1
|
@@ -2,7 +2,7 @@ import {
|
|
|
2
2
|
assertCorsConfig,
|
|
3
3
|
corsHeaders,
|
|
4
4
|
corsPreflightResponse
|
|
5
|
-
} from "./index-
|
|
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-
|
|
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
|
-
|
|
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
|
|
165
|
-
|
|
166
|
-
|
|
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
|
-
|
|
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((
|
|
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
|
|
243
|
-
const
|
|
266
|
+
if (parseTrailingWildcard(route.path)) {
|
|
267
|
+
const routeSegs = route.path.split("/").filter(Boolean);
|
|
244
268
|
const pathSegs = pathname.split("/").filter(Boolean);
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
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
|
|
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:
|
|
829
|
-
logDone(
|
|
901
|
+
const empty = new Response(null, { status: responseStatus, headers: responseHeaders });
|
|
902
|
+
logDone(responseStatus);
|
|
830
903
|
return empty;
|
|
831
904
|
}
|
|
832
|
-
const body = json(result,
|
|
833
|
-
logDone(
|
|
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
|
}
|
|
@@ -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
|
|
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
|
|
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
|
|
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
|
|
190
|
-
items:
|
|
191
|
-
nextCursor:
|
|
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-
|
|
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';
|
package/dist/index.d.ts.map
CHANGED
|
@@ -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,
|
|
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"}
|