stitchkit 0.47.0 → 0.48.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. package/README.md +9 -0
  2. package/dist/browser/client.d.ts +3 -0
  3. package/dist/browser/client.d.ts.map +1 -1
  4. package/dist/browser/http.d.ts +7 -4
  5. package/dist/browser/http.d.ts.map +1 -1
  6. package/dist/cli.js +3 -2
  7. package/dist/contract/define.d.ts +11 -3
  8. package/dist/contract/define.d.ts.map +1 -1
  9. package/dist/{index-nrytvb30.js → index-03j2t778.js} +5 -8
  10. package/dist/{index-kp8xamqp.js → index-0hj37z43.js} +4 -2
  11. package/dist/index-48ffdxgk.js +6 -0
  12. package/dist/index-h05ygjqx.js +149 -0
  13. package/dist/{index-ee621cmy.js → index-jewp9r0a.js} +5 -139
  14. package/dist/index-p9d4cxt5.js +436 -0
  15. package/dist/{index-44xysy8r.js → index-ts21eyz4.js} +141 -19
  16. package/dist/{index-8ekq6res.js → index-v58mwa19.js} +4 -2
  17. package/dist/index.d.ts +1 -1
  18. package/dist/index.d.ts.map +1 -1
  19. package/dist/index.js +40 -27
  20. package/dist/node.d.ts +1 -1
  21. package/dist/node.d.ts.map +1 -1
  22. package/dist/node.js +8 -3
  23. package/dist/observability/audit.d.ts +4 -1
  24. package/dist/observability/audit.d.ts.map +1 -1
  25. package/dist/observability/index.d.ts +1 -0
  26. package/dist/observability/index.d.ts.map +1 -1
  27. package/dist/observability/index.js +150 -16
  28. package/dist/observability/status.d.ts +105 -0
  29. package/dist/observability/status.d.ts.map +1 -0
  30. package/dist/server/implement.d.ts +16 -6
  31. package/dist/server/implement.d.ts.map +1 -1
  32. package/dist/server/index.d.ts +1 -1
  33. package/dist/server/index.d.ts.map +1 -1
  34. package/dist/server/index.js +14 -6
  35. package/dist/server/router.d.ts +4 -1
  36. package/dist/server/router.d.ts.map +1 -1
  37. package/dist/testing.d.ts +34 -0
  38. package/dist/testing.d.ts.map +1 -0
  39. package/dist/testing.js +36 -0
  40. package/dist/tools/mcp.d.ts +1 -0
  41. package/dist/tools/mcp.d.ts.map +1 -1
  42. package/dist/tools.js +21 -430
  43. package/llms-full.txt +177 -22
  44. package/package.json +7 -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 eight entrypoints. Each is import-safe for one environment —
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,6 +686,30 @@ 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
715
  `createServer(config)` builds the router and starts `Bun.serve()`. It returns
@@ -991,6 +1016,21 @@ not retain the text. `maxJsonBodyBytes` may also be set once on `createServer` /
991
1016
  `createHandler`; a route value wins. Both limits are opt-in and abort an
992
1017
  oversized stream before it is fully buffered. → ADR 0051
993
1018
 
1019
+ ### Choosing an HTTP boundary
1020
+
1021
+ | Need | Contract declaration | What remains framework-owned |
1022
+ |------|----------------------|------------------------------|
1023
+ | Typed JSON request/response | ordinary `input` / `output` | routing, auth, schemas, hooks, client, OpenAPI |
1024
+ | HMAC-signed JSON | `rawBody: true` + `input` / `output` | the same pipeline plus the exact decoded request text |
1025
+ | File upload | typed `multipart` descriptor | file cardinality/limits, text input validation and client form encoding |
1026
+ | File, stream or redirect response behind contract auth | `rawResponse: true` | request parsing, route identity, auth and typed URL/client surface |
1027
+ | Transport that cannot be expressed as the contract pipeline | `RawRoute` | only raw routing, CORS, request hook and error normalisation |
1028
+
1029
+ `rawBody` is for JSON signatures: verify `ctx.rawBody` against the signature
1030
+ header, then use the already validated `ctx.input`. A provider-specific binary
1031
+ signature protocol, OAuth callback or WebSocket upgrade can be a real raw route,
1032
+ but it must own its validation and authorization explicitly.
1033
+
994
1034
  ## Typed JSON response metadata
