stitchkit 0.54.0 → 0.56.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 (120) hide show
  1. package/README.md +4 -1
  2. package/dist/browser/socket-io.d.ts +25 -7
  3. package/dist/browser/socket-io.d.ts.map +1 -1
  4. package/dist/cli.js +14 -9
  5. package/dist/contract/errors.d.ts +7 -0
  6. package/dist/contract/errors.d.ts.map +1 -1
  7. package/dist/contract/file-ref.d.ts +13 -0
  8. package/dist/contract/file-ref.d.ts.map +1 -0
  9. package/dist/contract/index.d.ts +1 -0
  10. package/dist/contract/index.d.ts.map +1 -1
  11. package/dist/contract/index.js +21 -17
  12. package/dist/files/boundary.d.ts +55 -0
  13. package/dist/files/boundary.d.ts.map +1 -0
  14. package/dist/files.d.ts +3 -0
  15. package/dist/files.d.ts.map +1 -0
  16. package/dist/files.js +16 -0
  17. package/dist/index-1bx83sw4.js +4 -0
  18. package/dist/{index-pfqjb5xy.js → index-28cqssm3.js} +30 -5
  19. package/dist/index-557by2db.js +34 -0
  20. package/dist/index-5r13htq1.js +283 -0
  21. package/dist/index-6k1937bx.js +20 -0
  22. package/dist/index-6y759j86.js +1031 -0
  23. package/dist/{index-bmnhqtya.js → index-7eq9va6e.js} +1 -4
  24. package/dist/index-bfcpjw20.js +363 -0
  25. package/dist/{index-yxpe3phd.js → index-cby4ar3v.js} +1 -18
  26. package/dist/{index-7vb44hy8.js → index-escqg10p.js} +193 -751
  27. package/dist/{index-173tvqcp.js → index-j3dem06f.js} +9 -114
  28. package/dist/{index-gvha32jh.js → index-jcc611vh.js} +2 -2
  29. package/dist/{index-z7761jk2.js → index-n4nfa7gh.js} +16 -22
  30. package/dist/{index-e1es808r.js → index-qh4xevf5.js} +1 -1
  31. package/dist/index-sbdmyz75.js +18 -0
  32. package/dist/{index-b15rtha1.js → index-sm2tjx06.js} +98 -3
  33. package/dist/{index-yez419px.js → index-xy8fmh6w.js} +99 -7
  34. package/dist/index.d.ts +2 -2
  35. package/dist/index.d.ts.map +1 -1
  36. package/dist/index.js +188 -40
  37. package/dist/node.js +31 -27
  38. package/dist/observability/index.js +28 -25
  39. package/dist/react.js +2 -2
  40. package/dist/realtime/contract.d.ts +9 -0
  41. package/dist/realtime/contract.d.ts.map +1 -1
  42. package/dist/realtime/index.d.ts +2 -1
  43. package/dist/realtime/index.d.ts.map +1 -1
  44. package/dist/realtime/request.d.ts +21 -0
  45. package/dist/realtime/request.d.ts.map +1 -0
  46. package/dist/realtime/socket.d.ts +3 -0
  47. package/dist/realtime/socket.d.ts.map +1 -1
  48. package/dist/remote.d.ts +9 -0
  49. package/dist/remote.d.ts.map +1 -0
  50. package/dist/remote.js +79 -0
  51. package/dist/server/context-contribution.d.ts +18 -0
  52. package/dist/server/context-contribution.d.ts.map +1 -0
  53. package/dist/server/context.d.ts.map +1 -1
  54. package/dist/server/index.d.ts +2 -1
  55. package/dist/server/index.d.ts.map +1 -1
  56. package/dist/server/index.js +136 -88
  57. package/dist/server/lifecycle.d.ts +4 -0
  58. package/dist/server/lifecycle.d.ts.map +1 -0
  59. package/dist/server/middleware/auth.d.ts +64 -9
  60. package/dist/server/middleware/auth.d.ts.map +1 -1
  61. package/dist/server/types.d.ts +1 -1
  62. package/dist/server/types.d.ts.map +1 -1
  63. package/dist/testing/surface-conformance.d.ts +180 -0
  64. package/dist/testing/surface-conformance.d.ts.map +1 -0
  65. package/dist/testing/surface-manifest.d.ts +255 -0
  66. package/dist/testing/surface-manifest.d.ts.map +1 -0
  67. package/dist/testing.d.ts +2 -0
  68. package/dist/testing.d.ts.map +1 -1
  69. package/dist/testing.js +714 -3
  70. package/dist/tools/agent.d.ts.map +1 -1
  71. package/dist/tools/async-operation.d.ts +360 -0
  72. package/dist/tools/async-operation.d.ts.map +1 -0
  73. package/dist/tools/cli-args.d.ts +1 -1
  74. package/dist/tools/cli-args.d.ts.map +1 -1
  75. package/dist/tools/cli-format.d.ts +2 -2
  76. package/dist/tools/cli-format.d.ts.map +1 -1
  77. package/dist/tools/cli-wait.d.ts +11 -2
  78. package/dist/tools/cli-wait.d.ts.map +1 -1
  79. package/dist/tools/cli.d.ts +2 -0
  80. package/dist/tools/cli.d.ts.map +1 -1
  81. package/dist/tools/define-download-tool.d.ts +6 -5
  82. package/dist/tools/define-download-tool.d.ts.map +1 -1
  83. package/dist/tools/define-upload-tool.d.ts +3 -1
  84. package/dist/tools/define-upload-tool.d.ts.map +1 -1
  85. package/dist/tools/define-wait-tool.d.ts +1 -1
  86. package/dist/tools/define-wait-tool.d.ts.map +1 -1
  87. package/dist/tools/download-core.d.ts +5 -10
  88. package/dist/tools/download-core.d.ts.map +1 -1
  89. package/dist/tools/internal/surface-projector.d.ts +101 -0
  90. package/dist/tools/internal/surface-projector.d.ts.map +1 -0
  91. package/dist/tools/lifecycle.d.ts +4 -0
  92. package/dist/tools/lifecycle.d.ts.map +1 -0
  93. package/dist/tools/managed-file-error.d.ts +6 -0
  94. package/dist/tools/managed-file-error.d.ts.map +1 -0
  95. package/dist/tools/mcp-prepare.d.ts +3 -21
  96. package/dist/tools/mcp-prepare.d.ts.map +1 -1
  97. package/dist/tools/mcp-round-policy.d.ts +9 -0
  98. package/dist/tools/mcp-round-policy.d.ts.map +1 -0
  99. package/dist/tools/mcp-round.d.ts +0 -4
  100. package/dist/tools/mcp-round.d.ts.map +1 -1
  101. package/dist/tools/mount-download.d.ts +5 -4
  102. package/dist/tools/mount-download.d.ts.map +1 -1
  103. package/dist/tools/mount-upload.d.ts +4 -2
  104. package/dist/tools/mount-upload.d.ts.map +1 -1
  105. package/dist/tools/mount.d.ts +5 -9
  106. package/dist/tools/mount.d.ts.map +1 -1
  107. package/dist/tools/runtime-tool.d.ts.map +1 -1
  108. package/dist/tools/surface.d.ts +7 -0
  109. package/dist/tools/surface.d.ts.map +1 -1
  110. package/dist/tools/upload-core.d.ts +3 -2
  111. package/dist/tools/upload-core.d.ts.map +1 -1
  112. package/dist/tools/view-file.d.ts +4 -3
  113. package/dist/tools/view-file.d.ts.map +1 -1
  114. package/dist/tools/wait-core.d.ts +6 -1
  115. package/dist/tools/wait-core.d.ts.map +1 -1
  116. package/dist/tools.d.ts +3 -2
  117. package/dist/tools.d.ts.map +1 -1
  118. package/dist/tools.js +705 -599
  119. package/llms-full.txt +727 -34
  120. package/package.json +10 -2
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 nine entrypoints. Each is import-safe for one environment —
36
+ stitchkit ships eleven 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 |
@@ -44,12 +44,15 @@ keeping server-only code (`Bun.serve`, the MCP SDK) out of browser bundles.
44
44
  | `stitchkit/node` | server (Node ≥ 22) | `serveNode` + the runtime-agnostic core — the Node mirror of `/server` |
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
+ | `stitchkit/remote` | browser **and** server | peer-free `implementRemote` for thin HTTP proxy processes |
48
+ | `stitchkit/files` | server (Bun or Node) | peer-free managed local-file boundary |
47
49
  | `stitchkit/observability` | server | request/tool event projections — `createObservability`, trace context, sanitisation |
48
50
  | `stitchkit/testing` | tests on Bun or Node | in-process generated clients over a real Fetch handler, without a TCP port |
49
51
  | `stitchkit/react` | browser | `createCursorQuery`, `createCacheBridge` |
50
52
 
51
53
  Rule of thumb: browser code imports `stitchkit` and `stitchkit/react`; server
52
- code adds `stitchkit/server` (or `stitchkit/node` on Node) and `stitchkit/tools`.
54
+ code adds `stitchkit/server` (or `stitchkit/node` on Node) and opts into
55
+ `stitchkit/tools`, `stitchkit/remote` or `stitchkit/files` by capability.
53
56
  The full export list of each is in the [API reference](../api/reference.md).
