stitchkit 0.110.4 → 0.110.6

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 (44) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/dist/agent-runtime/branch-observations.d.ts +16 -16
  3. package/dist/entrypoints/agent-runtime/coding-tools.js +2 -2
  4. package/dist/entrypoints/agent-runtime/sandbox.js +2 -2
  5. package/dist/entrypoints/agent-runtime/sqlite/bun.js +4 -4
  6. package/dist/entrypoints/agent-runtime/sqlite/node.js +4 -4
  7. package/dist/entrypoints/agent-runtime/testing.js +4 -4
  8. package/dist/entrypoints/agent-runtime.js +4 -4
  9. package/dist/entrypoints/cli.js +1 -1
  10. package/dist/entrypoints/process.js +1 -1
  11. package/dist/entrypoints/server.d.ts +1 -0
  12. package/dist/entrypoints/server.d.ts.map +1 -1
  13. package/dist/entrypoints/server.js +55 -3
  14. package/dist/entrypoints/tools.js +1 -1
  15. package/dist/{index-mpg1t7nv.js → index-0z3kfphz.js} +1 -1
  16. package/dist/{index-5pbtbce0.js → index-7c957abz.js} +1 -1
  17. package/dist/{index-e1sjcjfy.js → index-7r4knwf8.js} +94 -23
  18. package/dist/{index-bm180f2d.js → index-ey15wx6j.js} +1 -1
  19. package/dist/{index-9ket8wzx.js → index-mnk5vgdr.js} +1 -1
  20. package/dist/{index-2tazbwhv.js → index-n7hrpngg.js} +151 -92
  21. package/dist/process/group-diagnostics.d.ts +20 -0
  22. package/dist/process/group-diagnostics.d.ts.map +1 -0
  23. package/dist/process/group-settlement.d.ts +1 -1
  24. package/dist/process/group-settlement.d.ts.map +1 -1
  25. package/dist/process/group.d.ts.map +1 -1
  26. package/dist/server/bun.d.ts +9 -0
  27. package/dist/server/bun.d.ts.map +1 -1
  28. package/dist/server/proxy-origin.d.ts +12 -0
  29. package/dist/server/proxy-origin.d.ts.map +1 -0
  30. package/dist/tools/cli/args-json.d.ts +12 -0
  31. package/dist/tools/cli/args-json.d.ts.map +1 -0
  32. package/dist/tools/cli/args-tokens.d.ts +27 -0
  33. package/dist/tools/cli/args-tokens.d.ts.map +1 -0
  34. package/dist/tools/cli/args.d.ts +4 -0
  35. package/dist/tools/cli/args.d.ts.map +1 -1
  36. package/dist/tools/cli/help.d.ts.map +1 -1
  37. package/llms/cli.2.txt +7 -2
  38. package/llms/cli.txt +17 -0
  39. package/llms/server.2.txt +131 -0
  40. package/llms/server.5.txt +1 -0
  41. package/llms/server.txt +55 -131
  42. package/llms-full.txt +80 -2
  43. package/llms.txt +2 -2
  44. package/package.json +1 -1
