stitchkit 0.48.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 (41) hide show
  1. package/dist/browser/client.d.ts +3 -0
  2. package/dist/browser/client.d.ts.map +1 -1
  3. package/dist/browser/http.d.ts +7 -4
  4. package/dist/browser/http.d.ts.map +1 -1
  5. package/dist/cli.js +3 -2
  6. package/dist/{index-nrytvb30.js → index-03j2t778.js} +5 -8
  7. package/dist/{index-kp8xamqp.js → index-0hj37z43.js} +4 -2
  8. package/dist/index-48ffdxgk.js +6 -0
  9. package/dist/index-h05ygjqx.js +149 -0
  10. package/dist/{index-ee621cmy.js → index-jewp9r0a.js} +5 -139
  11. package/dist/index-p9d4cxt5.js +436 -0
  12. package/dist/{index-44xysy8r.js → index-ts21eyz4.js} +141 -19
  13. package/dist/{index-8ekq6res.js → index-v58mwa19.js} +4 -2
  14. package/dist/index.d.ts +1 -1
  15. package/dist/index.d.ts.map +1 -1
  16. package/dist/index.js +18 -3
  17. package/dist/node.d.ts +1 -1
  18. package/dist/node.d.ts.map +1 -1
  19. package/dist/node.js +8 -3
  20. package/dist/observability/audit.d.ts +4 -1
  21. package/dist/observability/audit.d.ts.map +1 -1
  22. package/dist/observability/index.d.ts +1 -0
  23. package/dist/observability/index.d.ts.map +1 -1
  24. package/dist/observability/index.js +150 -16
  25. package/dist/observability/status.d.ts +105 -0
  26. package/dist/observability/status.d.ts.map +1 -0
  27. package/dist/server/implement.d.ts +16 -6
  28. package/dist/server/implement.d.ts.map +1 -1
  29. package/dist/server/index.d.ts +1 -1
  30. package/dist/server/index.d.ts.map +1 -1
  31. package/dist/server/index.js +14 -6
  32. package/dist/server/router.d.ts +4 -1
  33. package/dist/server/router.d.ts.map +1 -1
  34. package/dist/testing.d.ts +34 -0
  35. package/dist/testing.d.ts.map +1 -0
  36. package/dist/testing.js +36 -0
  37. package/dist/tools/mcp.d.ts +1 -0
  38. package/dist/tools/mcp.d.ts.map +1 -1
  39. package/dist/tools.js +21 -428
  40. package/llms-full.txt +127 -16
  41. 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
 
@@ -2251,15 +2301,18 @@ validateMcpSchemas({
2251
2301
  extend,
2252
2302
  flattenUnionInput: true,
2253
2303
  requireTypedProperties: true,
2254
- allowUntyped: ['docs_create.payload'], // deliberately free-form
2255
2304
  requirePortableFormats: true,
2256
2305
  allowFormats: [],
2257
2306
  })
2258
2307
  ```
2259
2308
 
2260
- Off by default, because a contract may legitimately declare `z.unknown()`.
2261
- `allowUntyped` takes dotted `tool.property` paths an entry there is a decision,
2262
- 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
2263
2316
  `schemaValidation`; the handler supplies its real `extend` and
2264
2317
  `flattenUnionInput`, so the check cannot vet a different document from the one
2265
2318
  advertised.
@@ -3981,8 +4034,8 @@ export const observability = createObservability({
3981
4034
  },
3982
4035
  })
3983
4036
 
3984
- await observability.flush()
3985
- await observability.close()
4037
+ const status = observability.getStatus()
4038
+ const drained = await observability.close()
3986
4039
  ```
3987
4040
 
3988
4041
  `maxPending` defaults to `1000` per sink and must be a positive safe integer.
@@ -3993,9 +4046,15 @@ cancelled. Sink, filter and diagnostic-callback failures remain isolated from
3993
4046
  the observed request or tool call and are reported through `onSinkError` when
3994
4047
  configured; `onSinkError`/`onDrop` cannot create unhandled rejections.
3995
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
+
3996
4054
  `flush()` snapshots the current generation and waits only for events admitted
3997
4055
  up to that call. `close()` atomically stops admission, drains every accepted
3998
- 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:
3999
4058
 
4000
4059
  1. stop HTTP/MCP admission;
4001
4060
  2. wait for active requests and tool calls;
@@ -4431,7 +4490,33 @@ never block or break the request. Swallow the sink's own errors.
4431
4490
  stitchkit's own test suite runs on `bun:test`. The contract makes most of an
4432
4491
  API testable without a live socket.
4433
4492
 
4434
- ### 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
4435
4520
 
4436
4521
  `createHandler` is the router as a plain `(req) => Promise<Response>` function —
4437
4522
  no `Bun.serve`, no port. Drive it with a `Request`:
@@ -4461,8 +4546,8 @@ test('create returns the note', async () => {
4461
4546
  })
