stitchkit 0.18.0 → 0.20.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 (70) hide show
  1. package/dist/browser/client.d.ts.map +1 -1
  2. package/dist/browser/socket-io.d.ts.map +1 -1
  3. package/dist/cli.js +3 -3
  4. package/dist/contract/errors-factory.d.ts +18 -0
  5. package/dist/contract/errors-factory.d.ts.map +1 -0
  6. package/dist/contract/factory.d.ts +36 -0
  7. package/dist/contract/factory.d.ts.map +1 -0
  8. package/dist/contract/index.d.ts +2 -0
  9. package/dist/contract/index.d.ts.map +1 -1
  10. package/dist/contract/index.js +6 -1
  11. package/dist/contract/pagination.d.ts.map +1 -1
  12. package/dist/index-809wc1tt.js +18 -0
  13. package/dist/{index-tm7dqzxc.js → index-c7nyw0yt.js} +9 -9
  14. package/dist/{index-dz2k8d5h.js → index-dzx781tm.js} +1 -1
  15. package/dist/{index-6qv1fxqy.js → index-f5e4efj1.js} +15 -57
  16. package/dist/{index-ktn76xz0.js → index-ng2v2ts4.js} +17 -1
  17. package/dist/index-tje0q6gp.js +44 -0
  18. package/dist/{index-yzac2zbh.js → index-x62gnfsk.js} +50 -9
  19. package/dist/{index-akv8rk4w.js → index-xmncnw8z.js} +2 -2
  20. package/dist/{index-yv93m4yr.js → index-ynnh9x6h.js} +11 -29
  21. package/dist/index.js +82 -62
  22. package/dist/internal/base64url.d.ts +16 -0
  23. package/dist/internal/base64url.d.ts.map +1 -0
  24. package/dist/node.js +5 -4
  25. package/dist/observability/index.js +2 -2
  26. package/dist/react/entity-cache.d.ts +54 -0
  27. package/dist/react/entity-cache.d.ts.map +1 -0
  28. package/dist/react.d.ts +1 -0
  29. package/dist/react.d.ts.map +1 -1
  30. package/dist/react.js +74 -0
  31. package/dist/server/create.d.ts.map +1 -1
  32. package/dist/server/error-hook.d.ts +56 -0
  33. package/dist/server/error-hook.d.ts.map +1 -0
  34. package/dist/server/index.d.ts +2 -1
  35. package/dist/server/index.d.ts.map +1 -1
  36. package/dist/server/index.js +32 -7
  37. package/dist/server/middleware/auth.d.ts.map +1 -1
  38. package/dist/server/middleware/cors.d.ts +13 -0
  39. package/dist/server/middleware/cors.d.ts.map +1 -1
  40. package/dist/server/middleware/pkce.d.ts +0 -6
  41. package/dist/server/middleware/pkce.d.ts.map +1 -1
  42. package/dist/server/multipart.d.ts +10 -3
  43. package/dist/server/multipart.d.ts.map +1 -1
  44. package/dist/server/router.d.ts +0 -2
  45. package/dist/server/router.d.ts.map +1 -1
  46. package/dist/server/types.d.ts +0 -8
  47. package/dist/server/types.d.ts.map +1 -1
  48. package/dist/tools/agent.d.ts +2 -2
  49. package/dist/tools/agent.d.ts.map +1 -1
  50. package/dist/tools/cli-format.d.ts +2 -2
  51. package/dist/tools/cli-format.d.ts.map +1 -1
  52. package/dist/tools/cli.d.ts +2 -2
  53. package/dist/tools/cli.d.ts.map +1 -1
  54. package/dist/tools/execute.d.ts +7 -0
  55. package/dist/tools/execute.d.ts.map +1 -1
  56. package/dist/tools/mcp.d.ts +3 -3
  57. package/dist/tools/mcp.d.ts.map +1 -1
  58. package/dist/tools/mount.d.ts +3 -3
  59. package/dist/tools/mount.d.ts.map +1 -1
  60. package/dist/tools/oauth-metadata.d.ts.map +1 -1
  61. package/dist/tools/oauth-provider.d.ts.map +1 -1
  62. package/dist/tools/tool-logger.d.ts +31 -0
  63. package/dist/tools/tool-logger.d.ts.map +1 -0
  64. package/dist/tools/transports.d.ts +28 -0
  65. package/dist/tools/transports.d.ts.map +1 -0
  66. package/dist/tools.d.ts +3 -1
  67. package/dist/tools.d.ts.map +1 -1
  68. package/dist/tools.js +71 -11
  69. package/llms-full.txt +254 -4
  70. package/package.json +1 -1
package/dist/tools.js CHANGED
@@ -2,7 +2,10 @@ import {
2
2
  inputIsQuery,
3
3
  signJwt,
4
4
  verifyPkce
5
- } from "./index-yv93m4yr.js";
5
+ } from "./index-ynnh9x6h.js";
6
+ import {
7
+ DEFAULT_CORS_ALLOW_HEADERS
8
+ } from "./index-tje0q6gp.js";
6
9
  import {
7
10
  coerceJsonArgs,
8
11
  collectTools,
@@ -15,19 +18,19 @@ import {
15
18
  pollUntil,
16
19
  readCapped,
17
20
  toolResultFromError
18
- } from "./index-akv8rk4w.js";
21
+ } from "./index-xmncnw8z.js";
19
22
  import {
20
23
  toJsonSchema
21
24
  } from "./index-0ed3bx43.js";