package/llms/server.2.txt CHANGED
@@ -18,6 +18,137 @@ 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
+ The error helpers (`badRequest`, `notFound`, …) **throw** an `AppError`, so raise
89
+ them inside the `try` and let `errorResponse(err)` render it — that runs any
90
+ thrown value through the framework's `normalizeError`, so a raw route returns the
91
+ **identical** `{ error: { code, message, … } }` shape (and `x-request-id`) a
92
+ contract route does.
93
+
94
+ ### Serving files & Range requests
95
+
96
+ `staticRoute` is basic on purpose — it reads the whole file into memory and has
97
+ no `Range` or conditional support; put a CDN in front, or use it for small web
98
+ assets. To serve **media** (video / audio / large downloads) that a browser must
99
+ seek and cache, use **`serveFile`** (Bun) — it streams the requested byte range
100
+ and speaks the conditional-request half of RFC 7233 / 9110:
101
+
102
+ ```ts
103
+ import { defineContract } from 'stitchkit/contract'
104
+ import { implement, serveFile } from 'stitchkit/server'
105
+ import { z } from 'zod'
106
+
107
+ const MediaParams = z.object({ id: z.string() })
108
+ const media = defineContract({ prefix: 'media' }, {
109
+ download: {
110
+ method: 'GET', path: '/:id', desc: 'Download media',
111
+ params: MediaParams, rawResponse: true, contentType: 'video/mp4',
112
+ },
113
+ inspect: {
114
+ method: 'HEAD', path: '/:id', desc: 'Inspect media',
115
+ params: MediaParams, rawResponse: true, contentType: 'video/mp4',
116
+ },
117
+ })
118
+
119
+ const mediaService = implement(media, {
120
+ download: ({ req, params }) =>
121
+ serveFile(req, { path: pathForId(params.id), filename: 'clip.mp4' }),
122
+ inspect: ({ req, params }) =>
123
+ serveFile(req, { path: pathForId(params.id), filename: 'clip.mp4' }),
124
+ })
125
+ ```
126
+
127
+ GET and HEAD are separate operations deliberately: declaring GET never creates
128
+ a hidden HEAD alias. Both travel through the normal contract router, params,
129
+ lifecycle/RBAC and request logging. A HEAD handler may inspect the raw query via
130
+ `ctx.req.url`, but cannot declare a request input schema or body.
131
+
132
+ `serveFile` returns `206` (range, with `Content-Range` + `Content-Length`), `200`
133
+ (full), `416` (unsatisfiable, `Content-Range: bytes */size`), `304`
134
+ (`If-None-Match` / `If-Modified-Since`), `404` (missing) or `405` (non GET/HEAD).
135
+ It always sets `Accept-Ranges: bytes`, a weak `ETag` and `Last-Modified` (so
136
+ `If-Range` keeps a changing file from being stitched from stale + fresh bytes),
137
+ and `nosniff`. `Content-Type` is auto-detected from the path — override it, or
138
+ pass `disposition` / `cacheControl` / `etag: false`, via the options.
139
+
140
+ Cross-origin, the browser lets JavaScript read only the CORS-safelisted response
141
+ headers. stitchkit therefore exposes the download-relevant ones by default
142
+ (`Content-Disposition`, `Content-Range`, `ETag`, …) — without that a `fetch`-based
143
+ download cannot recover the file's name. Override with `cors.exposeHeaders`
144
+ (extend `DEFAULT_CORS_EXPOSE_HEADERS` rather than replacing it), or pass `[]` to
145
+ emit none.
146
+
147
+ `serveFile` takes an explicit `path` and trusts it — **the caller owns
148
+ containment**. For a URL-derived path use `staticRoute` (which enforces it) or
149
+ `isWithinDir` first. The byte-range parser is exported on its own as
150
+ `parseByteRange(header, size)` for direct use and testing. → ADR 0023.
151
+
21
152
  ## Server primitives
22
153
 
23
154
  `stitchkit/server` also exports the primitives most APIs need. Each is a small,
