stitchkit 0.44.1 → 0.46.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.
Files changed (104) hide show
  1. package/README.md +1 -1
  2. package/dist/browser/http.d.ts.map +1 -1
  3. package/dist/browser/socket-io.d.ts +16 -0
  4. package/dist/browser/socket-io.d.ts.map +1 -1
  5. package/dist/cli.js +3 -4
  6. package/dist/contract/errors.d.ts +1 -0
  7. package/dist/contract/errors.d.ts.map +1 -1
  8. package/dist/contract/index.js +2 -2
  9. package/dist/{index-yh0p5ddn.js → index-5s8b7z6q.js} +31 -11
  10. package/dist/{index-mvjyq9h9.js → index-62pqb23z.js} +309 -144
  11. package/dist/index-c40tkxcd.js +181 -0
  12. package/dist/{index-r6czv7yt.js → index-pwyedf7b.js} +4 -3
  13. package/dist/{index-6jypn22c.js → index-r1qp4rve.js} +9 -1
  14. package/dist/{index-s4qsmgwe.js → index-tss6bk5c.js} +4 -1
  15. package/dist/{index-36f1xkdb.js → index-w1s873ng.js} +2 -2
  16. package/dist/index-yxpe3phd.js +63 -0
  17. package/dist/{index-4fyn9f09.js → index-zwqty9zf.js} +396 -246
  18. package/dist/index.d.ts +4 -2
  19. package/dist/index.d.ts.map +1 -1
  20. package/dist/index.js +266 -17
  21. package/dist/internal/errors.d.ts.map +1 -1
  22. package/dist/internal/secure-fetch.d.ts +23 -3
  23. package/dist/internal/secure-fetch.d.ts.map +1 -1
  24. package/dist/internal/typed.d.ts +7 -0
  25. package/dist/internal/typed.d.ts.map +1 -1
  26. package/dist/internal/within-dir.d.ts +6 -0
  27. package/dist/internal/within-dir.d.ts.map +1 -1
  28. package/dist/logger.d.ts +8 -0
  29. package/dist/logger.d.ts.map +1 -0
  30. package/dist/node.d.ts +1 -0
  31. package/dist/node.d.ts.map +1 -1
  32. package/dist/node.js +5 -4
  33. package/dist/observability/audit.d.ts.map +1 -1
  34. package/dist/observability/event.d.ts +7 -0
  35. package/dist/observability/event.d.ts.map +1 -1
  36. package/dist/observability/index.js +54 -125
  37. package/dist/observability/sanitize.d.ts +8 -1
  38. package/dist/observability/sanitize.d.ts.map +1 -1
  39. package/dist/observability/trace.d.ts +3 -1
  40. package/dist/observability/trace.d.ts.map +1 -1
  41. package/dist/react/cache-bridge.d.ts +4 -0
  42. package/dist/react/cache-bridge.d.ts.map +1 -1
  43. package/dist/react.js +46 -5
  44. package/dist/realtime/contract.d.ts +39 -0
  45. package/dist/realtime/contract.d.ts.map +1 -0
  46. package/dist/realtime/index.d.ts +3 -0
  47. package/dist/realtime/index.d.ts.map +1 -0
  48. package/dist/realtime/rejection.d.ts +10 -0
  49. package/dist/realtime/rejection.d.ts.map +1 -0
  50. package/dist/realtime/socket.d.ts +30 -0
  51. package/dist/realtime/socket.d.ts.map +1 -0
  52. package/dist/server/cache.d.ts +5 -1
  53. package/dist/server/cache.d.ts.map +1 -1
  54. package/dist/server/error-hook.d.ts +1 -0
  55. package/dist/server/error-hook.d.ts.map +1 -1
  56. package/dist/server/file.d.ts +6 -0
  57. package/dist/server/file.d.ts.map +1 -1
  58. package/dist/server/implement.d.ts.map +1 -1
  59. package/dist/server/index.d.ts +3 -3
  60. package/dist/server/index.d.ts.map +1 -1
  61. package/dist/server/index.js +110 -11
  62. package/dist/server/middleware/cors.d.ts.map +1 -1
  63. package/dist/server/openapi.d.ts.map +1 -1
  64. package/dist/server/realtime.d.ts +24 -0
  65. package/dist/server/realtime.d.ts.map +1 -0
  66. package/dist/server/router.d.ts +0 -8
  67. package/dist/server/router.d.ts.map +1 -1
  68. package/dist/server/stream.d.ts.map +1 -1
  69. package/dist/server/types.d.ts +2 -7
  70. package/dist/server/types.d.ts.map +1 -1
  71. package/dist/tools/cli-args.d.ts +11 -3
  72. package/dist/tools/cli-args.d.ts.map +1 -1
  73. package/dist/tools/cli.d.ts +5 -0
  74. package/dist/tools/cli.d.ts.map +1 -1
  75. package/dist/tools/coerce.d.ts.map +1 -1
  76. package/dist/tools/json-schema.d.ts.map +1 -1
  77. package/dist/tools/mcp-handler.d.ts +4 -3
  78. package/dist/tools/mcp-handler.d.ts.map +1 -1
  79. package/dist/tools/mcp-prepare.d.ts +95 -0
  80. package/dist/tools/mcp-prepare.d.ts.map +1 -0
  81. package/dist/tools/mcp-round.d.ts +4 -0
  82. package/dist/tools/mcp-round.d.ts.map +1 -1
  83. package/dist/tools/mcp-stdio.d.ts +2 -1
  84. package/dist/tools/mcp-stdio.d.ts.map +1 -1
  85. package/dist/tools/mcp.d.ts +2 -78
  86. package/dist/tools/mcp.d.ts.map +1 -1
  87. package/dist/tools/mount-download.d.ts +5 -0
  88. package/dist/tools/mount-download.d.ts.map +1 -1
  89. package/dist/tools/native-mcp.d.ts +1 -1
  90. package/dist/tools/native-mcp.d.ts.map +1 -1
  91. package/dist/tools/oauth-provider.d.ts +20 -1
  92. package/dist/tools/oauth-provider.d.ts.map +1 -1
  93. package/dist/tools/tool-logger.d.ts +1 -1
  94. package/dist/tools/tool-logger.d.ts.map +1 -1
  95. package/dist/tools/view-file.d.ts +9 -0
  96. package/dist/tools/view-file.d.ts.map +1 -1
  97. package/dist/tools.d.ts +2 -1
  98. package/dist/tools.d.ts.map +1 -1
  99. package/dist/tools.js +342 -214
  100. package/llms-full.txt +175 -44
  101. package/llms.txt +1 -1
  102. package/package.json +2 -2
  103. package/dist/index-frfyw9fa.js +0 -33
  104. package/dist/index-x3fcszf8.js +0 -8
