stitchkit 0.114.1 → 0.114.2

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,14 @@ additive**; the first breaking change landed in 0.10.0. Grep the file for
15
15
 
16
16
  ## [Unreleased]
17
17
 
18
+ ## [0.114.2] - 2026-10-31
19
+
20
+ ### Fixed
21
+
22
+ - `stitchkit/tools`, `stitchkit/cli` — the guarded fetch behind `view_file`, `mountDownload` and the CLI `--output-dir` download pins every hop to the address that passed the public-address check. It resolved a hostname, validated the answer, then let `fetch` resolve the same name again, so a resolver that answers public and then private could open a private or loopback socket (SSRF rebinding). The connection is now opened to the validated address with the URL's `Host` header and, over TLS, its server name, on every redirect hop; a URL carrying credentials is refused rather than moved to whatever host a redirect names. Streaming, backpressure, the header/body deadlines, the body cap and output-dir containment are unchanged; `allowPrivateHosts` / `allowPrivateDownloadHosts` still widen the address check without a second lookup.
23
+ **Affects:** `stitchkit/tools` behaviour; `stitchkit/cli` behaviour
24
+
25
+
18
26
  ## [0.114.1] - 2026-10-31
19
27
 
20
28
  ### Added
@@ -15,7 +15,7 @@ import {
15
15
  gzipCliAsset,
16
16
  decodeCliAsset,
17
17
  compareCliVersions2
18
- } from "../../index-kdnfe0sr.js";
18
+ } from "../../index-912sza5h.js";
19
19
  import {
20
20
  withExclusiveLock2
21
21
  } from "../../index-6b3pcq5z.js";
@@ -16,7 +16,7 @@ import {
16
16
  renderCliView2,
17
17
  pollUntilDone2,
18
18
  createCli2
19
- } from "../index-km2qw407.js";
19
+ } from "../index-tnxsq7cw.js";
20
20
  import"../index-k42ckyzx.js";
21
21
  import"../index-ct75c3at.js";
22
22
  import"../index-rhzkndr5.js";
@@ -40,7 +40,7 @@ import {
40
40
  checkCliUpdate2,
41
41
  applyCliUpdate2,
42
42
  rollbackCliUpdate2
43
- } from "../index-kdnfe0sr.js";
43
+ } from "../index-912sza5h.js";
44
44
  import"../index-86nbfysb.js";
45
45
  import"../index-ega3yz4c.js";
46
46
  import {
@@ -74,7 +74,7 @@ import {
74
74
  WaitTimeoutError,
75
75
  runWaitOperation,
76
76
  createCli2
77
- } from "../index-km2qw407.js";
77
+ } from "../index-tnxsq7cw.js";
78
78
  import {
79
79
  ManagedFilePathSchema2,
80
80
  ManagedFileRefSchema2
@@ -110,7 +110,7 @@ import {
110
110
  fetchGuarded,
111
111
  fetchPinnedDocument,
112
112
  readCapped
113
- } from "../index-t65erk4z.js";
113
+ } from "../index-mgb7r8b8.js";
114
114
  import"../index-1bx1xv1k.js";
115
115
  import {
116
116
  readBoundedChunks,
@@ -5,7 +5,7 @@ import {
5
5
  fetchGuarded,
6
6
  PrivateAddressRefusal,
7
7
  readCapped
8
- } from "./index-t65erk4z.js";
8
+ } from "./index-mgb7r8b8.js";
9
9
  import {
10
10
  writeFileAtomicSync2
11
11
  } from "./index-07dcf2yn.js";