package/llms/server.5.txt CHANGED
@@ -65,6 +65,7 @@ Also re-exports the error helpers from `stitchkit/contract`.
65
65
  | `parseBody` | function | parse + Zod-validate a JSON body → `data` or `null` (no throw) |
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
+ | `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
69
  | `UnixListenConfig` | _type_ | unix listener — a socket path, or `{ path, mode }` to tighten the file mode after listen |
69
70
  | `BunServerHandle` | _type_ | managed Bun handle (`url`, `port`, `runtime`, `status`, `shutdown`) |
70
71
  | `ManagedServerHandle` | _type_ | shared lifecycle shape generic over the runtime escape hatch |
package/llms/server.txt CHANGED
@@ -268,6 +268,7 @@ server. See [Testing & deployment](./testing-and-deployment.md).
268
268
  | `logging` | `true` for built-in request logs, or a `LoggingConfig` (see below) |
269
269
  | `traceId` | override per-request trace-id resolution — may return `undefined` to fall back |
270
270
  | `wrapFetch` | compose wrappers around the finished handler (request context, audit) |
271
+ | `proxyOrigin` | Bun TCP ingress policy: `{ trustedPeer(socketIp) }` restores the external scheme before all composition; independent of client-IP `trustProxy` |
271
272
  | `socket` | full Stitchkit Socket.IO handle; route, default WebSocket handler and shutdown are mounted once |
272
273
  | `websocket` | custom Bun WebSocket handler; with `socket`, this is the explicit composed handler |
273
274
  | `development` / `bun` | passthrough to `Bun.serve` |
@@ -276,6 +277,60 @@ Native Bun `routes` are intentionally not accepted: Bun matches them before
276
277
  `fetch`, so they could bypass shutdown admission. Use `rawRoutes`; they retain
277
278
  the Fetch `Request → Response` model and participate in lifecycle tracking.
278
279
 
280
+ ### External origin behind a trusted proxy
281
+
282
+ An HTTPS ingress often forwards plain HTTP to Bun. `trustProxy` trusts client-IP
283
+ headers only; it does not change `Request.url`. Opt into the separate URL policy:
284
+
285
+ ```ts
286
+ import { createServer, isLoopbackAddress } from 'stitchkit/server'
287
+
288
+ const server = createServer({
289
+ hostname: '127.0.0.1',
290
+ port: 3000,
291
+ proxyOrigin: { trustedPeer: isLoopbackAddress },
292
+ services,
293
+ })
294
+ ```
295
+
296
+ The predicate receives the **real TCP peer**, never `X-Forwarded-For` or
297
+ `X-Real-IP`. Match only the ingress you control. Loopback is appropriate only
298
+ when other local processes are trusted. Unmatched peers, missing peer metadata
299
+ and the default configuration ignore URL forwarding headers. Unix listeners have
300
+ no TCP peer and do not use this policy. This option belongs to Bun `createServer`,
301
+ not the Fetch-clean `createHandler` or Node `serveNode`.
302
+
303
+ The trusted ingress must overwrite `Host` with the external authority and
304
+ `X-Forwarded-Proto` with exactly `http` or `https`. It must remove `Forwarded`,
305
+ `X-Forwarded-Host` and `X-Forwarded-Port`; their presence, a missing/invalid
306
+ proto, a list of values or a malformed Host returns HTTP 400 before application
307
+ code. Header names are case-insensitive; proto values are lowercase. Multiple
308
+ ingress chains require a single sanitizing ingress at this boundary.
309
+
310
+ For an nginx HTTPS terminator, the relevant forwarding settings are:
311
+
312
+ ```nginx
313
+ proxy_set_header Host $http_host;
314
+ proxy_set_header X-Forwarded-Proto $scheme;
315
+ proxy_set_header Forwarded "";
316
+ proxy_set_header X-Forwarded-Host "";
317
+ proxy_set_header X-Forwarded-Port "";
318
+ ```
319
+
320
+ The normalized `Request.url` reaches `wrapFetch`, hooks, contracts, raw routes,
321
+ and the managed Socket.IO route. It preserves the path/query, wire authority
322
+ (including explicit ports), method, body, headers and linked cancellation signal.
323
+ Request cloning uses the normalized URL. The same runtime handle resolves
324
+ `requestIP`, `timeout` and `upgrade` against the original native request, retaining
325
+ WebSocket and shutdown behavior.
326
+
327
+ This is a scheme normalization policy, **not host authorization**. Applications
328
+ must retain their hostname/tenant allowlist and exact same-origin checks for
329
+ Origin and Referer. The TLS terminator must reject SNI/Host mismatches before
330
+ forwarding; a plain-HTTP Bun listener cannot observe the TLS SNI. The normalizer
331
+ does not accept `X-Forwarded-Host`, authorize a foreign Host, or treat HTTP and
332
+ HTTPS as aliases.
333
+
279
334
  ### Local daemon over a unix socket
280
335
 
281
336
  A local daemon whose door is a socket file needs no TCP port at all:
@@ -863,134 +918,3 @@ file: (ctx) => {
863
918
  `Response` is forwarded verbatim. Request headers are not relayed, so a `Range`
864
919
  sent to the proxy does not reach the origin and the full body comes back.
865
920
  → 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.
package/llms-full.txt CHANGED
@@ -1369,6 +1369,7 @@ server. See [Testing & deployment](./testing-and-deployment.md).
1369
1369
  | `logging` | `true` for built-in request logs, or a `LoggingConfig` (see below) |
1370
1370
  | `traceId` | override per-request trace-id resolution — may return `undefined` to fall back |
1371
1371
  | `wrapFetch` | compose wrappers around the finished handler (request context, audit) |
1372
+ | `proxyOrigin` | Bun TCP ingress policy: `{ trustedPeer(socketIp) }` restores the external scheme before all composition; independent of client-IP `trustProxy` |
1372
1373
  | `socket` | full Stitchkit Socket.IO handle; route, default WebSocket handler and shutdown are mounted once |
1373
1374
  | `websocket` | custom Bun WebSocket handler; with `socket`, this is the explicit composed handler |
1374
1375
  | `development` / `bun` | passthrough to `Bun.serve` |
@@ -1377,6 +1378,60 @@ Native Bun `routes` are intentionally not accepted: Bun matches them before
1377
1378
  `fetch`, so they could bypass shutdown admission. Use `rawRoutes`; they retain
1378
1379
  the Fetch `Request → Response` model and participate in lifecycle tracking.
1379
1380
 
1381
+ ### External origin behind a trusted proxy
1382
+
1383
+ An HTTPS ingress often forwards plain HTTP to Bun. `trustProxy` trusts client-IP
1384
+ headers only; it does not change `Request.url`. Opt into the separate URL policy:
1385
+
1386
+ ```ts
1387
+ import { createServer, isLoopbackAddress } from 'stitchkit/server'
1388
+
1389
+ const server = createServer({
1390
+ hostname: '127.0.0.1',
1391
+ port: 3000,
1392
+ proxyOrigin: { trustedPeer: isLoopbackAddress },
1393
+ services,
1394
+ })
1395
+ ```
1396
+
1397
+ The predicate receives the **real TCP peer**, never `X-Forwarded-For` or
1398
+ `X-Real-IP`. Match only the ingress you control. Loopback is appropriate only
1399
+ when other local processes are trusted. Unmatched peers, missing peer metadata
1400
+ and the default configuration ignore URL forwarding headers. Unix listeners have
1401
+ no TCP peer and do not use this policy. This option belongs to Bun `createServer`,
1402
+ not the Fetch-clean `createHandler` or Node `serveNode`.
1403
+
1404
+ The trusted ingress must overwrite `Host` with the external authority and
1405
+ `X-Forwarded-Proto` with exactly `http` or `https`. It must remove `Forwarded`,
1406
+ `X-Forwarded-Host` and `X-Forwarded-Port`; their presence, a missing/invalid
1407
+ proto, a list of values or a malformed Host returns HTTP 400 before application
1408
+ code. Header names are case-insensitive; proto values are lowercase. Multiple
1409
+ ingress chains require a single sanitizing ingress at this boundary.
1410
+
1411
+ For an nginx HTTPS terminator, the relevant forwarding settings are:
1412
+
1413
+ ```nginx
1414
+ proxy_set_header Host $http_host;
1415
+ proxy_set_header X-Forwarded-Proto $scheme;
1416
+ proxy_set_header Forwarded "";
1417
+ proxy_set_header X-Forwarded-Host "";
1418
+ proxy_set_header X-Forwarded-Port "";
1419
+ ```
1420
+
1421
+ The normalized `Request.url` reaches `wrapFetch`, hooks, contracts, raw routes,
1422
+ and the managed Socket.IO route. It preserves the path/query, wire authority
1423
+ (including explicit ports), method, body, headers and linked cancellation signal.
1424
+ Request cloning uses the normalized URL. The same runtime handle resolves
1425
+ `requestIP`, `timeout` and `upgrade` against the original native request, retaining
1426
+ WebSocket and shutdown behavior.
1427
+
1428
+ This is a scheme normalization policy, **not host authorization**. Applications
1429
+ must retain their hostname/tenant allowlist and exact same-origin checks for
1430
+ Origin and Referer. The TLS terminator must reject SNI/Host mismatches before
1431
+ forwarding; a plain-HTTP Bun listener cannot observe the TLS SNI. The normalizer
1432
+ does not accept `X-Forwarded-Host`, authorize a foreign Host, or treat HTTP and
1433
+ HTTPS as aliases.
1434
+
1380
1435
  ### Local daemon over a unix socket
1381
1436
 
1382
1437
  A local daemon whose door is a socket file needs no TCP port at all:
@@ -9773,6 +9828,23 @@ string, the schema says what it should be:
9773
9828
  | `z.record(z.string(), z.unknown())` | `--data '{"code":"007"}'` or `--data.code=007` → `"007"` |
9774
9829
  | `.optional()` / `.default()` | not required |
9775
9830
 
9831
+ Use the inline `:=` form to assign a whole JSON value without scalar coercion:
9832
+
9833
+ ```bash
9834
+ myapp upsert --productId:=null --count:=null
9835
+ myapp upsert --productId null # the literal string "null"
9836
+ myapp upsert --productId:='"null"' # the same string, written as JSON
9837
+ ```
9838
+
9839
+ `null` is accepted only when the unchanged command schema permits it. Omitting an
9840
+ optional field leaves it absent; it does not imply `null`. The same syntax works
9841
+ for application global options and dotted leaves (`--settings.note:=null`). A
9842
+ JSON assignment supplies the whole field, so an array uses one assignment such as
9843
+ `--tags:='["a","b"]'`; mixing it with repeated element flags or another assignment
9844
+ is refused. Invalid JSON is a usage error before the handler runs. Framework
9845
+ options such as `--json` retain their existing grammar. An exact declared field
9846
+ name ending in `:` retains its existing inline spelling.
9847
+
9776
9848
  Primitive literals use the same coercion through optional, nullable and default wrappers and as
9777
9849
  array elements. A literal set whose members have different primitive types stays lexical because
9778
9850
  argv has no type tag: for `z.literal([7, '007'])`, `--value 007` remains the string `"007"` rather
@@ -10635,11 +10707,16 @@ the code (so `exitCodes` can map it instead of every remote failure exiting `1`)
10635
10707
  the message the operator needs, and the error's own class — a plain `Error` is
10636
10708
  an *unexpected* error to the runner, which would print a code frame of the framework
10637
10709
  bundle before the JSON failure and scrub it to `INTERNAL_SERVER_ERROR`.
10638
- A structured `{ error, details }` body is relayed as the contract error it
10710
+ A `tools/call` response with `isError: true` and a structured `{ error, details }` body is relayed as the contract error it
10639
10711
  is, on every transport; anything else fails as `UPSTREAM_TOOL_ERROR` carrying
10640
10712
  what the server did send. Not `INTERNAL_SERVER_ERROR`, because nothing of ours
10641
10713
  broke.
10642
10714
 
10715
+ The CLI writes this refusal to stderr, leaves stdout empty and exits with the
10716
+ mapped nonzero error code (`1` for an unmapped code). A successful response may
10717
+ contain an ordinary field named `error`; its payload alone does not establish a
10718
+ tool failure.
10719
+
10643
10720
  **On the CLI a discovered command prints the answer, not the envelope.**
10644
10721
  `tools/call` returns `{ content: [...], structuredContent? }`, and an agent mount
10645
10722
  needs exactly that — the parts are what a model is shown. The CLI is different in
@@ -21223,6 +21300,7 @@ Also re-exports the error helpers from `stitchkit/contract`.
21223
21300
  | `parseBody` | function | parse + Zod-validate a JSON body → `data` or `null` (no throw) |
21224
21301
  | `HandlerConfig` | _type_ | config for `createHandler`, including optional `maxJsonBodyBytes`; bound to `BunServer` on this entrypoint |
21225
21302
  | `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) |