package/llms-full.txt CHANGED
@@ -677,7 +677,7 @@ server. See [Testing & deployment](./testing-and-deployment.md).
677
677
  | `maxUploadBytes` | default multipart upload cap (bytes); per-route `EndpointDef.maxUploadBytes` overrides |
678
678
  | `maxJsonBodyBytes` | optional JSON body cap (bytes); per-route value overrides; unset preserves existing behaviour |
679
679
  | `port` / `hostname` | listen address — port defaults to `3000` |
680
- | `cors` | CORS policy — `{ origin, credentials, methods, headers, exposeHeaders }` |
680
+ | `cors` | CORS policy — `{ origin, credentials, methods, headers, exposeHeaders }`. `origin` is **required** when `cors` is present: pass an explicit origin (or list), or `'*'` to deliberately allow every origin — an origin-less config is a construction error, never a silent wildcard. Omit `cors` entirely to emit no CORS headers. |
681
681
  | `hooks` | lifecycle hooks (see below) |
682
682
  | `logging` | `true` for built-in request logs, or a `LoggingConfig` (see below) |
683
683
  | `traceId` | override per-request trace-id resolution — may return `undefined` to fall back |
@@ -685,6 +685,34 @@ server. See [Testing & deployment](./testing-and-deployment.md).
685
685
  | `websocket` | Bun WebSocket handlers — e.g. from `createSocketIOServer` |
686
686
  | `routes` / `development` / `bun` | passthrough to `Bun.serve` |
687
687
 
688
+ ### Trusted HTTPS in development
689
+
690
+ Stitchkit does not own local certificate generation or a frontend development
691
+ server. When a device or browser feature requires a secure context, generate a
692
+ trusted certificate outside the application (for example with `mkcert`) and pass
693
+ its files through the Bun server boundary:
694
+
695
+ ```ts
696
+ import { readFileSync } from 'node:fs'
697
+ import { createServer } from 'stitchkit/server'
698
+
699
+ createServer({
700
+ services,
701
+ hostname: '0.0.0.0',
702
+ bun: {
703
+ tls: {
704
+ cert: readFileSync('./certs/dev.pem'),
705
+ key: readFileSync('./certs/dev-key.pem'),
706
+ },
707
+ },
708
+ })
709
+ ```
710
+
711
+ Trust the certificate authority on each test device and configure the frontend's
712
+ HTTPS mode in that frontend project. Certificate renewal, interface discovery
713
+ and device onboarding remain application infrastructure rather than framework
714
+ or starter behavior.
715
+
688
716
  ### Request logging
689
717
 
690
718
  `logging: true` is shorthand for `logging: {}` — **any object turns logging
