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.
- package/dist/browser/client.d.ts.map +1 -1
- package/dist/browser/socket-io.d.ts.map +1 -1
- package/dist/cli.js +3 -3
- package/dist/contract/errors-factory.d.ts +18 -0
- package/dist/contract/errors-factory.d.ts.map +1 -0
- package/dist/contract/factory.d.ts +36 -0
- package/dist/contract/factory.d.ts.map +1 -0
- package/dist/contract/index.d.ts +2 -0
- package/dist/contract/index.d.ts.map +1 -1
- package/dist/contract/index.js +6 -1
- package/dist/contract/pagination.d.ts.map +1 -1
- package/dist/index-809wc1tt.js +18 -0
- package/dist/{index-tm7dqzxc.js → index-c7nyw0yt.js} +9 -9
- package/dist/{index-dz2k8d5h.js → index-dzx781tm.js} +1 -1
- package/dist/{index-6qv1fxqy.js → index-f5e4efj1.js} +15 -57
- package/dist/{index-ktn76xz0.js → index-ng2v2ts4.js} +17 -1
- package/dist/index-tje0q6gp.js +44 -0
- package/dist/{index-yzac2zbh.js → index-x62gnfsk.js} +50 -9
- package/dist/{index-akv8rk4w.js → index-xmncnw8z.js} +2 -2
- package/dist/{index-yv93m4yr.js → index-ynnh9x6h.js} +11 -29
- package/dist/index.js +82 -62
- package/dist/internal/base64url.d.ts +16 -0
- package/dist/internal/base64url.d.ts.map +1 -0
- package/dist/node.js +5 -4
- package/dist/observability/index.js +2 -2
- package/dist/react/entity-cache.d.ts +54 -0
- package/dist/react/entity-cache.d.ts.map +1 -0
- package/dist/react.d.ts +1 -0
- package/dist/react.d.ts.map +1 -1
- package/dist/react.js +74 -0
- package/dist/server/create.d.ts.map +1 -1
- package/dist/server/error-hook.d.ts +56 -0
- package/dist/server/error-hook.d.ts.map +1 -0
- package/dist/server/index.d.ts +2 -1
- package/dist/server/index.d.ts.map +1 -1
- package/dist/server/index.js +32 -7
- package/dist/server/middleware/auth.d.ts.map +1 -1
- package/dist/server/middleware/cors.d.ts +13 -0
- package/dist/server/middleware/cors.d.ts.map +1 -1
- package/dist/server/middleware/pkce.d.ts +0 -6
- package/dist/server/middleware/pkce.d.ts.map +1 -1
- package/dist/server/multipart.d.ts +10 -3
- package/dist/server/multipart.d.ts.map +1 -1
- package/dist/server/router.d.ts +0 -2
- package/dist/server/router.d.ts.map +1 -1
- package/dist/server/types.d.ts +0 -8
- package/dist/server/types.d.ts.map +1 -1
- package/dist/tools/agent.d.ts +2 -2
- package/dist/tools/agent.d.ts.map +1 -1
- package/dist/tools/cli-format.d.ts +2 -2
- package/dist/tools/cli-format.d.ts.map +1 -1
- package/dist/tools/cli.d.ts +2 -2
- package/dist/tools/cli.d.ts.map +1 -1
- package/dist/tools/execute.d.ts +7 -0
- package/dist/tools/execute.d.ts.map +1 -1
- package/dist/tools/mcp.d.ts +3 -3
- package/dist/tools/mcp.d.ts.map +1 -1
- package/dist/tools/mount.d.ts +3 -3
- package/dist/tools/mount.d.ts.map +1 -1
- package/dist/tools/oauth-metadata.d.ts.map +1 -1
- package/dist/tools/oauth-provider.d.ts.map +1 -1
- package/dist/tools/tool-logger.d.ts +31 -0
- package/dist/tools/tool-logger.d.ts.map +1 -0
- package/dist/tools/transports.d.ts +28 -0
- package/dist/tools/transports.d.ts.map +1 -0
- package/dist/tools.d.ts +3 -1
- package/dist/tools.d.ts.map +1 -1
- package/dist/tools.js +71 -11
- package/llms-full.txt +254 -4
- 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-
|
|
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-
|
|
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-
|
|
28
|
+
} from "./index-ng2v2ts4.js";
|
|
26
29
|
import"./index-khwedj16.js";
|
|
27
30
|
import {
|
|
28
31
|
isRecord,
|
|
29
32
|
typedEntries
|
|
30
|
-
} from "./index-
|
|
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":
|
|
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":
|
|
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
|
|
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
|
|
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/
|
|
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