stitchkit 0.25.0 → 0.27.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (59) hide show
  1. package/dist/browser/client-multipart.d.ts +22 -0
  2. package/dist/browser/client-multipart.d.ts.map +1 -0
  3. package/dist/browser/client.d.ts.map +1 -1
  4. package/dist/browser/http.d.ts +7 -1
  5. package/dist/browser/http.d.ts.map +1 -1
  6. package/dist/cli.js +2 -2
  7. package/dist/contract/define.d.ts +79 -2
  8. package/dist/contract/define.d.ts.map +1 -1
  9. package/dist/contract/factory.d.ts +1 -0
  10. package/dist/contract/factory.d.ts.map +1 -1
  11. package/dist/contract/index.js +1 -1
  12. package/dist/{index-h9d1fm0p.js → index-4whcb3c3.js} +64 -22
  13. package/dist/{index-a9n8m4ec.js → index-jvescqgr.js} +1 -1
  14. package/dist/{index-x62gnfsk.js → index-pmftwk2a.js} +19 -1
  15. package/dist/{index-g8kyab85.js → index-y4132gqh.js} +81 -12
  16. package/dist/{index-tje0q6gp.js → index-zfscdskj.js} +6 -1
  17. package/dist/{index-4gawbm74.js → index-zza375qp.js} +41 -3
  18. package/dist/index.js +39 -25
  19. package/dist/internal/errors.d.ts +6 -1
  20. package/dist/internal/errors.d.ts.map +1 -1
  21. package/dist/internal/write-download.d.ts +17 -0
  22. package/dist/internal/write-download.d.ts.map +1 -0
  23. package/dist/node.js +3 -3
  24. package/dist/server/create.d.ts.map +1 -1
  25. package/dist/server/error-hook.d.ts +14 -3
  26. package/dist/server/error-hook.d.ts.map +1 -1
  27. package/dist/server/implement.d.ts.map +1 -1
  28. package/dist/server/index.d.ts +2 -1
  29. package/dist/server/index.d.ts.map +1 -1
  30. package/dist/server/index.js +21 -8
  31. package/dist/server/middleware/cors.d.ts +19 -0
  32. package/dist/server/middleware/cors.d.ts.map +1 -1
  33. package/dist/server/openapi.d.ts.map +1 -1
  34. package/dist/server/router.d.ts +27 -0
  35. package/dist/server/router.d.ts.map +1 -1
  36. package/dist/server/types.d.ts +44 -1
  37. package/dist/server/types.d.ts.map +1 -1
  38. package/dist/tools/agent.d.ts +2 -0
  39. package/dist/tools/agent.d.ts.map +1 -1
  40. package/dist/tools/cli.d.ts.map +1 -1
  41. package/dist/tools/execute.d.ts +1 -1
  42. package/dist/tools/execute.d.ts.map +1 -1
  43. package/dist/tools/list-names.d.ts +2 -1
  44. package/dist/tools/list-names.d.ts.map +1 -1
  45. package/dist/tools/mcp.d.ts +10 -3
  46. package/dist/tools/mcp.d.ts.map +1 -1
  47. package/dist/tools/mount-download.d.ts.map +1 -1
  48. package/dist/tools/mount-upload.d.ts.map +1 -1
  49. package/dist/tools/mount-wait.d.ts.map +1 -1
  50. package/dist/tools/mount.d.ts +13 -0
  51. package/dist/tools/mount.d.ts.map +1 -1
  52. package/dist/tools/names.d.ts +34 -0
  53. package/dist/tools/names.d.ts.map +1 -1
  54. package/dist/tools/remote.d.ts.map +1 -1
  55. package/dist/tools/transports.d.ts.map +1 -1
  56. package/dist/tools.js +69 -49
  57. package/llms-full.txt +223 -12
  58. package/llms.txt +1 -1
  59. package/package.json +1 -1
@@ -1,5 +1,6 @@
1
1
  // src/server/middleware/cors.ts
2
2
  var DEFAULT_CORS_ALLOW_HEADERS = "Content-Type, Authorization, X-Trace-Id, traceparent, tracestate";
