stitchkit 0.55.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 (69) hide show
  1. package/dist/browser/socket-io.d.ts +14 -6
  2. package/dist/browser/socket-io.d.ts.map +1 -1
  3. package/dist/cli.js +4 -4
  4. package/dist/contract/errors.d.ts +7 -0
  5. package/dist/contract/errors.d.ts.map +1 -1
  6. package/dist/contract/index.js +1 -1
  7. package/dist/files/boundary.d.ts +8 -5
  8. package/dist/files/boundary.d.ts.map +1 -1
  9. package/dist/files.js +1 -1
  10. package/dist/{index-rgd18hx3.js → index-28cqssm3.js} +7 -0
  11. package/dist/index-557by2db.js +34 -0
  12. package/dist/index-5r13htq1.js +283 -0
  13. package/dist/index-6y759j86.js +1031 -0
  14. package/dist/{index-1k16zv57.js → index-bfcpjw20.js} +125 -31
  15. package/dist/{index-0gv7k0ra.js → index-escqg10p.js} +73 -666
  16. package/dist/{index-v4bbq3p4.js → index-j3dem06f.js} +1 -1
  17. package/dist/{index-qx84bqzk.js → index-jcc611vh.js} +1 -1
  18. package/dist/{index-npye5kt7.js → index-n4nfa7gh.js} +3 -3
  19. package/dist/{index-pzc32v75.js → index-sm2tjx06.js} +41 -4
  20. package/dist/{index-js7yexng.js → index-xy8fmh6w.js} +83 -4
  21. package/dist/index.d.ts +2 -2
  22. package/dist/index.d.ts.map +1 -1
  23. package/dist/index.js +27 -6
  24. package/dist/node.js +5 -4
  25. package/dist/observability/index.js +3 -3
  26. package/dist/remote.js +1 -1
  27. package/dist/server/context-contribution.d.ts +8 -0
  28. package/dist/server/context-contribution.d.ts.map +1 -1
  29. package/dist/server/index.d.ts +1 -1
  30. package/dist/server/index.d.ts.map +1 -1
  31. package/dist/server/index.js +8 -5
  32. package/dist/server/middleware/auth.d.ts +29 -2
  33. package/dist/server/middleware/auth.d.ts.map +1 -1
  34. package/dist/testing/surface-conformance.d.ts +143 -10
  35. package/dist/testing/surface-conformance.d.ts.map +1 -1
  36. package/dist/testing/surface-manifest.d.ts +132 -33
  37. package/dist/testing/surface-manifest.d.ts.map +1 -1
  38. package/dist/testing.d.ts +2 -2
  39. package/dist/testing.d.ts.map +1 -1
  40. package/dist/testing.js +470 -122
  41. package/dist/tools/async-operation.d.ts +210 -18
  42. package/dist/tools/async-operation.d.ts.map +1 -1
  43. package/dist/tools/define-download-tool.d.ts.map +1 -1
  44. package/dist/tools/define-upload-tool.d.ts.map +1 -1
  45. package/dist/tools/internal/surface-projector.d.ts +101 -0
  46. package/dist/tools/internal/surface-projector.d.ts.map +1 -0
  47. package/dist/tools/managed-file-error.d.ts +6 -0
  48. package/dist/tools/managed-file-error.d.ts.map +1 -0
  49. package/dist/tools/mcp-prepare.d.ts +3 -21
  50. package/dist/tools/mcp-prepare.d.ts.map +1 -1
  51. package/dist/tools/mcp-round-policy.d.ts +9 -0
  52. package/dist/tools/mcp-round-policy.d.ts.map +1 -0
  53. package/dist/tools/mcp-round.d.ts +0 -4
  54. package/dist/tools/mcp-round.d.ts.map +1 -1
  55. package/dist/tools/mount-download.d.ts.map +1 -1
  56. package/dist/tools/mount-upload.d.ts.map +1 -1
  57. package/dist/tools/mount.d.ts +5 -9
  58. package/dist/tools/mount.d.ts.map +1 -1
  59. package/dist/tools/runtime-tool.d.ts.map +1 -1
  60. package/dist/tools/surface.d.ts +7 -0
  61. package/dist/tools/surface.d.ts.map +1 -1
  62. package/dist/tools/view-file.d.ts.map +1 -1
  63. package/dist/tools.d.ts +1 -1
  64. package/dist/tools.d.ts.map +1 -1
  65. package/dist/tools.js +348 -383
  66. package/llms-full.txt +361 -58
  67. package/package.json +1 -1
  68. package/dist/index-h60df7rj.js +0 -110
  69. package/dist/index-k4dftwf7.js +0 -66