21303
+ | `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) |
21226
21304
  | `UnixListenConfig` | _type_ | unix listener — a socket path, or `{ path, mode }` to tighten the file mode after listen |
21227
21305
  | `BunServerHandle` | _type_ | managed Bun handle (`url`, `port`, `runtime`, `status`, `shutdown`) |
21228
21306
  | `ManagedServerHandle` | _type_ | shared lifecycle shape generic over the runtime escape hatch |
@@ -23158,7 +23236,7 @@ SDK nor the `ai` peer.
23158
23236
  |--------|------|---------|
23159
23237
  | `createCli` | function | build and run a CLI from contracts — [guide](../guide/cli.md) |
23160
23238
  | `defineCliCommand` | function | define one Zod-typed CLI-only executable command with optional validated-result presentation/exit policy |
23161
- | `parseCliArgs` | function | argv → typed tool args against a schema (advanced) |
23239
+ | `parseCliArgs` | function | argv → typed tool args against a schema; inline `--field:=JSON` assigns a whole JSON value, including null, while ordinary string flags remain lexical (advanced) |
23162
23240
  | `routeCliArgv` | function | select the command out of argv without duplicating the global-option grammar (advanced) |
23163
23241
  | `extractCliGlobalOptions` | function | lift the application's own global options out of argv before routing (advanced) |
23164
23242
  | `coerceJsonArgs` | function | the second half of `parseCliArgs` — parse array/object values a consumer sends itself, including discovered MCP object guards restored from JSON Schema, without the `stitchkit/tools` barrel |
package/llms.txt CHANGED
@@ -18,12 +18,12 @@ Load the slice for each entrypoint your code imports — `import … from 'stitc
18
18
  - **stitchkit/tools/contract**: [llms/tools-contract.txt](llms/tools-contract.txt) 0.2 KB