3
+ var DEFAULT_CORS_EXPOSE_HEADERS = "Content-Disposition, Content-Length, Content-Range, Accept-Ranges, ETag, Last-Modified, X-Trace-Id";
3
4
  function assertCorsConfig(config) {
4
5
  if (config.credentials && (config.origin === undefined || config.origin === "*")) {
5
6
  throw new Error("[stitchkit] cors: `credentials: true` cannot be combined with a wildcard origin. " + "Set `origin` to an explicit string or list.");
@@ -23,6 +24,10 @@ function corsHeaders(config, requestOrigin) {
23
24
  "Access-Control-Allow-Methods": config.methods ?? "GET, POST, PUT, PATCH, DELETE, OPTIONS",
24
25
  "Access-Control-Allow-Headers": config.headers ?? DEFAULT_CORS_ALLOW_HEADERS
25
26
  };
27
+ const expose = Array.isArray(config.exposeHeaders) ? config.exposeHeaders.join(", ") : config.exposeHeaders ?? DEFAULT_CORS_EXPOSE_HEADERS;
28
+ if (expose !== "") {
29
+ headers["Access-Control-Expose-Headers"] = expose;
30
+ }
26
31
  if (allowOrigin !== undefined) {
27
32
  headers["Access-Control-Allow-Origin"] = allowOrigin;
28
33
  if (config.credentials) {
@@ -41,4 +46,4 @@ function corsPreflightResponse(config, req) {
41
46
  });
42
47
  }
43
48
 
44
- export { DEFAULT_CORS_ALLOW_HEADERS, assertCorsConfig, corsHeaders, corsPreflightResponse };
49
+ export { DEFAULT_CORS_ALLOW_HEADERS, DEFAULT_CORS_EXPOSE_HEADERS, assertCorsConfig, corsHeaders, corsPreflightResponse };
@@ -1,3 +1,7 @@
1
+ import {
2
+ isRecord
3
+ } from "./index-c7nyw0yt.js";
4
+
1
5
  // src/contract/errors.ts
2
6
  var APP_ERROR_BRAND = Symbol.for("stitchkit.AppError");
3
7
 
@@ -64,6 +68,15 @@ function isStitchErrorCode(code) {
64
68
  function appError(code, message, details) {
65
69
  throw new AppError(code, message, isStitchErrorCode(code) ? STITCH_ERROR_STATUS[code] : 500, details);
66
70
  }
71
+
72
+ // src/contract/define.ts
73
+ function mergeMeta(contractMeta, endpointMeta) {
74
+ if (!contractMeta)
75
+ return endpointMeta;
76
+ if (!endpointMeta)
77
+ return { ...contractMeta };
78
+ return { ...contractMeta, ...endpointMeta };
79
+ }
67
80
  // src/contract/pagination.ts
68
81
  import { z } from "zod";
69
82
 
@@ -121,10 +134,35 @@ function normalizeError(err) {
121
134
  console.error("[stitchkit] unhandled error:", err);
122
135
  return new AppError("INTERNAL_SERVER_ERROR", "Internal server error", 500);
123
136
  }
124
- function validateHandlerOutput(schema, data) {
137
+ function strippedPaths(before, after, prefix) {
138
+ if (Array.isArray(before)) {
139
+ if (!Array.isArray(after))
140
+ return [];
141
+ return before.flatMap((item, i) => strippedPaths(item, after[i], `${prefix}[${i}]`));
142
+ }
143
+ if (!isRecord(before) || !isRecord(after))
144
+ return [];
145
+ const paths = [];
146
+ for (const [key, value] of Object.entries(before)) {
147
+ const path = prefix ? `${prefix}.${key}` : key;
148
+ if (!(key in after)) {
149
+ paths.push(path);
150
+ continue;
151
+ }
152
+ paths.push(...strippedPaths(value, after[key], path));
153
+ }
154
+ return paths;
155
+ }
156
+ function validateHandlerOutput(schema, data, onStripped) {
125
157
  const parsed = schema.safeParse(data);
126
- if (parsed.success)
158
+ if (parsed.success) {
159
+ if (onStripped) {
160
+ const paths = strippedPaths(data, parsed.data, "");
161
+ if (paths.length > 0)
162
+ onStripped(paths);
163
+ }
127
164
  return { ok: true, data: parsed.data };
165
+ }
128
166
  return {
129
167
  ok: false,
130
168
  message: `Handler output does not match the contract: ${formatZodError(parsed.error)}`
@@ -138,4 +176,4 @@ function isWithinDir(root, target) {
138
176
  return target === root || target === base || target.startsWith(base + sep);
139
177
  }
140
178
 
141
- export { AppError, notFound, badRequest, unauthorized, forbidden, conflict, rateLimited, STITCH_ERROR_STATUS, isStitchErrorCode, appError, bytesToBase64Url, base64UrlToBytes, formatZodError, zodIssues, errorCode, normalizeError, validateHandlerOutput, isWithinDir };
179
+ export { mergeMeta, AppError, notFound, badRequest, unauthorized, forbidden, conflict, rateLimited, STITCH_ERROR_STATUS, isStitchErrorCode, appError, bytesToBase64Url, base64UrlToBytes, formatZodError, zodIssues, errorCode, normalizeError, validateHandlerOutput, isWithinDir };
package/dist/index.js CHANGED
@@ -16,7 +16,7 @@ import {
16
16
  paginatedSchema,
17
17
  rateLimited,
18
18
  unauthorized
19
- } from "./index-x62gnfsk.js";
19
+ } from "./index-pmftwk2a.js";
20
20
  import {
21
21
  isRecord,
22
22
  mapObject,
@@ -28,6 +28,25 @@ function inputIsQuery(method) {
28
28
  return method === "GET" || method === "DELETE";
29
29
  }
30
30
 
31
+ // src/browser/client-multipart.ts
32
+ function isFileDescriptor(value) {
33
+ return typeof value === "object" && value !== null && !(value instanceof Blob) && "uri" in value && typeof value.uri === "string" && "name" in value && typeof value.name === "string" && "type" in value && typeof value.type === "string";
34
+ }
35
+ function isMultipartFile(value) {
36
+ return value instanceof Blob || isFileDescriptor(value);
37
+ }
38
+ function appendMultipartFile(form, field, file) {
39
+ const sink = form;
40
+ sink.append(field, file);
41
+ }
42
+ function appendFormFields(formData, values, skipKeys) {
43
+ for (const [key, value] of Object.entries(values)) {
44
+ if (skipKeys.has(key) || value === undefined || value === null)
45
+ continue;
46
+ formData.append(key, typeof value === "string" ? value : JSON.stringify(value));
47
+ }
48
+ }
49
+
31
50
  // src/browser/http.ts
32
51
  import ky, { isHTTPError } from "ky";
33
52
 
@@ -188,6 +207,9 @@ function createHttpClient(config) {
188
207
  if (options.responseType === "blob") {
189
208
  return client[method](url, kyOptions).blob();
190
209
  }
210
+ if (options.responseType === "response") {
211
+ return await client[method](url, kyOptions);
212
+ }
191
213
  const response = await client[method](url, kyOptions);
192
214
  if (response.status === 204 || response.headers.get("content-length") === "0") {
193
215
  return;
@@ -229,10 +251,15 @@ function createHttpClient(config) {
229
251
  }
230
252
 
231
253
  // src/browser/client.ts
232
- function withTimeout(options, timeout) {
233
- if (timeout === undefined)
254
+ function withTimeout(options, endpoint) {
255
+ const responseType = endpoint.rawResponse ? "response" : undefined;
256
+ if (endpoint.timeout === undefined && responseType === undefined)
234
257
  return options;
235
- return { ...options, timeout };
258
+ return {
259
+ ...options,
260
+ ...endpoint.timeout !== undefined && { timeout: endpoint.timeout },
261
+ ...responseType && { responseType }
262
+ };
236
263
  }
237
264
  function withOutput(endpoint, result) {
238
265
  const schema = endpoint.output;
@@ -312,15 +339,15 @@ function createHttpMethod(endpoint, prefix, client, config) {
312
339
  const formData = new FormData;
313
340
  appendMultipartFile(formData, endpoint.multipart, file);
314
341
  appendFormFields(formData, firstArg, new Set([...prefixKeys, endpoint.multipart]));
315
- return withOutput(endpoint, client[httpMethod](url, formData, withTimeout(undefined, endpoint.timeout)));
342
+ return withOutput(endpoint, client[httpMethod](url, formData, withTimeout(undefined, endpoint)));
316
343
  }
317
344
  if (isGet) {
318
345
  const params = collectQueryParams(firstArg, prefixKeys, endpoint);
319
- return withOutput(endpoint, client.get(url, withTimeout(params ? { params } : undefined, endpoint.timeout)));
346
+ return withOutput(endpoint, client.get(url, withTimeout(params ? { params } : undefined, endpoint)));
320
347
  }
321
348
  if (httpMethod === "delete") {
322
349
  const params = collectQueryParams(firstArg, prefixKeys, endpoint);
323
- return withOutput(endpoint, client.delete(url, withTimeout(params ? { params } : undefined, endpoint.timeout)));
350
+ return withOutput(endpoint, client.delete(url, withTimeout(params ? { params } : undefined, endpoint)));
324
351
  }
325
352
  const payload = {};
326
353
  for (const [key, value] of Object.entries(firstArg)) {
@@ -328,7 +355,7 @@ function createHttpMethod(endpoint, prefix, client, config) {
328
355
  payload[key] = value;
329
356
  }
330
357
  }
331
- return withOutput(endpoint, client[httpMethod](url, Object.keys(payload).length > 0 ? payload : undefined, withTimeout(undefined, endpoint.timeout)));
358
+ return withOutput(endpoint, client[httpMethod](url, Object.keys(payload).length > 0 ? payload : undefined, withTimeout(undefined, endpoint)));
332
359
  };
333
360
  }
334
361
  function createFetchMethod(endpoint, prefix, config, contractConfig) {
@@ -384,6 +411,8 @@ function createFetchMethod(endpoint, prefix, config, contractConfig) {
384
411
  if (!res2.ok) {
385
412
  await throwForErrorResponse(res2, config, null);
386
413
  }
414
+ if (endpoint.rawResponse)
415
+ return res2;
387
416
  if (res2.status === 204)
388
417
  return;
389
418
  const json2 = await res2.json();
@@ -401,29 +430,14 @@ function createFetchMethod(endpoint, prefix, config, contractConfig) {
401
430
  if (!res.ok) {
402
431
  await throwForErrorResponse(res, config, { error: res.statusText });
403
432
  }
433
+ if (endpoint.rawResponse)
434
+ return res;
404
435
  if (res.status === 204)
405
436
  return;
406
437
  const json = await res.json();
407
438
  return endpoint.output ? endpoint.output.parse(json) : json;
408
439
  };
409
440
  }
410
- function isFileDescriptor(value) {
411
- return typeof value === "object" && value !== null && !(value instanceof Blob) && "uri" in value && typeof value.uri === "string" && "name" in value && typeof value.name === "string" && "type" in value && typeof value.type === "string";
412
- }
413
- function isMultipartFile(value) {
414
- return value instanceof Blob || isFileDescriptor(value);
415
- }
416
- function appendMultipartFile(form, field, file) {
417
- const sink = form;
418
- sink.append(field, file);
419
- }
420
- function appendFormFields(formData, values, skipKeys) {
421
- for (const [key, value] of Object.entries(values)) {
422
- if (skipKeys.has(key) || value === undefined || value === null)
423
- continue;
424
- formData.append(key, typeof value === "string" ? value : JSON.stringify(value));
425
- }
426
- }
427
441
  async function throwForErrorResponse(res, config, fallbackBody) {
428
442
  const body = await res.json().catch(() => fallbackBody);
429
443
  config.onError?.(res.status, body);
@@ -29,8 +29,13 @@ export declare function normalizeError(err: unknown): AppError;
29
29
  * Validate a handler's return value against the contract `output` schema. A
30
30
  * mismatch is a **server** fault (the handler broke its own contract) — shared
31
31
  * by the HTTP and tool transports so both report it identically.
32
+ *
33
+ * `onStripped` is the migration diagnostic: a handler returning more than its
34
+ * contract declares has the extra fields **deleted**, correctly but invisibly —
35
+ * types cannot catch it (structural typing does not reject excess properties) and
36
+ * nothing logs it. Pass a reporter to find out; omit it and nothing is computed.
32
37
  */
33
- export declare function validateHandlerOutput(schema: ZodType, data: unknown): {
38
+ export declare function validateHandlerOutput(schema: ZodType, data: unknown, onStripped?: (paths: string[]) => void): {
34
39
  ok: true;
35
40
  data: unknown;
36
41
  } | {
@@ -1 +1 @@
1
- {"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../../src/internal/errors.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACtC,OAAO,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AAOvC,wBAAgB,cAAc,CAAC,KAAK,EAAE,CAAC,CAAC,QAAQ,GAAG,MAAM,CAMxD;AAED,qFAAqF;AACrF,MAAM,WAAW,eAAe;IAC9B,2EAA2E;IAC3E,IAAI,EAAE,MAAM,CAAC;IACb,yDAAyD;IACzD,IAAI,EAAE,MAAM,CAAC;IACb,6CAA6C;IAC7C,OAAO,EAAE,MAAM,CAAC;CACjB;AAED;;;;;GAKG;AACH,wBAAgB,SAAS,CAAC,KAAK,EAAE,CAAC,CAAC,QAAQ,GAAG,eAAe,EAAE,CAM9D;AAKD;;;;;GAKG;AACH,wBAAgB,SAAS,CAAC,GAAG,EAAE,OAAO,GAAG,MAAM,GAAG,SAAS,CAI1D;AAED,wBAAgB,cAAc,CAAC,GAAG,EAAE,OAAO,GAAG,QAAQ,CAgBrD;AAED;;;;GAIG;AACH,wBAAgB,qBAAqB,CACnC,MAAM,EAAE,OAAO,EACf,IAAI,EAAE,OAAO,GACZ;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,IAAI,EAAE,OAAO,CAAA;CAAE,GAAG;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CAO9D"}
1
+ {"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../../src/internal/errors.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACtC,OAAO,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AAQvC,wBAAgB,cAAc,CAAC,KAAK,EAAE,CAAC,CAAC,QAAQ,GAAG,MAAM,CAMxD;AAED,qFAAqF;AACrF,MAAM,WAAW,eAAe;IAC9B,2EAA2E;IAC3E,IAAI,EAAE,MAAM,CAAC;IACb,yDAAyD;IACzD,IAAI,EAAE,MAAM,CAAC;IACb,6CAA6C;IAC7C,OAAO,EAAE,MAAM,CAAC;CACjB;AAED;;;;;GAKG;AACH,wBAAgB,SAAS,CAAC,KAAK,EAAE,CAAC,CAAC,QAAQ,GAAG,eAAe,EAAE,CAM9D;AAKD;;;;;GAKG;AACH,wBAAgB,SAAS,CAAC,GAAG,EAAE,OAAO,GAAG,MAAM,GAAG,SAAS,CAI1D;AAED,wBAAgB,cAAc,CAAC,GAAG,EAAE,OAAO,GAAG,QAAQ,CAgBrD;AA4BD;;;;;;;;;GASG;AACH,wBAAgB,qBAAqB,CACnC,MAAM,EAAE,OAAO,EACf,IAAI,EAAE,OAAO,EACb,UAAU,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,IAAI,GACrC;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,IAAI,EAAE,OAAO,CAAA;CAAE,GAAG;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CAe9D"}
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Write a downloaded body to disk, with the containment check both download
3
+ * paths need.
4
+ *
5
+ * Shared by the CLI's `--output-dir` and the `mountDownload` native tool. It
6
+ * deliberately takes an **already-resolved** target rather than building one:
7
+ * the two callers derive the filename differently (an untrusted `file.name` vs a
8
+ * name derived from the URL) and, more importantly, report the path back to the
9
+ * user — so composing it here would silently change one of those outputs from
10
+ * relative to absolute.
11
+ *
12
+ * The containment re-check is cheap and belongs on this side: both callers reduce
13
+ * the name to a basename first, but that is an invariant of *their* code, and
14
+ * this is where the write actually happens.
15
+ */
16
+ export declare function writeDownload(root: string, target: string, data: Uint8Array): Promise<void>;
17
+ //# sourceMappingURL=write-download.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"write-download.d.ts","sourceRoot":"","sources":["../../src/internal/write-download.ts"],"names":[],"mappings":"AAIA;;;;;;;;;;;;;;GAcG;AACH,wBAAsB,aAAa,CACjC,IAAI,EAAE,MAAM,EACZ,MAAM,EAAE,MAAM,EACd,IAAI,EAAE,UAAU,GACf,OAAO,CAAC,IAAI,CAAC,CAMf"}
package/dist/node.js CHANGED
@@ -3,8 +3,8 @@ import {
3
3
  createImplement,
4
4
  createSocketIOServer,
5
5
  implement
6
- } from "./index-g8kyab85.js";
7
- import"./index-tje0q6gp.js";
6
+ } from "./index-y4132gqh.js";
7
+ import"./index-zfscdskj.js";
8
8
  import {
9
9
  AppError,
10
10
  appError,
@@ -14,7 +14,7 @@ import {
14
14
  notFound,
15
15
  rateLimited,
16
16
  unauthorized
17
- } from "./index-4gawbm74.js";
17
+ } from "./index-zza375qp.js";
18
18
  import"./index-dzx781tm.js";
19
19
  import"./index-khwedj16.js";
20
20
  import"./index-c7nyw0yt.js";
@@ -1 +1 @@
1
- {"version":3,"file":"create.d.ts","sourceRoot":"","sources":["../../src/server/create.ts"],"names":[],"mappings":"AA+BA,OAAO,KAAK,EAEV,eAAe,EACf,aAAa,EAGd,MAAM,SAAS,CAAC;AAEjB,wBAAgB,aAAa,CAAC,MAAM,EAAE,aAAa,GAAG,CAAC,GAAG,EAAE,OAAO,KAAK,OAAO,CAAC,QAAQ,CAAC,CA8NxF;AAED,wBAAgB,YAAY,CAAC,MAAM,EAAE,eAAe,uBAsBnD"}
1
+ {"version":3,"file":"create.d.ts","sourceRoot":"","sources":["../../src/server/create.ts"],"names":[],"mappings":"AAgCA,OAAO,KAAK,EAEV,eAAe,EACf,aAAa,EAGd,MAAM,SAAS,CAAC;AAEjB,wBAAgB,aAAa,CAAC,MAAM,EAAE,aAAa,GAAG,CAAC,GAAG,EAAE,OAAO,KAAK,OAAO,CAAC,QAAQ,CAAC,CAyRxF;AAED,wBAAgB,YAAY,CAAC,MAAM,EAAE,eAAe,uBAsBnD"}
@@ -22,11 +22,16 @@
22
22
  * CONFLICT: 'conflict', RATE_LIMITED: 'rate_limited',
23
23
  * INTERNAL_SERVER_ERROR: 'internal',
24
24
  * } satisfies Record<StitchErrorCode, string>,
25
- * render: (info) => ({ ok: false, error: { code: info.code, message: info.message } }),
25
+ * render: (info, ctx) => ({
26
+ * ok: false,
27
+ * error: { code: info.code, message: info.message },
28
+ * traceId: ctx.traceId,
29
+ * }),
26
30
  * });
27
31
  * createServer({ services, hooks: { onError } });
28
32
  * ```
29
33
  */
34
+ import type { RuntimeContext } from '../contract';
30
35
  import { type StitchErrorCode } from '../contract';
31
36
  import type { LifecycleHooks } from './types';
32
37
  /** The normalised error handed to `render` — code already remapped. */
@@ -51,9 +56,15 @@ export interface ErrorHookConfig<TWireCode extends string = string> {
51
56
  */
52
57
  codeMap?: Record<StitchErrorCode, TWireCode>;
53
58
  /** Build the response body from the resolved error. */
54
- render: (info: ResolvedError) => unknown;
59
+ /**
60
+ * Build the response body from the resolved error. `ctx` is the request's
61
+ * `RuntimeContext` — read `ctx.traceId` / `ctx.spanId` to put a correlation id
62
+ * in the envelope, which is the ordinary reason to have one. Declaring the
63
+ * parameter is optional: a one-argument `render` stays assignable.
64
+ */
65
+ render: (info: ResolvedError, ctx: RuntimeContext) => unknown;
55
66
  /** Observe the raw thrown value before rendering — logging / metrics. */
56
- onError?: (error: unknown, info: ResolvedError) => void;
67
+ onError?: (error: unknown, info: ResolvedError, ctx: RuntimeContext) => void;
57
68
  }
58
69
  /** Build an `onError` hook from a code map + envelope renderer. */
59
70
  export declare function createErrorHook<TWireCode extends string = string>(config: ErrorHookConfig<TWireCode>): NonNullable<LifecycleHooks['onError']>;
@@ -1 +1 @@
1
- {"version":3,"file":"error-hook.d.ts","sourceRoot":"","sources":["../../src/server/error-hook.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AACH,OAAO,EAAqB,KAAK,eAAe,EAAE,MAAM,aAAa,CAAC;AAEtE,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,SAAS,CAAC;AAE9C,uEAAuE;AACvE,MAAM,WAAW,aAAa;IAC5B,wFAAwF;IACxF,IAAI,EAAE,MAAM,CAAC;IACb,mBAAmB;IACnB,MAAM,EAAE,MAAM,CAAC;IACf,+FAA+F;IAC/F,OAAO,EAAE,MAAM,CAAC;IAChB,mEAAmE;IACnE,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAClC,6DAA6D;IAC7D,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAED,oCAAoC;AACpC,MAAM,WAAW,eAAe,CAAC,SAAS,SAAS,MAAM,GAAG,MAAM;IAChE;;;;OAIG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC,eAAe,EAAE,SAAS,CAAC,CAAC;IAC7C,uDAAuD;IACvD,MAAM,EAAE,CAAC,IAAI,EAAE,aAAa,KAAK,OAAO,CAAC;IACzC,yEAAyE;IACzE,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,OAAO,EAAE,IAAI,EAAE,aAAa,KAAK,IAAI,CAAC;CACzD;AAED,mEAAmE;AACnE,wBAAgB,eAAe,CAAC,SAAS,SAAS,MAAM,GAAG,MAAM,EAC/D,MAAM,EAAE,eAAe,CAAC,SAAS,CAAC,GACjC,WAAW,CAAC,cAAc,CAAC,SAAS,CAAC,CAAC,CAyBxC"}
1
+ {"version":3,"file":"error-hook.d.ts","sourceRoot":"","sources":["../../src/server/error-hook.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AAEH,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAClD,OAAO,EAAqB,KAAK,eAAe,EAAE,MAAM,aAAa,CAAC;AAEtE,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,SAAS,CAAC;AAE9C,uEAAuE;AACvE,MAAM,WAAW,aAAa;IAC5B,wFAAwF;IACxF,IAAI,EAAE,MAAM,CAAC;IACb,mBAAmB;IACnB,MAAM,EAAE,MAAM,CAAC;IACf,+FAA+F;IAC/F,OAAO,EAAE,MAAM,CAAC;IAChB,mEAAmE;IACnE,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAClC,6DAA6D;IAC7D,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAED,oCAAoC;AACpC,MAAM,WAAW,eAAe,CAAC,SAAS,SAAS,MAAM,GAAG,MAAM;IAChE;;;;OAIG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC,eAAe,EAAE,SAAS,CAAC,CAAC;IAC7C,uDAAuD;IACvD;;;;;OAKG;IACH,MAAM,EAAE,CAAC,IAAI,EAAE,aAAa,EAAE,GAAG,EAAE,cAAc,KAAK,OAAO,CAAC;IAC9D,yEAAyE;IACzE,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,OAAO,EAAE,IAAI,EAAE,aAAa,EAAE,GAAG,EAAE,cAAc,KAAK,IAAI,CAAC;CAC9E;AAED,mEAAmE;AACnE,wBAAgB,eAAe,CAAC,SAAS,SAAS,MAAM,GAAG,MAAM,EAC/D,MAAM,EAAE,eAAe,CAAC,SAAS,CAAC,GACjC,WAAW,CAAC,cAAc,CAAC,SAAS,CAAC,CAAC,CAyBxC"}
@@ -1 +1 @@
1
- {"version":3,"file":"implement.d.ts","sourceRoot":"","sources":["../../src/server/implement.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,WAAW,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAE5E,OAAO,KAAK,EAAE,QAAQ,EAAa,UAAU,EAAE,MAAM,SAAS,CAAC;AAE/D;;;;;GAKG;AACH,wBAAgB,SAAS,CACvB,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,WAAW,CAAC,EACrC,IAAI,SAAS,cAAc,GAAG,cAAc,EAC5C,QAAQ,EAAE,WAAW,CAAC,CAAC,EAAE,MAAM,CAAC,EAAE,QAAQ,EAAE,QAAQ,CAAC,CAAC,EAAE,IAAI,CAAC,GAAG,UAAU,CA+C3E;AAED;;;;GAIG;AACH,wBAAgB,eAAe,CAAC,IAAI,SAAS,cAAc,MACjD,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,WAAW,CAAC,EAC3C,UAAU,WAAW,CAAC,CAAC,EAAE,MAAM,CAAC,EAChC,UAAU,QAAQ,CAAC,CAAC,EAAE,IAAI,CAAC,KAC1B,UAAU,CACd"}
1
+ {"version":3,"file":"implement.d.ts","sourceRoot":"","sources":["../../src/server/implement.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,WAAW,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAG5E,OAAO,KAAK,EAAE,QAAQ,EAAa,UAAU,EAAE,MAAM,SAAS,CAAC;AAK/D;;;;;GAKG;AACH,wBAAgB,SAAS,CACvB,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,WAAW,CAAC,EACrC,IAAI,SAAS,cAAc,GAAG,cAAc,EAC5C,QAAQ,EAAE,WAAW,CAAC,CAAC,EAAE,MAAM,CAAC,EAAE,QAAQ,EAAE,QAAQ,CAAC,CAAC,EAAE,IAAI,CAAC,GAAG,UAAU,CAyD3E;AAED;;;;GAIG;AACH,wBAAgB,eAAe,CAAC,IAAI,SAAS,cAAc,MACjD,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,WAAW,CAAC,EAC3C,UAAU,WAAW,CAAC,CAAC,EAAE,MAAM,CAAC,EAChC,UAAU,QAAQ,CAAC,CAAC,EAAE,IAAI,CAAC,KAC1B,UAAU,CACd"}
@@ -1,5 +1,6 @@
1
1
  export { AppError, appError, badRequest, conflict, forbidden, isStitchErrorCode, notFound, rateLimited, STITCH_ERROR_STATUS, type StitchErrorCode, unauthorized, } from '../contract';
2
2
  export { errorCode, formatZodError, normalizeError, type ZodIssueSummary, zodIssues, } from '../internal/errors';
3
+ export { isWithinDir } from '../internal/within-dir';
3
4
  export { cacheHeaders, createCache } from './cache';
4
5
  export { createHandler, createServer } from './create';
5
6
  export { createErrorHook, type ErrorHookConfig, type ResolvedError, } from './error-hook';
@@ -8,7 +9,7 @@ export { type ByteRange, parseByteRange, type ServeFileOptions, serveFile, weakE
8
9
  export { createImplement, implement } from './implement';
9
10
  export { type AuthHook, type AuthHookConfig, type AuthRule, type BearerResolverConfig, createAuthHook, createBearerResolver, extractToken, type JwtPayload, type SignJwtOptions, signJwt, verifyJwt, } from './middleware/auth';
10
11
  export { type CookieDef, type CookieOptions, defineCookie, parseCookies, serializeCookie, } from './middleware/cookies';
11
- export { type CorsConfig, corsHeaders, corsPreflightResponse, DEFAULT_CORS_ALLOW_HEADERS, } from './middleware/cors';
12
+ export { type CorsConfig, corsHeaders, corsPreflightResponse, DEFAULT_CORS_ALLOW_HEADERS, DEFAULT_CORS_EXPOSE_HEADERS, } from './middleware/cors';
12
13
  export { deriveCodeChallenge, type PkceMethod, verifyPkce } from './middleware/pkce';
13
14
  export { parseMultipart } from './multipart';
14
15
  export { generateOpenApiDocument, type OpenApiConfig, type OpenApiDocument, type OpenApiInfo, type OpenApiServer, openApiRoute, } from './openapi';
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/server/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,QAAQ,EACR,QAAQ,EACR,UAAU,EACV,QAAQ,EACR,SAAS,EACT,iBAAiB,EACjB,QAAQ,EACR,WAAW,EACX,mBAAmB,EACnB,KAAK,eAAe,EACpB,YAAY,GACb,MAAM,aAAa,CAAC;AAIrB,OAAO,EACL,SAAS,EACT,cAAc,EACd,cAAc,EACd,KAAK,eAAe,EACpB,SAAS,GACV,MAAM,oBAAoB,CAAC;AAC5B,OAAO,EAAE,YAAY,EAAE,WAAW,EAAE,MAAM,SAAS,CAAC;AACpD,OAAO,EAAE,aAAa,EAAE,YAAY,EAAE,MAAM,UAAU,CAAC;AACvD,OAAO,EACL,eAAe,EACf,KAAK,eAAe,EACpB,KAAK,aAAa,GACnB,MAAM,cAAc,CAAC;AACtB,OAAO,EAAE,cAAc,EAAE,KAAK,QAAQ,EAAE,MAAM,aAAa,CAAC;AAC5D,OAAO,EACL,KAAK,SAAS,EACd,cAAc,EACd,KAAK,gBAAgB,EACrB,SAAS,EACT,QAAQ,GACT,MAAM,QAAQ,CAAC;AAChB,OAAO,EAAE,eAAe,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AACzD,OAAO,EACL,KAAK,QAAQ,EACb,KAAK,cAAc,EACnB,KAAK,QAAQ,EACb,KAAK,oBAAoB,EACzB,cAAc,EACd,oBAAoB,EACpB,YAAY,EACZ,KAAK,UAAU,EACf,KAAK,cAAc,EACnB,OAAO,EACP,SAAS,GACV,MAAM,mBAAmB,CAAC;AAC3B,OAAO,EACL,KAAK,SAAS,EACd,KAAK,aAAa,EAClB,YAAY,EACZ,YAAY,EACZ,eAAe,GAChB,MAAM,sBAAsB,CAAC;AAC9B,OAAO,EACL,KAAK,UAAU,EACf,WAAW,EACX,qBAAqB,EACrB,0BAA0B,GAC3B,MAAM,mBAAmB,CAAC;AAC3B,OAAO,EAAE,mBAAmB,EAAE,KAAK,UAAU,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAC;AACrF,OAAO,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAC7C,OAAO,EACL,uBAAuB,EACvB,KAAK,aAAa,EAClB,KAAK,eAAe,EACpB,KAAK,WAAW,EAChB,KAAK,aAAa,EAClB,YAAY,GACb,MAAM,WAAW,CAAC;AACnB,OAAO,EAAE,iBAAiB,EAAE,KAAK,eAAe,EAAE,MAAM,cAAc,CAAC;AACvE,OAAO,EAAE,aAAa,EAAE,SAAS,EAAE,WAAW,EAAE,MAAM,OAAO,CAAC;AAC9D,OAAO,EACL,KAAK,eAAe,EACpB,SAAS,EACT,eAAe,EACf,aAAa,EACb,eAAe,EACf,cAAc,GACf,MAAM,WAAW,CAAC;AACnB,OAAO,EAAE,WAAW,EAAE,MAAM,UAAU,CAAC;AACvC,YAAY,EAAE,oBAAoB,EAAE,oBAAoB,EAAE,MAAM,aAAa,CAAC;AAC9E,OAAO,EAAE,oBAAoB,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AACjE,OAAO,EAAE,KAAK,eAAe,EAAE,QAAQ,EAAE,SAAS,EAAE,MAAM,UAAU,CAAC;AACrE,YAAY,EACV,SAAS,EACT,eAAe,EACf,aAAa,EACb,QAAQ,EACR,cAAc,EACd,SAAS,EACT,QAAQ,EACR,eAAe,EACf,UAAU,EACV,iBAAiB,EACjB,UAAU,EACV,YAAY,GACb,MAAM,SAAS,CAAC;AACjB,OAAO,EACL,KAAK,YAAY,EACjB,wBAAwB,EACxB,KAAK,sBAAsB,EAC3B,KAAK,aAAa,EAClB,aAAa,GACd,MAAM,aAAa,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/server/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,QAAQ,EACR,QAAQ,EACR,UAAU,EACV,QAAQ,EACR,SAAS,EACT,iBAAiB,EACjB,QAAQ,EACR,WAAW,EACX,mBAAmB,EACnB,KAAK,eAAe,EACpB,YAAY,GACb,MAAM,aAAa,CAAC;AAIrB,OAAO,EACL,SAAS,EACT,cAAc,EACd,cAAc,EACd,KAAK,eAAe,EACpB,SAAS,GACV,MAAM,oBAAoB,CAAC;AAG5B,OAAO,EAAE,WAAW,EAAE,MAAM,wBAAwB,CAAC;AACrD,OAAO,EAAE,YAAY,EAAE,WAAW,EAAE,MAAM,SAAS,CAAC;AACpD,OAAO,EAAE,aAAa,EAAE,YAAY,EAAE,MAAM,UAAU,CAAC;AACvD,OAAO,EACL,eAAe,EACf,KAAK,eAAe,EACpB,KAAK,aAAa,GACnB,MAAM,cAAc,CAAC;AACtB,OAAO,EAAE,cAAc,EAAE,KAAK,QAAQ,EAAE,MAAM,aAAa,CAAC;AAC5D,OAAO,EACL,KAAK,SAAS,EACd,cAAc,EACd,KAAK,gBAAgB,EACrB,SAAS,EACT,QAAQ,GACT,MAAM,QAAQ,CAAC;AAChB,OAAO,EAAE,eAAe,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AACzD,OAAO,EACL,KAAK,QAAQ,EACb,KAAK,cAAc,EACnB,KAAK,QAAQ,EACb,KAAK,oBAAoB,EACzB,cAAc,EACd,oBAAoB,EACpB,YAAY,EACZ,KAAK,UAAU,EACf,KAAK,cAAc,EACnB,OAAO,EACP,SAAS,GACV,MAAM,mBAAmB,CAAC;AAC3B,OAAO,EACL,KAAK,SAAS,EACd,KAAK,aAAa,EAClB,YAAY,EACZ,YAAY,EACZ,eAAe,GAChB,MAAM,sBAAsB,CAAC;AAC9B,OAAO,EACL,KAAK,UAAU,EACf,WAAW,EACX,qBAAqB,EACrB,0BAA0B,EAC1B,2BAA2B,GAC5B,MAAM,mBAAmB,CAAC;AAC3B,OAAO,EAAE,mBAAmB,EAAE,KAAK,UAAU,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAC;AACrF,OAAO,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAC7C,OAAO,EACL,uBAAuB,EACvB,KAAK,aAAa,EAClB,KAAK,eAAe,EACpB,KAAK,WAAW,EAChB,KAAK,aAAa,EAClB,YAAY,GACb,MAAM,WAAW,CAAC;AACnB,OAAO,EAAE,iBAAiB,EAAE,KAAK,eAAe,EAAE,MAAM,cAAc,CAAC;AACvE,OAAO,EAAE,aAAa,EAAE,SAAS,EAAE,WAAW,EAAE,MAAM,OAAO,CAAC;AAC9D,OAAO,EACL,KAAK,eAAe,EACpB,SAAS,EACT,eAAe,EACf,aAAa,EACb,eAAe,EACf,cAAc,GACf,MAAM,WAAW,CAAC;AACnB,OAAO,EAAE,WAAW,EAAE,MAAM,UAAU,CAAC;AACvC,YAAY,EAAE,oBAAoB,EAAE,oBAAoB,EAAE,MAAM,aAAa,CAAC;AAC9E,OAAO,EAAE,oBAAoB,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AACjE,OAAO,EAAE,KAAK,eAAe,EAAE,QAAQ,EAAE,SAAS,EAAE,MAAM,UAAU,CAAC;AACrE,YAAY,EACV,SAAS,EACT,eAAe,EACf,aAAa,EACb,QAAQ,EACR,cAAc,EACd,SAAS,EACT,QAAQ,EACR,eAAe,EACf,UAAU,EACV,iBAAiB,EACjB,UAAU,EACV,YAAY,GACb,MAAM,SAAS,CAAC;AACjB,OAAO,EACL,KAAK,YAAY,EACjB,wBAAwB,EACxB,KAAK,sBAAsB,EAC3B,KAAK,aAAa,EAClB,aAAa,GACd,MAAM,aAAa,CAAC"}
@@ -10,7 +10,7 @@ import {
10
10
  socketIoLane,
11
11
  staticRoute,
12
12
  webSocketLane
13
- } from "../index-g8kyab85.js";
13
+ } from "../index-y4132gqh.js";
14
14
  import {
15
15
  createAuthHook,
16
16
  createBearerResolver,
@@ -23,12 +23,13 @@ import {
23
23
  signJwt,
24
24
  verifyJwt,
25
25
  verifyPkce
26
- } from "../index-a9n8m4ec.js";
26
+ } from "../index-jvescqgr.js";
27
27
  import {
28
28
  DEFAULT_CORS_ALLOW_HEADERS,
29
+ DEFAULT_CORS_EXPOSE_HEADERS,
29
30
  corsHeaders,
30
31
  corsPreflightResponse
31
- } from "../index-tje0q6gp.js";
32
+ } from "../index-zfscdskj.js";
32
33
  import {
33
34
  jsonSchemaFields,
34
35
  toJsonSchema
@@ -43,12 +44,13 @@ import {
43
44
  forbidden,
44
45
  formatZodError,
45
46
  isStitchErrorCode,
47
+ isWithinDir,
46
48
  normalizeError,
47
49
  notFound,
48
50
  rateLimited,
49
51
  unauthorized,
50
52
  zodIssues
51
- } from "../index-4gawbm74.js";
53
+ } from "../index-zza375qp.js";
52
54
  import {
53
55
  extractIp,
54
56
  generateTraceId,
@@ -120,7 +122,7 @@ function cacheHeaders(maxAge, scope = "public") {
120
122
  }
121
123
  // src/server/error-hook.ts
122
124
  function createErrorHook(config) {
123
- return (_ctx, error) => {
125
+ return (ctx, error) => {
124
126
  const appErr = normalizeError(error);
125
127
  const code = config.codeMap && isStitchErrorCode(appErr.code) ? config.codeMap[appErr.code] : appErr.code;
126
128
  const info = {
@@ -130,8 +132,8 @@ function createErrorHook(config) {
130
132
  details: appErr.details,
131
133
  hint: appErr.hint
132
134
  };
133
- config.onError?.(error, info);
134
- return new Response(JSON.stringify(config.render(info)), {
135
+ config.onError?.(error, info, ctx);
136
+ return new Response(JSON.stringify(config.render(info, ctx)), {
135
137
  status: info.status,
136
138
  headers: { "content-type": "application/json" }
137
139
  });
@@ -425,7 +427,16 @@ function generateOpenApiDocument(config) {
425
427
  }
426
428
  }
427
429
  const responses = { ...errorResponses(method.scope) };
428
- if (method.outputSchema) {
430
+ if (method.rawResponse) {
431
+ responses["200"] = {
432
+ description: "Success",
433
+ content: {
434
+ [method.contentType ?? "application/octet-stream"]: {
435
+ schema: { type: "string", format: "binary" }
436
+ }
437
+ }
438
+ };
439
+ } else if (method.outputSchema) {
429
440
  responses["200"] = {
430
441
  description: "Success",
431
442
  content: { "application/json": { schema: safeJson(method.outputSchema, "output") } }
@@ -645,6 +656,7 @@ export {
645
656
  openApiRoute,
646
657
  notFound,
647
658
  normalizeError,
659
+ isWithinDir,
648
660
  isStitchErrorCode,
649
661
  implement,
650
662
  getClientInfo,
@@ -676,6 +688,7 @@ export {
676
688
  badRequest,
677
689
  appError,
678
690
  STITCH_ERROR_STATUS,
691
+ DEFAULT_CORS_EXPOSE_HEADERS,
679
692
  DEFAULT_CORS_ALLOW_HEADERS,
680
693
  AppError
681
694
  };
@@ -3,6 +3,12 @@ export interface CorsConfig {
3
3
  credentials?: boolean;
4
4
  methods?: string;
5
5
  headers?: string;
6
+ /**
7
+ * Response headers a cross-origin caller is allowed to *read*
8
+ * (`Access-Control-Expose-Headers`). Defaults to
9
+ * {@link DEFAULT_CORS_EXPOSE_HEADERS}; pass `[]` to emit none.
10
+ */
11
+ exposeHeaders?: string | string[];
6
12
  }
7
13
  /**
8
14
  * The default `Access-Control-Allow-Headers` — the request headers stitchkit's
@@ -17,6 +23,19 @@ export interface CorsConfig {
17
23
  * feature dies on the cross-origin preflight.
18
24
  */
19
25
  export declare const DEFAULT_CORS_ALLOW_HEADERS = "Content-Type, Authorization, X-Trace-Id, traceparent, tracestate";
26
+ /**
27
+ * The default `Access-Control-Expose-Headers`. Without it a cross-origin
28
+ * `fetch` can read only the CORS-safelisted response headers, so a browser
29
+ * downloading a file cannot recover its name (`Content-Disposition`), and a
30
+ * client cannot revalidate (`ETag`) or resume (`Content-Range`) — the very
31
+ * headers `serveFile` exists to emit.
32
+ *
33
+ * An explicit list rather than `*`, because the wildcard is ignored whenever
34
+ * the request carries credentials — exactly the authenticated download case.
35
+ * These are headers the server already chose to send; exposing them reveals
36
+ * nothing new. Opt out with `exposeHeaders: []`.
37
+ */
38
+ export declare const DEFAULT_CORS_EXPOSE_HEADERS = "Content-Disposition, Content-Length, Content-Range, Accept-Ranges, ETag, Last-Modified, X-Trace-Id";
20
39
  /**
21
40
  * Reject an unsafe CORS config at construction. `credentials: true` with a
22
41
  * wildcard origin would reflect *any* caller's `Origin` with
@@ -1 +1 @@
1
- {"version":3,"file":"cors.d.ts","sourceRoot":"","sources":["../../../src/server/middleware/cors.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,UAAU;IACzB,MAAM,CAAC,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC;IAC3B,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,0BAA0B,qEAC6B,CAAC;AAErE;;;;;GAKG;AACH,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,UAAU,GAAG,IAAI,CAOzD;AAoBD,wBAAgB,WAAW,CACzB,MAAM,EAAE,UAAU,EAClB,aAAa,CAAC,EAAE,MAAM,GAAG,IAAI,GAC5B,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAqBxB;AAED,wBAAgB,qBAAqB,CAAC,MAAM,EAAE,UAAU,EAAE,GAAG,EAAE,OAAO,GAAG,QAAQ,CAKhF"}
1
+ {"version":3,"file":"cors.d.ts","sourceRoot":"","sources":["../../../src/server/middleware/cors.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,UAAU;IACzB,MAAM,CAAC,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC;IAC3B,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB;;;;OAIG;IACH,aAAa,CAAC,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC;CACnC;AAED;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,0BAA0B,qEAC6B,CAAC;AAErE;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,2BAA2B,uGAC8D,CAAC;AAEvG;;;;;GAKG;AACH,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,UAAU,GAAG,IAAI,CAOzD;AAoBD,wBAAgB,WAAW,CACzB,MAAM,EAAE,UAAU,EAClB,aAAa,CAAC,EAAE,MAAM,GAAG,IAAI,GAC5B,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CA2BxB;AAED,wBAAgB,qBAAqB,CAAC,MAAM,EAAE,UAAU,EAAE,GAAG,EAAE,OAAO,GAAG,QAAQ,CAKhF"}
@@ -1 +1 @@
1
- {"version":3,"file":"openapi.d.ts","sourceRoot":"","sources":["../../src/server/openapi.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAKH,OAAO,KAAK,EAAE,SAAS,EAAE,QAAQ,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AAE/D,MAAM,WAAW,WAAW;IAC1B,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,MAAM,CAAC;IAChB,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,WAAW,aAAa;IAC5B,GAAG,EAAE,MAAM,CAAC;IACZ,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,WAAW,aAAa;IAC5B,IAAI,EAAE,WAAW,CAAC;IAClB,kDAAkD;IAClD,QAAQ,CAAC,EAAE,UAAU,EAAE,CAAC;IACxB,iFAAiF;IACjF,MAAM,CAAC,EAAE,KAAK,CAAC;QAAE,UAAU,CAAC,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,UAAU,EAAE,CAAA;KAAE,CAAC,CAAC;IAChE,oCAAoC;IACpC,OAAO,CAAC,EAAE,aAAa,EAAE,CAAC;IAC1B;;;;;;;;;;;OAWG;IACH,aAAa,CAAC,EAAE,CAAC,MAAM,EAAE,QAAQ,CAAC,SAAS,CAAC,KAAK,OAAO,CAAC;CAC1D;AAED,MAAM,WAAW,eAAe;IAC9B,OAAO,EAAE,OAAO,CAAC;IACjB,IAAI,EAAE,WAAW,CAAC;IAClB,OAAO,CAAC,EAAE,aAAa,EAAE,CAAC;IAC1B,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;CAChD;AAsED;;;;;;GAMG;AACH,wBAAgB,uBAAuB,CAAC,MAAM,EAAE,aAAa,GAAG,eAAe,CAuH9E;AAED;;;GAGG;AACH,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,eAAe,GAAG,QAAQ,CAM9E"}
1
+ {"version":3,"file":"openapi.d.ts","sourceRoot":"","sources":["../../src/server/openapi.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAKH,OAAO,KAAK,EAAE,SAAS,EAAE,QAAQ,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AAE/D,MAAM,WAAW,WAAW;IAC1B,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,MAAM,CAAC;IAChB,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,WAAW,aAAa;IAC5B,GAAG,EAAE,MAAM,CAAC;IACZ,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,WAAW,aAAa;IAC5B,IAAI,EAAE,WAAW,CAAC;IAClB,kDAAkD;IAClD,QAAQ,CAAC,EAAE,UAAU,EAAE,CAAC;IACxB,iFAAiF;IACjF,MAAM,CAAC,EAAE,KAAK,CAAC;QAAE,UAAU,CAAC,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,UAAU,EAAE,CAAA;KAAE,CAAC,CAAC;IAChE,oCAAoC;IACpC,OAAO,CAAC,EAAE,aAAa,EAAE,CAAC;IAC1B;;;;;;;;;;;OAWG;IACH,aAAa,CAAC,EAAE,CAAC,MAAM,EAAE,QAAQ,CAAC,SAAS,CAAC,KAAK,OAAO,CAAC;CAC1D;AAED,MAAM,WAAW,eAAe;IAC9B,OAAO,EAAE,OAAO,CAAC;IACjB,IAAI,EAAE,WAAW,CAAC;IAClB,OAAO,CAAC,EAAE,aAAa,EAAE,CAAC;IAC1B,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;CAChD;AAsED;;;;;;GAMG;AACH,wBAAgB,uBAAuB,CAAC,MAAM,EAAE,aAAa,GAAG,eAAe,CAmI9E;AAED;;;GAGG;AACH,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,eAAe,GAAG,QAAQ,CAM9E"}
@@ -27,6 +27,33 @@ export declare function matchRoute(routeMap: RouteMap, httpMethod: string, pathn
27
27
  export declare function allowedMethods(routeMap: RouteMap, pathname: string): string[];
28
28
  /** Startup guard — throws when two routes collapse to the same shape. */
29
29
  export declare function validateRoutes(routeMap: RouteMap): void;
30
+ /** One contract route that a raw route matches first, so the contract route is dead. */
31
+ export interface ShadowedRoute {
32
+ /** The contract route pattern, e.g. `GET /documents/:id/pdf`. */
33
+ pattern: string;
34
+ /** `serviceName.key` of the endpoint that will never run. */
35
+ endpoint: string;
36
+ /** The raw route that wins, e.g. `GET /documents/:id/pdf` or `ALL /files/*`. */
37
+ rawRoute: string;
38
+ /** The shadowed endpoint's scope — the auth gate the raw route bypasses. */
39
+ scope?: string;
40
+ }
41
+ /**
42
+ * Startup diagnostic — raw routes are matched **before** contract routes, so a
43
+ * raw route covering a contract path silently wins and the endpoint never runs.
44
+ *
45
+ * This matters most for the migration raw-response endpoints exist to enable
46
+ * (→ ADR 0038): move a download out of `rawRoutes` into the contract to gain the
47
+ * auth gate, forget to delete the old raw route, and the bytes keep being served
48
+ * ungated — with no error anywhere. Reported, not thrown: an overlapping
49
+ * wildcard (a SPA fallback) can be deliberate, and refusing to boot a working
50
+ * app would be the worse failure.
51
+ *
52
+ * Detection runs the **real** `matchRawRoute` against a concrete probe path
53
+ * built from each contract route, so it can never disagree with what the
54
+ * dispatcher actually does.
55
+ */
56
+ export declare function findShadowedRoutes(routeMap: RouteMap, rawRoutes: RawRoute[] | undefined): ShadowedRoute[];
30
57
  export declare function matchRawRoute(rawRoutes: RawRoute[], httpMethod: string, pathname: string): {
31
58
  route: RawRoute;
32
59
  params: Record<string, string>;
@@ -1 +1 @@
1
- {"version":3,"file":"router.d.ts","sourceRoot":"","sources":["../../src/server/router.ts"],"names":[],"mappings":"AAQA,OAAO,KAAK,EAAE,cAAc,EAAE,SAAS,EAAE,QAAQ,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AAE/E,4EAA4E;AAC5E,MAAM,WAAW,eAAe;IAC9B,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,UAAU,CAAC;IACpB,KAAK,CAAC,EAAE,cAAc,CAAC;CACxB;AAED,UAAU,UAAU;IAClB,MAAM,EAAE,SAAS,CAAC;IAClB,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,MAAM,EAAE,CAAC;IACnB,UAAU,CAAC,EAAE,cAAc,CAAC;CAC7B;AAED,+EAA+E;AAC/E,MAAM,MAAM,QAAQ,GAAG,GAAG,CAAC,MAAM,EAAE,UAAU,EAAE,CAAC,CAAC;AAEjD,MAAM,WAAW,UAAU;IACzB,MAAM,EAAE,SAAS,CAAC;IAClB,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACnC,UAAU,CAAC,EAAE,cAAc,CAAC;CAC7B;AAqCD,wBAAgB,aAAa,CAAC,MAAM,EAAE,eAAe,EAAE,GAAG,QAAQ,CAmCjE;AAED,wBAAgB,UAAU,CACxB,QAAQ,EAAE,QAAQ,EAClB,UAAU,EAAE,MAAM,EAClB,QAAQ,EAAE,MAAM,GACf,UAAU,GAAG,IAAI,CAkBnB;AAED;;;GAGG;AACH,wBAAgB,cAAc,CAAC,QAAQ,EAAE,QAAQ,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,EAAE,CAc7E;AAED,yEAAyE;AACzE,wBAAgB,cAAc,CAAC,QAAQ,EAAE,QAAQ,GAAG,IAAI,CAiBvD;AAID,wBAAgB,aAAa,CAC3B,SAAS,EAAE,QAAQ,EAAE,EACrB,UAAU,EAAE,MAAM,EAClB,QAAQ,EAAE,MAAM,GACf;IAAE,KAAK,EAAE,QAAQ,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;CAAE,GAAG,IAAI,CAkC5D;AAED;;;;;;GAMG;AACH,wBAAgB,WAAW,CAAC,MAAM,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,QAAQ,CAkCjE"}
1
+ {"version":3,"file":"router.d.ts","sourceRoot":"","sources":["../../src/server/router.ts"],"names":[],"mappings":"AAQA,OAAO,KAAK,EAAE,cAAc,EAAE,SAAS,EAAE,QAAQ,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AAE/E,4EAA4E;AAC5E,MAAM,WAAW,eAAe;IAC9B,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,UAAU,CAAC;IACpB,KAAK,CAAC,EAAE,cAAc,CAAC;CACxB;AAED,UAAU,UAAU;IAClB,MAAM,EAAE,SAAS,CAAC;IAClB,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,MAAM,EAAE,CAAC;IACnB,UAAU,CAAC,EAAE,cAAc,CAAC;CAC7B;AAED,+EAA+E;AAC/E,MAAM,MAAM,QAAQ,GAAG,GAAG,CAAC,MAAM,EAAE,UAAU,EAAE,CAAC,CAAC;AAEjD,MAAM,WAAW,UAAU;IACzB,MAAM,EAAE,SAAS,CAAC;IAClB,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACnC,UAAU,CAAC,EAAE,cAAc,CAAC;CAC7B;AAqCD,wBAAgB,aAAa,CAAC,MAAM,EAAE,eAAe,EAAE,GAAG,QAAQ,CAmCjE;AAED,wBAAgB,UAAU,CACxB,QAAQ,EAAE,QAAQ,EAClB,UAAU,EAAE,MAAM,EAClB,QAAQ,EAAE,MAAM,GACf,UAAU,GAAG,IAAI,CAkBnB;AAED;;;GAGG;AACH,wBAAgB,cAAc,CAAC,QAAQ,EAAE,QAAQ,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,EAAE,CAc7E;AAED,yEAAyE;AACzE,wBAAgB,cAAc,CAAC,QAAQ,EAAE,QAAQ,GAAG,IAAI,CAiBvD;AAED,wFAAwF;AACxF,MAAM,WAAW,aAAa;IAC5B,iEAAiE;IACjE,OAAO,EAAE,MAAM,CAAC;IAChB,6DAA6D;IAC7D,QAAQ,EAAE,MAAM,CAAC;IACjB,gFAAgF;IAChF,QAAQ,EAAE,MAAM,CAAC;IACjB,4EAA4E;IAC5E,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,kBAAkB,CAChC,QAAQ,EAAE,QAAQ,EAClB,SAAS,EAAE,QAAQ,EAAE,GAAG,SAAS,GAChC,aAAa,EAAE,CAmBjB;AAID,wBAAgB,aAAa,CAC3B,SAAS,EAAE,QAAQ,EAAE,EACrB,UAAU,EAAE,MAAM,EAClB,QAAQ,EAAE,MAAM,GACf;IAAE,KAAK,EAAE,QAAQ,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;CAAE,GAAG,IAAI,CAkC5D;AAED;;;;;;GAMG;AACH,wBAAgB,WAAW,CAAC,MAAM,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,QAAQ,CAkCjE"}
@@ -5,11 +5,33 @@ type Prop<T, K extends string> = K extends keyof T ? T[K] : undefined;
5
5
  type InferParams<E> = Prop<E, 'params'> extends ZodType<infer P> ? P : undefined;
6
6
  type InferInput<E> = Prop<E, 'input'> extends ZodType<infer I> ? I : undefined;
7
7
  type InferOutput<E> = Prop<E, 'output'> extends ZodType<infer O> ? O : never;
8
+ /**
9
+ * What an endpoint's handler must return: the `Response` itself for a `raw`
10
+ * endpoint, the output type when there is an output schema, nothing otherwise.
11
+ * Enforced in both directions — a raw handler returning data and a normal
12
+ * handler returning a `Response` are both compile errors (the latter used to
13
+ * be accepted and silently serialized to `{}`).
14
+ */
15
+ type HandlerReturn<E> = E extends {
16
+ rawResponse: true;
17
+ } ? Response | Promise<Response> : Prop<E, 'output'> extends ZodType ? Promise<InferOutput<E>> | InferOutput<E> : void | Promise<void>;
18
+ /**
19
+ * `ctx.req` is optional in general — a tool call has no `Request`. A raw
20
+ * endpoint is HTTP-only by declaration, and its handler needs the request
21
+ * (`serveFile(ctx.req, …)` reads `Range` / `If-None-Match`), so it is narrowed
22
+ * to a guaranteed `Request` there rather than making every raw handler write
23
+ * a non-null assertion. → ADR 0038.
24
+ */
25
+ type RequiredRequest<E> = E extends {
26
+ rawResponse: true;
27
+ } ? {
28
+ req: Request;
29
+ } : unknown;
8
30
  export type Handlers<C extends Record<string, EndpointDef>, TCtx extends RuntimeContext = HandlerContext> = {
9
31
  [K in keyof C]: (ctx: TCtx & {
10
32
  params: InferParams<C[K]>;
11
33
  input: InferInput<C[K]>;
12
- }) => Prop<C[K], 'output'> extends ZodType ? Promise<InferOutput<C[K]>> | InferOutput<C[K]> : void | Promise<void>;
34
+ } & RequiredRequest<C[K]>) => HandlerReturn<C[K]>;
13
35
  };
14
36
  export interface MethodDef<TParams = unknown, TInput = unknown, TOutput = unknown> {
15
37
  method: HttpMethod;
@@ -54,6 +76,14 @@ export interface MethodDef<TParams = unknown, TInput = unknown, TOutput = unknow
54
76
  * the consumer narrows the type. Never serialized to OpenAPI. → ADR 0021.
55
77
  */
56
78
  meta?: Record<string, unknown>;
79
+ /**
80
+ * The handler returns the `Response` itself — from `EndpointDef.rawResponse`. Routed
81
+ * and gated like any endpoint, but never serialized, never validated against
82
+ * an output schema and never mounted as a tool. → ADR 0038.
83
+ */
84
+ rawResponse?: true;
85
+ /** Documented response media type of a raw-response endpoint — OpenAPI only. */
86
+ contentType?: string;
57
87
  handler: (ctx: RuntimeContext) => Promise<TOutput> | TOutput;
58
88
  }
59
89
  export interface ServiceDef {
@@ -152,6 +182,19 @@ export interface HandlerConfig {
152
182
  cors?: CorsConfig;
153
183
  hooks?: LifecycleHooks;
154
184
  logging?: boolean | StitchLogger;
185
+ /**
186
+ * Report handler-output keys the contract schema removed, as dot-paths, through
187
+ * the configured logger. **Off by default** — the strip itself is correct (the
188
+ * contract is the published shape of the response), it is only *invisible*, and
189
+ * a permanent key-diff on every response is the wrong price for that.
190
+ *
191
+ * Turn it on while migrating a live API: your handlers may be returning more
192
+ * than the contract declares, and nothing else will tell you — types cannot
193
+ * (structural typing does not reject excess properties) and the client just
194
+ * receives fewer fields. Read the list, fix or widen the contracts, turn it off.
195
+ * → ADR 0037.
196
+ */
197
+ warnOnOutputStrip?: boolean;
155
198
  traceId?: (req: Request) => string;
156
199
  /**
157
200
  * Trust the `x-forwarded-for` / `x-real-ip` headers for the client IP.