995
1035
 
996
1036
  A JSON endpoint that must attach dynamic HTTP headers while preserving typed
@@ -1158,6 +1198,13 @@ wildcard — and the two combine: `/app/:slug/*filePath` matches `/app/x/a/b` wi
1158
1198
  deep-link fallback). List more specific routes before the wildcard — the first
1159
1199
  match wins. `staticRoute()` builds a raw route that serves a directory.
1160
1200
 
1201
+ At startup Stitchkit rejects exact duplicates, equivalent parameter shapes
1202
+ (`/users/:id` vs `/users/:userId`) and any later raw route completely hidden by
1203
+ an earlier raw route. Partial overlap stays legal, including the recommended
1204
+ specific-before-wildcard order. `GET` and `HEAD` remain independent because that
1205
+ is how the actual raw matcher dispatches them. Raw-vs-contract overlap remains a
1206
+ startup diagnostic naming the bypassed contract identity and scope.
1207
+
1161
1208
  ### Raw-route helpers
1162
1209
 
1163
1210
  A raw route gives up the contract pipeline, so three things get re-implemented in
@@ -1483,7 +1530,7 @@ without repeating transport configuration.
1483
1530
  | `baseUrl` | — | URL prefix for every request |
1484
1531
  | `timeout` | `30000` | request timeout, ms |
1485
1532
  | `credentials` | `'include'` | fetch credentials mode |
1486
- | `retry` | 2× GET, network errors only | transport retry policy |
1533
+ | `retry` | 2 retries after the initial GET; network errors only | transport retry policy |
1487
1534
  | `headers` | — | extra headers — an object, or a function re-run per request |
1488
1535
  | `suppressUnauthorizedFor` | `[]` | exact contract-derived operation matchers whose expected 401 does not emit `unauthorized` |
1489
1536
  | `parseError` | built-in | map an error body to `{ code, message, details, hint }` |
@@ -1509,7 +1556,10 @@ end-to-end. A `traceparent` you set yourself (via `headers`) always wins.
1509
1556
  Retry is deliberately conservative: only a connection that never landed (a
1510
1557
  network error), only on idempotent `GET`. A server that *responded* with a 5xx
1511
1558
  is the data layer's call (TanStack Query), not the transport's — retrying in
1512
- both places multiplies attempts.
1559
+ both places multiplies attempts. `retry.limit` counts retries after the initial
1560
+ attempt, so the default `2` permits at most three total attempts. The default
1561
+ `statusCodes: []` does not replay HTTP responses; explicit `methods` or
1562
+ `statusCodes` expand that policy when a project has a proven idempotent case.
1513
1563
 
1514
1564
  ## `createClient`
1515
1565
 
@@ -1560,14 +1610,15 @@ contract:
1560
1610
 
1561
1611
  ### Per-call cancellation
1562
1612
 
1563
- Every endpoint accepts optional `ClientRequestOptions` containing an
1564
- `AbortSignal`. Endpoints with arguments use `(args, options?)`; endpoints with
1565
- no arguments use `(options?)`:
1613
+ Every endpoint callable exposes a `withOptions` method accepting required
1614
+ `ClientRequestOptions`. The ordinary callable contains only contract arguments,
1615
+ so it can be passed directly to callback APIs such as `react-query-kit` without
1616
+ mistaking their callback context for Stitchkit transport options:
1566
1617
 
