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.
- package/README.md +4 -2
- package/dist/browser/client-url.d.ts +11 -0
- package/dist/browser/client-url.d.ts.map +1 -0
- package/dist/browser/client.d.ts +16 -4
- package/dist/browser/client.d.ts.map +1 -1
- package/dist/browser/http.d.ts +7 -3
- package/dist/browser/http.d.ts.map +1 -1
- package/dist/cli.js +3 -3
- package/dist/contract/define.d.ts +102 -2
- package/dist/contract/define.d.ts.map +1 -1
- package/dist/contract/errors.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-bx49hskg.js → index-bgdd42pt.js} +1 -1
- package/dist/{index-0ed3bx43.js → index-frfyw9fa.js} +2 -2
- package/dist/{index-82gncajj.js → index-n5t4gnfz.js} +557 -337
- package/dist/{index-dvrn81q4.js → index-p3kwf73n.js} +165 -55
- package/dist/{index-pmftwk2a.js → index-x4wbc8sz.js} +54 -0
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +147 -114
- package/dist/node.d.ts +1 -1
- package/dist/node.d.ts.map +1 -1
- package/dist/node.js +7 -3
- package/dist/observability/index.js +3 -3
- package/dist/server/bun.d.ts +32 -0
- package/dist/server/bun.d.ts.map +1 -0
- package/dist/server/context.d.ts +1 -1
- package/dist/server/context.d.ts.map +1 -1
- package/dist/server/create.d.ts +2 -3
- package/dist/server/create.d.ts.map +1 -1
- package/dist/server/implement.d.ts.map +1 -1
- package/dist/server/index.d.ts +3 -2
- package/dist/server/index.d.ts.map +1 -1
- package/dist/server/index.js +41 -7
- package/dist/server/logger.d.ts +5 -5
- package/dist/server/logger.d.ts.map +1 -1
- package/dist/server/logging.d.ts +4 -1
- package/dist/server/logging.d.ts.map +1 -1
- package/dist/server/middleware/auth.d.ts +2 -2
- package/dist/server/middleware/auth.d.ts.map +1 -1
- package/dist/server/openapi.d.ts.map +1 -1
- package/dist/server/request-body.d.ts +4 -0
- package/dist/server/request-body.d.ts.map +1 -0
- 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 +3 -3
- package/dist/server/router.d.ts.map +1 -1
- package/dist/server/socket-io-config.d.ts +20 -0
- package/dist/server/socket-io-config.d.ts.map +1 -0
- package/dist/server/socket-io-node.d.ts +14 -0
- package/dist/server/socket-io-node.d.ts.map +1 -0
- package/dist/server/socket-io.d.ts +5 -32
- package/dist/server/socket-io.d.ts.map +1 -1
- package/dist/server/types.d.ts +63 -50
- package/dist/server/types.d.ts.map +1 -1
- package/dist/tools/agent.d.ts.map +1 -1
- package/dist/tools/execute.d.ts +46 -16
- package/dist/tools/execute.d.ts.map +1 -1
- package/dist/tools/flatten-join.d.ts +8 -0
- package/dist/tools/flatten-join.d.ts.map +1 -0
- package/dist/tools/flatten.d.ts +7 -44
- package/dist/tools/flatten.d.ts.map +1 -1
- package/dist/tools/json-schema.d.ts +1 -1
- package/dist/tools/json-schema.d.ts.map +1 -1
- package/dist/tools/manifest.d.ts.map +1 -1
- package/dist/tools/mcp-handler.d.ts +7 -13
- package/dist/tools/mcp-handler.d.ts.map +1 -1
- package/dist/tools/mcp.d.ts +61 -24
- package/dist/tools/mcp.d.ts.map +1 -1
- package/dist/tools/mount.d.ts +9 -6
- package/dist/tools/mount.d.ts.map +1 -1
- package/dist/tools/names.d.ts +1 -1
- package/dist/tools/native-mcp.d.ts +57 -0
- package/dist/tools/native-mcp.d.ts.map +1 -0
- package/dist/tools/portable-formats.d.ts +15 -0
- package/dist/tools/portable-formats.d.ts.map +1 -0
- package/dist/tools/presentation.d.ts +15 -0
- package/dist/tools/presentation.d.ts.map +1 -0
- package/dist/tools/remote.d.ts.map +1 -1
- package/dist/tools/schema.d.ts +3 -8
- package/dist/tools/schema.d.ts.map +1 -1
- package/dist/tools/tool-logger.d.ts.map +1 -1
- package/dist/tools/view-file.d.ts +5 -3
- package/dist/tools/view-file.d.ts.map +1 -1
- package/dist/tools.d.ts +7 -4
- package/dist/tools.d.ts.map +1 -1
- package/dist/tools.js +481 -205
- package/llms-full.txt +536 -87
- package/package.json +14 -11
- /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-
|
|
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
|
-
|
|
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
|
|
165
|
-
|
|
166
|
-
|
|
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((
|
|
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
|
|
264
|
+
const routeSegs = route.path.split("/").filter(Boolean);
|
|
244
265
|
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
|
-
}
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
...
|
|
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:
|
|
762
|
-
logDone(
|
|
888
|
+
const empty = new Response(null, { status: responseStatus, headers: responseHeaders });
|
|
889
|
+
logDone(responseStatus);
|
|
763
890
|
return empty;
|
|
764
891
|
}
|
|
765
|
-
const body = json(result,
|
|
766
|
-
logDone(
|
|
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,
|
|
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';
|
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,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"}
|