stitchkit 0.48.0 → 0.49.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 +1 -2
- package/dist/browser/client.d.ts +3 -0
- package/dist/browser/client.d.ts.map +1 -1
- package/dist/browser/http.d.ts +7 -4
- package/dist/browser/http.d.ts.map +1 -1
- package/dist/cli.js +3 -2
- package/dist/{index-nrytvb30.js → index-03j2t778.js} +5 -8
- package/dist/{index-kp8xamqp.js → index-0hj37z43.js} +4 -2
- package/dist/index-48ffdxgk.js +6 -0
- package/dist/{index-44xysy8r.js → index-fjfzsq6y.js} +383 -26
- package/dist/index-h05ygjqx.js +149 -0
- package/dist/{index-ee621cmy.js → index-jewp9r0a.js} +5 -139
- package/dist/index-p9d4cxt5.js +436 -0
- package/dist/{index-8ekq6res.js → index-v58mwa19.js} +4 -2
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +47 -3
- package/dist/node.d.ts +4 -3
- package/dist/node.d.ts.map +1 -1
- package/dist/node.js +109 -14
- package/dist/observability/audit.d.ts +4 -1
- package/dist/observability/audit.d.ts.map +1 -1
- package/dist/observability/index.d.ts +1 -0
- package/dist/observability/index.d.ts.map +1 -1
- package/dist/observability/index.js +150 -16
- package/dist/observability/status.d.ts +105 -0
- package/dist/observability/status.d.ts.map +1 -0
- package/dist/server/bun.d.ts +7 -8
- package/dist/server/bun.d.ts.map +1 -1
- package/dist/server/implement.d.ts +16 -6
- package/dist/server/implement.d.ts.map +1 -1
- package/dist/server/index.d.ts +4 -3
- package/dist/server/index.d.ts.map +1 -1
- package/dist/server/index.js +132 -11
- package/dist/server/node.d.ts +12 -13
- package/dist/server/node.d.ts.map +1 -1
- package/dist/server/router.d.ts +4 -1
- package/dist/server/router.d.ts.map +1 -1
- package/dist/server/shutdown.d.ts +76 -0
- package/dist/server/shutdown.d.ts.map +1 -0
- package/dist/server/socket-io-config.d.ts +5 -1
- package/dist/server/socket-io-config.d.ts.map +1 -1
- package/dist/server/socket-io-node.d.ts +4 -1
- package/dist/server/socket-io-node.d.ts.map +1 -1
- package/dist/server/socket-io.d.ts +12 -4
- package/dist/server/socket-io.d.ts.map +1 -1
- package/dist/testing.d.ts +34 -0
- package/dist/testing.d.ts.map +1 -0
- package/dist/testing.js +36 -0
- package/dist/tools/mcp.d.ts +1 -0
- package/dist/tools/mcp.d.ts.map +1 -1
- package/dist/tools.js +21 -428
- package/llms-full.txt +290 -43
- package/package.json +8 -3
package/llms-full.txt
CHANGED
|
@@ -33,7 +33,7 @@ realtime, `@tanstack/react-query` + `react-query-kit` for React). See
|
|
|
33
33
|
|
|
34
34
|
## Entrypoints
|
|
35
35
|
|
|
36
|
-
stitchkit ships
|
|
36
|
+
stitchkit ships nine entrypoints. Each is import-safe for one environment —
|
|
37
37
|
keeping server-only code (`Bun.serve`, the MCP SDK) out of browser bundles.
|
|
38
38
|
|
|
39
39
|
| Import | Use in | Holds |
|
|
@@ -45,6 +45,7 @@ keeping server-only code (`Bun.serve`, the MCP SDK) out of browser bundles.
|
|
|
45
45
|
| `stitchkit/tools` | server | `createMcpHandler`, `mountMcp`, `mountAgent`, the OAuth provider, native tools |
|
|
46
46
|
| `stitchkit/cli` | server | `createCli` — the CLI transport, light (no MCP SDK / `ai`) |
|
|
47
47
|
| `stitchkit/observability` | server | request/tool event projections — `createObservability`, trace context, sanitisation |
|
|
48
|
+
| `stitchkit/testing` | tests on Bun or Node | in-process generated clients over a real Fetch handler, without a TCP port |
|
|
48
49
|
| `stitchkit/react` | browser | `createCursorQuery`, `createCacheBridge` |
|
|
49
50
|
|
|
50
51
|
Rule of thumb: browser code imports `stitchkit` and `stitchkit/react`; server
|
|
@@ -685,10 +686,35 @@ export const implement = createImplement<AppContext>()
|
|
|
685
686
|
// every implement() call now has ctx.user typed
|
|
686
687
|
```
|
|
687
688
|
|
|
689
|
+
### `implementRegistry` — one backend registry
|
|
690
|
+
|
|
691
|
+
When contracts already live in one literal registry, bind the backend from that
|
|
692
|
+
same source instead of maintaining a parallel `services` list:
|
|
693
|
+
|
|
694
|
+
```ts
|
|
695
|
+
import { implementRegistry } from 'stitchkit/server'
|
|
696
|
+
|
|
697
|
+
export const apiServices = implementRegistry(apiContractRegistry, {
|
|
698
|
+
users: usersHandlers,
|
|
699
|
+
posts: postsHandlers,
|
|
700
|
+
})
|
|
701
|
+
```
|
|
702
|
+
|
|
703
|
+
Every registry key is required, extra keys fail, and each handler map is checked
|
|
704
|
+
against its own contract. The returned service order follows the contract
|
|
705
|
+
registry order. `createImplementRegistry<AppContext>()` fixes the application
|
|
706
|
+
context once, like `createImplement` does for a single contract. Runtime callers
|
|
707
|
+
also fail first on missing/extra keys and duplicate contract prefixes.
|
|
708
|
+
|
|
709
|
+
The registry is intentionally flat: every key must point to one concrete
|
|
710
|
+
`defineContract()` result. Composed namespace arrays are mounted explicitly with
|
|
711
|
+
`implement()` because they do not have a one-key-to-one-handler-map boundary.
|
|
712
|
+
|
|
688
713
|
## `createServer`
|
|
689
714
|
|
|
690
|
-
`createServer(config)` builds the router and starts `Bun.serve()`. It returns
|
|
691
|
-
the
|
|
715
|
+
`createServer(config)` builds the router and starts `Bun.serve()`. It returns a
|
|
716
|
+
managed handle with `url`, `port`, the concrete server under `runtime`, live
|
|
717
|
+
`status`, and one idempotent `shutdown()` lifecycle.
|
|
692
718
|
|
|
693
719
|
```ts
|
|
694
720
|
import { createServer } from 'stitchkit/server'
|
|
@@ -721,8 +747,34 @@ server. See [Testing & deployment](./testing-and-deployment.md).
|
|
|
721
747
|
| `logging` | `true` for built-in request logs, or a `LoggingConfig` (see below) |
|
|
722
748
|
| `traceId` | override per-request trace-id resolution — may return `undefined` to fall back |
|
|
723
749
|
| `wrapFetch` | compose wrappers around the finished handler (request context, audit) |
|
|
724
|
-
| `
|
|
725
|
-
| `
|
|
750
|
+
| `socket` | full Stitchkit Socket.IO handle; route, default WebSocket handler and shutdown are mounted once |
|
|
751
|
+
| `websocket` | custom Bun WebSocket handler; with `socket`, this is the explicit composed handler |
|
|
752
|
+
| `development` / `bun` | passthrough to `Bun.serve` |
|
|
753
|
+
|
|
754
|
+
Native Bun `routes` are intentionally not accepted: Bun matches them before
|
|
755
|
+
`fetch`, so they could bypass shutdown admission. Use `rawRoutes`; they retain
|
|
756
|
+
the Fetch `Request → Response` model and participate in lifecycle tracking.
|
|
757
|
+
|
|
758
|
+
### Managed shutdown
|
|
759
|
+
|
|
760
|
+
```ts
|
|
761
|
+
const server = createServer({ services, socket })
|
|
762
|
+
|
|
763
|
+
const result = await server.shutdown({
|
|
764
|
+
gracePeriodMs: 30_000,
|
|
765
|
+
retryAfterSeconds: 5,
|
|
766
|
+
signal: shutdownController.signal,
|
|
767
|
+
})
|
|
768
|
+
```
|
|
769
|
+
|
|
770
|
+
The first call closes HTTP and Socket.IO admission, drains accepted application
|
|
771
|
+
requests, closes realtime transports and stops the runtime within one total
|
|
772
|
+
budget. Repeated calls return the same Promise; the first options win. New
|
|
773
|
+
ordinary HTTP work receives `503`, `Retry-After` and `Connection: close` outside
|
|
774
|
+
`wrapFetch`. `result.outcome` is `clean` or `forced`; a forced result preserves
|
|
775
|
+
the pending snapshot and reason while final pending counters describe the
|
|
776
|
+
post-close transport state. `runtime` is a diagnostics escape hatch, not a
|
|
777
|
+
second canonical stop path.
|
|
726
778
|
|
|
727
779
|
### Trusted HTTPS in development
|
|
728
780
|
|
|
@@ -991,6 +1043,21 @@ not retain the text. `maxJsonBodyBytes` may also be set once on `createServer` /
|
|
|
991
1043
|
`createHandler`; a route value wins. Both limits are opt-in and abort an
|
|
992
1044
|
oversized stream before it is fully buffered. → ADR 0051
|
|
993
1045
|
|
|
1046
|
+
### Choosing an HTTP boundary
|
|
1047
|
+
|
|
1048
|
+
| Need | Contract declaration | What remains framework-owned |
|
|
1049
|
+
|------|----------------------|------------------------------|
|
|
1050
|
+
| Typed JSON request/response | ordinary `input` / `output` | routing, auth, schemas, hooks, client, OpenAPI |
|
|
1051
|
+
| HMAC-signed JSON | `rawBody: true` + `input` / `output` | the same pipeline plus the exact decoded request text |
|
|
1052
|
+
| File upload | typed `multipart` descriptor | file cardinality/limits, text input validation and client form encoding |
|
|
1053
|
+
| File, stream or redirect response behind contract auth | `rawResponse: true` | request parsing, route identity, auth and typed URL/client surface |
|
|
1054
|
+
| Transport that cannot be expressed as the contract pipeline | `RawRoute` | only raw routing, CORS, request hook and error normalisation |
|
|
1055
|
+
|
|
1056
|
+
`rawBody` is for JSON signatures: verify `ctx.rawBody` against the signature
|
|
1057
|
+
header, then use the already validated `ctx.input`. A provider-specific binary
|
|
1058
|
+
signature protocol, OAuth callback or WebSocket upgrade can be a real raw route,
|
|
1059
|
+
but it must own its validation and authorization explicitly.
|
|
1060
|
+
|
|
994
1061
|
## Typed JSON response metadata
|
|
995
1062
|
|
|
996
1063
|
A JSON endpoint that must attach dynamic HTTP headers while preserving typed
|
|
@@ -1158,6 +1225,13 @@ wildcard — and the two combine: `/app/:slug/*filePath` matches `/app/x/a/b` wi
|
|
|
1158
1225
|
deep-link fallback). List more specific routes before the wildcard — the first
|
|
1159
1226
|
match wins. `staticRoute()` builds a raw route that serves a directory.
|
|
1160
1227
|
|
|
1228
|
+
At startup Stitchkit rejects exact duplicates, equivalent parameter shapes
|
|
1229
|
+
(`/users/:id` vs `/users/:userId`) and any later raw route completely hidden by
|
|
1230
|
+
an earlier raw route. Partial overlap stays legal, including the recommended
|
|
1231
|
+
specific-before-wildcard order. `GET` and `HEAD` remain independent because that
|
|
1232
|
+
is how the actual raw matcher dispatches them. Raw-vs-contract overlap remains a
|
|
1233
|
+
startup diagnostic naming the bypassed contract identity and scope.
|
|
1234
|
+
|
|
1161
1235
|
### Raw-route helpers
|
|
1162
1236
|
|
|
1163
1237
|
A raw route gives up the contract pipeline, so three things get re-implemented in
|
|
@@ -1483,7 +1557,7 @@ without repeating transport configuration.
|
|
|
1483
1557
|
| `baseUrl` | — | URL prefix for every request |
|
|
1484
1558
|
| `timeout` | `30000` | request timeout, ms |
|
|
1485
1559
|
| `credentials` | `'include'` | fetch credentials mode |
|
|
1486
|
-
| `retry` | 2
|
|
1560
|
+
| `retry` | 2 retries after the initial GET; network errors only | transport retry policy |
|
|
1487
1561
|
| `headers` | — | extra headers — an object, or a function re-run per request |
|
|
1488
1562
|
| `suppressUnauthorizedFor` | `[]` | exact contract-derived operation matchers whose expected 401 does not emit `unauthorized` |
|
|
1489
1563
|
| `parseError` | built-in | map an error body to `{ code, message, details, hint }` |
|
|
@@ -1509,7 +1583,19 @@ end-to-end. A `traceparent` you set yourself (via `headers`) always wins.
|
|
|
1509
1583
|
Retry is deliberately conservative: only a connection that never landed (a
|
|
1510
1584
|
network error), only on idempotent `GET`. A server that *responded* with a 5xx
|
|
1511
1585
|
is the data layer's call (TanStack Query), not the transport's — retrying in
|
|
1512
|
-
both places multiplies attempts.
|
|
1586
|
+
both places multiplies attempts. `retry.limit` counts retries after the initial
|
|
1587
|
+
attempt, so the default `2` permits at most three total attempts. The default
|
|
1588
|
+
`statusCodes: []` does not replay HTTP responses; explicit `methods` or
|
|
1589
|
+
`statusCodes` expand that policy when a project has a proven idempotent case.
|
|
1590
|
+
|
|
1591
|
+
Inside Next.js 16 server rendering, the first attempt still uses Next's normal
|
|
1592
|
+
request memoization. If Ky authorizes a retry after a network rejection,
|
|
1593
|
+
Stitchkit passes that retry's current `Request.signal` in the second fetch
|
|
1594
|
+
argument and materializes the current Request as URL + init. Next 16.3 otherwise
|
|
1595
|
+
merges `init` into a Request before its dedupe layer and loses the explicit
|
|
1596
|
+
signal opt-out. The retry therefore performs a new network attempt instead of
|
|
1597
|
+
returning the cached rejection. This adapter does not broaden retry policy: POST, unconfigured HTTP
|
|
1598
|
+
statuses, cancellation and exhausted budgets retain the rules above.
|
|
1513
1599
|
|
|
1514
1600
|
## `createClient`
|
|
1515
1601
|
|
|
@@ -2251,15 +2337,18 @@ validateMcpSchemas({
|
|
|
2251
2337
|
extend,
|
|
2252
2338
|
flattenUnionInput: true,
|
|
2253
2339
|
requireTypedProperties: true,
|
|
2254
|
-
allowUntyped: ['docs_create.payload'], // deliberately free-form
|
|
2255
2340
|
requirePortableFormats: true,
|
|
2256
2341
|
allowFormats: [],
|
|
2257
2342
|
})
|
|
2258
2343
|
```
|
|
2259
2344
|
|
|
2260
|
-
|
|
2261
|
-
|
|
2262
|
-
|
|
2345
|
+
For an arbitrary **JSON value**, declare `z.json()`: its recursive JSON Schema
|
|
2346
|
+
describes strings, numbers, booleans, null, arrays and objects, so strict schema
|
|
2347
|
+
validation needs no exception. `z.unknown()` means an unknown runtime value —
|
|
2348
|
+
including values JSON cannot represent — and therefore gives a model no usable
|
|
2349
|
+
shape. The guard is off by default for existing contracts. `allowUntyped` takes
|
|
2350
|
+
dotted `tool.property` paths and is reserved for a presentation value that is
|
|
2351
|
+
genuinely impossible to represent as JSON Schema. On `createMcpHandler`, put the policy under
|
|
2263
2352
|
`schemaValidation`; the handler supplies its real `extend` and
|
|
2264
2353
|
`flattenUnionInput`, so the check cannot vet a different document from the one
|
|
2265
2354
|
advertised.
|
|
@@ -3050,13 +3139,12 @@ realtime.onConnection(({ raw, events, to }) => {
|
|
|
3050
3139
|
})
|
|
3051
3140
|
```
|
|
3052
3141
|
|
|
3053
|
-
|
|
3142
|
+
Pass the full handle to `createServer`; it mounts and owns the transport once:
|
|
3054
3143
|
|
|
3055
3144
|
```ts
|
|
3056
3145
|
createServer({
|
|
3057
3146
|
services,
|
|
3058
|
-
|
|
3059
|
-
rawRoutes: [socket.route], // ready-made /socket.io/*socketPath route
|
|
3147
|
+
socket,
|
|
3060
3148
|
})
|
|
3061
3149
|
|
|
3062
3150
|
// elsewhere — validated broadcast:
|
|
@@ -3076,11 +3164,15 @@ export function publishExampleNote(realtime: ExampleRealtimePublisher): void {
|
|
|
3076
3164
|
| Handle field | Purpose |
|
|
3077
3165
|
|--------------|---------|
|
|
3078
3166
|
| `io` | raw Socket.IO server for middleware, handshake auth and transport ownership |
|
|
3079
|
-
| `websocket` | Bun
|
|
3080
|
-
| `route` |
|
|
3081
|
-
|
|
3082
|
-
`
|
|
3083
|
-
|
|
3167
|
+
| `websocket` | Bun handlers used directly only in explicit raw-lane composition |
|
|
3168
|
+
| `route` | `/socket.io/*socketPath`, mounted automatically by `createServer({ socket })` |
|
|
3169
|
+
| `close()` | idempotent standalone close for CLI/tools with no HTTP server |
|
|
3170
|
+
| `beginShutdown()` / `connections()` | lifecycle surface consumed by the managed server |
|
|
3171
|
+
|
|
3172
|
+
`SocketIOServerConfig` also takes `path`, `transports`, `pingTimeout`,
|
|
3173
|
+
`pingInterval` and a runtime-neutral `allowRequest(Request)` handshake policy.
|
|
3174
|
+
The policy is composed with managed-shutdown admission on both Bun and Node.
|
|
3175
|
+
For anything else socket.io's `ServerOptions` exposes, use the
|
|
3084
3176
|
typed **`serverOptions`** passthrough — most often `maxHttpBufferSize` to lift the
|
|
3085
3177
|
1 MB default for large emits:
|
|
3086
3178
|
|
|
@@ -3091,7 +3183,8 @@ await createSocketIOServer({
|
|
|
3091
3183
|
})
|
|
3092
3184
|
```
|
|
3093
3185
|
|
|
3094
|
-
The wrapper-owned fields (`cors` / `path` / `transports` / `ping*`
|
|
3186
|
+
The wrapper-owned fields (`cors` / `path` / `transports` / `ping*` /
|
|
3187
|
+
`allowRequest`) take
|
|
3095
3188
|
precedence over the same keys in `serverOptions`. On Bun the engine-level options
|
|
3096
3189
|
(`maxHttpBufferSize`, the ping heartbeat, `upgradeTimeout`) are forwarded to
|
|
3097
3190
|
`@socket.io/bun-engine` too — so a configured `maxHttpBufferSize` actually applies
|
|
@@ -3388,8 +3481,9 @@ const websocket = composeWebSocketHandlers(
|
|
|
3388
3481
|
|
|
3389
3482
|
createServer({
|
|
3390
3483
|
services,
|
|
3484
|
+
socket,
|
|
3391
3485
|
websocket,
|
|
3392
|
-
rawRoutes: [
|
|
3486
|
+
rawRoutes: [pcmRoute],
|
|
3393
3487
|
})
|
|
3394
3488
|
```
|
|
3395
3489
|
|
|
@@ -3981,8 +4075,8 @@ export const observability = createObservability({
|
|
|
3981
4075
|
},
|
|
3982
4076
|
})
|
|
3983
4077
|
|
|
3984
|
-
|
|
3985
|
-
await observability.close()
|
|
4078
|
+
const status = observability.getStatus()
|
|
4079
|
+
const drained = await observability.close()
|
|
3986
4080
|
```
|
|
3987
4081
|
|
|
3988
4082
|
`maxPending` defaults to `1000` per sink and must be a positive safe integer.
|
|
@@ -3993,9 +4087,15 @@ cancelled. Sink, filter and diagnostic-callback failures remain isolated from
|
|
|
3993
4087
|
the observed request or tool call and are reported through `onSinkError` when
|
|
3994
4088
|
configured; `onSinkError`/`onDrop` cannot create unhandled rejections.
|
|
3995
4089
|
|
|
4090
|
+
`getStatus()` returns immutable per-surface and aggregate counters: capacity,
|
|
4091
|
+
received, accepted, filtered, completed, dropped, failed, preparation failures,
|
|
4092
|
+
preparing/pending work and the closed flag. Read it from readiness or metrics
|
|
4093
|
+
without parsing callback logs.
|
|
4094
|
+
|
|
3996
4095
|
`flush()` snapshots the current generation and waits only for events admitted
|
|
3997
4096
|
up to that call. `close()` atomically stops admission, drains every accepted
|
|
3998
|
-
generation and
|
|
4097
|
+
generation and returns the final counters plus `durationMs`; repeated calls
|
|
4098
|
+
return the same report. Graceful shutdown order is therefore:
|
|
3999
4099
|
|
|
4000
4100
|
1. stop HTTP/MCP admission;
|
|
4001
4101
|
2. wait for active requests and tool calls;
|
|
@@ -4431,7 +4531,33 @@ never block or break the request. Swallow the sink's own errors.
|
|
|
4431
4531
|
stitchkit's own test suite runs on `bun:test`. The contract makes most of an
|
|
4432
4532
|
API testable without a live socket.
|
|
4433
4533
|
|
|
4434
|
-
### Test
|
|
4534
|
+
### Test generated clients in process
|
|
4535
|
+
|
|
4536
|
+
`createHandlerTestClient` runs the real generated client against the real Fetch
|
|
4537
|
+
handler without opening a TCP port. It keeps URL construction, headers/cookies,
|
|
4538
|
+
multipart encoding, cancellation, output validation, `ApiError` and
|
|
4539
|
+
`x-request-id` correlation in the test path:
|
|
4540
|
+
|
|
4541
|
+
```ts
|
|
4542
|
+
import { createHandlerTestClient } from 'stitchkit/testing'
|
|
4543
|
+
|
|
4544
|
+
const api = createHandlerTestClient({
|
|
4545
|
+
contract: notes,
|
|
4546
|
+
handler,
|
|
4547
|
+
pathPrefix: 'api',
|
|
4548
|
+
client: { headers: { cookie: 'session=test' } },
|
|
4549
|
+
})
|
|
4550
|
+
|
|
4551
|
+
expect(await api.create({ text: 'hi' })).toEqual({ id: '1', text: 'hi' })
|
|
4552
|
+
```
|
|
4553
|
+
|
|
4554
|
+
`contractConfig` accepts the same scoped `pathPrefix` / `stripPrefixKeys` as
|
|
4555
|
+
`createClient`. `createHandlerTestClients` is the batch form for a literal
|
|
4556
|
+
contract registry. Both helpers are Fetch-only: they construct ordinary
|
|
4557
|
+
absolute `Request` objects and call `createHandler` directly, so Bun and Node
|
|
4558
|
+
exercise the same framework pipeline.
|
|
4559
|
+
|
|
4560
|
+
### Test handlers with raw Requests
|
|
4435
4561
|
|
|
4436
4562
|
`createHandler` is the router as a plain `(req) => Promise<Response>` function —
|
|
4437
4563
|
no `Bun.serve`, no port. Drive it with a `Request`:
|
|
@@ -4461,8 +4587,8 @@ test('create returns the note', async () => {
|
|
|
4461
4587
|
})
|
|
4462
4588
|
```
|
|
4463
4589
|
|
|
4464
|
-
This exercises the
|
|
4465
|
-
|
|
4590
|
+
This lower-level form exercises the same pipeline while letting a test inspect
|
|
4591
|
+
the raw `Response` itself.
|
|
4466
4592
|
|
|
4467
4593
|
### Test handlers directly
|
|
4468
4594
|
|
|
@@ -4505,8 +4631,34 @@ createServer({
|
|
|
4505
4631
|
})
|
|
4506
4632
|
```
|
|
4507
4633
|
|
|
4508
|
-
|
|
4509
|
-
|
|
4634
|
+
Keep the managed handle and wire process policy explicitly:
|
|
4635
|
+
|
|
4636
|
+
```ts
|
|
4637
|
+
const server = createServer({ services, socket })
|
|
4638
|
+
const force = new AbortController()
|
|
4639
|
+
let closing: Promise<void> | undefined
|
|
4640
|
+
|
|
4641
|
+
function shutdown() {
|
|
4642
|
+
if (closing) {
|
|
4643
|
+
force.abort() // a later signal shortens the same shutdown, not a second chain
|
|
4644
|
+
return closing
|
|
4645
|
+
}
|
|
4646
|
+
closing = server.shutdown({ gracePeriodMs: 30_000, signal: force.signal }).then(async result => {
|
|
4647
|
+
await mcp.close()
|
|
4648
|
+
await prisma.$disconnect()
|
|
4649
|
+
console.log(result)
|
|
4650
|
+
})
|
|
4651
|
+
return closing
|
|
4652
|
+
}
|
|
4653
|
+
|
|
4654
|
+
process.on('SIGTERM', () => void shutdown())
|
|
4655
|
+
process.on('SIGINT', () => void shutdown())
|
|
4656
|
+
```
|
|
4657
|
+
|
|
4658
|
+
The server owns HTTP/Socket.IO transport resources. MCP, databases, queues and
|
|
4659
|
+
domain run-state remain application resources and close explicitly after server
|
|
4660
|
+
drain. Do not call `runtime.stop()` or `socket.io.close()` in parallel with
|
|
4661
|
+
`shutdown()`.
|
|
4510
4662
|
|
|
4511
4663
|
### Deploy on Node
|
|
4512
4664
|
|
|
@@ -4517,10 +4669,13 @@ the listener differs: replace `createServer` with **`serveNode`** (from
|
|
|
4517
4669
|
```ts
|
|
4518
4670
|
import { serveNode } from 'stitchkit/node'
|
|
4519
4671
|
|
|
4520
|
-
serveNode({
|
|
4672
|
+
const server = await serveNode({
|
|
4521
4673
|
services,
|
|
4674
|
+
socket,
|
|
4522
4675
|
port: Number(process.env.PORT ?? 3000),
|
|
4523
4676
|
})
|
|
4677
|
+
|
|
4678
|
+
await server.shutdown({ gracePeriodMs: 30_000 })
|
|
4524
4679
|
```
|
|
4525
4680
|
|
|
4526
4681
|
Notes for a Node host:
|
|
@@ -4529,9 +4684,6 @@ Notes for a Node host:
|
|
|
4529
4684
|
raw routes use `RawRoute<TServer = unknown>`; supply a host server generic
|
|
4530
4685
|
only when an embedding adapter passes one to `createHandler`.
|
|
4531
4686
|
|
|
4532
|
-
- Add **`@types/bun`** as a dev dependency — it is an optional peer that types the
|
|
4533
|
-
shared `stitchkit/server` surface (without it `tsc` reports a missing `Bun`
|
|
4534
|
-
namespace).
|
|
4535
4687
|
- **Socket.IO** attaches to the Node HTTP server via `serveNode({ socket })`, and
|
|
4536
4688
|
on Node the default transport is `['websocket']` — set the client to match
|
|
4537
4689
|
(`transports: ['websocket']`). See [realtime](./realtime.md).
|
|
@@ -5541,9 +5693,68 @@ socket.io
|
|
|
5541
5693
|
socket.attach(nodeHttpServer)
|
|
5542
5694
|
```
|
|
5543
5695
|
|
|
5544
|
-
|
|
5545
|
-
|
|
5546
|
-
|
|
5696
|
+
Node consumers can remove `@types/bun` unless another dependency independently
|
|
5697
|
+
requires it.
|
|
5698
|
+
|
|
5699
|
+
### Managed server shutdown
|
|
5700
|
+
|
|
5701
|
+
`createServer()` and `serveNode()` now return the same structural managed
|
|
5702
|
+
lifecycle. Replace every direct runtime stop and parallel Socket.IO close:
|
|
5703
|
+
|
|
5704
|
+
```ts
|
|
5705
|
+
// before — split ownership
|
|
5706
|
+
const socket = await createSocketIOServer(config)
|
|
5707
|
+
const server = createServer({
|
|
5708
|
+
services,
|
|
5709
|
+
websocket: socket.websocket,
|
|
5710
|
+
rawRoutes: [socket.route],
|
|
5711
|
+
})
|
|
5712
|
+
server.stop()
|
|
5713
|
+
await socket.io.close()
|
|
5714
|
+
|
|
5715
|
+
// after — one owner and one total deadline
|
|
5716
|
+
const socket = await createSocketIOServer(config)
|
|
5717
|
+
const server = createServer({ services, socket })
|
|
5718
|
+
const result = await server.shutdown({ gracePeriodMs: 30_000 })
|
|
5719
|
+
```
|
|
5720
|
+
|
|
5721
|
+
On Node, keep the same `socket` field and replace `handle.close()` with
|
|
5722
|
+
`handle.shutdown()`. Runtime-specific diagnostics move under `handle.runtime`;
|
|
5723
|
+
do not use it as a second shutdown path. Standalone CLI/tools that create a
|
|
5724
|
+
Socket.IO handle without an HTTP server call `await socket.close()`.
|
|
5725
|
+
|
|
5726
|
+
If Bun Socket.IO shares the port with a raw lane, keep the explicit composition
|
|
5727
|
+
but let the server mount the Socket.IO route:
|
|
5728
|
+
|
|
5729
|
+
```ts
|
|
5730
|
+
createServer({
|
|
5731
|
+
services,
|
|
5732
|
+
socket,
|
|
5733
|
+
websocket: composeWebSocketHandlers([
|
|
5734
|
+
webSocketLane({ match: isRaw, handlers: rawHandlers }),
|
|
5735
|
+
socketIoLane(socket.websocket),
|
|
5736
|
+
]),
|
|
5737
|
+
rawRoutes: [rawUpgradeRoute],
|
|
5738
|
+
})
|
|
5739
|
+
```
|
|
5740
|
+
|
|
5741
|
+
Move native Bun `routes` entries to `rawRoutes`. Native routes run before the
|
|
5742
|
+
Fetch handler and therefore cannot participate in admission or drain. Wire
|
|
5743
|
+
`SIGTERM`/`SIGINT` in the application; the first signal starts `shutdown()`, and
|
|
5744
|
+
a later signal may abort the same controller. Close MCP, databases and queues
|
|
5745
|
+
after the server result—those resources remain application-owned.
|
|
5746
|
+
|
|
5747
|
+
Move a handshake policy from the Node-only callback shape inside
|
|
5748
|
+
`serverOptions` to the runtime-neutral top-level policy. It receives a Web
|
|
5749
|
+
`Request`, may be async, and returns whether to admit the handshake:
|
|
5750
|
+
|
|
5751
|
+
```ts
|
|
5752
|
+
// before
|
|
5753
|
+
serverOptions: { allowRequest: (request, done) => done(null, allowed(request)) }
|
|
5754
|
+
|
|
5755
|
+
// after
|
|
5756
|
+
allowRequest: (request) => allowed(request)
|
|
5757
|
+
```
|
|
5547
5758
|
|
|
5548
5759
|
## Your handlers may be returning more than the contract declares
|
|
5549
5760
|
|
|
@@ -5628,15 +5839,16 @@ The browser-and-server entrypoint. Re-exports everything from
|
|
|
5628
5839
|
| `createUrlBuilders` | function | build one exact URL builder per contract in a registry |
|
|
5629
5840
|
| `UrlBuilderConfig` | _type_ | explicit `{ baseUrl }` source for a URL builder |
|
|
5630
5841
|
| `ClientConfig` | _type_ | config for `createClient`'s bare-fetch mode (2nd arg, no `HttpClient`) |
|
|
5842
|
+
| `ClientFetch` | _type_ | injectable Fetch-compatible transport used by framework testing adapters |
|
|
5631
5843
|
| `ClientRequestOptions` | _type_ | per-call `{ signal?: AbortSignal }` passed through an endpoint callable's `.withOptions(...)`; caller abort is distinct from timeout — [guide](../guide/client.md#per-call-cancellation) |
|
|
5632
5844
|
| `ContractClientConfig` | _type_ | per-tenant / resource-scoped client config — dynamic `pathPrefix` + `stripPrefixKeys` ([guide](../guide/client.md#contractclientconfig--per-tenant--resource-scoped-clients)) |
|
|
5633
5845
|
| `contractEndpointMatchers` | function | compile exact pathname matchers for selected HTTP contract operations and expected-401 policy |
|
|
5634
5846
|
| `PathPrefixArgs` | _type_ | required string-valued keys exposed to a typed dynamic `pathPrefix` callback |
|
|
5635
|
-
| `createHttpClient` | function | the Ky-based HTTP transport — [guide](../guide/client.md#createhttpclient) |
|
|
5847
|
+
| `createHttpClient` | function | the Ky-based HTTP transport; on Next.js SSR its first attempt stays request-memoizable while every retry is a distinct transport attempt — [guide](../guide/client.md#createhttpclient) |
|
|
5636
5848
|
| `ApiError` | class | a non-2xx response, with `code` / `status` / `details` / `hint` and optional readonly `traceId` from `x-request-id` |
|
|
5637
5849
|
| `HttpClient` | _type_ | the transport interface `createClient` builds on |
|
|
5638
5850
|
| `ConfiguredHttpClient` | _type_ | a framework-created `HttpClient` carrying its readonly `baseUrl` for URL builders |
|
|
5639
|
-
| `HttpClientConfig` | _type_ | config for `createHttpClient` |
|
|
5851
|
+
| `HttpClientConfig` | _type_ | config for `createHttpClient`; retry `limit` counts retries after the initial attempt (default 2 = at most 3 GET attempts), with `statusCodes: []` by default — [details](../guide/client.md#createhttpclient) |
|
|
5640
5852
|
| `UnauthorizedMatcher` | _type_ | exact `(pathname) => boolean` policy accepted by `suppressUnauthorizedFor` |
|
|
5641
5853
|
| `RequestOptions` | _type_ | per-call options — params, timeout, response type |
|
|
5642
5854
|
| `HeaderProvider` | _type_ | static or per-request headers |
|
|
@@ -5789,6 +6001,11 @@ Also re-exports the error helpers from `stitchkit/contract`.
|
|
|
5789
6001
|
| `createHandler` | function | the router as a bare `(req) => Response` — [guide](../guide/server.md#createserver) |
|
|
5790
6002
|
| `implement` | function | bind a contract to typed handlers — [guide](../guide/server.md#implement) |
|
|
5791
6003
|
| `createImplement` | function | fix the handler context type once |
|
|
6004
|
+
| `implementRegistry` | function | bind one exact contract registry to one exact backend handler registry |
|
|
6005
|
+
| `createImplementRegistry` | function | context-typed factory for `implementRegistry` |
|
|
6006
|
+
| `ImplementationRegistry` | _type_ | flat literal registry of concrete contracts accepted by `implementRegistry` |
|
|
6007
|
+
| `RegistryHandlers` | _type_ | exact backend handler registry inferred from a contract registry |
|
|
6008
|
+
| `ExactRegistryHandlers` | _type_ | fail-first handler shape that rejects extra registry and endpoint keys |
|
|
5792
6009
|
| `staticRoute` | function | a raw route that serves a directory |
|
|
5793
6010
|
| `serveFile` | function | serve a file with `Range` / `304` / `HEAD` — [guide](../guide/server.md#serving-files--range-requests) |
|
|
5794
6011
|
| `parseByteRange` | function | parse a single `Range` header → range / `unsatisfiable` / `null` |
|
|
@@ -5806,6 +6023,12 @@ Also re-exports the error helpers from `stitchkit/contract`.
|
|
|
5806
6023
|
| `parseBody` | function | parse + Zod-validate a JSON body → `data` or `null` (no throw) |
|
|
5807
6024
|
| `HandlerConfig` | _type_ | config for `createHandler`, including optional `maxJsonBodyBytes`; bound to `BunServer` on this entrypoint |
|
|
5808
6025
|
| `BunServerConfig` | _type_ | config for `createServer` (Bun) |
|
|
6026
|
+
| `BunServerHandle` | _type_ | managed Bun handle (`url`, `port`, `runtime`, `status`, `shutdown`) |
|
|
6027
|
+
| `ManagedServerHandle` | _type_ | shared lifecycle shape generic over the runtime escape hatch |
|
|
6028
|
+
| `ShutdownOptionsSchema` / `ShutdownOptions` | schema / _type_ | one grace budget, retry hint and optional external abort signal |
|
|
6029
|
+
| `ShutdownStatusSchema` / `ShutdownStatus` | schema / _type_ | live state and request/WebSocket counters |
|
|
6030
|
+
| `ShutdownResultSchema` / `ShutdownResult` | schema / _type_ | clean/forced result with final counters and at-force snapshots |
|
|
6031
|
+
| `ShutdownStateSchema` / `ShutdownState` | schema / _type_ | managed lifecycle state machine |
|
|
5809
6032
|
| `ServiceDef` | _type_ | the result of `implement` |
|
|
5810
6033
|
| `MethodDef` | _type_ | one resolved endpoint inside a service |
|
|
5811
6034
|
| `OperationIdentity` | _type_ | path-free service/action/scope/method identity shared by contract and native tool operations |
|
|
@@ -5871,8 +6094,10 @@ Also re-exports the error helpers from `stitchkit/contract`.
|
|
|
5871
6094
|
| `RealtimeServer` | _type_ | validated broadcast and connection API inferred from a realtime contract |
|
|
5872
6095
|
| `RealtimeServerConnection` | _type_ | one validated connection with raw socket access for auth and rooms |
|
|
5873
6096
|
| `RealtimeServerHandle` | _type_ | minimal Socket.IO server handle accepted by `bindRealtimeServer` |
|
|
6097
|
+
| `SocketIORequestPolicy` | _type_ | runtime-neutral async-capable Web `Request` handshake admission policy |
|
|
5874
6098
|
| `SocketIOServerConfig` | _type_ | config for `createSocketIOServer` |
|
|
5875
|
-
| `SocketIOServerHandle` | _type_ |
|
|
6099
|
+
| `SocketIOServerHandle` | _type_ | typed Socket.IO server plus Bun mount fields and idempotent lifecycle |
|
|
6100
|
+
| `SocketIOServerLifecycle` | _type_ | non-generic Bun mount/shutdown portion accepted by `createServer` |
|
|
5876
6101
|
| `composeWebSocketHandlers` | function | compose one Bun `websocket` from N lanes — a raw binary lane beside Socket.IO ([guide](../guide/realtime.md#raw-binary-lane-bun)) |
|
|
5877
6102
|
| `webSocketLane` | function | a typed, cast-free lane for `composeWebSocketHandlers` |
|
|
5878
6103
|
| `socketIoLane` | function | the Socket.IO catch-all lane for `composeWebSocketHandlers` |
|
|
@@ -5938,7 +6163,11 @@ audit event. See the [Observability guide](../guide/observability.md).
|
|
|
5938
6163
|
| `createObservability` | function | configure framework-owned request completion and canonical tool event sinks — [guide](../guide/observability.md#createobservability) |
|
|
5939
6164
|
| `RequestEvent` | _type_ | the normalised audit event handed to the sink |
|
|
5940
6165
|
| `ObservabilityConfig` | _type_ | independent request and tool sink configuration |
|
|
5941
|
-
| `Observability` | _type_ | `{ request?, toolCall, flush(), close() }` with bounded sink lifecycle |
|
|
6166
|
+
| `Observability` | _type_ | `{ request?, toolCall, getStatus(), flush(), close() }` with bounded sink lifecycle |
|
|
6167
|
+
| `ObservabilitySinkStatus` | _type_ | immutable counters for one bounded request/tool sink |
|
|
6168
|
+
| `ObservabilityStatus` | _type_ | per-surface plus aggregate operational snapshot |
|
|
6169
|
+
| `ObservabilityDrainReport` | _type_ | final closed/drained snapshot plus duration |
|
|
6170
|
+
| `ObservabilitySinkStatusSchema` / `ObservabilityStatusSchema` / `ObservabilityDrainReportSchema` | schema | runtime schemas for status/report integration boundaries |
|
|
5942
6171
|
| `RequestEventSinkConfig` | _type_ | `write`, filter/sanitisation, `maxPending`, `onSinkError` and `onDrop` |
|
|
5943
6172
|
| `RequestObservabilityConfig` | _type_ | request sink plus opt-in payload capture |
|
|
5944
6173
|
| `SinkDropReason` | _type_ | `'capacity' \| 'closed'` |
|
|
@@ -6005,7 +6234,7 @@ payload.
|
|
|
6005
6234
|
| `createMcpHandler` | function | a stateless dual-era Streamable-HTTP MCP handler — [guide](../guide/mcp-and-agents.md#mcp--createmcphandler) |
|
|
6006
6235
|
| `createMcpHttpRoute` | function | framework-owned `RawRoute` adapter for an MCP HTTP handler |
|
|
6007
6236
|
| `createStdioMcpServer` | function | a complete stdio MCP server — [guide](../guide/mcp-and-agents.md#mcp-over-stdio--createstdiomcpserver) |
|
|
6008
|
-
| `buildMcpServer` | function | build an `McpServer` from contract/runtime surfaces
|
|
6237
|
+
| `buildMcpServer` | function | build an `McpServer` from contract/runtime surfaces; no-auth configs omit the second argument |
|
|
6009
6238
|
| `mountMcp` | function | add contract tools to an existing `McpServer` — [guide](../guide/mcp-and-agents.md#mountmcp) |
|
|
6010
6239
|
| `implementRemote` | function | bind a contract to a remote HTTP API — [guide](../guide/mcp-and-agents.md#proxying-a-remote-api--implementremote) |
|
|
6011
6240
|
| `mountAgent` | function | a Vercel AI SDK `ToolSet` from a service — [guide](../guide/mcp-and-agents.md#ai-agents--mountagent) |
|
|
@@ -6167,6 +6396,22 @@ Advanced building blocks — the shared machinery the mounts are built on.
|
|
|
6167
6396
|
|
|
6168
6397
|
---
|
|
6169
6398
|
|
|
6399
|
+
## `stitchkit/testing`
|
|
6400
|
+
|
|
6401
|
+
Fetch-only integration helpers that preserve the real generated-client and
|
|
6402
|
+
handler pipeline without opening a TCP port.
|
|
6403
|
+
|
|
6404
|
+
| Export | Kind | Summary |
|
|
6405
|
+
|--------|------|---------|
|
|
6406
|
+
| `createHandlerTestClient` | function | one contract client backed by an in-process `FetchHandler` |
|
|
6407
|
+
| `createHandlerTestClients` | function | exact contract-registry batch form |
|
|
6408
|
+
| `HandlerTestClientDefaults` | _type_ | ordinary bare-client defaults with handler-owned `baseUrl` and `fetch` removed |
|
|
6409
|
+
| `HandlerTestClientConfig` | _type_ | handler, contract, path prefix, scoped config and client request defaults |
|
|
6410
|
+
| `HandlerTestClientsConfig` | _type_ | batch helper configuration |
|
|
6411
|
+
| `HandlerTestTransportConfig` | _type_ | shared in-process handler, origin, prefix, client defaults and optional server handle |
|
|
6412
|
+
|
|
6413
|
+
---
|
|
6414
|
+
|
|
6170
6415
|
## `stitchkit/node`
|
|
6171
6416
|
|
|
6172
6417
|
Server-only, for Node ≥ 22 (Bun uses `stitchkit/server`). The runtime-agnostic
|
|
@@ -6181,9 +6426,11 @@ runtime-agnostic pieces of `stitchkit/server` and the error helpers.
|
|
|
6181
6426
|
| `createSocketIOServer` | function | the typed Node Socket.IO server (`io` + `attach`; no Bun engine declarations) |
|
|
6182
6427
|
| `implement` / `createImplement` | function | bind a contract to typed handlers (same as `/server`) |
|
|
6183
6428
|
| `NodeServerConfig` | _type_ | config for `serveNode` |
|
|
6184
|
-
| `NodeServerHandle` | _type_ |
|
|
6429
|
+
| `NodeServerHandle` | _type_ | managed Node handle (`url`, `port`, `runtime`, `status`, `shutdown`) |
|
|
6430
|
+
| `NodeRuntimeServer` | _type_ | concrete `srvx/node` runtime escape hatch |
|
|
6431
|
+
| `NodeSocketLifecycle` | _type_ | Bun-free Socket.IO lifecycle accepted by `serveNode` |
|
|
6185
6432
|
| `HandlerConfig` / `ServiceDef` / `RawRoute` / `RawRouteContext` | _type_ | runtime-neutral handler types; raw routes default their host server to `unknown` |
|
|
6186
|
-
| `SocketIOServerConfig` / `SocketIOServerHandle` | _type_ |
|
|
6433
|
+
| `SocketIORequestPolicy` / `SocketIOServerConfig` / `SocketIOServerHandle` | _type_ | runtime-neutral handshake policy, config and Bun-free Node handle with `io`, `attach` and lifecycle |
|
|
6187
6434
|
| `AppError` + `appError` / `badRequest` / `unauthorized` / `forbidden` / `notFound` / `conflict` / `rateLimited` | — | error helpers (same as `/contract`) |
|
|
6188
6435
|
|
|
6189
6436
|
---
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "stitchkit",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.49.0",
|
|
4
4
|
"description": "Contract-first backend framework — one defineContract() into an HTTP API, MCP tools, AI-agent tools and a typed client. Bun and Node.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"bun",
|
|
@@ -72,6 +72,10 @@
|
|
|
72
72
|
"types": "./dist/observability/index.d.ts",
|
|
73
73
|
"import": "./dist/observability/index.js"
|
|
74
74
|
},
|
|
75
|
+
"./testing": {
|
|
76
|
+
"types": "./dist/testing.d.ts",
|
|
77
|
+
"import": "./dist/testing.js"
|
|
78
|
+
},
|
|
75
79
|
"./node": {
|
|
76
80
|
"types": "./dist/node.d.ts",
|
|
77
81
|
"import": "./dist/node.js"
|
|
@@ -83,7 +87,7 @@
|
|
|
83
87
|
"scripts": {
|
|
84
88
|
"check": "bun x tsc --noEmit",
|
|
85
89
|
"build:browser": "bun build src/index.ts src/react.ts src/contract/index.ts --outdir dist --target node --packages external --splitting --root src",
|
|
86
|
-
"build:server": "bun build src/server/index.ts src/node.ts src/tools.ts src/cli.ts src/observability/index.ts --outdir dist --target node --packages external --splitting --root src",
|
|
90
|
+
"build:server": "bun build src/server/index.ts src/node.ts src/tools.ts src/cli.ts src/testing.ts src/observability/index.ts --outdir dist --target node --packages external --splitting --root src",
|
|
87
91
|
"build:js": "bun run build:browser && bun run build:server",
|
|
88
92
|
"build:types": "bun x tsc -p tsconfig.build.json --emitDeclarationOnly",
|
|
89
93
|
"build": "rm -rf dist && bun run build:js && bun run build:types && bun scripts/check-browser-clean.mjs && bun scripts/check-env-live.mjs && bun scripts/check-public-types.mjs",
|
|
@@ -91,6 +95,7 @@
|
|
|
91
95
|
"prepack": "cp ../../README.md ./README.md && bun ../../scripts/gen-llms.ts && bun run build",
|
|
92
96
|
"test": "bun test",
|
|
93
97
|
"smoke:node": "node scripts/node-smoke.mjs",
|
|
98
|
+
"smoke:next-ssr": "node scripts/next-ssr-retry-smoke.mjs",
|
|
94
99
|
"consumer-lane": "bun scripts/consumer-lane/run.mjs",
|
|
95
100
|
"bench:mcp-preparation": "bun scripts/benchmark-mcp-preparation.ts"
|
|
96
101
|
},
|
|
@@ -161,7 +166,7 @@
|
|
|
161
166
|
"@types/json-schema": "^7.0.15",
|
|
162
167
|
"@types/react": "^19.2.18",
|
|
163
168
|
"@typescript/typescript6": "^6.0.2",
|
|
164
|
-
"ai": "^7.0.
|
|
169
|
+
"ai": "^7.0.65",
|
|
165
170
|
"react": "^19.2.8",
|
|
166
171
|
"react-query-kit": "^3.3.4",
|
|
167
172
|
"socket.io": "^4.8.3",
|