1567
1618
  ```ts
1568
1619
  const controller = new AbortController()
1569
1620
 
1570
- const pending = api.upload(
1621
+ const pending = api.upload.withOptions(
1571
1622
  { file: selectedFile, title: 'Draft' },
1572
1623
  { signal: controller.signal },
1573
1624
  )
@@ -1590,6 +1641,12 @@ Abort and timeout do not emit `network_error` and are not retried. The same
1590
1641
  options work for query, JSON, multipart and raw-response calls. Stitchkit does
1591
1642
  not expose upload progress: Fetch has no portable upload-progress primitive.
1592
1643
 
1644
+ For an endpoint without contract arguments, pass only the options object:
1645
+
1646
+ ```ts
1647
+ await api.health.withOptions({ signal: controller.signal })
1648
+ ```
1649
+
1593
1650
  ### Many contracts at once
1594
1651
 
1595
1652
  ```ts
@@ -1823,6 +1880,10 @@ export const useUsers = createQuery({ queryKey: ['users'], fetcher: () => a
1823
1880
  export const useCreateUser = createMutation({ mutationFn: api.create })
1824
1881
  ```
1825
1882
 
1883
+ Generated methods intentionally keep their ordinary call signature limited to
1884
+ contract variables. Use `api.create.withOptions(variables, { signal })` only for
1885
+ an imperative call that needs per-request cancellation.
1886
+
1826
1887
  ### Cursor pagination
1827
1888
 
1828
1889
  For a cursor-paginated list, `createCursorQuery` is the canonical helper:
@@ -2240,15 +2301,18 @@ validateMcpSchemas({
2240
2301
  extend,
2241
2302
  flattenUnionInput: true,
2242
2303
  requireTypedProperties: true,
2243
- allowUntyped: ['docs_create.payload'], // deliberately free-form
2244
2304
  requirePortableFormats: true,
2245
2305
  allowFormats: [],
2246
2306
  })
2247
2307
  ```
2248
2308
 
2249
- Off by default, because a contract may legitimately declare `z.unknown()`.
2250
- `allowUntyped` takes dotted `tool.property` paths an entry there is a decision,
2251
- anything else is a finding. On `createMcpHandler`, put the policy under
2309
+ For an arbitrary **JSON value**, declare `z.json()`: its recursive JSON Schema
2310
+ describes strings, numbers, booleans, null, arrays and objects, so strict schema
2311
+ validation needs no exception. `z.unknown()` means an unknown runtime value
2312
+ including values JSON cannot represent — and therefore gives a model no usable
2313
+ shape. The guard is off by default for existing contracts. `allowUntyped` takes
2314
+ dotted `tool.property` paths and is reserved for a presentation value that is
2315
+ genuinely impossible to represent as JSON Schema. On `createMcpHandler`, put the policy under
2252
2316
  `schemaValidation`; the handler supplies its real `extend` and
2253
2317
  `flattenUnionInput`, so the check cannot vet a different document from the one
2254
2318
  advertised.
@@ -3970,8 +4034,8 @@ export const observability = createObservability({
3970
4034
  },
3971
4035
  })
3972
4036
 
3973
- await observability.flush()
3974
- await observability.close()
4037
+ const status = observability.getStatus()
4038
+ const drained = await observability.close()
3975
4039
  ```
3976
4040
 
3977
4041
  `maxPending` defaults to `1000` per sink and must be a positive safe integer.
@@ -3982,9 +4046,15 @@ cancelled. Sink, filter and diagnostic-callback failures remain isolated from
3982
4046
  the observed request or tool call and are reported through `onSinkError` when
3983
4047
  configured; `onSinkError`/`onDrop` cannot create unhandled rejections.
3984
4048
 
4049
+ `getStatus()` returns immutable per-surface and aggregate counters: capacity,
4050
+ received, accepted, filtered, completed, dropped, failed, preparation failures,
4051
+ preparing/pending work and the closed flag. Read it from readiness or metrics
4052
+ without parsing callback logs.
4053
+
3985
4054
  `flush()` snapshots the current generation and waits only for events admitted
3986
4055
  up to that call. `close()` atomically stops admission, drains every accepted
3987
- generation and is idempotent. Graceful shutdown order is therefore:
4056
+ generation and returns the final counters plus `durationMs`; repeated calls
4057
+ return the same report. Graceful shutdown order is therefore:
3988
4058
 
3989
4059
  1. stop HTTP/MCP admission;
3990
4060
  2. wait for active requests and tool calls;
@@ -4420,7 +4490,33 @@ never block or break the request. Swallow the sink's own errors.
4420
4490
  stitchkit's own test suite runs on `bun:test`. The contract makes most of an
4421
4491
  API testable without a live socket.
4422
4492
 
4423
- ### Test handlers in process
4493
+ ### Test generated clients in process
4494
+
4495
+ `createHandlerTestClient` runs the real generated client against the real Fetch
4496
+ handler without opening a TCP port. It keeps URL construction, headers/cookies,
4497
+ multipart encoding, cancellation, output validation, `ApiError` and
4498
+ `x-request-id` correlation in the test path:
4499
+
4500
+ ```ts
4501
+ import { createHandlerTestClient } from 'stitchkit/testing'
4502
+
4503
+ const api = createHandlerTestClient({
4504
+ contract: notes,
4505
+ handler,
4506
+ pathPrefix: 'api',
4507
+ client: { headers: { cookie: 'session=test' } },
4508
+ })
4509
+
4510
+ expect(await api.create({ text: 'hi' })).toEqual({ id: '1', text: 'hi' })
4511
+ ```
4512
+
4513
+ `contractConfig` accepts the same scoped `pathPrefix` / `stripPrefixKeys` as
4514
+ `createClient`. `createHandlerTestClients` is the batch form for a literal
4515
+ contract registry. Both helpers are Fetch-only: they construct ordinary
4516
+ absolute `Request` objects and call `createHandler` directly, so Bun and Node
4517
+ exercise the same framework pipeline.
4518
+
4519
+ ### Test handlers with raw Requests
4424
4520
 
4425
4521
  `createHandler` is the router as a plain `(req) => Promise<Response>` function —
4426
4522
  no `Bun.serve`, no port. Drive it with a `Request`:
@@ -4450,8 +4546,8 @@ test('create returns the note', async () => {
4450
4546
  })
4451
4547
  ```
4452
4548
 
4453
- This exercises the full pipeline routing, schema parsing, hooks, the error
4454
- envelope with no network.
4549
+ This lower-level form exercises the same pipeline while letting a test inspect
4550
+ the raw `Response` itself.
4455
4551
 
4456
4552
  ### Test handlers directly
4457
4553
 
@@ -4817,7 +4913,40 @@ current one *up to* your target, and apply each snippet.
4817
4913
  runtime): bootstrap the server, one HTTP request, and any feature you rely on