4462
4547
  ```
4463
4548
 
4464
- This exercises the full pipeline routing, schema parsing, hooks, the error
4465
- envelope with no network.
4549
+ This lower-level form exercises the same pipeline while letting a test inspect
4550
+ the raw `Response` itself.
4466
4551
 
4467
4552
  ### Test handlers directly
4468
4553
 
@@ -5628,6 +5713,7 @@ The browser-and-server entrypoint. Re-exports everything from
5628
5713
  | `createUrlBuilders` | function | build one exact URL builder per contract in a registry |
5629
5714
  | `UrlBuilderConfig` | _type_ | explicit `{ baseUrl }` source for a URL builder |
5630
5715
  | `ClientConfig` | _type_ | config for `createClient`'s bare-fetch mode (2nd arg, no `HttpClient`) |
5716
+ | `ClientFetch` | _type_ | injectable Fetch-compatible transport used by framework testing adapters |
5631
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) |
5632
5718
  | `ContractClientConfig` | _type_ | per-tenant / resource-scoped client config — dynamic `pathPrefix` + `stripPrefixKeys` ([guide](../guide/client.md#contractclientconfig--per-tenant--resource-scoped-clients)) |
5633
5719
  | `contractEndpointMatchers` | function | compile exact pathname matchers for selected HTTP contract operations and expected-401 policy |
@@ -5636,7 +5722,7 @@ The browser-and-server entrypoint. Re-exports everything from
5636
5722
  | `ApiError` | class | a non-2xx response, with `code` / `status` / `details` / `hint` and optional readonly `traceId` from `x-request-id` |
5637
5723
  | `HttpClient` | _type_ | the transport interface `createClient` builds on |
5638
5724
  | `ConfiguredHttpClient` | _type_ | a framework-created `HttpClient` carrying its readonly `baseUrl` for URL builders |
5639
- | `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) |
5640
5726
  | `UnauthorizedMatcher` | _type_ | exact `(pathname) => boolean` policy accepted by `suppressUnauthorizedFor` |
5641
5727
  | `RequestOptions` | _type_ | per-call options — params, timeout, response type |
5642
5728
  | `HeaderProvider` | _type_ | static or per-request headers |
@@ -5789,6 +5875,11 @@ Also re-exports the error helpers from `stitchkit/contract`.
5789
5875
  | `createHandler` | function | the router as a bare `(req) => Response` — [guide](../guide/server.md#createserver) |
5790
5876
  | `implement` | function | bind a contract to typed handlers — [guide](../guide/server.md#implement) |
5791
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 |
5792
5883
  | `staticRoute` | function | a raw route that serves a directory |
5793
5884
  | `serveFile` | function | serve a file with `Range` / `304` / `HEAD` — [guide](../guide/server.md#serving-files--range-requests) |
5794
5885
  | `parseByteRange` | function | parse a single `Range` header → range / `unsatisfiable` / `null` |
@@ -5938,7 +6029,11 @@ audit event. See the [Observability guide](../guide/observability.md).
5938
6029
  | `createObservability` | function | configure framework-owned request completion and canonical tool event sinks — [guide](../guide/observability.md#createobservability) |
5939
6030
  | `RequestEvent` | _type_ | the normalised audit event handed to the sink |
5940
6031
  | `ObservabilityConfig` | _type_ | independent request and tool sink configuration |
5941
- | `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 |
5942
6037
  | `RequestEventSinkConfig` | _type_ | `write`, filter/sanitisation, `maxPending`, `onSinkError` and `onDrop` |
5943
6038
  | `RequestObservabilityConfig` | _type_ | request sink plus opt-in payload capture |
5944
6039
  | `SinkDropReason` | _type_ | `'capacity' \| 'closed'` |
@@ -6005,7 +6100,7 @@ payload.
6005
6100
  | `createMcpHandler` | function | a stateless dual-era Streamable-HTTP MCP handler — [guide](../guide/mcp-and-agents.md#mcp--createmcphandler) |
6006
6101
  | `createMcpHttpRoute` | function | framework-owned `RawRoute` adapter for an MCP HTTP handler |
6007
6102
  | `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 the transport-neutral core |
6103
+ | `buildMcpServer` | function | build an `McpServer` from contract/runtime surfaces; no-auth configs omit the second argument |
6009
6104
  | `mountMcp` | function | add contract tools to an existing `McpServer` — [guide](../guide/mcp-and-agents.md#mountmcp) |
6010
6105
  | `implementRemote` | function | bind a contract to a remote HTTP API — [guide](../guide/mcp-and-agents.md#proxying-a-remote-api--implementremote) |
6011
6106
  | `mountAgent` | function | a Vercel AI SDK `ToolSet` from a service — [guide](../guide/mcp-and-agents.md#ai-agents--mountagent) |
@@ -6167,6 +6262,22 @@ Advanced building blocks — the shared machinery the mounts are built on.
6167
6262
 
6168
6263
  ---
6169
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
+
6170
6281
  ## `stitchkit/node`
6171
6282
 
6172
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.48.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",