stitchkit 0.25.0 → 0.27.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 (59) hide show
  1. package/dist/browser/client-multipart.d.ts +22 -0
  2. package/dist/browser/client-multipart.d.ts.map +1 -0
  3. package/dist/browser/client.d.ts.map +1 -1
  4. package/dist/browser/http.d.ts +7 -1
  5. package/dist/browser/http.d.ts.map +1 -1
  6. package/dist/cli.js +2 -2
  7. package/dist/contract/define.d.ts +79 -2
  8. package/dist/contract/define.d.ts.map +1 -1
  9. package/dist/contract/factory.d.ts +1 -0
  10. package/dist/contract/factory.d.ts.map +1 -1
  11. package/dist/contract/index.js +1 -1
  12. package/dist/{index-h9d1fm0p.js → index-4whcb3c3.js} +64 -22
  13. package/dist/{index-a9n8m4ec.js → index-jvescqgr.js} +1 -1
  14. package/dist/{index-x62gnfsk.js → index-pmftwk2a.js} +19 -1
  15. package/dist/{index-g8kyab85.js → index-y4132gqh.js} +81 -12
  16. package/dist/{index-tje0q6gp.js → index-zfscdskj.js} +6 -1
  17. package/dist/{index-4gawbm74.js → index-zza375qp.js} +41 -3
  18. package/dist/index.js +39 -25
  19. package/dist/internal/errors.d.ts +6 -1
  20. package/dist/internal/errors.d.ts.map +1 -1
  21. package/dist/internal/write-download.d.ts +17 -0
  22. package/dist/internal/write-download.d.ts.map +1 -0
  23. package/dist/node.js +3 -3
  24. package/dist/server/create.d.ts.map +1 -1
  25. package/dist/server/error-hook.d.ts +14 -3
  26. package/dist/server/error-hook.d.ts.map +1 -1
  27. package/dist/server/implement.d.ts.map +1 -1
  28. package/dist/server/index.d.ts +2 -1
  29. package/dist/server/index.d.ts.map +1 -1
  30. package/dist/server/index.js +21 -8
  31. package/dist/server/middleware/cors.d.ts +19 -0
  32. package/dist/server/middleware/cors.d.ts.map +1 -1
  33. package/dist/server/openapi.d.ts.map +1 -1
  34. package/dist/server/router.d.ts +27 -0
  35. package/dist/server/router.d.ts.map +1 -1
  36. package/dist/server/types.d.ts +44 -1
  37. package/dist/server/types.d.ts.map +1 -1
  38. package/dist/tools/agent.d.ts +2 -0
  39. package/dist/tools/agent.d.ts.map +1 -1
  40. package/dist/tools/cli.d.ts.map +1 -1
  41. package/dist/tools/execute.d.ts +1 -1
  42. package/dist/tools/execute.d.ts.map +1 -1
  43. package/dist/tools/list-names.d.ts +2 -1
  44. package/dist/tools/list-names.d.ts.map +1 -1
  45. package/dist/tools/mcp.d.ts +10 -3
  46. package/dist/tools/mcp.d.ts.map +1 -1
  47. package/dist/tools/mount-download.d.ts.map +1 -1
  48. package/dist/tools/mount-upload.d.ts.map +1 -1
  49. package/dist/tools/mount-wait.d.ts.map +1 -1
  50. package/dist/tools/mount.d.ts +13 -0
  51. package/dist/tools/mount.d.ts.map +1 -1
  52. package/dist/tools/names.d.ts +34 -0
  53. package/dist/tools/names.d.ts.map +1 -1
  54. package/dist/tools/remote.d.ts.map +1 -1
  55. package/dist/tools/transports.d.ts.map +1 -1
  56. package/dist/tools.js +69 -49
  57. package/llms-full.txt +223 -12
  58. package/llms.txt +1 -1
  59. package/package.json +1 -1
