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.
- package/CHANGELOG.md +17 -0
- package/dist/agent-runtime/branch-observations.d.ts +16 -16
- package/dist/entrypoints/agent-runtime/coding-tools.js +2 -2
- package/dist/entrypoints/agent-runtime/sandbox.js +2 -2
- package/dist/entrypoints/agent-runtime/sqlite/bun.js +4 -4
- package/dist/entrypoints/agent-runtime/sqlite/node.js +4 -4
- package/dist/entrypoints/agent-runtime/testing.js +4 -4
- package/dist/entrypoints/agent-runtime.js +4 -4
- package/dist/entrypoints/cli.js +1 -1
- package/dist/entrypoints/process.js +1 -1
- package/dist/entrypoints/server.d.ts +1 -0
- package/dist/entrypoints/server.d.ts.map +1 -1
- package/dist/entrypoints/server.js +55 -3
- package/dist/entrypoints/tools.js +1 -1
- package/dist/{index-mpg1t7nv.js → index-0z3kfphz.js} +1 -1
- package/dist/{index-5pbtbce0.js → index-7c957abz.js} +1 -1
- package/dist/{index-e1sjcjfy.js → index-7r4knwf8.js} +94 -23
- package/dist/{index-bm180f2d.js → index-ey15wx6j.js} +1 -1
- package/dist/{index-9ket8wzx.js → index-mnk5vgdr.js} +1 -1
- package/dist/{index-2tazbwhv.js → index-n7hrpngg.js} +151 -92
- package/dist/process/group-diagnostics.d.ts +20 -0
- package/dist/process/group-diagnostics.d.ts.map +1 -0
- package/dist/process/group-settlement.d.ts +1 -1
- package/dist/process/group-settlement.d.ts.map +1 -1
- package/dist/process/group.d.ts.map +1 -1
- package/dist/server/bun.d.ts +9 -0
- package/dist/server/bun.d.ts.map +1 -1
- package/dist/server/proxy-origin.d.ts +12 -0
- package/dist/server/proxy-origin.d.ts.map +1 -0
- package/dist/tools/cli/args-json.d.ts +12 -0
- package/dist/tools/cli/args-json.d.ts.map +1 -0
- package/dist/tools/cli/args-tokens.d.ts +27 -0
- package/dist/tools/cli/args-tokens.d.ts.map +1 -0
- package/dist/tools/cli/args.d.ts +4 -0
- package/dist/tools/cli/args.d.ts.map +1 -1
- package/dist/tools/cli/help.d.ts.map +1 -1
- package/llms/cli.2.txt +7 -2
- package/llms/cli.txt +17 -0
- package/llms/server.2.txt +131 -0
- package/llms/server.5.txt +1 -0
- package/llms/server.txt +55 -131
- package/llms-full.txt +80 -2
- package/llms.txt +2 -2
- 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)
|
|
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)
|
|
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