stitchkit 0.67.0 → 0.68.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 (83) hide show
  1. package/dist/agent-runtime.js +5 -6
  2. package/dist/application/admission.d.ts +131 -0
  3. package/dist/application/admission.d.ts.map +1 -0
  4. package/dist/application/channel.d.ts +106 -0
  5. package/dist/application/channel.d.ts.map +1 -0
  6. package/dist/application/latest-sink.d.ts.map +1 -1
  7. package/dist/application-grammy.js +1 -1
  8. package/dist/application-opentelemetry.js +1 -1
  9. package/dist/application.d.ts +2 -0
  10. package/dist/application.d.ts.map +1 -1
  11. package/dist/application.js +386 -17
  12. package/dist/browser/client.d.ts +2 -3
  13. package/dist/browser/client.d.ts.map +1 -1
  14. package/dist/browser/contract-stream.d.ts +5 -0
  15. package/dist/browser/contract-stream.d.ts.map +1 -0
  16. package/dist/browser/http.d.ts +3 -0
  17. package/dist/browser/http.d.ts.map +1 -1
  18. package/dist/browser/stream.d.ts +4 -1
  19. package/dist/browser/stream.d.ts.map +1 -1
  20. package/dist/browser/transport.d.ts +3 -0
  21. package/dist/browser/transport.d.ts.map +1 -0
  22. package/dist/cli.js +6 -7
  23. package/dist/contract/define.d.ts +34 -2
  24. package/dist/contract/define.d.ts.map +1 -1
  25. package/dist/contract/errors.d.ts +4 -0
  26. package/dist/contract/errors.d.ts.map +1 -1
  27. package/dist/contract/index.d.ts +2 -1
  28. package/dist/contract/index.d.ts.map +1 -1
  29. package/dist/contract/index.js +5 -1
  30. package/dist/contract/stream.d.ts +19 -0
  31. package/dist/contract/stream.d.ts.map +1 -0
  32. package/dist/{index-xxye8j3k.js → index-0w9abg87.js} +4 -0
  33. package/dist/index-69m2278y.js +115 -0
  34. package/dist/{index-physbf4z.js → index-6wzr93cg.js} +1 -1
  35. package/dist/{index-1pgeyyee.js → index-7pn02bgv.js} +4 -6
  36. package/dist/index-9ky9hhg3.js +484 -0
  37. package/dist/{index-wrhf06ak.js → index-avjd67h6.js} +8 -8
  38. package/dist/{index-413xk7ga.js → index-cq9q73nf.js} +50 -1
  39. package/dist/{index-6taryy00.js → index-dafax5md.js} +1 -1
  40. package/dist/{index-8mxadm34.js → index-nymn7wbg.js} +1092 -19
  41. package/dist/{index-y91zd0ch.js → index-pr0qsmjy.js} +3 -2
  42. package/dist/index-v5bayx1z.js +138 -0
  43. package/dist/{index-mfw1pec7.js → index-wqyrvhzz.js} +3 -3
  44. package/dist/{index-svqhcrrj.js → index-zd4v31v2.js} +156 -10
  45. package/dist/{index-x8v8b7rc.js → index-zpyj7hsv.js} +330 -58
  46. package/dist/index.js +270 -105
  47. package/dist/internal/bounded-lines.d.ts +4 -0
  48. package/dist/internal/bounded-lines.d.ts.map +1 -0
  49. package/dist/node.d.ts +1 -0
  50. package/dist/node.d.ts.map +1 -1
  51. package/dist/node.js +11 -8
  52. package/dist/observability/index.js +6 -7
  53. package/dist/remote.js +5 -6
  54. package/dist/server/contract-stream.d.ts +5 -0
  55. package/dist/server/contract-stream.d.ts.map +1 -0
  56. package/dist/server/create.d.ts.map +1 -1
  57. package/dist/server/implement.d.ts.map +1 -1
  58. package/dist/server/index.d.ts +1 -0
  59. package/dist/server/index.d.ts.map +1 -1
  60. package/dist/server/index.js +41 -283
  61. package/dist/server/openapi.d.ts.map +1 -1
  62. package/dist/server/stream.d.ts +3 -1
  63. package/dist/server/stream.d.ts.map +1 -1
  64. package/dist/server/streaming-route.d.ts +2 -0
  65. package/dist/server/streaming-route.d.ts.map +1 -1
  66. package/dist/server/types.d.ts +13 -3
  67. package/dist/server/types.d.ts.map +1 -1
  68. package/dist/server/unix-client-body.d.ts +4 -0
  69. package/dist/server/unix-client-body.d.ts.map +1 -0
  70. package/dist/server/unix-client-bun.d.ts +13 -0
  71. package/dist/server/unix-client-bun.d.ts.map +1 -0
  72. package/dist/server/unix-client-error.d.ts +9 -0
  73. package/dist/server/unix-client-error.d.ts.map +1 -0
  74. package/dist/server/unix-client.d.ts +25 -0
  75. package/dist/server/unix-client.d.ts.map +1 -0
  76. package/dist/testing.js +6 -5
  77. package/dist/tools.js +14 -16
  78. package/llms-full.txt +337 -17
  79. package/package.json +1 -1
  80. package/dist/index-48ffdxgk.js +0 -6
  81. package/dist/index-pzyt11ch.js +0 -127
  82. package/dist/index-tvwcrx2d.js +0 -224
  83. package/dist/index-wd8g8z6e.js +0 -206