54
57
 
55
58
  ## Project layout
@@ -2777,7 +2780,8 @@ through the contract's typed client:
2777
2780
 
2778
2781
  ```ts
2779
2782
  import { createHttpClient } from 'stitchkit'
2780
- import { createStdioMcpServer, implementRemote } from 'stitchkit/tools'
2783
+ import { implementRemote } from 'stitchkit/remote'
2784
+ import { createStdioMcpServer } from 'stitchkit/tools'
2781
2785
 
2782
2786
  const http = createHttpClient({
2783
2787
  baseUrl: 'https://api.example.com',
@@ -3017,8 +3021,17 @@ import {
3017
3021
  defineViewFileTool,
3018
3022
  defineWaitTool,
3019
3023
  } from 'stitchkit/tools'
3024
+ import { createManagedFileBoundary } from 'stitchkit/files'
3020
3025
  import { z } from 'zod'
3021
3026
 
3027
+ const files = await createManagedFileBoundary({
3028
+ root: '/srv/job-media',
3029
+ createRoot: true, // creates only this final directory; /srv must already be trusted
3030
+ inspectionTimeoutMs: 15_000,
3031
+ inspect: ({ prefix, name, declaredMediaType, signal }) =>
3032
+ inspectFile(prefix, { name, declaredMediaType, signal }),
3033
+ })
3034
+
3022
3035
  const waitForJob = defineWaitTool({
3023
3036
  name: 'wait_for_job',
3024
3037
  description: 'Wait for a job to finish',
@@ -3035,10 +3048,10 @@ const downloadResult = defineDownloadTool({
3035
3048
  name: 'download_result',
3036
3049
  description: 'Download a completed result',
3037
3050
  identity: { serviceName: 'jobs', action: 'downloadResult', scope: 'user' },
3038
- input: z.object({ id: z.string(), dir: z.string().optional() }),
3051
+ input: z.object({ id: z.string() }),
3039
3052
  resolveUrl: ({ id }) => resolveResultUrl(id),
3040
- defaultDir: './downloads',
3041
- dirFromInput: ({ dir }) => dir,
3053
+ files,
3054
+ pathFromInput: ({ id }) => `results/${id}.bin`,
3042
3055
  })
3043
3056
 
3044
3057
  const uploadInput = defineUploadTool({
@@ -3046,16 +3059,17 @@ const uploadInput = defineUploadTool({
3046
3059
  description: 'Upload a local input file',
3047
3060
  identity: { serviceName: 'jobs', action: 'uploadInput', scope: 'user' },
3048
3061
  output: z.object({ url: z.url() }),
3049
- upload: (path, context) => uploadFile(path, context.signal),
3062
+ files,
3063
+ upload: (source, context) => uploadBytes(source.bytes, context.signal),
3050
3064
  })
3051
3065
 
3052
3066
  const viewResult = defineViewFileTool({
3053
3067
  name: 'view_result',
3054
3068
  description: 'Inspect generated media',
3055
3069
  identity: { serviceName: 'jobs', action: 'viewResult', scope: 'user' },
3056
- // Local files are disabled when baseDir is omitted. URL fetches retain SSRF
3070
+ // Local files are disabled when files is omitted. URL fetches retain SSRF
3057
3071
  // protection; use allowPrivateHosts only for an explicitly trusted network.
3058
- baseDir: '/srv/job-media',
3072
+ files,
3059
3073
  })
3060
3074
 
3061
3075
  const runtimeTools = [waitForJob, downloadResult, uploadInput, viewResult]
@@ -3078,6 +3092,142 @@ same neutral mechanics but do not gain lifecycle or hooks. The raw view-file
3078
3092
  adapter preserves its content-only MCP envelope while using the same bounded
3079
3093
  batch operation.
3080
3094
 
3095
+ ### Async-operation protocol
3096
+
3097
+ For a complete long-running operation, define the linked runtime-only surface
3098
+ once. Stitchkit owns transport names, validation and wait mechanics; the
3099
+ application still owns storage, execution, retries and domain states:
3100
+
3101
+ ```ts
3102
+ import {
3103
+ createAsyncOperationSnapshotSchema,
3104
+ defineAsyncOperation,
3105
+ } from 'stitchkit/tools'
3106
+
3107
+ const id = z.object({ id: z.string() })
3108
+ const state = z.object({
3109
+ phase: z.enum(['pending', 'running', 'succeeded']),
3110
+ value: z.string().optional(),
3111
+ })
3112
+ const snapshot = createAsyncOperationSnapshotSchema({
3113
+ failure: z.object({ code: z.string() }),
3114
+ progress: z.object({ current: z.number(), total: z.number() }),
3115
+ })
3116
+
3117
+ const exportOperation = defineAsyncOperation({
3118
+ mode: 'runtime-only',
3119
+ name: 'export',
3120
+ description: 'Export project data',
3121
+ identity: { serviceName: 'exports', action: 'export', scope: 'user' },
3122
+ startInput: z.object({ projectId: z.string() }),
3123
+ id,
3124
+ state,
3125
+ snapshot,
3126
+ start: ({ projectId }) => enqueueExport(projectId),
3127
+ authorize: ({ id }, capability, ctx) => authorizeExport(id, capability, ctx),
3128
+ inspect: ({ id }, ctx) => readExport(id, ctx.signal),
3129
+ classify: (job) => ({ phase: job.phase }),
3130
+ result: {
3131
+ output: z.object({ value: z.string() }),
3132
+ handler: (job) => ({ value: job.value ?? '' }),
3133
+ },
3134
+ })
3135
+
3136
+ const runtimeTools = exportOperation.runtimeTools
3137
+ ```
3138
+
3139
+ Every follow-up repeats `authorize`; an opaque id is never authority. Aborting
3140
+ `wait` only stops waiting and never calls optional domain `cancel`.
3141
+
3142
+ For a new HTTP surface, derive the canonical capability contract once. The
3143
+ factory defines schemas and routes only; the application still supplies normal
3144
+ handlers to `implement` and owns the job:
3145
+
3146
+ ```ts
3147
+ import { defineAsyncOperationContract } from 'stitchkit/tools'
3148
+ import { z } from 'zod'
3149
+
3150
+ const operationId = z.object({ id: z.string() })
3151
+ const operationSnapshot = z.object({
3152
+ phase: z.enum(['pending', 'running', 'succeeded', 'failed', 'cancelled']),
3153
+ })
3154
+
3155
+ const operations = defineAsyncOperationContract({
3156
+ prefix: 'exports',
3157
+ scope: 'user',
3158
+ description: 'Export project data',
3159
+ startInput: z.object({ projectId: z.string() }),
3160
+ id: operationId,
3161
+ snapshot: operationSnapshot,
3162
+ cancel: true,
3163
+ result: z.object({ downloadUrl: z.url() }),
3164
+ })
3165
+
3166
+ // Pass operations.contract and ordinary handlers to implement(...).
3167
+ // operations.capabilities/schemas/adapters are the canonical protocol metadata.
3168
+ ```
3169
+
3170
+ If an existing dedicated contract has a different wire shape — for example
3171
+ `start` returns a snapshot while follow-ups accept separate envelopes — bind it
3172
+ without casts or duplicate endpoints. Inline callbacks are contextually typed;
3173
+ every returned id/input is parsed by the destination schema:
3174
+
3175
+ ```ts
3176
+ import { defineContract } from 'stitchkit/contract'
3177
+ import { bindContractAsyncOperation } from 'stitchkit/tools'
3178
+
3179
+ const startOutput = z.object({ operation: operationId, acceptedAt: z.string() })
3180
+ const statusInput = z.object({ operationId: z.string() })
3181
+ const waitInput = z.object({ lookup: operationId })
3182
+
3183
+ const existing = defineContract({ prefix: 'exports' }, {
3184
+ start: {
3185
+ method: 'POST', path: '/', desc: 'Start export',
3186
+ input: z.object({ projectId: z.string() }), output: startOutput,
3187
+ },
3188
+ status: {
3189
+ method: 'POST', path: '/status', desc: 'Read export status',
3190
+ input: statusInput, output: operationSnapshot,
3191
+ },
3192
+ wait: {
3193
+ method: 'POST', path: '/wait', desc: 'Wait for export',
3194
+ input: waitInput, output: operationSnapshot,
3195
+ },
3196
+ })
3197
+
3198
+ const operationHandlers = {
3199
+ start: () => ({ operation: { id: 'example' }, acceptedAt: new Date().toISOString() }),
3200
+ status: (): z.output<typeof operationSnapshot> => ({ phase: 'pending' }),
3201
+ wait: (): z.output<typeof operationSnapshot> => ({ phase: 'succeeded' }),
3202
+ }
3203
+
3204
+ const bound = bindContractAsyncOperation({
3205
+ mode: 'contract-backed',
3206
+ binding: 'adapted',
3207
+ contract: existing,
3208
+ id: operationId,
3209
+ capabilities: { start: 'start', status: 'status', wait: 'wait' },
3210
+ adapters: {
3211
+ idFromStart: (output) => output.operation,
3212
+ inputFor: {
3213
+ status: (id) => ({ operationId: id.id }),
3214
+ wait: (id) => ({ lookup: id }),
3215
+ },
3216
+ },
3217
+ handlers: operationHandlers,
3218
+ })
3219
+ ```
3220
+
3221
+ Direct binding remains the short form when start already returns the id and
3222
+ every follow-up reuses that exact schema instance. Its ID schema must be
3223
+ wire-stable: input and output types match and parsing has no transform,
3224
+ coercion, default or overwrite. This also excludes same-type transforms because
3225
+ the direct adapters would parse an already parsed ID again. Use adapted binding
3226
+ and explicitly project the parsed ID back into each follow-up input. In both
3227
+ modes the binder creates no router;
3228
+ it returns the existing contract, handlers and protocol metadata for
3229
+ application composition.
3230
+
3081
3231
  ### Explicit raw SDK registration
3082
3232
 
3083
3233
  `rawTools` is deliberately named as an escape hatch. A tool registered there
@@ -3093,7 +3243,7 @@ const handleMcp = createMcpHandler({
3093
3243
  serverInfo: { name: 'my-app', version: '1.0.0' },
3094
3244
  auth,
3095
3245
  services: [service],
3096
- rawTools: (server) => mountViewFile(server, { baseDir: '/srv/uploads' }),
3246
+ rawTools: (server) => mountViewFile(server, { files }),
3097
3247
  })
3098
3248
  ```
