stitchkit 0.118.0 → 0.118.1

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/CHANGELOG.md CHANGED
@@ -15,6 +15,18 @@ additive**; the first breaking change landed in 0.10.0. Grep the file for
15
15
 
16
16
  ## [Unreleased]
17
17
 
18
+ ## [0.118.1] - 2026-11-03
19
+
20
+ ### Added
21
+
22
+ - `stitchkit/server` — `staticRoute(prefix, dir, { cacheControl })` sets `Cache-Control` on every file the route serves, its `304`s and ranges included. [ADR 0281](docs/decisions/0281-static-files-are-served-by-one-responder.md)
23
+ **Affects:** `stitchkit/server` staticRoute, StaticRouteOptions
24
+
25
+ ### Fixed
26
+
27
+ - `stitchkit/server` — `staticRoute` serves a file the way `serveFile` does. It read the whole file into memory and answered without `ETag`, `Last-Modified` or `Accept-Ranges`, so a browser re-downloaded every image and video on every visit, and a `<video>` that needs `206` (Safari, a Telegram Mini App on iOS) got the whole file. It now streams the body, revalidates to `304` and answers a single range with `206` or `416`; containment refusals are unchanged. `serveFile` runs on Node as well as Bun, and on both a directory is `404`. [ADR 0281](docs/decisions/0281-static-files-are-served-by-one-responder.md)
28
+ **Affects:** `stitchkit/server` staticRoute(behaviour), serveFile(behaviour)
29
+
18
30
  ## [0.118.0] - 2026-11-03
19
31
 
20
32
  ### ⚠️ Breaking changes
@@ -9,7 +9,7 @@ export { type CacheOptions, cacheHeaders, createCache } from '../server/cache.js
9
9
  export { createHandler } from '../server/create.js';
10
10
  export { createErrorHook, type ErrorHookBase, type ErrorHookConfig, type ErrorHookMapping, type ResolvedError, } from '../server/error-hook.js';
11
11
  export { createEventBus, type DefaultEventMap, type EventBus, type EventBusOptions, type EventHandler, } from '../server/event-bus.js';
12
- export { type ByteRange, parseByteRange, type ServeFileOptions, serveFile, staticRoute, weakETag, } from '../server/file.js';
12
+ export { type ByteRange, parseByteRange, type ServeFileOptions, type StaticRouteOptions, serveFile, staticRoute, weakETag, } from '../server/file.js';
13
13
  export { createImplement, createMultipartStream, createScopedImplement, defineMultipartStream, type ImplementOptions, implement, type MissingHandlerPolicy, type MultipartStreamConfig, type StreamScope, } from '../server/implement.js';
14
14
  export { createImplementRegistry, createScopedImplementRegistry, type ExactRegistryHandlers, type ExactScopedRegistryHandlers, type ImplementationRegistry, implementRegistry, type KeyedServices, type RegistryHandlers, type ScopedImplementationRegistry, type ScopedRegistryHandlers, } from '../server/implement-registry.js';
15
15
  export { composeLifecycleHooks } from '../server/lifecycle.js';
@@ -1 +1 @@
1
- {"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../../src/entrypoints/server.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../../src/entrypoints/server.ts"],"names":[],"mappings":"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"}
@@ -853,7 +853,8 @@ function createEventBus(options = {}) {
853
853
  };
854
854
  }
855
855
  // src/server/file.ts
856
- import { readFile, stat } from "node:fs/promises";
856
+ import { createReadStream } from "node:fs";
857
+ import { stat } from "node:fs/promises";
857
858
  import { resolve } from "node:path";
858
859
 
859
860
  // src/server/mime.ts
@@ -893,7 +894,7 @@ function mimeForPath(path) {
893
894
  }
894
895
 
895
896
  // src/server/file.ts