@@ -7,9 +7,88 @@ import {
7
7
 
8
8
  // src/internal/secure-fetch.ts
9
9
  import { lookup } from "node:dns/promises";
10
+ import { isIP } from "node:net";
11
+
12
+ // src/internal/pinned-http.ts
10
13
  import { request as httpRequest } from "node:http";
11
14
  import { request as httpsRequest } from "node:https";
12
- import { isIP } from "node:net";
15
+ async function requestPinned(url, resolved, signal, headers = {}) {
16
+ const transport = url.protocol === "https:" ? httpsRequest : httpRequest;
17
+ const incoming = await new Promise((resolve, reject) => {
18
+ let answered = false;
19
+ const request = transport({
20
+ protocol: url.protocol,
21
+ hostname: resolved.address,
22
+ family: resolved.family,
23
+ port: url.port || undefined,
24
+ path: `${url.pathname}${url.search}`,
25
+ method: "GET",
26
+ signal,
27
+ ...url.protocol === "https:" ? { servername: url.hostname } : {},
28
+ headers: { host: url.host, accept: "*/*", ...headers }
29
+ }, (response) => {
30
+ answered = true;
31
+ resolve(response);
32
+ });
33
+ request.once("error", (error) => {
34
+ if (answered)
35
+ return;
36
+ reject(signal?.aborted && signal.reason !== undefined ? signal.reason : error);
37
+ });
38
+ request.end();
39
+ });
40
+ return pinnedResponse(incoming);
41
+ }
42
+ function pinnedResponse(incoming) {
43
+ const status = incoming.statusCode ?? 0;
44
+ const headers = new Headers;
45
+ for (const [name, value] of Object.entries(incoming.headers)) {
46
+ if (Array.isArray(value)) {
47
+ for (const item of value)
48
+ headers.append(name, item);
49
+ } else if (value !== undefined) {
50
+ headers.set(name, value);
51
+ }
52
+ }
53
+ if (status === 204 || status === 205 || status === 304) {
54
+ incoming.resume();
55
+ return new Response(null, { status, statusText: incoming.statusMessage, headers });
56
+ }
57
+ let done = false;
58
+ const body = new ReadableStream({
59
+ start(controller) {
60
+ incoming.on("data", (chunk) => {
61
+ if (done)
62
+ return;
63
+ controller.enqueue(chunk);
64
+ if ((controller.desiredSize ?? 1) <= 0)
65
+ incoming.pause();
66
+ });
67
+ incoming.once("end", () => {
68
+ if (done)
69
+ return;
70
+ done = true;
71
+ controller.close();
72
+ });
73
+ incoming.once("error", (error) => {
74
+ if (done)
75
+ return;
76
+ done = true;
77
+ controller.error(error);
78
+ });
79
+ },
80
+ pull() {
81
+ incoming.resume();
82
+ },
83
+ cancel() {
84
+ done = true;
85
+ incoming.destroy();
86
+ }
87
+ });
88
+ return new Response(body, { status, statusText: incoming.statusMessage, headers });
89
+ }
90
+
91
+ // src/internal/secure-fetch.ts
13
92
  var NUMERIC_HOST = /^(0x[0-9a-f]+|[0-9.]+)$/i;
14
93
  var MAX_REDIRECTS = 5;
15
94
  function assertHttpUrl(url) {
@@ -102,9 +181,10 @@ function isPrivateIp(ip) {
102
181
  }
103
182
  return false;
104
183
  }
105
- async function assertPublicUrl(url, signal) {
106
- await resolvePublicAddress(url, signal);
107
- }
184
+ var defaultLookup = async (host) => {
185
+ const records = await lookup(host, { all: true });
186
+ return Array.isArray(records) ? records : [records];
187
+ };
108
188
  async function fetchGuarded(start, allowPrivate, options = {}) {
109
189
  const timeoutMs = options.timeoutMs ?? 15000;
110
190
  const bodyTimeoutMs = options.bodyTimeoutMs ?? 120000;
@@ -120,15 +200,16 @@ async function fetchGuarded(start, allowPrivate, options = {}) {
120
200
  controller.abort(new Error(`fetch did not produce response headers within ${timeoutMs}ms`));
121
201
  }, timeoutMs);
122
202
  headerTimer.unref?.();
203
+ const transport = options.transport ?? requestPinned;
123
204
  let settled = false;
124
205
  try {
125
206
  let url = start;
126
207
  for (let hop = 0;hop <= MAX_REDIRECTS; hop++) {
127
- if (allowPrivate)
128
- assertHttpUrl(url);
129
- else
130
- await assertPublicUrl(url, signal);
131
- const res = await fetch(url, { redirect: "manual", signal });
208
+ if (url.username || url.password) {
209
+ throw new Error("refusing to fetch a URL that carries credentials");
210
+ }
211
+ const resolved = await resolveAddress(url, allowPrivate, signal, options.dns);
212
+ const res = await transport(url, resolved, signal);
132
213
  const location = res.status >= 300 && res.status < 400 ? res.headers.get("location") : null;
133
214
  if (location === null) {
134
215
  settled = true;
@@ -148,11 +229,27 @@ async function fetchGuarded(start, allowPrivate, options = {}) {
148
229
  clearTimeout(headerTimer);
149
230
  }
150
231
  }
232
+ async function resolveAddress(url, allowPrivate, signal, dns) {
233
+ if (!allowPrivate)
234
+ return resolvePublicAddress(url, signal, dns);
235
+ assertHttpUrl(url);
236
+ const host = url.hostname.replace(/^\[|\]$/g, "");
237
+ if (!host)
238
+ throw new Error("refusing to fetch a URL with no host");
239
+ const ipFamily = isIP(host);
240
+ if (ipFamily)
241
+ return { address: host, family: ipFamily === 6 ? 6 : 4 };
242
+ const records = await (dns ?? defaultLookup)(host, { all: true });
243
+ const record = records[0];
244
+ if (!record)
245
+ throw new Error("host resolved to no addresses");
246
+ return { address: record.address, family: record.family === 6 ? 6 : 4 };
247
+ }
151
248
 
152
249
  class PrivateAddressRefusal extends Error {
153
250
  name = "PrivateAddressRefusal";
154
251
  }
155
- async function resolvePublicAddress(url, signal) {
252
+ async function resolvePublicAddress(url, signal, dns) {
156
253
  assertHttpUrl(url);
157
254
  const host = url.hostname.replace(/^\[|\]$/g, "");
158
255
  if (!host)
@@ -169,7 +266,7 @@ async function resolvePublicAddress(url, signal) {
169
266
  if (host === "localhost" || host.endsWith(".local") || host.endsWith(".internal")) {
170
267
  throw new PrivateAddressRefusal("refusing to fetch an internal host");
171
268
  }
172
- const pending = lookup(host, { all: true });
269
+ const pending = (dns ?? defaultLookup)(host, { all: true });
173
270
  const records = signal ? await raceAbort(pending, signal) : await pending;
174
271
  for (const record of records) {
175
272
  if (isPrivateIp(record.address)) {
@@ -202,61 +299,24 @@ async function fetchPinnedDocument(start, options) {
202
299
  throw new Error("document URL cannot contain credentials");
203
300
  }
204
301
  const resolved = await resolvePublicAddress(url, signal);
205
- const transport = url.protocol === "https:" ? httpsRequest : httpRequest;
206
- const response = await new Promise((resolve, reject) => {
207
- const request = transport({
208
- protocol: url.protocol,
209
- hostname: resolved.address,
210
- family: resolved.family,
211
- port: url.port || undefined,
212
- path: `${url.pathname}${url.search}`,
213
- method: "GET",
214
- signal,
215
- servername: url.hostname,
216
- headers: {
217
- host: url.host,
218
- accept: "application/json",
219
- ...options.headers
220
- }
221
- }, (incoming) => {
222
- const headers = new Headers;
223
- for (const [name, value] of Object.entries(incoming.headers)) {
224
- if (Array.isArray(value)) {
225
- for (const item of value)
226
- headers.append(name, item);
227
- } else if (value !== undefined) {
228
- headers.set(name, value);
229
- }
230
- }
231
- const chunks = [];
232
- let total = 0;
233
- incoming.on("data", (chunk) => {
234
- total += chunk.byteLength;
235
- if (total > options.maxBytes) {
236
- incoming.destroy(new Error("response body exceeds configured byte limit"));
237
- return;
238
- }
239
- chunks.push(chunk);
240
- });
241
- incoming.once("error", reject);
242
- incoming.once("end", () => {
243
- const body = new Uint8Array(total);
244
- let offset = 0;
245
- for (const chunk of chunks) {
246
- body.set(chunk, offset);
247
- offset += chunk.byteLength;
248
- }
249
- resolve({ status: incoming.statusCode ?? 0, headers, body, url });
250
- });
251
- });
252
- request.once("error", reject);
253
- request.end();
302
+ const response = await requestPinned(url, resolved, signal, {
303
+ accept: "application/json",
304
+ ...options.headers
254
305
  });
306
+ const body = await readCapped(response, options.maxBytes);
307
+ if (body === null)
308
+ throw new Error("response body exceeds configured byte limit");
309
+ const document = {
310
+ status: response.status,
311
+ headers: response.headers,
312
+ body: new Uint8Array(body),
313
+ url
314
+ };
255
315
  if (response.status < 300 || response.status >= 400)
256
- return response;
316
+ return document;
257
317
  const location = response.headers.get("location");
258
318
  if (!location)
259
- return response;
319
+ return document;
260
320
  url = new URL(location, url);
261
321
  }
262
322
  throw new Error("too many redirects");
@@ -29,7 +29,7 @@ import {
29
29
  import {
30
30
  fetchGuarded,
31
31
  readCapped
32
- } from "./index-t65erk4z.js";
32
+ } from "./index-mgb7r8b8.js";
33
33
  import {
34
34
  isWithinDir2
35
35
  } from "./index-5cc6r990.js";
@@ -0,0 +1,27 @@
1
+ /**
2
+ * One hop over a socket opened to the address the guard validated.
3
+ *
4
+ * The address is given, never looked up here: that is what makes a validated
5
+ * answer the one the connection uses. The `Host` header keeps the URL's host
6
+ * and, over TLS, the certificate is verified against it (`servername`), so a
7
+ * pinned connection is not a weaker one. The body is a Fetch stream over the
8
+ * socket, with backpressure and cancellation preserved.
9
+ */
10
+ import type { IncomingMessage } from 'node:http';
11
+ /** One hop, already resolved: open the connection to exactly `address`. */
12
+ export type PinnedTransport = (url: URL, resolved: {
13
+ address: string;
14
+ family: 4 | 6;
15
+ }, signal: AbortSignal | undefined) => Promise<Response>;
16
+ /**
17
+ * One hop over a socket opened to `resolved.address`: the `Host` header keeps
18
+ * the URL's host and, for TLS, the certificate is verified against it
19
+ * (`servername`), so a pinned connection is not a weaker connection.
20
+ */
21
+ export declare function requestPinned(url: URL, resolved: {
22
+ address: string;
23
+ family: 4 | 6;
24
+ }, signal: AbortSignal | undefined, headers?: Record<string, string>): Promise<Response>;
25
+ /** An `IncomingMessage` as a streaming Fetch response, keeping backpressure and cancellation. */
26
+ export declare function pinnedResponse(incoming: IncomingMessage): Response;
27
+ //# sourceMappingURL=pinned-http.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pinned-http.d.ts","sourceRoot":"","sources":["../../src/internal/pinned-http.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AACH,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,WAAW,CAAC;AAIjD,2EAA2E;AAC3E,MAAM,MAAM,eAAe,GAAG,CAC5B,GAAG,EAAE,GAAG,EACR,QAAQ,EAAE;IAAE,OAAO,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAA;CAAE,EAC5C,MAAM,EAAE,WAAW,GAAG,SAAS,KAC5B,OAAO,CAAC,QAAQ,CAAC,CAAC;AAEvB;;;;GAIG;AACH,wBAAsB,aAAa,CACjC,GAAG,EAAE,GAAG,EACR,QAAQ,EAAE;IAAE,OAAO,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAA;CAAE,EAC5C,MAAM,EAAE,WAAW,GAAG,SAAS,EAC/B,OAAO,GAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAM,GACnC,OAAO,CAAC,QAAQ,CAAC,CAgCnB;AAED,iGAAiG;AACjG,wBAAgB,cAAc,CAAC,QAAQ,EAAE,eAAe,GAAG,QAAQ,CA6ClE"}
@@ -1,3 +1,4 @@
1
+ import type { PinnedTransport } from './pinned-http.js';
1
2
  /** True unless the literal belongs to the globally routable unicast Internet. */
2
3
  export declare function isPrivateIp(ip: string): boolean;
3
4
  /**
@@ -14,13 +15,21 @@ export declare function assertPublicUrl(url: URL, signal?: AbortSignal): Promise
14
15
  /**
15
16
  * Fetch a URL with the SSRF guard enforced on the initial URL and on every
16
17
  * redirect hop — a public host can otherwise `302` to an internal address or a
17
- * non-http(s) scheme.
18
- *
19
- * Note: the DNS resolution here and the resolution `fetch` performs are
20
- * separate, so a hostile resolver retains a narrow rebinding window. The
21
- * per-hop re-check is the larger and fully-closed hole; a static private DNS
22
- * record is rejected outright.
18
+ * non-http(s) scheme — and with the connection pinned to the address that
19
+ * passed the check, so a resolver cannot answer public and then connect
20
+ * somewhere private.
21
+ */
22
+ /**
23
+ * The resolver seam: one `lookup` call per hop. Production uses `node:dns`;
24
+ * a test supplies a controlled answer to prove which address the socket went
25
+ * to, and that a second answer cannot open another one.
23
26
  */
27
+ export type GuardedDnsLookup = (host: string, options: {
28
+ all: true;
29
+ }) => Promise<Array<{
30
+ address: string;
31
+ family: number;
32
+ }>>;
24
33
  export interface GuardedFetchOptions {
25
34
  /**
26
35
  * Deadline for producing the FINAL response headers — DNS, connects and every
@@ -36,6 +45,17 @@ export interface GuardedFetchOptions {
36
45
  bodyTimeoutMs?: number;
37
46
  /** Optional caller cancellation, combined with both deadlines. */
38
47
  signal?: AbortSignal;
48
+ /**
49
+ * Test seam: the resolver whose answer the connection is pinned to. Default
50
+ * `node:dns`. It replaces the name lookup, never the address check.
51
+ */
52
+ dns?: GuardedDnsLookup;
53
+ /**
54
+ * Test seam: how one resolved hop is opened. Default the pinned
55
+ * `http`/`https` request. Only the redirect tests replace it; production and
56
+ * the rebinding test use the default, which is where pinning happens.
57
+ */
58
+ transport?: PinnedTransport;
39
59
  }
40
60
  export declare function fetchGuarded(start: URL, allowPrivate: boolean, options?: GuardedFetchOptions): Promise<Response>;
41
61
  export interface PinnedDocumentFetchOptions {
@@ -1 +1 @@
1
- {"version":3,"file":"secure-fetch.d.ts","sourceRoot":"","sources":["../../src/internal/secure-fetch.ts"],"names":[],"mappings":"AAmEA,iFAAiF;AACjF,wBAAgB,WAAW,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CA8D/C;AAED;;;;;;;;;GASG;AACH,wBAAsB,eAAe,CAAC,GAAG,EAAE,GAAG,EAAE,MAAM,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC,IAAI,CAAC,CAEnF;AAED;;;;;;;;;GASG;AACH,MAAM,WAAW,mBAAmB;IAClC;;;;OAIG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB;;;;OAIG;IACH,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,kEAAkE;IAClE,MAAM,CAAC,EAAE,WAAW,CAAC;CACtB;AAED,wBAAsB,YAAY,CAChC,KAAK,EAAE,GAAG,EACV,YAAY,EAAE,OAAO,EACrB,OAAO,GAAE,mBAAwB,GAChC,OAAO,CAAC,QAAQ,CAAC,CAiDnB;AAED,MAAM,WAAW,0BAA0B;IACzC,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;IAClB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACjC,6DAA6D;IAC7D,YAAY,CAAC,EAAE,OAAO,CAAC;CACxB;AAED,MAAM,WAAW,sBAAsB;IACrC,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,OAAO,CAAC;IACjB,IAAI,EAAE,UAAU,CAAC;IACjB,GAAG,EAAE,GAAG,CAAC;CACV;AAED;;;;;;;;GAQG;AACH,qBAAa,qBAAsB,SAAQ,KAAK;IAC9C,QAAQ,CAAC,IAAI,2BAA2B;CACzC;AA8CD;;;;GAIG;AACH,wBAAsB,mBAAmB,CACvC,KAAK,EAAE,GAAG,EACV,OAAO,EAAE,0BAA0B,GAClC,OAAO,CAAC,sBAAsB,CAAC,CAkFjC;AAED;;;;GAIG;AACH,wBAAsB,UAAU,CAAC,GAAG,EAAE,QAAQ,EAAE,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAGnF"}
1
+ {"version":3,"file":"secure-fetch.d.ts","sourceRoot":"","sources":["../../src/internal/secure-fetch.ts"],"names":[],"mappings":"AAsBA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,eAAe,CAAC;AAmDrD,iFAAiF;AACjF,wBAAgB,WAAW,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CA8D/C;AAED;;;;;;;;;GASG;AACH,wBAAsB,eAAe,CAAC,GAAG,EAAE,GAAG,EAAE,MAAM,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC,IAAI,CAAC,CAEnF;AAED;;;;;;GAMG;AACH;;;;GAIG;AACH,MAAM,MAAM,gBAAgB,GAAG,CAC7B,IAAI,EAAE,MAAM,EACZ,OAAO,EAAE;IAAE,GAAG,EAAE,IAAI,CAAA;CAAE,KACnB,OAAO,CAAC,KAAK,CAAC;IAAE,OAAO,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAC,CAAC,CAAC;AAQzD,MAAM,WAAW,mBAAmB;IAClC;;;;OAIG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB;;;;OAIG;IACH,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,kEAAkE;IAClE,MAAM,CAAC,EAAE,WAAW,CAAC;IACrB;;;OAGG;IACH,GAAG,CAAC,EAAE,gBAAgB,CAAC;IACvB;;;;OAIG;IACH,SAAS,CAAC,EAAE,eAAe,CAAC;CAC7B;AAED,wBAAsB,YAAY,CAChC,KAAK,EAAE,GAAG,EACV,YAAY,EAAE,OAAO,EACrB,OAAO,GAAE,mBAAwB,GAChC,OAAO,CAAC,QAAQ,CAAC,CAuDnB;AAqBD,MAAM,WAAW,0BAA0B;IACzC,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;IAClB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACjC,6DAA6D;IAC7D,YAAY,CAAC,EAAE,OAAO,CAAC;CACxB;AAED,MAAM,WAAW,sBAAsB;IACrC,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,OAAO,CAAC;IACjB,IAAI,EAAE,UAAU,CAAC;IACjB,GAAG,EAAE,GAAG,CAAC;CACV;AAED;;;;;;;;GAQG;AACH,qBAAa,qBAAsB,SAAQ,KAAK;IAC9C,QAAQ,CAAC,IAAI,2BAA2B;CACzC;AA+CD;;;;GAIG;AACH,wBAAsB,mBAAmB,CACvC,KAAK,EAAE,GAAG,EACV,OAAO,EAAE,0BAA0B,GAClC,OAAO,CAAC,sBAAsB,CAAC,CA2CjC;AAED;;;;GAIG;AACH,wBAAsB,UAAU,CAAC,GAAG,EAAE,QAAQ,EAAE,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAGnF"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "stitchkit",
3
- "version": "0.114.1",
3
+ "version": "0.114.2",
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",