3099
3249
 
@@ -3234,6 +3384,12 @@ await createCli({
3234
3384
  })
3235
3385
  ```
3236
3386
 
3387
+ `createCli({ signal })` and its wait loop honor an explicit caller
3388
+ `AbortSignal`. Stitchkit does not install a process-global SIGINT handler for
3389
+ ordinary CLI programs: if desired, the application binds SIGINT to an
3390
+ `AbortController` and passes its signal. Aborting wait stops polling only; it
3391
+ does not cancel the underlying job.
3392
+
3237
3393
  ```json
3238
3394
  // package.json
3239
3395
  { "bin": { "myapp": "./dist/cli.js" } }
@@ -3347,7 +3503,7 @@ same Zod schema an HTTP or MCP call does.
3347
3503
 
3348
3504
  | Flag | Effect |
3349
3505
  | --------------------- | ---------------------------------------------------------- |
3350
- | `--json` | Raw JSON on stdout for piping / scripts |
3506
+ | `--json` | Compact success/error JSON records for scripts |
3351
3507
  | `--wait` | Block-poll an async result to a terminal state |
3352
3508
  | `--wait-timeout <s>` | Override the `--wait` timeout |
3353
3509
  | `--output-dir <dir>` | Download result media into a directory |
@@ -3355,10 +3511,18 @@ same Zod schema an HTTP or MCP call does.
3355
3511
  | `--dry-run` | Print the resolved call without executing |
3356
3512
  | `--help`, `-h` | Usage — top-level or per-command flag table |
3357
3513
 
3358
- stdout carries the result; errors and progress go to stderr, so `--json` stays
3359
- pipeable and `2>/dev/null` stays clean. The process exit code carries the error
3360
- class (`0` ok, `VALIDATION_ERROR 1`, `UNAUTHORIZED 2`, `FORBIDDEN → 3`,
3361
- `NOT_FOUND 4`, …) override per app with `exitCodes`.
3514
+ stdout carries the result; structured errors and progress go to stderr. With
3515
+ `--json`, a success or structured failure is exactly one compact,
3516
+ newline-terminated JSON record on its respective stream; progress and CLI usage
3517
+ diagnostics remain ordinary stderr text. This keeps stdout pipeable and
3518
+ `2>/dev/null` clean. The process exit code carries the error class (`0` ok,
3519
+ `VALIDATION_ERROR → 1`, `UNAUTHORIZED → 2`, `FORBIDDEN → 3`, `NOT_FOUND → 4`,
3520
+ …) — override per app with `exitCodes`.
3521
+
3522
+ Per-command help derives the positional form from the same schema order as the
3523
+ argv parser. For example, a required `action` and optional `profile` render as
3524
+ `Usage: myapp skill <action> [profile] [--flags]`; the argument table also shows
3525
+ `<action> | --action` and `[profile] | --profile`. Boolean fields remain flags.
3362
3526
 
3363
3527
  ## `--wait` — background-friendly generation
3364
3528
 
@@ -3375,12 +3539,19 @@ await createCli({
3375
3539
  generate: {
3376
3540
  tool: 'get_generation',
3377
3541
  poll: (r) => (isRecord(r) && typeof r.id === 'string' ? { id: r.id } : null),
3378
- done: (r) => isRecord(r) && r.status === 'COMPLETED',
3542
+ done: (r) => isRecord(r) && ['COMPLETED', 'FAILED'].includes(String(r.status)),
3543
+ failed: (r) => isRecord(r) && r.status === 'FAILED',
3379
3544
  },
3380
3545
  },
3381
3546
  })
3382
3547
  ```
3383
3548
 
3549
+ `failed` is optional. When it matches either the initial result or a later poll,
3550
+ polling stops and the CLI emits `WAIT_FAILED` on stderr with a non-zero exit;
3551
+ the terminal payload is retained under `details.result`. `failed` is checked
3552
+ before `done`, so overlapping predicates fail closed. A failed poll tool call
3553
+ keeps its own error code, and an elapsed deadline remains `TIMEOUT`.
3554
+
3384
3555
  ```bash
3385
3556
  # foreground
3386
3557
  myapp generate "a fox" --wait --output-dir ./out
@@ -3486,6 +3657,57 @@ runtime. A malformed inbound tuple never reaches the application handler;
3486
3657
  invalid outbound data throws before Socket.IO publishes it. Rejections call the
3487
3658
  optional `onRejected` hook with event, direction, phase and the Zod error.
3488
3659
 
3660
+ ### Protocol generations
3661
+
3662
+ For a distributed producer/consumer pair, put a literal generation first in the
3663
+ first payload object. Zod validates object fields in declaration order, so an
3664
+ incompatible peer is identified before the rest of its payload is interpreted:
3665
+
3666
+ ```ts
3667
+ const ReplicationMessage = z.object({
3668
+ v: z.literal(2),
3669
+ item: ReplicatedItem,
3670
+ })
3671
+
3672
+ const realtimeContract = defineRealtimeContract({
3673
+ serverToClient: {
3674
+ replicated: { args: z.tuple([ReplicationMessage]) },
3675
+ },
3676
+ clientToServer: {},
3677
+ })
3678
+ ```
3679
+
3680
+ The original `z.ZodError` is retained as `RealtimeRejectedEvent.error.cause`.
3681
+ Because event arguments are a tuple, the first payload's `v` path is
3682
+ `[0, 'v']` (not `['v']`):
3683
+
3684
+ ```ts
3685
+ import type { RealtimeRejectedEvent } from 'stitchkit'
3686
+ import { z } from 'zod'
3687
+
3688
+ function isProtocolGenerationMismatch(rejected: RealtimeRejectedEvent): boolean {
3689
+ const cause = rejected.error.cause
3690
+ if (!(cause instanceof z.ZodError)) return false
3691
+ const first = cause.issues[0]
3692
+ return first?.code === 'invalid_value'
3693
+ && first.path.length === 2
3694
+ && first.path[0] === 0
3695
+ && first.path[1] === 'v'
3696
+ }
3697
+
3698
+ const socket = createRealtimeClient(realtimeContract, {
3699
+ url,
3700
+ onRejected: (rejected) => {
3701
+ if (isProtocolGenerationMismatch(rejected)) schedulePeerUpgrade()
3702
+ else reportMalformedRealtimePayload(rejected)
3703
+ },
3704
+ })
3705
+ ```
3706
+
3707
+ A generation mismatch means “upgrade the peer”; another schema rejection means
3708
+ “fix the producer or payload”. This stays an application convention, not a core
3709
+ API: only the application knows which field denotes protocol compatibility.
3710
+
3489
3711
  ## Server — `createSocketIOServer`
3490
3712
 
3491
3713
  ```ts
@@ -3584,11 +3806,59 @@ socket.on('note:created', (note) => { /* typed note */ })
3584
3806
  socket.emit('room:join', 'general', ({ joined }) => { /* typed + validated */ })
3585
3807
  ```
3586
3808
 
