stitchkit 0.26.0 → 0.28.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 (51) hide show
  1. package/dist/browser/client.d.ts.map +1 -1
  2. package/dist/browser/http.d.ts +7 -1
  3. package/dist/browser/http.d.ts.map +1 -1
  4. package/dist/cli.js +2 -2
  5. package/dist/contract/define.d.ts +60 -3
  6. package/dist/contract/define.d.ts.map +1 -1
  7. package/dist/contract/index.js +1 -1
  8. package/dist/{index-bkccbx64.js → index-17jpjnhd.js} +196 -44
  9. package/dist/{index-q5w3cvvp.js → index-4whcb3c3.js} +3 -1
  10. package/dist/{index-dzx781tm.js → index-9g9d6r1h.js} +40 -4
  11. package/dist/{index-tje0q6gp.js → index-czmqks7r.js} +6 -1
  12. package/dist/{index-p6fge9a5.js → index-jvescqgr.js} +1 -1
  13. package/dist/{index-h4y2wg3n.js → index-pmftwk2a.js} +18 -0
  14. package/dist/index.js +20 -8
  15. package/dist/node.d.ts +1 -1
  16. package/dist/node.d.ts.map +1 -1
  17. package/dist/node.js +7 -7
  18. package/dist/observability/context.d.ts +8 -4
  19. package/dist/observability/context.d.ts.map +1 -1
  20. package/dist/observability/index.js +6 -8
  21. package/dist/server/create.d.ts +2 -2
  22. package/dist/server/create.d.ts.map +1 -1
  23. package/dist/server/implement.d.ts.map +1 -1
  24. package/dist/server/index.d.ts +3 -2
  25. package/dist/server/index.d.ts.map +1 -1
  26. package/dist/server/index.js +19 -7
  27. package/dist/server/logger.d.ts +36 -4
  28. package/dist/server/logger.d.ts.map +1 -1
  29. package/dist/server/logging.d.ts +20 -0
  30. package/dist/server/logging.d.ts.map +1 -0
  31. package/dist/server/middleware/cors.d.ts +25 -0
  32. package/dist/server/middleware/cors.d.ts.map +1 -1
  33. package/dist/server/node.d.ts +2 -2
  34. package/dist/server/node.d.ts.map +1 -1
  35. package/dist/server/openapi.d.ts.map +1 -1
  36. package/dist/server/router.d.ts +27 -0
  37. package/dist/server/router.d.ts.map +1 -1
  38. package/dist/server/types.d.ts +116 -4
  39. package/dist/server/types.d.ts.map +1 -1
  40. package/dist/tools/list-names.d.ts +2 -1
  41. package/dist/tools/list-names.d.ts.map +1 -1
  42. package/dist/tools/mount.d.ts.map +1 -1
  43. package/dist/tools/remote.d.ts.map +1 -1
  44. package/dist/tools/tool-logger.d.ts +7 -0
  45. package/dist/tools/tool-logger.d.ts.map +1 -1
  46. package/dist/tools.js +29 -14
  47. package/llms-full.txt +234 -14
  48. package/llms.txt +1 -1
  49. package/package.json +1 -1
  50. package/dist/index-khwedj16.js +0 -40
  51. /package/dist/{index-0d0rb85d.js → index-zza375qp.js} +0 -0
package/llms-full.txt CHANGED
@@ -242,6 +242,8 @@ export const users = defineContract({ prefix: 'users' }, {
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
 
@@ -356,10 +360,22 @@ defineContract({ prefix: 'admin', meta: { public: true } }, {
356
360
  })
357
361
  ```
358
362
 
359
- One level deep — no deep merge, no way to unset an inherited key; declare it on
360
- the endpoint instead. `expose` deliberately has **no** contract-level equivalent
361
- (→ [ADR 0036](../decisions/0036-contract-level-meta.md)).
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
+ ```
362
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)).
363
379
 
364
380
  `meta` is an **opaque, app-defined** bag the core attaches no meaning to — the