896
- function staticRoute(prefix, dir) {
897
+ function staticRoute(prefix, dir, options = {}) {
897
898
  const cleanPrefix = prefix.replace(/\/+$/, "");
898
899
  const root = resolve(dir.replace(/\/+$/, ""));
899
900
  return {
@@ -913,15 +914,7 @@ function staticRoute(prefix, dir) {
913
914
  const realTarget = await realPathWithinDir(root, target);
914
915
  if (realTarget === null)
915
916
  return new Response("Not found", { status: 404 });
916
- const info = await stat(realTarget).catch(() => null);
917
- if (!info?.isFile())
918
- return new Response("Not found", { status: 404 });
919
- return new Response(await readFile(realTarget), {
920
- headers: {
921
- "Content-Type": mimeForPath(realTarget),
922
- "X-Content-Type-Options": "nosniff"
923
- }
924
- });
917
+ return serveFile(req, { path: realTarget, cacheControl: options.cacheControl });
925
918
  }
926
919
  };
927
920
  }
@@ -1021,12 +1014,12 @@ async function serveFile(req, opts) {
1021
1014
  headers: { Allow: "GET, HEAD" }
1022
1015
  });
1023
1016
  }
1024
- const file = Bun.file(opts.path);
1025
- if (!await file.exists()) {
1017
+ const info = await stat(opts.path).catch(() => null);
1018
+ if (!info?.isFile()) {
1026
1019
  return new Response("Not found", { status: 404 });
1027
1020
  }
1028
- const size = file.size;
1029
- const mtimeMs = file.lastModified;
1021
+ const size = info.size;
1022
+ const mtimeMs = info.mtimeMs;
1030
1023
  const contentType = opts.contentType ?? mimeForPath(opts.path);
1031
1024
  const useEtag = opts.etag !== false;
1032
1025
  const useLastModified = opts.lastModified !== false;
@@ -1065,12 +1058,33 @@ async function serveFile(req, opts) {
1065
1058
  base.set("Content-Type", contentType);
1066
1059
  base.set("Content-Range", `bytes ${range.start}-${range.end}/${size}`);
1067
1060
  base.set("Content-Length", String(length));
1068
- const body = isHead ? null : file.slice(range.start, range.end + 1);
1061
+ const body = isHead ? null : fileBody(opts.path, range.start, range.end);
1069
1062
  return new Response(body, { status: 206, headers: base });
1070
1063
  }
1071
1064
  base.set("Content-Type", contentType);
1072
1065
  base.set("Content-Length", String(size));
1073
- return new Response(isHead ? null : file, { status: 200, headers: base });
1066
+ return new Response(isHead || size === 0 ? null : fileBody(opts.path, 0, size - 1), {
1067
+ status: 200,
1068
+ headers: base
1069
+ });
1070
+ }
1071
+ function fileBody(path, start, end) {
1072
+ if ("Bun" in globalThis)
1073
+ return Bun.file(path).slice(start, end + 1);
1074
+ const source = createReadStream(path, { start, end });
1075
+ const chunks = source[Symbol.asyncIterator]();
1076
+ return new ReadableStream({
1077
+ async pull(controller) {
1078
+ const next = await chunks.next();
1079
+ if (next.done)
1080
+ controller.close();
1081
+ else
1082
+ controller.enqueue(next.value);
1083
+ },
1084
+ cancel() {
1085
+ source.destroy();
1086
+ }
1087
+ });
1074
1088
  }
1075
1089
  // src/server/lifecycle.ts