package/llms-full.txt CHANGED
@@ -237,11 +237,13 @@ export const users = defineContract({ prefix: 'users' }, {
237
237
  | `output` | no | Zod schema for the **response body** |
238
238
  | `scope` | no | access scope for this endpoint — see [Auth & errors](./auth-and-errors.md) |
239
239
  | `expose` | no | which transports carry this endpoint — see [below](#transports) |
240
- | `toolName` | no | explicit MCP / agent tool name (defaults to `prefix_key`) |
240
+ | `toolName` | no | explicit MCP / agent tool name (default: a verb-aware derivation, see below — not a literal `prefix_key`) |
241
241
  | `multipart` | no | field name of a file upload — see [below](#file-uploads) |
242
242
  | `timeout` | no | per-endpoint client timeout in ms, for slow endpoints |
243
243
  | `idempotent` | no | safe to call twice with the same input (like `PUT`/`DELETE`); a retrying transport reads it — see [Realtime](./realtime.md#bring-your-own-transport) |
244
244
  | `meta` | no | opaque app metadata — read in hooks / on tool mounts, never in OpenAPI ([below](#endpoint-metadata-meta)) |
245
+ | `rawResponse` | no | the handler returns the `Response` itself — a download, a file, an SSE stream. HTTP-only, never a tool, no `output`. See [Raw-response endpoints](./server.md#raw-response-endpoints) |
246
+ | `contentType` | no | documented response media type of a `rawResponse` endpoint (OpenAPI only) |
245
247
 
246
248
  ## `params` vs `input` vs `output`
247
249
 
@@ -313,8 +315,10 @@ tools. Narrow it with `expose`:
313
315
  - `expose: ['MCP', 'AGENT']` — a tool only; no HTTP route.
314
316
  - omit `expose` — all transports.
315
317
 
316
- Tool transports (`MCP`, `AGENT`) skip `multipart` endpoints automatically — a
317
- file upload is not a tool call.
318
+ Tool transports (`MCP`, `AGENT`) skip two kinds of endpoint automatically:
319
+ `multipart` (a file upload is not a tool call) and
320
+ [`rawResponse`](./server.md#raw-response-endpoints) (its answer is bytes, which
321
+ a tool result cannot carry — it would serialize to `{}`).
318
322
 
319
323
  ## `toolName`
320
324
 
@@ -326,8 +330,53 @@ derivation from the method key + prefix (`users` + `create` ⇒ `create_user`,
326
330
  { method: 'POST', path: '/', desc: 'Create a user', toolName: 'create_user', /* … */ }
327
331
  ```
328
332
 
333
+ **Every tool name — derived or explicit — must match `[a-zA-Z0-9_-]` and be at
334
+ most 64 characters**, the character class every major provider accepts.
335
+
336
+ Note the two classes differ. What is *accepted* includes the hyphen; what
337
+ derivation *keeps* does not — a derived name normalises everything outside
338
+ `[a-zA-Z0-9_]` to `_`, the hyphen included, so `bot-status` + `get` ⇒
339
+ `get_bot_status` and `admin/analytics` + `get` ⇒ `get_admin_analytics`. A hyphen
340
+ survives only in an explicit `toolName`, which is taken verbatim.
341
+
342
+ A prefix with *no* usable character at all (`'///'`, `'_'`, a fully non-ASCII
343
+ prefix) and any explicit `toolName` outside the accepted class **throw at
344
+ mount**. An unusable prefix is rescued by setting an explicit `toolName` — the
345
+ prefix then never enters the name. An over-long name is fixed by a shorter
346
+ explicit `toolName` (or a shorter prefix / method key). Nothing downstream checks this: the provider rejects the whole
347
+ request, so one bad name takes every tool of that mount down with it.
348
+ → [ADR 0035](../decisions/0035-tool-name-derivation-and-validation.md).
349
+
329
350
  ## Endpoint metadata (`meta`)
330
351
 
352
+ A contract can declare a **default** `meta` that every endpoint inherits, and an
353
+ endpoint's own keys are **shallow-merged over** it:
354
+
355
+ ```ts
356
+ defineContract({ prefix: 'admin', meta: { public: true } }, {
357
+ list: { method: 'GET', path: '/', desc: 'List', /* meta → { public: true } */ },
358
+ purge: { method: 'POST', path: '/purge', desc: 'Purge', meta: { rateTier: 2 } },
359
+ // meta → { public: true, rateTier: 2 }
360
+ })
361
+ ```
362
+
363
+ One level deep — no deep merge. To **opt an endpoint out** of an inherited key,
364
+ declare it with an explicit `undefined`:
365
+
366
+ ```ts
367
+ defineContract({ prefix: 'leads', meta: { page: 'LEADS' } }, {
368
+ // Public website form inside an otherwise admin-gated contract:
369
+ submit: { method: 'POST', path: '/', desc: 'Submit', meta: { page: undefined } },
370
+ // meta → { page: undefined } — gate off
371
+ list: { method: 'GET', path: '/list', desc: 'List' }, // meta → { page: 'LEADS' }
372
+ })
373
+ ```
374
+
375
+ The key stays *present* with value `undefined`, so read `meta` by **value**
376
+ (`method.meta?.page`), never by key membership. `expose` deliberately has **no**
377
+ contract-level equivalent
378
+ (→ [ADR 0036](../decisions/0036-contract-level-meta.md)).
379
+
331
380
  `meta` is an **opaque, app-defined** bag the core attaches no meaning to — the
332
381
  same escape-hatch spirit as `scope` being a free string ([ADR 0002](../decisions/0002-generic-core.md) /
333
382
  [ADR 0021](../decisions/0021-endpoint-meta-passthrough.md)). Declare app concerns
@@ -569,7 +618,7 @@ server. See [Testing & deployment](./testing-and-deployment.md).
569
618
  | `rawRoutes` | non-contract routes (see below) |
570
619
  | `maxUploadBytes` | default multipart upload cap (bytes); per-route `EndpointDef.maxUploadBytes` overrides |
571
620
  | `port` / `hostname` | listen address — port defaults to `3000` |
572
- | `cors` | CORS policy — `{ origin, … }` |
621
+ | `cors` | CORS policy — `{ origin, credentials, methods, headers, exposeHeaders }` |
573
622
  | `hooks` | lifecycle hooks (see below) |
574
623
  | `logging` | `true` for built-in request logs, or a custom `StitchLogger` |
575
624
  | `traceId` | override per-request trace-id resolution |
@@ -689,6 +738,87 @@ createServer({
689
738
  Hooks see `RuntimeContext` (loose types); handlers see `HandlerContext` (typed).
690
739
  That split is deliberate — see [ADR 0003](../decisions/0003-two-context-types.md).
691
740
 
741
+ ## Raw-response endpoints
742
+
743
+ An endpoint that answers with **bytes rather than data** — a PDF download, a
744
+ file, an SSE stream — declares `rawResponse: true` and returns the `Response` itself:
745
+
746
+ ```ts
747
+ // contract
748
+ export const documents = defineContract(
749
+ { prefix: 'documents', scope: 'admin' },
750
+ {
751
+ download: {
752
+ method: 'GET', path: '/:id/pdf', desc: 'Download a document as a PDF',
753
+ params: z.object({ id: z.uuid() }),
754
+ rawResponse: true, contentType: 'application/pdf',
755
+ },
756
+ },
757
+ )
758
+
759
+ // handler — no guard on the first line; `beforeHandle` already ran
760
+ download: (ctx) => serveFile(ctx.req, { path: pathFor(ctx.params.id),
761
+ filename: 'offer.pdf' }),
762
+ ```
763
+
764
+ The request half is untouched: `params`, `input` and `multipart` parse and
765
+ validate exactly as elsewhere, and the endpoint goes through `beforeHandle` — so
766
+ the **auth gate applies without a guard in the handler**. Only the response is
767
+ handed over, so there is no `output` schema, `afterHandle` is skipped (it
768
+ transforms data; there is none) and the endpoint is HTTP-only: never an MCP
769
+ tool, an agent tool or a CLI command. Declaring `output`, `toolName`, `ui`,
770
+ `annotations` or a non-HTTP `expose` alongside `rawResponse` is a type error, and throws
771
+ at definition time for a contract assembled at runtime.
772
+
773
+ On the typed client the method resolves to the untouched `Response` — the
774
+ filename lives in `Content-Disposition`, so a `Blob` alone would lose it:
775
+
776
+ ```ts
777
+ const client = createClient(documents, http)
778
+ const res = await client.download({ id }) // Response
779
+ const name = res.headers.get('Content-Disposition')
780
+ const blob = await res.blob()
781
+ ```
782
+
783
+ Cross-origin, remember that those headers are readable only because CORS exposes
784
+ them — see [`cors.exposeHeaders`](#serving-files--range-requests).
785
+
786
+ **Raw response or [raw route](#raw-routes)?** Both hand the `Response` to your
787
+ code. A raw-response *endpoint* stays in the contract: only its response is
788
+ raw — it is still routed, gated, typed and documented like every other endpoint.
789
+ A raw *route* is outside the contract entirely — no schemas, no auth gate, no
790
+ client — which is what you want for an OAuth redirect or a webhook, and what you
791
+ do not want for a download.
792
+
793
+ ⚠️ **Delete the old raw route when you move an endpoint into the contract.** Raw
794
+ routes are matched **first**, so a leftover one keeps serving the bytes and the
795
+ contract endpoint — with its auth gate — never runs. stitchkit warns at startup
796
+ when a raw route shadows a contract route, naming both and the scope being
797
+ bypassed; treat that warning as a bug.
798
+
799
+ ⚠️ **A path built from user input needs a containment check.** `staticRoute`
800
+ enforces it; `serveFile` deliberately leaves it to the caller, so an endpoint
801
+ serving `/:filename` must not pass it through:
802
+
803
+ ```ts
804
+ import { isWithinDir, serveFile } from 'stitchkit/server'
805
+ import { resolve } from 'node:path'
806
+
807
+ const ROOT = resolve('./uploads')
808
+
809
+ file: (ctx) => {
810
+ const target = resolve(ROOT, ctx.params.filename)
811
+ // `../../etc/passwd` resolves outside ROOT — reject before touching disk.
812
+ if (!isWithinDir(ROOT, target)) throw notFound('File not found')
813
+ return serveFile(ctx.req, { path: target })
814
+ },
815
+ ```
816
+
817
+ `implementRemote` proxies a raw-response endpoint like any other — the remote
818
+ `Response` is forwarded verbatim. Request headers are not relayed, so a `Range`
819
+ sent to the proxy does not reach the origin and the full body comes back.
820
+ → ADR 0038.
821
+
692
822
  ## Raw routes
693
823
 
694
824
  Some routes cannot be a clean JSON contract — an OAuth redirect, a webhook with
@@ -782,6 +912,13 @@ It always sets `Accept-Ranges: bytes`, a weak `ETag` and `Last-Modified` (so
782
912
  and `nosniff`. `Content-Type` is auto-detected from the path — override it, or
783
913
  pass `disposition` / `cacheControl` / `etag: false`, via the options.
784
914
 
915
+ Cross-origin, the browser lets JavaScript read only the CORS-safelisted response
916
+ headers. stitchkit therefore exposes the download-relevant ones by default
917
+ (`Content-Disposition`, `Content-Range`, `ETag`, …) — without that a `fetch`-based
918
+ download cannot recover the file's name. Override with `cors.exposeHeaders`
919
+ (extend `DEFAULT_CORS_EXPOSE_HEADERS` rather than replacing it), or pass `[]` to
920
+ emit none.
921
+
785
922
  `serveFile` takes an explicit `path` and trusts it — **the caller owns
786
923
  containment**. For a URL-derived path use `staticRoute` (which enforces it) or
787
924
  `isWithinDir` first. The byte-range parser is exported on its own as
@@ -803,11 +940,20 @@ focused helper — not a sub-framework.
803
940
 
804
941
  ### SSE streaming
805
942
 
943
+ `streamSSE` returns a `Response`, so its endpoint declares
944
+ [`rawResponse: true`](#raw-response-endpoints) — in a plain contract handler the response
945
+ would be serialized into `{}` (that now fails loudly instead of shipping silently).
946
+
806
947
  ```ts
807
948
  import { streamSSE } from 'stitchkit/server'
808
949
 
950
+ // contract
951
+ stream: { method: 'GET', path: '/stream', desc: 'Stream tokens',
952
+ rawResponse: true, contentType: 'text/event-stream' },
953
+
954
+ // handler
809
955
  async function* tokens() { yield 'a'; yield 'b' }
810
- return streamSSE(tokens()) // → a text/event-stream Response
956
+ stream: () => streamSSE(tokens()), // → a text/event-stream Response
811
957
  ```
812
958
 
813
959
  The client side is [`parseSSE`](./client.md#sse).
@@ -1169,10 +1315,23 @@ By default every endpoint is a tool on every transport. `expose` narrows it:
1169
1315
  { method: 'GET', path: '/lookup', desc: 'Look up a price', expose: ['MCP'] } // MCP tool only
1170
1316
  ```
1171
1317
 
1318
+ Two kinds of endpoint are **never** tools, whatever `expose` says: a `multipart`
1319
+ upload (not a tool call), and a
1320
+ [`rawResponse`](./server.md#raw-response-endpoints) endpoint (its answer is
1321
+ bytes — a tool result cannot carry them, and it would reach the model as `{}`).
1322
+ Pin the full list with `listToolNames` in a snapshot test.
1323
+
1172
1324
  `desc` is the tool description the model reads — write it for the model, not
1173
- just for a human. A `multipart` endpoint is never a tool. The tool name defaults
1325
+ just for a human. The tool name defaults
1174
1326
  to a verb-aware name from the method + prefix (`list` → `list_widgets`, `get` →
1175
- `get_widget`); set `toolName` for an explicit one. See
1327
+ `get_widget`); set `toolName` for an explicit one. Derivation normalises every
1328
+ character outside `[a-zA-Z0-9_]` to `_` — the hyphen included, so `bot-status`
1329
+ derives `get_bot_status` — while a name is *accepted* if it matches
1330
+ `[a-zA-Z0-9_-]`, so a hyphen survives in an explicit `toolName`. A name that
1331
+ still cannot be delivered (illegal explicit `toolName`, over 64 characters, or a
1332
+ prefix with no usable character) throws at mount rather than at the first model
1333
+ call —
1334
+ → [ADR 0035](../decisions/0035-tool-name-derivation-and-validation.md). See
1176
1335
  [Contracts → transports](./contracts.md#transports).
1177
1336
 
1178
1337
  ### Pinning tool names — `listToolNames`
@@ -1196,6 +1355,14 @@ now fails this test instead of silently breaking the clients that call the
1196
1355
  tool. It is also the mechanical diff when migrating a service: run it before
1197
1356
  and after, compare.
1198
1357
 
1358
+ > **This is also the guard against a forgotten `expose`.** An endpoint that
1359
+ > declares none is a tool on MCP **and** AGENT — the default is fail-open, and
1360
+ > there is no contract-level `expose` to set once (→
1361
+ > [ADR 0036](../decisions/0036-contract-level-meta.md)). A snapshot of
1362
+ > `listToolNames` fails the build the moment an endpoint you meant to keep
1363
+ > HTTP-only shows up in the list, which is the one check that catches it however
1364
+ > many places the line was forgotten.
1365
+
1199
1366
  ## MCP — `createMcpHandler`
1200
1367
 
1201
1368
  `createMcpHandler` builds a complete Streamable-HTTP MCP server as a single
@@ -1234,11 +1401,11 @@ createServer({
1234
1401
  | `hooks` | tool-call observability hooks — `afterToolCall` fires on every result |
1235
1402
  | `onIncompatibleSchema` | `'throw'` (default) · `'skip'` · `'warn'` — see below |
1236
1403
  | `logger` | a `StitchLogger` for the `'warn'` policy |
1237
- | `nativeTools` | register non-contract tools directly on the `McpServer` |
1404
+ | `nativeTools` | `(server, auth) => …` — register non-contract tools directly on the `McpServer`; receives the resolved identity, but is **not** a scope gate (`lifecycle` does not run for native tools) |
1238
1405
  | `instructions` | a short host-facing usage hint, surfaced to MCP tool-search |
1239
1406
 
1240
- `services` and `context` receive the resolved identity, so a tenant can be
1241
- shown only its own tools and every handler can read `ctx.tenantId`.
1407
+ `services`, `context` and `nativeTools` all receive the resolved identity, so a
1408
+ tenant can be shown only its own tools and every handler can read `ctx.tenantId`.
1242
1409
 
1243
1410
  ### Guarding tools — `lifecycle`
1244
1411
 
@@ -2479,7 +2646,13 @@ const onError = createErrorHook({
2479
2646
  CONFLICT: 'conflict', RATE_LIMITED: 'rate_limited',
2480
2647
  INTERNAL_SERVER_ERROR: 'internal',
2481
2648
  } satisfies Record<StitchErrorCode, string>,
2482
- render: (info) => ({ ok: false, error: { code: info.code, message: info.message } }),
2649
+ // `ctx` is the request's RuntimeContext — read `ctx.traceId` for a
2650
+ // correlation id in the envelope. Declaring it is optional.
2651
+ render: (info, ctx) => ({
2652
+ ok: false,
2653
+ error: { code: info.code, message: info.message },
2654
+ traceId: ctx.traceId,
2655
+ }),
2483
2656
  })
2484
2657
 
2485
2658
  createServer({ services, hooks: { onError } })
@@ -3123,6 +3296,42 @@ current one *up to* your target, and apply each snippet.
3123
3296
  (`STITCH_ERROR_STATUS`, `serveFile`, `scopePrefixes`, `afterToolCall`'s
3124
3297
  `MethodDef`, `maxUploadBytes`) are available to adopt, not required.
3125
3298
 
3299
+ ## Your handlers may be returning more than the contract declares
3300
+
3301
+ stitchkit validates every handler's return value against the endpoint's `output`
3302
+ schema and **passes on the parsed result** — so any field the schema does not
3303
+ declare is silently removed. That is deliberate (the contract is the published
3304
+ shape of the response), but when you are moving a *live* API onto stitchkit it is
3305
+ invisible: TypeScript does not reject excess properties, nothing logs it, and the
3306
+ client just receives fewer fields.
3307
+
3308
+ While migrating, turn the diagnostic on:
3309
+
3310
+ ```ts
3311
+ createServer({ services, warnOnOutputStrip: true }) // off by default
3312
+ ```
3313
+
3314
+ Every removed key is logged as a dot-path with the endpoint that produced it
3315
+ (`notes.get: secret, nested.alsoSecret`). Tool transports strip identically —
3316
+ `mountMcp` / `mountAgent` take `onOutputStrip: (toolName, paths) => …`. Read the
3317
+ list, then either widen the contract or stop returning the field, and turn the
3318
+ flag back off: it is for the migration window, not for production.
3319
+
3320
+ ## Tool names may shift between versions
3321
+
3322
+ Derived tool names are part of your public surface — an MCP client config or an
3323
+ agent prompt refers to them by string. Before and after any upgrade that touches
3324
+ name derivation, diff them mechanically:
3325
+
3326
+ ```ts
3327
+ import { listToolNames } from 'stitchkit/tools'
3328
+ console.log(JSON.stringify(listToolNames(services), null, 2))
3329
+ ```
3330
+
3331
+ `listToolNames` never throws on an illegal name — that is deliberate, so it can
3332
+ show you the offending row when a mount would refuse it. Pin it in a snapshot
3333
+ test and a shift fails your build instead of your clients.
3334
+
3126
3335
  ## When you author a breaking change in stitchkit
3127
3336
 
3128
3337
  You are on the other side of this flow — see
@@ -3212,7 +3421,7 @@ from the root `stitchkit`.
3212
3421
  | `ScopedDefineContract` | _type_ | the `defineContract` `createContractFactory` returns |
3213
3422
  | `ALL_TRANSPORTS` | constant | `['HTTP', 'MCP', 'AGENT', 'CLI']` |
3214
3423
  | `ContractDef` | _type_ | a defined contract |
3215
- | `ContractMeta` | _type_ | a contract's `prefix` + optional `scope` |
3424
+ | `ContractMeta` | _type_ | a contract's `prefix` + optional `scope` and `meta` (a default every endpoint shallow-merges over) |
3216
3425
  | `EndpointDef` | _type_ | a single endpoint definition |
3217
3426
  | `HttpMethod` | _type_ | `GET \| POST \| PUT \| PATCH \| DELETE` |
3218
3427
  | `Transport` | _type_ | `HTTP \| MCP \| AGENT \| CLI` |
@@ -3276,6 +3485,7 @@ Also re-exports the error helpers from `stitchkit/contract`.
3276
3485
  | `staticRoute` | function | a raw route that serves a directory |
3277
3486
  | `serveFile` | function | serve a file with `Range` / `304` / `HEAD` — [guide](../guide/server.md#serving-files--range-requests) |
3278
3487
  | `parseByteRange` | function | parse a single `Range` header → range / `unsatisfiable` / `null` |
3488
+ | `isWithinDir` | function | path containment — `(root, resolvedTarget) => boolean`; call it before `serveFile` on any URL-derived path |
3279
3489
  | `weakETag` | function | a weak `ETag` from size + mtime |
3280
3490
  | `ServeFileOptions` | _type_ | options for `serveFile` |
3281
3491
  | `ByteRange` | _type_ | an inclusive `{ start, end }` byte range |
@@ -3332,6 +3542,7 @@ Also re-exports the error helpers from `stitchkit/contract`.
3332
3542
  | `corsHeaders` | function | compute CORS response headers |
3333
3543
  | `corsPreflightResponse` | function | build a preflight `Response` |
3334
3544
  | `DEFAULT_CORS_ALLOW_HEADERS` | const | the default `Access-Control-Allow-Headers` (incl. `traceparent`) — extend it when overriding `cors.headers` |
3545
+ | `DEFAULT_CORS_EXPOSE_HEADERS` | const | the default `Access-Control-Expose-Headers` (incl. `Content-Disposition`, `ETag`, `Content-Range`) — extend it when overriding `cors.exposeHeaders` |
3335
3546
  | `CookieDef` | _type_ | the `defineCookie` handle |
3336
3547
  | `CookieOptions` | _type_ | cookie attributes |
3337
3548
  | `CorsConfig` | _type_ | CORS policy |
package/llms.txt CHANGED
@@ -7,7 +7,7 @@ Build with stitchkit: define a contract once, then `implement` it and serve it (
7
7
  ## Guide
8
8
  - [Getting started](https://github.com/max-listov/stitchkit/blob/master/docs/guide/getting-started.md): install, entrypoints, and a first contract → server → client app
9
9
  - [Contracts](https://github.com/max-listov/stitchkit/blob/master/docs/guide/contracts.md): every endpoint field — method, path, params/input/output, scope, expose, meta, multipart
10
- - [HTTP server](https://github.com/max-listov/stitchkit/blob/master/docs/guide/server.md): createServer/createHandler, implement, lifecycle hooks, raw routes + helpers, scopePrefixes, serveFile, primitives
10
+ - [HTTP server](https://github.com/max-listov/stitchkit/blob/master/docs/guide/server.md): createServer/createHandler, implement, lifecycle hooks, raw routes + raw-response endpoints + helpers, scopePrefixes, serveFile, primitives
11
11
  - [Typed client](https://github.com/max-listov/stitchkit/blob/master/docs/guide/client.md): createClient/createHttpClient, the typed call surface, scoped clients, SSE
12
12
  - [MCP & agents](https://github.com/max-listov/stitchkit/blob/master/docs/guide/mcp-and-agents.md): contracts as MCP tools (createMcpHandler) and AI-agent tools (mountAgent); tool lifecycle, extend, identity
13
13
  - [CLI](https://github.com/max-listov/stitchkit/blob/master/docs/guide/cli.md): contracts as a command-line program
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "stitchkit",
3
- "version": "0.25.0",
3
+ "version": "0.27.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",