365
381
  same escape-hatch spirit as `scope` being a free string ([ADR 0002](../decisions/0002-generic-core.md) /
@@ -602,13 +618,49 @@ server. See [Testing & deployment](./testing-and-deployment.md).
602
618
  | `rawRoutes` | non-contract routes (see below) |
603
619
  | `maxUploadBytes` | default multipart upload cap (bytes); per-route `EndpointDef.maxUploadBytes` overrides |
604
620
  | `port` / `hostname` | listen address — port defaults to `3000` |
605
- | `cors` | CORS policy — `{ origin, … }` |
621
+ | `cors` | CORS policy — `{ origin, credentials, methods, headers, exposeHeaders }` |
606
622
  | `hooks` | lifecycle hooks (see below) |
607
- | `logging` | `true` for built-in request logs, or a custom `StitchLogger` |
608
- | `traceId` | override per-request trace-id resolution |
623
+ | `logging` | `true` for built-in request logs, or a `LoggingConfig` (see below) |
624
+ | `traceId` | override per-request trace-id resolution — may return `undefined` to fall back |
625
+ | `wrapFetch` | compose wrappers around the finished handler (request context, audit) |
609
626
  | `websocket` | Bun WebSocket handlers — e.g. from `createSocketIOServer` |
610
627
  | `routes` / `development` / `bun` | passthrough to `Bun.serve` |
611
628
 
629
+ ### Request logging
630
+
631
+ `logging: true` is shorthand for `logging: {}` — **any object turns logging
632
+ on**, and the fields tune it:
633
+
634
+ ```ts
635
+ createServer({
636
+ services,
637
+ logging: {
638
+ // Route lines into your stack instead of the built-in formatter.
639
+ logger: myLogger,
640
+ // Silence noise. Runs after the built-in filter (framework assets,
641
+ // favicon, preflights), so it can only quieten more.
642
+ skip: (_req, url) => url.pathname === '/health' || url.pathname.startsWith('/socket.io/'),
643
+ // Extra fields on the completion line.
644
+ enrich: (req, _url, { status }) => ({
645
+ userAgent: req.headers.get('user-agent') ?? undefined,
646
+ cacheable: status === 200,
647
+ }),
648
+ },
649
+ })
650
+ ```
651
+
652
+ Three things worth knowing about `enrich`: it reaches the **structured** output
653
+ only (the production JSON line and a custom logger's `data`) — the development
654
+ `←` line stays human-readable, so enriched fields are invisible in dev; it runs
655
+ at close, when the request body is already consumed; and framework fields
656
+ (`traceId`, `status`, `path`, …) always win a key collision. A throw in `skip`
657
+ or `enrich` is swallowed — neither can fail a request.
658
+
659
+ With an observability context active, the line also carries `userId`,
660
+ `serviceName`, `action` and `dimensions` for free — and, like `enrich`'s fields,
661
+ only in the structured output, never on the development `←` line. See
662
+ [Observability](./observability.md).
663
+
612
664
  ## Route groups
613
665
 
614
666
  A group gives a set of services a shared path prefix and its own hooks:
@@ -722,6 +774,87 @@ createServer({
722
774
  Hooks see `RuntimeContext` (loose types); handlers see `HandlerContext` (typed).
723
775
  That split is deliberate — see [ADR 0003](../decisions/0003-two-context-types.md).
724
776
 
777
+ ## Raw-response endpoints
778
+
779
+ An endpoint that answers with **bytes rather than data** — a PDF download, a
780
+ file, an SSE stream — declares `rawResponse: true` and returns the `Response` itself:
781
+
782
+ ```ts
783
+ // contract
784
+ export const documents = defineContract(
785
+ { prefix: 'documents', scope: 'admin' },
786
+ {
787
+ download: {
788
+ method: 'GET', path: '/:id/pdf', desc: 'Download a document as a PDF',
789
+ params: z.object({ id: z.uuid() }),
790
+ rawResponse: true, contentType: 'application/pdf',
791
+ },
792
+ },
793
+ )
794
+
795
+ // handler — no guard on the first line; `beforeHandle` already ran
796
+ download: (ctx) => serveFile(ctx.req, { path: pathFor(ctx.params.id),
797
+ filename: 'offer.pdf' }),
798
+ ```
799
+
800
+ The request half is untouched: `params`, `input` and `multipart` parse and
801
+ validate exactly as elsewhere, and the endpoint goes through `beforeHandle` — so
802
+ the **auth gate applies without a guard in the handler**. Only the response is
803
+ handed over, so there is no `output` schema, `afterHandle` is skipped (it
804
+ transforms data; there is none) and the endpoint is HTTP-only: never an MCP
805
+ tool, an agent tool or a CLI command. Declaring `output`, `toolName`, `ui`,
806
+ `annotations` or a non-HTTP `expose` alongside `rawResponse` is a type error, and throws
807
+ at definition time for a contract assembled at runtime.
808
+
809
+ On the typed client the method resolves to the untouched `Response` — the
810
+ filename lives in `Content-Disposition`, so a `Blob` alone would lose it:
811
+
812
+ ```ts
813
+ const client = createClient(documents, http)
814
+ const res = await client.download({ id }) // Response
815
+ const name = res.headers.get('Content-Disposition')
816
+ const blob = await res.blob()
817
+ ```
818
+
819
+ Cross-origin, remember that those headers are readable only because CORS exposes
820
+ them — see [`cors.exposeHeaders`](#serving-files--range-requests).
821
+
822
+ **Raw response or [raw route](#raw-routes)?** Both hand the `Response` to your
823
+ code. A raw-response *endpoint* stays in the contract: only its response is
824
+ raw — it is still routed, gated, typed and documented like every other endpoint.
825
+ A raw *route* is outside the contract entirely — no schemas, no auth gate, no
826
+ client — which is what you want for an OAuth redirect or a webhook, and what you
827
+ do not want for a download.
828
+
829
+ ⚠️ **Delete the old raw route when you move an endpoint into the contract.** Raw
830
+ routes are matched **first**, so a leftover one keeps serving the bytes and the
831
+ contract endpoint — with its auth gate — never runs. stitchkit warns at startup
832
+ when a raw route shadows a contract route, naming both and the scope being
833
+ bypassed; treat that warning as a bug.
834
+
835
+ ⚠️ **A path built from user input needs a containment check.** `staticRoute`
836
+ enforces it; `serveFile` deliberately leaves it to the caller, so an endpoint
837
+ serving `/:filename` must not pass it through:
838
+
839
+ ```ts
840
+ import { isWithinDir, serveFile } from 'stitchkit/server'
841
+ import { resolve } from 'node:path'
842
+
843
+ const ROOT = resolve('./uploads')
844
+
845
+ file: (ctx) => {
846
+ const target = resolve(ROOT, ctx.params.filename)
847
+ // `../../etc/passwd` resolves outside ROOT — reject before touching disk.
848
+ if (!isWithinDir(ROOT, target)) throw notFound('File not found')
849
+ return serveFile(ctx.req, { path: target })
850
+ },
851
+ ```
852
+
853
+ `implementRemote` proxies a raw-response endpoint like any other — the remote
854
+ `Response` is forwarded verbatim. Request headers are not relayed, so a `Range`
855
+ sent to the proxy does not reach the origin and the full body comes back.
856
+ → ADR 0038.
857
+
725
858
  ## Raw routes
726
859
 
727
860
  Some routes cannot be a clean JSON contract — an OAuth redirect, a webhook with
@@ -815,6 +948,13 @@ It always sets `Accept-Ranges: bytes`, a weak `ETag` and `Last-Modified` (so
815
948
  and `nosniff`. `Content-Type` is auto-detected from the path — override it, or
816
949
  pass `disposition` / `cacheControl` / `etag: false`, via the options.
817
950
 
951
+ Cross-origin, the browser lets JavaScript read only the CORS-safelisted response
952
+ headers. stitchkit therefore exposes the download-relevant ones by default
953
+ (`Content-Disposition`, `Content-Range`, `ETag`, …) — without that a `fetch`-based
954
+ download cannot recover the file's name. Override with `cors.exposeHeaders`
955
+ (extend `DEFAULT_CORS_EXPOSE_HEADERS` rather than replacing it), or pass `[]` to
956
+ emit none.
957
+
818
958
  `serveFile` takes an explicit `path` and trusts it — **the caller owns
819
959
  containment**. For a URL-derived path use `staticRoute` (which enforces it) or
820
960
  `isWithinDir` first. The byte-range parser is exported on its own as
@@ -836,11 +976,20 @@ focused helper — not a sub-framework.
836
976
 
837
977
  ### SSE streaming
838
978
 
979
+ `streamSSE` returns a `Response`, so its endpoint declares
980
+ [`rawResponse: true`](#raw-response-endpoints) — in a plain contract handler the response
981
+ would be serialized into `{}` (that now fails loudly instead of shipping silently).
982
+
839
983
  ```ts
840
984
  import { streamSSE } from 'stitchkit/server'
841
985
 
986
+ // contract
987
+ stream: { method: 'GET', path: '/stream', desc: 'Stream tokens',
988
+ rawResponse: true, contentType: 'text/event-stream' },
989
+
990
+ // handler
842
991
  async function* tokens() { yield 'a'; yield 'b' }
843
- return streamSSE(tokens()) // → a text/event-stream Response
992
+ stream: () => streamSSE(tokens()), // → a text/event-stream Response
844
993
  ```
845
994
 
846
995
  The client side is [`parseSSE`](./client.md#sse).
@@ -1202,8 +1351,14 @@ By default every endpoint is a tool on every transport. `expose` narrows it:
1202
1351
  { method: 'GET', path: '/lookup', desc: 'Look up a price', expose: ['MCP'] } // MCP tool only
1203
1352
  ```
1204
1353
 
1354
+ Two kinds of endpoint are **never** tools, whatever `expose` says: a `multipart`
1355
+ upload (not a tool call), and a
1356
+ [`rawResponse`](./server.md#raw-response-endpoints) endpoint (its answer is
1357
+ bytes — a tool result cannot carry them, and it would reach the model as `{}`).
1358
+ Pin the full list with `listToolNames` in a snapshot test.
1359
+
1205
1360
  `desc` is the tool description the model reads — write it for the model, not
1206
- just for a human. A `multipart` endpoint is never a tool. The tool name defaults
1361
+ just for a human. The tool name defaults
1207
1362
  to a verb-aware name from the method + prefix (`list` → `list_widgets`, `get` →
1208
1363
  `get_widget`); set `toolName` for an explicit one. Derivation normalises every
1209
1364
  character outside `[a-zA-Z0-9_]` to `_` — the hyphen included, so `bot-status`
@@ -1633,7 +1788,10 @@ mountMcp(server, services, { hooks: createToolLogger() })
1633
1788
  ```
1634
1789
 
1635
1790
  Pass `log` to redirect the line, or `onRecord` to feed a metrics sink the
1636
- structured `ToolCallRecord`. For a boot-time picture of what is exposed where,
1791
+ structured `ToolCallRecord`. That record carries `traceId` whenever an
1792
+ observability context is active, so a tool call made inside an HTTP request
1793
+ joins that request's log line on one key — see
1794
+ [Observability](./observability.md). For a boot-time picture of what is exposed where,
1637
1795
  `summarizeTransports(services)` returns per-transport operation counts (HTTP /
1638
1796
  MCP / AGENT / CLI) for you to log.
1639
1797
 
@@ -2671,6 +2829,16 @@ Bun.serve({
2671
2829
  })
2672
2830
  ```
2673
2831
 
2832
+ `createServer` and `serveNode` build their own `fetch`, so compose through
2833
+ **`wrapFetch`** instead — same order, the context outermost:
2834
+
2835
+ ```ts
2836
+ createServer({
2837
+ services,
2838
+ wrapFetch: (fetch) => wrapInRequestContext(audit.http(fetch)),
2839
+ })
2840
+ ```
2841
+
2674
2842
  Some fields are filled in late. Set them from the hooks that know:
2675
2843
 
2676
2844
  ```ts
@@ -2710,8 +2878,51 @@ application logs carry one id — by passing `getTraceId` as the resolver:
2710
2878
  createHandler({ /* … */ traceId: getTraceId })
2711
2879
  ```
2712
2880
 
2881
+ `getTraceId` returns `undefined` outside an active context, and the framework
2882
+ falls back to its own resolver — a trusted inbound `x-request-id` / `x-trace-id`,
2883
+ else a fresh id — so the line never carries the string `"undefined"`.
2884
+
2713
2885
  `getRequestContext()` / `getTraceId()` then return the active values from
2714
2886
  anywhere in the call — stamp `getTraceId()` onto every line your logger writes.
2887
+ The **request log picks the context up on its own**: with a context active, each
2888
+ completion line carries `userId`, `serviceName`, `action` and `dimensions`
2889
+ without any configuration.
2890
+
2891
+ ⚠️ In the **structured** output only — the production JSON line and a custom
2892
+ `logger`'s `data`. The development `←` line is a line to read, not a record to
2893
+ query, and never carries them (nor `enrich`'s fields). On `logging: true` in
2894
+ development you will see no difference; check with `NODE_ENV=production` or a
2895
+ custom `logger`.
2896
+
2897
+ ### Correlating with a reverse proxy
2898
+
2899
+ Every response the stitchkit handler produces carries the resolved id as
2900
+ **`x-request-id`**. With `cors` configured it is in the default
2901
+ `Access-Control-Expose-Headers`, so a browser client can read it and quote it in
2902
+ a bug report. Note the deliberate asymmetry: inbound the id may arrive as
2903
+ `X-Trace-Id` *or* `X-Request-Id`; outbound there is one name and no alias.
2904
+
2905
+ Log the same id from nginx and the two logs join on one key. `log_format` and
2906
+ `map` are `http {}`-context directives — put the block there, not inside
2907
+ `server {}`:
2908
+
2909
+ ```nginx
2910
+ log_format stitch '$remote_addr $status $request_time rid=$rid "$request"';
2911
+
2912
+ # Fall back to nginx's own id for responses stitchkit never produced — Bun's
2913
+ # native `routes`, a throwing `onRequest` (which escapes before any response
2914
+ # exists), a redirect whose headers are immutable.
2915
+ map $upstream_http_x_request_id $rid {
2916
+ "" $request_id;
2917
+ default $upstream_http_x_request_id;
2918
+ }
2919
+
2920
+ access_log /var/log/nginx/access.log stitch;
2921
+ ```
2922
+
2923
+ `grep rid=<id>` across both logs then reconstructs the whole request. Tool calls
2924
+ join too: `createToolLogger`'s record carries `traceId`, so an MCP or agent call
2925
+ made inside a request lines up with it.
2715
2926
 
2716
2927
  ### Trace context
2717
2928
 
@@ -2957,10 +3168,13 @@ Notes for a Node host:
2957
3168
 
2958
3169
  - **CORS** — set `cors.origin` to your real front-end origin(s). Do not ship
2959
3170
  `origin: '*'` with credentials.
2960
- - **Logging** — `logging: true` for built-in request logs, or pass a
2961
- `StitchLogger` to route them into your logging stack.
3171
+ - **Logging** — `logging: true` for built-in request logs, or
3172
+ `logging: { logger, skip, enrich }` to route them into your logging stack,
3173
+ drop probe noise and add your own fields.
2962
3174
  - **Trace ids** — override `traceId` to reuse an id your platform already
2963
- assigns, so request logs and application logs share one id.
3175
+ assigns, so request logs and application logs share one id. Every response
3176
+ carries it as `x-request-id`; log `$upstream_http_x_request_id` at the proxy
3177
+ to join the two logs.
2964
3178
  - **Rate limiting** — `createRateLimiter` in `onRequest` for a global limit;
2965
3179
  per-route limits belong in `beforeHandle`.
2966
3180
  - **Auth** — a `createAuthHook` `beforeHandle` guards every transport at once;
@@ -3366,6 +3580,7 @@ Also re-exports the error helpers from `stitchkit/contract`.
3366
3580
  | `staticRoute` | function | a raw route that serves a directory |
3367
3581
  | `serveFile` | function | serve a file with `Range` / `304` / `HEAD` — [guide](../guide/server.md#serving-files--range-requests) |
3368
3582
  | `parseByteRange` | function | parse a single `Range` header → range / `unsatisfiable` / `null` |
3583
+ | `isWithinDir` | function | path containment — `(root, resolvedTarget) => boolean`; call it before `serveFile` on any URL-derived path |
3369
3584
  | `weakETag` | function | a weak `ETag` from size + mtime |
3370
3585
  | `ServeFileOptions` | _type_ | options for `serveFile` |
3371
3586
  | `ByteRange` | _type_ | an inclusive `{ start, end }` byte range |
@@ -3389,6 +3604,10 @@ Also re-exports the error helpers from `stitchkit/contract`.
3389
3604
  | `BunServer` | _type_ | the `Bun.serve` instance type |
3390
3605
  | `ServerPassthrough` | _type_ | extra `Bun.serve` options |
3391
3606
  | `StitchLogger` | _type_ | the custom-logger interface |
3607
+ | `LoggingConfig` | _type_ | the `logging` object — `logger` / `skip` / `enrich` |
3608
+ | `LogOutcome` | _type_ | how a request finished, as `enrich` sees it |
3609
+ | `FetchHandler` | _type_ | what `createHandler` returns |
3610
+ | `FetchComposition` | _type_ | the `wrapFetch` seam shared by the servers |
3392
3611
 
3393
3612
  ### Auth
3394
3613
 
@@ -3422,6 +3641,7 @@ Also re-exports the error helpers from `stitchkit/contract`.
3422
3641
  | `corsHeaders` | function | compute CORS response headers |
3423
3642
  | `corsPreflightResponse` | function | build a preflight `Response` |
3424
3643
  | `DEFAULT_CORS_ALLOW_HEADERS` | const | the default `Access-Control-Allow-Headers` (incl. `traceparent`) — extend it when overriding `cors.headers` |
3644
+ | `DEFAULT_CORS_EXPOSE_HEADERS` | const | the default `Access-Control-Expose-Headers` (incl. `Content-Disposition`, `ETag`, `Content-Range`) — extend it when overriding `cors.exposeHeaders` |
3425
3645
  | `CookieDef` | _type_ | the `defineCookie` handle |
3426
3646
  | `CookieOptions` | _type_ | cookie attributes |
3427
3647
  | `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.26.0",
3
+ "version": "0.28.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",
@@ -1,40 +0,0 @@
1
- // src/observability/trace.ts
2
- var TRACEPARENT_RE = /^00-([0-9a-f]{32})-([0-9a-f]{16})-[0-9a-f]{2}$/i;
3
- function randomHex(bytes) {
4
- const arr = new Uint8Array(bytes);
5
- crypto.getRandomValues(arr);
6
- let hex = "";
7
- for (const byte of arr)
8
- hex += byte.toString(16).padStart(2, "0");
9
- return hex;
10
- }
11
- function createTraceContext() {
12
- return { traceId: randomHex(16), spanId: randomHex(8) };
13
- }
14
- function parseTraceparent(header) {
15
- if (!header)
16
- return null;
17
- const match = TRACEPARENT_RE.exec(header.trim());
18
- if (!match?.[1] || !match[2])
19
- return null;
20
- const traceId = match[1].toLowerCase();
21
- const parentSpanId = match[2].toLowerCase();
22
- if (/^0+$/.test(traceId) || /^0+$/.test(parentSpanId))
23
- return null;
24
- return { traceId, spanId: randomHex(8), parentSpanId };
25
- }
26
- function formatTraceparent(ctx) {
27
- return `00-${ctx.traceId}-${ctx.spanId}-01`;
28
- }
29
- function resolveTraceContext(req) {
30
- return parseTraceparent(req.headers.get("traceparent")) ?? createTraceContext();
31
- }
32
- function childSpan(parent) {
33
- return {
34
- traceId: parent.traceId,
35
- spanId: randomHex(8),
36
- parentSpanId: parent.spanId
37
- };
38
- }
39
-
40
- export { createTraceContext, parseTraceparent, formatTraceparent, resolveTraceContext, childSpan };
File without changes