@@ -0,0 +1,25 @@
1
+ import type { ClientFetch } from '../browser/transport';
2
+ export interface UnixClientTransportConfig {
3
+ /** Absolute local socket path selected by deployment configuration. */
4
+ socketPath: string;
5
+ maxRequestBytes?: number;
6
+ maxResponseBytes?: number;
7
+ headersTimeoutMs?: number;
8
+ maxHeaderBytes?: number;
9
+ maxConnections?: number;
10
+ /** Redirects stay on this Unix transport. Default 5. */
11
+ maxRedirects?: number;
12
+ }
13
+ export interface UnixClientTransport {
14
+ readonly fetch: ClientFetch;
15
+ readonly closed: boolean;
16
+ close(): Promise<void>;
17
+ }
18
+ /**
19
+ * A Fetch-compatible Unix transport shared by Bun and Node. Socket selection is
20
+ * structural: every request, including redirects, is dispatched through the
21
+ * configured path and can never fall back to TCP.
22
+ */
23
+ export declare function createUnixClientTransport(config: UnixClientTransportConfig): UnixClientTransport;
24
+ export { type UnixClientDeliveryState, UnixClientTransportError, type UnixClientTransportErrorCode, } from './unix-client-error';
25
+ //# sourceMappingURL=unix-client.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"unix-client.d.ts","sourceRoot":"","sources":["../../src/server/unix-client.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,sBAAsB,CAAC;AAWxD,MAAM,WAAW,yBAAyB;IACxC,uEAAuE;IACvE,UAAU,EAAE,MAAM,CAAC;IACnB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,wDAAwD;IACxD,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AAED,MAAM,WAAW,mBAAmB;IAClC,QAAQ,CAAC,KAAK,EAAE,WAAW,CAAC;IAC5B,QAAQ,CAAC,MAAM,EAAE,OAAO,CAAC;IACzB,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CACxB;AAoED;;;;GAIG;AACH,wBAAgB,yBAAyB,CACvC,MAAM,EAAE,yBAAyB,GAChC,mBAAmB,CAsPrB;AAED,OAAO,EACL,KAAK,uBAAuB,EAC5B,wBAAwB,EACxB,KAAK,4BAA4B,GAClC,MAAM,qBAAqB,CAAC"}
package/dist/testing.js CHANGED
@@ -4,7 +4,7 @@ import {
4
4
  } from "./index-fhsmrzj7.js";
5
5
  import {
6
6
  createApplication
7
- } from "./index-physbf4z.js";
7
+ } from "./index-6wzr93cg.js";
8
8
  import"./index-2k4yrqkc.js";
9
9
  import"./index-8eywc9zv.js";
10
10
  import {
@@ -25,15 +25,16 @@ import {
25
25
  import {
26
26
  createClient,
27
27
  createClients
28
- } from "./index-svqhcrrj.js";
29
- import"./index-48ffdxgk.js";
28
+ } from "./index-zd4v31v2.js";
29
+ import"./index-v5bayx1z.js";
30
30
  import {
31
31
  joinRoutePath
32
- } from "./index-pzyt11ch.js";
32
+ } from "./index-9ky9hhg3.js";
33
33
  import {
34
34
  isRecord
35
35
  } from "./index-smpbdg6k.js";
36
- import"./index-xxye8j3k.js";
36
+ import"./index-0w9abg87.js";
37
+ import"./index-6k1937bx.js";
37
38
 
38
39
  // src/agent-runtime/testing.ts
39
40
  function createAgentRaceBarrier(timeoutMs = 5000) {
package/dist/tools.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  signJwt,
3
3
  verifyPkce
4
- } from "./index-mfw1pec7.js";
4
+ } from "./index-wqyrvhzz.js";
5
5
  import {
6
6
  DEFAULT_CORS_ALLOW_HEADERS,
7
7
  DEFAULT_PROCESS_SIGNALS,
@@ -9,7 +9,7 @@ import {
9
9
  defaultSignalSource,
10
10
  guardSignalCallback,
11
11
  reportSignalError
12
- } from "./index-y91zd0ch.js";
12
+ } from "./index-pr0qsmjy.js";
13
13
  import {
14
14
  WaitTimeoutError,
15
15
  collectToolSurface,
@@ -24,7 +24,7 @@ import {
24
24
  formatToolError,
25
25
  readCapped,
26
26
  runWaitOperation
27
- } from "./index-1pgeyyee.js";
27
+ } from "./index-7pn02bgv.js";
28
28
  import {
29
29
  ToolExecutionControlError,
30
30
  coerceJsonArgs,
@@ -32,7 +32,7 @@ import {
32
32
  isToolExecutionControlError,
33
33
  toolErrorFromResult,
34
34
  toolResultFromError
35
- } from "./index-wrhf06ak.js";
35
+ } from "./index-avjd67h6.js";
36
36
  import {
37
37
  ManagedFileError
38
38
  } from "./index-bfcpjw20.js";
@@ -52,30 +52,28 @@ import"./index-6djpbnda.js";
52
52
  import"./index-cby4ar3v.js";
53
53
  import {
54
54
  redact
55
- } from "./index-6taryy00.js";
55
+ } from "./index-dafax5md.js";
56
56
  import {
57
57
  getRequestContext,
58
58
  getTraceId,
59
- normalizeError,
60
59
  runWithRequestContext
61
- } from "./index-tvwcrx2d.js";
62
- import {
63
- defineContract
64
- } from "./index-wd8g8z6e.js";
65
- import {
66
- ManagedFilePathSchema,
67
- ManagedFileRefSchema
68
- } from "./index-6k1937bx.js";
60
+ } from "./index-69m2278y.js";
69
61
  import {
62
+ defineContract,
63
+ normalizeError,
70
64
  resolvePropagationContext
71
- } from "./index-pzyt11ch.js";
65
+ } from "./index-9ky9hhg3.js";
72
66
  import {
73
67
  isRecord
74
68
  } from "./index-smpbdg6k.js";
75
69
  import {
76
70
  AppError,
77
71
  STITCH_ERROR_STATUS
78
- } from "./index-xxye8j3k.js";
72
+ } from "./index-0w9abg87.js";
73
+ import {
74
+ ManagedFilePathSchema,
75
+ ManagedFileRefSchema
76
+ } from "./index-6k1937bx.js";
79
77
 