3809
+ When an application already owns the low-level Stitchkit transport, bind the
3810
+ contract without opening a second connection:
3811
+
3812
+ ```ts
3813
+ import { bindRealtimeClient, createSocketIOClient } from 'stitchkit'
3814
+
3815
+ const transport = createSocketIOClient({ url: 'https://api.example.com' })
3816
+ const events = bindRealtimeClient(realtimeContract, transport, { onRejected })
3817
+
3818
+ transport.connect() // lifecycle stays with the transport owner
3819
+ events.on('note:created', handleNote)
3820
+ await events.request('room:join', 'general', { timeoutMs: 5_000 })
3821
+ ```
3822
+
3823
+ The bound handle intentionally has no `connect()` or `disconnect()`. Its
3824
+ `on`/`emit`/`request`, rejection and timeout semantics are exactly the path used
3825
+ by `createRealtimeClient`; only transport construction/lifecycle differs.
3826
+
3827
+ ### Request-response over realtime
3828
+
3829
+ For an event with an `ack` schema, `request()` is the Promise form of the same
3830
+ native Socket.IO acknowledgement. Arguments and the resolved value are inferred
3831
+ from the contract and validated on both sides:
3832
+
3833
+ ```ts
3834
+ const result = await socket.request(
3835
+ 'room:join',
3836
+ 'general',
3837
+ { timeoutMs: 5_000 },
3838
+ )
3839
+ // result: { joined: boolean }
3840
+ ```
3841
+
3842
+ An event without `ack` cannot be passed to `request()` by type. A disconnected
3843
+ client rejects immediately with `RealtimeRequestDisconnectedError`; an in-flight
3844
+ disconnect uses the same error; an elapsed native acknowledgement timeout uses
3845
+ `RealtimeRequestTimeoutError`. These are distinct stable classes and codes, so
3846
+ never parse Socket.IO error text. An invalid acknowledgement still fires the
3847
+ existing `onRejected` hook with phase `acknowledgement`, then rejects with
3848
+ `RealtimeRequestInvalidAcknowledgementError`.
3849
+
3850
+ `timeoutMs` must be finite and greater than zero. Use acknowledgements only for
3851
+ bounded request-response work. A job that runs for minutes, progress streaming,
3852
+ or resumable delivery should use separate correlated events or the async
3853
+ operation protocol; keeping one acknowledgement open is not a durable RPC/job
3854
+ transport.
3855
+
3587
3856
  ## Low-level transport
3588
3857
 
3589
3858
  `createSocketIOClient` remains the low-level Socket.IO transport wrapper for
3590
3859
  schema-agnostic infrastructure. Application wire events should use
3591
3860
  `createRealtimeClient`; it adds the shared contract without replacing Socket.IO.
3861
+ Use `bindRealtimeClient` when that low-level transport already exists.
3592
3862
 
3593
3863
  ### Durable subscriptions
3594
3864
 
@@ -4068,10 +4338,11 @@ The value of each `rules` entry, keyed by scope:
4068
4338
 
4069
4339
  - **`'public'`** — always passes; the identity is attached if present.
4070
4340
  - **`'authenticated'`** — any resolved identity passes; no identity ⇒ 401.
4071
- - **a function** `(identity, ctx) => boolean | Promise<boolean>` — a custom
4341
+ - **a function** `(identity, ctx) => boolean | contribution`, sync or async — a custom
4072
4342
  check. It receives request metadata and validated path params, so a
4073
4343
  resource-scoped rule can do a DB lookup. It cannot read `input` or files: the
4074
- body has deliberately not been consumed yet. May be async.
4344
+ body has deliberately not been consumed yet. `false` denies, `true` passes,
4345
+ and a returned plain object passes and contributes inferred handler fields.
4075
4346
 
4076
4347
  #### Resource-scoped rule — reading a path/prefix param
4077
4348
 
@@ -4101,6 +4372,30 @@ identity *and* any derived values (role, parent-id) on `ctx` for handlers. Read
4101
4372
  them back as `ctx.tenantRole` (typed `unknown` — narrow at the read site). The
4102
4373
  endpoints under the group declare `scope: 'tenant'`.
4103
4374
 
4375
+ When the async authorization lookup already produced handler data, return it
4376
+ instead of repeating the lookup in `inject` and a handwritten scope map:
4377
+
4378
+ ```ts
4379
+ const auth = createAuthHook({
4380
+ resolve: sessionResolver,
4381
+ rules: {
4382
+ project: async (user, ctx) => {
4383
+ const membership = await findMembership(user.id, String(ctx.projectId))
4384
+ return membership
4385
+ ? { userId: user.id, projectId: membership.projectId, role: membership.role }
4386
+ : false
4387
+ },
4388
+ },
4389
+ })
4390
+
4391
+ const implementFor = createScopedImplement<AuthScopes<typeof auth>>()
4392
+ ```
4393
+
4394
+ Contributions are merged only after full validation. Runtime-owned context keys,
4395
+ arrays, class instances, accessors, symbols, unsafe prototypes and partial proxy
4396
+ reads fail without mutating context. `false | object` contributes required
4397
+ fields; `true | object` correctly makes them optional.
4398
+
4104
4399
  ### `AuthHookConfig`
4105
4400
 
4106
4401
  | Field | Purpose |
@@ -4116,10 +4411,10 @@ endpoints under the group declare `scope: 'tenant'`.
4116
4411
  Annotate `rules` with `satisfies Record<MyScope, AuthRule<User>>` so the compiler
4117
4412
  catches a scope you forgot to cover. With the scoped rule objects below, widen
4118
4413
  the annotation to
4119
- `satisfies Record<MyScope, AuthRule<User> | ScopedAuthRule<User, object>>` — it
4120
- keeps the coverage check and does not disturb the derivation. (When every scope
4121
- comes from the derived map, the check is already implicit: a scope missing from
4122
- `rules` is no key of the map, and a contract using it fails to compile.)
4414
+ `satisfies Record<MyScope, AuthRule<User> | ScopedAuthRule<User, AuthRuleContribution>>`
4415
+ — it keeps the coverage check and does not disturb the derivation. (When every
4416
+ scope comes from the derived map, the check is already implicit: a scope missing
4417
+ from `rules` is no key of the map, and a contract using it fails to compile.)
4123
4418
 
4124
4419
  ### Scoped rules — the map `createScopedImplement` consumes
4125
4420
 
@@ -4165,11 +4460,58 @@ widens its inject and degrades that scope's fields to `object`.
4165
4460
 
4166
4461
  Derivation needs the identity generic to be **inferred** (write no explicit
4167
4462
  `createAuthHook<User>` — TypeScript has no partial inference); with an explicit
4168
- generic, or fields injected outside the hook, keep the hand-written map. Two
4169
- hooks guarding different scopes compose by intersection:
4170
- `createScopedImplement<AuthScopes<typeof userHook> & AuthScopes<typeof botHook>>()`.
4463
+ generic, or fields injected outside the hook, keep the hand-written map.
4171
4464
  → ADR 0078
4172
4465
 
4466
+ ### Multiple auth domains — `composeAuthHooks`
4467
+
4468
+ Independent identity domains should keep separate resolvers and rules. Compose
4469
+ their canonical hooks and derive one handler context without a manual dispatcher
4470
+ or scope-map intersection:
4471
+
4472
+ ```ts
4473
+ const userAuth = createAuthHook({
4474
+ resolve: resolveUser,
4475
+ resolveFromContext: resolveToolUser,
4476
+ rules: {
4477
+ user: (user) => ({ userId: user.id }),
4478
+ workspace: (user) => ({ userId: user.id }),
4479
+ },
4480
+ })
4481
+
4482
+ const workspaceAuth = createAuthHook({
4483
+ resolve: resolveMembership,
4484
+ resolveFromContext: resolveToolMembership,
4485
+ rules: {
4486
+ workspace: (membership) => ({ workspaceId: membership.workspaceId }),
4487
+ },
4488
+ })
4489
+
4490
+ export const auth = composeAuthHooks({
4491
+ hooks: [userAuth, workspaceAuth],
4492
+ defaultScope: 'user',
4493
+ })
4494
+ export const implementFor = createScopedImplement<AuthScopes<typeof auth>>()
4495
+ ```
4496
+
4497
+ Only hooks that declare the selected scope run. An unknown scope fails closed;
4498
+ when several hooks own one scope, every owner must pass in declaration order.
4499
+ Each owner evaluates on an isolated shadow context. Stitchkit validates all
4500
+ changed fields, rejects reserved/unsafe fields and cross-owner key collisions,
4501
+ then commits the combined contribution once. A rejected or cancelled composite
4502
+ therefore exposes no partial fields to the handler.
4503
+
4504
+ Isolation is per-key, not deep: a contribution is the set of context keys whose
4505
+ descriptor changed. Mutating an existing value **in place**
4506
+ (`ctx.user.role = 'admin'`) edits the object every owner already shares, so it
4507
+ is neither reported as a contribution nor checked for collisions — contribute
4508
+ new fields instead of editing another owner's object. External side effects
4509
+ likewise remain consumer-owned and are not rolled back.
4510
+
4511
+ The composite's `defaultScope` is the only implicit scope. A child hook's own
4512
+ default must be absent or equal to it, so reordering hooks cannot change which
4513
+ scope protects an endpoint.
4514
+
4173
4515
  ### Auth on the tool surface — `resolveFromContext`
4174
4516
 
4175
4517
  The same hook guards every transport, but the lifecycle slot and identity
@@ -4968,6 +5310,25 @@ observe. (This is also why it lives on `ToolCallHooks` rather than being an
4968
5310
  `Response`, which a tool call has no use for, and a whole `createServer` hooks
4969
5311
  object must stay assignable to `ToolLifecycle`.)
4970
5312
 
5313
+ ### Compose lifecycle policy explicitly
5314
+
5315
+ When auth, a feature policy and audit identity are independent modules, compose
5316
+ their existing hooks instead of forwarding each phase by hand:
5317
+
5318
+ ```ts
5319
+ import { composeLifecycleHooks } from 'stitchkit/server'
5320
+ import { composeToolLifecycle } from 'stitchkit/tools'
5321
+
5322
+ const httpLifecycle = composeLifecycleHooks(authLifecycle, featurePolicy, auditIdentity)
5323
+ const toolLifecycle = composeToolLifecycle(authHook, featureToolPolicy, auditToolIdentity)
5324
+ ```
5325
+
5326
+ Phases run left to right. `onRequest` stops on the first `Response`; `onError`
5327
+ falls through until a hook handles it; after transforms feed the current value
5328
+ forward and preserve it when a hook returns `undefined`. Throws remain owned by
5329
+ the existing outer error boundary, and the original context/signal identity is
5330
+ never replaced.
5331
+
4971
5332
  **Do not reach for `setRequestError` here.** It writes to the *request* context,