@@ -1207,7 +1235,7 @@ The client side is [`parseSSE`](./client.md#sse).
1207
1235
  ```ts
1208
1236
  import { parseMultipart } from 'stitchkit/server'
1209
1237
 
1210
- const { file, fields } = await parseMultipart(req, { maxBytes: 10_000_000 })
1238
+ const { file, fields } = await parseMultipart(req, 'file', undefined, 10_000_000)
1211
1239
  ```
1212
1240
 
1213
1241
  When an endpoint declares `multipart`, the framework parses the upload for you
@@ -1232,8 +1260,11 @@ createServer({ services, maxUploadBytes: 50 * 1024 * 1024 })
1232
1260
  ```ts
1233
1261
  import { createRateLimiter } from 'stitchkit/server'
1234
1262
 
1235
- const limiter = createRateLimiter({ capacity: 60, refillPerSecond: 1 })
1236
- // in onRequest: if (!limiter.take(ip)) return new Response('Too many', { status: 429 })
1263
+ const limiter = createRateLimiter()
1264
+ // in onRequest:
1265
+ if (!limiter.check(ip, { window: 60_000, max: 60 })) {
1266
+ return new Response('Too many', { status: 429 })
1267
+ }
1237
1268
  ```
1238
1269
 
1239
1270
  ### Event bus
@@ -2259,6 +2290,15 @@ timeouts, redirect count and body size are bounded. Its bounded cache respects
2259
2290
  `Last-Modified`, coalesces concurrent misses and reports sanitized cache events;
2260
2291
  invalid or unavailable identity receives only a short fail-closed cache entry.
2261
2292
 
2293
+ Resolution itself is rate-limited on two levels (`CimdCachePolicy`):
2294
+ `maxResolutionsPerClient` (default 10) bounds ONE `client_id` per
2295
+ `resolutionWindowMs` window (default 60 s) — a client whose document disables
2296
+ caching burns its own budget, not the server's — while `maxResolutions`
2297
+ (default 120) caps the server-wide outbound rate as the flood backstop.
2298
+ Positive and negative cache entries keep separate pools of `maxEntries` each,
2299
+ so a flood of unresolvable ids cannot evict warmed clients and a burst of
2300
+ successes cannot reset a failing client's backoff.
2301
+
2262
2302
  ### Authorization hardening (MCP 2026-07-28)
2263
2303
 
2264
2304
  - **`iss` on every authorization response (RFC 9207, SEP-2468).** Success *and*
@@ -2586,6 +2626,11 @@ mountMcp(server, services, { hooks: createToolLogger() })
2586
2626
  // [tool] warn get_widget (widgets.get) NOT_FOUND 4ms
2587
2627
  ```
2588
2628
 
2629
+ By default the line goes to `console.error` — **stderr, never stdout**. That is
2630
+ deliberate: in a stdio-MCP server stdout is the JSON-RPC protocol channel, and a
2631
+ log line written there corrupts the stream. If you pass your own `log`, keep it
2632
+ off stdout for any stdio deployment.
2633
+
2589
2634
  Pass `log` to redirect the line, or `onRecord` to feed a metrics sink the
2590
2635
  structured `ToolCallRecord`. That record carries `traceId` whenever an
2591
2636
  observability context is active, so a tool call made inside an HTTP request
@@ -2798,36 +2843,64 @@ fallback, heartbeats, acks, a mature client. stitchkit does not ship its own
2798
2843
  WebSocket engine; it ships thin, typed wrappers over Socket.IO and a bridge that
2799
2844
  syncs socket events into the TanStack Query cache. See
2800
2845
  [ADR 0008](../decisions/0008-thin-wrappers.md).
2846
+ The separate contract shape and rejection ownership are recorded in
2847
+ [ADR 0069](../decisions/0069-realtime-contracts-validate-without-owning-delivery.md).
2801
2848
 
2802
- ## Typed events
2849
+ ## Zod-first event contract
2803
2850
 
2804
- Declare the event maps once, in the shared module both sides import them:
2851
+ Declare wire events once in the shared module. Each `args` schema is a tuple, so
2852
+ Socket.IO's no-payload and variadic forms stay intact; `ack` describes the value
2853
+ returned through an acknowledgement callback.
2805
2854
 
2806
2855
  ```ts
2807
2856
  // shared/contracts.ts
2808
- export interface ServerToClientEvents {
2809
- 'note:created': (note: Note) => void
2810
- 'note:deleted': (id: string) => void
2811
- }
2812
- export interface ClientToServerEvents {
2813
- 'room:join': (room: string) => void
2814
- }
2857
+ import { defineRealtimeContract } from 'stitchkit'
2858
+ import { z } from 'zod'
2859
+
2860
+ export const realtimeContract = defineRealtimeContract({
2861
+ serverToClient: {
2862
+ 'note:created': { args: z.tuple([NoteSchema]) },
2863
+ 'note:deleted': { args: z.tuple([z.string()]) },
2864
+ },
2865
+ clientToServer: {
2866
+ 'room:join': {
2867
+ args: z.tuple([z.string()]),
2868
+ ack: z.object({ joined: z.boolean() }),
2869
+ },
2870
+ ready: { args: z.tuple([]) },
2871
+ },
2872
+ })
2815
2873
  ```
2816
2874
 
2817
- Every `emit` and `on` on both the server and client wrapper is typed against
2818
- these maps.
2875
+ The contract derives every event handler type and validates both directions at
2876
+ runtime. A malformed inbound tuple never reaches the application handler;
2877
+ invalid outbound data throws before Socket.IO publishes it. Rejections call the
2878
+ optional `onRejected` hook with event, direction, phase and the Zod error.
2819
2879
 
2820
2880
  ## Server — `createSocketIOServer`
2821
2881
 
2822
2882
  ```ts
2823
- import { createServer, createSocketIOServer } from 'stitchkit/server'
2883
+ import {
2884
+ bindRealtimeServer,
2885
+ createServer,
2886
+ createSocketIOServer,
2887
+ } from 'stitchkit/server'
2888
+ import { realtimeContract } from '@app/shared'
2824
2889
 
2825
- const socket = await createSocketIOServer<ServerToClientEvents, ClientToServerEvents>({
2890
+ const socket = await createSocketIOServer({
2826
2891
  cors: { origin: 'https://app.example.com' },
2827
2892
  })
2828
2893
 
2829
- socket.io.on('connection', (s) => {
2830
- s.on('room:join', (room) => s.join(room)) // rooms, handshake auth — your logic
2894
+ const realtime = bindRealtimeServer(realtimeContract, socket, {
2895
+ onRejected: (event) => audit.realtimeRejected(event),
2896
+ })
2897
+
2898
+ realtime.onConnection(({ raw, events, to }) => {
2899
+ events.on('room:join', (room, acknowledge) => {
2900
+ raw.join(room) // authorization and room membership remain application policy
2901
+ acknowledge({ joined: true })
2902
+ to(room).emit('note:created', note)
2903
+ })
2831
2904
  })
2832
2905
  ```
2833
2906
 
@@ -2840,13 +2913,23 @@ createServer({
2840
2913
  rawRoutes: [socket.route], // ready-made /socket.io/*socketPath route
2841
2914
  })
2842
2915
 
2843
- // elsewhere — broadcast:
2844
- socket.io.emit('note:created', note)
2916
+ // elsewhere — validated broadcast:
2917
+ realtime.emit('note:created', note)
2918
+ ```
2919
+
2920
+ The canonical room-broadcast example below is executed by the test suite. Its
2921
+ body is kept byte-identical to `packages/core/examples/realtime-room.ts`.
2922
+
2923
+ ```ts canonical-realtime-room
2924
+ export function publishExampleNote(realtime: ExampleRealtimePublisher): void {
2925
+ const note = { id: 'note-1', text: 'Ready' };
2926
+ realtime.to('general').emit('note:created', note);
2927
+ }
2845
2928
  ```
2846
2929
 
2847
2930
  | Handle field | Purpose |
2848
2931
  |--------------|---------|
2849
- | `io` | the typed Socket.IO server attach `connection` handlers, broadcast |
2932
+ | `io` | raw Socket.IO server for middleware, handshake auth and transport ownership |
2850
2933
  | `websocket` | Bun WebSocket handlers — pass to `createServer({ websocket })` |
2851
2934
  | `route` | the `/socket.io/*socketPath` raw route — pass to `createServer({ rawRoutes })` |
2852
2935
 
@@ -2868,20 +2951,29 @@ precedence over the same keys in `serverOptions`. On Bun the engine-level option
2868
2951
  `@socket.io/bun-engine` too — so a configured `maxHttpBufferSize` actually applies
2869
2952
  instead of silently truncating at 1 MB.
2870
2953
 
2871
- ## Client — `createSocketIOClient`
2954
+ ## Client — `createRealtimeClient`
2872
2955
 
2873
2956
  ```ts
2874
- import { createSocketIOClient } from 'stitchkit'
2957
+ import { createRealtimeClient } from 'stitchkit'
2958
+ import { realtimeContract } from '@app/shared'
2875
2959
 
2876
- const socket = createSocketIOClient<ServerToClientEvents, ClientToServerEvents>({
2960
+ const socket = createRealtimeClient(realtimeContract, {
2877
2961
  url: 'https://api.example.com',
2962
+ retain: ['note:created'],
2963
+ onRejected: (event) => reportClientError(event),
2878
2964
  })
2879
2965
 
2880
2966
  socket.connect()
2881
2967
  socket.on('note:created', (note) => { /* typed note */ })
2882
- socket.emit('room:join', 'general') // typed
2968
+ socket.emit('room:join', 'general', ({ joined }) => { /* typed + validated */ })
2883
2969
  ```
2884
2970
 
2971
+ ## Low-level transport
2972
+
2973
+ `createSocketIOClient` remains the low-level Socket.IO transport wrapper for
2974
+ schema-agnostic infrastructure. Application wire events should use
2975
+ `createRealtimeClient`; it adds the shared contract without replacing Socket.IO.
2976
+
2885
2977
  ### Durable subscriptions
2886
2978
 
2887
2979
  `socket.on(...)` returns an unsubscribe and is **durable** — the handler is
@@ -2901,7 +2993,7 @@ subscribes (and on the next subscribe after a re-render). It is the pub/sub
2901
2993
  analogue of an MQTT *retained* message or an RxJS `BehaviorSubject`.
2902
2994
 
2903
2995
  ```ts
2904
- const client = createSocketIOClient<ServerEvents, ClientEvents>({
2996
+ const client = createRealtimeClient(realtimeContract, {
2905
2997
  url,
2906
2998
  retain: ['presence:changed', 'job:state'], // events to keep the last value of
2907
2999
  })
@@ -2938,7 +3030,7 @@ handshake with **`auth`** instead — the token reaches the server as
2938
3030
  // client — a function is re-read on every (re)connect, so a rotated token is
2939
3031
  // picked up automatically; no need to recreate the client (or lose durable
2940
3032
  // subscriptions). It may be async.
2941
- const socket = createSocketIOClient<ServerToClientEvents, ClientToServerEvents>({
3033
+ const socket = createRealtimeClient(realtimeContract, {
2942
3034
  url: 'https://api.example.com',
2943
3035
  auth: () => ({ token: getAccessToken() }),
2944
3036
  })
@@ -3092,11 +3184,9 @@ policies; this helper only applies declared CRUD semantics.
3092
3184
 
3093
3185
  ## Raw binary lane (Bun)
3094
3186
 
3095
- Socket.IO carries binary fine for most streams a binary event (`pcm(frame)`)
3096
- is enough. But a *truly* high-throughput binary channel (video, large
3097
- transfers) may want a raw WebSocket with no Socket.IO framing, on the **same**
3098
- port. On Bun that is awkward: `Bun.serve` has a single `websocket` handler, and
3099
- `createSocketIOServer().websocket` claims it.
3187
+ For a high-throughput raw binary channel beside Socket.IO, use the orthogonal
3188
+ composition boundary from
3189
+ [ADR 0020](../decisions/0020-raw-websocket-lane.md).
3100
3190
 
3101
3191
  `composeWebSocketHandlers` composes that one handler from several lanes. A raw
3102
3192
  lane stamps its own marker onto `ws.data` at upgrade and is matched positively;
@@ -3379,9 +3469,11 @@ pick the scheme), and checks `exp` and `nbf`.
3379
3469
 
3380
3470
  ```ts
3381
3471
  import { verifyJwt, extractToken } from 'stitchkit/server'
3472
+ import { env } from './env'
3382
3473
 
3383
3474
  const token = extractToken(req) // from Authorization, or a cookie name
3384
- const payload = await verifyJwt(token, process.env.JWT_SECRET!)
3475
+ if (!token) throw unauthorized('Bearer token is required')
3476
+ const payload = await verifyJwt(token, env.JWT_SECRET)
3385
3477
  ```
3386
3478
 
3387
3479
  `extractToken(req, cookieName?)` reads a bearer token from the `Authorization`
@@ -3579,6 +3671,7 @@ const onError = createErrorHook({
3579
3671
  NOT_FOUND: 'not_found', METHOD_NOT_ALLOWED: 'not_found',
3580
3672
  CONFLICT: 'conflict', RATE_LIMITED: 'rate_limited',
3581
3673
  INTERNAL_SERVER_ERROR: 'internal',
3674
+ REALTIME_CONTRACT_VIOLATION: 'internal',
3582
3675
  } satisfies Record<StitchErrorCode, string>,
3583
3676
  // `ctx` is the request's RuntimeContext — read `ctx.traceId` for a
3584
3677
  // correlation id in the envelope. Declaring it is optional.
@@ -3932,8 +4025,10 @@ each request.
3932
4025
 
3933
4026
  A payload goes into an audit row only after `sanitizePayload`:
3934
4027
 
3935
- - **secret-named keys are masked** — `password`, `token`, `apiKey`, `secret`,
3936
- `authorization`, `cookie`, (value `[redacted]`);
4028
+ - **secret-named keys are masked** — a key whose words include a secret term is
4029
+ redacted (`password`, `sessionToken`, `X-Api-Key`, `authorization`, `cookie`,
4030
+ …), while identifiers that merely contain one (`authorId`, `sessionCount`,
4031
+ `tokenizer`) survive;
3937
4032
  - **binary blobs** (`Uint8Array`, `Blob`, `FormData`) collapse to metadata —
3938
4033
  never the bytes;
3939
4034
  - the result is **capped** — anything over the byte limit becomes a preview.
@@ -4529,7 +4624,7 @@ current one *up to* your target, and apply each snippet.
4529
4624
  (`STITCH_ERROR_STATUS`, `serveFile`, `scopePrefixes`, `afterToolCall`'s
4530
4625
  `MethodDef`, `maxUploadBytes`) are available to adopt, not required.
4531
4626
 
4532
- ## Unreleased breaking migrations
4627
+ ## Released migration: 0.44.0
4533
4628
 
4534
4629
  ### MCP TypeScript SDK v2 and protocol `2026-07-28`
4535
4630
 
@@ -4724,14 +4819,29 @@ implemented or advertised.
4724
4819
  3. Delete all session mode, event-store and session-id code.
4725
4820
  4. Move OAuth client policy under `clientRegistration`; publish CIMD or enable
4726
4821
  DCR explicitly.
4727
- 5. Snapshot `listToolNames`, run one contract tool, one runtime tool, any raw
4822
+ 5. Make `authorizeUser` return the exact consented scope subset. The framework
4823
+ validates that it is a subset of the request before saving the authorization
4824
+ code:
4825
+
4826
+ ```ts
4827
+ // before
4828
+ authorizeUser: async () => ({ userId })
4829
+
4830
+ // after
4831
+ authorizeUser: async (_req, request) => ({
4832
+ userId,
4833
+ approvedScopes: request.scope?.split(' ') ?? [],
4834
+ })
4835
+ ```
4836
+
4837
+ 6. Snapshot `listToolNames`, run one contract tool, one runtime tool, any raw
4728
4838
  multimodal tool and every MCP App resource you use.
4729
- 6. Exercise modern HTTP and stdio with protocol `2026-07-28`; exercise legacy
4839
+ 7. Exercise modern HTTP and stdio with protocol `2026-07-28`; exercise legacy
4730
4840
  only if `legacy: 'serve'` is part of your support policy.
4731
- 7. Run the consumer's typecheck and runtime gates. A browser/HTTP-only consumer
4841
+ 8. Run the consumer's typecheck and runtime gates. A browser/HTTP-only consumer
4732
4842
  must continue to work without either MCP package.
4733
4843
 
4734
- ## Historical breaking migrations through 0.43.1
4844
+ ## Historical breaking migrations through 0.44.0
4735
4845
 
4736
4846
  HTTP observability now completes inside the framework handler instead of a
4737
4847
  nested fetch wrapper. Configure request and tool sinks explicitly:
@@ -5207,12 +5317,27 @@ The browser-and-server entrypoint. Re-exports everything from
5207
5317
 
5208
5318
  | Export | Kind | Summary |
5209
5319
  |--------|------|---------|
5210
- | `createSocketIOClient` | function | the typed Socket.IO client — [guide](../guide/realtime.md#client--createsocketioclient) |
5320
+ | `createSocketIOClient` | function | low-level typed Socket.IO transport primitive — [guide](../guide/realtime.md#low-level-transport) |
5321
+ | `defineRealtimeContract` | function | Zod-first shared Socket.IO event contract — [guide](../guide/realtime.md#zod-first-event-contract) |
5322
+ | `createRealtimeClient` | function | inferred, runtime-validated Socket.IO client — [guide](../guide/realtime.md#client--createrealtimeclient) |
5211
5323
  | `createRetainedTopics` | function | retained last-value store for sticky events — [guide](../guide/realtime.md#sticky-events) |
5212
5324
  | `parseSSE` | function | parse an SSE `Response` into an async generator — [guide](../guide/client.md#sse) |
5213
5325
  | `SocketIOClient` | _type_ | the client handle |
5214
5326
  | `SocketIOClientConfig` | _type_ | config for `createSocketIOClient` (incl. `retain`) |
5215
5327
  | `SocketEventMap` | _type_ | the shape of an event map |
5328
+ | `RealtimeClient` | _type_ | validated client inferred from a realtime contract |
5329
+ | `RealtimeClientOptions` | _type_ | transport options and the rejected-event hook for `createRealtimeClient` |
5330
+ | `RealtimeContract` | _type_ | shared server-to-client and client-to-server event registries |
5331
+ | `RealtimeEventRegistry` | _type_ | string-keyed registry of event definitions |
5332
+ | `RealtimeEventDefinition` | _type_ | one tuple-shaped event and optional acknowledgement schema |
5333
+ | `RealtimeEventArguments` | _type_ | tuple inferred from an event definition |
5334
+ | `RealtimeEmitArguments` | _type_ | emit arguments including an inferred acknowledgement callback |
5335
+ | `RealtimeEventHandler` | _type_ | handler inferred from an event definition |
5336
+ | `InferRealtimeEventMap` | _type_ | inferred Socket.IO-compatible event map |
5337
+ | `RealtimeRejectDirection` | _type_ | server/client inbound/outbound rejection direction |
5338
+ | `RealtimeRejectedEvent` | _type_ | structured rejected event with event, direction, phase, reason and fault |
5339
+ | `RealtimeRejectedEventHook` | _type_ | sync/async observer for structured realtime rejections |
5340
+ | `ValidatedRealtimeSocket` | _type_ | runtime-validating `on`/`emit` surface inferred from registries |
5216
5341
  | `RetainedTopics` | _type_ | the `createRetainedTopics` handle |
5217
5342
  | `ParseSSEOptions` | _type_ | options for `parseSSE` |
5218
5343
 
@@ -5407,6 +5532,10 @@ Also re-exports the error helpers from `stitchkit/contract`.
5407
5532
  | Export | Kind | Summary |
5408
5533
  |--------|------|---------|
5409
5534
  | `createSocketIOServer` | function | the typed Socket.IO server — [guide](../guide/realtime.md#server--createsocketioserver) |
5535
+ | `bindRealtimeServer` | function | inferred, runtime-validated connection and broadcast boundary |
5536
+ | `RealtimeServer` | _type_ | validated broadcast and connection API inferred from a realtime contract |
5537
+ | `RealtimeServerConnection` | _type_ | one validated connection with raw socket access for auth and rooms |
5538
+ | `RealtimeServerHandle` | _type_ | minimal Socket.IO server handle accepted by `bindRealtimeServer` |
5410
5539
  | `SocketIOServerConfig` | _type_ | config for `createSocketIOServer` |
5411
5540
  | `SocketIOServerHandle` | _type_ | the `{ io, websocket, route }` handle |
5412
5541
  | `composeWebSocketHandlers` | function | compose one Bun `websocket` from N lanes — a raw binary lane beside Socket.IO ([guide](../guide/realtime.md#raw-binary-lane-bun)) |
@@ -5426,6 +5555,7 @@ Also re-exports the error helpers from `stitchkit/contract`.
5426
5555
  | `parseMultipart` | function | parse a `multipart/form-data` request — [guide](../guide/server.md#multipart) |
5427
5556
  | `createRateLimiter` | function | token-bucket rate limiting — [guide](../guide/server.md#rate-limiting) |
5428
5557
  | `createCache` | function | an in-memory TTL cache |
5558
+ | `CacheOptions` | _type_ | bounded-cache options, including the maximum retained entry count |
5429
5559
  | `cacheHeaders` | function | build a `Cache-Control` header |
5430
5560
  | `EventBusOptions` | _type_ | options for `createEventBus` |
5431
5561
  | `EventHandler` | _type_ | one event-bus subscriber |
@@ -5553,6 +5683,7 @@ payload.
5553
5683
  | `StdioMcpServerConfig` | _type_ | config for `createStdioMcpServer` |
5554
5684
  | `McpServerBuildConfig` | _type_ | shared config for `buildMcpServer` |
5555
5685
  | `McpServerSharedConfig` | _type_ | transport-neutral options shared by direct and finite surface configs |
5686
+ | `McpServer` | _type_ | official split-SDK server instance accepted by raw extension points |
5556
5687
  | `DirectMcpSurfaceConfig` | _type_ | static or identity-dynamic `services` / `runtimeTools` source |
5557
5688
  | `FiniteMcpSurfaceConfig` | _type_ | bounded `surfaces` registry plus typed selector |
5558
5689
  | `McpSurfaceDefinition` | _type_ | one immutable `{ services, runtimeTools }` MCP surface |
@@ -5583,7 +5714,7 @@ payload.
5583
5714
  | `CimdClientMetadataFetcher` | _type_ | injectable secure network boundary for metadata loading |
5584
5715
  | `CimdFetchResponse` | _type_ | bounded metadata fetch result passed across the injectable network boundary |
5585
5716
  | `CimdFetchPolicy` | _type_ | CIMD timeout, redirect and size limits |
5586
- | `CimdCachePolicy` | _type_ | bounded HTTP-aware positive/negative cache policy |
5717
+ | `CimdCachePolicy` | _type_ | bounded HTTP-aware cache policy: separate positive/negative pools (`maxEntries`), per-client and server-wide resolution rate limits (`maxResolutionsPerClient` / `maxResolutions` per `resolutionWindowMs`) |
5587
5718
  | `CimdCacheEvent` | _type_ | observable CIMD cache hit, miss, revalidation and eviction event |
5588
5719
  | `createSecureClientMetadataFetcher` | function | production HTTPS, DNS/IP-pinned CIMD fetcher |
5589
5720
  | `RuntimeAgentModelOutput` | _type_ | AI SDK model-facing text/JSON/content output returned by `present.agent` |
@@ -5696,7 +5827,7 @@ Advanced building blocks — the shared machinery the mounts are built on.
5696
5827
 
5697
5828
  Server-only, for Node ≥ 22 (Bun uses `stitchkit/server`). The runtime-agnostic
5698
5829
  core plus a Node HTTP adapter — [ADR 0013](../decisions/0013-runtime-agnostic-core.md),
5699
- [deployment guide](../guide/testing-and-deployment.md#node). Re-exports the
5830
+ [deployment guide](../guide/testing-and-deployment.md#deploy-on-node). Re-exports the
5700
5831
  runtime-agnostic pieces of `stitchkit/server` and the error helpers.
5701
5832
 
5702
5833
  | Export | Kind | Summary |
package/llms.txt CHANGED
@@ -13,7 +13,7 @@ Build with stitchkit: define a contract once, then `implement` it and serve it (
13
13
  - [CLI](https://github.com/max-listov/stitchkit/blob/master/docs/guide/cli.md): contracts as a command-line program
14
14
  - [Realtime](https://github.com/max-listov/stitchkit/blob/master/docs/guide/realtime.md): Socket.IO server/client wrappers, handshake auth, the cache bridge, a raw WebSocket lane
15
15
  - [Auth & errors](https://github.com/max-listov/stitchkit/blob/master/docs/guide/auth-and-errors.md): scopes, createAuthHook, JWT/cookies, the AppError model, the stitch error-code registry
16
- - [Observability](https://github.com/max-listov/stitchkit/blob/master/docs/guide/observability.md): request and tool-call logging via hooks, W3C trace context, createAuditHook
16
+ - [Observability](https://github.com/max-listov/stitchkit/blob/master/docs/guide/observability.md): request and tool-call observability, W3C trace context, createObservability
17
17
  - [Testing & deployment](https://github.com/max-listov/stitchkit/blob/master/docs/guide/testing-and-deployment.md): in-process testing; deploying on Bun and on Node (serveNode)
18
18
  - [Multi-tenant](https://github.com/max-listov/stitchkit/blob/master/docs/guide/multi-tenant.md): a /tenants/:id/… scenario end-to-end — scopePrefixes, scoped client, extend
19
19
  - [Frontend integrations](https://github.com/max-listov/stitchkit/blob/master/docs/guide/frontend-integrations.md): React Router resource routes and a separate Vite development proxy
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "stitchkit",
3
- "version": "0.44.1",
3
+ "version": "0.46.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",
@@ -88,7 +88,7 @@
88
88
  "build:types": "bun x tsc -p tsconfig.build.json --emitDeclarationOnly",
89
89
  "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",
90
90
  "dev": "bun run build:js -- --watch",
91
- "prepublishOnly": "cp ../../README.md ./README.md && bun ../../scripts/gen-llms.ts && bun run build",
91
+ "prepack": "cp ../../README.md ./README.md && bun ../../scripts/gen-llms.ts && bun run build",
92
92
  "test": "bun test",
93
93
  "smoke:node": "node scripts/node-smoke.mjs",
94
94
  "consumer-lane": "bun scripts/consumer-lane/run.mjs",
@@ -1,33 +0,0 @@
1
- // src/tools/json-schema.ts
2
- import { z } from "zod";
3
- function toJsonSchema(schema, io, unrepresentable = "throw", target = "draft-2020-12") {
4
- return z.toJSONSchema(schema, {
5
- io,
6
- target,
7
- unrepresentable,
8
- cycles: "ref"
9
- });
10
- }
11
- function isObjectNode(value) {
12
- return typeof value === "object" && value !== null && !Array.isArray(value);
13
- }
14
- function jsonSchemaFields(jsonSchema) {
15
- const properties = jsonSchema.properties;
16
- if (!isObjectNode(properties))
17
- return [];
18
- const required = new Set(Array.isArray(jsonSchema.required) ? jsonSchema.required.filter((k) => typeof k === "string") : []);
19
- const fields = [];
20
- for (const [name, raw] of Object.entries(properties)) {
21
- const schema = isObjectNode(raw) ? raw : {};
22
- const description = typeof schema.description === "string" ? schema.description : undefined;
23
- fields.push({
24
- name,
25
- schema,
26
- required: required.has(name),
27
- ...description && { description }
28
- });
29
- }
30
- return fields;
31
- }
32
-
33
- export { toJsonSchema, jsonSchemaFields };
@@ -1,8 +0,0 @@
1
- // src/internal/within-dir.ts
2
- import { sep } from "node:path";
3
- function isWithinDir(root, target) {
4
- const base = root.endsWith(sep) ? root.slice(0, -sep.length) : root;
5
- return target === root || target === base || target.startsWith(base + sep);
6
- }
7
-
8
- export { isWithinDir };