80
78
  // src/tools/agent.ts
81
79
  import { jsonSchema, tool } from "ai";
package/llms-full.txt CHANGED
@@ -58,9 +58,9 @@ own, recorded as an ADR.
58
58
  | `stitchkit/testing` | tests on Bun or Node | stable | in-process generated clients over a real Fetch handler, plus the store and managed-resource conformance kits |
59
59
  | `stitchkit/declaration` | build and deployment tooling (Bun or Node) | evolving | `ProjectDeclarationSchema` — the one machine-readable statement a repository makes about itself |
60
60
  | `stitchkit/react` | browser | stable | `createCursorQuery`, `createCacheBridge` |
61
- | `stitchkit/agent-runtime` | server | evolving<br>_redefined in 9 of the 12 minors since 0.56.2, most recently 0.66.0_ | optional durable conversation/run loop, history, models, prompts, fencing and events |
61
+ | `stitchkit/agent-runtime` | server | evolving<br>_redefined in 9 of the 13 minors since 0.56.2, most recently 0.66.0_ | optional durable conversation/run loop, history, models, prompts, fencing and events |
62
62
  | `stitchkit/agent-runtime/openrouter` | server | evolving | isolated OpenRouter language-model adapter |
63
- | `stitchkit/application` | server | evolving<br>_redefined in 3 of the 12 minors since 0.56.2, most recently 0.67.0_ | managed resource graph, readiness, admission, schedules and bounded shutdown |
63
+ | `stitchkit/application` | server | evolving<br>_redefined in 3 of the 13 minors since 0.56.2, most recently 0.67.0_ | managed resource graph, readiness, admission, schedules and bounded shutdown |
64
64
  | `stitchkit/application/grammy` | server | evolving | isolated grammY polling and webhook lifecycle adapters |
65
65
  | `stitchkit/application/opentelemetry` | server | evolving | maps application snapshots onto an injected OpenTelemetry `Meter` |
66
66
 
@@ -1521,6 +1521,57 @@ focused helper — not a sub-framework.
1521
1521
  | `createCache()` + `cacheHeaders()` | in-memory TTL cache; `Cache-Control` builder |
1522
1522
  | `createEventBus<EventMap>()` | typed in-process pub/sub |
1523
1523
 
1524
+ ### Contract-first streams
1525
+
1526
+ Use an endpoint `stream` descriptor when the item schema and completion belong
1527
+ to the contract, rather than handing an application-owned `Response` to
1528
+ `rawResponse`:
1529
+
1530
+ ```ts
1531
+ const Item = z.discriminatedUnion('kind', [
1532
+ z.object({ kind: z.literal('line'), text: z.string() }),
1533
+ z.object({ kind: z.literal('complete'), count: z.number().int() }),
1534
+ ])
1535
+
1536
+ const reports = defineContract({ prefix: 'reports' }, {
1537
+ watch: {
1538
+ method: 'GET', path: '/:id/watch', desc: 'Watch one report',
1539
+ params: z.object({ id: z.string() }),
1540
+ stream: {
1541
+ item: Item,
1542
+ format: 'ndjson',
1543
+ maxFrameBytes: 64 * 1024,
1544
+ terminal: z.object({ kind: z.literal('complete') }).loose(),
1545
+ },
1546
+ },
1547
+ })
1548
+
1549
+ const service = implement(reports, {
1550
+ watch: async function* ({ params, signal }) {
1551
+ yield { kind: 'line', text: `starting ${params.id}` }
1552
+ if (signal.aborted) return
1553
+ yield { kind: 'complete', count: 1 }
1554
+ },
1555
+ })
1556
+ ```
1557
+
1558
+ The handler return is inferred as `AsyncIterable<z.output<typeof Item>>`; an
1559
+ invalid JavaScript producer is rejected before its value reaches the wire. Data,
1560
+ safe errors and normal end use a framework envelope. The default encoded frame
1561
+ limit is 256 KiB. `format` defaults to `ndjson`; `heartbeatMs` defaults to five
1562
+ seconds; `idleTimeoutSeconds` defaults to disabled; `lifetimeMs` is optional.
1563
+
1564
+ After headers, HTTP status can no longer report failure, so the stream sends a
1565
+ normalized code and never the raw internal exception. Normal producer completion
1566
+ sends an explicit `end`; when `terminal` is declared, at least one item must
1567
+ match it. Request abort and client iterator return abort `signal`, including a
1568
+ source waiting in `next()` or suspended at a yielded item.
1569
+
1570
+ Contract streams are HTTP-only and cannot declare `output`, `rawResponse`,
1571
+ multipart or tool exposure. They do not provide replay, cursors or durable
1572
+ subscriptions. Keep using `streamingRoute` for an application-owned protocol and
1573
+ `rawResponse` for arbitrary response bodies. → ADR 0117.
1574
+
1524
1575
  ### SSE streaming
1525
1576
 
1526
1577
  `streamSSE` returns a `Response`, so its endpoint declares
@@ -1834,23 +1885,46 @@ your own `(pathname) => boolean`.
1834
1885
 
1835
1886
  ### Unix domain sockets
1836
1887
 