4972
5333
  which the built-in **tool** row does not read: a tool event takes
4973
5334
  `errorCode` / `errorMessage` / `errorDetail` from the `ToolResult`, and only
@@ -5083,6 +5444,101 @@ contract registry. Both helpers are Fetch-only: they construct ordinary
5083
5444
  absolute `Request` objects and call `createHandler` directly, so Bun and Node
5084
5445
  exercise the same framework pipeline.
5085
5446
 
5447
+ ### Transport conformance
5448
+
5449
+ `buildSurfaceManifest` v2 snapshots canonical operations separately from actual
5450
+ HTTP topology and mounted MCP/Agent/CLI projections. Named MCP surfaces keep
5451
+ their own advertised input digests (including `extend`, flattening and schema
5452
+ policy), and named realtime contracts keep directional argument/ack input and
5453
+ output digests. Compare only discovery you actually observed, then run explicit
5454
+ drivers for the transports you provide:
5455
+
5456
+ ```ts
5457
+ import { bindRealtimeClient } from 'stitchkit'
5458
+ import {
5459
+ assertSurfaceDiscovery,
5460
+ buildSurfaceManifest,
5461
+ createRealtimeProbeDriver,
5462
+ defineRealtimeProbe,
5463
+ runSurfaceProbes,
5464
+ } from 'stitchkit/testing'
5465
+
5466
+ const manifest = buildSurfaceManifest({
5467
+ groups: [{ pathPrefix: '/api', services: httpServices }],
5468
+ mcpPreparation: { extend, schemaValidation, multiRound },
5469
+ mcpSurfaces: {
5470
+ member: { services: memberServices, runtimeTools },
5471
+ admin: { services: adminServices, runtimeTools },
5472
+ },
5473
+ toolSurfaces: {
5474
+ AGENT: { services: agentServices, runtimeTools },
5475
+ CLI: { services: cliServices, runtimeTools: cliRuntimeTools },
5476
+ },
5477
+ realtime: { primary: realtimeContract },
5478
+ cliCommands,
5479
+ })
5480
+ assertSurfaceDiscovery(manifest, {
5481
+ openApi,
5482
+ toolSurfaces: [{
5483
+ transport: 'MCP',
5484
+ surface: 'member',
5485
+ names: (await mcpClient.listTools()).tools.map((tool) => tool.name),
5486
+ }],
5487
+ AGENT: Object.keys(agentTools),
5488
+ CLI: cliHelpNames,
5489
+ realtime: {
5490
+ primary: { serverToClient: observedServerEvents, clientToServer: observedClientEvents },
5491
+ },
5492
+ })
5493
+
5494
+ const invalidInbound = defineRealtimeProbe({
5495
+ name: 'invalid inbound payload',
5496
+ scenario: 'invalid_arguments',
5497
+ fixture: invalidPayloadFixture,
5498
+ expected: {
5499
+ outcome: 'realtime_rejected',
5500
+ code: 'REALTIME_CONTRACT_VIOLATION',
5501
+ rejection: {
5502
+ direction: 'server-inbound', phase: 'arguments',
5503
+ reason: 'invalid-arguments', fault: 'peer',
5504
+ },
5505
+ handlerCalls: 0,
5506
+ },
5507
+ })
5508
+
5509
+ const realtimeDriver = createRealtimeProbeDriver({
5510
+ bind: (onRejected, fixture) => {
5511
+ const client = bindRealtimeClient(realtimeContract, existingTransport, { onRejected })
5512
+ const scenario = bindApplicationRealtimeScenario(client, fixture)
5513
+ return {
5514
+ connected: () => client.connected,
5515
+ invoke: scenario.invoke,
5516
+ dispose: scenario.dispose, // subscriptions only; never disconnect the transport
5517
+ }
5518
+ },
5519
+ handlerCalls: () => applicationRealtimeHandlerCalls,
5520
+ })
5521
+
5522
+ await runSurfaceProbes({
5523
+ probes: [invalidInbound],
5524
+ drivers: { REALTIME: realtimeDriver },
5525
+ })
5526
+ ```
5527
+
5528
+ MCP preparation is global because the real MCP mount has one preparation
5529
+ policy; named surfaces select tools, while `extend.filter` decides which
5530
+ selected operations receive extra fields. CLI selection is deliberately plain,
5531
+ and Agent owns its own reachable presentation shaping.
5532
+
5533
+ The kit never starts a server, discovers Socket.IO topology remotely, invents
5534
+ credentials or synthesises invalid Zod values. Fixtures and observations come
5535
+ from the application. The realtime driver creates a rejection channel per
5536
+ scenario, observes connection state before invocation, and disposes only
5537
+ probe-owned subscriptions. Each scenario has one absolute deadline shared by
5538
+ signalled setup, invocation and teardown. An outer timeout stops waiting; it
5539
+ does not disconnect a foreign transport or retract an already emitted packet.
5540
+ A missing driver is unsupported, not silently marked conformant.
5541
+
5086
5542
  ### Test handlers with raw Requests
5087
5543
 
5088
5544
  `createHandler` is the router as a plain `(req) => Promise<Response>` function —
@@ -5642,6 +6098,133 @@ current one *up to* your target, and apply each snippet.
5642
6098
  runtime): bootstrap the server, one HTTP request, and any feature you rely on
5643
6099
  (Socket.IO connect, an MCP tool call, a multipart upload, …).
5644
6100
 
6101
+ ## Released migration: 0.56.0
6102
+
6103
+ ### Surface manifests are version 2
6104
+
6105
+ `operation.tools` could not describe a role-selected MCP surface, a different
6106
+ Agent or CLI selection, or an advertised `extend` schema, so projections moved
6107
+ out of the canonical operation:
6108
+
6109
+ ```ts
6110
+ // before
6111
+ manifest.operations[0].tools.MCP
6112
+ // after
6113
+ manifest.toolSurfaces.find((s) => s.transport === 'MCP' && s.surface === null)?.tools
6114
+
6115
+ // before
6116
+ buildSurfaceManifest({ mcpSurfaces: { admin: { services, extend } } })
6117
+ // after
6118
+ buildSurfaceManifest({ mcpSurfaces: { admin: { services } }, mcpPreparation: { extend } })
6119
+ ```
6120
+
6121
+ A committed snapshot is regenerated once, deliberately: `manifestVersion` is
6122
+ `2`, and `ConformanceTransport` gained `REALTIME`, so any exhaustive
6123
+ `Record<ConformanceTransport, …>` must handle it.
6124
+
6125
+ ### `FILE_*` codes joined the error registry
6126
+
6127
+ `StitchErrorCode` gained `FILE_INVALID_PATH`, `FILE_OUTSIDE_ROOT`,
6128
+ `FILE_NOT_FOUND`, `FILE_NOT_REGULAR`, `FILE_INSPECTION_REJECTED`,
6129
+ `FILE_TOO_LARGE` and `FILE_EXISTS`. Only exhaustive maps break:
6130
+
6131
+ ```ts
6132
+ // before — compiled while the registry had no file codes
6133
+ const copy: Record<StitchErrorCode, string> = { …, RATE_LIMITED: '…' }
6134
+ // after — add the seven managed-file codes (or use Partial<Record<…>>)
6135
+ ```
6136
+
6137
+ Unexpected IO stays scrubbed as `INTERNAL_SERVER_ERROR`: the new codes are the
6138
+ caller-safe ones only.
6139
+
6140
+ ### `ScopedAuthHook` is nominal
6141
+
6142
+ A hand-written function shaped like an auth hook is no longer assignable —
6143
+ identity now comes from the factory, so scope ownership and the inferred
6144
+ context cannot drift apart:
6145
+
6146
+ ```ts
6147
+ // before — a structural stand-in
6148
+ const auth: ScopedAuthHook<Scopes> = async (ctx, endpoint) => { … }
6149
+ // after — create it, then compose domains
6150
+ const auth = createAuthHook({ resolve, rules })
6151
+ const composed = composeAuthHooks({ hooks: [auth, billingAuth], defaultScope: 'public' })
6152
+ ```
6153
+
6154
+ ### Managed-file inspectors also run on reads
6155
+
6156
+ An inspector is no longer write-only, and it has a finite default deadline
6157
+ (15 s). Make it read-aware and idempotent — a read carries no
6158
+ `declaredMediaType`:
6159
+
6160
+ ```ts
6161
+ // before
6162
+ inspect: ({ declaredMediaType }) => inspectDeclaredType(declaredMediaType!)
6163
+ // after
6164
+ inspect: ({ prefix, declaredMediaType, signal }) =>
6165
+ inspectBytes(prefix, { declaredMediaType, signal })
6166
+ ```
6167
+
6168
+ Set `inspectionTimeoutMs` explicitly when 15 seconds is the wrong budget.
6169
+
6170
+ ### Direct async-operation binding needs a wire-stable ID
6171
+
6172
+ A direct binding reuses the start output as the follow-up wire input, so the ID
6173
+ schema must parse to itself (`z.input` equals `z.output`, no transform,
6174
+ coercion, default or overwrite). Anything else is parsed twice:
6175
+
6176
+ ```ts
6177
+ // before — a transform silently ran on start and again on every follow-up
6178
+ defineAsyncOperationContract({ binding: 'direct', id: z.string().transform(Number) })
6179
+ // after — keep the wire shape, adapt explicitly
6180
+ defineAsyncOperationContract({
6181
+ binding: 'adapted',
6182
+ id,
6183
+ adapters: { idFromStart, inputFor },
6184
+ })
6185
+ ```
6186
+
6187
+ ## Released migration: 0.55.0
6188
+
6189
+ ### Peer-free `implementRemote`
6190
+
6191
+ `implementRemote` now has one canonical, optional-peer-free owner. This keeps
6192
+ MCP SDK and AI SDK modules out of CLI bundles that only proxy HTTP calls:
6193
+
6194
+ ```ts
6195
+ // before
6196
+ import { implementRemote } from 'stitchkit/tools'
6197
+ // after
6198
+ import { implementRemote } from 'stitchkit/remote'
6199
+ ```
6200
+
6201
+ ### Managed file boundary and strict auth returns
6202
+
6203
+ Create one boundary during application bootstrap and pass the capability, never
6204
+ a per-call directory or host path:
6205
+
6206
+ ```ts
6207
+ import { createManagedFileBoundary } from 'stitchkit/files'
6208
+
6209
+ const files = await createManagedFileBoundary({ root: '/srv/app-files' })
6210
+
6211
+ // before
6212
+ defineDownloadTool({ defaultDir: '/srv/app-files', resolveUrl, ...common })
6213
+ defineUploadTool({ upload: (path) => provider.uploadFile(path), ...common })
6214
+ defineViewFileTool({ baseDir: '/srv/app-files', ...common })
6215
+
6216
+ // after
6217
+ defineDownloadTool({ files, resolveUrl, ...common })
6218
+ defineUploadTool({ files, upload: ({ bytes }) => provider.upload(bytes), ...common })
6219
+ defineViewFileTool({ files, ...common })
6220
+ ```
6221
+
6222
+ Downloaded `path` is now relative to the boundary and MIME metadata is named
6223
+ `mediaType`. Update raw `mountDownload`/`mountUpload`/`mountViewFile` configs the
6224
+ same way. Auth predicates must explicitly return `true`, `false`, or a plain
6225
+ object of context fields; replace accidental `undefined` fallthroughs with the
6226
+ intended boolean.
6227
+
5645
6228
  ## Released migration: 0.53.0
