stitchkit 0.110.5 → 0.111.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 (100) hide show
  1. package/CHANGELOG.md +43 -0
  2. package/dist/agent-runtime/branch-observations.d.ts +16 -16
  3. package/dist/agent-runtime/sandbox-broker.d.ts.map +1 -1
  4. package/dist/browser/stream.d.ts +3 -2
  5. package/dist/browser/stream.d.ts.map +1 -1
  6. package/dist/contract/runtime-context.d.ts +3 -1
  7. package/dist/contract/runtime-context.d.ts.map +1 -1
  8. package/dist/entrypoints/agent-runtime/coding-tools.js +2 -2
  9. package/dist/entrypoints/agent-runtime/sandbox.js +15 -29
  10. package/dist/entrypoints/agent-runtime/sqlite/bun.js +4 -4
  11. package/dist/entrypoints/agent-runtime/sqlite/node.js +4 -4
  12. package/dist/entrypoints/agent-runtime/testing.js +4 -4
  13. package/dist/entrypoints/agent-runtime.js +4 -4
  14. package/dist/entrypoints/bin/upgrade-cli.d.ts.map +1 -1
  15. package/dist/entrypoints/bin/upgrade-cli.js +123 -29
  16. package/dist/entrypoints/cli/publish.js +1 -1
  17. package/dist/entrypoints/cli.js +2 -2
  18. package/dist/entrypoints/http.d.ts +2 -0
  19. package/dist/entrypoints/http.d.ts.map +1 -1
  20. package/dist/entrypoints/http.js +5 -1
  21. package/dist/entrypoints/index.d.ts +1 -0
  22. package/dist/entrypoints/index.d.ts.map +1 -1
  23. package/dist/entrypoints/index.js +5 -1
  24. package/dist/entrypoints/node.d.ts +1 -0
  25. package/dist/entrypoints/node.d.ts.map +1 -1
  26. package/dist/entrypoints/node.js +3 -1
  27. package/dist/entrypoints/observability.js +24 -9
  28. package/dist/entrypoints/process.js +1 -1
  29. package/dist/entrypoints/remote.js +1 -1
  30. package/dist/entrypoints/server.d.ts +2 -0
  31. package/dist/entrypoints/server.d.ts.map +1 -1
  32. package/dist/entrypoints/server.js +79 -6
  33. package/dist/entrypoints/telegram.js +3 -3
  34. package/dist/entrypoints/testing.js +1 -1
  35. package/dist/entrypoints/tools/connections.js +16 -31
  36. package/dist/entrypoints/tools.js +39 -7
  37. package/dist/{index-mpg1t7nv.js → index-0z3kfphz.js} +1 -1
  38. package/dist/{index-jk44mbqb.js → index-1y0arq4z.js} +3 -57
  39. package/dist/{index-5pbtbce0.js → index-7c957abz.js} +1 -1
  40. package/dist/{index-e1sjcjfy.js → index-7r4knwf8.js} +94 -23
  41. package/dist/index-cjbn98zt.js +68 -0
  42. package/dist/{index-bm180f2d.js → index-ey15wx6j.js} +1 -1
  43. package/dist/{index-s0xbnpfh.js → index-kdnfe0sr.js} +1 -1
  44. package/dist/{index-s6d8zsha.js → index-krng2a5j.js} +64 -110
  45. package/dist/{index-9ket8wzx.js → index-mnk5vgdr.js} +1 -1
  46. package/dist/index-qndsr7f0.js +55 -0
  47. package/dist/{index-n7hrpngg.js → index-syzngt7v.js} +1 -1
  48. package/dist/{index-3h0ztg0z.js → index-t65erk4z.js} +5 -21
  49. package/dist/internal/bounded-body.d.ts +44 -0
  50. package/dist/internal/bounded-body.d.ts.map +1 -0
  51. package/dist/internal/bounded-lines.d.ts +1 -2
  52. package/dist/internal/bounded-lines.d.ts.map +1 -1
  53. package/dist/internal/secure-fetch.d.ts.map +1 -1
  54. package/dist/internal/stream-chunks.d.ts +13 -0
  55. package/dist/internal/stream-chunks.d.ts.map +1 -0
  56. package/dist/internal/upgrade-affects.d.ts +5 -0
  57. package/dist/internal/upgrade-affects.d.ts.map +1 -1
  58. package/dist/internal/upgrade-plan.d.ts +18 -3
  59. package/dist/internal/upgrade-plan.d.ts.map +1 -1
  60. package/dist/internal/upgrade-usage.d.ts +21 -5
  61. package/dist/internal/upgrade-usage.d.ts.map +1 -1
  62. package/dist/observability/audit.d.ts +10 -0
  63. package/dist/observability/audit.d.ts.map +1 -1
  64. package/dist/observability/event.d.ts +5 -0
  65. package/dist/observability/event.d.ts.map +1 -1
  66. package/dist/process/group-diagnostics.d.ts +20 -0
  67. package/dist/process/group-diagnostics.d.ts.map +1 -0
  68. package/dist/process/group-settlement.d.ts +1 -1
  69. package/dist/process/group-settlement.d.ts.map +1 -1
  70. package/dist/process/group.d.ts.map +1 -1
  71. package/dist/server/bun.d.ts +9 -0
  72. package/dist/server/bun.d.ts.map +1 -1
  73. package/dist/server/multipart.d.ts.map +1 -1
  74. package/dist/server/oauth/endpoints.d.ts.map +1 -1
  75. package/dist/server/oauth/provider.d.ts +5 -0
  76. package/dist/server/oauth/provider.d.ts.map +1 -1
  77. package/dist/server/proxy-origin.d.ts +12 -0
  78. package/dist/server/proxy-origin.d.ts.map +1 -0
  79. package/dist/server/raw.d.ts +6 -3
  80. package/dist/server/raw.d.ts.map +1 -1
  81. package/dist/server/request-body.d.ts.map +1 -1
  82. package/dist/server/request-completion.d.ts.map +1 -1
  83. package/dist/server/request-ingress.d.ts +31 -0
  84. package/dist/server/request-ingress.d.ts.map +1 -0
  85. package/dist/server/unix-client-body.d.ts.map +1 -1
  86. package/dist/tools/connections/limits.d.ts.map +1 -1
  87. package/llms/http.txt +3 -0
  88. package/llms/observability.txt +19 -2
  89. package/llms/server.2.txt +135 -0
  90. package/llms/server.5.txt +3 -1
  91. package/llms/server.txt +97 -131
  92. package/llms/stitchkit.2.txt +28 -0
  93. package/llms/stitchkit.txt +3 -3
  94. package/llms/tools.3.txt +1 -1
  95. package/llms/tools.txt +4 -0
  96. package/llms/{upgrading-0.101.0-0.110.0.txt → upgrading-0.101.0-0.111.0.txt} +29 -3
  97. package/llms/upgrading.txt +8 -2
  98. package/llms-full.txt +192 -8
  99. package/llms.txt +6 -6
  100. package/package.json +1 -1