package/llms-full.txt CHANGED
@@ -3024,7 +3024,13 @@ import {
3024
3024
  import { createManagedFileBoundary } from 'stitchkit/files'
3025
3025
  import { z } from 'zod'
3026
3026
 
3027
- const files = await createManagedFileBoundary({ root: '/srv/job-media' })
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
+ })
3028
3034
 
3029
3035
  const waitForJob = defineWaitTool({
3030
3036
  name: 'wait_for_job',
@@ -3131,16 +3137,14 @@ const runtimeTools = exportOperation.runtimeTools
3131
3137
  ```
3132
3138
 
3133
3139
  Every follow-up repeats `authorize`; an opaque id is never authority. Aborting
3134
- `wait` only stops waiting and never calls optional domain `cancel`. A
3135
- contract-backed operation uses `bindContractAsyncOperation` with literal methods
3136
- from a dedicated existing contract, so it creates no second HTTP router or
3137
- duplicate schemas. Capability keys are narrowed by schema-compatible TypeScript
3138
- input/output types. At runtime, the binder additionally requires the exact same
3139
- id and snapshot Zod instances; declare each once and reuse it:
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:
3140
3145
 
3141
3146
  ```ts
3142
- import { defineContract } from 'stitchkit/contract'
3143
- import { bindContractAsyncOperation } from 'stitchkit/tools'
3147
+ import { defineAsyncOperationContract } from 'stitchkit/tools'
3144
3148
  import { z } from 'zod'
3145
3149
 
3146
3150
  const operationId = z.object({ id: z.string() })
@@ -3148,38 +3152,82 @@ const operationSnapshot = z.object({
3148
3152
  phase: z.enum(['pending', 'running', 'succeeded', 'failed', 'cancelled']),
3149
3153
  })
3150
3154
 
3151
- const operations = defineContract(
3152
- { prefix: 'exports' },
3153
- {
3154
- start: {
3155
- method: 'POST', path: '/', desc: 'Start export',
3156
- input: z.object({ projectId: z.string() }), output: operationId,
3157
- },
3158
- status: {
3159
- method: 'POST', path: '/status', desc: 'Read export status',
3160
- input: operationId, output: operationSnapshot,
3161
- },
3162
- wait: {
3163
- method: 'POST', path: '/wait', desc: 'Wait for export',
3164
- input: operationId, output: operationSnapshot,
3165
- },
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,
3166
3187
  },
3167
- )
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
+ })
3168
3197
 
3169
3198
  const operationHandlers = {
3170
- start: () => ({ id: 'example' }),
3199
+ start: () => ({ operation: { id: 'example' }, acceptedAt: new Date().toISOString() }),
3171
3200
  status: (): z.output<typeof operationSnapshot> => ({ phase: 'pending' }),
3172
3201
  wait: (): z.output<typeof operationSnapshot> => ({ phase: 'succeeded' }),
3173
3202
  }
3174
3203
 
3175
3204
  const bound = bindContractAsyncOperation({
3176
3205
  mode: 'contract-backed',
3177
- contract: operations,
3206
+ binding: 'adapted',
3207
+ contract: existing,
3208
+ id: operationId,
3178
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
+ },
3179
3217
  handlers: operationHandlers,
3180
3218
  })
3181
3219
  ```
3182
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
+
3183
3231
  ### Explicit raw SDK registration
3184
3232
 
3185
3233
  `rawTools` is deliberately named as an escape hatch. A tool registered there
@@ -3758,6 +3806,24 @@ socket.on('note:created', (note) => { /* typed note */ })
3758
3806
  socket.emit('room:join', 'general', ({ joined }) => { /* typed + validated */ })
3759
3807
  ```
3760
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
+
3761
3827
  ### Request-response over realtime
3762
3828
 
3763
3829
  For an event with an `ack` schema, `request()` is the Promise form of the same
@@ -3792,6 +3858,7 @@ transport.
3792
3858
  `createSocketIOClient` remains the low-level Socket.IO transport wrapper for
3793
3859
  schema-agnostic infrastructure. Application wire events should use
3794
3860
  `createRealtimeClient`; it adds the shared contract without replacing Socket.IO.
3861
+ Use `bindRealtimeClient` when that low-level transport already exists.
3795
3862
 
3796
3863
  ### Durable subscriptions
3797
3864
 
@@ -4344,10 +4411,10 @@ fields; `true | object` correctly makes them optional.
4344
4411
  Annotate `rules` with `satisfies Record<MyScope, AuthRule<User>>` so the compiler