1837
- The same typed client dials a local daemon's socket file
1838
- ([server side](server.md#local-daemon-over-a-unix-socket)):
1888
+ The same typed client dials a local daemon's socket file through an explicit,
1889
+ owned Bun/Node transport ([server side](server.md#local-daemon-over-a-unix-socket)):
1839
1890
 
1840
1891
  ```ts
1892
+ import { createHttpClient } from 'stitchkit'
1893
+ import { createUnixClientTransport } from 'stitchkit/server' // or stitchkit/node
1894
+
1895
+ const transport = createUnixClientTransport({
1896
+ socketPath: '/run/my-daemon.sock',
1897
+ maxRequestBytes: 4 * 1024 * 1024,
1898
+ maxResponseBytes: 16 * 1024 * 1024,
1899
+ })
1841
1900
  const http = createHttpClient({
1842
- baseUrl: 'http://localhost', // required prefix source; its host is ignored
1843
- unix: '/run/my-daemon.sock',
1901
+ baseUrl: 'http://my-daemon', // URL/Host source; never dialled as TCP
1902
+ fetch: transport.fetch,
1903
+ retry: { limit: 0 },
1844
1904
  })
1845
1905
  const daemon = createClient(daemonContract, http)
1906
+
1907
+ // At application shutdown:
1908
+ await transport.close()
1846
1909
  ```
1847
1910
 
1848
- `baseUrl` stays required it supplies the path prefix and the `Host` header,
1849
- while the connection itself goes through the socket file. Bun runtime only:
1850
- other runtimes ignore the option and dial `baseUrl` over TCP (Node's fetch
1851
- would need an undici dispatcher out of scope). A missing socket file
1852
- surfaces as a normal `ApiError` and is not retried (transport retry stays
1853
- connection-refused-only).
1911
+ `baseUrl` stays required because it supplies the URL and `Host` header. The
1912
+ adapter structurally owns dispatch: relative and absolute redirects use the same
1913
+ socket, and a missing socket cannot fall through to that host over TCP. Defaults
1914
+ are 16 MiB request/response bodies, 64 KiB headers, 30 s to response headers,
1915
+ eight connections and five redirects.
1916
+
1917
+ `UnixClientTransportError` carries a stable `code` and `delivery`:
1918
+ `not-dispatched`, `possibly-dispatched` or `response-received`. Only the first
1919
+ proves that the remote operation did not begin; Stitchkit never silently retries
1920
+ an ambiguous write. Response consumption/cancellation belongs to the operation,
1921
+ and `close()` interrupts active work and destroys owned connections.
1922
+
1923
+ The legacy `createHttpClient({ unix: '/absolute/path' })` spelling remains a
1924
+ Bun-only convenience. On a non-Bun runtime it now refuses before dispatch
1925
+ instead of ignoring the selection and dialing TCP. `unix` and an injected
1926
+ `fetch` are mutually exclusive; use `createUnixClientTransport` for portable,
1927
+ explicit lifecycle ownership. → ADR 0116.
1854
1928
 
1855
1929
  `trace: true` mints a fresh root trace per request. The stitchkit server
1856
1930
  [continues an inbound `traceparent`](./observability.md#trace-context), so the
@@ -2218,6 +2292,41 @@ sends one. The result keeps the full `react-query-kit` surface (`.getKey()`,
2218
2292
  `useSuspenseInfiniteQuery`, every option). The endpoint must return the
2219
2293
  `{ items, nextCursor }` envelope — see [Contracts → pagination](./contracts.md#pagination).
2220
2294
 
2295
+ ## Contract-first streams
2296
+
2297
+ When an endpoint declares `stream`, `createClient` returns a schema-derived
2298
+ owned iterator rather than an untyped `Response`:
2299
+
2300
+ ```ts
2301
+ const Progress = z.discriminatedUnion('kind', [
2302
+ z.object({ kind: z.literal('line'), text: z.string() }),
2303
+ z.object({ kind: z.literal('complete'), count: z.number().int() }),
2304
+ ])
2305
+
2306
+ const contract = defineContract({ prefix: 'reports' }, {
2307
+ watch: {
2308
+ method: 'GET', path: '/:id/watch', desc: 'Watch one report',
2309
+ params: z.object({ id: z.string() }),
2310
+ stream: {
2311
+ item: Progress,
2312
+ format: 'ndjson', // default; `sse` is also supported
2313
+ maxFrameBytes: 64 * 1024, // default 256 KiB
2314
+ terminal: z.object({ kind: z.literal('complete') }).loose(),
2315
+ },
2316
+ },
2317
+ })
2318
+
2319
+ const stream = await createClient(contract, http).watch({ id: 'r-1' })
2320
+ for await (const item of stream) console.log(item) // inferred from Progress
2321
+ ```
2322
+
2323
+ The iterator validates every frame and item. Normal completion requires the
2324
+ wire `end` frame and, when declared, at least one matching terminal item; EOF is
2325
+ `STREAM_TRUNCATED`, and a missing terminal is `STREAM_TERMINAL_MISSING`.
2326
+ `return()`/`break`, caller abort, producer failure and optional `lifetimeMs`
2327
+ converge on the request operation. See the
2328
+ [server half](./server.md#contract-first-streams). → ADR 0117.
2329
+
2221
2330
  ## SSE
2222
2331
 
2223
2332
  For a streaming endpoint, consume the response with `parseSSE`:
@@ -2233,7 +2342,9 @@ for await (const event of parseSSE(res)) {
2233
2342
 
2234
2343
  The server side is [`streamSSE`](./server.md#sse-streaming), or
2235
2344
  [`sseRoute`](./server.md#long-lived-subscriptions) for a subscription that stays
2236
- open.
2345
+ open. One line is bounded by `maxLineBytes` (default 1 MiB), UTF-8 is decoded
2346
+ strictly and malformed input throws. Supply `onParseError` only when
2347
+ skip-and-report is an explicit application policy.
2237
2348
 
2238
2349
  ## NDJSON
2239
2350
 
@@ -2265,8 +2376,9 @@ convenience: a long-lived stream must send something while it is idle or
2265
2376
  intermediaries drop it, and an empty line is the natural pulse for this framing.
2266
2377
  Writing the rule down on both sides is what stops it being a verbal agreement —
2267
2378
  the server's keep-alive and the reader's skip are one decision with two
2268
- implementations. A frame that is not valid JSON goes to `onParseError` rather
2269
- than throwing, so one bad line does not end the subscription.
2379
+ implementations. One line is bounded by `maxLineBytes` (default 1 MiB), UTF-8 is
2380
+ decoded strictly and malformed input throws. Passing `onParseError` explicitly
2381
+ selects tolerant skip-and-report behaviour.
2270
2382
 
2271
2383
 
2272
2384
  ==============================================================================
@@ -4530,6 +4642,69 @@ try {
4530
4642
  `release()` is idempotent. Admission and counter increment are atomic, so work
4531
4643
  cannot slip between the shutdown check and drain accounting.
4532
4644
 
4645
+ ### Bounded operation admission
4646
+
4647
+ Compose `createBoundedAdmission` when accepted work also competes for a finite
4648
+ process-local resource:
4649
+
4650
+ ```ts
4651
+ const generations = createBoundedAdmission({
4652
+ upstream: app.admission,
4653
+ policy: {
4654
+ global: { maxConcurrent: 8, rate: { limit: 120, intervalMs: 60_000 } },
4655
+ perKey: { maxConcurrent: 1, maxKeys: 2_000 },
4656
+ },
4657
+ })
4658
+
4659
+ await generations.run(accountId, ({ signal }) => generate({ signal }), {
4660
+ signal: request.signal,
4661
+ timeoutMs: 30_000,
4662
+ })
4663
+ ```
4664
+
4665
+ Acquisition is no-queue and atomic across every configured budget. Refusal names
4666
+ the exact bound; only a rate refusal carries `retryAfterMs`. `maxKeys` keeps the
4667
+ per-key registry finite, and expired idle entries are retired.
4668
+
4669
+ The caller timeout is a wait budget, not proof that the resource stopped. It
4670
+ aborts the signal and settles the caller, but the lease remains active until the
4671
+ underlying Promise actually settles. `drain()` therefore reports real work;
4672
+ `force()` closes admission and reports remaining work without claiming to have
4673
+ terminated it. → ADR 0118.
4674
+
4675
+ ### Bounded delivery and byte credit
4676
+
4677
+ `createBoundedChannel` is for one asynchronous reader when an event bus is not a
4678
+ queue:
4679
+
4680
+ ```ts
4681
+ const output = createBoundedChannel<string>({
4682
+ policy: 'ordered',
4683
+ maxItems: 64,
4684
+ maxBytes: 256 * 1024,
4685
+ sizeOf: (value) => new TextEncoder().encode(value).byteLength,
4686
+ })
4687
+
4688
+ const progress = createBoundedChannel<{ revision: number }>({
4689
+ policy: 'latest',
4690
+ maxItems: 1,
4691
+ maxBytes: 128,
4692
+ sizeOf: () => 128,
4693
+ })
4694
+ ```
4695
+
4696
+ `ordered` never overwrites accepted values; overflow is a reasoned refusal.
4697
+ `latest` retains exactly one pending replaceable value and reports
4698
+ `coalesced`. Offers never create a hidden writer queue, and only one `next()` may
4699
+ wait. Close chooses `drain` (default) or `discard`; abort discards; failure
4700
+ rejects the parked and all later reads.
4701
+
4702
+ `createCreditWindow({ capacityBytes })` is the smaller primitive for a protocol
4703
+ that already owns its queue but needs exact byte permission. Each credit lease
4704
+ replenishes once; it is flow-control credit, not a durable acknowledgement. The
4705
+ application snapshot sink now shares the same latest-value mechanics without
4706
+ changing its revision or status contract. → ADR 0119.
4707
+
4533
4708
  Shutdown performs one phase barrier at a time: stop admission everywhere,
4534
4709
  cancel future schedules, drain admitted work, then close in reverse stable
4535
4710
  topological order. Every hook shares the same grace deadline. Forced cleanup
@@ -4804,6 +4979,56 @@ asynchronous subscriber callback racing cleanup: publish `getSnapshot()` before
4804
4979
  closing the outer sink, so any older or duplicate late delivery is rejected as
4805
4980
  stale instead of dropping the final state.
4806
4981
 
4982
+ ## Bound a handler and a local worker with one lease policy
4983
+
4984
+ Use a bounded admission when two entry paths consume the same finite local
4985
+ capacity. Composing it with `application.admission` keeps readiness and shutdown
4986
+ as the upstream gate:
4987
+
4988
+ ```ts
4989
+ const work = createBoundedAdmission({
4990
+ upstream: app.admission,
4991
+ policy: {
4992
+ global: { maxConcurrent: 4 },
4993
+ perKey: { maxConcurrent: 1, maxKeys: 1_000 },
4994
+ },
4995
+ })
4996
+
4997
+ const fromHttp = (key: string, signal: AbortSignal) =>
4998
+ work.run(key, (context) => render(context.signal), { signal, timeoutMs: 20_000 })
4999
+
5000
+ const fromWorker = (key: string) =>
5001
+ work.run(key, (context) => reconcile(context.signal))
5002
+ ```
5003
+
5004
+ A timeout in `fromHttp` does not free a permit while `render` remains active.
5005
+ At shutdown call `work.stopAdmission()` with the other admission owners and
5006
+ await `work.drain(...)`; the result reports the actual remainder.
5007
+
5008
+ ## Replace ad-hoc output and progress queues
5009
+
5010
+ Ordered output and replaceable progress are separate declarations:
5011
+
5012
+ ```ts
5013
+ const lines = createBoundedChannel<string>({
5014
+ policy: 'ordered', maxItems: 100, maxBytes: 1_000_000,
5015
+ sizeOf: (line) => new TextEncoder().encode(line).byteLength,
5016
+ })
5017
+
5018
+ const state = createBoundedChannel<{ revision: number; percent: number }>({
5019
+ policy: 'latest', maxItems: 1, maxBytes: 64,
5020
+ sizeOf: () => 64,
5021
+ })
5022
+
5023
+ const lineResult = lines.offer('one durable-in-process ordering unit')
5024
+ const stateResult = state.offer({ revision: 2, percent: 50 })
5025
+ ```
5026
+
5027
+ Handle `refused` from `lines` at the protocol boundary; do not turn it into
5028
+ implicit loss. A `coalesced` state result is expected latest-value behaviour,
5029
+ not evidence that an ordered event was delivered. Neither channel is durable;
5030
+ persist first when restart replay is required.
5031
+
4807
5032
  ## Handing a handle to the resources that depend on it
4808
5033
 
4809
5034
  `dependsOn` carries ordering. To carry the object as well, return a `value` from
@@ -8038,6 +8263,56 @@ implement `AgentRuntimeStoreDriver` and compose the aggregate with
8038
8263
  runtime): bootstrap the server, one HTTP request, and any feature you rely on
8039
8264
  (Socket.IO connect, an MCP tool call, a multipart upload, …).
8040
8265
 
8266
+ ## Released migration: 0.68.0
8267
+
8268
+ ### Make Unix transport selection explicit outside Bun
8269
+
8270
+ The legacy `unix` option remains a Bun convenience, but on Node or another
8271
+ runtime it now fails before dispatch. Replace it with the owned adapter and close
8272
+ that adapter with the application:
8273
+
8274
+ ```ts
8275
+ // before — unsafe outside Bun: an unsupported fetch could dial baseUrl over TCP
8276
+ const http = createHttpClient({ baseUrl, unix: '/run/service.sock' })
8277
+
8278
+ // after — Bun and Node; every dispatch and redirect stays on the socket
8279
+ import { createUnixClientTransport } from 'stitchkit/server' // or stitchkit/node
8280
+ const transport = createUnixClientTransport({ socketPath: '/run/service.sock' })
8281
+ const http = createHttpClient({ baseUrl, fetch: transport.fetch })
8282
+ // during shutdown
8283
+ await transport.close()
8284
+ ```
8285
+
8286
+ Do not automatically replay `possibly-dispatched`: a timeout or connection loss
8287
+ after bytes left the process does not prove that a write did not happen.
8288
+
8289
+ ### Choose tolerant stream parsing explicitly
8290
+
8291
+ `parseNDJSON` and `parseSSE` now throw on malformed JSON, invalid UTF-8 and an
8292
+ over-limit line. The default line ceiling is 1 MiB. If a feed deliberately skips
8293
+ bad records, retain that policy explicitly:
8294
+
8295
+ ```ts
8296
+ // before — malformed input disappeared implicitly
8297
+ parseNDJSON(response)
8298
+
8299
+ // after — ordinary fail-closed path
8300
+ parseNDJSON(response, { maxLineBytes: 256 * 1024 })
8301
+
8302
+ // after — deliberately tolerant path
8303
+ parseNDJSON(response, { onParseError: (raw, error) => report(raw, error) })
8304
+ ```
8305
+
8306
+ ### Extend exhaustive framework-error handling
8307
+
8308
+ If a switch makes `StitchErrorCode` exhaustive, add
8309
+ `STREAM_ITEM_INVALID`, `STREAM_FRAME_TOO_LARGE`,
8310
+ `STREAM_TERMINAL_MISSING` and `STREAM_LIFETIME_EXCEEDED`. A partial application
8311
+ status map needs no change (ADR 0105).
8312
+
8313
+ The admission/channel APIs and endpoint `stream` descriptor are additive; raw
8314
+ responses and raw `streamingRoute` remain supported.
8315
+
8041
8316
  ## Released migration: 0.67.0
8042
8317
 
8043
8318
  Three application-kernel changes. Two of them fix silent failures, so the most
@@ -10431,7 +10706,7 @@ The browser-and-server entrypoint. Re-exports everything from
10431
10706
  | `createUrlBuilders` | function | build one exact URL builder per contract in a registry |
10432
10707
  | `UrlBuilderConfig` | _type_ | explicit `{ baseUrl }` source for a URL builder |
10433
10708
  | `ClientConfig` | _type_ | config for `createClient`'s bare-fetch mode (2nd arg, no `HttpClient`) |
10434
- | `ClientFetch` | _type_ | injectable Fetch-compatible transport used by framework testing adapters |
10709
+ | `ClientFetch` | _type_ | injectable Fetch-compatible transport used by framework and application-owned adapters |
10435
10710
  | `ClientRequestOptions` | _type_ | per-call `{ signal?: AbortSignal }` passed through an endpoint callable's `.withOptions(...)`; caller abort is distinct from timeout — [guide](../guide/client.md#per-call-cancellation) |
10436
10711
  | `ContractClientConfig` | _type_ | per-tenant / resource-scoped client config — dynamic `pathPrefix` + `stripPrefixKeys` ([guide](../guide/client.md#contractclientconfig--per-tenant--resource-scoped-clients)) |
10437
10712
  | `contractEndpointMatchers` | function | compile exact pathname matchers for selected HTTP contract operations and expected-401 policy |
@@ -10440,7 +10715,7 @@ The browser-and-server entrypoint. Re-exports everything from
10440
10715
  | `ApiError` | class | a non-2xx response, with `code` / `status` / `details` / `hint` and optional readonly `traceId` from `x-request-id` |
10441
10716
  | `HttpClient` | _type_ | the transport interface `createClient` builds on |
10442
10717
  | `ConfiguredHttpClient` | _type_ | a framework-created `HttpClient` carrying its readonly `baseUrl` for URL builders |
10443
- | `HttpClientConfig` | _type_ | config for `createHttpClient`; retry `limit` counts retries after the initial attempt (default 2 = at most 3 GET attempts), with `statusCodes: []` by default; `unix` dials a unix domain socket (Bun only) — [details](../guide/client.md#createhttpclient) |
10718
+ | `HttpClientConfig` | _type_ | config for `createHttpClient`; retry `limit` counts retries after the initial attempt (default 2 = at most 3 GET attempts), with `statusCodes: []` by default; `fetch` installs an explicit transport and is mutually exclusive with the legacy Bun-only `unix` option — [details](../guide/client.md#createhttpclient) |
10444
10719
  | `UnauthorizedMatcher` | _type_ | exact `(pathname) => boolean` policy accepted by `suppressUnauthorizedFor` |
10445
10720
  | `RequestOptions` | _type_ | per-call options — params, timeout, response type |
10446
10721
  | `HeaderProvider` | _type_ | static or per-request headers |
@@ -10458,6 +10733,8 @@ The browser-and-server entrypoint. Re-exports everything from
10458
10733
  | `createRetainedTopics` | function | retained last-value store for sticky events — [guide](../guide/realtime.md#sticky-events) |
10459
10734
  | `parseSSE` | function | parse an SSE `Response` into an async generator — [guide](../guide/client.md#sse) |
10460
10735
  | `parseNDJSON` | function | parse an NDJSON `Response`; blank keep-alive lines are skipped — [guide](../guide/client.md#ndjson) |
10736
+ | `ContractStreamFrameSchema` / `ContractStreamFrame` | schema / _type_ | internal-on-the-wire `data` / safe `error` / `end` envelope of a contract-first stream |
10737
+ | `DEFAULT_CONTRACT_STREAM_FRAME_BYTES` | const | default maximum encoded contract-stream frame: 256 KiB |
10461
10738
  | `SocketIOClient` | _type_ | low-level client handle; `emit` reports disconnected drops and `emitWithAck` exposes the native Promise primitive used by validated `request()` |
10462
10739
  | `SocketIOClientPeerLoaders` | _type_ | inject `socket.io-client` so a bundler can put it in a self-contained artifact |
10463
10740
  | `SocketIOClientConfig` | _type_ | config for `createSocketIOClient` (incl. `retain`, `onConnectError`, `onDroppedEmit`) |
@@ -10533,6 +10810,7 @@ from the root `stitchkit`.
10533
10810
  | `ContractDef` | _type_ | a defined contract |
10534
10811
  | `ContractMeta` | _type_ | a contract's `prefix` + optional `scope` and `meta` (a default every endpoint shallow-merges over) |
10535
10812
  | `EndpointDef` | _type_ | a single endpoint definition; `output` declares JSON response presence (`null` is data, `undefined` is invalid) |
10813
+ | `EndpointStreamDescriptor` | _type_ | HTTP-only schema-derived stream declaration: item schema, NDJSON/SSE framing, frame/lifetime/heartbeat/idle bounds and optional terminal predicate — [guide](../guide/server.md#contract-first-streams) |
10536
10814
  | `HeadEndpointDef` | _type_ | explicit HTTP-only, bodyless `HEAD` endpoint definition |
10537
10815
  | `EndpointResponseMeta` | _type_ | static success metadata declared by an HTTP-only typed-data endpoint |
10538
10816
  | `ResponseMetadata` | _type_ | per-request outbound collector exposed as `ctx.response` only for a `responseMeta` endpoint |
@@ -10791,6 +11069,12 @@ Also re-exports the error helpers from `stitchkit/contract`.
10791
11069
  | `StreamingRouteOptions` | _type_ | options for `streamingRoute` / `ndjsonRoute` / `sseRoute` |
10792
11070
  | `StreamingSourceContext` | _type_ | what a streaming source is given, including the cancellation `signal` |
10793
11071
  | `StreamingFormat` | _type_ | `'ndjson' \| 'sse'` |
11072
+ | `createUnixClientTransport` | function | owned Fetch-compatible Unix-socket transport on Bun and Node; every redirect stays on the socket — [guide](../guide/client.md#unix-domain-sockets) |
11073
+ | `UnixClientTransportConfig` | _type_ | absolute socket path plus request/response/header/connection/redirect bounds |
11074
+ | `UnixClientTransport` | _type_ | `{ fetch, closed, close() }`; `close()` settles owned active work |
11075
+ | `UnixClientTransportError` | class | stable transport failure with `code` and dispatch certainty in `delivery` |
11076
+ | `UnixClientTransportErrorCode` | _type_ | finite Unix transport failure-code union |
11077
+ | `UnixClientDeliveryState` | _type_ | `not-dispatched \| possibly-dispatched \| response-received`; input to application retry policy, never an implicit retry |
10794
11078
 
10795
11079
  ### OpenAPI
10796
11080
 
@@ -10840,6 +11124,39 @@ cutovers are covered by the executable
10840
11124
  | `ApplicationHealthHandlerOptions` / `ApplicationHealthHandlerOptionsSchema` | _type_ / schema | liveness/readiness selection and sanitized `Retry-After` policy |
10841
11125
  | `ApplicationOperationalHandlers` / `ApplicationOperationalHandlersOptions` / `ApplicationOperationalHandlersOptionsSchema` | _type_ / schema | conventional status/readiness/liveness route surface and shared retry policy |
10842
11126
 
11127
+ ### Bounded admission
11128
+
11129
+ | Export | Kind | Summary |
11130
+ |--------|------|---------|
11131
+ | `createBoundedAdmission` | function | process-local no-queue global/per-key concurrency and rate leases, optionally composed with application admission |
11132
+ | `BoundedAdmissionPolicySchema` / `BoundedAdmissionPolicy` | schema / _type_ | finite global budget and optional finite per-key budget with `maxKeys` |
11133
+ | `BoundedRateBudgetSchema` / `BoundedRateBudget` | schema / _type_ | `{ limit, intervalMs }` monotonic sliding-window budget |
11134
+ | `BoundedAdmissionStateSchema` / `BoundedAdmissionState` | schema / _type_ | `accepting \| draining \| closed` |
11135
+ | `BoundedAdmissionRefusalReasonSchema` / `BoundedAdmissionRefusalReason` | schema / _type_ | exact local/upstream refusal vocabulary |
11136
+ | `BoundedAdmissionSnapshotSchema` / `BoundedAdmissionSnapshot` | schema / _type_ | absolute active/lifetime/refusal/rate-accounting counters |
11137
+ | `BoundedAdmission` / `BoundedAdmissionConfig` / `BoundedAdmissionClock` | _type_ | handle, policy/upstream/clock configuration and monotonic clock seam |
11138
+ | `BoundedAdmissionResult` / `BoundedAdmissionLeaseResult` / `BoundedAdmissionRefusedResult` | _type_ | explicit leased-or-refused acquisition result; retry time exists only for rate bounds |
11139
+ | `BoundedOperationLease` | _type_ | idempotent release lease, optionally carrying its key |
11140
+ | `BoundedOperationRunContext` / `BoundedOperationRunOptions` | _type_ | underlying work signal and caller abort/timeout wait budget |
11141
+ | `BoundedAdmissionDrainOptions` / `BoundedAdmissionDrainResult` / `BoundedAdmissionForceResult` | _type_ | bounded drain inputs and honest remaining-work results |
11142
+ | `BoundedAdmissionRefusalError` | class | `run()` refusal with reason and optional `retryAfterMs` |
11143
+ | `BoundedOperationWaitError` | class | caller wait ended as `cancelled` or `timed-out`; underlying capacity remains leased until work settles |
11144
+
11145
+ ### Bounded delivery
11146
+
11147
+ | Export | Kind | Summary |
11148
+ |--------|------|---------|
11149
+ | `createBoundedChannel` | function | finite single-reader async channel with explicit ordered or latest-value policy |
11150
+ | `BoundedChannelPolicySchema` / `BoundedChannelPolicy` | schema / _type_ | `ordered \| latest` retention policy |
11151
+ | `BoundedChannelStateSchema` / `BoundedChannelState` | schema / _type_ | `open \| draining \| closed \| failed` |
11152
+ | `BoundedChannelSnapshotSchema` / `BoundedChannelSnapshot` | schema / _type_ | exact retained count/bytes, waiter and outcome counters |
11153
+ | `BoundedChannel` / `BoundedChannelConfig` / `BoundedChannelCloseOptions` | _type_ | iterator/offer handle, explicit count/byte/size policy and drain/discard close mode |
11154
+ | `BoundedChannelOfferResult` | _type_ | `delivered`, `queued`, `coalesced`, or reasoned `refused` outcome |
11155
+ | `BoundedChannelReaderError` | class | refusal of a second concurrent pending `next()` |
11156
+ | `createCreditWindow` | function | finite byte-credit lease window with exact once-only replenishment |
11157
+ | `CreditWindow` / `CreditWindowSnapshot` / `CreditWindowSnapshotSchema` | _type_ / schema | byte-credit handle and absolute accounting record |
11158
+ | `CreditAcquireResult` / `CreditLease` | _type_ | reasoned refusal or idempotently releasable byte-credit lease |
11159
+
10843
11160
  ### Managed schedules
10844
11161
 
10845
11162
  | Export | Kind | Summary |
@@ -11556,6 +11873,7 @@ runtime-agnostic pieces of `stitchkit/server` and the error helpers.
11556
11873
  | `serveNode` | function | build the router and start a Node HTTP server (via `srvx`) |
11557
11874
  | `createHandler` | function | the router as a bare `(req) => Response` (same as `/server`) |
11558
11875
  | `createSocketIOServer` | function | the typed Node Socket.IO server (`io` + `attach`; no Bun engine declarations) |
11876
+ | `createUnixClientTransport` | function | the same fail-closed Bun/Node Unix client adapter exported by `stitchkit/server` |
11559
11877
  | `implement` / `createImplement` / `createScopedImplement` / `createScopedImplementRegistry` / `createMultipartStream` | function | bind a contract to typed handlers, optionally typed per endpoint scope (same as `/server`) |
11560
11878
  | `NodeServerConfig` | _type_ | config for `serveNode` |
11561
11879
  | `NodeServerHandle` | _type_ | managed Node handle (`url`, `port`, `runtime`, `status`, `shutdown`) |
@@ -11563,6 +11881,8 @@ runtime-agnostic pieces of `stitchkit/server` and the error helpers.
11563
11881
  | `NodeSocketLifecycle` | _type_ | Bun-free Socket.IO lifecycle accepted by `serveNode` |
11564
11882
  | `HandlerConfig` / `ServiceDef` / `RawRoute` / `RawRouteContext` | _type_ | runtime-neutral handler types; raw routes default their host server to `unknown` |
11565
11883
  | `SocketIORequestPolicy` / `SocketIOServerConfig` / `SocketIOPeerLoaders` / `SocketIOServerHandle` | _type_ | runtime-neutral handshake policy, config, optional-peer loaders and the Bun-free Node handle with `io`, `attach` and lifecycle |
11884
+ | `UnixClientTransportConfig` / `UnixClientTransport` | _type_ | Unix socket bounds and owned Fetch-compatible handle |
11885
+ | `UnixClientTransportError` / `UnixClientTransportErrorCode` / `UnixClientDeliveryState` | class / _type_ | stable failure plus dispatch certainty; no cross-transport fallback |
11566
11886
  | `AppError` + `appError` / `badRequest` / `unauthorized` / `forbidden` / `notFound` / `conflict` / `rateLimited` | — | error helpers (same as `/contract`) |
11567
11887
 
11568
11888
  ---
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "stitchkit",
3
- "version": "0.67.0",
3
+ "version": "0.68.0",
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",
@@ -1,6 +0,0 @@
1
- // src/internal/http-input.ts
2
- function inputIsQuery(method) {
3
- return method === "GET" || method === "DELETE";
4
- }
5
-
6
- export { inputIsQuery };