stitchkit 0.49.1 → 0.50.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.
package/llms-full.txt CHANGED
@@ -617,6 +617,21 @@ the exact config without `NonNullable` or another wrapper. Plain `ContractDef`
617
617
  keeps optional scope because ordinary `defineContract` still supports its
618
618
  default-public overload.
619
619
 
620
+ The union covers **per-endpoint overrides too**, not just the contract's scope —
621
+ that is where typos are densest, and one there used to compile and then fail
622
+ closed at request time (`[stitchkit] auth: no rule for scope "…"`):
623
+
624
+ ```ts
625
+ defineContract({ prefix: 'posts', scope: 'user' }, {
626
+ drop: { method: 'DELETE', path: '/:id', desc: 'Drop', scope: 'admn', … },
627
+ // ^ compile error, and TypeScript
628
+ // suggests 'admin'
629
+ })
630
+ ```
631
+
632
+ Contracts built with plain `defineContract` are unaffected — their endpoint
633
+ `scope` stays a free string.
634
+
620
635
  ## One source of truth
621
636
 
622
637
  A contract is plain data — no classes, no decorators, no codegen. It is imported
@@ -686,6 +701,74 @@ export const implement = createImplement<AppContext>()
686
701
  // every implement() call now has ctx.user typed
687
702
  ```
688
703
 
704
+ ### Per-scope handler context — `createScopedImplement`
705
+
706
+ `createImplement<Ctx>()` gives every handler the **same** context type. In an app
707
+ where each scope guarantees different injected fields, that single type is a
708
+ superset: a `public` handler is told it has a `userId` the runtime never injects
709
+ there. `createScopedImplement` types each handler by its endpoint's **effective**
710
+ scope instead — the endpoint's own `scope`, else the contract's:
711
+
712
+ ```ts
713
+ import { createScopedImplement } from 'stitchkit/server'
714
+
715
+ export const implementFor = createScopedImplement<{
716
+ public: object // no extra fields
717
+ user: { userId: string }
718
+ admin: { userId: string; isAdmin: boolean }
719
+ }>()
720
+
721
+ implementFor(postsContract, {
722
+ list: (ctx) => listPosts(ctx.userId), // contract scope 'user'
723
+ purge: (ctx) => (ctx.isAdmin ? purge() : []), // endpoint scope 'admin'
724
+ ping: () => ({ ok: true }), // endpoint scope 'public'
725
+ })
726
+ ```
727
+
728
+ The map is **type-only** — scope fields are types, so there is no runtime
729
+ argument and no `{} as UserFields` at the call site. `'public'` must be a key:
730
+ a contract without a `scope` is `'public'`. A scope outside the map is a compile
731
+ error on that endpoint, naming the scope.
732
+
733
+ Three boundaries worth knowing:
734
+
735
+ - A field of another scope is not a compile error on access — `RuntimeContext`
736
+ keeps its `[key: string]: unknown` index signature (transports write through
737
+ it). It degrades to `unknown`, so it can no longer pose as a `string`; using it
738
+ in a typed position fails.
739
+ - The map states what **your** `beforeHandle` / `createAuthHook.inject` puts on
740
+ the context. The framework does not verify that claim (→ ADR 0075).
741
+ - An endpoint hoisted out of the contract literal widens its `scope` to `string`,
742
+ which is no key of any map, and lands in the error branch. An endpoint whose
743
+ `scope` is added by a conditional spread is optional, so it resolves to **both**
744
+ scopes and is typed against what they guarantee in common. Write endpoints
745
+ inline for the precise type.
746
+ - Scope keys are string literals. A map declared as an index signature
747
+ (`Record<string, …>`) makes every scope valid and disables the check.
748
+
749
+ The same map has a registry form and a streaming form:
750
+
751
+ ```ts
752
+ // one contract registry, handlers still typed per endpoint scope
753
+ const implementAll = createScopedImplementRegistry<Scopes>()
754
+ export const services = implementAll(apiContractRegistry, { posts, users })
755
+
756
+ // a streaming multipart handler that reads its scope's fields. The endpoint
757
+ // must declare its own `scope`, and only that literal is accepted: an endpoint
758
+ // inheriting the contract's scope cannot be verified from here, and guessing it
759
+ // would rebuild the superset this factory removes.
760
+ implementFor(mediaContract, {
761
+ upload: implementFor.stream('admin', mediaContract.endpoints.upload, {
762
+ files: { file: receiveFile },
763
+ handler: ({ files, userId }) => store(files.file, userId),
764
+ }),
765
+ })
766
+ ```
767
+
768
+ Outside a scoped app, `createMultipartStream<Ctx>()` does for streaming handlers
769
+ what `createImplement<Ctx>()` does for ordinary ones. Plain
770
+ `defineMultipartStream` still gives the loose `RuntimeContext`.
771
+
689
772
  ### `implementRegistry` — one backend registry
690
773
 
691
774
  When contracts already live in one literal registry, bind the backend from that
@@ -974,9 +1057,11 @@ createServer({
974
1057
 
975
1058
  A prefix may carry `:param` segments (they land on the context exactly as above).
976
1059
  Services listed under explicit `groups` are unaffected — the group prefix wins.
977
- Scope stays a free string; the core attaches no meaning beyond this lookup
978
- (→ ADR 0024). When each scope needs a different handler-context shape, declare one
979
- `createImplement<Ctx>()` per scope rather than a single superset context.
1060
+ Scope stays a free string (unless the contract is authored through
1061
+ `createContractFactory<Scope>()`); the core attaches no meaning beyond this lookup
1062
+ (→ ADR 0024). When each scope needs a different handler-context shape, use
1063
+ [`createScopedImplement`](#per-scope-handler-context--createscopedimplement)
1064
+ rather than a single superset context.
980
1065
 
981
1066
  ## Lifecycle hooks
982
1067
 
@@ -3578,7 +3663,9 @@ the scope vocabulary are yours.
3578
3663
  ## Scopes
3579
3664
 
3580
3665
  An endpoint declares a `scope` — a free string. The contract `meta.scope` is the
3581
- default for endpoints that declare none.
3666
+ default for endpoints that declare none. (Authored through
3667
+ [`createContractFactory<Scope>()`](./contracts.md#scope), both the contract's
3668
+ scope and any endpoint override are held to your own union instead.)
3582
3669
 
3583
3670
  ```ts
