stitchkit 0.46.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 +4 -3
- 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/cli.js +2 -2
- package/dist/contract/define.d.ts +59 -15
- package/dist/contract/define.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 +1 -1
- package/dist/{index-zwqty9zf.js → index-44xysy8r.js} +465 -49
- package/dist/{index-pwyedf7b.js → index-45dz4m51.js} +46 -0
- package/dist/{index-c40tkxcd.js → index-8ekq6res.js} +1 -1
- package/dist/{index-5s8b7z6q.js → index-ee621cmy.js} +9 -9
- package/dist/{index-62pqb23z.js → index-kp8xamqp.js} +1 -1
- package/dist/{index-w1s873ng.js → index-nrytvb30.js} +4 -3
- package/dist/index.js +191 -88
- package/dist/node.js +2 -2
- package/dist/observability/audit.d.ts +22 -3
- package/dist/observability/audit.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 +82 -15
- 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/implement.d.ts +33 -2
- package/dist/server/implement.d.ts.map +1 -1
- package/dist/server/index.d.ts +3 -3
- package/dist/server/index.d.ts.map +1 -1
- package/dist/server/index.js +28 -6
- package/dist/server/middleware/auth.d.ts +7 -4
- package/dist/server/middleware/auth.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/types.d.ts +48 -15
- package/dist/server/types.d.ts.map +1 -1
- package/dist/tools.js +167 -68
- package/llms-full.txt +367 -67
- package/package.json +1 -1
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
|
+
},
|
|
506
|
+
}
|
|
507
|
+
```
|
|
508
|
+
|
|
509
|
+
```ts
|
|
510
|
+
uploadAttachments: ({ params, input, files }) => {
|
|
511
|
+
files.cover // File | undefined
|
|
512
|
+
files.attachments // File[] in multipart order
|
|
487
513
|
}
|
|
488
514
|
```
|
|
489
515
|
|
|
490
|
-
|
|
491
|
-
`
|
|
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,7 +714,6 @@ 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
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. |
|
|
@@ -794,7 +833,7 @@ A group gives a set of services a shared path prefix and its own hooks:
|
|
|
794
833
|
createServer({
|
|
795
834
|
groups: [
|
|
796
835
|
{ pathPrefix: '/api', services: [usersService, postsService] },
|
|
797
|
-
{ pathPrefix: '/api/admin', services: [adminService], hooks: {
|
|
836
|
+
{ pathPrefix: '/api/admin', services: [adminService], hooks: { authorize: adminAuth } },
|
|
798
837
|
],
|
|
799
838
|
})
|
|
800
839
|
```
|
|
@@ -810,7 +849,7 @@ or resource-scoped API:
|
|
|
810
849
|
```ts
|
|
811
850
|
createServer({
|
|
812
851
|
groups: [
|
|
813
|
-
{ pathPrefix: '/tenants/:tenantId', services: [widgetsService], hooks: {
|
|
852
|
+
{ pathPrefix: '/tenants/:tenantId', services: [widgetsService], hooks: { authorize: auth } },
|
|
814
853
|
],
|
|
815
854
|
})
|
|
816
855
|
// widgetsService (prefix 'widgets') → /tenants/:tenantId/widgets/...
|
|
@@ -819,8 +858,8 @@ createServer({
|
|
|
819
858
|
**Where the prefix param lands.** The router matches the *full* path (group
|
|
820
859
|
prefix + service prefix + endpoint path) and collects every `:param` — from the
|
|
821
860
|
prefix and from the endpoint alike — into one set. Each is spread onto the
|
|
822
|
-
context root, so it is available as **`ctx.tenantId`** (a raw `string`) in
|
|
823
|
-
|
|
861
|
+
context root, so it is available as **`ctx.tenantId`** (a raw `string`) in the
|
|
862
|
+
`authorize` hook, handler and later lifecycle hooks:
|
|
824
863
|
|
|
825
864
|
```ts
|
|
826
865
|
beforeHandle: (ctx) => {
|
|
@@ -884,14 +923,17 @@ Scope stays a free string; the core attaches no meaning beyond this lookup
|
|
|
884
923
|
|
|
885
924
|
## Lifecycle hooks
|
|
886
925
|
|
|
887
|
-
|
|
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:
|
|
888
929
|
|
|
889
930
|
```ts
|
|
890
931
|
createServer({
|
|
891
932
|
services,
|
|
892
933
|
hooks: {
|
|
893
934
|
onRequest(req) { /* logging, global rate limit — may return a Response to short-circuit */ },
|
|
894
|
-
|
|
935
|
+
authorize(ctx, endpoint) { /* identity + scope, before body reads — throw to reject */ },
|
|
936
|
+
beforeHandle(ctx, endpoint) { /* validated-input preconditions */ },
|
|
895
937
|
afterHandle(ctx, result, ep) { /* transform the result data */ },
|
|
896
938
|
onError(ctx, error, ep) { /* custom error response — return a Response */ },
|
|
897
939
|
},
|
|
@@ -900,9 +942,13 @@ createServer({
|
|
|
900
942
|
|
|
901
943
|
- **`onRequest`** — runs first, with the raw `Request`. Return a `Response` to
|
|
902
944
|
short-circuit (a rate-limit 429, a redirect); return nothing to continue.
|
|
903
|
-
- **`
|
|
904
|
-
|
|
905
|
-
|
|
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.
|
|
906
952
|
- **`afterHandle`** — receives the handler result; return a replacement to
|
|
907
953
|
transform it.
|
|
908
954
|
- **`onError`** — receives any thrown error; return a `Response` to customise
|
|
@@ -1205,7 +1251,7 @@ focused helper — not a sub-framework.
|
|
|
1205
1251
|
|--------|------|
|
|
1206
1252
|
| `serveFile()` | serve a file with `Range` / `304` / `HEAD` (media seeking) |
|
|
1207
1253
|
| `streamSSE()` | turn an `AsyncGenerator` into a Server-Sent-Events `Response` |
|
|
1208
|
-
| `parseMultipart()` | parse a
|
|
1254
|
+
| `parseMultipart()` | parse a typed buffered/streaming multipart descriptor |
|
|
1209
1255
|
| `createRateLimiter()` | per-key token-bucket rate limiting |
|
|
1210
1256
|
| `createCache()` + `cacheHeaders()` | in-memory TTL cache; `Cache-Control` builder |
|
|
1211
1257
|
| `createEventBus<EventMap>()` | typed in-process pub/sub |
|
|
@@ -1232,29 +1278,85 @@ The client side is [`parseSSE`](./client.md#sse).
|
|
|
1232
1278
|
|
|
1233
1279
|
### Multipart
|
|
1234
1280
|
|
|
1235
|
-
|
|
1236
|
-
import { parseMultipart } from 'stitchkit/server'
|
|
1281
|
+
The contract owns one descriptor for buffered and streaming delivery:
|
|
1237
1282
|
|
|
1238
|
-
|
|
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
|
+
})
|
|
1239
1299
|
```
|
|
1240
1300
|
|
|
1241
|
-
|
|
1242
|
-
and
|
|
1243
|
-
|
|
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.
|
|
1244
1308
|
|
|
1245
|
-
|
|
1246
|
-
`
|
|
1247
|
-
|
|
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:
|
|
1248
1312
|
|
|
1249
1313
|
```ts
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
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
|
+
})
|
|
1253
1326
|
|
|
1254
|
-
|
|
1255
|
-
|
|
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
|
+
})
|
|
1256
1341
|
```
|
|
1257
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
|
+
|
|
1258
1360
|
### Rate limiting
|
|
1259
1361
|
|
|
1260
1362
|
```ts
|
|
@@ -1450,10 +1552,43 @@ contract:
|
|
|
1450
1552
|
- for `GET` / `DELETE`, the remaining fields become the **query string**
|
|
1451
1553
|
(arrays become repeated keys),
|
|
1452
1554
|
- for `POST` / `PUT` / `PATCH`, they become the **JSON body**,
|
|
1453
|
-
- a `multipart` field is a `Blob` (web / Bun) or
|
|
1454
|
-
(`{ uri, name, type }`,
|
|
1455
|
-
|
|
1456
|
-
|
|
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.
|
|
1457
1592
|
|
|
1458
1593
|
### Many contracts at once
|
|
1459
1594
|
|
|
@@ -2030,15 +2165,15 @@ handlers must still be idempotent where retries matter.
|
|
|
2030
2165
|
|
|
2031
2166
|
A tool call runs the same handler an HTTP request would. `lifecycle` makes it
|
|
2032
2167
|
run the same gate: a `beforeHandle` (throw to reject) and an `afterHandle`
|
|
2033
|
-
(transform the result)
|
|
2034
|
-
|
|
2035
|
-
|
|
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:
|
|
2036
2171
|
|
|
2037
2172
|
```ts
|
|
2038
2173
|
createMcpHandler({ serverInfo, auth, services, lifecycle: { beforeHandle: authHook } })
|
|
2039
2174
|
```
|
|
2040
2175
|
|
|
2041
|
-
Without it, a tool call bypasses the HTTP `
|
|
2176
|
+
Without it, a tool call bypasses the HTTP `authorize` gate — the contract's
|
|
2042
2177
|
`scope` is not enforced on the MCP / agent surface. `mountMcp`, `mountAgent` and
|
|
2043
2178
|
`buildMcpServer` take `lifecycle` too.
|
|
2044
2179
|
|
|
@@ -2400,7 +2535,7 @@ the merged `params` + `input`. `context` is merged into every tool handler's
|
|
|
2400
2535
|
| `runtimeTools` | framework-managed pathless operations from `defineRuntimeTool` |
|
|
2401
2536
|
|
|
2402
2537
|
`lifecycle` works the same as on the MCP server — without it an agent tool call
|
|
2403
|
-
bypasses the HTTP `
|
|
2538
|
+
bypasses the HTTP `authorize` gate. Pass your `createAuthHook` result.
|
|
2404
2539
|
|
|
2405
2540
|
### Adding tool-only args — `extend`
|
|
2406
2541
|
|
|
@@ -2795,7 +2930,7 @@ await createCli({
|
|
|
2795
2930
|
version: '1.0.0',
|
|
2796
2931
|
auth: await resolveIdentityFromToken(process.env.MYAPP_TOKEN),
|
|
2797
2932
|
context: (identity) => ({ user: identity }), // resolveFromContext reads this
|
|
2798
|
-
lifecycle: { beforeHandle: authHook }, // same
|
|
2933
|
+
lifecycle: { beforeHandle: authHook }, // same policy; HTTP wires it as authorize
|
|
2799
2934
|
services,
|
|
2800
2935
|
})
|
|
2801
2936
|
```
|
|
@@ -3348,10 +3483,12 @@ The framework attaches no meaning to the strings — `'public'`, `'user'`,
|
|
|
3348
3483
|
|
|
3349
3484
|
## `createAuthHook`
|
|
3350
3485
|
|
|
3351
|
-
`createAuthHook` builds
|
|
3352
|
-
|
|
3353
|
-
|
|
3354
|
-
|
|
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.
|
|
3355
3492
|
|
|
3356
3493
|
```ts
|
|
3357
3494
|
import { createAuthHook, createServer } from 'stitchkit/server'
|
|
@@ -3366,7 +3503,7 @@ const authHook = createAuthHook<User>({
|
|
|
3366
3503
|
inject: (ctx, user) => { ctx.user = user },
|
|
3367
3504
|
})
|
|
3368
3505
|
|
|
3369
|
-
createServer({ services, hooks: {
|
|
3506
|
+
createServer({ services, hooks: { authorize: authHook } })
|
|
3370
3507
|
```
|
|
3371
3508
|
|
|
3372
3509
|
### `AuthRule`
|
|
@@ -3376,8 +3513,9 @@ The value of each `rules` entry, keyed by scope:
|
|
|
3376
3513
|
- **`'public'`** — always passes; the identity is attached if present.
|
|
3377
3514
|
- **`'authenticated'`** — any resolved identity passes; no identity ⇒ 401.
|
|
3378
3515
|
- **a function** `(identity, ctx) => boolean | Promise<boolean>` — a custom
|
|
3379
|
-
check. It receives
|
|
3380
|
-
|
|
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.
|
|
3381
3519
|
|
|
3382
3520
|
#### Resource-scoped rule — reading a path/prefix param
|
|
3383
3521
|
|
|
@@ -3424,14 +3562,15 @@ catches a scope you forgot to cover.
|
|
|
3424
3562
|
|
|
3425
3563
|
### Auth on the tool surface — `resolveFromContext`
|
|
3426
3564
|
|
|
3427
|
-
The hook
|
|
3428
|
-
|
|
3429
|
-
surface:
|
|
3565
|
+
The same hook guards every transport, but the lifecycle slot and identity
|
|
3566
|
+
source differ:
|
|
3430
3567
|
|
|
3431
|
-
- **HTTP** — `resolve(ctx)`
|
|
3568
|
+
- **HTTP** — `hooks.authorize` calls `resolve(ctx)` from `ctx.req` (a cookie or
|
|
3569
|
+
bearer token) before body parsing.
|
|
3432
3570
|
- **Tool calls (MCP / agent)** — there is no `req`. The transport authenticated
|
|
3433
3571
|
the caller (an MCP API key) and `buildMcpServer`'s `context` injected the
|
|
3434
|
-
identity into `ctx`. `
|
|
3572
|
+
identity into `ctx`. `lifecycle.beforeHandle` calls
|
|
3573
|
+
`resolveFromContext(ctx)` to locate it.
|
|
3435
3574
|
|
|
3436
3575
|
```ts
|
|
3437
3576
|
const authHook = createAuthHook<User>({
|
|
@@ -3446,6 +3585,11 @@ differs. If you omit `resolveFromContext`, a scoped tool call has no identity
|
|
|
3446
3585
|
and **fails closed** (rejected by `onAnonymous`) — the hook never silently
|
|
3447
3586
|
passes a tool call it cannot authenticate. → [ADR 0014](../decisions/0014-tool-http-parity.md)
|
|
3448
3587
|
|
|
3588
|
+
```ts
|
|
3589
|
+
createServer({ services, hooks: { authorize: authHook } })
|
|
3590
|
+
createMcpHandler({ services, lifecycle: { beforeHandle: authHook } })
|
|
3591
|
+
```
|
|
3592
|
+
|
|
3449
3593
|
## `createBearerResolver`
|
|
3450
3594
|
|
|
3451
3595
|
For API-key or bearer-token auth (the usual MCP case), `createBearerResolver`
|
|
@@ -3806,6 +3950,66 @@ Each sink runs fire-and-forget and fails independently: a slow or broken request
|
|
|
3806
3950
|
sink cannot block the response, suppress operational logging or break the tool
|
|
3807
3951
|
sink.
|
|
3808
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
|
+
|
|
3809
4013
|
### RequestEvent
|
|
3810
4014
|
|
|
3811
4015
|
Every surface produces the same shape — so a single audit table stays
|
|
@@ -4337,8 +4541,9 @@ Notes for a Node host:
|
|
|
4337
4541
|
to join the two logs.
|
|
4338
4542
|
- **Rate limiting** — `createRateLimiter` in `onRequest` for a global limit;
|
|
4339
4543
|
per-route limits belong in `beforeHandle`.
|
|
4340
|
-
- **Auth** —
|
|
4341
|
-
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.
|
|
4342
4547
|
- **Errors** — handlers throw `AppError`; let the standard envelope render them.
|
|
4343
4548
|
Add an `onError` hook only to integrate an error tracker.
|
|
4344
4549
|
- **Secrets** — read them from the environment; never commit them.
|
|
@@ -4397,7 +4602,7 @@ matched value is on the context root as `ctx.tenantId`
|
|
|
4397
4602
|
```ts
|
|
4398
4603
|
createServer({
|
|
4399
4604
|
groups: [
|
|
4400
|
-
{ pathPrefix: '/tenants/:tenantId', services: [widgetsService], hooks: {
|
|
4605
|
+
{ pathPrefix: '/tenants/:tenantId', services: [widgetsService], hooks: { authorize: authHook } },
|
|
4401
4606
|
],
|
|
4402
4607
|
})
|
|
4403
4608
|
// → /tenants/:tenantId/widgets
|
|
@@ -4411,7 +4616,7 @@ the flat `services` list — no hand-partitioning, the mapping lives in one plac
|
|
|
4411
4616
|
createServer({
|
|
4412
4617
|
services, // mixed scopes, listed once
|
|
4413
4618
|
scopePrefixes: { tenant: 'tenants/:tenantId', project: 'projects/:projectId' },
|
|
4414
|
-
hooks: {
|
|
4619
|
+
hooks: { authorize: authHook },
|
|
4415
4620
|
})
|
|
4416
4621
|
// `tenant`-scoped → /tenants/:tenantId/..., `project` → /projects/:projectId/..., the rest flat
|
|
4417
4622
|
```
|
|
@@ -4612,6 +4817,86 @@ current one *up to* your target, and apply each snippet.
|
|
|
4612
4817
|
runtime): bootstrap the server, one HTTP request, and any feature you rely on
|
|
4613
4818
|
(Socket.IO connect, an MCP tool call, a multipart upload, …).
|
|
4614
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
|
+
|
|
4615
4900
|
## Worked example — frozen on 0.3, jumping to 0.7
|
|
4616
4901
|
|
|
4617
4902
|
1. `bun.lock` → consumer resolves `stitchkit@0.3.x`.
|
|
@@ -5299,6 +5584,7 @@ The browser-and-server entrypoint. Re-exports everything from
|
|
|
5299
5584
|
| `createUrlBuilders` | function | build one exact URL builder per contract in a registry |
|
|
5300
5585
|
| `UrlBuilderConfig` | _type_ | explicit `{ baseUrl }` source for a URL builder |
|
|
5301
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) |
|
|
5302
5588
|
| `ContractClientConfig` | _type_ | per-tenant / resource-scoped client config — dynamic `pathPrefix` + `stripPrefixKeys` ([guide](../guide/client.md#contractclientconfig--per-tenant--resource-scoped-clients)) |
|
|
5303
5589
|
| `contractEndpointMatchers` | function | compile exact pathname matchers for selected HTTP contract operations and expected-401 policy |
|
|
5304
5590
|
| `PathPrefixArgs` | _type_ | required string-valued keys exposed to a typed dynamic `pathPrefix` callback |
|
|
@@ -5388,6 +5674,7 @@ from the root `stitchkit`.
|
|
|
5388
5674
|
| `TransportSource` | _type_ | `http \| mcp \| agent \| cli` — the value of `ctx.source` |
|
|
5389
5675
|
| `RuntimeContext` | _type_ | the loose context seen by transport and hooks |
|
|
5390
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 |
|
|
5391
5678
|
| `EndpointFn` | _type_ | the call signature of one client method |
|
|
5392
5679
|
| `TypedClient` | _type_ | the full typed client for a contract |
|
|
5393
5680
|
| `TypedHttpClient` | _type_ | the typed client, HTTP endpoints only (`= ScopedHttpClient<C, unknown>`) |
|
|
@@ -5398,6 +5685,9 @@ from the root `stitchkit`.
|
|
|
5398
5685
|
| `ScopedUrlFn` | _type_ | one URL method's signature with scoped-prefix keys folded in |
|
|
5399
5686
|
| `MultipartFile` | _type_ | a `multipart` file field — `Blob \| FileDescriptor` |
|
|
5400
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 |
|
|
5401
5691
|
| `EndpointToolAnnotations` | _type_ | MCP behavioural hints on an endpoint (`readOnlyHint` / `destructiveHint` / `title`) |
|
|
5402
5692
|
| `EndpointUiMeta` | _type_ | MCP Apps widget metadata on an endpoint |
|
|
5403
5693
|
| `EndpointMcpInputRequired` | _type_ | typed MCP multi-round input request (`key`, message and Zod object schema) |
|
|
@@ -5476,7 +5766,8 @@ Also re-exports the error helpers from `stitchkit/contract`.
|
|
|
5476
5766
|
| `MethodDef` | _type_ | one resolved endpoint inside a service |
|
|
5477
5767
|
| `OperationIdentity` | _type_ | path-free service/action/scope/method identity shared by contract and native tool operations |
|
|
5478
5768
|
| `Handlers` | _type_ | the typed handler map `implement` expects |
|
|
5479
|
-
| `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 |
|
|
5480
5771
|
| `RouteGroup` | _type_ | a prefixed group of services with its own hooks |
|
|
5481
5772
|
| `RawRoute` | _type_ | a non-contract `Request → Response` route with a concrete `BunServer` context |
|
|
5482
5773
|
| `RawRouteContext` | _type_ | the Bun-bound routing context a raw handler receives |
|
|
@@ -5493,7 +5784,7 @@ Also re-exports the error helpers from `stitchkit/contract`.
|
|
|
5493
5784
|
|
|
5494
5785
|
| Export | Kind | Summary |
|
|
5495
5786
|
|--------|------|---------|
|
|
5496
|
-
| `createAuthHook` | function |
|
|
5787
|
+
| `createAuthHook` | function | one scope gate for HTTP `authorize` and tool `beforeHandle` — [guide](../guide/auth-and-errors.md#createauthhook) |
|
|
5497
5788
|
| `createErrorHook` | function | an async-capable, endpoint-aware `onError` hook from a code map + envelope renderer — [guide](../guide/auth-and-errors.md#createerrorhook) |
|
|
5498
5789
|
| `ErrorHookConfig` | _type_ | async observer/renderer config for `createErrorHook` |
|
|
5499
5790
|
| `ResolvedError` | _type_ | the normalised error handed to `createErrorHook`'s `render` |
|
|
@@ -5551,8 +5842,14 @@ Also re-exports the error helpers from `stitchkit/contract`.
|
|
|
5551
5842
|
|--------|------|---------|
|
|
5552
5843
|
| `streamSSE` | function | an async generator → SSE `Response` — [guide](../guide/server.md#sse-streaming) |
|
|
5553
5844
|
| `parseSSE` | function | parse an SSE `Response` (also on the root entrypoint) |
|
|
5845
|
+
| `MultipartLifecycle` | _type_ | request-scoped rollback ownership for accepted streamed handles |
|
|
5554
5846
|
| `MultipartResult` | _type_ | what `parseMultipart` returns |
|
|
5555
|
-
| `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` |
|
|
5556
5853
|
| `createRateLimiter` | function | token-bucket rate limiting — [guide](../guide/server.md#rate-limiting) |
|
|
5557
5854
|
| `createCache` | function | an in-memory TTL cache |
|
|
5558
5855
|
| `CacheOptions` | _type_ | bounded-cache options, including the maximum retained entry count |
|
|
@@ -5597,9 +5894,12 @@ audit event. See the [Observability guide](../guide/observability.md).
|
|
|
5597
5894
|
| `createObservability` | function | configure framework-owned request completion and canonical tool event sinks — [guide](../guide/observability.md#createobservability) |
|
|
5598
5895
|
| `RequestEvent` | _type_ | the normalised audit event handed to the sink |
|
|
5599
5896
|
| `ObservabilityConfig` | _type_ | independent request and tool sink configuration |
|
|
5600
|
-
| `Observability` | _type_ |
|
|
5601
|
-
| `RequestEventSinkConfig` | _type_ | `write`,
|
|
5897
|
+
| `Observability` | _type_ | `{ request?, toolCall, flush(), close() }` with bounded sink lifecycle |
|
|
5898
|
+
| `RequestEventSinkConfig` | _type_ | `write`, filter/sanitisation, `maxPending`, `onSinkError` and `onDrop` |
|
|
5602
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 |
|
|
5603
5903
|
| `HttpRequestCompletion` | _type_ | the single framework-owned HTTP outcome projected to logging and request events |
|
|
5604
5904
|
| `HttpRequestObserver` | _type_ | server-facing projection consumed by `HandlerConfig.observability` |
|
|
5605
5905
|
|