4345
4412
  catches a scope you forgot to cover. With the scoped rule objects below, widen
4346
4413
  the annotation to
4347
- `satisfies Record<MyScope, AuthRule<User> | ScopedAuthRule<User, object>>` — it
4348
- keeps the coverage check and does not disturb the derivation. (When every scope
4349
- comes from the derived map, the check is already implicit: a scope missing from
4350
- `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.)
4351
4418
 
4352
4419
  ### Scoped rules — the map `createScopedImplement` consumes
4353
4420
 
@@ -4393,11 +4460,58 @@ widens its inject and degrades that scope's fields to `object`.
4393
4460
 
4394
4461
  Derivation needs the identity generic to be **inferred** (write no explicit
4395
4462
  `createAuthHook<User>` — TypeScript has no partial inference); with an explicit
4396
- generic, or fields injected outside the hook, keep the hand-written map. Two
4397
- hooks guarding different scopes compose by intersection:
4398
- `createScopedImplement<AuthScopes<typeof userHook> & AuthScopes<typeof botHook>>()`.
4463
+ generic, or fields injected outside the hook, keep the hand-written map.
4399
4464
  → ADR 0078
4400
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
+
4401
4515
  ### Auth on the tool surface — `resolveFromContext`
4402
4516
 
4403
4517
  The same hook guards every transport, but the lifecycle slot and identity
@@ -5332,35 +5446,98 @@ exercise the same framework pipeline.
5332
5446
 
5333
5447
  ### Transport conformance
5334
5448
 
5335
- `buildSurfaceManifest` snapshots actual HTTP topology, MCP/Agent/CLI names,
5336
- CLI-only commands, extensions and canonical schema digests. Compare real runner
5337
- discovery with `assertSurfaceDiscovery`, then use `runSurfaceProbes` only for
5338
- the transports you explicitly provide:
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:
5339
5455
 
5340
5456
  ```ts
5457
+ import { bindRealtimeClient } from 'stitchkit'
5341
5458
  import {
5342
5459
  assertSurfaceDiscovery,
5343
5460
  buildSurfaceManifest,
5461
+ createRealtimeProbeDriver,
5462
+ defineRealtimeProbe,
5344
5463
  runSurfaceProbes,
5345
5464
  } from 'stitchkit/testing'
5346
5465
 
5347
- const manifest = buildSurfaceManifest({ services, runtimeTools, cliCommands })
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
+ })
5348
5480
  assertSurfaceDiscovery(manifest, {
5349
5481
  openApi,
5350
- MCP: (await mcpClient.listTools()).tools.map((tool) => tool.name),
5482
+ toolSurfaces: [{
5483
+ transport: 'MCP',
5484
+ surface: 'member',
5485
+ names: (await mcpClient.listTools()).tools.map((tool) => tool.name),
5486
+ }],
5351
5487
  AGENT: Object.keys(agentTools),
5352
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,
5353
5520
  })
5354
5521
 
5355
5522
  await runSurfaceProbes({
5356
- probes,
5357
- drivers: { HTTP: httpDriver, MCP: mcpDriver, AGENT: agentDriver, CLI: cliDriver },
5523
+ probes: [invalidInbound],
5524
+ drivers: { REALTIME: realtimeDriver },
5358
5525
  })
5359
5526
  ```
5360
5527
 
5361
- The kit never starts a server or invents credentials. Each probe declares its
5362
- expected normalized outcome and has bounded timeout, setup/teardown and an
5363
- AbortSignal. A missing driver is unsupported, not silently marked conformant.
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.
5364
5541
 
5365
5542
  ### Test handlers with raw Requests
5366
5543
 
@@ -5921,7 +6098,95 @@ current one *up to* your target, and apply each snippet.
5921
6098
  runtime): bootstrap the server, one HTTP request, and any feature you rely on
5922
6099
  (Socket.IO connect, an MCP tool call, a multipart upload, …).
5923
6100
 
5924
- ## Unreleased migration: peer-free `implementRemote`
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`
5925
6190
 
5926
6191
  `implementRemote` now has one canonical, optional-peer-free owner. This keeps
5927
6192
  MCP SDK and AI SDK modules out of CLI bundles that only proxy HTTP calls:
@@ -5933,7 +6198,7 @@ import { implementRemote } from 'stitchkit/tools'
5933
6198
  import { implementRemote } from 'stitchkit/remote'