3584
3671
  export const posts = defineContract({ prefix: 'posts', scope: 'user' }, {
@@ -3870,36 +3957,57 @@ onError: (ctx, err) => {
3870
3957
 
3871
3958
  ## Domain errors — `defineErrors`
3872
3959
 
3873
- Declare each domain code, HTTP status and optional structured-details schema in
3874
- one immutable registry. The generated functions construct typed branded
3875
- `AppError` instances; ordinary `throw` remains explicit at the call site:
3960
+ Declare each domain code, HTTP status, default message and optional
3961
+ structured-details schema in one immutable registry. The generated functions
3962
+ construct typed branded `AppError` instances; ordinary `throw` remains explicit
3963
+ at the call site:
3876
3964
 
3877
3965
  ```ts
3878
3966
  import { z } from 'zod'
3879
3967
 
3880
3968
  export const { errors, codes, definitions, isCode } = defineErrors({
3881
- SESSION_NOT_FOUND: { status: 404 },
3969
+ SESSION_NOT_FOUND: { status: 404, message: 'No such session' },
3882
3970
  QUOTA_EXCEEDED: {
3883
3971
  status: 429,
3972
+ message: 'Monthly quota exhausted',
3884
3973
  details: z.object({ retryAfterSeconds: z.number().int().positive() }),
3885
3974
  },
3886
3975
  })
3887
3976
 
3888
- throw errors.SESSION_NOT_FOUND({ message: 'No such session' })
3977
+ throw errors.SESSION_NOT_FOUND() // uses the declared message
3889
3978
  throw errors.QUOTA_EXCEEDED({
3890
- message: 'Try later',
3979
+ message: 'Try later', // overrides it here only
3891
3980
  details: { retryAfterSeconds: 30 },
3892
3981
  hint: 'Wait for the current window to expire',
3893
3982
  })
3894
3983
 
3895
3984
  // Construction without throwing is useful for composition or inspection.
3896
3985
  const error = errors.QUOTA_EXCEEDED({ details: { retryAfterSeconds: 30 } })
3897
- definitions.QUOTA_EXCEEDED.status // 429 — same source, no copied status map
3986
+ definitions.QUOTA_EXCEEDED.status // 429 — same source, no copied status map
3987
+ definitions[code].message // the declared text, by a `code` variable
3898
3988
 
3899
3989
  // client — match the code, never a magic string
3900
3990
  if (err instanceof ApiError && err.code === codes.SESSION_NOT_FOUND) { … }
3901
3991
  ```
3902
3992
 
3993
+ `message` is optional: declare it once instead of repeating the sentence at every
3994
+ `throw`, and a per-call `message` still wins. A code without one keeps the
3995
+ existing fallback — `AppError.message` is the code itself. An empty declared
3996
+ message is rejected when the registry is declared, not when the error is thrown.
3997
+
3998
+ **The tool envelope has no `message` field**, and what the model sees depends on
3999
+ whether the code declares `details`:
4000
+
4001
+ | | HTTP / typed client | MCP / agent / CLI |
4002
+ |---|---|---|
4003
+ | code **with** a `details` schema | the declared message | no text at all — only `details` and `hint` |
4004
+ | code **without** one | the declared message | the same text, delivered as `details.message` |
4005
+
4006
+ The model-facing envelope is `{ error, details?, _hint? }`; for a code with no
4007
+ details schema the framework fills `details` with `{ message }`, so declaring a
4008
+ message changes what the model reads there — it used to be the code itself. Put
4009
+ anything the model must reliably read in `details` or `hint`, not in `message`.
4010
+
3903
4011
  With no `details` schema, the options object forbids `details`. A required
3904
4012
  `z.object` makes `details` required; `z.object(...).optional()` makes it
3905
4013
  optional. Supplied details are parsed when the error is constructed, before any
@@ -4612,8 +4720,9 @@ expect(res.status).toBe(404)
4612
4720
  expect(await res.json()).toEqual({ error: { code: 'NOT_FOUND', message: 'Note not found' } })
4613
4721
  ```
4614
4722
 
4615
- stitchkit's own suite — 143 tests in `packages/core/tests` is the working
4616
- reference for testing each piece.
4723
+ The suite in `packages/core/tests` is the working reference for testing each
4724
+ piece. Its size changes with the public surface, so the guide does not pin a
4725
+ count that can drift independently from the test runner.
4617
4726
 
4618
4727
  ## Deployment
4619
4728
 
@@ -4636,34 +4745,58 @@ createServer({
4636
4745
  })
4637
4746
  ```
4638
4747
 
4639
- Keep the managed handle and wire process policy explicitly:
4748
+ ### Process signals `bindProcessSignals`
4749
+
4750
+ Keep the managed handle and wire process policy explicitly. The framework
4751
+ registers no signal listener on its own; `bindProcessSignals` is that explicit
4752
+ step, and it owns the state machine:
4640
4753
 
4641
4754
  ```ts
4755
+ import { bindProcessSignals } from 'stitchkit/server'
4756
+
4642
4757
  const server = createServer({ services, socket })
4643
- const force = new AbortController()
4644
- let closing: Promise<void> | undefined
4645
4758
 
4646
- function shutdown() {
4647
- if (closing) {
4648
- force.abort() // a later signal shortens the same shutdown, not a second chain
4649
- return closing
4650
- }
4651
- closing = server.shutdown({
4652
- gracePeriodMs: 30_000,
4653
- forceTimeoutMs: 5_000,
4654
- signal: force.signal,
4655
- }).then(async result => {
4759
+ bindProcessSignals(server, {
4760
+ shutdown: { gracePeriodMs: 30_000, forceTimeoutMs: 5_000 },
4761
+ onShutdown: () => stopSchedulers(), // before the drain starts
4762
+ onComplete: async (result) => { // after it finishes
4656
4763
  await mcp.close()
4657
4764
  await prisma.$disconnect()
4658
- console.log(result)
4659
- })
4660
- return closing
4661
- }
4662
-
4663
- process.on('SIGTERM', () => void shutdown())
4664
- process.on('SIGINT', () => void shutdown())
4765
+ process.exitCode = result.outcome === 'clean' ? 0 : 1
4766
+ },
4767
+ onError: (error) => {
4768
+ console.error(error)
4769
+ process.exitCode = 1
4770
+ },
4771
+ })
4665
4772
  ```
4666
4773
 
4774
+ - The first signal runs `onShutdown`, then one `shutdown()`.
4775
+ - A later signal **forces that same chain** — never a second shutdown. Signals
4776
+ delivered in the same turn as the first (a supervisor sending `SIGINT` and
4777
+ `SIGTERM` together) are not counted, so the grace period is not collapsed to
4778
+ zero.
4779
+ - The signal after the force — or one arriving while `onComplete` still runs —
4780
+ re-delivers the signal so its default disposition applies, letting an operator
4781
+ kill a process stuck on some other resource. This works only while nothing else
4782
+ in the process listens for that signal; if something does, the signal is
4783
+ swallowed and `onEscalationBlocked` fires instead. The framework will not call
4784
+ `process.exit` on your behalf.
4785
+ - `shutdown` budgets are validated when you bind, not when the signal arrives.
4786
+ `signal` is not accepted there — the binding owns it, and that is what makes a
4787
+ later signal force the first chain.
4788
+ - `onError(phase, error)` separates the phases. A failing `onShutdown`
4789
+ (`'prepare'`) is reported and the shutdown **still runs** — a failed
4790
+ preparation must not leave the server listening. A failing `onComplete`
4791
+ (`'complete'`) leaves `promise` resolved, because the transport did shut down.
4792
+ Only a failing `shutdown()` (`'shutdown'`) rejects it.
4793
+
4794
+ `close()` removes the listeners. Closing a binding that never received a signal
4795
+ resolves `promise` with `undefined`, so an awaiting application does not hang;
4796
+ closing one whose chain is already running leaves that chain to finish and keeps
4797
+ the handle claimed, since a second binding could not force it. `promise` is
4798
+ already observed internally, so ignoring it never raises an `unhandledRejection`.
4799
+
4667
4800
  The server owns HTTP/Socket.IO transport resources. MCP, databases, queues and
4668
4801
  domain run-state remain application resources and close explicitly after server
4669
4802
  drain. Do not call `runtime.stop()` or `socket.io.close()` in parallel with
@@ -4796,10 +4929,20 @@ createServer({
4796
4929
  Handlers read `ctx.tenantId` (a raw `string` — narrow it). To get it typed inside
4797
4930
  `ctx.params`, add `tenantId` to the endpoint's `params` schema (a `z.strictObject`
4798
4931
  that omits it will reject the request). When each scope guarantees different
4799
- injected fields, call `createImplement<TenantCtx>()` / `createImplement<BaseCtx>()`
4800
- **once per scope** and implement each contract with the matching factory — every
4801
- handler is typed to its scope, with no superset context that lies about a
4802
- `tenantId` a `public` handler never has.
4932
+ injected fields, declare the scope map once with
4933
+ [`createScopedImplement`](./server.md#per-scope-handler-context--createscopedimplement):
4934
+
4935
+ ```ts
4936
+ export const implementFor = createScopedImplement<{
4937
+ public: object
4938
+ tenant: { tenantId: string; userId: string }
4939
+ project: { projectId: string; userId: string }
4940
+ }>()
4941
+ ```
4942
+
4943
+ Each handler is then typed by its endpoint's effective scope — no superset
4944
+ context that promises a `tenantId` a `public` handler never receives. A contract
4945
+ that mixes scopes across its endpoints needs no splitting.
4803
4946
 
4804
4947
  ## 3. Auth — gate the tenant in the path
4805
4948
 
@@ -4989,6 +5132,69 @@ current one *up to* your target, and apply each snippet.
4989
5132
  runtime): bootstrap the server, one HTTP request, and any feature you rely on
4990
5133
  (Socket.IO connect, an MCP tool call, a multipart upload, …).
4991
5134
 
5135
+ ## Released migration: 0.50.0
5136
+
5137
+ ### The factory's scope union now covers per-endpoint overrides
5138
+
5139
+ `createContractFactory<Scope>()` already required a typed `scope` on the
5140
+ contract. It now holds a per-endpoint `scope` override to the same union. Nothing
5141
+ to migrate unless an override is outside the union — which was always a bug: the
5142
+ scope reached no auth rule, and `createAuthHook` threw
5143
+ `[stitchkit] auth: no rule for scope "…"` on the first request to it.
5144
+
5145
+ ```ts
5146
+ const { defineContract } = createContractFactory<'public' | 'user' | 'admin'>()
5147
+
5148
+ defineContract({ prefix: 'posts', scope: 'user' }, {
5149
+ // before: compiled; failed at request time
5150
+ // after: compile error naming the scope (TypeScript even suggests 'admin')
5151
+ purge: { method: 'DELETE', path: '/all', desc: 'Purge', scope: 'admn', output },
5152
+ })
5153
+ ```
5154
+
5155
+ Fix the typo, or widen the factory union if the scope is real. Contracts built
5156
+ with plain `defineContract` are unaffected.
5157
+
5158
+ ### A declared `defineErrors` message is now used
5159
+
5160
+ `ErrorDefinition` accepts `message`. Nothing to migrate unless a registry already
5161
+ wrote that key: it type-checked before (excess-property checking does not fire
5162
+ through a `const` generic) and was ignored, so the code itself went on the wire.
5163
+
5164
+ ```ts
5165
+ const { errors } = defineErrors({ GONE: { status: 410, message: 'Long gone' } })
5166
+ // before: errors.GONE().message === 'GONE'
5167
+ // after: errors.GONE().message === 'Long gone'
5168
+ ```
5169
+
5170
+ If a registry carried `message` as a note to the reader rather than as
5171
+ user-facing text, either fix the text or drop the key. A code with no `details`
5172
+ schema also shows that text to a model (its tool `details` is `{ message }`).
5173
+
5174
+ ### Optional: adopt `createScopedImplement`
5175
+
5176
+ If the app declares one superset handler context because different scopes inject
5177
+ different fields, replace it with a scope map. This is additive — `createImplement`
5178
+ still works.
5179
+
5180
+ ```ts
5181
+ // before — one context for every scope; `ctx.userId` is typed even in a
5182
+ // `public` handler, where the runtime never injects it
5183
+ interface AppContext extends RuntimeContext { userId: string; isAdmin: boolean }
5184
+ export const implement = createImplement<AppContext>()
5185
+
5186
+ // after — each handler typed by its endpoint's effective scope
5187
+ export const implementFor = createScopedImplement<{
5188
+ public: object
5189
+ user: { userId: string }
5190
+ admin: { userId: string; isAdmin: boolean }
5191
+ }>()
5192
+ ```
5193
+
5194
+ `'public'` must be a key (a contract with no `scope` is `'public'`). Write
5195
+ endpoints inline in the contract literal: an endpoint hoisted into a variable
5196
+ widens its `scope` to `string` and is reported as undeclared.
5197
+
4992
5198
  ## Released migration: 0.48.0
4993
5199
 
4994
5200
  ### Typed-client request options move to `.withOptions`
@@ -5918,13 +6124,14 @@ from the root `stitchkit`.
5918
6124
  | Export | Kind | Summary |
5919
6125
  |--------|------|---------|
5920
6126
  | `defineContract` | function | declare a contract — [guide](../guide/contracts.md#definecontract) |
5921
- | `createContractFactory` | function | a `defineContract` with a required allowed scope that retains each concrete literal — [guide](../guide/contracts.md#scope) |
6127
+ | `createContractFactory` | function | a `defineContract` whose scope — on the contract **and** on any endpoint override — is required and held to your union, retaining each concrete literal — [guide](../guide/contracts.md#scope) |
5922
6128
  | `ContractFactoryConfig` | _type_ | optional scoped-factory policy, including explicit tool exposure |
5923
6129
  | `ContractFactoryToolExposure` | _type_ | `'explicit'` — omitted endpoint exposure materializes as HTTP-only |
5924
6130
  | `ExplicitScopedDefineContract` | _type_ | scoped factory authoring with explicit tool opt-in |
5925
6131
  | `ExplicitToolExposureEndpoints` | _type_ | endpoint map after missing exposure is materialized as `['HTTP']` |
6132
+ | `FactoryScopedEndpoint` | _type_ | an endpoint authored through the factory — its own `scope` override is held to the same union |
5926
6133
  | `ScopedContractDef` | _type_ | a factory-defined contract whose `meta.scope` is the required concrete literal |
5927
- | `ScopedDefineContract` | _type_ | the `defineContract` `createContractFactory` returns |
6134
+ | `ScopedDefineContract` | _type_ | the `defineContract` `createContractFactory` returns — endpoint `scope` overrides join the union |
5928
6135
  | `ALL_TRANSPORTS` | constant | `['HTTP', 'MCP', 'AGENT', 'CLI']` |
5929
6136
  | `ContractDef` | _type_ | a defined contract |
5930
6137
  | `ContractMeta` | _type_ | a contract's `prefix` + optional `scope` and `meta` (a default every endpoint shallow-merges over) |
@@ -5974,7 +6181,7 @@ from the root `stitchkit`.
5974
6181
  | `defineErrors` | function | declare immutable domain error definitions → typed `AppError` constructors, codes and schemas — [guide](../guide/auth-and-errors.md#domain-errors--defineerrors) |
5975
6182
  | `DefinedErrors` | _type_ | the `{ errors, codes, definitions, isCode }` handle `defineErrors` returns |
5976
6183
  | `DefinedAppError` | _type_ | literal-code error instance with schema-refined details |
5977
- | `ErrorDefinition` | _type_ | `{ status, details? }` definition for one domain code |
6184
+ | `ErrorDefinition` | _type_ | `{ status, message?, details? }` definition for one domain code |
5978
6185
  | `ErrorDefinitions` | _type_ | string-keyed domain error definition registry |
5979
6186
  | `ErrorDetailsSchema` | _type_ | required or optional Zod object accepted for structured details |
5980
6187
  | `ErrorDetailsOutput` | _type_ | parsed details inferred from one definition |
@@ -6010,6 +6217,15 @@ Also re-exports the error helpers from `stitchkit/contract`.
6010
6217
  | `createHandler` | function | the router as a bare `(req) => Response` — [guide](../guide/server.md#createserver) |
6011
6218
  | `implement` | function | bind a contract to typed handlers — [guide](../guide/server.md#implement) |
6012
6219
  | `createImplement` | function | fix the handler context type once |
6220
+ | `createScopedImplement` | function | fix one scope→context map, then type every handler by its endpoint's effective scope — [guide](../guide/server.md#per-scope-handler-context--createscopedimplement) |
6221
+ | `ScopedHandlers` | _type_ | handler map typed per endpoint by its effective scope |
6222
+ | `ScopeContexts` | _type_ | scope → extra context fields map accepted by `createScopedImplement` |
6223
+ | `EffectiveScope` | _type_ | an endpoint's own `scope`, else its contract's group scope |
6224
+ | `createScopedImplementRegistry` | function | the registry form of `createScopedImplement` — one contract registry, handlers still typed per endpoint scope |
6225
+ | `ScopedRegistryHandlers` | _type_ | scoped handler registry inferred from a contract registry |
6226
+ | `ScopedImplementationRegistry` | _type_ | contract registry whose every group scope is a key of the scope map |
6227
+ | `StreamScope` | _type_ | the scope `createScopedImplement(...).stream` accepts for one endpoint |
6228
+ | `ExactScopedRegistryHandlers` | _type_ | fail-first scoped registry shape that rejects extra registry and endpoint keys |
6013
6229
  | `implementRegistry` | function | bind one exact contract registry to one exact backend handler registry |
6014
6230
  | `createImplementRegistry` | function | context-typed factory for `implementRegistry` |
6015
6231
  | `ImplementationRegistry` | _type_ | flat literal registry of concrete contracts accepted by `implementRegistry` |
@@ -6124,10 +6340,19 @@ Also re-exports the error helpers from `stitchkit/contract`.
6124
6340
  | `MultipartResult` | _type_ | what `parseMultipart` returns |
6125
6341
  | `parseMultipart` | function | parse a typed buffered/streaming multipart descriptor — [guide](../guide/server.md#multipart) |
6126
6342
  | `defineMultipartStream` | function | bind typed streaming file receivers and a final endpoint handler |
6343
+ | `createMultipartStream` | function | fix one handler context type for streaming multipart endpoints |
6344
+ | `MultipartStreamConfig` | _type_ | receivers plus the context-typed handler accepted by the streaming builders |
6127
6345
  | `MultipartFileMetadata` | _type_ | field, filename, declared media type and optional declared size |
6128
6346
  | `MultipartReceiver` | _type_ | consumer-owned Web-stream storage receiver |
6129
6347
  | `MultipartReceiverResult` | _type_ | receiver value plus rollback cleanup |
6130
6348
  | `StreamingMultipartImplementation` | _type_ | receiver registry and handler shape inferred by `defineMultipartStream` |
6349
+ | `bindProcessSignals` | function | bind `SIGINT` / `SIGTERM` to one managed `shutdown()` — one chain, force on a later signal, default disposition on the one after — [guide](../guide/testing-and-deployment.md#process-signals--bindprocesssignals) |
6350
+ | `ProcessSignalsOptions` | _type_ | config for `bindProcessSignals` |
6351
+ | `ProcessSignalsBinding` | _type_ | the `{ promise, close }` handle `bindProcessSignals` returns |
6352
+ | `ProcessSignalsErrorPhase` | _type_ | `'prepare' \| 'shutdown' \| 'complete'` — which phase an `onError` report came from |
6353
+ | `SignalSource` | _type_ | injectable signal source — `process` by default |
6354
+ | `ProcessSignalName` | _type_ | signal names the binding accepts (owned, so the published types need no `@types/node`) |
6355
+ | `ShutdownTarget` | _type_ | the `shutdown`-only slice of a managed handle a binding needs |
6131
6356
  | `createRateLimiter` | function | token-bucket rate limiting — [guide](../guide/server.md#rate-limiting) |
6132
6357
  | `createCache` | function | an in-memory TTL cache |
6133
6358
  | `CacheOptions` | _type_ | bounded-cache options, including the maximum retained entry count |
@@ -6433,7 +6658,7 @@ runtime-agnostic pieces of `stitchkit/server` and the error helpers.
6433
6658
  | `serveNode` | function | build the router and start a Node HTTP server (via `srvx`) |
6434
6659
  | `createHandler` | function | the router as a bare `(req) => Response` (same as `/server`) |
6435
6660
  | `createSocketIOServer` | function | the typed Node Socket.IO server (`io` + `attach`; no Bun engine declarations) |
6436
- | `implement` / `createImplement` | function | bind a contract to typed handlers (same as `/server`) |
6661
+ | `implement` / `createImplement` / `createScopedImplement` / `createScopedImplementRegistry` / `createMultipartStream` | function | bind a contract to typed handlers, optionally typed per endpoint scope (same as `/server`) |
6437
6662
  | `NodeServerConfig` | _type_ | config for `serveNode` |
6438
6663
  | `NodeServerHandle` | _type_ | managed Node handle (`url`, `port`, `runtime`, `status`, `shutdown`) |
6439
6664
  | `NodeRuntimeServer` | _type_ | concrete `srvx/node` runtime escape hatch |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "stitchkit",
3
- "version": "0.49.1",
3
+ "version": "0.50.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",