22
25
  import {
23
26
  AppError,
24
27
  isWithinDir
25
- } from "./index-ktn76xz0.js";
28
+ } from "./index-ng2v2ts4.js";
26
29
  import"./index-khwedj16.js";
27
30
  import {
28
31
  isRecord,
29
32
  typedEntries
30
- } from "./index-tm7dqzxc.js";
33
+ } from "./index-c7nyw0yt.js";
31
34
 
32
35
  // src/tools/agent.ts
33
36
  import { tool, zodSchema } from "ai";
@@ -319,7 +322,7 @@ function wwwAuthenticateHeader(resource) {
319
322
  var PUBLIC_CORS = {
320
323
  "Access-Control-Allow-Origin": "*",
321
324
  "Access-Control-Allow-Methods": "GET, OPTIONS",
322
- "Access-Control-Allow-Headers": "Authorization, Content-Type"
325
+ "Access-Control-Allow-Headers": DEFAULT_CORS_ALLOW_HEADERS
323
326
  };
324
327
  function oauthProtectedResourceRoute(config) {
325
328
  const body = JSON.stringify({
@@ -599,7 +602,7 @@ import { randomUUID as randomUUID2 } from "node:crypto";
599
602
  var PUBLIC_CORS2 = {
600
603
  "Access-Control-Allow-Origin": "*",
601
604
  "Access-Control-Allow-Methods": "GET, POST, OPTIONS",
602
- "Access-Control-Allow-Headers": "Authorization, Content-Type"
605
+ "Access-Control-Allow-Headers": DEFAULT_CORS_ALLOW_HEADERS
603
606
  };
604
607
  var AS_METADATA_PATH = "/.well-known/oauth-authorization-server";
605
608
  var AUTH_CODE_TTL_MS = 60000;
@@ -895,6 +898,12 @@ function withTimeout(options, timeout) {
895
898
  return options;
896
899
  return { ...options, timeout };
897
900
  }
901
+ function withOutput(endpoint, result) {
902
+ const schema = endpoint.output;
903
+ if (!schema)
904
+ return result;
905
+ return result.then((value) => value === undefined ? undefined : schema.parse(value));
906
+ }
898
907
  function isParamArray(value) {
899
908
  return Array.isArray(value) && value.every((v) => typeof v === "string" || typeof v === "number");
900
909
  }
@@ -958,18 +967,21 @@ function createHttpMethod(endpoint, prefix, client, config) {
958
967
  if (!isMultipartFile(file)) {
959
968
  throw new Error(`Missing multipart file field: ${endpoint.multipart}`);
960
969
  }
970
+ if (httpMethod === "get" || httpMethod === "delete") {
971
+ throw new Error(`Multipart endpoint ${endpoint.method} ${endpoint.path} must be POST / PUT / PATCH`);
972
+ }
961
973
  const formData = new FormData;
962
974
  appendMultipartFile(formData, endpoint.multipart, file);
963
975
  appendFormFields(formData, firstArg, new Set([...prefixKeys, endpoint.multipart]));
964
- return client.post(url, formData, withTimeout(undefined, endpoint.timeout));
976
+ return withOutput(endpoint, client[httpMethod](url, formData, withTimeout(undefined, endpoint.timeout)));
965
977
  }
966
978
  if (isGet) {
967
979
  const params = collectQueryParams(firstArg, prefixKeys, endpoint);
968
- return client.get(url, withTimeout(params ? { params } : undefined, endpoint.timeout));
980
+ return withOutput(endpoint, client.get(url, withTimeout(params ? { params } : undefined, endpoint.timeout)));
969
981
  }
970
982
  if (httpMethod === "delete") {
971
983
  const params = collectQueryParams(firstArg, prefixKeys, endpoint);
972
- return client.delete(url, withTimeout(params ? { params } : undefined, endpoint.timeout));
984
+ return withOutput(endpoint, client.delete(url, withTimeout(params ? { params } : undefined, endpoint.timeout)));
973
985
  }
974
986
  const payload = {};
975
987
  for (const [key, value] of Object.entries(firstArg)) {
@@ -977,7 +989,7 @@ function createHttpMethod(endpoint, prefix, client, config) {
977
989
  payload[key] = value;
978
990
  }
979
991
  }
980
- return client[httpMethod](url, Object.keys(payload).length > 0 ? payload : undefined, withTimeout(undefined, endpoint.timeout));
992
+ return withOutput(endpoint, client[httpMethod](url, Object.keys(payload).length > 0 ? payload : undefined, withTimeout(undefined, endpoint.timeout)));
981
993
  };
982
994
  }
983
995
  function createFetchMethod(endpoint, prefix, config, contractConfig) {
@@ -994,6 +1006,7 @@ function createFetchMethod(endpoint, prefix, config, contractConfig) {
994
1006
  Accept: "application/json",
995
1007
  ...typeof config.headers === "function" ? config.headers() : config.headers
996
1008
  };
1009
+ const signal = endpoint.timeout !== undefined ? AbortSignal.timeout(endpoint.timeout) : undefined;
997
1010
  const isQuery = inputIsQuery(endpoint.method);
998
1011
  const hasBody = !isQuery && !endpoint.multipart && endpoint.input && args;
999
1012
  if (isQuery && args) {
@@ -1026,7 +1039,8 @@ function createFetchMethod(endpoint, prefix, config, contractConfig) {
1026
1039
  method: endpoint.method,
1027
1040
  headers,
1028
1041
  credentials: config.credentials,
1029
- body: formData
1042
+ body: formData,
1043
+ signal
1030
1044
  });
1031
1045
  if (!res2.ok) {
1032
1046
  await throwForErrorResponse(res2, config, null);
@@ -1040,6 +1054,7 @@ function createFetchMethod(endpoint, prefix, config, contractConfig) {
1040
1054
  method: endpoint.method,
1041
1055
  headers,
1042
1056
  credentials: config.credentials,
1057
+ signal,
1043
1058
  ...hasBody && {
1044
1059
  body: JSON.stringify(stripParams(hasBody, endpoint.path, prefixKeys))
1045
1060
  }
@@ -1165,6 +1180,26 @@ function implementRemote(contract, http, options) {
1165
1180
  methods
1166
1181
  };
1167
1182
  }
1183
+ // src/tools/tool-logger.ts
1184
+ function createToolLogger(config = {}) {
1185
+ const log = config.log ?? ((line) => console.info(line));
1186
+ return {
1187
+ afterToolCall: (toolName, _args, result, durationMs, context, endpoint) => {
1188
+ const record = {
1189
+ tool: toolName,
1190
+ service: endpoint.serviceName,
1191
+ action: endpoint.key,
1192
+ ok: result.ok,
1193
+ code: result.ok ? undefined : result.code,
1194
+ durationMs: Math.round(durationMs),
1195
+ source: String(context.source)
1196
+ };
1197
+ const codePart = result.ok ? "" : ` ${result.code}`;
1198
+ log(`[tool] ${result.ok ? "ok" : "warn"} ${toolName} (${endpoint.serviceName}.${endpoint.key})${codePart} ${record.durationMs}ms`);
1199
+ config.onRecord?.(record);
1200
+ }
1201
+ };
1202
+ }
1168
1203
  // src/tools/toolkit.ts
1169
1204
  function createToolkit() {
1170
1205
  return {
@@ -1176,6 +1211,29 @@ function createToolkit() {
1176
1211
  createCli: (config) => createCli(config)
1177
1212
  };
1178
1213
  }
1214
+ // src/tools/transports.ts
1215
+ var TOOL_TRANSPORTS2 = ["MCP", "AGENT", "CLI"];
1216
+ function emptyCounts() {
1217
+ return { HTTP: 0, MCP: 0, AGENT: 0, CLI: 0 };
1218
+ }
1219
+ function summarizeTransports(services) {
1220
+ const totals = emptyCounts();
1221
+ const perService = services.map((service) => {
1222
+ const counts = emptyCounts();
1223
+ for (const method of Object.values(service.methods)) {
1224
+ if (!method.expose || method.expose.includes("HTTP"))
1225
+ counts.HTTP += 1;
1226
+ }
1227
+ for (const transport of TOOL_TRANSPORTS2) {
1228
+ counts[transport] = collectTools(service, transport).length;
1229
+ }
1230
+ for (const transport of Object.keys(counts)) {
1231
+ totals[transport] += counts[transport];
1232
+ }
1233
+ return { service: service.name, counts };
1234
+ });
1235
+ return { services: services.length, totals, perService };
1236
+ }
1179
1237
  // src/tools/view-file.ts
1180
1238
  import { readFile, realpath, stat } from "node:fs/promises";
1181
1239
  import { extname as extname2, resolve } from "node:path";
@@ -1301,6 +1359,7 @@ function mountViewFile(server, options = {}) {
1301
1359
  export {
1302
1360
  wwwAuthenticateHeader,
1303
1361
  validateMcpSchemas,
1362
+ summarizeTransports,
1304
1363
  resolveMedia,
1305
1364
  protectedResourceMetadataUrl,
1306
1365
  oauthProtectedResourceRoute,
@@ -1318,6 +1377,7 @@ export {
1318
1377
  flattenUnionsDeep,
1319
1378
  flattenDiscriminatedUnion,
1320
1379
  createToolkit,
1380
+ createToolLogger,
1321
1381
  createStdioMcpServer,
1322
1382
  createMcpHandler,
1323
1383
  createCli,
package/llms-full.txt CHANGED
@@ -33,19 +33,23 @@ realtime, `@tanstack/react-query` + `react-query-kit` for React). See
33
33
 
34
34
  ## Entrypoints
35
35
 
36
- stitchkit ships five entrypoints. Each is import-safe for one environment —
36
+ stitchkit ships eight entrypoints. Each is import-safe for one environment —
37
37
  keeping server-only code (`Bun.serve`, the MCP SDK) out of browser bundles.
38
38
 
39
39
  | Import | Use in | Holds |
40
40
  |--------|--------|-------|
41
41
  | `stitchkit` | browser **and** server | `defineContract`, `createClient`, `createHttpClient`, `createSocketIOClient`, `parseSSE`, the error model |
42
42
  | `stitchkit/contract` | browser **and** server | the contract layer alone — `defineContract`, errors, pagination |
43
- | `stitchkit/server` | server | `createServer`, `implement`, hooks, auth, Socket.IO server, server primitives |
44
- | `stitchkit/tools` | server | `createMcpHandler`, `mountMcp`, `mountAgent` |
43
+ | `stitchkit/server` | server (Bun) | `createServer`, `implement`, hooks, auth, Socket.IO server, server primitives |
44
+ | `stitchkit/node` | server (Node ≥ 22) | `serveNode` + the runtime-agnostic core — the Node mirror of `/server` |
45
+ | `stitchkit/tools` | server | `createMcpHandler`, `mountMcp`, `mountAgent`, the OAuth provider, native tools |
46
+ | `stitchkit/cli` | server | `createCli` — the CLI transport, light (no MCP SDK / `ai`) |
47
+ | `stitchkit/observability` | server | the audit layer — `createAuditHook`, trace context, sanitisation |
45
48
  | `stitchkit/react` | browser | `createCursorQuery`, `createCacheBridge` |
46
49
 
47
50
  Rule of thumb: browser code imports `stitchkit` and `stitchkit/react`; server
48
- code adds `stitchkit/server` and `stitchkit/tools`.
51
+ code adds `stitchkit/server` (or `stitchkit/node` on Node) and `stitchkit/tools`.
52
+ The full export list of each is in the [API reference](../api/reference.md).
49
53
 
50
54
  ## Project layout
51
55
 
@@ -379,6 +383,25 @@ upload: {
379
383
  The client sends a `multipart/form-data` request; the field value must be a
380
384
  `Blob`. See [HTTP server → multipart](./server.md#multipart).
381
385
 
386
+ ### Multipart text fields
387
+
388
+ Any non-file fields sent alongside the file are validated by the endpoint's
389
+ `input` schema. A multipart text field is **always a string** (per the spec) —
390
+ the schema owns its type, exactly as with [query input](#query-input-get--delete):
391
+
392
+ ```ts
393
+ input: z.object({
394
+ id: z.string(), // an id like '33111715' stays a string
395
+ count: z.coerce.number(), // '5' → 5
396
+ active: z.coerce.boolean(), // 'true' → true
397
+ meta: z.preprocess((v) => JSON.parse(String(v)), MetaSchema), // opt a field into JSON
398
+ })
399
+ ```
400
+
401
+ The content is never sniffed to guess a type — a field is a string until the
402
+ schema coerces it. Send a JSON blob as a stringified field and parse it with
403
+ `z.preprocess`; do not rely on the framework to auto-decode it.
404
+
382
405
  ## Pagination
383
406
 
384
407
  Every list endpoint should return the cursor envelope — one shape, one infinite-
@@ -427,6 +450,21 @@ for every endpoint that declares none. Scopes are free strings — the framework
427
450
  attaches no meaning, your auth hook does. See
428
451
  [Auth & errors](./auth-and-errors.md).
429
452
 
453
+ A plain `defineContract` defaults a missing `scope` to `'public'` — forget it and
454
+ the endpoint is public (fail-open). If every contract in your app must be scoped,
455
+ `createContractFactory` binds your scope vocabulary once and makes `scope`
456
+ **required and typed** — a missing or mistyped scope is a compile error:
457
+
458
+ ```ts
459
+ // app: one line, once
460
+ export const { defineContract } = createContractFactory<'public' | 'user' | 'admin'>()
461
+
462
+ // scope is now mandatory and checked against the union
463
+ export const users = defineContract({ prefix: 'users', scope: 'user' }, { … })
464
+ ```
465
+
466
+ The vocabulary is yours; the returned contracts are ordinary `ContractDef`s.
467
+
430
468
  ## One source of truth
431
469
 
432
470
  A contract is plain data — no classes, no decorators, no codegen. It is imported
@@ -1440,6 +1478,25 @@ const handleMcp = createMcpHandler({
1440
1478
  })
1441
1479
  ```
1442
1480
 
1481
+ ## Logging tool calls — `createToolLogger`
1482
+
1483
+ Every tool mount fires an `afterToolCall` hook. `createToolLogger` is a ready
1484
+ preset for it — one line logs each call (ok / failed, duration, which endpoint,
1485
+ keyed by the endpoint's stable `serviceName` / `key` identity):
1486
+
1487
+ ```ts
1488
+ import { createToolLogger } from 'stitchkit/tools'
1489
+
1490
+ mountMcp(server, services, { hooks: createToolLogger() })
1491
+ // [tool] ok list_widgets (widgets.list) 12ms
1492
+ // [tool] warn get_widget (widgets.get) NOT_FOUND 4ms
1493
+ ```
1494
+
1495
+ Pass `log` to redirect the line, or `onRecord` to feed a metrics sink the
1496
+ structured `ToolCallRecord`. For a boot-time picture of what is exposed where,
1497
+ `summarizeTransports(services)` returns per-transport operation counts (HTTP /
1498
+ MCP / AGENT / CLI) for you to log.
1499
+
1443
1500
  ## One handler, three callers
1444
1501
 
1445
1502
  A contract handler runs the same for an HTTP request, an MCP tool call and an
@@ -1858,6 +1915,35 @@ onSuccess: () => bridge.markFresh(['notes'])
1858
1915
  to the socket and call `queryClient` directly. The bridge just centralises the
1859
1916
  event-to-cache mapping and the echo guard.
1860
1917
 
1918
+ ### Entity cache handlers
1919
+
1920
+ The created / updated / deleted events of one entity almost always patch the
1921
+ cache the same way: prepend to the list, replace by id, remove by id — plus the
1922
+ detail query. `createEntityCacheHandlers` builds those three handlers from a
1923
+ small config, so you wire them onto the bridge instead of hand-rolling the
1924
+ updater per entity:
1925
+
1926
+ ```ts
1927
+ import { createEntityCacheHandlers } from 'stitchkit/react'
1928
+
1929
+ const widgetCache = createEntityCacheHandlers<Widget>({
1930
+ getId: (w) => w.id,
1931
+ listKey: ['widgets'],
1932
+ detailKey: (id) => ['widgets', id],
1933
+ })
1934
+
1935
+ createCacheBridge({ socket, queryClient, handlers: {
1936
+ widgetCreated: widgetCache.created,
1937
+ widgetUpdated: widgetCache.updated,
1938
+ widgetDeleted: widgetCache.deleted,
1939
+ }})
1940
+ ```
1941
+
1942
+ It patches stitchkit's `Paginated<T>` list envelope (plain or an infinite list
1943
+ of pages) and honours the same `isFresh` echo guard. It deliberately does **not**
1944
+ flatten pages or add a `useAllX` surface — flattening stays in the component;
1945
+ this only keeps the cache correct.
1946
+
1861
1947
  ## Raw binary lane (Bun)
1862
1948
 
1863
1949
  Socket.IO carries binary fine — for most streams a binary event (`pcm(frame)`)
@@ -2242,6 +2328,54 @@ onError: (ctx, err) => {
2242
2328
  }
2243
2329
  ```
2244
2330
 
2331
+ ## Domain errors — `defineErrors`
2332
+
2333
+ Declaring your app's error codes once gives you typed throwers on the server and
2334
+ a code table the client matches with autocomplete — instead of reading the raw
2335
+ `message` string (which breaks the moment a code expects a string but gets an
2336
+ object):
2337
+
2338
+ ```ts
2339
+ export const { errors, codes, isCode } = defineErrors({
2340
+ SESSION_NOT_FOUND: 404,
2341
+ QUOTA_EXCEEDED: 429,
2342
+ })
2343
+
2344
+ // server — a typed thrower, the right HTTP status baked in
2345
+ throw errors.SESSION_NOT_FOUND('no such session')
2346
+
2347
+ // client — match the code, never a magic string
2348
+ if (err instanceof ApiError && err.code === codes.SESSION_NOT_FOUND) { … }
2349
+ ```
2350
+
2351
+ The `code` rides through unchanged in both the HTTP envelope and the MCP tool
2352
+ result, so one vocabulary covers every transport. The codes are yours; the core
2353
+ stays domain-free.
2354
+
2355
+ ## `createErrorHook`
2356
+
2357
+ `createErrorHook` is the code-map above, packaged — you supply the exhaustive
2358
+ `codeMap` and the envelope shape, it does the normalisation (including the
2359
+ never-leak-an-internal-message rule for a raw throw):
2360
+
2361
+ ```ts
2362
+ const onError = createErrorHook({
2363
+ codeMap: {
2364
+ BAD_REQUEST: 'bad_request', VALIDATION_ERROR: 'bad_request',
2365
+ UNAUTHORIZED: 'unauthenticated', FORBIDDEN: 'forbidden',
2366
+ NOT_FOUND: 'not_found', METHOD_NOT_ALLOWED: 'not_found',
2367
+ CONFLICT: 'conflict', RATE_LIMITED: 'rate_limited',
2368
+ INTERNAL_SERVER_ERROR: 'internal',
2369
+ } satisfies Record<StitchErrorCode, string>,
2370
+ render: (info) => ({ ok: false, error: { code: info.code, message: info.message } }),
2371
+ })
2372
+
2373
+ createServer({ services, hooks: { onError } })
2374
+ ```
2375
+
2376
+ Codes you threw yourself (not stitchkit's) pass through `codeMap` unchanged; the
2377
+ `satisfies Record<StitchErrorCode, …>` keeps the map exhaustive across upgrades.
2378
+
2245
2379
 
2246
2380
  ==============================================================================
2247
2381
  # Guide: Observability (docs/guide/observability.md)
@@ -2414,6 +2548,11 @@ fresh root `traceparent` on every request, which the server then continues. The
2414
2548
  trace helpers themselves are browser-safe and also exported from the root
2415
2549
  `stitchkit` entry — a custom client can format its own header.
2416
2550
 
2551
+ The default CORS allow-list already permits `traceparent` / `tracestate`, so a
2552
+ cross-origin `trace: true` client works out of the box. If you set a custom
2553
+ `cors.headers`, extend `DEFAULT_CORS_ALLOW_HEADERS` rather than replacing it, or
2554
+ the preflight will reject the trace header.
2555
+
2417
2556
  > **Span ids live in the request context, not on `ctx`.** The handler `ctx`
2418
2557
  > carries a single `traceId`; the full `{ traceId, spanId, parentSpanId }` is on
2419
2558
  > the observability request context. To stamp `spanId` / `parentSpanId` into an
@@ -2938,6 +3077,8 @@ from the root `stitchkit`.
2938
3077
  | Export | Kind | Summary |
2939
3078
  |--------|------|---------|
2940
3079
  | `defineContract` | function | declare a contract — [guide](../guide/contracts.md#definecontract) |
3080
+ | `createContractFactory` | function | a `defineContract` with a required, typed `scope` — [guide](../guide/contracts.md#scope) |
3081
+ | `ScopedDefineContract` | _type_ | the `defineContract` `createContractFactory` returns |
2941
3082
  | `ALL_TRANSPORTS` | constant | `['HTTP', 'MCP', 'AGENT', 'CLI']` |
2942
3083
  | `ContractDef` | _type_ | a defined contract |
2943
3084
  | `ContractMeta` | _type_ | a contract's `prefix` + optional `scope` |
@@ -2954,6 +3095,8 @@ from the root `stitchkit`.
2954
3095
  | `ScopedEndpointFn` | _type_ | one method's signature with the consumed keys folded in |
2955
3096
  | `MultipartFile` | _type_ | a `multipart` file field — `Blob \| FileDescriptor` |
2956
3097
  | `FileDescriptor` | _type_ | a React Native / Expo file — `{ uri, name, type }` |
3098
+ | `EndpointToolAnnotations` | _type_ | MCP behavioural hints on an endpoint (`readOnlyHint` / `destructiveHint` / `title`) |
3099
+ | `EndpointUiMeta` | _type_ | MCP Apps widget metadata on an endpoint |
2957
3100
 
2958
3101
  ### Errors
2959
3102
 
@@ -2968,6 +3111,9 @@ from the root `stitchkit`.
2968
3111
  | `conflict` | function | throw `409 CONFLICT` |
2969
3112
  | `rateLimited` | function | throw `429 RATE_LIMITED` |
2970
3113
  | `appError` | function | throw an `AppError` for any code |
3114
+ | `defineErrors` | function | declare domain error codes → typed throwers + a code table — [guide](../guide/auth-and-errors.md#domain-errors--defineerrors) |
3115
+ | `DefinedErrors` | _type_ | the `{ errors, codes, isCode }` handle `defineErrors` returns |
3116
+ | `ErrorThrower` | _type_ | one `defineErrors` thrower — `(message?, details?, hint?) => never` |
2971
3117
  | `STITCH_ERROR_STATUS` | const | `code → HTTP status` map for stitchkit's own error codes — [guide](../guide/auth-and-errors.md#stitch-codes-vs-your-codes) |
2972
3118
  | `StitchErrorCode` | _type_ | a code stitchkit itself emits (`keyof STITCH_ERROR_STATUS`) |
2973
3119
  | `isStitchErrorCode` | function | type guard — is a code one of stitchkit's own? |
@@ -3023,14 +3169,22 @@ Also re-exports the error helpers from `stitchkit/contract`.
3023
3169
  | Export | Kind | Summary |
3024
3170
  |--------|------|---------|
3025
3171
  | `createAuthHook` | function | a scope-enforcing `beforeHandle` hook — [guide](../guide/auth-and-errors.md#createauthhook) |
3172
+ | `createErrorHook` | function | an `onError` hook from a code map + envelope renderer — [guide](../guide/auth-and-errors.md#createerrorhook) |
3173
+ | `ErrorHookConfig` | _type_ | config for `createErrorHook` |
3174
+ | `ResolvedError` | _type_ | the normalised error handed to `createErrorHook`'s `render` |
3026
3175
  | `createBearerResolver` | function | a bearer-token identity resolver |
3176
+ | `signJwt` | function | sign an HS256 JWT |
3027
3177
  | `verifyJwt` | function | verify an HS256 JWT |
3028
3178
  | `extractToken` | function | read a bearer token from header or cookie |
3179
+ | `deriveCodeChallenge` | function | PKCE — derive the `code_challenge` from a verifier |
3180
+ | `verifyPkce` | function | PKCE — verify a verifier against a stored challenge |
3029
3181
  | `AuthHook` | _type_ | the hook `createAuthHook` returns |
3030
3182
  | `AuthHookConfig` | _type_ | config for `createAuthHook` |
3031
3183
  | `AuthRule` | _type_ | `'public' \| 'authenticated' \| predicate` |
3032
3184
  | `BearerResolverConfig` | _type_ | config for `createBearerResolver` |
3033
3185
  | `JwtPayload` | _type_ | a decoded JWT payload |
3186
+ | `SignJwtOptions` | _type_ | options for `signJwt` (expiry, claims) |
3187
+ | `PkceMethod` | _type_ | the PKCE challenge method — `'S256' \| 'plain'` |
3034
3188
 
3035
3189
  ### Cookies & CORS
3036
3190
 
@@ -3041,6 +3195,7 @@ Also re-exports the error helpers from `stitchkit/contract`.
3041
3195
  | `serializeCookie` | function | build a `Set-Cookie` value |
3042
3196
  | `corsHeaders` | function | compute CORS response headers |
3043
3197
  | `corsPreflightResponse` | function | build a preflight `Response` |
3198
+ | `DEFAULT_CORS_ALLOW_HEADERS` | const | the default `Access-Control-Allow-Headers` (incl. `traceparent`) — extend it when overriding `cors.headers` |
3044
3199
  | `CookieDef` | _type_ | the `defineCookie` handle |
3045
3200
  | `CookieOptions` | _type_ | cookie attributes |
3046
3201
  | `CorsConfig` | _type_ | CORS policy |
@@ -3073,9 +3228,11 @@ Also re-exports the error helpers from `stitchkit/contract`.
3073
3228
  | `generateTraceId` | function | a fresh trace id |
3074
3229
  | `resolveTraceId` | function | the default per-request trace-id resolver |
3075
3230
  | `extractIp` | function | the caller IP from a request |
3231
+ | `resolveSocketIp` | function | the caller IP for a Socket.IO handshake (proxy-aware) |
3076
3232
  | `getClientInfo` | function | caller IP + user-agent |
3077
3233
  | `EventBus` | _type_ | the `createEventBus` handle |
3078
3234
  | `RateLimitConfig` | _type_ | config for `createRateLimiter` |
3235
+ | `ClientIpOptions` | _type_ | trusted-proxy config for `extractIp` / `resolveSocketIp` |
3079
3236
  | `ParseSSEOptions` | _type_ | options for `parseSSE` |
3080
3237
 
3081
3238
  ### OpenAPI
@@ -3115,6 +3272,8 @@ audit event. See the [Observability guide](../guide/observability.md).
3115
3272
  | `getTraceId` | function | the active trace id — pass as `traceId` to `createServer` |
3116
3273
  | `getUserId` | function | the active user id, once auth has resolved it |
3117
3274
  | `setRequestUser` | function | attach the resolved user to the active context |
3275
+ | `setRequestEndpoint` | function | attach the resolved endpoint identity to the active context |
3276
+ | `setRequestDimensions` | function | attach custom audit dimensions to the active context |
3118
3277
  | `setRequestError` | function | record the error outcome on the active context |
3119
3278
  | `runWithRequestContext` | function | run a function inside a given context |
3120
3279
  | `RequestContext` | _type_ | the per-request record |
@@ -3177,11 +3336,98 @@ Server-only. Turns contracts into MCP and AI-agent tools. Needs the
3177
3336
  | `ToolExtend` | _type_ | extra-args extension for `mountMcp` / `mountAgent` |
3178
3337
  | `ToolLifecycle` | _type_ | `beforeHandle` / `afterHandle` gate for tool calls — [guide](../guide/mcp-and-agents.md#guarding-tools--lifecycle) |
3179
3338
  | `ToolCallHooks` | _type_ | `beforeToolCall` / `afterToolCall` observability hooks |
3339
+ | `ErrorHintFn` | _type_ | `(toolName, errorCode) => string \| null` — a per-tool recovery hint, shared by every mount |
3180
3340
  | `ToolResult` | _type_ | the result of one tool call |
3181
3341
  | `ToolNameEntry` | _type_ | one `listToolNames` row — `{ name, service, method, transports }` |
3182
3342
  | `IncompatibleSchemaPolicy` | _type_ | `'throw' \| 'skip' \| 'warn'` |
3183
3343
  | `McpMediaContent` | _type_ | a multimodal MCP content item |
3184
3344
 
3345
+ ### Native tools
3346
+
3347
+ Generic host-supplied tools mounted onto a server — not derived from a contract.
3348
+
3349
+ | Export | Kind | Summary |
3350
+ |--------|------|---------|
3351
+ | `mountDownload` | function | a "download a URL to disk" tool (SSRF-guarded, size-capped) |
3352
+ | `mountUpload` | function | an "upload a local file" tool |
3353
+ | `mountWait` | function | a generic `--wait`-style polling tool |
3354
+ | `DownloadToolConfig` | _type_ | config for `mountDownload` |
3355
+ | `UploadToolConfig` | _type_ | config for `mountUpload` |
3356
+ | `WaitToolConfig` | _type_ | config for `mountWait` |
3357
+
3358
+ ### OAuth 2.1 provider
3359
+
3360
+ A native remote-connector auth surface for MCP — [guide](../guide/mcp-and-agents.md#oauth-21--a-native-remote-connector).
3361
+
3362
+ | Export | Kind | Summary |
3363
+ |--------|------|---------|
3364
+ | `mountOAuthProvider` | function | the OAuth 2.1 provider routes (DCR, PKCE, token) |
3365
+ | `oauthProtectedResourceRoute` | function | the RFC 9728 protected-resource-metadata route |
3366
+ | `protectedResourceMetadataUrl` | function | build the metadata URL for a resource |
3367
+ | `wwwAuthenticateHeader` | function | build the `WWW-Authenticate` challenge header |
3368
+ | `PROTECTED_RESOURCE_PATH` | const | the well-known metadata path |
3369
+ | `OAuthProviderConfig` | _type_ | config for `mountOAuthProvider` |
3370
+ | `ProtectedResourceConfig` | _type_ | config for `oauthProtectedResourceRoute` |
3371
+ | `AuthCodeData` | _type_ | a stored authorization-code record |
3372
+ | `AuthRequest` | _type_ | a parsed authorization request |
3373
+ | `ClientMetadata` | _type_ | dynamic-client-registration metadata |
3374
+ | `RefreshData` | _type_ | a stored refresh-token record |
3375
+ | `RegisteredClient` | _type_ | a registered OAuth client |
3376
+
3377
+ ### MCP Apps (widgets)
3378
+
3379
+ Interactive MCP resources — [ADR 0019](../decisions/0019-generic-native-tools.md).
3380
+
3381
+ | Export | Kind | Summary |
3382
+ |--------|------|---------|
3383
+ | `mountMcpResource` | function | mount an MCP Apps widget resource |
3384
+ | `inlineMcpAppBundle` | function | inline a built widget bundle into a resource |
3385
+ | `EXT_APPS_BUNDLE_PLACEHOLDER` | const | the placeholder token `inlineMcpAppBundle` replaces |
3386
+ | `RESOURCE_MIME_TYPE` | const | the MCP Apps resource MIME type |
3387
+ | `McpResourceDef` | _type_ | an MCP Apps resource definition |
3388
+ | `McpAppResourceMeta` | _type_ | resource `_meta` for an MCP App |
3389
+ | `McpAppCsp` | _type_ | the widget content-security policy |
3390
+
3391
+ ### Introspection & internals
3392
+
3393
+ Advanced building blocks — the shared machinery the mounts are built on.
3394
+
3395
+ | Export | Kind | Summary |
3396
+ |--------|------|---------|
3397
+ | `collectTools` | function | resolve a service's methods to mountable tools (the shared resolver) |
3398
+ | `createToolLogger` | function | a ready `afterToolCall` that logs every tool call — [guide](../guide/mcp-and-agents.md#logging-tool-calls--createtoollogger) |
3399
+ | `summarizeTransports` | function | per-transport operation counts for a boot-time summary |
3400
+ | `buildToolManifest` | function | a searchable `{ name, description, inputSchema }` manifest for a `tool_search` tool |
3401
+ | `ToolLoggerConfig` | _type_ | config for `createToolLogger` |
3402
+ | `ToolCallRecord` | _type_ | the structured record `createToolLogger` passes to `onRecord` |
3403
+ | `TransportSummary` | _type_ | the result of `summarizeTransports` |
3404
+ | `TransportCounts` | _type_ | per-transport counts (`{ HTTP, MCP, AGENT, CLI }`) |
3405
+ | `coerceJsonArgs` | function | coerce JSON-stringified array/object tool arguments |
3406
+ | `flattenDiscriminatedUnion` | function | flatten one discriminated union into a single object schema |
3407
+ | `flattenUnionsDeep` | function | flatten discriminated unions at every depth (advertised schema only) |
3408
+ | `MountableTool` | _type_ | one contract method resolved for mounting |
3409
+ | `ToolManifestEntry` | _type_ | one `buildToolManifest` row |
3410
+
3411
+ ---
3412
+
3413
+ ## `stitchkit/node`
3414
+
3415
+ Server-only, for Node ≥ 22 (Bun uses `stitchkit/server`). The runtime-agnostic
3416
+ core plus a Node HTTP adapter — [ADR 0013](../decisions/0013-runtime-agnostic-core.md),
3417
+ [deployment guide](../guide/testing-and-deployment.md#node). Re-exports the
3418
+ runtime-agnostic pieces of `stitchkit/server` and the error helpers.
3419
+
3420
+ | Export | Kind | Summary |
3421
+ |--------|------|---------|
3422
+ | `serveNode` | function | build the router and start a Node HTTP server (via `srvx`) |
3423
+ | `createHandler` | function | the router as a bare `(req) => Response` (same as `/server`) |
3424
+ | `createSocketIOServer` | function | the typed Socket.IO server (same as `/server`) |
3425
+ | `implement` / `createImplement` | function | bind a contract to typed handlers (same as `/server`) |
3426
+ | `NodeServerConfig` | _type_ | config for `serveNode` |
3427
+ | `NodeServerHandle` | _type_ | the `serveNode` handle (`{ port, stop }`) |
3428
+ | `HandlerConfig` / `ServiceDef` / `RawRoute` / `RawRouteContext` / `SocketIOServerConfig` / `SocketIOServerHandle` | _type_ | re-exported from `/server` |
3429
+ | `AppError` + `appError` / `badRequest` / `unauthorized` / `forbidden` / `notFound` / `conflict` / `rateLimited` | — | error helpers (same as `/contract`) |
3430
+
3185
3431
  ---
3186
3432
 
3187
3433
  ## `stitchkit/cli`
@@ -3216,6 +3462,10 @@ and `react-query-kit` peers.
3216
3462
  |--------|------|---------|
3217
3463
  | `createCursorQuery` | function | a cursor-paginated infinite query — [guide](../guide/client.md#cursor-pagination) |
3218
3464
  | `createCacheBridge` | function | sync socket events into the Query cache — [guide](../guide/realtime.md#cache-bridge) |
3465
+ | `createEntityCacheHandlers` | function | created/updated/deleted cache handlers for one entity — [guide](../guide/realtime.md#entity-cache-handlers) |
3466
+ | `EntityCacheConfig` | _type_ | config for `createEntityCacheHandlers` |
3467
+ | `EntityCacheHandlers` | _type_ | the `{ created, updated, deleted }` handlers it returns |
3468
+ | `DeletedPayload` | _type_ | a `deleted` event payload — the entity or a bare `{ id }` |
3219
3469
  | `CursorQueryConfig` | _type_ | config for `createCursorQuery` |
3220
3470
  | `CacheBridge` | _type_ | the `createCacheBridge` handle |
3221
3471
  | `CacheBridgeConfig` | _type_ | config for `createCacheBridge` |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "stitchkit",
3
- "version": "0.18.0",
3
+ "version": "0.20.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",