4818
4914
  (Socket.IO connect, an MCP tool call, a multipart upload, …).
4819
4915
 
4820
- ## Upcoming migration: `[Unreleased]`
4916
+ ## Released migration: 0.48.0
4917
+
4918
+ ### Typed-client request options move to `.withOptions`
4919
+
4920
+ Generated endpoint methods reserve their ordinary call signature for contract
4921
+ variables. This keeps them directly assignable to callback APIs whose runtime
4922
+ supplies its own second context argument, including `react-query-kit` and
4923
+ TanStack Query. Move imperative cancellation to the callable's explicit method:
4924
+
4925
+ ```ts
4926
+ // before — endpoint with arguments
4927
+ await api.create({ name: 'Max' }, { signal })
4928
+
4929
+ // after
4930
+ await api.create.withOptions({ name: 'Max' }, { signal })
4931
+
4932
+ // before — endpoint without arguments
4933
+ await api.health({ signal })
4934
+
4935
+ // after
4936
+ await api.health.withOptions({ signal })
4937
+ ```
4938
+
4939
+ Direct query and mutation composition remains unchanged:
4940
+
4941
+ ```ts
4942
+ createMutation({ mutationFn: api.create })
4943
+ createQuery({ queryKey: ['search'], fetcher: api.search })
4944
+ ```
4945
+
4946
+ There is no positional-options alias. Ordinary generated methods ignore extra
4947
+ runtime callback arguments; only `.withOptions` reads `ClientRequestOptions`.
4948
+
4949
+ ## Released migration: 0.47.0
4821
4950
 
4822
4951
  ### HTTP auth moves to the pre-body `authorize` phase