5934
6199
  ```
5935
6200
 
5936
- ## Unreleased migration: managed file boundary and strict auth returns
6201
+ ### Managed file boundary and strict auth returns
5937
6202
 
5938
6203
  Create one boundary during application bootstrap and pass the capability, never
5939
6204
  a per-call directory or host path:
@@ -6927,6 +7192,7 @@ The browser-and-server entrypoint. Re-exports everything from
6927
7192
  | `createSocketIOClient` | function | low-level typed Socket.IO transport primitive — [guide](../guide/realtime.md#low-level-transport) |
6928
7193
  | `defineRealtimeContract` | function | Zod-first shared Socket.IO event contract — [guide](../guide/realtime.md#zod-first-event-contract) |
6929
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 |
6930
7196
  | `createRetainedTopics` | function | retained last-value store for sticky events — [guide](../guide/realtime.md#sticky-events) |
6931
7197
  | `parseSSE` | function | parse an SSE `Response` into an async generator — [guide](../guide/client.md#sse) |
6932
7198
  | `SocketIOClient` | _type_ | low-level client handle; `emit` reports disconnected drops and `emitWithAck` exposes the native Promise primitive used by validated `request()` |
@@ -6934,6 +7200,9 @@ The browser-and-server entrypoint. Re-exports everything from
6934
7200
  | `SocketEventMap` | _type_ | the shape of an event map |
6935
7201
  | `RealtimeClient` | _type_ | validated client inferred from a realtime contract |
6936
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 |
6937
7206
  | `RealtimeAcknowledgedEvent` | _type_ | event-name union restricted to definitions with an `ack` schema |
6938
7207
  | `RealtimeAcknowledgement` | _type_ | validated acknowledgement output inferred from an event definition |
6939
7208
  | `RealtimeRequestArguments` | _type_ | request arguments inferred from an acknowledged event tuple |
@@ -7140,6 +7409,7 @@ Also re-exports the error helpers from `stitchkit/contract`.
7140
7409
  | Export | Kind | Summary |
7141
7410
  |--------|------|---------|
7142
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 |
7143
7413
  | `createErrorHook` | function | an async-capable, endpoint-aware `onError` hook from a code map + envelope renderer — [guide](../guide/auth-and-errors.md#createerrorhook) |
7144
7414
  | `ErrorHookConfig` | _type_ | async observer/renderer config for `createErrorHook` |
7145
7415
  | `ResolvedError` | _type_ | the normalised error handed to `createErrorHook`'s `render` |
@@ -7159,6 +7429,8 @@ Also re-exports the error helpers from `stitchkit/contract`.
7159
7429
  | `RuleScopes` | _type_ | scope→context map derived from a `rules` object; `'public'` fields become optional |
7160
7430
  | `ScopedAuthHook` | _type_ | an auth hook carrying its derived scope map at the type level |
7161
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 |
7162
7434
  | `BearerResolverConfig` | _type_ | config for `createBearerResolver` |
7163
7435
  | `JwtPayload` | _type_ | a decoded JWT payload |
7164
7436
  | `SignJwtOptions` | _type_ | options for `signJwt` (expiry, claims) |
@@ -7477,6 +7749,7 @@ runtime-tool runner, plus deliberate raw MCP adapters over the same mechanics.
7477
7749
  | `ManagedWaitRender` | _type_ | optional managed wait terminal text and failure classification |
7478
7750
  | `UploadToolInputSchema` | constant | fixed `{ path: string }` input schema for `defineUploadTool` |
7479
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 |
7480
7753
  | `bindContractAsyncOperation` | function | bind literal methods from an existing contract without creating another HTTP surface |
7481
7754
  | `createAsyncOperationSnapshotSchema` | function | canonical pending/running/succeeded/failed/cancelled Zod snapshot |
7482
7755
  | `AsyncOperationCancelResultSchema` | constant | validated accepted/already_terminal/rejected cancellation result |
@@ -7488,9 +7761,18 @@ runtime-tool runner, plus deliberate raw MCP adapters over the same mechanics.
7488
7761
  | `AsyncOperationOutputCapability` | _type_ | optional result/artifact schema plus handler |
7489
7762
  | `RuntimeAsyncOperationConfig` | _type_ | runtime-only descriptor configuration |
7490
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 |
7491
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 |
7492
7774
  | `ContractAsyncOperationKeys` | _type_ | literal method-key union of a bound contract |
7493
- | `ContractAsyncOperationStartKey` | _type_ | contract keys with a declared output schema, valid for `start` |
7775
+ | `ContractAsyncOperationStartKey` | _type_ | direct-binding start keys whose declared ID schema has stable input/output |
7494
7776
  | `ContractAsyncOperationFollowKey` | _type_ | contract keys whose input schema type matches the selected start output |
7495
7777
  | `ContractAsyncOperationWaitKey` | _type_ | follow-up keys whose output schema type also matches the selected status output |
7496
7778
  | `composeToolLifecycle` | function | ordered composition of tool before/after phases |
@@ -7573,21 +7855,42 @@ handler pipeline without opening a TCP port.
7573
7855
  | `HandlerTestClientConfig` | _type_ | handler, contract, path prefix, scoped config and client request defaults |
7574
7856
  | `HandlerTestClientsConfig` | _type_ | batch helper configuration |
7575
7857
  | `HandlerTestTransportConfig` | _type_ | shared in-process handler, origin, prefix, client defaults and optional server handle |
7576
- | `buildSurfaceManifest` | function | deterministic actual HTTP/tool/CLI/extension topology with versioned schema digests |
7858
+ | `buildSurfaceManifest` | function | deterministic manifest v2 with HTTP topology, transport-specific tool projections, named MCP surfaces and realtime schema digests |
7577
7859
  | `assertSurfaceManifestSnapshot` | function | bounded deterministic manifest drift assertion |
7578
- | `assertSurfaceDiscovery` | function | compare real OpenAPI/MCP/Agent/CLI discovery to a manifest |
7579
- | `runSurfaceProbes` | function | execute explicit transport drivers with per-probe setup, teardown, timeout and cancellation |
7580
- | `TransportObservationSchema` | constant | normalized success/validation/domain-error/aborted probe outcome |
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 |
7581
7867
  | `ConformanceTransport` | _type_ | explicit probe transport union |
7582
7868
  | `RunSurfaceProbesConfig` | _type_ | probes, drivers and diagnostic byte cap |
7583
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 |
7584
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 |
7585
7879
  | `TransportObservation` | _type_ | validated normalized driver result |
7586
7880
  | `SurfaceManifest` / `SurfaceManifestConfig` | _type_ | deterministic surface snapshot and its inputs |
7587
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 |
7588
7885
  | `SurfaceManifestExtension` / `SurfaceManifestExtensionSchema` | _type_ / schema | declared transport extension row |
7589
7886
  | `SurfaceManifestSchema` | schema | complete deterministic manifest schema |
7590
- | `SurfaceRuntimeToolDefinition` | _type_ | peer-free structural runtime-operation subset accepted by manifests |
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 |
7591
7894
  | `SurfaceSchemaDigestsSchema` | schema | params/input/output/multipart digest object |
7592
7895
  | `serializeSurfaceValue` | function | canonical versioned serialization used for deterministic digests |
7593
7896
 
@@ -7600,16 +7903,16 @@ available from `stitchkit/contract`.
7600
7903
 
7601
7904
  | Export | Kind | Summary |
7602
7905
  |--------|------|---------|
7603
- | `createManagedFileBoundary` | function | bind one existing application-owned root for capped reads and atomic writes |
7906
+ | `createManagedFileBoundary` | function | bind an application-owned root, optionally creating one final directory under a trusted existing parent |
7604
7907
  | `ManagedFileBoundary` | _type_ | non-reopenable `read`/`write` capability over canonical relative paths |
7605
- | `ManagedFileBoundaryConfig` | _type_ | bound root, finite limits, inspector and cleanup observer |
7908
+ | `ManagedFileBoundaryConfig` | _type_ | bound root, optional `createRoot`, finite read/write/inspection limits, inspector and cleanup observer |
7606
7909
  | `ManagedFileRefSchema` / `ManagedFileRef` | schema / _type_ | transport-safe relative path, measured size and optional media metadata |
7607
7910
  | `ManagedFilePathSchema` / `ManagedFilePath` | schema / _type_ | canonical POSIX relative managed-file path |
7608
7911
  | `ManagedFileSource` | _type_ | bounded immutable bytes plus validated ref passed to upload callbacks |
7609
7912
  | `ManagedFileReadOptions` / `ManagedFileWriteOptions` | _type_ | per-operation byte cap, signal and atomic write policy |
7610
- | `ManagedFileError` / `ManagedFileErrorCode` | class / _type_ | stable caller-safe boundary failures |
7611
- | `ManagedFileInspector` | _type_ | bounded-prefix content inspection callback that cannot own path or size |
7612
- | `ManagedFileInspectionInput` / `ManagedFileInspection` | _type_ | inspector input and normalized metadata-only result |
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 |
7613
7916
 
7614
7917
  ---
7615
7918
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "stitchkit",
3
- "version": "0.55.0",
3
+ "version": "0.56.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",