19
19
  - **stitchkit/tools/invoker**: [llms/tools-invoker.txt](llms/tools-invoker.txt) 0.2 KB
20
20
  - **stitchkit/tools/connections**: [llms/tools-connections.txt](llms/tools-connections.txt) 3.7 KB
21
- - **stitchkit/cli**: [llms/cli.txt](llms/cli.txt) 43.8 KB → [llms/cli.2.txt](llms/cli.2.txt) 20.2 KB
21
+ - **stitchkit/cli**: [llms/cli.txt](llms/cli.txt) 44.6 KB → [llms/cli.2.txt](llms/cli.2.txt) 20.6 KB
22
22
  - **stitchkit/cli/publish**: [llms/cli-publish.txt](llms/cli-publish.txt) 1.5 KB
23
23
  - **stitchkit/remote**: [llms/remote.txt](llms/remote.txt) 1.0 KB
24
24
  - **stitchkit/contract**: [llms/contract.txt](llms/contract.txt) 31.9 KB
25
25
  - **stitchkit/primitives**: [llms/primitives.txt](llms/primitives.txt) 11.7 KB
26
- - **stitchkit/server**: [llms/server.txt](llms/server.txt) 45.3 KB → [llms/server.2.txt](llms/server.2.txt) 14.6 KB → [llms/server.3.txt](llms/server.3.txt) 45.6 KB → [llms/server.4.txt](llms/server.4.txt) 38.3 KB → [llms/server.5.txt](llms/server.5.txt) 22.1 KB
26
+ - **stitchkit/server**: [llms/server.txt](llms/server.txt) 42.2 KB → [llms/server.2.txt](llms/server.2.txt) 20.3 KB → [llms/server.3.txt](llms/server.3.txt) 45.6 KB → [llms/server.4.txt](llms/server.4.txt) 38.3 KB → [llms/server.5.txt](llms/server.5.txt) 22.3 KB
27
27
  - **stitchkit/observability**: [llms/observability.txt](llms/observability.txt) 45.9 KB
28
28
  - **stitchkit/agent-runtime**: [llms/agent-runtime.txt](llms/agent-runtime.txt) 42.7 KB → [llms/agent-runtime.2.txt](llms/agent-runtime.2.txt) 46.7 KB → [llms/agent-runtime.3.txt](llms/agent-runtime.3.txt) 35.6 KB
29
29
  - **stitchkit/agent-runtime/testing**: [llms/agent-runtime-testing.txt](llms/agent-runtime-testing.txt) 2.6 KB
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "stitchkit",
3
- "version": "0.110.4",
3
+ "version": "0.110.6",
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",