4823
4952
 
@@ -5584,7 +5713,8 @@ The browser-and-server entrypoint. Re-exports everything from
5584
5713
  | `createUrlBuilders` | function | build one exact URL builder per contract in a registry |
5585
5714
  | `UrlBuilderConfig` | _type_ | explicit `{ baseUrl }` source for a URL builder |
5586
5715
  | `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) |
5716
+ | `ClientFetch` | _type_ | injectable Fetch-compatible transport used by framework testing adapters |
5717
+ | `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) |
5588
5718
  | `ContractClientConfig` | _type_ | per-tenant / resource-scoped client config — dynamic `pathPrefix` + `stripPrefixKeys` ([guide](../guide/client.md#contractclientconfig--per-tenant--resource-scoped-clients)) |
5589
5719
  | `contractEndpointMatchers` | function | compile exact pathname matchers for selected HTTP contract operations and expected-401 policy |
5590
5720
  | `PathPrefixArgs` | _type_ | required string-valued keys exposed to a typed dynamic `pathPrefix` callback |
@@ -5592,7 +5722,7 @@ The browser-and-server entrypoint. Re-exports everything from
5592
5722
  | `ApiError` | class | a non-2xx response, with `code` / `status` / `details` / `hint` and optional readonly `traceId` from `x-request-id` |
5593
5723
  | `HttpClient` | _type_ | the transport interface `createClient` builds on |
5594
5724
  | `ConfiguredHttpClient` | _type_ | a framework-created `HttpClient` carrying its readonly `baseUrl` for URL builders |
5595
- | `HttpClientConfig` | _type_ | config for `createHttpClient` |
5725
+ | `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) |
5596
5726
  | `UnauthorizedMatcher` | _type_ | exact `(pathname) => boolean` policy accepted by `suppressUnauthorizedFor` |
5597
5727
  | `RequestOptions` | _type_ | per-call options — params, timeout, response type |
5598
5728
  | `HeaderProvider` | _type_ | static or per-request headers |
@@ -5745,6 +5875,11 @@ Also re-exports the error helpers from `stitchkit/contract`.
5745
5875
  | `createHandler` | function | the router as a bare `(req) => Response` — [guide](../guide/server.md#createserver) |
5746
5876
  | `implement` | function | bind a contract to typed handlers — [guide](../guide/server.md#implement) |
5747
5877
  | `createImplement` | function | fix the handler context type once |
5878
+ | `implementRegistry` | function | bind one exact contract registry to one exact backend handler registry |
5879
+ | `createImplementRegistry` | function | context-typed factory for `implementRegistry` |
5880
+ | `ImplementationRegistry` | _type_ | flat literal registry of concrete contracts accepted by `implementRegistry` |
5881
+ | `RegistryHandlers` | _type_ | exact backend handler registry inferred from a contract registry |
5882
+ | `ExactRegistryHandlers` | _type_ | fail-first handler shape that rejects extra registry and endpoint keys |
5748
5883
  | `staticRoute` | function | a raw route that serves a directory |
5749
5884
  | `serveFile` | function | serve a file with `Range` / `304` / `HEAD` — [guide](../guide/server.md#serving-files--range-requests) |
5750
5885
  | `parseByteRange` | function | parse a single `Range` header → range / `unsatisfiable` / `null` |
@@ -5894,7 +6029,11 @@ audit event. See the [Observability guide](../guide/observability.md).
5894
6029
  | `createObservability` | function | configure framework-owned request completion and canonical tool event sinks — [guide](../guide/observability.md#createobservability) |
5895
6030
  | `RequestEvent` | _type_ | the normalised audit event handed to the sink |
5896
6031
  | `ObservabilityConfig` | _type_ | independent request and tool sink configuration |
5897
- | `Observability` | _type_ | `{ request?, toolCall, flush(), close() }` with bounded sink lifecycle |
6032
+ | `Observability` | _type_ | `{ request?, toolCall, getStatus(), flush(), close() }` with bounded sink lifecycle |
6033
+ | `ObservabilitySinkStatus` | _type_ | immutable counters for one bounded request/tool sink |
6034
+ | `ObservabilityStatus` | _type_ | per-surface plus aggregate operational snapshot |
6035
+ | `ObservabilityDrainReport` | _type_ | final closed/drained snapshot plus duration |
6036
+ | `ObservabilitySinkStatusSchema` / `ObservabilityStatusSchema` / `ObservabilityDrainReportSchema` | schema | runtime schemas for status/report integration boundaries |
5898
6037
  | `RequestEventSinkConfig` | _type_ | `write`, filter/sanitisation, `maxPending`, `onSinkError` and `onDrop` |
5899
6038
  | `RequestObservabilityConfig` | _type_ | request sink plus opt-in payload capture |
5900
6039
  | `SinkDropReason` | _type_ | `'capacity' \| 'closed'` |
@@ -5961,7 +6100,7 @@ payload.
5961
6100
  | `createMcpHandler` | function | a stateless dual-era Streamable-HTTP MCP handler — [guide](../guide/mcp-and-agents.md#mcp--createmcphandler) |
5962
6101
  | `createMcpHttpRoute` | function | framework-owned `RawRoute` adapter for an MCP HTTP handler |
5963
6102
  | `createStdioMcpServer` | function | a complete stdio MCP server — [guide](../guide/mcp-and-agents.md#mcp-over-stdio--createstdiomcpserver) |
5964
- | `buildMcpServer` | function | build an `McpServer` from contract/runtime surfaces the transport-neutral core |
6103
+ | `buildMcpServer` | function | build an `McpServer` from contract/runtime surfaces; no-auth configs omit the second argument |
5965
6104
  | `mountMcp` | function | add contract tools to an existing `McpServer` — [guide](../guide/mcp-and-agents.md#mountmcp) |
5966
6105
  | `implementRemote` | function | bind a contract to a remote HTTP API — [guide](../guide/mcp-and-agents.md#proxying-a-remote-api--implementremote) |
5967
6106
  | `mountAgent` | function | a Vercel AI SDK `ToolSet` from a service — [guide](../guide/mcp-and-agents.md#ai-agents--mountagent) |
@@ -6123,6 +6262,22 @@ Advanced building blocks — the shared machinery the mounts are built on.
6123
6262
 
6124
6263
  ---
6125
6264
 
6265
+ ## `stitchkit/testing`
6266
+
6267
+ Fetch-only integration helpers that preserve the real generated-client and
6268
+ handler pipeline without opening a TCP port.
6269
+
6270
+ | Export | Kind | Summary |
6271
+ |--------|------|---------|
6272
+ | `createHandlerTestClient` | function | one contract client backed by an in-process `FetchHandler` |
6273
+ | `createHandlerTestClients` | function | exact contract-registry batch form |
6274
+ | `HandlerTestClientDefaults` | _type_ | ordinary bare-client defaults with handler-owned `baseUrl` and `fetch` removed |
6275
+ | `HandlerTestClientConfig` | _type_ | handler, contract, path prefix, scoped config and client request defaults |
6276
+ | `HandlerTestClientsConfig` | _type_ | batch helper configuration |
6277
+ | `HandlerTestTransportConfig` | _type_ | shared in-process handler, origin, prefix, client defaults and optional server handle |
6278
+
6279
+ ---
6280
+
6126
6281
  ## `stitchkit/node`
6127
6282
 
6128
6283
  Server-only, for Node ≥ 22 (Bun uses `stitchkit/server`). The runtime-agnostic
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "stitchkit",
3
- "version": "0.47.0",
3
+ "version": "0.48.1",
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",
@@ -161,7 +165,7 @@
161
165
  "@types/json-schema": "^7.0.15",
162
166
  "@types/react": "^19.2.18",
163
167
  "@typescript/typescript6": "^6.0.2",
164
- "ai": "^7.0.58",
168
+ "ai": "^7.0.65",
165
169
  "react": "^19.2.8",
166
170
  "react-query-kit": "^3.3.4",
167
171
  "socket.io": "^4.8.3",