1076
1090
  function composeLifecycleHooks(...hooks) {
@@ -1,9 +1,15 @@
1
1
  import type { RawRoute } from './types.js';
2
+ /** What a `staticRoute` adds to every file it serves. */
3
+ export interface StaticRouteOptions {
4
+ /** Value for the `Cache-Control` header, verbatim. */
5
+ cacheControl?: string;
6
+ }
2
7
  /**
3
- * Build a basic static-file route. URL-derived paths pass both lexical and
4
- * realpath containment checks, so an in-root symlink cannot escape `dir`.
8
+ * Build a static-file route. URL-derived paths pass both lexical and realpath
9
+ * containment checks, so an in-root symlink cannot escape `dir`; the file
10
+ * itself is answered by `serveFile` (Range, validators, `304`, streaming).
5
11
  */
6
- export declare function staticRoute(prefix: string, dir: string): RawRoute;
12
+ export declare function staticRoute(prefix: string, dir: string, options?: StaticRouteOptions): RawRoute;
7
13
  /** An inclusive byte range `[start, end]`. */
8
14
  export interface ByteRange {
9
15
  start: number;
@@ -49,10 +55,10 @@ export interface ServeFileOptions {
49
55
  * `405` (non GET/HEAD), `404` (missing), `304` (fresh per `If-None-Match` /
50
56
  * `If-Modified-Since`), `200` (full — no/ignored Range), `206` (range), or `416`
51
57
  * (unsatisfiable range). Always sets `Accept-Ranges: bytes` and `nosniff`; a
52
- * `HEAD` returns every header with an empty body.
58
+ * `HEAD` returns every header with an empty body. A directory is `404`.
53
59
  *
54
- * Bun-first (`Bun.file`). The caller is responsible for path containment — for
55
- * URL-derived paths use `staticRoute` or `isWithinDir`. → ADR 0023.
60
+ * Runs on Bun and Node. The caller is responsible for path containment — for
61
+ * URL-derived paths use `staticRoute` or `isWithinDir`. → ADR 0023, ADR 0281.
56
62
  */
57
63
  export declare function serveFile(req: Request, opts: ServeFileOptions): Promise<Response>;
58
64
  //# sourceMappingURL=file.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"file.d.ts","sourceRoot":"","sources":["../../src/server/file.ts"],"names":[],"mappings":"AAeA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AAExC;;;GAGG;AACH,wBAAgB,WAAW,CAAC,MAAM,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,QAAQ,CA4BjE;AAED,8CAA8C;AAC9C,MAAM,WAAW,SAAS;IACxB,KAAK,EAAE,MAAM,CAAC;IACd,GAAG,EAAE,MAAM,CAAC;CACb;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,cAAc,CAC5B,MAAM,EAAE,MAAM,GAAG,IAAI,EACrB,IAAI,EAAE,MAAM,GACX,SAAS,GAAG,eAAe,GAAG,IAAI,CAkCpC;AAED;;;;GAIG;AACH,wBAAgB,QAAQ,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM,CAE9D;AA6ED,MAAM,WAAW,gBAAgB;IAC/B,gFAAgF;IAChF,IAAI,EAAE,MAAM,CAAC;IACb,qEAAqE;IACrE,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,qDAAqD;IACrD,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,8EAA8E;IAC9E,WAAW,CAAC,EAAE,QAAQ,GAAG,YAAY,CAAC;IACtC,sDAAsD;IACtD,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,2CAA2C;IAC3C,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,6CAA6C;IAC7C,YAAY,CAAC,EAAE,OAAO,CAAC;CACxB;AAED;;;;;;;;;GASG;AACH,wBAAsB,SAAS,CAAC,GAAG,EAAE,OAAO,EAAE,IAAI,EAAE,gBAAgB,GAAG,OAAO,CAAC,QAAQ,CAAC,CAwEvF"}
1
+ {"version":3,"file":"file.d.ts","sourceRoot":"","sources":["../../src/server/file.ts"],"names":[],"mappings":"AAeA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AAExC,yDAAyD;AACzD,MAAM,WAAW,kBAAkB;IACjC,sDAAsD;IACtD,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AAED;;;;GAIG;AACH,wBAAgB,WAAW,CACzB,MAAM,EAAE,MAAM,EACd,GAAG,EAAE,MAAM,EACX,OAAO,GAAE,kBAAuB,GAC/B,QAAQ,CAqBV;AAED,8CAA8C;AAC9C,MAAM,WAAW,SAAS;IACxB,KAAK,EAAE,MAAM,CAAC;IACd,GAAG,EAAE,MAAM,CAAC;CACb;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,cAAc,CAC5B,MAAM,EAAE,MAAM,GAAG,IAAI,EACrB,IAAI,EAAE,MAAM,GACX,SAAS,GAAG,eAAe,GAAG,IAAI,CAkCpC;AAED;;;;GAIG;AACH,wBAAgB,QAAQ,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM,CAE9D;AA6ED,MAAM,WAAW,gBAAgB;IAC/B,gFAAgF;IAChF,IAAI,EAAE,MAAM,CAAC;IACb,qEAAqE;IACrE,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,qDAAqD;IACrD,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,8EAA8E;IAC9E,WAAW,CAAC,EAAE,QAAQ,GAAG,YAAY,CAAC;IACtC,sDAAsD;IACtD,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,2CAA2C;IAC3C,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,6CAA6C;IAC7C,YAAY,CAAC,EAAE,OAAO,CAAC;CACxB;AAED;;;;;;;;;GASG;AACH,wBAAsB,SAAS,CAAC,GAAG,EAAE,OAAO,EAAE,IAAI,EAAE,gBAAgB,GAAG,OAAO,CAAC,QAAQ,CAAC,CA0EvF"}
package/llms/server.2.txt CHANGED
@@ -97,11 +97,24 @@ contract route does.
97
97
 
98
98
  ### Serving files & Range requests
99
99
 
100
- `staticRoute` is basic on purpose — it reads the whole file into memory and has
101
- no `Range` or conditional support; put a CDN in front, or use it for small web
102
- assets. To serve **media** (video / audio / large downloads) that a browser must
103
- seek and cache, use **`serveFile`** (Bun) — it streams the requested byte range
104
- and speaks the conditional-request half of RFC 7233 / 9110:
100
+ `staticRoute(prefix, dir, { cacheControl })` serves a directory: it checks
101
+ containment, then answers the file through `serveFile`, so a static image or
102
+ video carries `ETag`, `Last-Modified` and `Accept-Ranges`, revalidates to `304`
103
+ and seeks with `206`. `cacheControl` sets `Cache-Control` on every answer; by
104
+ default none is sent.
105
+
106
+ ```ts
107
+ import { createServer, staticRoute } from 'stitchkit/server'
108
+
109
+ createServer({
110
+ rawRoutes: [staticRoute('/static', './public', { cacheControl: 'public, max-age=3600' })],
111
+ })
112
+ ```
113
+
114
+ The route answers `GET`. To serve a file chosen by your own logic — through a
115
+ contract, with `HEAD`, a download name or a custom type — call **`serveFile`**
116
+ directly. It runs on Bun and Node, streams the requested byte range and speaks
117
+ the conditional-request half of RFC 7233 / 9110:
105
118
 
106
119
  ```ts
107
120
  import { defineContract } from 'stitchkit/contract'
@@ -151,7 +164,7 @@ emit none.
151
164
  `serveFile` takes an explicit `path` and trusts it — **the caller owns
152
165
  containment**. For a URL-derived path use `staticRoute` (which enforces it) or
153
166
  `isWithinDir` first. The byte-range parser is exported on its own as
154
- `parseByteRange(header, size)` for direct use and testing. → ADR 0023.
167
+ `parseByteRange(header, size)` for direct use and testing. → ADR 0023, ADR 0281.
155
168
 
156
169
  ## Server primitives
157
170
 
package/llms/server.5.txt CHANGED
@@ -47,7 +47,8 @@ Also re-exports the error helpers from `stitchkit/contract`.
47
47
  | `KeyedServices` | _type_ | registry result: the mount-ordered `ServiceDef[]` carrying the same services under `.byKey` |
48
48
  | `RegistryHandlers` | _type_ | exact backend handler registry inferred from a contract registry |
49
49
  | `ExactRegistryHandlers` | _type_ | fail-first handler shape that rejects extra registry and endpoint keys |
50
- | `staticRoute` | function | a raw route that serves a directory |
50
+ | `staticRoute` | function | a raw route that serves a contained directory through `serveFile` (`Range` / `304` / validators) |
51
+ | `StaticRouteOptions` | _type_ | `{ cacheControl }` — what `staticRoute` adds to every file it serves |
51
52
  | `serveFile` | function | serve a file with `Range` / `304` / `HEAD` — [guide](../guide/server.md#serving-files--range-requests) |
52
53
  | `parseByteRange` | function | parse a single `Range` header → range / `unsatisfiable` / `null` |
53
54
  | `isWithinDir` | function | path containment — `(root, resolvedTarget) => boolean`; call it before `serveFile` on any URL-derived path |
package/llms/testing.txt CHANGED
@@ -608,9 +608,9 @@ Notes for a Node host:
608
608
  - **Socket.IO** attaches to the Node HTTP server via `serveNode({ socket })`, and
609
609
  on Node the default transport is `['websocket']` — set the client to match
610
610
  (`transports: ['websocket']`). See [realtime](./realtime.md).
611
- - **Bun-only helpers** do not run on Node: `serveFile` (uses `Bun.file`) and the
612
- raw WebSocket lane. `staticRoute` is runtime-neutral (`node:fs`) and works on
613
- both, but in production prefer a CDN / the static front-end below.
611
+ - **Bun-only helpers** do not run on Node: the raw WebSocket lane. `serveFile`
612
+ and `staticRoute` work on both — on Node the body is a `node:fs` read stream —
613
+ but in production prefer a CDN / the static front-end below.
614
614
 
615
615
  ### Production checklist
616
616
 
package/llms-full.txt CHANGED
@@ -2236,11 +2236,24 @@ contract route does.
2236
2236
 
2237
2237
  ### Serving files & Range requests
2238
2238
 
2239
- `staticRoute` is basic on purpose — it reads the whole file into memory and has
2240
- no `Range` or conditional support; put a CDN in front, or use it for small web
2241
- assets. To serve **media** (video / audio / large downloads) that a browser must
2242
- seek and cache, use **`serveFile`** (Bun) — it streams the requested byte range
2243
- and speaks the conditional-request half of RFC 7233 / 9110:
2239
+ `staticRoute(prefix, dir, { cacheControl })` serves a directory: it checks
2240
+ containment, then answers the file through `serveFile`, so a static image or
2241
+ video carries `ETag`, `Last-Modified` and `Accept-Ranges`, revalidates to `304`
2242
+ and seeks with `206`. `cacheControl` sets `Cache-Control` on every answer; by
2243
+ default none is sent.
2244
+
2245
+ ```ts
2246
+ import { createServer, staticRoute } from 'stitchkit/server'
2247
+
2248
+ createServer({
2249
+ rawRoutes: [staticRoute('/static', './public', { cacheControl: 'public, max-age=3600' })],
2250
+ })
2251
+ ```
2252
+
2253
+ The route answers `GET`. To serve a file chosen by your own logic — through a
2254
+ contract, with `HEAD`, a download name or a custom type — call **`serveFile`**
2255
+ directly. It runs on Bun and Node, streams the requested byte range and speaks
2256
+ the conditional-request half of RFC 7233 / 9110:
2244
2257
 
2245
2258
  ```ts
2246
2259
  import { defineContract } from 'stitchkit/contract'
@@ -2290,7 +2303,7 @@ emit none.
2290
2303
  `serveFile` takes an explicit `path` and trusts it — **the caller owns
2291
2304
  containment**. For a URL-derived path use `staticRoute` (which enforces it) or
2292
2305
  `isWithinDir` first. The byte-range parser is exported on its own as
2293
- `parseByteRange(header, size)` for direct use and testing. → ADR 0023.
2306
+ `parseByteRange(header, size)` for direct use and testing. → ADR 0023, ADR 0281.
2294
2307
 
2295
2308
  ## Server primitives
2296
2309
 
@@ -15141,9 +15154,9 @@ Notes for a Node host:
15141
15154
  - **Socket.IO** attaches to the Node HTTP server via `serveNode({ socket })`, and
15142
15155
  on Node the default transport is `['websocket']` — set the client to match
15143
15156
  (`transports: ['websocket']`). See [realtime](./realtime.md).
15144
- - **Bun-only helpers** do not run on Node: `serveFile` (uses `Bun.file`) and the
15145
- raw WebSocket lane. `staticRoute` is runtime-neutral (`node:fs`) and works on
15146
- both, but in production prefer a CDN / the static front-end below.
15157
+ - **Bun-only helpers** do not run on Node: the raw WebSocket lane. `serveFile`
15158
+ and `staticRoute` work on both — on Node the body is a `node:fs` read stream —
15159
+ but in production prefer a CDN / the static front-end below.
15147
15160
 
15148
15161
  ### Production checklist
15149
15162
 
@@ -22558,7 +22571,8 @@ Also re-exports the error helpers from `stitchkit/contract`.
22558
22571
  | `KeyedServices` | _type_ | registry result: the mount-ordered `ServiceDef[]` carrying the same services under `.byKey` |
22559
22572
  | `RegistryHandlers` | _type_ | exact backend handler registry inferred from a contract registry |
22560
22573
  | `ExactRegistryHandlers` | _type_ | fail-first handler shape that rejects extra registry and endpoint keys |
22561
- | `staticRoute` | function | a raw route that serves a directory |
22574
+ | `staticRoute` | function | a raw route that serves a contained directory through `serveFile` (`Range` / `304` / validators) |
22575
+ | `StaticRouteOptions` | _type_ | `{ cacheControl }` — what `staticRoute` adds to every file it serves |
22562
22576
  | `serveFile` | function | serve a file with `Range` / `304` / `HEAD` — [guide](../guide/server.md#serving-files--range-requests) |
22563
22577
  | `parseByteRange` | function | parse a single `Range` header → range / `unsatisfiable` / `null` |
22564
22578
  | `isWithinDir` | function | path containment — `(root, resolvedTarget) => boolean`; call it before `serveFile` on any URL-derived path |
package/llms.txt CHANGED
@@ -24,7 +24,7 @@ Load the slice for each entrypoint your code imports — `import … from 'stitc
24
24
  - **stitchkit/remote**: [llms/remote.txt](llms/remote.txt) 1.0 KB
25
25
  - **stitchkit/contract**: [llms/contract.txt](llms/contract.txt) 31.9 KB
26
26
  - **stitchkit/primitives**: [llms/primitives.txt](llms/primitives.txt) 13.2 KB
27
- - **stitchkit/server**: [llms/server.txt](llms/server.txt) 44.2 KB → [llms/server.2.txt](llms/server.2.txt) 20.5 KB → [llms/server.3.txt](llms/server.3.txt) 47.0 KB → [llms/server.4.txt](llms/server.4.txt) 38.6 KB → [llms/server.5.txt](llms/server.5.txt) 22.9 KB
27
+ - **stitchkit/server**: [llms/server.txt](llms/server.txt) 44.2 KB → [llms/server.2.txt](llms/server.2.txt) 20.9 KB → [llms/server.3.txt](llms/server.3.txt) 47.0 KB → [llms/server.4.txt](llms/server.4.txt) 38.6 KB → [llms/server.5.txt](llms/server.5.txt) 23.1 KB
28
28
  - **stitchkit/observability**: [llms/observability.txt](llms/observability.txt) 46.9 KB
29
29
  - **stitchkit/agent-runtime**: [llms/agent-runtime.txt](llms/agent-runtime.txt) 44.3 KB → [llms/agent-runtime.2.txt](llms/agent-runtime.2.txt) 46.7 KB → [llms/agent-runtime.3.txt](llms/agent-runtime.3.txt) 43.4 KB
30
30
  - **stitchkit/agent-runtime/testing**: [llms/agent-runtime-testing.txt](llms/agent-runtime-testing.txt) 2.6 KB
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "stitchkit",
3
- "version": "0.118.0",
3
+ "version": "0.118.1",
4
4
  "description": "Contract-first backend framework — one defineContract() into an HTTP API, MCP tools, AI-agent tools and a typed client. Bun and Node.",
5
5
  "keywords": [
6
6
  "bun",