5646
6229
 
5647
6230
  ### Realtime `emit` returns `boolean` instead of `void`
@@ -6609,13 +7192,24 @@ The browser-and-server entrypoint. Re-exports everything from
6609
7192
  | `createSocketIOClient` | function | low-level typed Socket.IO transport primitive — [guide](../guide/realtime.md#low-level-transport) |
6610
7193
  | `defineRealtimeContract` | function | Zod-first shared Socket.IO event contract — [guide](../guide/realtime.md#zod-first-event-contract) |
6611
7194
  | `createRealtimeClient` | function | inferred, runtime-validated Socket.IO client — [guide](../guide/realtime.md#client--createrealtimeclient) |
7195
+ | `bindRealtimeClient` | function | bind contract validation and typed acknowledgements to an existing Stitchkit client transport without owning its lifecycle |
6612
7196
  | `createRetainedTopics` | function | retained last-value store for sticky events — [guide](../guide/realtime.md#sticky-events) |
6613
7197
  | `parseSSE` | function | parse an SSE `Response` into an async generator — [guide](../guide/client.md#sse) |
6614
- | `SocketIOClient` | _type_ | the client handle; `emit` returns `false` for an emit dropped while disconnected |
7198
+ | `SocketIOClient` | _type_ | low-level client handle; `emit` reports disconnected drops and `emitWithAck` exposes the native Promise primitive used by validated `request()` |
6615
7199
  | `SocketIOClientConfig` | _type_ | config for `createSocketIOClient` (incl. `retain`, `onConnectError`, `onDroppedEmit`) |
6616
7200
  | `SocketEventMap` | _type_ | the shape of an event map |
6617
7201
  | `RealtimeClient` | _type_ | validated client inferred from a realtime contract |
6618
7202
  | `RealtimeClientOptions` | _type_ | transport options and the rejected-event hook for `createRealtimeClient` |
7203
+ | `BoundRealtimeClient` | _type_ | validated non-owning `on`/`emit`/`request` client with no `connect`/`disconnect` |
7204
+ | `RealtimeClientTransport` | _type_ | minimal existing transport capability accepted by `bindRealtimeClient` |
7205
+ | `BindRealtimeClientOptions` | _type_ | rejection/logger options for a bound existing transport |
7206
+ | `RealtimeAcknowledgedEvent` | _type_ | event-name union restricted to definitions with an `ack` schema |
7207
+ | `RealtimeAcknowledgement` | _type_ | validated acknowledgement output inferred from an event definition |
7208
+ | `RealtimeRequestArguments` | _type_ | request arguments inferred from an acknowledged event tuple |
7209
+ | `RealtimeRequestOptions` | _type_ | finite positive native acknowledgement `timeoutMs` |
7210
+ | `RealtimeRequestTimeoutError` | class | stable `REALTIME_REQUEST_TIMEOUT` rejection |
7211
+ | `RealtimeRequestDisconnectedError` | class | stable `REALTIME_REQUEST_DISCONNECTED` rejection, including an immediate disconnected call |
7212
+ | `RealtimeRequestInvalidAcknowledgementError` | class | invalid ack was reported through `onRejected` and the request rejected |
6619
7213
  | `RealtimeContract` | _type_ | shared server-to-client and client-to-server event registries |
6620
7214
  | `RealtimeEventRegistry` | _type_ | string-keyed registry of event definitions |
6621
7215
  | `RealtimeEventDefinition` | _type_ | one tuple-shaped event and optional acknowledgement schema |
@@ -6796,6 +7390,7 @@ Also re-exports the error helpers from `stitchkit/contract`.
6796
7390
  | `OperationIdentity` | _type_ | path-free service/action/scope/method identity shared by contract and native tool operations |
6797
7391
  | `Handlers` | _type_ | the typed handler map `implement` expects |
6798
7392
  | `LifecycleHooks` | _type_ | `onRequest` / pre-body `authorize` / `beforeHandle` / `afterHandle` / `onError` |
7393
+ | `composeLifecycleHooks` | function | compose HTTP lifecycle phases in declaration order with short-circuit/fallthrough semantics |
6799
7394
  | `AuthorizationContext` | _type_ | HTTP pre-body context with validated params, `input: undefined` and no files |
6800
7395
  | `RouteGroup` | _type_ | a prefixed group of services with its own hooks |
6801
7396
  | `RawRoute` | _type_ | a non-contract `Request → Response` route with a concrete `BunServer` context |
@@ -6814,6 +7409,7 @@ Also re-exports the error helpers from `stitchkit/contract`.
6814
7409
  | Export | Kind | Summary |
6815
7410
  |--------|------|---------|
6816
7411
  | `createAuthHook` | function | one scope gate for HTTP `authorize` and tool `beforeHandle` — [guide](../guide/auth-and-errors.md#createauthhook) |
7412
+ | `composeAuthHooks` | function | route multiple canonical auth domains by owned scope and atomically commit their typed contributions |
6817
7413
  | `createErrorHook` | function | an async-capable, endpoint-aware `onError` hook from a code map + envelope renderer — [guide](../guide/auth-and-errors.md#createerrorhook) |
6818
7414
  | `ErrorHookConfig` | _type_ | async observer/renderer config for `createErrorHook` |
6819
7415
  | `ResolvedError` | _type_ | the normalised error handed to `createErrorHook`'s `render` |
@@ -6827,11 +7423,14 @@ Also re-exports the error helpers from `stitchkit/contract`.
6827
7423
  | `AuthHook` | _type_ | the hook `createAuthHook` returns |
6828
7424
  | `AuthHookConfig` | _type_ | config for `createAuthHook` |
6829
7425
  | `AuthRule` | _type_ | `'public' \| 'authenticated' \| predicate` |
7426
+ | `AuthRuleContribution` | _type_ | plain context fields a sync/async rule may contribute after authorizing |
6830
7427
  | `ScopedAuthRule` | _type_ | a rule plus its typed context contribution — `{ rule, inject? }` |
6831
7428
  | `AuthRules` | _type_ | the `rules` map: bare rules or scoped rules |
6832
7429
  | `RuleScopes` | _type_ | scope→context map derived from a `rules` object; `'public'` fields become optional |
6833
7430
  | `ScopedAuthHook` | _type_ | an auth hook carrying its derived scope map at the type level |
6834
7431
  | `AuthScopes` | _type_ | recover the derived map — `createScopedImplement<AuthScopes<typeof hook>>()` |
7432
+ | `ComposeAuthHooksConfig` | _type_ | ordered canonical hooks plus the optional explicit composite default scope |
7433
+ | `ComposedAuthScopes` | _type_ | scope→context map derived from every owner in a composed auth hook |
6835
7434
  | `BearerResolverConfig` | _type_ | config for `createBearerResolver` |
6836
7435
  | `JwtPayload` | _type_ | a decoded JWT payload |
6837
7436
  | `SignJwtOptions` | _type_ | options for `signJwt` (expiry, claims) |
@@ -6995,6 +7594,18 @@ audit event. See the [Observability guide](../guide/observability.md).
6995
7594
 
6996
7595
  ---
6997
7596
 
7597
+ ## `stitchkit/remote`
7598
+
7599
+ Peer-free remote implementation boundary. Importing it does not load the MCP
7600
+ SDK, `ai`, or other optional tool peers, so it is safe in a thin CLI bundle.
7601
+
7602
+ | Export | Kind | Summary |
7603
+ |--------|------|---------|
7604
+ | `implementRemote` | function | bind a contract to a remote HTTP API — [guide](../guide/mcp-and-agents.md#proxying-a-remote-api--implementremote) |
7605
+ | `ImplementRemoteOptions` | _type_ | optional argument-rewrite hook for `implementRemote` |
7606
+
7607
+ ---
7608
+
6998
7609
  ## `stitchkit/tools`
6999
7610
 
7000
7611
  Server-only. Turns contracts into MCP and AI-agent tools. MCP server surfaces
@@ -7014,7 +7625,6 @@ payload.
7014
7625
  | `bindStdioProcessSignals` | function | explicitly bind OS signals to one close-only stdio handle — [guide](../guide/testing-and-deployment.md#stdio-process-signals) |
7015
7626
  | `buildMcpServer` | function | build an `McpServer` from contract/runtime surfaces; no-auth configs omit the second argument |
7016
7627
  | `mountMcp` | function | add contract tools to an existing `McpServer` — [guide](../guide/mcp-and-agents.md#mountmcp) |
7017
- | `implementRemote` | function | bind a contract to a remote HTTP API — [guide](../guide/mcp-and-agents.md#proxying-a-remote-api--implementremote) |
7018
7628
  | `mountAgent` | function | a Vercel AI SDK `ToolSet` from a service — [guide](../guide/mcp-and-agents.md#ai-agents--mountagent) |
7019
7629
  | `defineRuntimeTool` | function | define one validated pathless operation for explicit MCP, Agent and/or CLI surfaces — [guide](../guide/mcp-and-agents.md#pathless-runtime-tools-and-multimodal-results) |
7020
7630
  | `createRuntimeToolFactory` | function | bind shared identity and Zod-validated per-call context for runtime tools — [guide](../guide/mcp-and-agents.md#pathless-runtime-tools-and-multimodal-results) |
@@ -7046,7 +7656,6 @@ payload.
7046
7656
  | `McpSurfaceDefinition` | _type_ | one immutable `{ services, runtimeTools }` MCP surface |
7047
7657
  | `McpSurfaceRegistry` | _type_ | finite keyed surface registry for eager preparation |
7048
7658
  | `StdioAuthConfig` | _type_ | startup identity composed into `StdioMcpServerConfig` |
7049
- | `ImplementRemoteOptions` | _type_ | options for `implementRemote` |
7050
7659
  | `McpMountConfig` | _type_ | config for `mountMcp` |
7051
7660
  | `McpSchemaValidationConfig` | _type_ | shared `{ policy, requireTypedProperties, allowUntyped, requirePortableFormats, allowFormats }` profile |
7052
7661
  | `ValidateMcpSchemasConfig` | _type_ | standalone validation profile plus `services`, `extend`, flattening and logger |
@@ -7102,7 +7711,7 @@ payload.
7102
7711
  | `ToolInvocationOptions` | _type_ | per-call source, context, lifecycle, hooks and output-strip reporter |
7103
7712
  | `ToolInvokerTransport` | _type_ | invoker exposure policy: `MCP \| AGENT \| CLI` |
7104
7713
  | `ToolCallContext` | _type_ | the context every tool hook receives — `{ source, mcp? }` plus whatever the mount's `context` added |
7105
- | `ViewFileOptions` | _type_ | shared URL/local-sandbox policy for `defineViewFileTool`, `mountViewFile` and `resolveMedia` |
7714
+ | `ViewFileOptions` | _type_ | shared URL/managed-file-boundary policy for `defineViewFileTool`, `mountViewFile` and `resolveMedia` |
7106
7715
  | `ViewFileOutput` | _type_ | neutral managed batch result with multimodal `content` and per-item `errors` |
7107
7716
  | `ViewFileInputSchema` | constant | fixed one-or-many media path/URL input schema |
7108
7717
  | `ViewFileOutputSchema` | constant | Zod schema for the neutral managed view-file batch result |
@@ -7138,9 +7747,35 @@ runtime-tool runner, plus deliberate raw MCP adapters over the same mechanics.
7138
7747
  | `ManagedNativeToolConfig` | _type_ | shared name, description, identity, exposure and annotations for managed native factories |
7139
7748
  | `NativeToolIdentity` | _type_ | pathless service/action/scope/meta identity; semantic method is factory-owned |
7140
7749
  | `ManagedWaitRender` | _type_ | optional managed wait terminal text and failure classification |
7141
- | `DownloadResultSchema` | constant | Zod schema for a saved download's path, byte size and MIME type |
7142
- | `DownloadResult` | _type_ | validated output of `defineDownloadTool` |
7143
7750
  | `UploadToolInputSchema` | constant | fixed `{ path: string }` input schema for `defineUploadTool` |
7751
+ | `defineAsyncOperation` | function | runtime-only start/status/wait plus configured cancel/result/artifacts definitions |
7752
+ | `defineAsyncOperationContract` | function | define one canonical Zod-first HTTP contract for start/status/wait plus optional capabilities |
7753
+ | `bindContractAsyncOperation` | function | bind literal methods from an existing contract without creating another HTTP surface |
7754
+ | `createAsyncOperationSnapshotSchema` | function | canonical pending/running/succeeded/failed/cancelled Zod snapshot |
7755
+ | `AsyncOperationCancelResultSchema` | constant | validated accepted/already_terminal/rejected cancellation result |
7756
+ | `AsyncOperationCancelResult` | _type_ | validated cancel capability result |
7757
+ | `AsyncOperationCancelCapability` | _type_ | optional typed domain cancellation callback |
7758
+ | `AsyncOperationCapability` | _type_ | generated capability-name union |
7759
+ | `AsyncOperationStartDefinition` / `AsyncOperationFollowDefinition` | _type_ | generated runtime definition types |
7760
+ | `AsyncOperationIdentity` | _type_ | shared service/action/scope/meta identity for an operation |
7761
+ | `AsyncOperationOutputCapability` | _type_ | optional result/artifact schema plus handler |
7762
+ | `RuntimeAsyncOperationConfig` | _type_ | runtime-only descriptor configuration |
7763
+ | `RuntimeAsyncOperation` | _type_ | inferred generated definitions and schemas |
7764
+ | `AsyncOperationContractConfig` | _type_ | canonical contract config where start returns the operation id |
7765
+ | `AsyncOperationContractWithStartOutputConfig` | _type_ | canonical contract config with an application start envelope and typed id extractor |
7766
+ | `DefinedAsyncOperationContract` | _type_ | generated contract, capability keys, schemas and parsed adapters |
7767
+ | `ContractAsyncOperationConfig` | _type_ | literal contract method binding and handlers |
7768
+ | `AdaptedContractAsyncOperationConfig` | _type_ | existing-contract binding with explicit id and per-capability input adapters |
7769
+ | `BoundAdaptedContractAsyncOperation` | _type_ | inferred adapted binding with parsed id/input projectors |
7770
+ | `AdaptedContractAsyncOperationStartKey` | _type_ | existing-contract keys with a declared start output |
7771
+ | `AdaptedContractAsyncOperationFollowKey` | _type_ | existing-contract keys with both input and output schemas |
7772
+ | `AdaptedContractAsyncOperationWaitKey` | _type_ | adapted follow-up keys whose output matches the selected status snapshot |
7773
+ | `ContractAsyncOperationInputAdapters` | _type_ | guaranteed direct-binding status/wait input adapters plus configured optional capabilities |
7774
+ | `ContractAsyncOperationKeys` | _type_ | literal method-key union of a bound contract |
7775
+ | `ContractAsyncOperationStartKey` | _type_ | direct-binding start keys whose declared ID schema has stable input/output |
7776
+ | `ContractAsyncOperationFollowKey` | _type_ | contract keys whose input schema type matches the selected start output |
7777
+ | `ContractAsyncOperationWaitKey` | _type_ | follow-up keys whose output schema type also matches the selected status output |
7778
+ | `composeToolLifecycle` | function | ordered composition of tool before/after phases |
7144
7779
  | `mountDownload` | function | raw MCP "download a URL to disk" adapter (SSRF-guarded, size-capped) |
7145
7780
  | `mountUpload` | function | raw MCP "upload a local file" adapter |
7146
7781
  | `mountWait` | function | raw MCP generic `--wait`-style polling adapter |
@@ -7220,6 +7855,64 @@ handler pipeline without opening a TCP port.
7220
7855
  | `HandlerTestClientConfig` | _type_ | handler, contract, path prefix, scoped config and client request defaults |
7221
7856
  | `HandlerTestClientsConfig` | _type_ | batch helper configuration |
7222
7857
  | `HandlerTestTransportConfig` | _type_ | shared in-process handler, origin, prefix, client defaults and optional server handle |
7858
+ | `buildSurfaceManifest` | function | deterministic manifest v2 with HTTP topology, transport-specific tool projections, named MCP surfaces and realtime schema digests |
7859
+ | `assertSurfaceManifestSnapshot` | function | bounded deterministic manifest drift assertion |
7860
+ | `assertSurfaceDiscovery` | function | compare real OpenAPI/tool/realtime caller-observed discovery to the exact manifest projection |
7861
+ | `runSurfaceProbes` | function | execute explicit transport drivers under one per-scenario setup/invoke/teardown deadline |
7862
+ | `defineRealtimeProbe` | function | declare one canonical realtime scenario for a caller-supplied real transport driver |
7863
+ | `createRealtimeProbeDriver` | function | bind each scenario to a caller-owned transport and normalize actual event/ack/rejection/disconnect outcomes without lifecycle ownership |
7864
+ | `TransportObservationSchema` | constant | normalized HTTP/tool/realtime outcome including rejection fields and handler-call evidence |
7865
+ | `RealtimeRejectionObservationSchema` | constant | direction/phase/reason/fault observation for a rejected realtime event |
7866
+ | `RealtimeDisconnectObservationSchema` | constant | observed before-invoke versus in-flight disconnect phase |
7867
+ | `ConformanceTransport` | _type_ | explicit probe transport union |
7868
+ | `RunSurfaceProbesConfig` | _type_ | probes, drivers and diagnostic byte cap |
7869
+ | `SurfaceDiscoveryObservation` | _type_ | real OpenAPI/tool/CLI/extension discovery values |
7870
+ | `SurfaceToolDiscoveryObservation` | _type_ | transport, optional named surface and caller-observed tool names |
7871
+ | `SurfaceRealtimeDiscoveryObservation` | _type_ | caller-observed server→client and client→server event names |
7872
+ | `SurfaceProbe` / `SurfaceProbeDriver` | _type_ | one bounded scenario and its consumer-supplied runner |
7873
+ | `DefineRealtimeProbeConfig` | _type_ | name, canonical scenario, explicit fixture and expected realtime outcome |
7874
+ | `CreateRealtimeProbeDriverConfig` | _type_ | per-scenario foreign-transport binder and optional handler-call counter |
7875
+ | `RealtimeProbeAdapter` | _type_ | connected-state observation, scenario invocation and subscription-only cleanup |
7876
+ | `RealtimeProbeFixture` / `RealtimeProbeScenario` | _type_ | driver input and supported event/ack/invalid/disconnect/timeout scenario vocabulary |
7877
+ | `RealtimeRejectionObservation` | _type_ | parsed structured realtime rejection observation |
7878
+ | `RealtimeDisconnectObservation` | _type_ | normalized physical timing of a realtime disconnect |
7879
+ | `TransportObservation` | _type_ | validated normalized driver result |
7880
+ | `SurfaceManifest` / `SurfaceManifestConfig` | _type_ | deterministic surface snapshot and its inputs |
7881
+ | `SurfaceManifestOperation` / `SurfaceManifestOperationSchema` | _type_ / schema | one contract or runtime operation row |
7882
+ | `SurfaceManifestTool` / `SurfaceManifestToolSchema` | _type_ / schema | one mounted tool row with advertised input digest |
7883
+ | `SurfaceManifestToolSurface` / `SurfaceManifestToolSurfaceSchema` | _type_ / schema | one static transport projection, optionally keyed for a finite MCP surface |
7884
+ | `SurfaceManifestRealtimeEvent` / `SurfaceManifestRealtimeEventSchema` | _type_ / schema | one named directional realtime event with input/output args and ack digests |
7885
+ | `SurfaceManifestExtension` / `SurfaceManifestExtensionSchema` | _type_ / schema | declared transport extension row |
7886
+ | `SurfaceManifestSchema` | schema | complete deterministic manifest schema |
7887
+ | `SurfaceToolDefinition` | _type_ | plain service/runtime selection used by CLI and finite named MCP surfaces |
7888
+ | `SurfaceAgentProjection` | _type_ | Agent selection plus its reachable extend/flatten presentation policy |
7889
+ | `SurfaceMcpPreparation` | _type_ | one global MCP extend/flatten/schema-validation/multi-round preparation policy |
7890
+ | `SurfaceRuntimeToolDefinition` | _type_ | peer-free non-executable runtime-operation descriptor for manifest projection |
7891
+ | `SurfaceToolExtension` | _type_ | canonical structural tool extension including schema, resolver and optional filter |
7892
+ | `McpSchemaValidationConfig` / `IncompatibleSchemaPolicy` | _type_ | canonical MCP schema-preparation policy used by real mounts and manifests |
7893
+ | `SurfaceRealtimeSchemaPairSchema` | schema | input/output digest pair for args or acknowledgements |
7894
+ | `SurfaceSchemaDigestsSchema` | schema | params/input/output/multipart digest object |
7895
+ | `serializeSurfaceValue` | function | canonical versioned serialization used for deterministic digests |
7896
+
7897
+ ---
7898
+
7899
+ ## `stitchkit/files`
7900
+
7901
+ Peer-free Bun/Node filesystem capability. Browser/contract-safe refs are also
7902
+ available from `stitchkit/contract`.
7903
+
7904
+ | Export | Kind | Summary |
7905
+ |--------|------|---------|
7906
+ | `createManagedFileBoundary` | function | bind an application-owned root, optionally creating one final directory under a trusted existing parent |
7907
+ | `ManagedFileBoundary` | _type_ | non-reopenable `read`/`write` capability over canonical relative paths |
7908
+ | `ManagedFileBoundaryConfig` | _type_ | bound root, optional `createRoot`, finite read/write/inspection limits, inspector and cleanup observer |
7909
+ | `ManagedFileRefSchema` / `ManagedFileRef` | schema / _type_ | transport-safe relative path, measured size and optional media metadata |
7910
+ | `ManagedFilePathSchema` / `ManagedFilePath` | schema / _type_ | canonical POSIX relative managed-file path |
7911
+ | `ManagedFileSource` | _type_ | bounded immutable bytes plus validated ref passed to upload callbacks |
7912
+ | `ManagedFileReadOptions` / `ManagedFileWriteOptions` | _type_ | per-operation byte cap, signal and atomic write policy |
7913
+ | `ManagedFileError` / `ManagedFileErrorCode` | class / _type_ | stable boundary failures; registered `FILE_*` mistakes are caller-safe while `FILE_IO_ERROR` remains internal |
7914
+ | `ManagedFileInspector` | _type_ | bounded-prefix read/write inspection callback with a finite cancellation signal that cannot own path or size |
7915
+ | `ManagedFileInspectionInput` / `ManagedFileInspection` | _type_ | inspector prefix/name/declared media/signal input and validated metadata-only result |
7223
7916
 
7224
7917
  ---
7225
7918
 
@@ -7258,7 +7951,7 @@ SDK nor the `ai` peer.
7258
7951
  | `defineCliCommand` | function | define one Zod-typed CLI-only executable command |
7259
7952
  | `parseCliArgs` | function | argv → typed tool args against a schema (advanced) |
7260
7953
  | `pollUntilDone` | function | the generic `--wait` poller (advanced) |
7261
- | `emitResult` | function | write a `ToolResult` to stdout/stderr + exit code (advanced) |
7954
+ | `emitResult` | function | write a pretty or compact `ToolResult` record to stdout/stderr + exit code (advanced) |
7262
7955
  | `DEFAULT_EXIT_CODES` | const | the default `ToolResult.code` → exit-code map |
7263
7956
  | `CliConfig` | _type_ | config for `createCli` |
7264
7957
  | `CliSurfaceSource` | _type_ | static service/runtime array or identity-dependent factory |
@@ -7267,9 +7960,9 @@ SDK nor the `ai` peer.
7267
7960
  | `CliCommandDefinitionWithOutput` | _type_ | native command with validated declared output |
7268
7961
  | `CliCommandDefinitionWithoutOutput` | _type_ | native void command without an output contract |
7269
7962
  | `CliCommandContext` | _type_ | parsed input, global options and stdout/stderr writers |
7270
- | `CliRunOptions` | _type_ | parsed global flags (`--json`, `--wait`, …) |
7963
+ | `CliRunOptions` | _type_ | parsed global flags (`--json` compacts success/error records, `--wait`, …) |
7271
7964
  | `ParsedCliArgs` | _type_ | result of `parseCliArgs` |
7272
- | `CliWaitConfig` | _type_ | per-command `--wait` polling config |
7965
+ | `CliWaitConfig` | _type_ | per-command `--wait` polling config; optional `failed(result)` maps a terminal domain failure to `WAIT_FAILED` and a non-zero exit |
7273
7966
  | `ExitCodeMap` | _type_ | `ToolResult.code` → process exit code |
7274
7967
  | `PollParams` | _type_ | params for `pollUntilDone` |
7275
7968
  | `CliWriters` | _type_ | stdout/stderr sinks for `emitResult` |