@@ -266,7 +266,7 @@ queryable across all three:
266
266
  | `ok` / `statusCode` | legacy success bit plus real HTTP status, or `200`/`400` for a tool; an opted-in cancellation is `false` / `499`. `statusCode` is **absent** on a job — it had no transport to have a status on, and inventing one would be the same defect as the `method: 'AGENT'` this replaced. A job's outcome is `ok`, plus `errorCode` when it failed |
267
267
  | `durationMs` / `startedAt` | timing |
268
268
  | `errorCode` / `errorMessage` / `errorDetail` | failures only — `errorDetail` carries the structure the message flattens (e.g. Zod issues) |
269
- | `payload` | sanitised tool arguments; HTTP is `null` unless request `includePayload` is enabled |
269
+ | `payload` | sanitised tool arguments; HTTP is `null` unless request `includePayload` is enabled, or when the body was larger than `payloadMaxBytes` (then `payloadTruncated: true`) |
270
270
  | `resultSize` / `responseBytes` | result item count + serialised size |
271
271
  | `userId` / `ipAddress` / `userAgent` | identity |
272
272
 
@@ -638,6 +638,23 @@ createObservability({
638
638
  `redact`, `truncatePreview` and `measureSize` are exported on their own if you
639
639
  need to sanitise something outside the audit path.
640
640
 
641
+ ### How much of a body the audit row reads
642
+
643
+ With `includePayload: true` the row reads a clone of the request body, and it reads at most
644
+ `payloadMaxBytes` of it. Unset, the limit is the handler's `maxJsonBodyBytes`, or 1 MiB when the
645
+ handler has none, so one number bounds both. The clone is taken when the request opens, before
646
+ a route is matched, so an endpoint's own `maxJsonBodyBytes` is not known to it: set
647
+ `payloadMaxBytes` when endpoints accept more than the handler default. A body that is larger
648
+ is cancelled at the limit, its `payload` is `null`, and the row carries `payloadTruncated: true` — a `null` payload with that flag
649
+ means "too large to record", not "no body". Set `payloadMaxBytes` (a positive integer) to record
650
+ more or less than the handler accepts:
651
+
652
+ ```ts
653
+ createObservability({
654
+ request: { write, includePayload: true, payloadMaxBytes: 256 * 1024 },
655
+ })
656
+ ```
657
+
641
658
  ## The raw hooks
642
659
 
643
660
  Tool observability is built on hooks you can also use directly — for a one-off
@@ -887,7 +904,7 @@ audit event. See the [Observability guide](../guide/observability.md).
887
904
  | `ObservabilityDrainReport` | _type_ | final closed/drained snapshot, `durationMs` (the shared drain's age, not this call's wait), and `drained` — read from the counters, `false` only when `total.pending + total.preparing` is nonzero |
888
905
  | `ObservabilitySinkStatusSchema` / `ObservabilityStatusSchema` / `ObservabilityDrainReportSchema` | schema | runtime schemas for status/report integration boundaries |
889
906
  | `RequestEventSinkConfig` | _type_ | `write`, filter/sanitisation, `maxPending`, `onSinkError` and `onDrop` |
890
- | `RequestObservabilityConfig` | _type_ | request sink plus opt-in payload capture and default-off `includeCancelled` rows |
907
+ | `RequestObservabilityConfig` | _type_ | request sink plus opt-in payload capture (`payloadMaxBytes` bounds the body read, `payloadTruncated` marks a cut one) and default-off `includeCancelled` rows |
891
908
  | `SinkDropReason` | _type_ | `'capacity' \| 'closed'` |
892
909
  | `SinkError` | _type_ | isolated sink/projection failure and optional event |
893
910
  | `SinkDrop` | _type_ | rejected event, reason and current pending count |
package/llms/server.2.txt CHANGED
@@ -18,6 +18,141 @@ Also read:
18
18
  # Guide: HTTP server (docs/guide/server.md) (continued)
19
19
  ==============================================================================
20
20
 
21
+ ## Raw routes
22
+
23
+ Some routes cannot be a clean JSON contract — an OAuth redirect, a webhook with
24
+ signature verification, static files, the Socket.IO endpoint. `rawRoutes` are
25
+ plain `Request → Response` handlers, matched by the same router (shared CORS and
26
+ `onRequest`) but with no schema parsing and no `beforeHandle` gate — a raw route
27
+ authorises itself.
28
+
29
+ ```ts
30
+ createServer({
31
+ services,
32
+ rawRoutes: [
33
+ {
34
+ method: 'GET',
35
+ path: '/health',
36
+ handler: () => Response.json({ status: 'ok' }),
37
+ },
38
+ {
39
+ method: 'POST',
40
+ path: '/webhooks/:provider',
41
+ serviceName: 'webhooks',
42
+ action: 'receive',
43
+ handler: (req, ctx) => handleWebhook(ctx.params.provider, req),
44
+ },
45
+ ],
46
+ })
47
+ ```
48
+
49
+ `serviceName` and `action` are optional observability identity. Declare
50
+ `serviceName` when audit, logging, sampling or another cross-cutting policy
51
+ groups this raw route with a service; add `action` when the policy needs an
52
+ operation within that service. Stitchkit never derives either from `path`.
53
+ `action` without `serviceName`, and empty identity strings, fail at startup.
54
+
55
+ A path may be exact, carry `:param` segments, or end in `/*filePath` for a prefix
56
+ wildcard — and the two combine: `/app/:slug/*filePath` matches `/app/x/a/b` with
57
+ `ctx.params.slug === 'x'` and the remainder in `ctx.params.filePath` (a SPA
58
+ deep-link fallback). List more specific routes before the wildcard — the first
59
+ match wins. `staticRoute()` builds a raw route that serves a directory.
60
+
61
+ At startup Stitchkit rejects exact duplicates, equivalent parameter shapes
62
+ (`/users/:id` vs `/users/:userId`) and any later raw route completely hidden by
63
+ an earlier raw route. Partial overlap stays legal, including the recommended
64
+ specific-before-wildcard order. `GET` and `HEAD` remain independent because that
65
+ is how the actual raw matcher dispatches them. Raw-vs-contract overlap remains a
66
+ startup diagnostic naming the bypassed contract identity and scope.
67
+
68
+ ### Raw-route helpers
69
+
70
+ A raw route gives up the contract pipeline, so three things get re-implemented in
71
+ every one. `stitchkit/server` ships them — conveniences, not a second pipeline
72
+ (no auth, no schema gate beyond `parseBody`):
73
+
74
+ ```ts
75
+ import { respondJson, errorResponse, parseBody, badRequest } from 'stitchkit/server'
76
+
77
+ handler: async (req) => {
78
+ try {
79
+ const body = await parseBody(req, MySchema) // typed value, or null (no throw)
80
+ if (!body) throw badRequest('invalid body') // helpers THROW an AppError
81
+ return respondJson(await myService(body)) // JSON; 204 when null/undefined
82
+ } catch (err) {
83
+ return errorResponse(err) // same envelope as a contract route
84
+ }
85
+ }
86
+ ```
87
+
88
+ `parseBody` reads the body up to `{ maxBytes }` — 1 MiB when you pass none, as the third
89
+ argument — and returns `null` for a larger one without reading further, so a raw route
90
+ never holds an unbounded body in memory.
91
+
92
+ The error helpers (`badRequest`, `notFound`, …) **throw** an `AppError`, so raise
93
+ them inside the `try` and let `errorResponse(err)` render it — that runs any
94
+ thrown value through the framework's `normalizeError`, so a raw route returns the
95
+ **identical** `{ error: { code, message, … } }` shape (and `x-request-id`) a
96
+ contract route does.
97
+
98
+ ### Serving files & Range requests
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:
105
+
106
+ ```ts
107
+ import { defineContract } from 'stitchkit/contract'
108
+ import { implement, serveFile } from 'stitchkit/server'
109
+ import { z } from 'zod'
110
+
111
+ const MediaParams = z.object({ id: z.string() })
112
+ const media = defineContract({ prefix: 'media' }, {
113
+ download: {
114
+ method: 'GET', path: '/:id', desc: 'Download media',
115
+ params: MediaParams, rawResponse: true, contentType: 'video/mp4',
116
+ },
117
+ inspect: {
118
+ method: 'HEAD', path: '/:id', desc: 'Inspect media',
119
+ params: MediaParams, rawResponse: true, contentType: 'video/mp4',
120
+ },
121
+ })
122
+
123
+ const mediaService = implement(media, {
124
+ download: ({ req, params }) =>
125
+ serveFile(req, { path: pathForId(params.id), filename: 'clip.mp4' }),
126
+ inspect: ({ req, params }) =>
127
+ serveFile(req, { path: pathForId(params.id), filename: 'clip.mp4' }),
128
+ })
129
+ ```
130
+
131
+ GET and HEAD are separate operations deliberately: declaring GET never creates
132
+ a hidden HEAD alias. Both travel through the normal contract router, params,
133
+ lifecycle/RBAC and request logging. A HEAD handler may inspect the raw query via
134
+ `ctx.req.url`, but cannot declare a request input schema or body.
135
+
136
+ `serveFile` returns `206` (range, with `Content-Range` + `Content-Length`), `200`
137
+ (full), `416` (unsatisfiable, `Content-Range: bytes */size`), `304`
138
+ (`If-None-Match` / `If-Modified-Since`), `404` (missing) or `405` (non GET/HEAD).
139
+ It always sets `Accept-Ranges: bytes`, a weak `ETag` and `Last-Modified` (so
140
+ `If-Range` keeps a changing file from being stitched from stale + fresh bytes),
141
+ and `nosniff`. `Content-Type` is auto-detected from the path — override it, or
142
+ pass `disposition` / `cacheControl` / `etag: false`, via the options.
143
+
144
+ Cross-origin, the browser lets JavaScript read only the CORS-safelisted response
145
+ headers. stitchkit therefore exposes the download-relevant ones by default
146
+ (`Content-Disposition`, `Content-Range`, `ETag`, …) — without that a `fetch`-based
147
+ download cannot recover the file's name. Override with `cors.exposeHeaders`
148
+ (extend `DEFAULT_CORS_EXPOSE_HEADERS` rather than replacing it), or pass `[]` to
149
+ emit none.
150
+
151
+ `serveFile` takes an explicit `path` and trusts it — **the caller owns
152
+ containment**. For a URL-derived path use `staticRoute` (which enforces it) or
153
+ `isWithinDir` first. The byte-range parser is exported on its own as
154
+ `parseByteRange(header, size)` for direct use and testing. → ADR 0023.
155
+
21
156
  ## Server primitives
22
157
 
23
158
  `stitchkit/server` also exports the primitives most APIs need. Each is a small,
package/llms/server.5.txt CHANGED
@@ -62,9 +62,11 @@ Also re-exports the error helpers from `stitchkit/contract`.
62
62
  | `formatZodError` | function | a `ZodError` → a readable, field-summarised string |
63
63
  | `zodIssues` | function | a `ZodError` → structured `{ path, code, message }[]` — the machine-readable sibling of `formatZodError` |
64
64
  | `ZodIssueSummary` | _type_ | one structured validation issue (`{ path, code, message }`) |
65
- | `parseBody` | function | parse + Zod-validate a JSON body → `data` or `null` (no throw) |
65
+ | `parseBody` | function | parse + Zod-validate a JSON body → `data` or `null` (no throw); reads at most `options.maxBytes` (1 MiB by default) and is `null` for a larger body |
66
66
  | `HandlerConfig` | _type_ | config for `createHandler`, including optional `maxJsonBodyBytes`; bound to `BunServer` on this entrypoint |
67
67
  | `BunServerConfig` | _type_ | config for `createServer` (Bun); `unix` listens on a unix domain socket (mutually exclusive with `port`/`hostname`) — [details](../guide/server.md#local-daemon-over-a-unix-socket) |
68
+ | `defineRequestIngress` / `RequestIngress` / `RequestIngressSource` | function / _type_ | a typed key the code that builds a `Request` attaches a value to (`attach(request, value)`) and an endpoint, `authorize`, `beforeHandle` or `onError` reads from its `ctx` (`from(ctx)`); a header, `clone()` and `new Request(request)` carry nothing, a second `attach` is refused, and `ctx.req` is the very request the handler received — [guide](../guide/server.md#request-attributes-from-the-ingress) |
69
+ | `ProxyOriginConfig` | _type_ | explicit real-peer trust for Bun `createServer({ proxyOrigin })`; strict external-scheme normalization before composition — [details](../guide/server.md#external-origin-behind-a-trusted-proxy) |
68
70
  | `UnixListenConfig` | _type_ | unix listener — a socket path, or `{ path, mode }` to tighten the file mode after listen |
69
71
  | `BunServerHandle` | _type_ | managed Bun handle (`url`, `port`, `runtime`, `status`, `shutdown`) |
70
72
  | `ManagedServerHandle` | _type_ | shared lifecycle shape generic over the runtime escape hatch |
package/llms/server.txt CHANGED
@@ -61,6 +61,48 @@ Every handler receives one `ctx` argument:
61
61
  The same handler runs for HTTP, MCP and agent calls — `ctx.source` tells you
62
62
  which. Anything an auth hook attaches (e.g. the resolved user) is also on `ctx`.
63
63
 
64
+ ### Request attributes from the ingress
65
+
66
+ When your own code builds the `Request` — a local socket ingress, a relay, a test harness —
67
+ and calls the fetch handler of the core, it often knows something an endpoint needs and the
68
+ request cannot be allowed to say for itself: which authenticated peer sent it. A header can be
69
+ forged by any client. `defineRequestIngress` is the channel the request cannot carry:
70
+
71
+ ```ts
72
+ import { createHandler, defineRequestIngress, implement } from 'stitchkit/server'
73
+
74
+ interface Peer {
75
+ node: string
76
+ }
77
+ export const peer = defineRequestIngress<Peer>('peer')
78
+
79
+ // The ingress, after it has authenticated the peer and built the request:
80
+ peer.attach(request, { node: 'node-7' })
81
+ const response = await handler(request)
82
+
83
+ // An endpoint, `authorize`, `beforeHandle` or `onError` — `ctx` is the source:
84
+ implement(contract, {
85
+ place: (ctx) => ({ node: peer.from(ctx)?.node ?? null }),
86
+ })
87
+ ```
88
+
89
+ - The value belongs to the key. A header, a body, `request.clone()` and `new Request(request)`
90
+ carry nothing; a request from another ingress has no value, and `from` answers `undefined`.
91
+ Two keys never meet, even under one name, and a second `attach` of the same key to the same
92
+ request is an error.
93
+ - `ctx.req` is the `Request` the fetch handler of the core received, on every HTTP context: an
94
+ endpoint, `authorize`, `beforeHandle`, and the `onError` context of a validation failure, an
95
+ unknown path, a refused method or an `onRequest` that threw. That is the guarantee the lookup
96
+ stands on, and the ingress tests hold it.
97
+ - A raw route, `streamingRoute` and `onRequest` receive the request without a context: read with
98
+ `peer.from({ req })`. The MCP, agent and CLI lanes carry no `Request`, so there is nothing to
99
+ read.
100
+ - With `proxyOrigin` configured, the request is replaced before `wrapFetch`: attach in
101
+ `wrapFetch` or later, to the request you are handed.
102
+ - A Socket.IO handshake is routed before `wrapFetch` on Bun and rebuilt as a new `Request` on
103
+ Node: it never carries an attached value.
104
+ - `stitchkit/node` exports the same names.
105
+
64
106
  ### `createImplement` — a fixed context type
65
107
 
66
108
  To type `ctx` with your app's extras (the user an auth hook injects), fix the
@@ -268,6 +310,7 @@ server. See [Testing & deployment](./testing-and-deployment.md).
268
310
  | `logging` | `true` for built-in request logs, or a `LoggingConfig` (see below) |
269
311
  | `traceId` | override per-request trace-id resolution — may return `undefined` to fall back |
270
312
  | `wrapFetch` | compose wrappers around the finished handler (request context, audit) |
313
+ | `proxyOrigin` | Bun TCP ingress policy: `{ trustedPeer(socketIp) }` restores the external scheme before all composition; independent of client-IP `trustProxy` |
271
314
  | `socket` | full Stitchkit Socket.IO handle; route, default WebSocket handler and shutdown are mounted once |
272
315
  | `websocket` | custom Bun WebSocket handler; with `socket`, this is the explicit composed handler |
273
316
  | `development` / `bun` | passthrough to `Bun.serve` |
@@ -276,6 +319,60 @@ Native Bun `routes` are intentionally not accepted: Bun matches them before
276
319
  `fetch`, so they could bypass shutdown admission. Use `rawRoutes`; they retain
277
320
  the Fetch `Request → Response` model and participate in lifecycle tracking.
278
321
 
322
+ ### External origin behind a trusted proxy
323
+
324
+ An HTTPS ingress often forwards plain HTTP to Bun. `trustProxy` trusts client-IP
325
+ headers only; it does not change `Request.url`. Opt into the separate URL policy:
326
+
327
+ ```ts
328
+ import { createServer, isLoopbackAddress } from 'stitchkit/server'
329
+
330
+ const server = createServer({
331
+ hostname: '127.0.0.1',
332
+ port: 3000,
333
+ proxyOrigin: { trustedPeer: isLoopbackAddress },
334
+ services,
335
+ })
336
+ ```
337
+
338
+ The predicate receives the **real TCP peer**, never `X-Forwarded-For` or
339
+ `X-Real-IP`. Match only the ingress you control. Loopback is appropriate only
340
+ when other local processes are trusted. Unmatched peers, missing peer metadata
341
+ and the default configuration ignore URL forwarding headers. Unix listeners have
342
+ no TCP peer and do not use this policy. This option belongs to Bun `createServer`,
343
+ not the Fetch-clean `createHandler` or Node `serveNode`.
344
+
345
+ The trusted ingress must overwrite `Host` with the external authority and
346
+ `X-Forwarded-Proto` with exactly `http` or `https`. It must remove `Forwarded`,
347
+ `X-Forwarded-Host` and `X-Forwarded-Port`; their presence, a missing/invalid
348
+ proto, a list of values or a malformed Host returns HTTP 400 before application
349
+ code. Header names are case-insensitive; proto values are lowercase. Multiple
350
+ ingress chains require a single sanitizing ingress at this boundary.
351
+
352
+ For an nginx HTTPS terminator, the relevant forwarding settings are:
353
+
354
+ ```nginx
355
+ proxy_set_header Host $http_host;
356
+ proxy_set_header X-Forwarded-Proto $scheme;
357
+ proxy_set_header Forwarded "";
358
+ proxy_set_header X-Forwarded-Host "";
359
+ proxy_set_header X-Forwarded-Port "";
360
+ ```
361
+
362
+ The normalized `Request.url` reaches `wrapFetch`, hooks, contracts, raw routes,
363
+ and the managed Socket.IO route. It preserves the path/query, wire authority
364
+ (including explicit ports), method, body, headers and linked cancellation signal.
365
+ Request cloning uses the normalized URL. The same runtime handle resolves
366
+ `requestIP`, `timeout` and `upgrade` against the original native request, retaining
367
+ WebSocket and shutdown behavior.
368
+
369
+ This is a scheme normalization policy, **not host authorization**. Applications
370
+ must retain their hostname/tenant allowlist and exact same-origin checks for
371
+ Origin and Referer. The TLS terminator must reject SNI/Host mismatches before
372
+ forwarding; a plain-HTTP Bun listener cannot observe the TLS SNI. The normalizer
373
+ does not accept `X-Forwarded-Host`, authorize a foreign Host, or treat HTTP and
374
+ HTTPS as aliases.
375
+
279
376
  ### Local daemon over a unix socket
280
377
 
281
378
  A local daemon whose door is a socket file needs no TCP port at all:
@@ -863,134 +960,3 @@ file: (ctx) => {
863
960
  `Response` is forwarded verbatim. Request headers are not relayed, so a `Range`
864
961
  sent to the proxy does not reach the origin and the full body comes back.
865
962
  → ADR 0038.
866
-
867
- ## Raw routes
868
-
869
- Some routes cannot be a clean JSON contract — an OAuth redirect, a webhook with
870
- signature verification, static files, the Socket.IO endpoint. `rawRoutes` are
871
- plain `Request → Response` handlers, matched by the same router (shared CORS and
872
- `onRequest`) but with no schema parsing and no `beforeHandle` gate — a raw route
873
- authorises itself.
874
-
875
- ```ts
876
- createServer({
877
- services,
878
- rawRoutes: [
879
- {
880
- method: 'GET',
881
- path: '/health',
882
- handler: () => Response.json({ status: 'ok' }),
883
- },
884
- {
885
- method: 'POST',
886
- path: '/webhooks/:provider',
887
- serviceName: 'webhooks',
888
- action: 'receive',
889
- handler: (req, ctx) => handleWebhook(ctx.params.provider, req),
890
- },
891
- ],
892
- })
893
- ```
894
-
895
- `serviceName` and `action` are optional observability identity. Declare
896
- `serviceName` when audit, logging, sampling or another cross-cutting policy
897
- groups this raw route with a service; add `action` when the policy needs an
898
- operation within that service. Stitchkit never derives either from `path`.
899
- `action` without `serviceName`, and empty identity strings, fail at startup.
900
-
901
- A path may be exact, carry `:param` segments, or end in `/*filePath` for a prefix
902
- wildcard — and the two combine: `/app/:slug/*filePath` matches `/app/x/a/b` with
903
- `ctx.params.slug === 'x'` and the remainder in `ctx.params.filePath` (a SPA
904
- deep-link fallback). List more specific routes before the wildcard — the first
905
- match wins. `staticRoute()` builds a raw route that serves a directory.
906
-
907
- At startup Stitchkit rejects exact duplicates, equivalent parameter shapes
908
- (`/users/:id` vs `/users/:userId`) and any later raw route completely hidden by
909
- an earlier raw route. Partial overlap stays legal, including the recommended
910
- specific-before-wildcard order. `GET` and `HEAD` remain independent because that
911
- is how the actual raw matcher dispatches them. Raw-vs-contract overlap remains a
912
- startup diagnostic naming the bypassed contract identity and scope.
913
-
914
- ### Raw-route helpers
915
-
916
- A raw route gives up the contract pipeline, so three things get re-implemented in
917
- every one. `stitchkit/server` ships them — conveniences, not a second pipeline
918
- (no auth, no schema gate beyond `parseBody`):
919
-
920
- ```ts
921
- import { respondJson, errorResponse, parseBody, badRequest } from 'stitchkit/server'
922
-
923
- handler: async (req) => {
924
- try {
925
- const body = await parseBody(req, MySchema) // typed value, or null (no throw)
926
- if (!body) throw badRequest('invalid body') // helpers THROW an AppError
927
- return respondJson(await myService(body)) // JSON; 204 when null/undefined
928
- } catch (err) {
929
- return errorResponse(err) // same envelope as a contract route
930
- }
931
- }
932
- ```
933
-
934
- The error helpers (`badRequest`, `notFound`, …) **throw** an `AppError`, so raise
935
- them inside the `try` and let `errorResponse(err)` render it — that runs any
936
- thrown value through the framework's `normalizeError`, so a raw route returns the
937
- **identical** `{ error: { code, message, … } }` shape (and `x-request-id`) a
938
- contract route does.
939
-
940
- ### Serving files & Range requests
941
-
942
- `staticRoute` is basic on purpose — it reads the whole file into memory and has
943
- no `Range` or conditional support; put a CDN in front, or use it for small web
944
- assets. To serve **media** (video / audio / large downloads) that a browser must
945
- seek and cache, use **`serveFile`** (Bun) — it streams the requested byte range
946
- and speaks the conditional-request half of RFC 7233 / 9110:
947
-
948
- ```ts
949
- import { defineContract } from 'stitchkit/contract'
950
- import { implement, serveFile } from 'stitchkit/server'
951
- import { z } from 'zod'
952
-
953
- const MediaParams = z.object({ id: z.string() })
954
- const media = defineContract({ prefix: 'media' }, {
955
- download: {
956
- method: 'GET', path: '/:id', desc: 'Download media',
957
- params: MediaParams, rawResponse: true, contentType: 'video/mp4',
958
- },
959
- inspect: {
960
- method: 'HEAD', path: '/:id', desc: 'Inspect media',
961
- params: MediaParams, rawResponse: true, contentType: 'video/mp4',
962
- },
963
- })
964
-
965
- const mediaService = implement(media, {
966
- download: ({ req, params }) =>
967
- serveFile(req, { path: pathForId(params.id), filename: 'clip.mp4' }),
968
- inspect: ({ req, params }) =>
969
- serveFile(req, { path: pathForId(params.id), filename: 'clip.mp4' }),
970
- })
971
- ```
972
-
973
- GET and HEAD are separate operations deliberately: declaring GET never creates
974
- a hidden HEAD alias. Both travel through the normal contract router, params,
975
- lifecycle/RBAC and request logging. A HEAD handler may inspect the raw query via
976
- `ctx.req.url`, but cannot declare a request input schema or body.
977
-
978
- `serveFile` returns `206` (range, with `Content-Range` + `Content-Length`), `200`
979
- (full), `416` (unsatisfiable, `Content-Range: bytes */size`), `304`
980
- (`If-None-Match` / `If-Modified-Since`), `404` (missing) or `405` (non GET/HEAD).
981
- It always sets `Accept-Ranges: bytes`, a weak `ETag` and `Last-Modified` (so
982
- `If-Range` keeps a changing file from being stitched from stale + fresh bytes),
983
- and `nosniff`. `Content-Type` is auto-detected from the path — override it, or
984
- pass `disposition` / `cacheControl` / `etag: false`, via the options.
985
-
986
- Cross-origin, the browser lets JavaScript read only the CORS-safelisted response
987
- headers. stitchkit therefore exposes the download-relevant ones by default
988
- (`Content-Disposition`, `Content-Range`, `ETag`, …) — without that a `fetch`-based
989
- download cannot recover the file's name. Override with `cors.exposeHeaders`
990
- (extend `DEFAULT_CORS_EXPOSE_HEADERS` rather than replacing it), or pass `[]` to
991
- emit none.
992
-
993
- `serveFile` takes an explicit `path` and trusts it — **the caller owns
994
- containment**. For a URL-derived path use `staticRoute` (which enforces it) or
995
- `isWithinDir` first. The byte-range parser is exported on its own as
996
- `parseByteRange(header, size)` for direct use and testing. → ADR 0023.
@@ -398,6 +398,33 @@ that ends inside a line throws `StreamTruncatedLineError` (`lineBytes`) instead
398
398
  fragment as a value — the right policy for a socket or a child process, where a missing newline
399
399
  means the writer died. Both errors extend the `RangeError` / `SyntaxError` they replace.
400
400
 
401
+ ## Reading a body up to a byte limit
402
+
403
+ `readBoundedBody` (from `stitchkit` or `stitchkit/http`) reads a request or response body, a
404
+ `ReadableStream<Uint8Array>` or any `AsyncIterable<Uint8Array>`, and never reads past
405
+ `maxBytes` plus one chunk. Whoever owns the boundary picks the two errors, so a body that is too
406
+ large and a read that was cancelled stay different outcomes:
407
+
408
+ ```ts
409
+ import { readBoundedBody } from 'stitchkit'
410
+
411
+ const bytes = await readBoundedBody(request.body, {
412
+ maxBytes: 64 * 1024,
413
+ signal: request.signal,
414
+ limitError: (observedBytes) => new PayloadTooLarge(observedBytes),
415
+ abortError: (reason) => new ClientGone({ cause: reason }),
416
+ })
417
+ ```
418
+
419
+ - The source is cancelled when the limit is crossed and when `signal` aborts. A read that never
420
+ settles does not hold the call after an abort, and an abort is honoured before the first read
421
+ (even for a `null` body).
422
+ - A body of exactly `maxBytes` is read; one more byte calls `limitError` with the bytes seen so far
423
+ (the chunk that crossed the limit included).
424
+ - A `null` body, such as a request without one, is an empty result.
425
+ - Without `abortError` the signal's own reason is thrown.
426
+ - `maxBytes` is a positive safe integer.
427
+
401
428
  ## Resumable streams
402
429
 
403
430
  A stream that survives a dropped connection needs four things beyond opening it:
@@ -554,6 +581,7 @@ The browser-and-server entrypoint. Re-exports everything from
554
581
  | `parseSSE` | function | parse SSE from a `Response`, a web stream or an async byte iterable into an async generator — [guide](../guide/client.md#sse) |
555
582
  | `parseNDJSON` | function | parse bounded fatal-UTF-8 NDJSON from a `Response`, a `ReadableStream<Uint8Array>` or an `AsyncIterable<Uint8Array>` (a child's stdout); blank keep-alives are skipped, `finalLine: 'require-newline'` makes the delimiter mandatory and `signal` cancels the source — [guide](../guide/client.md#any-byte-source) |
556
583
  | `createNDJSONDecoder` / `NDJSONDecoder` | function / _type_ | the same bounded NDJSON reading driven by `push(chunk)` / `end()`, for a socket `data` callback |
584
+ | `readBoundedBody` / `BoundedBodyOptions` | function / _type_ | the whole body of a `ReadableStream<Uint8Array>` or async byte iterable as bytes, never reading past `maxBytes` plus one chunk; `limitError(observedBytes)` and `abortError(reason)` choose the two errors, the source is cancelled on both, a `null` body is empty — [guide](../guide/client.md#reading-a-body-up-to-a-byte-limit) |
557
585
  | `StreamByteSource` | _type_ | `Response \| ReadableStream<Uint8Array> \| AsyncIterable<Uint8Array>`, what the NDJSON and SSE readers accept |
558
586
  | `StreamLineLimitError` / `StreamTruncatedLineError` | class | a line past `maxLineBytes` (`limitBytes`, `lineBytes`; a `RangeError`) and a source ended inside a line under `require-newline` (`lineBytes`; a `SyntaxError`) |
559
587
  | `resumableIterator` | function | re-open a long-lived stream from the last delivered cursor, with jittered backoff, a caller-owned terminal item and prompt abort — [guide](../guide/client.md#resumable-streams) |
@@ -85,12 +85,12 @@ own, recorded as an ADR.
85
85
  | `stitchkit/geo` | server (Bun or Node) | evolving | managed GeoIP reader generations, last-known-good reload and the optional MaxMind adapter |
86
86
  | `stitchkit/oauth` | browser **and** server | evolving | provider-neutral Authorization Code + PKCE request and one-shot callback transaction mechanics |
87
87
  | `stitchkit/google` | server (Bun or Node) | evolving | optional Google code exchange and verified OIDC identity adapter |
88
- | `stitchkit/observability` | server | stable<br>_redefined in 2 of the 55 minors since 0.56.2, most recently 0.92.0_ | request/tool event projections — `createObservability`, trace context, sanitisation |
88
+ | `stitchkit/observability` | server | stable<br>_redefined in 2 of the 56 minors since 0.56.2, most recently 0.92.0_ | request/tool event projections — `createObservability`, trace context, sanitisation |
89
89
  | `stitchkit/testing` | tests on Bun or Node | stable | in-process generated clients over a real Fetch handler, the managed-resource conformance kit and manual time for schedules and retries |
90
90
  | `stitchkit/declaration` | browser + build and deployment tooling (Bun or Node) | evolving | `ProjectDeclarationSchema` — the one machine-readable statement a repository makes about itself |
91
91
  | `stitchkit/react` | browser + server rendering | stable | `createCursorQuery`, `createCacheBridge`, QueryClient and `ApiError` retry policy |
92
92
  | `stitchkit/react/keyboard` | browser | evolving | which part of a screen gets a key — ordered layers, one Escape per level, list zones with real focus; see [keyboard guide](keyboard.md) |
93
- | `stitchkit/agent-runtime` | server | evolving<br>_redefined in 23 of the 55 minors since 0.56.2, most recently 0.105.0_ | optional durable conversation/run loop, history, models, prompts, fencing and events |
93
+ | `stitchkit/agent-runtime` | server | evolving<br>_redefined in 23 of the 56 minors since 0.56.2, most recently 0.105.0_ | optional durable conversation/run loop, history, models, prompts, fencing and events |
94
94
  | `stitchkit/agent-runtime/sandbox` | server | evolving | optional Linux Bubblewrap sessions, durable workspaces and a host HTTP credential gateway; see [sandbox guide](sandbox.md) |
95
95
  | `stitchkit/agent-runtime/testing` | tests on Bun or Node | evolving | credential-free replay, scripted provider faults, deterministic race controls and the agent-store conformance kit |
96
96
  | `stitchkit/agent-runtime/harness` | server | evolving | resource-aware process-local facade over the canonical Agent runtime; supervision stays outside |
@@ -102,7 +102,7 @@ own, recorded as an ADR.
102
102
  | `stitchkit/agent-runtime/sqlite/bun` | server (Bun) | evolving | durable built-in SQLite store for the agent runtime |
103
103
  | `stitchkit/agent-runtime/sqlite/node` | server (Node ≥ 22.5) | evolving | durable built-in SQLite store for the agent runtime |
104
104
  | `stitchkit-tui` | terminal (Bun) | evolving | optional official OpenTUI host over a caller-composed headless runtime |
105
- | `stitchkit/application` | browser + server | evolving<br>_redefined in 14 of the 55 minors since 0.56.2, most recently 0.110.0_ | managed resource graph, readiness, admission, schedules, subtree restart and bounded shutdown |
105
+ | `stitchkit/application` | browser + server | evolving<br>_redefined in 14 of the 56 minors since 0.56.2, most recently 0.110.0_ | managed resource graph, readiness, admission, schedules, subtree restart and bounded shutdown |
106
106
  | `stitchkit/application/grammy` | server | evolving | a grammY bot as application resources — configuration, then polling with updates admitted by batch — and the webhook adapter |
107
107
  | `stitchkit/application/opentelemetry` | server | evolving | maps application snapshots onto an injected OpenTelemetry `Meter` |
108
108
  | `stitchkit/application/schemas` | browser + server | evolving | the application's snapshot, health and shutdown schemas alone, without the kernel |
package/llms/tools.3.txt CHANGED
@@ -196,7 +196,7 @@ A native remote-connector auth surface for MCP — [guide](../guide/mcp-and-agen
196
196
  | `protectedResourceMetadataUrl` | function | build the metadata URL for a resource |
197
197
  | `wwwAuthenticateHeader` | function | build the `WWW-Authenticate` challenge header |
198
198
  | `PROTECTED_RESOURCE_PATH` | const | the well-known metadata path |
199
- | `OAuthProviderConfig` | _type_ | config for `mountOAuthProvider` |
199
+ | `OAuthProviderConfig` | _type_ | config for `mountOAuthProvider`; `maxRequestBodyBytes` bounds the `/token` and `/register` bodies (64 KiB by default) |
200
200
  | `ProtectedResourceConfig` | _type_ | config for `oauthProtectedResourceRoute` |
201
201
  | `ApplicationType` | _type_ | DCR `application_type` — `'native'` (loopback allowed) \| `'web'` (https only) |
202
202
  | `AuthCodeData` | _type_ | a stored authorization-code record |
package/llms/tools.txt CHANGED
@@ -926,6 +926,10 @@ default; DCR is off unless `clientRegistration.dcr` supplies both `register` and
926
926
  exactly equals its URL, explicit redirect URIs and
927
927
  `token_endpoint_auth_method: 'none'`.
928
928
 
929
+ The token endpoint and `/register` read at most 64 KiB of a request body, or
930
+ `maxRequestBodyBytes` when you set it; a larger one is answered `413` with
931
+ `invalid_request` and the rest is not read.
932
+
929
933
  The production fetcher rejects credentials, fragments, non-HTTPS URLs and
930
934
  private/reserved targets. Every redirect is DNS-resolved and IP-pinned again;
931
935
  timeouts, redirect count and body size are bounded. Its bounded cache respects
@@ -1,14 +1,40 @@
1
- # Upgrading 0.101.0 → 0.110.0 — stitchkit docs slice
1
+ # Upgrading 0.101.0 → 0.111.0 — stitchkit docs slice
2
2
 
3
3
  > Generated from the `docs/` tree by `bun run gen:llms`. The index of all slices is
4
4
  > `llms.txt` in this package.
5
5
 
6
- Contains: Guide: Upgrading, 0.101.0 → 0.110.0 (docs/guide/upgrading.md)
6
+ Contains: Guide: Upgrading, 0.101.0 → 0.111.0 (docs/guide/upgrading.md)
7
7
 
8
8
  ==============================================================================
9
- # Guide: Upgrading, 0.101.0 → 0.110.0 (docs/guide/upgrading.md)
9
+ # Guide: Upgrading, 0.101.0 → 0.111.0 (docs/guide/upgrading.md)
10
10
  ==============================================================================
11
11
 
12
+ ## Released migration: 0.111.0
13
+
14
+ **Who must act:** applications whose raw routes call `parseBody` on JSON larger than 1 MiB, and applications whose OAuth clients send `/register` or `/token` bodies larger than 64 KiB.
15
+
16
+ ### Bounded raw and OAuth bodies
17
+
18
+ `parseBody` reads at most 1 MiB and returns `null` for a larger body, where it used to read any size. A route that takes larger JSON states its limit:
19
+
20
+ ```ts
21
+ // before
22
+ const body = await parseBody(req, Schema)
23
+ // after
24
+ const body = await parseBody(req, Schema, { maxBytes: 8 * 1024 * 1024 })
25
+ ```
26
+
27
+ `mountOAuthProvider` reads at most 64 KiB of a `/token` or `/register` body and answers `413` with `invalid_request` for a larger one. A deployment whose clients register larger metadata raises the limit:
28
+
29
+ ```ts
30
+ // before
31
+ mountOAuthProvider({ issuer, signingSecret, resource, codes, authorizeUser })
32
+ // after
33
+ mountOAuthProvider({ issuer, signingSecret, resource, codes, authorizeUser, maxRequestBodyBytes: 256 * 1024 })
34
+ ```
35
+
36
+ Both limits must be a positive safe integer; anything else is refused when the route is built or called.
37
+
12
38
  ## Released migration: 0.110.0
13
39
 
14
40
  ### Interrupted process drains
@@ -11,7 +11,7 @@ Contains: Guide: Upgrading, general (docs/guide/upgrading.md)
11
11
 
12
12
  Load this slice, then every range slice between your installed version and the target:
13
13
 
14
- - 0.101.0 → 0.110.0: `llms/upgrading-0.101.0-0.110.0.txt`
14
+ - 0.101.0 → 0.111.0: `llms/upgrading-0.101.0-0.111.0.txt`
15
15
  - 0.74.0 → 0.100.0: `llms/upgrading-0.74.0-0.100.0.txt`
16
16
  - 0.61.0 → 0.73.0: `llms/upgrading-0.61.0-0.73.0.txt`
17
17
  - 0.44.0 → 0.60.0: `llms/upgrading-0.44.0-0.60.0.txt`
@@ -36,7 +36,7 @@ Run it inside the project that depends on stitchkit. It reads your installed
36
36
  version out of `node_modules`, reads the changelog **shipped inside the
37
37
  package**, and prints every breaking section your range crosses, oldest first,
38
38
  each with its **Who must act** line — the list this guide's migrations expand
39
- on. Nothing to clone, nothing to be told: `--from` / `--to` override either end
39
+ on — and then the fixes of that range that name what they touch. Nothing to clone, nothing to be told: `--from` / `--to` override either end
40
40
  of the range if you want a different one.
41
41
 
42
42
  So upgrading is: read the `### ⚠️ Breaking changes` of every version *above* your
@@ -67,6 +67,12 @@ beside a `package.json`, which is build output) and gives each item one verdict:
67
67
  - **not declared — check by hand** — an item from before the line existed, with the files that
68
68
  import the entrypoints it leads with.
69
69
 
70
+ A fix that names what it touches gets the same verdicts. Since 0.110.7 every `### Fixed` item that
71
+ leads with an entrypoint carries the `**Affects:**` line, and `upgrade` prints them in a second
72
+ section, **Fixes that touch this project** (`fixes` in `--json`): a fix to `runNativeCommand` is
73
+ listed with the `path:line` that imports it, or as *not used here*. Fixes of earlier releases that
74
+ carry no line are not judged; one line counts them (`fixesWithoutAffects` in `--json`).
75
+
70
76
  `--json` prints the same verdicts per item for a CI step. The scan reads static `import` and
71
77
  `export … from` declarations, bare imports and `import()` / `require()` of a literal specifier; a
72
78
  specifier built at run time is invisible to it, and so is a file of another kind (`.astro`,