stitchkit 0.48.0 → 0.49.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 -2
- package/dist/browser/client.d.ts +3 -0
- package/dist/browser/client.d.ts.map +1 -1
- package/dist/browser/http.d.ts +7 -4
- package/dist/browser/http.d.ts.map +1 -1
- package/dist/cli.js +3 -2
- package/dist/{index-nrytvb30.js → index-03j2t778.js} +5 -8
- package/dist/{index-kp8xamqp.js → index-0hj37z43.js} +4 -2
- package/dist/index-48ffdxgk.js +6 -0
- package/dist/{index-44xysy8r.js → index-fjfzsq6y.js} +383 -26
- package/dist/index-h05ygjqx.js +149 -0
- package/dist/{index-ee621cmy.js → index-jewp9r0a.js} +5 -139
- package/dist/index-p9d4cxt5.js +436 -0
- package/dist/{index-8ekq6res.js → index-v58mwa19.js} +4 -2
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +47 -3
- package/dist/node.d.ts +4 -3
- package/dist/node.d.ts.map +1 -1
- package/dist/node.js +109 -14
- package/dist/observability/audit.d.ts +4 -1
- package/dist/observability/audit.d.ts.map +1 -1
- package/dist/observability/index.d.ts +1 -0
- package/dist/observability/index.d.ts.map +1 -1
- package/dist/observability/index.js +150 -16
- package/dist/observability/status.d.ts +105 -0
- package/dist/observability/status.d.ts.map +1 -0
- package/dist/server/bun.d.ts +7 -8
- package/dist/server/bun.d.ts.map +1 -1
- package/dist/server/implement.d.ts +16 -6
- package/dist/server/implement.d.ts.map +1 -1
- package/dist/server/index.d.ts +4 -3
- package/dist/server/index.d.ts.map +1 -1
- package/dist/server/index.js +132 -11
- package/dist/server/node.d.ts +12 -13
- package/dist/server/node.d.ts.map +1 -1
- package/dist/server/router.d.ts +4 -1
- package/dist/server/router.d.ts.map +1 -1
- package/dist/server/shutdown.d.ts +76 -0
- package/dist/server/shutdown.d.ts.map +1 -0
- package/dist/server/socket-io-config.d.ts +5 -1
- package/dist/server/socket-io-config.d.ts.map +1 -1
- package/dist/server/socket-io-node.d.ts +4 -1
- package/dist/server/socket-io-node.d.ts.map +1 -1
- package/dist/server/socket-io.d.ts +12 -4
- package/dist/server/socket-io.d.ts.map +1 -1
- package/dist/testing.d.ts +34 -0
- package/dist/testing.d.ts.map +1 -0
- package/dist/testing.js +36 -0
- package/dist/tools/mcp.d.ts +1 -0
- package/dist/tools/mcp.d.ts.map +1 -1
- package/dist/tools.js +21 -428
- package/llms-full.txt +290 -43
- package/package.json +8 -3
|
@@ -5,31 +5,33 @@ import {
|
|
|
5
5
|
} from "./index-r1qp4rve.js";
|
|
6
6
|
import {
|
|
7
7
|
AppError,
|
|
8
|
-
__require,
|
|
9
8
|
badRequest,
|
|
10
|
-
callRuntimeHandler,
|
|
11
9
|
errorCode,
|
|
12
10
|
extractIp,
|
|
13
11
|
getClientInfo,
|
|
14
12
|
getRequestContext,
|
|
15
|
-
isRecord,
|
|
16
13
|
isUnsafeKey,
|
|
17
14
|
mergeMeta,
|
|
18
15
|
normalizeError,
|
|
19
16
|
parseQueryParams,
|
|
20
|
-
parseTrailingWildcard,
|
|
21
17
|
recordedErrorMessage,
|
|
22
18
|
resolveSocketIp,
|
|
23
|
-
resolveTraceContext,
|
|
24
19
|
resolveTraceId,
|
|
25
20
|
runWithRequestContext,
|
|
26
21
|
safeJsonParse,
|
|
27
22
|
setRequestEndpoint,
|
|
28
23
|
setRequestError,
|
|
29
|
-
typedEntries,
|
|
30
24
|
validateDeclaredOutput,
|
|
31
25
|
zodIssues
|
|
32
|
-
} from "./index-
|
|
26
|
+
} from "./index-jewp9r0a.js";
|
|
27
|
+
import {
|
|
28
|
+
__require,
|
|
29
|
+
callRuntimeHandler,
|
|
30
|
+
isRecord,
|
|
31
|
+
parseTrailingWildcard,
|
|
32
|
+
resolveTraceContext,
|
|
33
|
+
typedEntries
|
|
34
|
+
} from "./index-h05ygjqx.js";
|
|
33
35
|
|
|
34
36
|
// src/server/multipart.ts
|
|
35
37
|
var DEFAULT_MAX_REQUEST_BYTES = 25 * 1024 * 1024;
|
|
@@ -803,6 +805,50 @@ function joinPath(...parts) {
|
|
|
803
805
|
const joined = parts.filter(Boolean).map((part) => part.replace(/^\/+|\/+$/g, "")).filter(Boolean).join("/");
|
|
804
806
|
return `/${joined}`;
|
|
805
807
|
}
|
|
808
|
+
function routeSegments(path) {
|
|
809
|
+
return path.split("/").filter(Boolean);
|
|
810
|
+
}
|
|
811
|
+
function rawRouteShape(path) {
|
|
812
|
+
parseTrailingWildcard(path);
|
|
813
|
+
const segments = routeSegments(path).map((segment) => {
|
|
814
|
+
if (segment.startsWith(":"))
|
|
815
|
+
return { kind: "param" };
|
|
816
|
+
if (segment.startsWith("*"))
|
|
817
|
+
return { kind: "wildcard" };
|
|
818
|
+
return { kind: "static", value: segment };
|
|
819
|
+
});
|
|
820
|
+
return {
|
|
821
|
+
segments,
|
|
822
|
+
signature: segments.map((segment) => segment.kind === "static" ? segment.value : `:${segment.kind}`).join("/"),
|
|
823
|
+
wildcard: segments.at(-1)?.kind === "wildcard"
|
|
824
|
+
};
|
|
825
|
+
}
|
|
826
|
+
function segmentCovers(earlier, later) {
|
|
827
|
+
if (earlier.kind === "param")
|
|
828
|
+
return later.kind !== "wildcard";
|
|
829
|
+
if (earlier.kind === "wildcard")
|
|
830
|
+
return true;
|
|
831
|
+
return later.kind === "static" && earlier.value === later.value;
|
|
832
|
+
}
|
|
833
|
+
function routeShapeCovers(earlier, later) {
|
|
834
|
+
const earlierPrefixLength = earlier.wildcard ? earlier.segments.length - 1 : earlier.segments.length;
|
|
835
|
+
const laterPrefixLength = later.wildcard ? later.segments.length - 1 : later.segments.length;
|
|
836
|
+
if (earlier.wildcard) {
|
|
837
|
+
if (earlierPrefixLength > laterPrefixLength)
|
|
838
|
+
return false;
|
|
839
|
+
} else {
|
|
840
|
+
if (later.wildcard || earlierPrefixLength !== laterPrefixLength)
|
|
841
|
+
return false;
|
|
842
|
+
}
|
|
843
|
+
for (let index = 0;index < earlierPrefixLength; index++) {
|
|
844
|
+
const earlierSegment = earlier.segments[index];
|
|
845
|
+
const laterSegment = later.segments[index];
|
|
846
|
+
if (!earlierSegment || !laterSegment || !segmentCovers(earlierSegment, laterSegment)) {
|
|
847
|
+
return false;
|
|
848
|
+
}
|
|
849
|
+
}
|
|
850
|
+
return true;
|
|
851
|
+
}
|
|
806
852
|
function matchSegments(patternSegments, requestSegments) {
|
|
807
853
|
const wildcardSegment = patternSegments.at(-1);
|
|
808
854
|
const wildcardName = wildcardSegment?.startsWith("*") ? wildcardSegment.slice(1) : null;
|
|
@@ -844,7 +890,7 @@ function buildRouteMap(groups) {
|
|
|
844
890
|
continue;
|
|
845
891
|
const servicePath = joinPath("/", service.prefix, method.path === "/" ? "" : method.path);
|
|
846
892
|
const fullPath = prefix ? joinPath(prefix, servicePath) : servicePath;
|
|
847
|
-
const segments = fullPath
|
|
893
|
+
const segments = routeSegments(fullPath);
|
|
848
894
|
const entries = map.get(method.method) ?? [];
|
|
849
895
|
entries.push({ method, pattern: fullPath, segments, groupHooks: hooks });
|
|
850
896
|
map.set(method.method, entries);
|
|
@@ -867,7 +913,7 @@ function matchRoute(routeMap, httpMethod, pathname) {
|
|
|
867
913
|
const entries = routeMap.get(httpMethod);
|
|
868
914
|
if (!entries)
|
|
869
915
|
return null;
|
|
870
|
-
const requestSegments = pathname
|
|
916
|
+
const requestSegments = routeSegments(pathname);
|
|
871
917
|
for (const entry of entries) {
|
|
872
918
|
const pathParams = matchSegments(entry.segments, requestSegments);
|
|
873
919
|
if (pathParams) {
|
|
@@ -881,7 +927,7 @@ function matchRoute(routeMap, httpMethod, pathname) {
|
|
|
881
927
|
return null;
|
|
882
928
|
}
|
|
883
929
|
function allowedMethods(routeMap, pathname) {
|
|
884
|
-
const requestSegments = pathname
|
|
930
|
+
const requestSegments = routeSegments(pathname);
|
|
885
931
|
const methods = [];
|
|
886
932
|
for (const [method, entries] of routeMap) {
|
|
887
933
|
for (const entry of entries) {
|
|
@@ -939,16 +985,16 @@ function matchRawRoute(rawRoutes, httpMethod, pathname) {
|
|
|
939
985
|
if (route.method !== "ALL" && route.method !== httpMethod)
|
|
940
986
|
continue;
|
|
941
987
|
if (parseTrailingWildcard(route.path)) {
|
|
942
|
-
const routeSegs = route.path
|
|
943
|
-
const pathSegs = pathname
|
|
988
|
+
const routeSegs = routeSegments(route.path);
|
|
989
|
+
const pathSegs = routeSegments(pathname);
|
|
944
990
|
const params = matchSegments(routeSegs, pathSegs);
|
|
945
991
|
if (params)
|
|
946
992
|
return { route, params };
|
|
947
993
|
continue;
|
|
948
994
|
}
|
|
949
995
|
if (route.path.includes("/:")) {
|
|
950
|
-
const routeSegs = route.path
|
|
951
|
-
const pathSegs = pathname
|
|
996
|
+
const routeSegs = routeSegments(route.path);
|
|
997
|
+
const pathSegs = routeSegments(pathname);
|
|
952
998
|
const params = matchSegments(routeSegs, pathSegs);
|
|
953
999
|
if (params)
|
|
954
1000
|
return { route, params };
|
|
@@ -960,8 +1006,37 @@ function matchRawRoute(rawRoutes, httpMethod, pathname) {
|
|
|
960
1006
|
return null;
|
|
961
1007
|
}
|
|
962
1008
|
function validateRawRoutes(rawRoutes) {
|
|
963
|
-
|
|
964
|
-
|
|
1009
|
+
const routes = rawRoutes ?? [];
|
|
1010
|
+
const shapes = routes.map((route) => rawRouteShape(route.path));
|
|
1011
|
+
const conflicts = [];
|
|
1012
|
+
for (const [laterIndex, later] of routes.entries()) {
|
|
1013
|
+
const laterShape = shapes[laterIndex];
|
|
1014
|
+
if (!laterShape)
|
|
1015
|
+
continue;
|
|
1016
|
+
for (let earlierIndex = 0;earlierIndex < laterIndex; earlierIndex++) {
|
|
1017
|
+
const earlier = routes[earlierIndex];
|
|
1018
|
+
const earlierShape = shapes[earlierIndex];
|
|
1019
|
+
if (!earlier || !earlierShape)
|
|
1020
|
+
continue;
|
|
1021
|
+
if (earlier.method === later.method && earlier.path === later.path) {
|
|
1022
|
+
conflicts.push(`${later.method} ${later.path} duplicates earlier ${earlier.method} ${earlier.path}`);
|
|
1023
|
+
continue;
|
|
1024
|
+
}
|
|
1025
|
+
if (earlier.method === later.method && earlierShape.signature === laterShape.signature) {
|
|
1026
|
+
conflicts.push(`${later.method} ${later.path} has the same parameter shape as earlier ${earlier.method} ${earlier.path}`);
|
|
1027
|
+
continue;
|
|
1028
|
+
}
|
|
1029
|
+
const methodCovered = earlier.method === "ALL" || earlier.method === later.method;
|
|
1030
|
+
if (methodCovered && routeShapeCovers(earlierShape, laterShape)) {
|
|
1031
|
+
conflicts.push(`${later.method} ${later.path} is unreachable because earlier ${earlier.method} ${earlier.path} matches every request it could receive`);
|
|
1032
|
+
}
|
|
1033
|
+
}
|
|
1034
|
+
}
|
|
1035
|
+
if (conflicts.length > 0) {
|
|
1036
|
+
throw new Error(`[stitchkit] conflicting raw routes:
|
|
1037
|
+
- ${conflicts.join(`
|
|
1038
|
+
- `)}`);
|
|
1039
|
+
}
|
|
965
1040
|
}
|
|
966
1041
|
|
|
967
1042
|
// src/server/create.ts
|
|
@@ -1322,6 +1397,193 @@ function json(data, status, cors, req) {
|
|
|
1322
1397
|
return Response.json(data, { status, headers: corsHeaders2(cors, req) });
|
|
1323
1398
|
}
|
|
1324
1399
|
|
|
1400
|
+
// src/server/shutdown.ts
|
|
1401
|
+
import { z } from "zod";
|
|
1402
|
+
var ShutdownStateSchema = z.enum([
|
|
1403
|
+
"running",
|
|
1404
|
+
"draining-http",
|
|
1405
|
+
"closing-realtime",
|
|
1406
|
+
"stopping-runtime",
|
|
1407
|
+
"clean",
|
|
1408
|
+
"forced"
|
|
1409
|
+
]);
|
|
1410
|
+
var ShutdownOptionsSchema = z.object({
|
|
1411
|
+
gracePeriodMs: z.number().int().nonnegative().default(30000),
|
|
1412
|
+
retryAfterSeconds: z.number().int().nonnegative().default(5),
|
|
1413
|
+
signal: z.custom((value) => typeof value === "object" && value !== null && ("aborted" in value) && ("addEventListener" in value), "Expected an AbortSignal").optional()
|
|
1414
|
+
});
|
|
1415
|
+
var ShutdownStatusSchema = z.object({
|
|
1416
|
+
state: ShutdownStateSchema,
|
|
1417
|
+
acceptedRequests: z.number().int().nonnegative(),
|
|
1418
|
+
completedRequests: z.number().int().nonnegative(),
|
|
1419
|
+
pendingRequests: z.number().int().nonnegative(),
|
|
1420
|
+
pendingWebSockets: z.number().int().nonnegative()
|
|
1421
|
+
});
|
|
1422
|
+
var ShutdownResultSchema = z.object({
|
|
1423
|
+
outcome: z.enum(["clean", "forced"]),
|
|
1424
|
+
reason: z.enum(["deadline", "signal"]).optional(),
|
|
1425
|
+
acceptedRequests: z.number().int().nonnegative(),
|
|
1426
|
+
completedRequests: z.number().int().nonnegative(),
|
|
1427
|
+
pendingRequests: z.number().int().nonnegative(),
|
|
1428
|
+
pendingWebSockets: z.number().int().nonnegative(),
|
|
1429
|
+
pendingRequestsAtForce: z.number().int().nonnegative(),
|
|
1430
|
+
pendingWebSocketsAtForce: z.number().int().nonnegative(),
|
|
1431
|
+
abortedRequests: z.number().int().nonnegative(),
|
|
1432
|
+
forcedWebSockets: z.number().int().nonnegative(),
|
|
1433
|
+
durationMs: z.number().nonnegative()
|
|
1434
|
+
});
|
|
1435
|
+
function rejectedResponse(retryAfterSeconds) {
|
|
1436
|
+
return Response.json({ error: { code: "SERVER_SHUTTING_DOWN", message: "Server is shutting down" } }, {
|
|
1437
|
+
status: 503,
|
|
1438
|
+
headers: {
|
|
1439
|
+
Connection: "close",
|
|
1440
|
+
"Retry-After": String(retryAfterSeconds)
|
|
1441
|
+
}
|
|
1442
|
+
});
|
|
1443
|
+
}
|
|
1444
|
+
function waitForZero(read, signal) {
|
|
1445
|
+
if (read() === 0)
|
|
1446
|
+
return Promise.resolve();
|
|
1447
|
+
return new Promise((resolve) => {
|
|
1448
|
+
const check = () => {
|
|
1449
|
+
if (read() === 0 || signal.aborted) {
|
|
1450
|
+
clearInterval(timer);
|
|
1451
|
+
signal.removeEventListener("abort", check);
|
|
1452
|
+
resolve();
|
|
1453
|
+
}
|
|
1454
|
+
};
|
|
1455
|
+
const timer = setInterval(check, 5);
|
|
1456
|
+
signal.addEventListener("abort", check, { once: true });
|
|
1457
|
+
});
|
|
1458
|
+
}
|
|
1459
|
+
function untilAbort(signal) {
|
|
1460
|
+
if (signal.aborted)
|
|
1461
|
+
return Promise.resolve();
|
|
1462
|
+
return new Promise((resolve) => signal.addEventListener("abort", () => resolve(), { once: true }));
|
|
1463
|
+
}
|
|
1464
|
+
function createServerLifecycle(getAdapter) {
|
|
1465
|
+
let state = "running";
|
|
1466
|
+
let acceptedRequests = 0;
|
|
1467
|
+
let completedRequests = 0;
|
|
1468
|
+
let pendingApplicationRequests = 0;
|
|
1469
|
+
let retryAfterSeconds = 5;
|
|
1470
|
+
let shutdownPromise;
|
|
1471
|
+
const status = () => {
|
|
1472
|
+
const adapter = getAdapter();
|
|
1473
|
+
return ShutdownStatusSchema.parse({
|
|
1474
|
+
state,
|
|
1475
|
+
acceptedRequests,
|
|
1476
|
+
completedRequests,
|
|
1477
|
+
pendingRequests: adapter.pendingRequests(),
|
|
1478
|
+
pendingWebSockets: adapter.pendingWebSockets()
|
|
1479
|
+
});
|
|
1480
|
+
};
|
|
1481
|
+
const wrapFetch = (handler) => {
|
|
1482
|
+
return async (request, server) => {
|
|
1483
|
+
if (state !== "running")
|
|
1484
|
+
return rejectedResponse(retryAfterSeconds);
|
|
1485
|
+
acceptedRequests += 1;
|
|
1486
|
+
pendingApplicationRequests += 1;
|
|
1487
|
+
try {
|
|
1488
|
+
return await handler(request, server);
|
|
1489
|
+
} finally {
|
|
1490
|
+
pendingApplicationRequests -= 1;
|
|
1491
|
+
completedRequests += 1;
|
|
1492
|
+
}
|
|
1493
|
+
};
|
|
1494
|
+
};
|
|
1495
|
+
const shutdown = (options) => {
|
|
1496
|
+
if (shutdownPromise)
|
|
1497
|
+
return shutdownPromise;
|
|
1498
|
+
const parsed = ShutdownOptionsSchema.parse(options ?? {});
|
|
1499
|
+
retryAfterSeconds = parsed.retryAfterSeconds;
|
|
1500
|
+
const startedAt = performance.now();
|
|
1501
|
+
const adapter = getAdapter();
|
|
1502
|
+
state = "draining-http";
|
|
1503
|
+
adapter.beginShutdown(retryAfterSeconds);
|
|
1504
|
+
shutdownPromise = new Promise((resolve, reject) => {
|
|
1505
|
+
const phaseAbort = new AbortController;
|
|
1506
|
+
let forcedReason;
|
|
1507
|
+
const force = (reason) => {
|
|
1508
|
+
if (forcedReason)
|
|
1509
|
+
return;
|
|
1510
|
+
forcedReason = reason;
|
|
1511
|
+
phaseAbort.abort();
|
|
1512
|
+
};
|
|
1513
|
+
const timer = setTimeout(() => force("deadline"), parsed.gracePeriodMs);
|
|
1514
|
+
const onExternalAbort = () => force("signal");
|
|
1515
|
+
parsed.signal?.addEventListener("abort", onExternalAbort, { once: true });
|
|
1516
|
+
if (parsed.signal?.aborted)
|
|
1517
|
+
force("signal");
|
|
1518
|
+
const cleanup = () => {
|
|
1519
|
+
clearTimeout(timer);
|
|
1520
|
+
parsed.signal?.removeEventListener("abort", onExternalAbort);
|
|
1521
|
+
};
|
|
1522
|
+
(async () => {
|
|
1523
|
+
await waitForZero(() => pendingApplicationRequests, phaseAbort.signal);
|
|
1524
|
+
if (!forcedReason) {
|
|
1525
|
+
state = "closing-realtime";
|
|
1526
|
+
const closeRealtime = adapter.closeRealtime();
|
|
1527
|
+
await Promise.race([
|
|
1528
|
+
closeRealtime.catch((error) => {
|
|
1529
|
+
if (!forcedReason)
|
|
1530
|
+
throw error;
|
|
1531
|
+
}),
|
|
1532
|
+
untilAbort(phaseAbort.signal)
|
|
1533
|
+
]);
|
|
1534
|
+
}
|
|
1535
|
+
if (!forcedReason) {
|
|
1536
|
+
state = "stopping-runtime";
|
|
1537
|
+
const stopGracefully = adapter.stopGracefully();
|
|
1538
|
+
await Promise.race([
|
|
1539
|
+
stopGracefully.catch((error) => {
|
|
1540
|
+
if (!forcedReason)
|
|
1541
|
+
throw error;
|
|
1542
|
+
}),
|
|
1543
|
+
untilAbort(phaseAbort.signal)
|
|
1544
|
+
]);
|
|
1545
|
+
}
|
|
1546
|
+
let pendingRequestsAtForce = 0;
|
|
1547
|
+
let pendingWebSocketsAtForce = 0;
|
|
1548
|
+
if (forcedReason) {
|
|
1549
|
+
state = "stopping-runtime";
|
|
1550
|
+
pendingRequestsAtForce = adapter.pendingRequests();
|
|
1551
|
+
pendingWebSocketsAtForce = adapter.pendingWebSockets();
|
|
1552
|
+
await adapter.forceStop();
|
|
1553
|
+
state = "forced";
|
|
1554
|
+
} else {
|
|
1555
|
+
state = "clean";
|
|
1556
|
+
}
|
|
1557
|
+
cleanup();
|
|
1558
|
+
resolve(ShutdownResultSchema.parse({
|
|
1559
|
+
outcome: forcedReason ? "forced" : "clean",
|
|
1560
|
+
...forcedReason && { reason: forcedReason },
|
|
1561
|
+
acceptedRequests,
|
|
1562
|
+
completedRequests,
|
|
1563
|
+
pendingRequests: adapter.pendingRequests(),
|
|
1564
|
+
pendingWebSockets: adapter.pendingWebSockets(),
|
|
1565
|
+
pendingRequestsAtForce,
|
|
1566
|
+
pendingWebSocketsAtForce,
|
|
1567
|
+
abortedRequests: pendingRequestsAtForce,
|
|
1568
|
+
forcedWebSockets: pendingWebSocketsAtForce,
|
|
1569
|
+
durationMs: performance.now() - startedAt
|
|
1570
|
+
}));
|
|
1571
|
+
})().catch((error) => {
|
|
1572
|
+
cleanup();
|
|
1573
|
+
reject(error);
|
|
1574
|
+
});
|
|
1575
|
+
});
|
|
1576
|
+
return shutdownPromise;
|
|
1577
|
+
};
|
|
1578
|
+
return {
|
|
1579
|
+
wrapFetch,
|
|
1580
|
+
get status() {
|
|
1581
|
+
return status();
|
|
1582
|
+
},
|
|
1583
|
+
shutdown
|
|
1584
|
+
};
|
|
1585
|
+
}
|
|
1586
|
+
|
|
1325
1587
|
// src/server/implement.ts
|
|
1326
1588
|
function isStreamingImplementation(value) {
|
|
1327
1589
|
return typeof value === "object" && value !== null && "kind" in value && value.kind === "stitchkit.multipart.stream";
|
|
@@ -1345,11 +1607,11 @@ function defineMultipartStream(endpoint, config) {
|
|
|
1345
1607
|
};
|
|
1346
1608
|
}
|
|
1347
1609
|
var HTTP_ONLY = Object.freeze(["HTTP"]);
|
|
1348
|
-
function
|
|
1610
|
+
function bindContract(contract, handlers) {
|
|
1349
1611
|
const methods = {};
|
|
1350
1612
|
const groupScope = contract.meta.scope ?? "public";
|
|
1351
1613
|
for (const [key, endpoint] of typedEntries(contract.endpoints)) {
|
|
1352
|
-
const typedHandler = handlers[key];
|
|
1614
|
+
const typedHandler = handlers[String(key)];
|
|
1353
1615
|
const isStreaming = endpoint.multipart?.delivery === "stream";
|
|
1354
1616
|
if (!isStreaming && typeof typedHandler !== "function") {
|
|
1355
1617
|
throw new Error(`[stitchkit] implement: missing handler for "${contract.meta.prefix}.${String(key)}"`);
|
|
@@ -1398,14 +1660,61 @@ function implement(contract, handlers) {
|
|
|
1398
1660
|
methods
|
|
1399
1661
|
};
|
|
1400
1662
|
}
|
|
1663
|
+
function implement(contract, handlers) {
|
|
1664
|
+
return bindContract(contract, handlers);
|
|
1665
|
+
}
|
|
1401
1666
|
function createImplement() {
|
|
1402
1667
|
return (contract, handlers) => implement(contract, handlers);
|
|
1403
1668
|
}
|
|
1669
|
+
function isImplementationContract(value) {
|
|
1670
|
+
return isRecord(value) && isRecord(value.meta) && typeof value.meta.prefix === "string" && isRecord(value.endpoints);
|
|
1671
|
+
}
|
|
1672
|
+
function bindRegistry(contracts, handlers) {
|
|
1673
|
+
const contractKeys = Object.keys(contracts);
|
|
1674
|
+
const handlerKeys = Object.keys(handlers);
|
|
1675
|
+
const missing = contractKeys.filter((key) => !Object.hasOwn(handlers, key));
|
|
1676
|
+
const extra = handlerKeys.filter((key) => !Object.hasOwn(contracts, key));
|
|
1677
|
+
if (missing.length > 0 || extra.length > 0) {
|
|
1678
|
+
throw new Error(`[stitchkit] implementRegistry: registry mismatch (missing: ${missing.join(", ") || "none"}; extra: ${extra.join(", ") || "none"})`);
|
|
1679
|
+
}
|
|
1680
|
+
const prefixes = new Map;
|
|
1681
|
+
const services = [];
|
|
1682
|
+
for (const [key, candidate] of Object.entries(contracts)) {
|
|
1683
|
+
if (!isImplementationContract(candidate)) {
|
|
1684
|
+
throw new TypeError(`[stitchkit] implementRegistry: registry entry "${key}" must be one contract; composed arrays and namespaces are not supported`);
|
|
1685
|
+
}
|
|
1686
|
+
const contract = candidate;
|
|
1687
|
+
const previousKey = prefixes.get(contract.meta.prefix);
|
|
1688
|
+
if (previousKey !== undefined) {
|
|
1689
|
+
throw new Error(`[stitchkit] implementRegistry: duplicate contract prefix "${contract.meta.prefix}" at "${previousKey}" and "${key}"`);
|
|
1690
|
+
}
|
|
1691
|
+
prefixes.set(contract.meta.prefix, key);
|
|
1692
|
+
const entryHandlers = handlers[key];
|
|
1693
|
+
if (!isRecord(entryHandlers)) {
|
|
1694
|
+
throw new TypeError(`[stitchkit] implementRegistry: handlers for "${key}" must be an object`);
|
|
1695
|
+
}
|
|
1696
|
+
const endpointKeys = Object.keys(contract.endpoints);
|
|
1697
|
+
const handlerEntryKeys = Object.keys(entryHandlers);
|
|
1698
|
+
const missingEndpoints = endpointKeys.filter((endpointKey) => !Object.hasOwn(entryHandlers, endpointKey));
|
|
1699
|
+
const extraEndpoints = handlerEntryKeys.filter((endpointKey) => !Object.hasOwn(contract.endpoints, endpointKey));
|
|
1700
|
+
if (missingEndpoints.length > 0 || extraEndpoints.length > 0) {
|
|
1701
|
+
throw new Error(`[stitchkit] implementRegistry: handlers for "${key}" mismatch (missing: ${missingEndpoints.join(", ") || "none"}; extra: ${extraEndpoints.join(", ") || "none"})`);
|
|
1702
|
+
}
|
|
1703
|
+
services.push(bindContract(contract, entryHandlers));
|
|
1704
|
+
}
|
|
1705
|
+
return services;
|
|
1706
|
+
}
|
|
1707
|
+
function implementRegistry(contracts, handlers) {
|
|
1708
|
+
return bindRegistry(contracts, handlers);
|
|
1709
|
+
}
|
|
1710
|
+
function createImplementRegistry() {
|
|
1711
|
+
return (contracts, handlers) => bindRegistry(contracts, handlers);
|
|
1712
|
+
}
|
|
1404
1713
|
|
|
1405
1714
|
// src/realtime/rejection.ts
|
|
1406
|
-
import { z } from "zod";
|
|
1715
|
+
import { z as z2 } from "zod";
|
|
1407
1716
|
function realtimeContractViolation(options) {
|
|
1408
|
-
const issues = options.cause instanceof
|
|
1717
|
+
const issues = options.cause instanceof z2.ZodError ? zodIssues(options.cause) : undefined;
|
|
1409
1718
|
const reason = options.reason.replaceAll("-", " ");
|
|
1410
1719
|
const error = new AppError("REALTIME_CONTRACT_VIOLATION", `Realtime event "${options.event}" (${options.direction}, ${options.phase}): ${reason}`, 500, {
|
|
1411
1720
|
event: options.event,
|
|
@@ -1710,6 +2019,34 @@ async function createSocketIOServer(config) {
|
|
|
1710
2019
|
credentials: config.cors.credentials ?? true,
|
|
1711
2020
|
methods: ["GET", "POST"]
|
|
1712
2021
|
};
|
|
2022
|
+
let accepting = true;
|
|
2023
|
+
let attached = false;
|
|
2024
|
+
let closePromise;
|
|
2025
|
+
const consumerAllowRequest = config.allowRequest;
|
|
2026
|
+
const checkRequest = async (request) => {
|
|
2027
|
+
if (consumerAllowRequest && !await consumerAllowRequest(request)) {
|
|
2028
|
+
throw new Error("Request rejected by the configured Socket.IO policy");
|
|
2029
|
+
}
|
|
2030
|
+
if (!accepting)
|
|
2031
|
+
throw new Error("Server is shutting down");
|
|
2032
|
+
};
|
|
2033
|
+
const allowRequest = (request, done) => {
|
|
2034
|
+
const headers = new Headers;
|
|
2035
|
+
for (const [name, value] of Object.entries(request.headers)) {
|
|
2036
|
+
if (Array.isArray(value)) {
|
|
2037
|
+
for (const item of value)
|
|
2038
|
+
headers.append(name, item);
|
|
2039
|
+
} else if (value !== undefined) {
|
|
2040
|
+
headers.set(name, value);
|
|
2041
|
+
}
|
|
2042
|
+
}
|
|
2043
|
+
const host = headers.get("host") ?? "localhost";
|
|
2044
|
+
const webRequest = new Request(new URL(request.url ?? "/", `http://${host}`), {
|
|
2045
|
+
method: request.method,
|
|
2046
|
+
headers
|
|
2047
|
+
});
|
|
2048
|
+
checkRequest(webRequest).then(() => done(null, true), (error) => done(error instanceof Error ? error.message : "Request rejected", false));
|
|
2049
|
+
};
|
|
1713
2050
|
const { Server } = await importPeer(() => import("socket.io"), "socket.io");
|
|
1714
2051
|
const io = new Server({
|
|
1715
2052
|
...config.serverOptions,
|
|
@@ -1717,15 +2054,31 @@ async function createSocketIOServer(config) {
|
|
|
1717
2054
|
cors,
|
|
1718
2055
|
transports,
|
|
1719
2056
|
pingTimeout,
|
|
1720
|
-
pingInterval
|
|
2057
|
+
pingInterval,
|
|
2058
|
+
allowRequest
|
|
1721
2059
|
});
|
|
2060
|
+
const lifecycle = {
|
|
2061
|
+
beginShutdown() {
|
|
2062
|
+
accepting = false;
|
|
2063
|
+
},
|
|
2064
|
+
close() {
|
|
2065
|
+
if (closePromise)
|
|
2066
|
+
return closePromise;
|
|
2067
|
+
closePromise = onBun || attached ? io.close() : Promise.resolve();
|
|
2068
|
+
return closePromise;
|
|
2069
|
+
},
|
|
2070
|
+
connections() {
|
|
2071
|
+
return io.engine?.clientsCount ?? 0;
|
|
2072
|
+
}
|
|
2073
|
+
};
|
|
1722
2074
|
if (onBun) {
|
|
1723
2075
|
const { Server: Engine } = await importPeer(() => import("@socket.io/bun-engine"), "@socket.io/bun-engine");
|
|
1724
2076
|
const engineOpts = {
|
|
1725
2077
|
path,
|
|
1726
2078
|
cors,
|
|
1727
2079
|
pingTimeout,
|
|
1728
|
-
pingInterval
|
|
2080
|
+
pingInterval,
|
|
2081
|
+
allowRequest: checkRequest
|
|
1729
2082
|
};
|
|
1730
2083
|
if (config.serverOptions?.maxHttpBufferSize !== undefined) {
|
|
1731
2084
|
engineOpts.maxHttpBufferSize = config.serverOptions.maxHttpBufferSize;
|
|
@@ -1746,11 +2099,14 @@ async function createSocketIOServer(config) {
|
|
|
1746
2099
|
return engine.handleRequest(req, ctx.server);
|
|
1747
2100
|
}
|
|
1748
2101
|
};
|
|
1749
|
-
return { io, websocket, route, attach: noop };
|
|
2102
|
+
return { io, websocket, route, attach: noop, ...lifecycle };
|
|
1750
2103
|
}
|
|
1751
2104
|
return {
|
|
1752
2105
|
io,
|
|
1753
|
-
attach: (server) =>
|
|
2106
|
+
attach: (server) => {
|
|
2107
|
+
io.attach(server);
|
|
2108
|
+
attached = true;
|
|
2109
|
+
},
|
|
1754
2110
|
websocket: { open: noop, message: noop, close: noop, maxPayloadLength: 0 },
|
|
1755
2111
|
route: {
|
|
1756
2112
|
method: "ALL",
|
|
@@ -1758,7 +2114,8 @@ async function createSocketIOServer(config) {
|
|
|
1758
2114
|
handler: () => {
|
|
1759
2115
|
throw new Error("[stitchkit] On Node, Socket.IO attaches via serveNode({ socket }) — this route is not mounted.");
|
|
1760
2116
|
}
|
|
1761
|
-
}
|
|
2117
|
+
},
|
|
2118
|
+
...lifecycle
|
|
1762
2119
|
};
|
|
1763
2120
|
}
|
|
1764
2121
|
function socketIoLane(websocket) {
|
|
@@ -1768,4 +2125,4 @@ function socketIoLane(websocket) {
|
|
|
1768
2125
|
});
|
|
1769
2126
|
}
|
|
1770
2127
|
|
|
1771
|
-
export { parseMultipart, createHandler, defineMultipartStream, implement, createImplement, bindRealtimeServer, webSocketLane, composeWebSocketHandlers, createSocketIOServer, socketIoLane };
|
|
2128
|
+
export { parseMultipart, createHandler, ShutdownStateSchema, ShutdownOptionsSchema, ShutdownStatusSchema, ShutdownResultSchema, createServerLifecycle, defineMultipartStream, implement, createImplement, implementRegistry, createImplementRegistry, bindRealtimeServer, webSocketLane, composeWebSocketHandlers, createSocketIOServer, socketIoLane };
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import { createRequire } from "node:module";
|
|
2
|
+
var __require = /* @__PURE__ */ createRequire(import.meta.url);
|
|
3
|
+
|
|
4
|
+
// src/observability/trace.ts
|
|
5
|
+
var TRACEPARENT_RE = /^([0-9a-f]{2})-([0-9a-f]{32})-([0-9a-f]{16})-([0-9a-f]{2})(.*)$/i;
|
|
6
|
+
function randomHex(bytes) {
|
|
7
|
+
const arr = new Uint8Array(bytes);
|
|
8
|
+
crypto.getRandomValues(arr);
|
|
9
|
+
let hex = "";
|
|
10
|
+
for (const byte of arr)
|
|
11
|
+
hex += byte.toString(16).padStart(2, "0");
|
|
12
|
+
return hex;
|
|
13
|
+
}
|
|
14
|
+
function createTraceContext() {
|
|
15
|
+
return { traceId: randomHex(16), spanId: randomHex(8) };
|
|
16
|
+
}
|
|
17
|
+
function parseTraceparent(header) {
|
|
18
|
+
if (!header)
|
|
19
|
+
return null;
|
|
20
|
+
const match = TRACEPARENT_RE.exec(header.trim());
|
|
21
|
+
if (!match?.[1] || !match[2] || !match[3] || !match[4] || match[5] === undefined)
|
|
22
|
+
return null;
|
|
23
|
+
const version = match[1].toLowerCase();
|
|
24
|
+
if (version === "ff")
|
|
25
|
+
return null;
|
|
26
|
+
const suffix = match[5];
|
|
27
|
+
if (version === "00" ? suffix !== "" : suffix !== "" && !/^(?:-[0-9a-f]{2,})+$/i.test(suffix)) {
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
const traceId = match[2].toLowerCase();
|
|
31
|
+
const parentSpanId = match[3].toLowerCase();
|
|
32
|
+
const traceFlags = match[4].toLowerCase();
|
|
33
|
+
if (/^0+$/.test(traceId) || /^0+$/.test(parentSpanId))
|
|
34
|
+
return null;
|
|
35
|
+
return { traceId, spanId: randomHex(8), parentSpanId, traceFlags };
|
|
36
|
+
}
|
|
37
|
+
function formatTraceparent(ctx) {
|
|
38
|
+
return `00-${ctx.traceId}-${ctx.spanId}-${ctx.traceFlags ?? "01"}`;
|
|
39
|
+
}
|
|
40
|
+
function resolveTraceContext(req) {
|
|
41
|
+
return parseTraceparent(req.headers.get("traceparent")) ?? createTraceContext();
|
|
42
|
+
}
|
|
43
|
+
function childSpan(parent) {
|
|
44
|
+
return {
|
|
45
|
+
traceId: parent.traceId,
|
|
46
|
+
spanId: randomHex(8),
|
|
47
|
+
parentSpanId: parent.spanId,
|
|
48
|
+
...parent.tracestate !== undefined && { tracestate: parent.tracestate },
|
|
49
|
+
...parent.baggage !== undefined && { baggage: parent.baggage },
|
|
50
|
+
...parent.traceFlags !== undefined && { traceFlags: parent.traceFlags }
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
var encoder = new TextEncoder;
|
|
54
|
+
function boundedPropagationValue(value, maxBytes, maxMembers) {
|
|
55
|
+
if (typeof value !== "string")
|
|
56
|
+
return;
|
|
57
|
+
const trimmed = value.trim();
|
|
58
|
+
if (!trimmed || encoder.encode(trimmed).byteLength > maxBytes)
|
|
59
|
+
return;
|
|
60
|
+
for (const character of trimmed) {
|
|
61
|
+
const code = character.charCodeAt(0);
|
|
62
|
+
if (code <= 31 || code === 127)
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
const members = trimmed.split(",");
|
|
66
|
+
if (members.length > maxMembers || members.some((member) => !member.trim())) {
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
return trimmed;
|
|
70
|
+
}
|
|
71
|
+
function resolvePropagationContext(metadata, fallback) {
|
|
72
|
+
const traceparent = metadata?.traceparent;
|
|
73
|
+
const parsed = typeof traceparent === "string" ? parseTraceparent(traceparent) : null;
|
|
74
|
+
const trace = traceparent === undefined ? fallback ?? createTraceContext() : parsed ?? createTraceContext();
|
|
75
|
+
const tracestate = parsed ? boundedPropagationValue(metadata?.tracestate, 512, 32) : undefined;
|
|
76
|
+
const baggage = boundedPropagationValue(metadata?.baggage, 8192, 180);
|
|
77
|
+
return {
|
|
78
|
+
...trace,
|
|
79
|
+
...tracestate !== undefined && { tracestate },
|
|
80
|
+
...baggage !== undefined && { baggage }
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// src/internal/route-pattern.ts
|
|
85
|
+
var PARAM_IDENTIFIER = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
86
|
+
function parseTrailingWildcard(path) {
|
|
87
|
+
const segments = path.split("/").filter(Boolean);
|
|
88
|
+
const paramNames = new Set;
|
|
89
|
+
let wildcard = null;
|
|
90
|
+
for (const [segmentIndex, segment] of segments.entries()) {
|
|
91
|
+
if (segment.startsWith(":")) {
|
|
92
|
+
const name2 = segment.slice(1);
|
|
93
|
+
if (!PARAM_IDENTIFIER.test(name2)) {
|
|
94
|
+
throw new Error(`Invalid route parameter name "${name2}" in path "${path}"`);
|
|
95
|
+
}
|
|
96
|
+
if (paramNames.has(name2)) {
|
|
97
|
+
throw new Error(`Duplicate route parameter name "${name2}" in path "${path}"`);
|
|
98
|
+
}
|
|
99
|
+
paramNames.add(name2);
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
if (!segment.startsWith("*")) {
|
|
103
|
+
if (segment.includes("*")) {
|
|
104
|
+
throw new Error(`Wildcard must occupy its own segment in path "${path}"`);
|
|
105
|
+
}
|
|
106
|
+
continue;
|
|
107
|
+
}
|
|
108
|
+
const name = segment.slice(1);
|
|
109
|
+
if (!PARAM_IDENTIFIER.test(name)) {
|
|
110
|
+
throw new Error(`Trailing wildcard in path "${path}" must be named, for example "/*filePath"`);
|
|
111
|
+
}
|
|
112
|
+
if (segmentIndex !== segments.length - 1) {
|
|
113
|
+
throw new Error(`Wildcard "*${name}" must be the final segment in path "${path}"`);
|
|
114
|
+
}
|
|
115
|
+
if (wildcard) {
|
|
116
|
+
throw new Error(`Path "${path}" contains more than one wildcard`);
|
|
117
|
+
}
|
|
118
|
+
if (paramNames.has(name)) {
|
|
119
|
+
throw new Error(`Duplicate route parameter name "${name}" in path "${path}"`);
|
|
120
|
+
}
|
|
121
|
+
wildcard = { name, segmentIndex };
|
|
122
|
+
}
|
|
123
|
+
return wildcard;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// src/internal/typed.ts
|
|
127
|
+
function typedEntries(value) {
|
|
128
|
+
return Object.entries(value);
|
|
129
|
+
}
|
|
130
|
+
function isRecord(value) {
|
|
131
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
132
|
+
}
|
|
133
|
+
function callRuntimeHandler(handler, context) {
|
|
134
|
+
if (typeof handler !== "function") {
|
|
135
|
+
throw new TypeError("Runtime handler must be a function");
|
|
136
|
+
}
|
|
137
|
+
return Reflect.apply(handler, undefined, [context]);
|
|
138
|
+
}
|
|
139
|
+
function mapObject(source, mapper) {
|
|
140
|
+
const result = {};
|
|
141
|
+
for (const [key, value] of typedEntries(source)) {
|
|
142
|
+
const mapped = mapper(key, value);
|
|
143
|
+
if (mapped !== undefined)
|
|
144
|
+
result[key] = mapped;
|
|
145
|
+
}
|
|
146
|
+
return result;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export { __require, parseTrailingWildcard, typedEntries, isRecord, callRuntimeHandler, mapObject, createTraceContext, parseTraceparent, formatTraceparent, resolveTraceContext, childSpan, resolvePropagationContext };
|