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.
Files changed (54) hide show
  1. package/README.md +1 -2
  2. package/dist/browser/client.d.ts +3 -0
  3. package/dist/browser/client.d.ts.map +1 -1
  4. package/dist/browser/http.d.ts +7 -4
  5. package/dist/browser/http.d.ts.map +1 -1
  6. package/dist/cli.js +3 -2
  7. package/dist/{index-nrytvb30.js → index-03j2t778.js} +5 -8
  8. package/dist/{index-kp8xamqp.js → index-0hj37z43.js} +4 -2
  9. package/dist/index-48ffdxgk.js +6 -0
  10. package/dist/{index-44xysy8r.js → index-fjfzsq6y.js} +383 -26
  11. package/dist/index-h05ygjqx.js +149 -0
  12. package/dist/{index-ee621cmy.js → index-jewp9r0a.js} +5 -139
  13. package/dist/index-p9d4cxt5.js +436 -0
  14. package/dist/{index-8ekq6res.js → index-v58mwa19.js} +4 -2
  15. package/dist/index.d.ts +1 -1
  16. package/dist/index.d.ts.map +1 -1
  17. package/dist/index.js +47 -3
  18. package/dist/node.d.ts +4 -3
  19. package/dist/node.d.ts.map +1 -1
  20. package/dist/node.js +109 -14
  21. package/dist/observability/audit.d.ts +4 -1
  22. package/dist/observability/audit.d.ts.map +1 -1
  23. package/dist/observability/index.d.ts +1 -0
  24. package/dist/observability/index.d.ts.map +1 -1
  25. package/dist/observability/index.js +150 -16
  26. package/dist/observability/status.d.ts +105 -0
  27. package/dist/observability/status.d.ts.map +1 -0
  28. package/dist/server/bun.d.ts +7 -8
  29. package/dist/server/bun.d.ts.map +1 -1
  30. package/dist/server/implement.d.ts +16 -6
  31. package/dist/server/implement.d.ts.map +1 -1
  32. package/dist/server/index.d.ts +4 -3
  33. package/dist/server/index.d.ts.map +1 -1
  34. package/dist/server/index.js +132 -11
  35. package/dist/server/node.d.ts +12 -13
  36. package/dist/server/node.d.ts.map +1 -1
  37. package/dist/server/router.d.ts +4 -1
  38. package/dist/server/router.d.ts.map +1 -1
  39. package/dist/server/shutdown.d.ts +76 -0
  40. package/dist/server/shutdown.d.ts.map +1 -0
  41. package/dist/server/socket-io-config.d.ts +5 -1
  42. package/dist/server/socket-io-config.d.ts.map +1 -1
  43. package/dist/server/socket-io-node.d.ts +4 -1
  44. package/dist/server/socket-io-node.d.ts.map +1 -1
  45. package/dist/server/socket-io.d.ts +12 -4
  46. package/dist/server/socket-io.d.ts.map +1 -1
  47. package/dist/testing.d.ts +34 -0
  48. package/dist/testing.d.ts.map +1 -0
  49. package/dist/testing.js +36 -0
  50. package/dist/tools/mcp.d.ts +1 -0
  51. package/dist/tools/mcp.d.ts.map +1 -1
  52. package/dist/tools.js +21 -428
  53. package/llms-full.txt +290 -43
  54. 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-ee621cmy.js";
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.split("/").filter(Boolean);
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.split("/").filter(Boolean);
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.split("/").filter(Boolean);
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.split("/").filter(Boolean);
943
- const pathSegs = pathname.split("/").filter(Boolean);
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.split("/").filter(Boolean);
951
- const pathSegs = pathname.split("/").filter(Boolean);
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
- for (const route of rawRoutes ?? [])
964
- parseTrailingWildcard(route.path);
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 implement(contract, handlers) {
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 z.ZodError ? zodIssues(options.cause) : undefined;
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) => io.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 };