stitchkit 0.45.0 → 0.47.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.
- package/README.md +5 -4
- package/dist/browser/cancellation.d.ts +14 -0
- package/dist/browser/cancellation.d.ts.map +1 -0
- package/dist/browser/client-multipart.d.ts +3 -1
- package/dist/browser/client-multipart.d.ts.map +1 -1
- package/dist/browser/client.d.ts +1 -0
- package/dist/browser/client.d.ts.map +1 -1
- package/dist/browser/http.d.ts +1 -0
- package/dist/browser/http.d.ts.map +1 -1
- package/dist/browser/socket-io.d.ts +3 -1
- package/dist/browser/socket-io.d.ts.map +1 -1
- package/dist/cli.js +3 -4
- package/dist/contract/define.d.ts +59 -15
- package/dist/contract/define.d.ts.map +1 -1
- package/dist/contract/errors.d.ts +1 -0
- package/dist/contract/errors.d.ts.map +1 -1
- package/dist/contract/index.d.ts +1 -1
- package/dist/contract/index.d.ts.map +1 -1
- package/dist/contract/index.js +2 -2
- package/dist/{index-w0mec79h.js → index-44xysy8r.js} +697 -301
- package/dist/{index-r6czv7yt.js → index-45dz4m51.js} +50 -3
- package/dist/index-8ekq6res.js +181 -0
- package/dist/{index-yh0p5ddn.js → index-ee621cmy.js} +39 -19
- package/dist/{index-mvjyq9h9.js → index-kp8xamqp.js} +309 -144
- package/dist/{index-36f1xkdb.js → index-nrytvb30.js} +5 -4
- package/dist/{index-6jypn22c.js → index-r1qp4rve.js} +9 -1
- package/dist/{index-s4qsmgwe.js → index-tss6bk5c.js} +4 -1
- package/dist/index-yxpe3phd.js +63 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +315 -134
- package/dist/internal/errors.d.ts.map +1 -1
- package/dist/internal/secure-fetch.d.ts +23 -3
- package/dist/internal/secure-fetch.d.ts.map +1 -1
- package/dist/internal/typed.d.ts +7 -0
- package/dist/internal/typed.d.ts.map +1 -1
- package/dist/internal/within-dir.d.ts +6 -0
- package/dist/internal/within-dir.d.ts.map +1 -1
- package/dist/logger.d.ts +8 -0
- package/dist/logger.d.ts.map +1 -0
- package/dist/node.js +3 -4
- package/dist/observability/audit.d.ts +22 -3
- package/dist/observability/audit.d.ts.map +1 -1
- package/dist/observability/event.d.ts +7 -0
- package/dist/observability/event.d.ts.map +1 -1
- package/dist/observability/index.d.ts +1 -1
- package/dist/observability/index.d.ts.map +1 -1
- package/dist/observability/index.js +130 -134
- package/dist/observability/sanitize.d.ts +8 -1
- package/dist/observability/sanitize.d.ts.map +1 -1
- package/dist/observability/trace.d.ts +3 -1
- package/dist/observability/trace.d.ts.map +1 -1
- package/dist/react/cache-bridge.d.ts +4 -0
- package/dist/react/cache-bridge.d.ts.map +1 -1
- package/dist/react.js +46 -5
- package/dist/realtime/contract.d.ts +4 -1
- package/dist/realtime/contract.d.ts.map +1 -1
- package/dist/realtime/rejection.d.ts +10 -0
- package/dist/realtime/rejection.d.ts.map +1 -0
- package/dist/realtime/socket.d.ts +14 -1
- package/dist/realtime/socket.d.ts.map +1 -1
- package/dist/server/cache.d.ts +5 -1
- package/dist/server/cache.d.ts.map +1 -1
- package/dist/server/context.d.ts +8 -5
- package/dist/server/context.d.ts.map +1 -1
- package/dist/server/create.d.ts.map +1 -1
- package/dist/server/error-hook.d.ts +1 -0
- package/dist/server/error-hook.d.ts.map +1 -1
- package/dist/server/file.d.ts +6 -0
- package/dist/server/file.d.ts.map +1 -1
- package/dist/server/implement.d.ts +33 -2
- package/dist/server/implement.d.ts.map +1 -1
- package/dist/server/index.d.ts +5 -6
- package/dist/server/index.d.ts.map +1 -1
- package/dist/server/index.js +133 -14
- package/dist/server/middleware/auth.d.ts +7 -4
- package/dist/server/middleware/auth.d.ts.map +1 -1
- package/dist/server/middleware/cors.d.ts.map +1 -1
- package/dist/server/multipart.d.ts +13 -18
- package/dist/server/multipart.d.ts.map +1 -1
- package/dist/server/openapi.d.ts.map +1 -1
- package/dist/server/realtime.d.ts +2 -0
- package/dist/server/realtime.d.ts.map +1 -1
- package/dist/server/router.d.ts +0 -8
- package/dist/server/router.d.ts.map +1 -1
- package/dist/server/stream.d.ts.map +1 -1
- package/dist/server/types.d.ts +50 -22
- package/dist/server/types.d.ts.map +1 -1
- package/dist/tools/cli-args.d.ts +11 -3
- package/dist/tools/cli-args.d.ts.map +1 -1
- package/dist/tools/cli.d.ts +5 -0
- package/dist/tools/cli.d.ts.map +1 -1
- package/dist/tools/coerce.d.ts.map +1 -1
- package/dist/tools/json-schema.d.ts.map +1 -1
- package/dist/tools/mcp-handler.d.ts +4 -3
- package/dist/tools/mcp-handler.d.ts.map +1 -1
- package/dist/tools/mcp-prepare.d.ts +95 -0
- package/dist/tools/mcp-prepare.d.ts.map +1 -0
- package/dist/tools/mcp-round.d.ts +4 -0
- package/dist/tools/mcp-round.d.ts.map +1 -1
- package/dist/tools/mcp-stdio.d.ts +2 -1
- package/dist/tools/mcp-stdio.d.ts.map +1 -1
- package/dist/tools/mcp.d.ts +2 -78
- package/dist/tools/mcp.d.ts.map +1 -1
- package/dist/tools/mount-download.d.ts +5 -0
- package/dist/tools/mount-download.d.ts.map +1 -1
- package/dist/tools/native-mcp.d.ts +1 -1
- package/dist/tools/native-mcp.d.ts.map +1 -1
- package/dist/tools/oauth-provider.d.ts +20 -1
- package/dist/tools/oauth-provider.d.ts.map +1 -1
- package/dist/tools/tool-logger.d.ts +1 -1
- package/dist/tools/tool-logger.d.ts.map +1 -1
- package/dist/tools/view-file.d.ts +9 -0
- package/dist/tools/view-file.d.ts.map +1 -1
- package/dist/tools.d.ts +2 -1
- package/dist/tools.d.ts.map +1 -1
- package/dist/tools.js +505 -278
- package/llms-full.txt +473 -85
- package/llms.txt +1 -1
- package/package.json +2 -2
- package/dist/index-frfyw9fa.js +0 -33
- package/dist/index-x3fcszf8.js +0 -8
package/llms-full.txt
CHANGED
|
@@ -253,7 +253,7 @@ export const users = defineContract({ prefix: 'users' }, {
|
|
|
253
253
|
| `scope` | no | access scope for this endpoint — see [Auth & errors](./auth-and-errors.md) |
|
|
254
254
|
| `expose` | no | which transports carry this endpoint — see [below](#transports) |
|
|
255
255
|
| `toolName` | no | explicit MCP / agent tool name (default: a verb-aware derivation, see below — not a literal `prefix_key`) |
|
|
256
|
-
| `multipart` | no |
|
|
256
|
+
| `multipart` | no | typed file fields, cardinality, delivery and upload policy — see [below](#file-uploads) |
|
|
257
257
|
| `maxJsonBodyBytes` | no | per-route JSON body ceiling; overrides the server default |
|
|
258
258
|
| `timeout` | no | per-endpoint client timeout in ms, for slow endpoints |
|
|
259
259
|
| `idempotent` | no | safe to call twice with the same input (like `PUT`/`DELETE`); a retrying transport reads it — see [Realtime](./realtime.md#bring-your-own-transport) |
|
|
@@ -474,21 +474,61 @@ it to curate a public spec (e.g. `meta: { public: true }`), without ever emittin
|
|
|
474
474
|
|
|
475
475
|
## File uploads
|
|
476
476
|
|
|
477
|
-
`multipart`
|
|
478
|
-
|
|
477
|
+
`multipart` is the single source of truth for file fields, cardinality and
|
|
478
|
+
transport-level upload policy. Buffered delivery (the default) gives the
|
|
479
|
+
handler a typed `ctx.files` map:
|
|
479
480
|
|
|
480
481
|
```ts
|
|
481
|
-
|
|
482
|
+
uploadAttachments: {
|
|
482
483
|
method: 'POST',
|
|
483
|
-
path: '/
|
|
484
|
-
desc: 'Upload
|
|
485
|
-
|
|
486
|
-
|
|
484
|
+
path: '/:answerId/attachments',
|
|
485
|
+
desc: 'Upload attachments',
|
|
486
|
+
params: AnswerIdParamsSchema,
|
|
487
|
+
input: UploadMetadataSchema,
|
|
488
|
+
output: UploadedAttachmentsSchema,
|
|
489
|
+
multipart: {
|
|
490
|
+
maxRequestBytes: 120 * 1024 * 1024,
|
|
491
|
+
maxFieldBytes: 64 * 1024,
|
|
492
|
+
files: {
|
|
493
|
+
cover: {
|
|
494
|
+
required: false,
|
|
495
|
+
maxBytes: 10 * 1024 * 1024,
|
|
496
|
+
contentTypes: ['image/*'],
|
|
497
|
+
},
|
|
498
|
+
attachments: {
|
|
499
|
+
multiple: true,
|
|
500
|
+
maxFiles: 8,
|
|
501
|
+
maxBytes: 20 * 1024 * 1024,
|
|
502
|
+
contentTypes: ['image/*', 'application/pdf'],
|
|
503
|
+
},
|
|
504
|
+
},
|
|
505
|
+
},
|
|
487
506
|
}
|
|
488
507
|
```
|
|
489
508
|
|
|
490
|
-
|
|
491
|
-
|
|
509
|
+
```ts
|
|
510
|
+
uploadAttachments: ({ params, input, files }) => {
|
|
511
|
+
files.cover // File | undefined
|
|
512
|
+
files.attachments // File[] in multipart order
|
|
513
|
+
}
|
|
514
|
+
```
|
|
515
|
+
|
|
516
|
+
`required` defaults to `true`; `multiple` defaults to `false`. `maxFiles` is
|
|
517
|
+
valid only for a multiple field. `contentTypes` accepts exact media types and
|
|
518
|
+
validated type wildcards such as `image/*`; it checks the declared multipart
|
|
519
|
+
header, not file contents. Content sniffing, antivirus and storage policy stay
|
|
520
|
+
in the application.
|
|
521
|
+
|
|
522
|
+
The typed client accepts a web `Blob`/`File` or React Native
|
|
523
|
+
`{ uri, name, type }` descriptor for each single value, and arrays for multiple
|
|
524
|
+
fields. It appends repeated multipart field names in stable order. Undeclared
|
|
525
|
+
file fields, duplicate single fields, missing required fields and wrong part
|
|
526
|
+
kinds fail before the handler. Multipart endpoints are HTTP-only.
|
|
527
|
+
|
|
528
|
+
Set `delivery: 'stream'` when files must go directly to consumer-owned storage
|
|
529
|
+
without becoming `File` objects in framework memory. The contract descriptor
|
|
530
|
+
stays the same; the implementation supplies receivers with Web streams. See
|
|
531
|
+
[HTTP server → multipart](./server.md#multipart).
|
|
492
532
|
|
|
493
533
|
### Multipart text fields
|
|
494
534
|
|
|
@@ -622,7 +662,7 @@ Every handler receives one `ctx` argument:
|
|
|
622
662
|
|-------------|------|--------|
|
|
623
663
|
| `params` | inferred from `params` schema | parsed path params |
|
|
624
664
|
| `input` | inferred from `input` schema | parsed body / query |
|
|
625
|
-
| `
|
|
665
|
+
| `files` | inferred `File` map or receiver values | the endpoint's typed multipart fields |
|
|
626
666
|
| `source` | `'http' \| 'mcp' \| 'agent'` | the transport that invoked the handler |
|
|
627
667
|
| `traceId` | `string` | per-request trace id |
|
|
628
668
|
| `ipAddress` | `string` | caller IP |
|
|
@@ -674,10 +714,9 @@ server. See [Testing & deployment](./testing-and-deployment.md).
|
|
|
674
714
|
| `groups` | route groups — a shared path prefix and hooks (see below) |
|
|
675
715
|
| `scopePrefixes` | `scope → path prefix` map — mount `services` by `service.scope` (see below) |
|
|
676
716
|
| `rawRoutes` | non-contract routes (see below) |
|
|
677
|
-
| `maxUploadBytes` | default multipart upload cap (bytes); per-route `EndpointDef.maxUploadBytes` overrides |
|
|
678
717
|
| `maxJsonBodyBytes` | optional JSON body cap (bytes); per-route value overrides; unset preserves existing behaviour |
|
|
679
718
|
| `port` / `hostname` | listen address — port defaults to `3000` |
|
|
680
|
-
| `cors` | CORS policy — `{ origin, credentials, methods, headers, exposeHeaders }` |
|
|
719
|
+
| `cors` | CORS policy — `{ origin, credentials, methods, headers, exposeHeaders }`. `origin` is **required** when `cors` is present: pass an explicit origin (or list), or `'*'` to deliberately allow every origin — an origin-less config is a construction error, never a silent wildcard. Omit `cors` entirely to emit no CORS headers. |
|
|
681
720
|
| `hooks` | lifecycle hooks (see below) |
|
|
682
721
|
| `logging` | `true` for built-in request logs, or a `LoggingConfig` (see below) |
|
|
683
722
|
| `traceId` | override per-request trace-id resolution — may return `undefined` to fall back |
|
|
@@ -685,6 +724,34 @@ server. See [Testing & deployment](./testing-and-deployment.md).
|
|
|
685
724
|
| `websocket` | Bun WebSocket handlers — e.g. from `createSocketIOServer` |
|
|
686
725
|
| `routes` / `development` / `bun` | passthrough to `Bun.serve` |
|
|
687
726
|
|
|
727
|
+
### Trusted HTTPS in development
|
|
728
|
+
|
|
729
|
+
Stitchkit does not own local certificate generation or a frontend development
|
|
730
|
+
server. When a device or browser feature requires a secure context, generate a
|
|
731
|
+
trusted certificate outside the application (for example with `mkcert`) and pass
|
|
732
|
+
its files through the Bun server boundary:
|
|
733
|
+
|
|
734
|
+
```ts
|
|
735
|
+
import { readFileSync } from 'node:fs'
|
|
736
|
+
import { createServer } from 'stitchkit/server'
|
|
737
|
+
|
|
738
|
+
createServer({
|
|
739
|
+
services,
|
|
740
|
+
hostname: '0.0.0.0',
|
|
741
|
+
bun: {
|
|
742
|
+
tls: {
|
|
743
|
+
cert: readFileSync('./certs/dev.pem'),
|
|
744
|
+
key: readFileSync('./certs/dev-key.pem'),
|
|
745
|
+
},
|
|
746
|
+
},
|
|
747
|
+
})
|
|
748
|
+
```
|
|
749
|
+
|
|
750
|
+
Trust the certificate authority on each test device and configure the frontend's
|
|
751
|
+
HTTPS mode in that frontend project. Certificate renewal, interface discovery
|
|
752
|
+
and device onboarding remain application infrastructure rather than framework
|
|
753
|
+
or starter behavior.
|
|
754
|
+
|
|
688
755
|
### Request logging
|
|
689
756
|
|
|
690
757
|
`logging: true` is shorthand for `logging: {}` — **any object turns logging
|
|
@@ -766,7 +833,7 @@ A group gives a set of services a shared path prefix and its own hooks:
|
|
|
766
833
|
createServer({
|
|
767
834
|
groups: [
|
|
768
835
|
{ pathPrefix: '/api', services: [usersService, postsService] },
|
|
769
|
-
{ pathPrefix: '/api/admin', services: [adminService], hooks: {
|
|
836
|
+
{ pathPrefix: '/api/admin', services: [adminService], hooks: { authorize: adminAuth } },
|
|
770
837
|
],
|
|
771
838
|
})
|
|
772
839
|
```
|
|
@@ -782,7 +849,7 @@ or resource-scoped API:
|
|
|
782
849
|
```ts
|
|
783
850
|
createServer({
|
|
784
851
|
groups: [
|
|
785
|
-
{ pathPrefix: '/tenants/:tenantId', services: [widgetsService], hooks: {
|
|
852
|
+
{ pathPrefix: '/tenants/:tenantId', services: [widgetsService], hooks: { authorize: auth } },
|
|
786
853
|
],
|
|
787
854
|
})
|
|
788
855
|
// widgetsService (prefix 'widgets') → /tenants/:tenantId/widgets/...
|
|
@@ -791,8 +858,8 @@ createServer({
|
|
|
791
858
|
**Where the prefix param lands.** The router matches the *full* path (group
|
|
792
859
|
prefix + service prefix + endpoint path) and collects every `:param` — from the
|
|
793
860
|
prefix and from the endpoint alike — into one set. Each is spread onto the
|
|
794
|
-
context root, so it is available as **`ctx.tenantId`** (a raw `string`) in
|
|
795
|
-
|
|
861
|
+
context root, so it is available as **`ctx.tenantId`** (a raw `string`) in the
|
|
862
|
+
`authorize` hook, handler and later lifecycle hooks:
|
|
796
863
|
|
|
797
864
|
```ts
|
|
798
865
|
beforeHandle: (ctx) => {
|
|
@@ -856,14 +923,17 @@ Scope stays a free string; the core attaches no meaning beyond this lookup
|
|
|
856
923
|
|
|
857
924
|
## Lifecycle hooks
|
|
858
925
|
|
|
859
|
-
|
|
926
|
+
Five hooks wrap every contract request. Route matching and path-param
|
|
927
|
+
validation happen before authorization; body/query parsing happens only after
|
|
928
|
+
authorization succeeds:
|
|
860
929
|
|
|
861
930
|
```ts
|
|
862
931
|
createServer({
|
|
863
932
|
services,
|
|
864
933
|
hooks: {
|
|
865
934
|
onRequest(req) { /* logging, global rate limit — may return a Response to short-circuit */ },
|
|
866
|
-
|
|
935
|
+
authorize(ctx, endpoint) { /* identity + scope, before body reads — throw to reject */ },
|
|
936
|
+
beforeHandle(ctx, endpoint) { /* validated-input preconditions */ },
|
|
867
937
|
afterHandle(ctx, result, ep) { /* transform the result data */ },
|
|
868
938
|
onError(ctx, error, ep) { /* custom error response — return a Response */ },
|
|
869
939
|
},
|
|
@@ -872,9 +942,13 @@ createServer({
|
|
|
872
942
|
|
|
873
943
|
- **`onRequest`** — runs first, with the raw `Request`. Return a `Response` to
|
|
874
944
|
short-circuit (a rate-limit 429, a redirect); return nothing to continue.
|
|
875
|
-
- **`
|
|
876
|
-
|
|
877
|
-
|
|
945
|
+
- **`authorize`** — runs after route matching and validated path params, but
|
|
946
|
+
before query, JSON or multipart parsing. It receives request metadata and
|
|
947
|
+
params, with `input: undefined` and no files. This is the HTTP home of
|
|
948
|
+
[`createAuthHook`](./auth-and-errors.md#createauthhook).
|
|
949
|
+
- **`beforeHandle`** — runs after the complete context has been parsed and
|
|
950
|
+
validated, immediately before the handler. Put input-dependent application
|
|
951
|
+
preconditions here, not authentication.
|
|
878
952
|
- **`afterHandle`** — receives the handler result; return a replacement to
|
|
879
953
|
transform it.
|
|
880
954
|
- **`onError`** — receives any thrown error; return a `Response` to customise
|
|
@@ -1177,7 +1251,7 @@ focused helper — not a sub-framework.
|
|
|
1177
1251
|
|--------|------|
|
|
1178
1252
|
| `serveFile()` | serve a file with `Range` / `304` / `HEAD` (media seeking) |
|
|
1179
1253
|
| `streamSSE()` | turn an `AsyncGenerator` into a Server-Sent-Events `Response` |
|
|
1180
|
-
| `parseMultipart()` | parse a
|
|
1254
|
+
| `parseMultipart()` | parse a typed buffered/streaming multipart descriptor |
|
|
1181
1255
|
| `createRateLimiter()` | per-key token-bucket rate limiting |
|
|
1182
1256
|
| `createCache()` + `cacheHeaders()` | in-memory TTL cache; `Cache-Control` builder |
|
|
1183
1257
|
| `createEventBus<EventMap>()` | typed in-process pub/sub |
|
|
@@ -1204,36 +1278,95 @@ The client side is [`parseSSE`](./client.md#sse).
|
|
|
1204
1278
|
|
|
1205
1279
|
### Multipart
|
|
1206
1280
|
|
|
1207
|
-
|
|
1208
|
-
import { parseMultipart } from 'stitchkit/server'
|
|
1281
|
+
The contract owns one descriptor for buffered and streaming delivery:
|
|
1209
1282
|
|
|
1210
|
-
|
|
1283
|
+
```ts
|
|
1284
|
+
const uploads = defineContract({ prefix: 'uploads' }, {
|
|
1285
|
+
create: {
|
|
1286
|
+
method: 'POST', path: '/', desc: 'Upload media',
|
|
1287
|
+
input: UploadMetadataSchema,
|
|
1288
|
+
output: UploadedMediaSchema,
|
|
1289
|
+
multipart: {
|
|
1290
|
+
maxRequestBytes: 220 * 1024 * 1024,
|
|
1291
|
+
maxFieldBytes: 64 * 1024,
|
|
1292
|
+
files: {
|
|
1293
|
+
cover: { required: false, maxBytes: 10 * 1024 * 1024, contentTypes: ['image/*'] },
|
|
1294
|
+
media: { multiple: true, maxFiles: 4, maxBytes: 50 * 1024 * 1024 },
|
|
1295
|
+
},
|
|
1296
|
+
},
|
|
1297
|
+
},
|
|
1298
|
+
})
|
|
1211
1299
|
```
|
|
1212
1300
|
|
|
1213
|
-
|
|
1214
|
-
and
|
|
1215
|
-
|
|
1301
|
+
Buffered delivery is the default. Each file becomes a Web `File`; single,
|
|
1302
|
+
optional and multiple cardinality is inferred on `ctx.files`. The request cap
|
|
1303
|
+
defaults to **25 MB** when omitted. The parser measures actual bytes including
|
|
1304
|
+
boundaries and headers instead of trusting `Content-Length`; per-file bytes,
|
|
1305
|
+
file count, text-field bytes and declared MIME policy are enforced while
|
|
1306
|
+
reading. Text fields remain strings until the endpoint's Zod `input` parses
|
|
1307
|
+
them.
|
|
1216
1308
|
|
|
1217
|
-
|
|
1218
|
-
`
|
|
1219
|
-
|
|
1309
|
+
For large files, set `delivery: 'stream'` and define receivers. A receiver gets
|
|
1310
|
+
a Web `ReadableStream<Uint8Array>` and writes directly to consumer-owned
|
|
1311
|
+
storage; Stitchkit holds only bounded parser state:
|
|
1220
1312
|
|
|
1221
1313
|
```ts
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1314
|
+
const streamingUploads = defineContract({ prefix: 'uploads' }, {
|
|
1315
|
+
create: {
|
|
1316
|
+
method: 'POST', path: '/', desc: 'Upload media',
|
|
1317
|
+
input: UploadMetadataSchema,
|
|
1318
|
+
output: UploadedMediaSchema,
|
|
1319
|
+
multipart: {
|
|
1320
|
+
delivery: 'stream',
|
|
1321
|
+
maxRequestBytes: 260 * 1024 * 1024,
|
|
1322
|
+
files: { media: { maxBytes: 250 * 1024 * 1024, contentTypes: ['video/*'] } },
|
|
1323
|
+
},
|
|
1324
|
+
},
|
|
1325
|
+
})
|
|
1225
1326
|
|
|
1226
|
-
|
|
1227
|
-
|
|
1327
|
+
const service = implement(streamingUploads, {
|
|
1328
|
+
create: defineMultipartStream(streamingUploads.endpoints.create, {
|
|
1329
|
+
files: {
|
|
1330
|
+
media: async ({ metadata, stream, signal }) => {
|
|
1331
|
+
const stored = await storage.write({ metadata, stream, signal })
|
|
1332
|
+
return {
|
|
1333
|
+
value: stored,
|
|
1334
|
+
cleanup: () => storage.remove(stored.key),
|
|
1335
|
+
}
|
|
1336
|
+
},
|
|
1337
|
+
},
|
|
1338
|
+
handler: ({ input, files }) => mediaService.attach(input, files.media),
|
|
1339
|
+
}),
|
|
1340
|
+
})
|
|
1228
1341
|
```
|
|
1229
1342
|
|
|
1343
|
+
Receivers run sequentially in multipart order. A multiple receiver runs once
|
|
1344
|
+
per part and the handler gets an ordered value array. `cleanup` is registered
|
|
1345
|
+
as soon as a receiver materialises external state. Disconnect, size/policy
|
|
1346
|
+
failure, a later receiver/text validation failure or handler failure rolls
|
|
1347
|
+
accepted handles back exactly once in reverse order. After handler success,
|
|
1348
|
+
ownership transfers to the application.
|
|
1349
|
+
|
|
1350
|
+
Authorization always completes before the multipart parser or any receiver is
|
|
1351
|
+
started. The receiver signal is tied to the request abort. The core does not
|
|
1352
|
+
provide filesystem/S3 adapters, retries, antivirus or a distributed
|
|
1353
|
+
storage/database transaction; those policies belong to the application.
|
|
1354
|
+
|
|
1355
|
+
`parseMultipart(req, descriptor, fieldsSchema?, receivers?)` is also exported
|
|
1356
|
+
for a custom raw transport. It uses the same descriptor and returns
|
|
1357
|
+
`{ files, fields, rollback }`; contract endpoints should prefer the automatic
|
|
1358
|
+
dispatcher path.
|
|
1359
|
+
|
|
1230
1360
|
### Rate limiting
|
|
1231
1361
|
|
|
1232
1362
|
```ts
|
|
1233
1363
|
import { createRateLimiter } from 'stitchkit/server'
|
|
1234
1364
|
|
|
1235
|
-
const limiter = createRateLimiter(
|
|
1236
|
-
// in onRequest:
|
|
1365
|
+
const limiter = createRateLimiter()
|
|
1366
|
+
// in onRequest:
|
|
1367
|
+
if (!limiter.check(ip, { window: 60_000, max: 60 })) {
|
|
1368
|
+
return new Response('Too many', { status: 429 })
|
|
1369
|
+
}
|
|
1237
1370
|
```
|
|
1238
1371
|
|
|
1239
1372
|
### Event bus
|
|
@@ -1419,10 +1552,43 @@ contract:
|
|
|
1419
1552
|
- for `GET` / `DELETE`, the remaining fields become the **query string**
|
|
1420
1553
|
(arrays become repeated keys),
|
|
1421
1554
|
- for `POST` / `PUT` / `PATCH`, they become the **JSON body**,
|
|
1422
|
-
- a `multipart` field is a `Blob` (web / Bun) or
|
|
1423
|
-
(`{ uri, name, type }`,
|
|
1424
|
-
|
|
1425
|
-
|
|
1555
|
+
- a single `multipart.files` field is a `Blob` (web / Bun) or platform
|
|
1556
|
+
`FileDescriptor` (`{ uri, name, type }`, React Native / Expo); a multiple
|
|
1557
|
+
field is an array and is appended under the same form field name in order.
|
|
1558
|
+
The exported `MultipartFile` / `FileDescriptor` types let you annotate your
|
|
1559
|
+
own upload helpers.
|
|
1560
|
+
|
|
1561
|
+
### Per-call cancellation
|
|
1562
|
+
|
|
1563
|
+
Every endpoint accepts optional `ClientRequestOptions` containing an
|
|
1564
|
+
`AbortSignal`. Endpoints with arguments use `(args, options?)`; endpoints with
|
|
1565
|
+
no arguments use `(options?)`:
|
|
1566
|
+
|
|
1567
|
+
```ts
|
|
1568
|
+
const controller = new AbortController()
|
|
1569
|
+
|
|
1570
|
+
const pending = api.upload(
|
|
1571
|
+
{ file: selectedFile, title: 'Draft' },
|
|
1572
|
+
{ signal: controller.signal },
|
|
1573
|
+
)
|
|
1574
|
+
|
|
1575
|
+
controller.abort()
|
|
1576
|
+
await pending
|
|
1577
|
+
```
|
|
1578
|
+
|
|
1579
|
+
Caller cancellation and the endpoint/client timeout are composed; whichever
|
|
1580
|
+
fires first owns the result. Both the bare-fetch and Ky-backed clients expose
|
|
1581
|
+
the same client-only errors:
|
|
1582
|
+
|
|
1583
|
+
| Failure | `ApiError.code` | `status` |
|
|
1584
|
+
|---------|-----------------|----------|
|
|
1585
|
+
| caller `AbortSignal` | `REQUEST_ABORTED` | `0` |
|
|
1586
|
+
| endpoint/client timeout | `REQUEST_TIMEOUT` | `0` |
|
|
1587
|
+
| other transport failure | `UNKNOWN_ERROR` | `0` |
|
|
1588
|
+
|
|
1589
|
+
Abort and timeout do not emit `network_error` and are not retried. The same
|
|
1590
|
+
options work for query, JSON, multipart and raw-response calls. Stitchkit does
|
|
1591
|
+
not expose upload progress: Fetch has no portable upload-progress primitive.
|
|
1426
1592
|
|
|
1427
1593
|
### Many contracts at once
|
|
1428
1594
|
|
|
@@ -1999,15 +2165,15 @@ handlers must still be idempotent where retries matter.
|
|
|
1999
2165
|
|
|
2000
2166
|
A tool call runs the same handler an HTTP request would. `lifecycle` makes it
|
|
2001
2167
|
run the same gate: a `beforeHandle` (throw to reject) and an `afterHandle`
|
|
2002
|
-
(transform the result)
|
|
2003
|
-
|
|
2004
|
-
|
|
2168
|
+
(transform the result). Pass the **same**
|
|
2169
|
+
[`createAuthHook`](./auth-and-errors.md#createauthhook) result used as the HTTP
|
|
2170
|
+
server's `authorize` hook and tool calls are scope-checked by identical rules:
|
|
2005
2171
|
|
|
2006
2172
|
```ts
|
|
2007
2173
|
createMcpHandler({ serverInfo, auth, services, lifecycle: { beforeHandle: authHook } })
|
|
2008
2174
|
```
|
|
2009
2175
|
|
|
2010
|
-
Without it, a tool call bypasses the HTTP `
|
|
2176
|
+
Without it, a tool call bypasses the HTTP `authorize` gate — the contract's
|
|
2011
2177
|
`scope` is not enforced on the MCP / agent surface. `mountMcp`, `mountAgent` and
|
|
2012
2178
|
`buildMcpServer` take `lifecycle` too.
|
|
2013
2179
|
|
|
@@ -2259,6 +2425,15 @@ timeouts, redirect count and body size are bounded. Its bounded cache respects
|
|
|
2259
2425
|
`Last-Modified`, coalesces concurrent misses and reports sanitized cache events;
|
|
2260
2426
|
invalid or unavailable identity receives only a short fail-closed cache entry.
|
|
2261
2427
|
|
|
2428
|
+
Resolution itself is rate-limited on two levels (`CimdCachePolicy`):
|
|
2429
|
+
`maxResolutionsPerClient` (default 10) bounds ONE `client_id` per
|
|
2430
|
+
`resolutionWindowMs` window (default 60 s) — a client whose document disables
|
|
2431
|
+
caching burns its own budget, not the server's — while `maxResolutions`
|
|
2432
|
+
(default 120) caps the server-wide outbound rate as the flood backstop.
|
|
2433
|
+
Positive and negative cache entries keep separate pools of `maxEntries` each,
|
|
2434
|
+
so a flood of unresolvable ids cannot evict warmed clients and a burst of
|
|
2435
|
+
successes cannot reset a failing client's backoff.
|
|
2436
|
+
|
|
2262
2437
|
### Authorization hardening (MCP 2026-07-28)
|
|
2263
2438
|
|
|
2264
2439
|
- **`iss` on every authorization response (RFC 9207, SEP-2468).** Success *and*
|
|
@@ -2360,7 +2535,7 @@ the merged `params` + `input`. `context` is merged into every tool handler's
|
|
|
2360
2535
|
| `runtimeTools` | framework-managed pathless operations from `defineRuntimeTool` |
|
|
2361
2536
|
|
|
2362
2537
|
`lifecycle` works the same as on the MCP server — without it an agent tool call
|
|
2363
|
-
bypasses the HTTP `
|
|
2538
|
+
bypasses the HTTP `authorize` gate. Pass your `createAuthHook` result.
|
|
2364
2539
|
|
|
2365
2540
|
### Adding tool-only args — `extend`
|
|
2366
2541
|
|
|
@@ -2586,6 +2761,11 @@ mountMcp(server, services, { hooks: createToolLogger() })
|
|
|
2586
2761
|
// [tool] warn get_widget (widgets.get) NOT_FOUND 4ms
|
|
2587
2762
|
```
|
|
2588
2763
|
|
|
2764
|
+
By default the line goes to `console.error` — **stderr, never stdout**. That is
|
|
2765
|
+
deliberate: in a stdio-MCP server stdout is the JSON-RPC protocol channel, and a
|
|
2766
|
+
log line written there corrupts the stream. If you pass your own `log`, keep it
|
|
2767
|
+
off stdout for any stdio deployment.
|
|
2768
|
+
|
|
2589
2769
|
Pass `log` to redirect the line, or `onRecord` to feed a metrics sink the
|
|
2590
2770
|
structured `ToolCallRecord`. That record carries `traceId` whenever an
|
|
2591
2771
|
observability context is active, so a tool call made inside an HTTP request
|
|
@@ -2750,7 +2930,7 @@ await createCli({
|
|
|
2750
2930
|
version: '1.0.0',
|
|
2751
2931
|
auth: await resolveIdentityFromToken(process.env.MYAPP_TOKEN),
|
|
2752
2932
|
context: (identity) => ({ user: identity }), // resolveFromContext reads this
|
|
2753
|
-
lifecycle: { beforeHandle: authHook }, // same
|
|
2933
|
+
lifecycle: { beforeHandle: authHook }, // same policy; HTTP wires it as authorize
|
|
2754
2934
|
services,
|
|
2755
2935
|
})
|
|
2756
2936
|
```
|
|
@@ -2798,6 +2978,8 @@ fallback, heartbeats, acks, a mature client. stitchkit does not ship its own
|
|
|
2798
2978
|
WebSocket engine; it ships thin, typed wrappers over Socket.IO and a bridge that
|
|
2799
2979
|
syncs socket events into the TanStack Query cache. See
|
|
2800
2980
|
[ADR 0008](../decisions/0008-thin-wrappers.md).
|
|
2981
|
+
The separate contract shape and rejection ownership are recorded in
|
|
2982
|
+
[ADR 0069](../decisions/0069-realtime-contracts-validate-without-owning-delivery.md).
|
|
2801
2983
|
|
|
2802
2984
|
## Zod-first event contract
|
|
2803
2985
|
|
|
@@ -2870,6 +3052,16 @@ createServer({
|
|
|
2870
3052
|
realtime.emit('note:created', note)
|
|
2871
3053
|
```
|
|
2872
3054
|
|
|
3055
|
+
The canonical room-broadcast example below is executed by the test suite. Its
|
|
3056
|
+
body is kept byte-identical to `packages/core/examples/realtime-room.ts`.
|
|
3057
|
+
|
|
3058
|
+
```ts canonical-realtime-room
|
|
3059
|
+
export function publishExampleNote(realtime: ExampleRealtimePublisher): void {
|
|
3060
|
+
const note = { id: 'note-1', text: 'Ready' };
|
|
3061
|
+
realtime.to('general').emit('note:created', note);
|
|
3062
|
+
}
|
|
3063
|
+
```
|
|
3064
|
+
|
|
2873
3065
|
| Handle field | Purpose |
|
|
2874
3066
|
|--------------|---------|
|
|
2875
3067
|
| `io` | raw Socket.IO server for middleware, handshake auth and transport ownership |
|
|
@@ -3127,11 +3319,9 @@ policies; this helper only applies declared CRUD semantics.
|
|
|
3127
3319
|
|
|
3128
3320
|
## Raw binary lane (Bun)
|
|
3129
3321
|
|
|
3130
|
-
|
|
3131
|
-
|
|
3132
|
-
|
|
3133
|
-
port. On Bun that is awkward: `Bun.serve` has a single `websocket` handler, and
|
|
3134
|
-
`createSocketIOServer().websocket` claims it.
|
|
3322
|
+
For a high-throughput raw binary channel beside Socket.IO, use the orthogonal
|
|
3323
|
+
composition boundary from
|
|
3324
|
+
[ADR 0020](../decisions/0020-raw-websocket-lane.md).
|
|
3135
3325
|
|
|
3136
3326
|
`composeWebSocketHandlers` composes that one handler from several lanes. A raw
|
|
3137
3327
|
lane stamps its own marker onto `ws.data` at upgrade and is matched positively;
|
|
@@ -3293,10 +3483,12 @@ The framework attaches no meaning to the strings — `'public'`, `'user'`,
|
|
|
3293
3483
|
|
|
3294
3484
|
## `createAuthHook`
|
|
3295
3485
|
|
|
3296
|
-
`createAuthHook` builds
|
|
3297
|
-
|
|
3298
|
-
|
|
3299
|
-
|
|
3486
|
+
`createAuthHook` builds one scope gate that can run in both HTTP and tool
|
|
3487
|
+
lifecycles. On HTTP it belongs in `hooks.authorize`: Stitchkit validates path
|
|
3488
|
+
params, resolves identity and scope, and rejects `401`/`403` **before reading a
|
|
3489
|
+
JSON or multipart body**. On MCP, agent and CLI surfaces it belongs in
|
|
3490
|
+
`lifecycle.beforeHandle`, because those transports have already received their
|
|
3491
|
+
arguments before the common tool runner starts.
|
|
3300
3492
|
|
|
3301
3493
|
```ts
|
|
3302
3494
|
import { createAuthHook, createServer } from 'stitchkit/server'
|
|
@@ -3311,7 +3503,7 @@ const authHook = createAuthHook<User>({
|
|
|
3311
3503
|
inject: (ctx, user) => { ctx.user = user },
|
|
3312
3504
|
})
|
|
3313
3505
|
|
|
3314
|
-
createServer({ services, hooks: {
|
|
3506
|
+
createServer({ services, hooks: { authorize: authHook } })
|
|
3315
3507
|
```
|
|
3316
3508
|
|
|
3317
3509
|
### `AuthRule`
|
|
@@ -3321,8 +3513,9 @@ The value of each `rules` entry, keyed by scope:
|
|
|
3321
3513
|
- **`'public'`** — always passes; the identity is attached if present.
|
|
3322
3514
|
- **`'authenticated'`** — any resolved identity passes; no identity ⇒ 401.
|
|
3323
3515
|
- **a function** `(identity, ctx) => boolean | Promise<boolean>` — a custom
|
|
3324
|
-
check. It receives
|
|
3325
|
-
|
|
3516
|
+
check. It receives request metadata and validated path params, so a
|
|
3517
|
+
resource-scoped rule can do a DB lookup. It cannot read `input` or files: the
|
|
3518
|
+
body has deliberately not been consumed yet. May be async.
|
|
3326
3519
|
|
|
3327
3520
|
#### Resource-scoped rule — reading a path/prefix param
|
|
3328
3521
|
|
|
@@ -3369,14 +3562,15 @@ catches a scope you forgot to cover.
|
|
|
3369
3562
|
|
|
3370
3563
|
### Auth on the tool surface — `resolveFromContext`
|
|
3371
3564
|
|
|
3372
|
-
The hook
|
|
3373
|
-
|
|
3374
|
-
surface:
|
|
3565
|
+
The same hook guards every transport, but the lifecycle slot and identity
|
|
3566
|
+
source differ:
|
|
3375
3567
|
|
|
3376
|
-
- **HTTP** — `resolve(ctx)`
|
|
3568
|
+
- **HTTP** — `hooks.authorize` calls `resolve(ctx)` from `ctx.req` (a cookie or
|
|
3569
|
+
bearer token) before body parsing.
|
|
3377
3570
|
- **Tool calls (MCP / agent)** — there is no `req`. The transport authenticated
|
|
3378
3571
|
the caller (an MCP API key) and `buildMcpServer`'s `context` injected the
|
|
3379
|
-
identity into `ctx`. `
|
|
3572
|
+
identity into `ctx`. `lifecycle.beforeHandle` calls
|
|
3573
|
+
`resolveFromContext(ctx)` to locate it.
|
|
3380
3574
|
|
|
3381
3575
|
```ts
|
|
3382
3576
|
const authHook = createAuthHook<User>({
|
|
@@ -3391,6 +3585,11 @@ differs. If you omit `resolveFromContext`, a scoped tool call has no identity
|
|
|
3391
3585
|
and **fails closed** (rejected by `onAnonymous`) — the hook never silently
|
|
3392
3586
|
passes a tool call it cannot authenticate. → [ADR 0014](../decisions/0014-tool-http-parity.md)
|
|
3393
3587
|
|
|
3588
|
+
```ts
|
|
3589
|
+
createServer({ services, hooks: { authorize: authHook } })
|
|
3590
|
+
createMcpHandler({ services, lifecycle: { beforeHandle: authHook } })
|
|
3591
|
+
```
|
|
3592
|
+
|
|
3394
3593
|
## `createBearerResolver`
|
|
3395
3594
|
|
|
3396
3595
|
For API-key or bearer-token auth (the usual MCP case), `createBearerResolver`
|
|
@@ -3414,9 +3613,11 @@ pick the scheme), and checks `exp` and `nbf`.
|
|
|
3414
3613
|
|
|
3415
3614
|
```ts
|
|
3416
3615
|
import { verifyJwt, extractToken } from 'stitchkit/server'
|
|
3616
|
+
import { env } from './env'
|
|
3417
3617
|
|
|
3418
3618
|
const token = extractToken(req) // from Authorization, or a cookie name
|
|
3419
|
-
|
|
3619
|
+
if (!token) throw unauthorized('Bearer token is required')
|
|
3620
|
+
const payload = await verifyJwt(token, env.JWT_SECRET)
|
|
3420
3621
|
```
|
|
3421
3622
|
|
|
3422
3623
|
`extractToken(req, cookieName?)` reads a bearer token from the `Authorization`
|
|
@@ -3614,6 +3815,7 @@ const onError = createErrorHook({
|
|
|
3614
3815
|
NOT_FOUND: 'not_found', METHOD_NOT_ALLOWED: 'not_found',
|
|
3615
3816
|
CONFLICT: 'conflict', RATE_LIMITED: 'rate_limited',
|
|
3616
3817
|
INTERNAL_SERVER_ERROR: 'internal',
|
|
3818
|
+
REALTIME_CONTRACT_VIOLATION: 'internal',
|
|
3617
3819
|
} satisfies Record<StitchErrorCode, string>,
|
|
3618
3820
|
// `ctx` is the request's RuntimeContext — read `ctx.traceId` for a
|
|
3619
3821
|
// correlation id in the envelope. Declaring it is optional.
|
|
@@ -3748,6 +3950,66 @@ Each sink runs fire-and-forget and fails independently: a slow or broken request
|
|
|
3748
3950
|
sink cannot block the response, suppress operational logging or break the tool
|
|
3749
3951
|
sink.
|
|
3750
3952
|
|
|
3953
|
+
The fire-and-forget work has an explicit bounded lifecycle:
|
|
3954
|
+
|
|
3955
|
+
```ts
|
|
3956
|
+
export const observability = createObservability({
|
|
3957
|
+
request: {
|
|
3958
|
+
maxPending: 1_000,
|
|
3959
|
+
write: persistRequestEvent,
|
|
3960
|
+
onSinkError: ({ error, event }) => reportAuditFailure(error, event),
|
|
3961
|
+
onDrop: ({ reason, event, pending }) => {
|
|
3962
|
+
reportAuditDrop({ reason, traceId: event.traceId, pending })
|
|
3963
|
+
},
|
|
3964
|
+
},
|
|
3965
|
+
tools: {
|
|
3966
|
+
maxPending: 500,
|
|
3967
|
+
write: persistToolEvent,
|
|
3968
|
+
onSinkError: ({ error, event }) => reportToolAuditFailure(error, event),
|
|
3969
|
+
onDrop: reportToolAuditDrop,
|
|
3970
|
+
},
|
|
3971
|
+
})
|
|
3972
|
+
|
|
3973
|
+
await observability.flush()
|
|
3974
|
+
await observability.close()
|
|
3975
|
+
```
|
|
3976
|
+
|
|
3977
|
+
`maxPending` defaults to `1000` per sink and must be a positive safe integer.
|
|
3978
|
+
Filtering and sanitisation happen before a write occupies a pending slot. At
|
|
3979
|
+
capacity, a new event is rejected with `onDrop({ reason: 'capacity', ... })`;
|
|
3980
|
+
after close, admission reports `reason: 'closed'`. Existing writes are never
|
|
3981
|
+
cancelled. Sink, filter and diagnostic-callback failures remain isolated from
|
|
3982
|
+
the observed request or tool call and are reported through `onSinkError` when
|
|
3983
|
+
configured; `onSinkError`/`onDrop` cannot create unhandled rejections.
|
|
3984
|
+
|
|
3985
|
+
`flush()` snapshots the current generation and waits only for events admitted
|
|
3986
|
+
up to that call. `close()` atomically stops admission, drains every accepted
|
|
3987
|
+
generation and is idempotent. Graceful shutdown order is therefore:
|
|
3988
|
+
|
|
3989
|
+
1. stop HTTP/MCP admission;
|
|
3990
|
+
2. wait for active requests and tool calls;
|
|
3991
|
+
3. `await observability.close()`;
|
|
3992
|
+
4. close the database/storage connection used by the sinks.
|
|
3993
|
+
|
|
3994
|
+
Stitchkit manages only in-process delivery. If process-crash durability matters,
|
|
3995
|
+
make `write` enqueue into a consumer-owned durable outbox and let that adapter
|
|
3996
|
+
own retry, replay and storage policy:
|
|
3997
|
+
|
|
3998
|
+
```ts
|
|
3999
|
+
interface AuditOutbox {
|
|
4000
|
+
enqueue(event: RequestEvent): Promise<void>
|
|
4001
|
+
}
|
|
4002
|
+
|
|
4003
|
+
const observability = createObservability({
|
|
4004
|
+
request: { write: (event) => outbox.enqueue(event) },
|
|
4005
|
+
tools: { write: (event) => outbox.enqueue(event) },
|
|
4006
|
+
})
|
|
4007
|
+
```
|
|
4008
|
+
|
|
4009
|
+
The core intentionally contains no retry scheduler, database dependency or
|
|
4010
|
+
disk queue. A durable adapter can use `(traceId, spanId, source, toolPhase)` as
|
|
4011
|
+
its idempotency identity.
|
|
4012
|
+
|
|
3751
4013
|
### RequestEvent
|
|
3752
4014
|
|
|
3753
4015
|
Every surface produces the same shape — so a single audit table stays
|
|
@@ -3967,8 +4229,10 @@ each request.
|
|
|
3967
4229
|
|
|
3968
4230
|
A payload goes into an audit row only after `sanitizePayload`:
|
|
3969
4231
|
|
|
3970
|
-
- **secret-named keys are masked** —
|
|
3971
|
-
`
|
|
4232
|
+
- **secret-named keys are masked** — a key whose words include a secret term is
|
|
4233
|
+
redacted (`password`, `sessionToken`, `X-Api-Key`, `authorization`, `cookie`,
|
|
4234
|
+
…), while identifiers that merely contain one (`authorId`, `sessionCount`,
|
|
4235
|
+
`tokenizer`) survive;
|
|
3972
4236
|
- **binary blobs** (`Uint8Array`, `Blob`, `FormData`) collapse to metadata —
|
|
3973
4237
|
never the bytes;
|
|
3974
4238
|
- the result is **capped** — anything over the byte limit becomes a preview.
|
|
@@ -4277,8 +4541,9 @@ Notes for a Node host:
|
|
|
4277
4541
|
to join the two logs.
|
|
4278
4542
|
- **Rate limiting** — `createRateLimiter` in `onRequest` for a global limit;
|
|
4279
4543
|
per-route limits belong in `beforeHandle`.
|
|
4280
|
-
- **Auth** —
|
|
4281
|
-
do not re-check auth per handler.
|
|
4544
|
+
- **Auth** — wire one `createAuthHook` as HTTP `hooks.authorize` and tool
|
|
4545
|
+
`lifecycle.beforeHandle`; do not re-check auth per handler. HTTP authorization
|
|
4546
|
+
runs before JSON or multipart body reads.
|
|
4282
4547
|
- **Errors** — handlers throw `AppError`; let the standard envelope render them.
|
|
4283
4548
|
Add an `onError` hook only to integrate an error tracker.
|
|
4284
4549
|
- **Secrets** — read them from the environment; never commit them.
|
|
@@ -4337,7 +4602,7 @@ matched value is on the context root as `ctx.tenantId`
|
|
|
4337
4602
|
```ts
|
|
4338
4603
|
createServer({
|
|
4339
4604
|
groups: [
|
|
4340
|
-
{ pathPrefix: '/tenants/:tenantId', services: [widgetsService], hooks: {
|
|
4605
|
+
{ pathPrefix: '/tenants/:tenantId', services: [widgetsService], hooks: { authorize: authHook } },
|
|
4341
4606
|
],
|
|
4342
4607
|
})
|
|
4343
4608
|
// → /tenants/:tenantId/widgets
|
|
@@ -4351,7 +4616,7 @@ the flat `services` list — no hand-partitioning, the mapping lives in one plac
|
|
|
4351
4616
|
createServer({
|
|
4352
4617
|
services, // mixed scopes, listed once
|
|
4353
4618
|
scopePrefixes: { tenant: 'tenants/:tenantId', project: 'projects/:projectId' },
|
|
4354
|
-
hooks: {
|
|
4619
|
+
hooks: { authorize: authHook },
|
|
4355
4620
|
})
|
|
4356
4621
|
// `tenant`-scoped → /tenants/:tenantId/..., `project` → /projects/:projectId/..., the rest flat
|
|
4357
4622
|
```
|
|
@@ -4552,6 +4817,86 @@ current one *up to* your target, and apply each snippet.
|
|
|
4552
4817
|
runtime): bootstrap the server, one HTTP request, and any feature you rely on
|
|
4553
4818
|
(Socket.IO connect, an MCP tool call, a multipart upload, …).
|
|
4554
4819
|
|
|
4820
|
+
## Upcoming migration: `[Unreleased]`
|
|
4821
|
+
|
|
4822
|
+
### HTTP auth moves to the pre-body `authorize` phase
|
|
4823
|
+
|
|
4824
|
+
Move the HTTP wiring of `createAuthHook` from `beforeHandle` to `authorize`.
|
|
4825
|
+
This lets Stitchkit reject an unauthorized JSON or multipart request after path
|
|
4826
|
+
parameter validation but before reading a body chunk. Keep application
|
|
4827
|
+
preconditions that depend on validated input in `beforeHandle`.
|
|
4828
|
+
|
|
4829
|
+
```ts
|
|
4830
|
+
const auth = createAuthHook({ authenticate, authorize })
|
|
4831
|
+
|
|
4832
|
+
// before
|
|
4833
|
+
createServer({ services, hooks: { beforeHandle: auth } })
|
|
4834
|
+
|
|
4835
|
+
// after
|
|
4836
|
+
createServer({ services, hooks: { authorize: auth } })
|
|
4837
|
+
```
|
|
4838
|
+
|
|
4839
|
+
Tool transports already receive parsed input, so their wiring does not move:
|
|
4840
|
+
|
|
4841
|
+
```ts
|
|
4842
|
+
createMcpHandler({ services, lifecycle: { beforeHandle: auth } })
|
|
4843
|
+
```
|
|
4844
|
+
|
|
4845
|
+
If a custom HTTP authorization hook read `ctx.input`, `ctx.files` or raw body
|
|
4846
|
+
state, split it: identity/scope checks belong in `authorize`; validated payload
|
|
4847
|
+
preconditions belong in `beforeHandle` or the domain service.
|
|
4848
|
+
|
|
4849
|
+
### Multipart uses a typed descriptor and `ctx.files`
|
|
4850
|
+
|
|
4851
|
+
Replace every string multipart declaration, top-level `maxUploadBytes` and
|
|
4852
|
+
`ctx.file`. The descriptor is now the only source of request, per-file,
|
|
4853
|
+
cardinality and declared media-type policy.
|
|
4854
|
+
|
|
4855
|
+
```ts
|
|
4856
|
+
// before
|
|
4857
|
+
upload: {
|
|
4858
|
+
method: 'POST',
|
|
4859
|
+
path: '/',
|
|
4860
|
+
multipart: 'file',
|
|
4861
|
+
maxUploadBytes: 25 * 1024 * 1024,
|
|
4862
|
+
}
|
|
4863
|
+
upload: ({ file, input }) => store(file, input)
|
|
4864
|
+
|
|
4865
|
+
// after
|
|
4866
|
+
upload: {
|
|
4867
|
+
method: 'POST',
|
|
4868
|
+
path: '/',
|
|
4869
|
+
multipart: {
|
|
4870
|
+
maxRequestBytes: 25 * 1024 * 1024,
|
|
4871
|
+
files: {
|
|
4872
|
+
file: {
|
|
4873
|
+
maxBytes: 20 * 1024 * 1024,
|
|
4874
|
+
contentTypes: ['image/*', 'application/pdf'],
|
|
4875
|
+
},
|
|
4876
|
+
},
|
|
4877
|
+
},
|
|
4878
|
+
}
|
|
4879
|
+
upload: ({ files, input }) => store(files.file, input)
|
|
4880
|
+
```
|
|
4881
|
+
|
|
4882
|
+
Multiple files are repeated under one multipart field name and arrive in the
|
|
4883
|
+
same order:
|
|
4884
|
+
|
|
4885
|
+
```ts
|
|
4886
|
+
files: {
|
|
4887
|
+
attachments: { multiple: true, maxFiles: 8 },
|
|
4888
|
+
}
|
|
4889
|
+
|
|
4890
|
+
await api.upload({ attachments: [firstFile, secondFile] })
|
|
4891
|
+
// handler: files.attachments is File[]
|
|
4892
|
+
```
|
|
4893
|
+
|
|
4894
|
+
For direct-to-storage delivery, set `delivery: 'stream'` and implement the
|
|
4895
|
+
endpoint with `defineMultipartStream`. A receiver must consume its Web Stream
|
|
4896
|
+
and return `{ value, cleanup }`; the final handler sees only receiver values.
|
|
4897
|
+
There is no deprecated overload or buffered compatibility path under the old
|
|
4898
|
+
contract shape.
|
|
4899
|
+
|
|
4555
4900
|
## Worked example — frozen on 0.3, jumping to 0.7
|
|
4556
4901
|
|
|
4557
4902
|
1. `bun.lock` → consumer resolves `stitchkit@0.3.x`.
|
|
@@ -4564,7 +4909,7 @@ current one *up to* your target, and apply each snippet.
|
|
|
4564
4909
|
(`STITCH_ERROR_STATUS`, `serveFile`, `scopePrefixes`, `afterToolCall`'s
|
|
4565
4910
|
`MethodDef`, `maxUploadBytes`) are available to adopt, not required.
|
|
4566
4911
|
|
|
4567
|
-
##
|
|
4912
|
+
## Released migration: 0.44.0
|
|
4568
4913
|
|
|
4569
4914
|
### MCP TypeScript SDK v2 and protocol `2026-07-28`
|
|
4570
4915
|
|
|
@@ -4759,14 +5104,29 @@ implemented or advertised.
|
|
|
4759
5104
|
3. Delete all session mode, event-store and session-id code.
|
|
4760
5105
|
4. Move OAuth client policy under `clientRegistration`; publish CIMD or enable
|
|
4761
5106
|
DCR explicitly.
|
|
4762
|
-
5.
|
|
5107
|
+
5. Make `authorizeUser` return the exact consented scope subset. The framework
|
|
5108
|
+
validates that it is a subset of the request before saving the authorization
|
|
5109
|
+
code:
|
|
5110
|
+
|
|
5111
|
+
```ts
|
|
5112
|
+
// before
|
|
5113
|
+
authorizeUser: async () => ({ userId })
|
|
5114
|
+
|
|
5115
|
+
// after
|
|
5116
|
+
authorizeUser: async (_req, request) => ({
|
|
5117
|
+
userId,
|
|
5118
|
+
approvedScopes: request.scope?.split(' ') ?? [],
|
|
5119
|
+
})
|
|
5120
|
+
```
|
|
5121
|
+
|
|
5122
|
+
6. Snapshot `listToolNames`, run one contract tool, one runtime tool, any raw
|
|
4763
5123
|
multimodal tool and every MCP App resource you use.
|
|
4764
|
-
|
|
5124
|
+
7. Exercise modern HTTP and stdio with protocol `2026-07-28`; exercise legacy
|
|
4765
5125
|
only if `legacy: 'serve'` is part of your support policy.
|
|
4766
|
-
|
|
5126
|
+
8. Run the consumer's typecheck and runtime gates. A browser/HTTP-only consumer
|
|
4767
5127
|
must continue to work without either MCP package.
|
|
4768
5128
|
|
|
4769
|
-
## Historical breaking migrations through 0.
|
|
5129
|
+
## Historical breaking migrations through 0.44.0
|
|
4770
5130
|
|
|
4771
5131
|
HTTP observability now completes inside the framework handler instead of a
|
|
4772
5132
|
nested fetch wrapper. Configure request and tool sinks explicitly:
|
|
@@ -5224,6 +5584,7 @@ The browser-and-server entrypoint. Re-exports everything from
|
|
|
5224
5584
|
| `createUrlBuilders` | function | build one exact URL builder per contract in a registry |
|
|
5225
5585
|
| `UrlBuilderConfig` | _type_ | explicit `{ baseUrl }` source for a URL builder |
|
|
5226
5586
|
| `ClientConfig` | _type_ | config for `createClient`'s bare-fetch mode (2nd arg, no `HttpClient`) |
|
|
5587
|
+
| `ClientRequestOptions` | _type_ | per-call `{ signal?: AbortSignal }`; caller abort is distinct from timeout — [guide](../guide/client.md#per-call-cancellation) |
|
|
5227
5588
|
| `ContractClientConfig` | _type_ | per-tenant / resource-scoped client config — dynamic `pathPrefix` + `stripPrefixKeys` ([guide](../guide/client.md#contractclientconfig--per-tenant--resource-scoped-clients)) |
|
|
5228
5589
|
| `contractEndpointMatchers` | function | compile exact pathname matchers for selected HTTP contract operations and expected-401 policy |
|
|
5229
5590
|
| `PathPrefixArgs` | _type_ | required string-valued keys exposed to a typed dynamic `pathPrefix` callback |
|
|
@@ -5252,6 +5613,17 @@ The browser-and-server entrypoint. Re-exports everything from
|
|
|
5252
5613
|
| `SocketEventMap` | _type_ | the shape of an event map |
|
|
5253
5614
|
| `RealtimeClient` | _type_ | validated client inferred from a realtime contract |
|
|
5254
5615
|
| `RealtimeClientOptions` | _type_ | transport options and the rejected-event hook for `createRealtimeClient` |
|
|
5616
|
+
| `RealtimeContract` | _type_ | shared server-to-client and client-to-server event registries |
|
|
5617
|
+
| `RealtimeEventRegistry` | _type_ | string-keyed registry of event definitions |
|
|
5618
|
+
| `RealtimeEventDefinition` | _type_ | one tuple-shaped event and optional acknowledgement schema |
|
|
5619
|
+
| `RealtimeEventArguments` | _type_ | tuple inferred from an event definition |
|
|
5620
|
+
| `RealtimeEmitArguments` | _type_ | emit arguments including an inferred acknowledgement callback |
|
|
5621
|
+
| `RealtimeEventHandler` | _type_ | handler inferred from an event definition |
|
|
5622
|
+
| `InferRealtimeEventMap` | _type_ | inferred Socket.IO-compatible event map |
|
|
5623
|
+
| `RealtimeRejectDirection` | _type_ | server/client inbound/outbound rejection direction |
|
|
5624
|
+
| `RealtimeRejectedEvent` | _type_ | structured rejected event with event, direction, phase, reason and fault |
|
|
5625
|
+
| `RealtimeRejectedEventHook` | _type_ | sync/async observer for structured realtime rejections |
|
|
5626
|
+
| `ValidatedRealtimeSocket` | _type_ | runtime-validating `on`/`emit` surface inferred from registries |
|
|
5255
5627
|
| `RetainedTopics` | _type_ | the `createRetainedTopics` handle |
|
|
5256
5628
|
| `ParseSSEOptions` | _type_ | options for `parseSSE` |
|
|
5257
5629
|
|
|
@@ -5302,6 +5674,7 @@ from the root `stitchkit`.
|
|
|
5302
5674
|
| `TransportSource` | _type_ | `http \| mcp \| agent \| cli` — the value of `ctx.source` |
|
|
5303
5675
|
| `RuntimeContext` | _type_ | the loose context seen by transport and hooks |
|
|
5304
5676
|
| `HandlerContext` | _type_ | the typed context seen by a handler |
|
|
5677
|
+
| `EndpointHandlerContext` | _type_ | one endpoint handler's fully inferred params, input, files and runtime context |
|
|
5305
5678
|
| `EndpointFn` | _type_ | the call signature of one client method |
|
|
5306
5679
|
| `TypedClient` | _type_ | the full typed client for a contract |
|
|
5307
5680
|
| `TypedHttpClient` | _type_ | the typed client, HTTP endpoints only (`= ScopedHttpClient<C, unknown>`) |
|
|
@@ -5312,6 +5685,9 @@ from the root `stitchkit`.
|
|
|
5312
5685
|
| `ScopedUrlFn` | _type_ | one URL method's signature with scoped-prefix keys folded in |
|
|
5313
5686
|
| `MultipartFile` | _type_ | a `multipart` file field — `Blob \| FileDescriptor` |
|
|
5314
5687
|
| `FileDescriptor` | _type_ | a React Native / Expo file — `{ uri, name, type }` |
|
|
5688
|
+
| `MultipartDescriptor` | _type_ | file fields, cardinality, delivery and request/text limits |
|
|
5689
|
+
| `MultipartFilePolicy` | _type_ | required/multiple, per-file bytes/count and declared MIME policy |
|
|
5690
|
+
| `MultipartBufferedFiles` | _type_ | `File` map inferred from a multipart descriptor |
|
|
5315
5691
|
| `EndpointToolAnnotations` | _type_ | MCP behavioural hints on an endpoint (`readOnlyHint` / `destructiveHint` / `title`) |
|
|
5316
5692
|
| `EndpointUiMeta` | _type_ | MCP Apps widget metadata on an endpoint |
|
|
5317
5693
|
| `EndpointMcpInputRequired` | _type_ | typed MCP multi-round input request (`key`, message and Zod object schema) |
|
|
@@ -5390,7 +5766,8 @@ Also re-exports the error helpers from `stitchkit/contract`.
|
|
|
5390
5766
|
| `MethodDef` | _type_ | one resolved endpoint inside a service |
|
|
5391
5767
|
| `OperationIdentity` | _type_ | path-free service/action/scope/method identity shared by contract and native tool operations |
|
|
5392
5768
|
| `Handlers` | _type_ | the typed handler map `implement` expects |
|
|
5393
|
-
| `LifecycleHooks` | _type_ | `onRequest` / `beforeHandle` / `afterHandle` / `onError` |
|
|
5769
|
+
| `LifecycleHooks` | _type_ | `onRequest` / pre-body `authorize` / `beforeHandle` / `afterHandle` / `onError` |
|
|
5770
|
+
| `AuthorizationContext` | _type_ | HTTP pre-body context with validated params, `input: undefined` and no files |
|
|
5394
5771
|
| `RouteGroup` | _type_ | a prefixed group of services with its own hooks |
|
|
5395
5772
|
| `RawRoute` | _type_ | a non-contract `Request → Response` route with a concrete `BunServer` context |
|
|
5396
5773
|
| `RawRouteContext` | _type_ | the Bun-bound routing context a raw handler receives |
|
|
@@ -5407,7 +5784,7 @@ Also re-exports the error helpers from `stitchkit/contract`.
|
|
|
5407
5784
|
|
|
5408
5785
|
| Export | Kind | Summary |
|
|
5409
5786
|
|--------|------|---------|
|
|
5410
|
-
| `createAuthHook` | function |
|
|
5787
|
+
| `createAuthHook` | function | one scope gate for HTTP `authorize` and tool `beforeHandle` — [guide](../guide/auth-and-errors.md#createauthhook) |
|
|
5411
5788
|
| `createErrorHook` | function | an async-capable, endpoint-aware `onError` hook from a code map + envelope renderer — [guide](../guide/auth-and-errors.md#createerrorhook) |
|
|
5412
5789
|
| `ErrorHookConfig` | _type_ | async observer/renderer config for `createErrorHook` |
|
|
5413
5790
|
| `ResolvedError` | _type_ | the normalised error handed to `createErrorHook`'s `render` |
|
|
@@ -5465,10 +5842,17 @@ Also re-exports the error helpers from `stitchkit/contract`.
|
|
|
5465
5842
|
|--------|------|---------|
|
|
5466
5843
|
| `streamSSE` | function | an async generator → SSE `Response` — [guide](../guide/server.md#sse-streaming) |
|
|
5467
5844
|
| `parseSSE` | function | parse an SSE `Response` (also on the root entrypoint) |
|
|
5845
|
+
| `MultipartLifecycle` | _type_ | request-scoped rollback ownership for accepted streamed handles |
|
|
5468
5846
|
| `MultipartResult` | _type_ | what `parseMultipart` returns |
|
|
5469
|
-
| `parseMultipart` | function | parse a
|
|
5847
|
+
| `parseMultipart` | function | parse a typed buffered/streaming multipart descriptor — [guide](../guide/server.md#multipart) |
|
|
5848
|
+
| `defineMultipartStream` | function | bind typed streaming file receivers and a final endpoint handler |
|
|
5849
|
+
| `MultipartFileMetadata` | _type_ | field, filename, declared media type and optional declared size |
|
|
5850
|
+
| `MultipartReceiver` | _type_ | consumer-owned Web-stream storage receiver |
|
|
5851
|
+
| `MultipartReceiverResult` | _type_ | receiver value plus rollback cleanup |
|
|
5852
|
+
| `StreamingMultipartImplementation` | _type_ | receiver registry and handler shape inferred by `defineMultipartStream` |
|
|
5470
5853
|
| `createRateLimiter` | function | token-bucket rate limiting — [guide](../guide/server.md#rate-limiting) |
|
|
5471
5854
|
| `createCache` | function | an in-memory TTL cache |
|
|
5855
|
+
| `CacheOptions` | _type_ | bounded-cache options, including the maximum retained entry count |
|
|
5472
5856
|
| `cacheHeaders` | function | build a `Cache-Control` header |
|
|
5473
5857
|
| `EventBusOptions` | _type_ | options for `createEventBus` |
|
|
5474
5858
|
| `EventHandler` | _type_ | one event-bus subscriber |
|
|
@@ -5510,9 +5894,12 @@ audit event. See the [Observability guide](../guide/observability.md).
|
|
|
5510
5894
|
| `createObservability` | function | configure framework-owned request completion and canonical tool event sinks — [guide](../guide/observability.md#createobservability) |
|
|
5511
5895
|
| `RequestEvent` | _type_ | the normalised audit event handed to the sink |
|
|
5512
5896
|
| `ObservabilityConfig` | _type_ | independent request and tool sink configuration |
|
|
5513
|
-
| `Observability` | _type_ |
|
|
5514
|
-
| `RequestEventSinkConfig` | _type_ | `write`,
|
|
5897
|
+
| `Observability` | _type_ | `{ request?, toolCall, flush(), close() }` with bounded sink lifecycle |
|
|
5898
|
+
| `RequestEventSinkConfig` | _type_ | `write`, filter/sanitisation, `maxPending`, `onSinkError` and `onDrop` |
|
|
5515
5899
|
| `RequestObservabilityConfig` | _type_ | request sink plus opt-in payload capture |
|
|
5900
|
+
| `SinkDropReason` | _type_ | `'capacity' \| 'closed'` |
|
|
5901
|
+
| `SinkError` | _type_ | isolated sink/projection failure and optional event |
|
|
5902
|
+
| `SinkDrop` | _type_ | rejected event, reason and current pending count |
|
|
5516
5903
|
| `HttpRequestCompletion` | _type_ | the single framework-owned HTTP outcome projected to logging and request events |
|
|
5517
5904
|
| `HttpRequestObserver` | _type_ | server-facing projection consumed by `HandlerConfig.observability` |
|
|
5518
5905
|
|
|
@@ -5596,6 +5983,7 @@ payload.
|
|
|
5596
5983
|
| `StdioMcpServerConfig` | _type_ | config for `createStdioMcpServer` |
|
|
5597
5984
|
| `McpServerBuildConfig` | _type_ | shared config for `buildMcpServer` |
|
|
5598
5985
|
| `McpServerSharedConfig` | _type_ | transport-neutral options shared by direct and finite surface configs |
|
|
5986
|
+
| `McpServer` | _type_ | official split-SDK server instance accepted by raw extension points |
|
|
5599
5987
|
| `DirectMcpSurfaceConfig` | _type_ | static or identity-dynamic `services` / `runtimeTools` source |
|
|
5600
5988
|
| `FiniteMcpSurfaceConfig` | _type_ | bounded `surfaces` registry plus typed selector |
|
|
5601
5989
|
| `McpSurfaceDefinition` | _type_ | one immutable `{ services, runtimeTools }` MCP surface |
|
|
@@ -5626,7 +6014,7 @@ payload.
|
|
|
5626
6014
|
| `CimdClientMetadataFetcher` | _type_ | injectable secure network boundary for metadata loading |
|
|
5627
6015
|
| `CimdFetchResponse` | _type_ | bounded metadata fetch result passed across the injectable network boundary |
|
|
5628
6016
|
| `CimdFetchPolicy` | _type_ | CIMD timeout, redirect and size limits |
|
|
5629
|
-
| `CimdCachePolicy` | _type_ | bounded HTTP-aware positive/negative
|
|
6017
|
+
| `CimdCachePolicy` | _type_ | bounded HTTP-aware cache policy: separate positive/negative pools (`maxEntries`), per-client and server-wide resolution rate limits (`maxResolutionsPerClient` / `maxResolutions` per `resolutionWindowMs`) |
|
|
5630
6018
|
| `CimdCacheEvent` | _type_ | observable CIMD cache hit, miss, revalidation and eviction event |
|
|
5631
6019
|
| `createSecureClientMetadataFetcher` | function | production HTTPS, DNS/IP-pinned CIMD fetcher |
|
|
5632
6020
|
| `RuntimeAgentModelOutput` | _type_ | AI SDK model-facing text/JSON/content output returned by `present.agent` |
|
|
@@ -5739,7 +6127,7 @@ Advanced building blocks — the shared machinery the mounts are built on.
|
|
|
5739
6127
|
|
|
5740
6128
|
Server-only, for Node ≥ 22 (Bun uses `stitchkit/server`). The runtime-agnostic
|
|
5741
6129
|
core plus a Node HTTP adapter — [ADR 0013](../decisions/0013-runtime-agnostic-core.md),
|
|
5742
|
-
[deployment guide](../guide/testing-and-deployment.md#node). Re-exports the
|
|
6130
|
+
[deployment guide](../guide/testing-and-deployment.md#deploy-on-node). Re-exports the
|
|
5743
6131
|
runtime-agnostic pieces of `stitchkit/server` and the error helpers.
|
|
5744
6132
|
|
|
5745
6133
|
| Export | Kind | Summary |
|