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.
- package/dist/browser/client.d.ts.map +1 -1
- package/dist/browser/http.d.ts +7 -1
- package/dist/browser/http.d.ts.map +1 -1
- package/dist/cli.js +2 -2
- package/dist/contract/define.d.ts +60 -3
- package/dist/contract/define.d.ts.map +1 -1
- package/dist/contract/index.js +1 -1
- package/dist/{index-bkccbx64.js → index-17jpjnhd.js} +196 -44
- package/dist/{index-q5w3cvvp.js → index-4whcb3c3.js} +3 -1
- package/dist/{index-dzx781tm.js → index-9g9d6r1h.js} +40 -4
- package/dist/{index-tje0q6gp.js → index-czmqks7r.js} +6 -1
- package/dist/{index-p6fge9a5.js → index-jvescqgr.js} +1 -1
- package/dist/{index-h4y2wg3n.js → index-pmftwk2a.js} +18 -0
- package/dist/index.js +20 -8
- package/dist/node.d.ts +1 -1
- package/dist/node.d.ts.map +1 -1
- package/dist/node.js +7 -7
- package/dist/observability/context.d.ts +8 -4
- package/dist/observability/context.d.ts.map +1 -1
- package/dist/observability/index.js +6 -8
- package/dist/server/create.d.ts +2 -2
- package/dist/server/create.d.ts.map +1 -1
- package/dist/server/implement.d.ts.map +1 -1
- package/dist/server/index.d.ts +3 -2
- package/dist/server/index.d.ts.map +1 -1
- package/dist/server/index.js +19 -7
- package/dist/server/logger.d.ts +36 -4
- package/dist/server/logger.d.ts.map +1 -1
- package/dist/server/logging.d.ts +20 -0
- package/dist/server/logging.d.ts.map +1 -0
- package/dist/server/middleware/cors.d.ts +25 -0
- package/dist/server/middleware/cors.d.ts.map +1 -1
- package/dist/server/node.d.ts +2 -2
- package/dist/server/node.d.ts.map +1 -1
- package/dist/server/openapi.d.ts.map +1 -1
- package/dist/server/router.d.ts +27 -0
- package/dist/server/router.d.ts.map +1 -1
- package/dist/server/types.d.ts +116 -4
- package/dist/server/types.d.ts.map +1 -1
- package/dist/tools/list-names.d.ts +2 -1
- package/dist/tools/list-names.d.ts.map +1 -1
- package/dist/tools/mount.d.ts.map +1 -1
- package/dist/tools/remote.d.ts.map +1 -1
- package/dist/tools/tool-logger.d.ts +7 -0
- package/dist/tools/tool-logger.d.ts.map +1 -1
- package/dist/tools.js +29 -14
- package/llms-full.txt +234 -14
- package/llms.txt +1 -1
- package/package.json +1 -1
- package/dist/index-khwedj16.js +0 -40
- /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
|
|
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
|
|
360
|
-
|
|
361
|
-
|
|
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
|
|
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
|
-
|
|
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.
|
|
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`.
|
|
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
|
|
2961
|
-
`
|
|
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
package/dist/index-khwedj16.js
DELETED
|
@@ -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
|