stitchkit 0.21.0 → 0.23.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/cli.js CHANGED
@@ -4,9 +4,9 @@ import {
4
4
  emitResult,
5
5
  parseCliArgs,
6
6
  pollUntilDone
7
- } from "./index-xmncnw8z.js";
7
+ } from "./index-x8m8e7dc.js";
8
8
  import"./index-0ed3bx43.js";
9
- import"./index-ng2v2ts4.js";
9
+ import"./index-4gawbm74.js";
10
10
  import"./index-c7nyw0yt.js";
11
11
  export {
12
12
  pollUntilDone,
@@ -84,17 +84,25 @@ function base64UrlToBytes(segment) {
84
84
  }
85
85
  // src/internal/errors.ts
86
86
  import { z as z2 } from "zod";
87
+ function issuePath(path) {
88
+ return path.length > 0 ? path.map(String).join(".") : "(root)";
89
+ }
87
90
  function formatZodError(error) {
88
91
  const issues = error.issues.slice(0, 5);
89
- const lines = issues.map((issue) => {
90
- const path = issue.path.length > 0 ? issue.path.join(".") : "(root)";
91
- return `${path}: ${issue.message}`;
92
- });
92
+ const lines = issues.map((issue) => `${issuePath(issue.path)}: ${issue.message}`);
93
93
  const suffix = error.issues.length > 5 ? `
94
94
  ...and ${error.issues.length - 5} more issues` : "";
95
95
  return lines.join(`
96
96
  `) + suffix;
97
97
  }
98
+ function zodIssues(error) {
99
+ return error.issues.map((issue) => ({
100
+ path: issuePath(issue.path),
101
+ code: issue.code,
102
+ message: issue.message
103
+ }));
104
+ }
105
+ var MAX_DETAIL_ISSUES = 20;
98
106
  function errorCode(err) {
99
107
  if (AppError.is(err))
100
108
  return err.code;
@@ -106,7 +114,9 @@ function normalizeError(err) {
106
114
  if (AppError.is(err))
107
115
  return err;
108
116
  if (err instanceof z2.ZodError) {
109
- return new AppError("VALIDATION_ERROR", formatZodError(err), 400);
117
+ return new AppError("VALIDATION_ERROR", formatZodError(err), 400, {
118
+ issues: zodIssues(err).slice(0, MAX_DETAIL_ISSUES)
119
+ });
110
120
  }
111
121
  console.error("[stitchkit] unhandled error:", err);
112
122
  return new AppError("INTERNAL_SERVER_ERROR", "Internal server error", 500);
@@ -128,4 +138,4 @@ function isWithinDir(root, target) {
128
138
  return target === root || target === base || target.startsWith(base + sep);
129
139
  }
130
140
 
131
- export { AppError, notFound, badRequest, unauthorized, forbidden, conflict, rateLimited, STITCH_ERROR_STATUS, isStitchErrorCode, appError, bytesToBase64Url, base64UrlToBytes, formatZodError, errorCode, normalizeError, validateHandlerOutput, isWithinDir };
141
+ export { AppError, notFound, badRequest, unauthorized, forbidden, conflict, rateLimited, STITCH_ERROR_STATUS, isStitchErrorCode, appError, bytesToBase64Url, base64UrlToBytes, formatZodError, zodIssues, errorCode, normalizeError, validateHandlerOutput, isWithinDir };
@@ -3,7 +3,7 @@ import {
3
3
  bytesToBase64Url,
4
4
  forbidden,
5
5
  unauthorized
6
- } from "./index-ng2v2ts4.js";
6
+ } from "./index-4gawbm74.js";
7
7
  import {
8
8
  isRecord,
9
9
  isUnsafeKey,
@@ -10,7 +10,7 @@ import {
10
10
  isWithinDir,
11
11
  normalizeError,
12
12
  validateHandlerOutput
13
- } from "./index-ng2v2ts4.js";
13
+ } from "./index-4gawbm74.js";
14
14
  import {
15
15
  extractIp,
16
16
  getClientInfo,
@@ -7,7 +7,7 @@ import {
7
7
  isWithinDir,
8
8
  normalizeError,
9
9
  validateHandlerOutput
10
- } from "./index-ng2v2ts4.js";
10
+ } from "./index-4gawbm74.js";
11
11
  import {
12
12
  isRecord,
13
13
  isUnsafeKey,
@@ -1,6 +1,22 @@
1
1
  import { type ZodType, z } from 'zod';
2
2
  import { AppError } from '../contract';
3
3
  export declare function formatZodError(error: z.ZodError): string;
4
+ /** One field-level validation issue — the structured sibling of `formatZodError`. */
5
+ export interface ZodIssueSummary {
6
+ /** Dotted path to the offending field (`(root)` for a top-level issue). */
7
+ path: string;
8
+ /** Zod issue code (e.g. `invalid_type`, `too_small`). */
9
+ code: string;
10
+ /** Human-readable message for this field. */
11
+ message: string;
12
+ }
13
+ /**
14
+ * Project a `ZodError` into structured, wire-safe field issues — path / code /
15
+ * message only, nothing server-internal. For a machine client that matches on
16
+ * fields rather than parsing the text `message`. Returns every issue; a caller
17
+ * that bounds response size slices it (see `normalizeError`).
18
+ */
19
+ export declare function zodIssues(error: z.ZodError): ZodIssueSummary[];
4
20
  /**
5
21
  * The stable error code for a thrown value — `AppError.code`, `VALIDATION_ERROR`
6
22
  * for a `ZodError`, else `undefined`. Side-effect-free (unlike `normalizeError`,
@@ -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;AAEvC,wBAAgB,cAAc,CAAC,KAAK,EAAE,CAAC,CAAC,QAAQ,GAAG,MAAM,CASxD;AAED;;;;;GAKG;AACH,wBAAgB,SAAS,CAAC,GAAG,EAAE,OAAO,GAAG,MAAM,GAAG,SAAS,CAI1D;AAED,wBAAgB,cAAc,CAAC,GAAG,EAAE,OAAO,GAAG,QAAQ,CAYrD;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;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"}
package/dist/node.js CHANGED
@@ -3,7 +3,7 @@ import {
3
3
  createImplement,
4
4
  createSocketIOServer,
5
5
  implement
6
- } from "./index-f5e4efj1.js";
6
+ } from "./index-g8kyab85.js";
7
7
  import"./index-tje0q6gp.js";
8
8
  import {
9
9
  AppError,
@@ -14,7 +14,7 @@ import {
14
14
  notFound,
15
15
  rateLimited,
16
16
  unauthorized
17
- } from "./index-ng2v2ts4.js";
17
+ } from "./index-4gawbm74.js";
18
18
  import"./index-dzx781tm.js";
19
19
  import"./index-khwedj16.js";
20
20
  import"./index-c7nyw0yt.js";
@@ -1,5 +1,5 @@
1
1
  export { AppError, appError, badRequest, conflict, forbidden, isStitchErrorCode, notFound, rateLimited, STITCH_ERROR_STATUS, type StitchErrorCode, unauthorized, } from '../contract';
2
- export { errorCode, formatZodError, normalizeError } from '../internal/errors';
2
+ export { errorCode, formatZodError, normalizeError, type ZodIssueSummary, zodIssues, } from '../internal/errors';
3
3
  export { cacheHeaders, createCache } from './cache';
4
4
  export { createHandler, createServer } from './create';
5
5
  export { createErrorHook, type ErrorHookConfig, type ResolvedError, } from './error-hook';
@@ -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,EAAE,SAAS,EAAE,cAAc,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAC;AAC/E,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;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"}
@@ -10,7 +10,7 @@ import {
10
10
  socketIoLane,
11
11
  staticRoute,
12
12
  webSocketLane
13
- } from "../index-f5e4efj1.js";
13
+ } from "../index-g8kyab85.js";
14
14
  import {
15
15
  createAuthHook,
16
16
  createBearerResolver,
@@ -23,7 +23,7 @@ import {
23
23
  signJwt,
24
24
  verifyJwt,
25
25
  verifyPkce
26
- } from "../index-ynnh9x6h.js";
26
+ } from "../index-a9n8m4ec.js";
27
27
  import {
28
28
  DEFAULT_CORS_ALLOW_HEADERS,
29
29
  corsHeaders,
@@ -46,8 +46,9 @@ import {
46
46
  normalizeError,
47
47
  notFound,
48
48
  rateLimited,
49
- unauthorized
50
- } from "../index-ng2v2ts4.js";
49
+ unauthorized,
50
+ zodIssues
51
+ } from "../index-4gawbm74.js";
51
52
  import {
52
53
  extractIp,
53
54
  generateTraceId,
@@ -397,6 +398,8 @@ function generateOpenApiDocument(config) {
397
398
  for (const [key, method] of Object.entries(service.methods)) {
398
399
  if (method.expose && !method.expose.includes("HTTP"))
399
400
  continue;
401
+ if (config.includeMethod && !config.includeMethod(method))
402
+ continue;
400
403
  const servicePath = joinPath("/", service.prefix, method.path === "/" ? "" : method.path);
401
404
  const fullPath = toOpenApiPath(pathPrefix ? joinPath(pathPrefix, servicePath) : servicePath);
402
405
  const parameters = [];
@@ -618,6 +621,7 @@ async function* parseSSE(response, options) {
618
621
  }
619
622
  }
620
623
  export {
624
+ zodIssues,
621
625
  webSocketLane,
622
626
  weakETag,
623
627
  verifyPkce,
@@ -10,7 +10,7 @@
10
10
  * emits — not a divergent code path. Schemas are inlined (valid OpenAPI);
11
11
  * `$ref` de-duplication can come later if a spec grows unwieldy.
12
12
  */
13
- import type { RawRoute, ServiceDef } from './types';
13
+ import type { MethodDef, RawRoute, ServiceDef } from './types';
14
14
  export interface OpenApiInfo {
15
15
  title: string;
16
16
  version: string;
@@ -31,6 +31,19 @@ export interface OpenApiConfig {
31
31
  }>;
32
32
  /** `servers` block for the spec. */
33
33
  servers?: OpenApiServer[];
34
+ /**
35
+ * Emit only the methods this predicate keeps — a curated public spec instead
36
+ * of the whole HTTP surface. The predicate decides the policy (the core stays
37
+ * generic): filter on `method.scope`, `method.meta` (the recommended
38
+ * declarative allowlist — mark endpoints `meta: { public: true }` and keep
39
+ * `(m) => m.meta?.public === true`), `method.key`, anything on the method.
40
+ * Omit to include every HTTP method (the default).
41
+ *
42
+ * This controls what the spec **advertises**, not access — a hidden endpoint
43
+ * is still callable; the auth `scope` gate is the actual guard. Build a
44
+ * separate filtered document for a public route (see the guide).
45
+ */
46
+ includeMethod?: (method: Readonly<MethodDef>) => boolean;
34
47
  }
35
48
  export interface OpenApiDocument {
36
49
  openapi: '3.1.0';
@@ -41,7 +54,9 @@ export interface OpenApiDocument {
41
54
  /**
42
55
  * Generate an OpenAPI 3.1 document from contract services. Only methods exposed
43
56
  * on HTTP are included — a method whose `expose` omits `'HTTP'` (an MCP/agent
44
- * only tool) is skipped, matching the router's own route-building rule.
57
+ * only tool) is skipped, matching the router's own route-building rule. Pass
58
+ * `includeMethod` to emit a curated subset (a public spec) instead of the whole
59
+ * surface.
45
60
  */
46
61
  export declare function generateOpenApiDocument(config: OpenApiConfig): OpenApiDocument;
47
62
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"openapi.d.ts","sourceRoot":"","sources":["../../src/server/openapi.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAKH,OAAO,KAAK,EAAE,QAAQ,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AAEpD,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;CAC3B;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;;;;GAIG;AACH,wBAAgB,uBAAuB,CAAC,MAAM,EAAE,aAAa,GAAG,eAAe,CAgH9E;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,CAuH9E;AAED;;;GAGG;AACH,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,eAAe,GAAG,QAAQ,CAM9E"}
package/dist/tools.js CHANGED
@@ -2,7 +2,7 @@ import {
2
2
  inputIsQuery,
3
3
  signJwt,
4
4
  verifyPkce
5
- } from "./index-ynnh9x6h.js";
5
+ } from "./index-a9n8m4ec.js";
6
6
  import {
7
7
  DEFAULT_CORS_ALLOW_HEADERS
8
8
  } from "./index-tje0q6gp.js";
@@ -18,14 +18,14 @@ import {
18
18
  pollUntil,
19
19
  readCapped,
20
20
  toolResultFromError
21
- } from "./index-xmncnw8z.js";
21
+ } from "./index-x8m8e7dc.js";
22
22
  import {
23
23
  toJsonSchema
24
24
  } from "./index-0ed3bx43.js";
25
25
  import {
26
26
  AppError,
27
27
  isWithinDir
28
- } from "./index-ng2v2ts4.js";
28
+ } from "./index-4gawbm74.js";
29
29
  import"./index-khwedj16.js";
30
30
  import {
31
31
  isRecord,
package/llms-full.txt CHANGED
@@ -355,7 +355,10 @@ beforeHandle: (ctx, endpoint) => {
355
355
  }
356
356
  ```
357
357
 
358
- `meta` is **app-private** — it is never serialized into the OpenAPI document.
358
+ `meta` is **app-private** — it is never serialized into the OpenAPI document. It
359
+ can still *drive* generation: `generateOpenApiDocument`'s `includeMethod` reads
360
+ it to curate a public spec (e.g. `meta: { public: true }`), without ever emitting
361
+ `meta` itself — see [Curating the spec](./server.md#curating-the-spec--includemethod).
359
362
 
360
363
  > **Declare a meta type as a `type`, an inline literal, or with `satisfies` — not
361
364
  > an `interface`.** A TS `interface` has no implicit index signature (it can be
@@ -856,6 +859,60 @@ bus.emit('user.created', { id: '1' })
856
859
  A typed in-process pub/sub — decouple a handler from the side effects of its
857
860
  write without reaching for an external queue.
858
861
 
862
+ ## OpenAPI
863
+
864
+ `generateOpenApiDocument` builds an OpenAPI 3.1 document straight from the
865
+ contracts — the contract *is* the spec, no decorators or hand-maintained
866
+ annotations (→ ADR 0018). `openApiRoute` serves it as a raw route:
867
+
868
+ ```ts
869
+ import { generateOpenApiDocument, openApiRoute } from 'stitchkit/server'
870
+
871
+ const doc = generateOpenApiDocument({
872
+ info: { title: 'My API', version: '1.0.0' },
873
+ services: [users, orders],
874
+ })
875
+ createServer({ services: [users, orders], rawRoutes: [openApiRoute('/openapi.json', doc)] })
876
+ ```
877
+
878
+ Only HTTP-exposed methods appear (an MCP/agent-only tool is skipped).
879
+
880
+ ### Curating the spec — `includeMethod`
881
+
882
+ To publish a **subset** — a public spec that advertises only some methods
883
+ without revealing the rest — pass `includeMethod`. It keeps the core generic:
884
+ *you* decide the policy, filtering on anything the method carries. The
885
+ recommended declarative allowlist marks endpoints with the existing `meta`
886
+ passthrough and keeps those:
887
+
888
+ ```ts
889
+ // contract — declarative, one source of truth
890
+ getBalance: { method: 'GET', path: '/balance', desc: '…', scope: 'account',
891
+ meta: { public: true }, output: BalanceSchema }
892
+
893
+ // generation — the app's policy
894
+ const publicDoc = generateOpenApiDocument({
895
+ info: { title: 'Public API', version: '1.0.0' },
896
+ services: [account],
897
+ includeMethod: (m) => m.meta?.public === true,
898
+ })
899
+ ```
900
+
901
+ An excluded method's whole entry — path *and* every schema inlined within it —
902
+ is simply never emitted, so nothing about a hidden endpoint leaks.
903
+
904
+ > **The filter advertises; it does not authorize.** Hiding a method from the
905
+ > spec does **not** protect it — it is still callable, and the auth `scope` gate
906
+ > is the only thing guarding it. And because `openApiRoute` closes over the
907
+ > document you hand it, the filter only matters if you feed it a filtered one:
908
+ > serve **two** documents — a full internal spec and a filtered public spec on
909
+ > separate routes — never one unfiltered `openApiRoute` on a public path.
910
+
911
+ ```ts
912
+ const internal = openApiRoute('/internal/openapi.json', fullDoc) // behind auth
913
+ const publicSpec = openApiRoute('/openapi.json', publicDoc) // curated
914
+ ```
915
+
859
916
 
860
917
  ==============================================================================
861
918
  # Guide: Typed client (docs/guide/client.md)
@@ -2293,7 +2350,27 @@ transport (HTTP, MCP, agent). → ADR 0032.
2293
2350
  `details` and `hint` are included when present. On HTTP this is the response
2294
2351
  body with the matching status; for an MCP or agent call the same `code` and
2295
2352
  `details` come back as a tool error. A schema-validation failure on the request
2296
- is turned into a `400 VALIDATION_ERROR` automatically.
2353
+ is turned into a `400 VALIDATION_ERROR` automatically — and it carries the
2354
+ offending fields as structured `details.issues`, so a machine client matches on
2355
+ them instead of parsing the text `message`:
2356
+
2357
+ ```json
2358
+ {
2359
+ "error": {
2360
+ "code": "VALIDATION_ERROR",
2361
+ "message": "name: Invalid input\nage: Invalid input",
2362
+ "details": {
2363
+ "issues": [
2364
+ { "path": "name", "code": "invalid_type", "message": "Invalid input" },
2365
+ { "path": "age", "code": "invalid_type", "message": "Invalid input" }
2366
+ ]
2367
+ }
2368
+ }
2369
+ }
2370
+ ```
2371
+
2372
+ Use the exported `zodIssues(error)` to build the same structured list from a
2373
+ `ZodError` in your own hook.
2297
2374
 
2298
2375
  ### On the client
2299
2376
 
@@ -2378,10 +2455,14 @@ Codes you threw yourself (not stitchkit's) pass through `codeMap` unchanged; the
2378
2455
  `satisfies Record<StitchErrorCode, …>` keeps the map exhaustive across upgrades.
2379
2456
 
2380
2457
  Invalid input (a `ZodError`) is classified as `VALIDATION_ERROR` 400 before it
2381
- reaches `render` — a client fault is an honest 400, not a 500. If you write a
2382
- **bespoke** `onError` instead of using `createErrorHook`, run the thrown value
2383
- through the exported `normalizeError` to get the same classification (a raw
2384
- `ZodError` reaches your hook untouched, so the framework can inspect it):
2458
+ reaches `render` — a client fault is an honest 400, not a 500 — and the
2459
+ offending fields arrive as structured `info.details.issues`, so your `render`
2460
+ can surface them to a machine client without parsing the message.
2461
+
2462
+ If you write a **bespoke** `onError` instead of using `createErrorHook`, run the
2463
+ thrown value through the exported `normalizeError` to get the same classification
2464
+ (a raw `ZodError` reaches your hook untouched, so the framework can inspect it —
2465
+ call `zodIssues(err)` yourself if you want the structured fields):
2385
2466
 
2386
2467
  ```ts
2387
2468
  import { normalizeError } from 'stitchkit/server'
@@ -3166,9 +3247,11 @@ Also re-exports the error helpers from `stitchkit/contract`.
3166
3247
  | `ByteRange` | _type_ | an inclusive `{ start, end }` byte range |
3167
3248
  | `respondJson` | function | a raw route's JSON response (`204` for null/undefined) |
3168
3249
  | `errorResponse` | function | any thrown value → the framework error envelope + `x-request-id` |
3169
- | `normalizeError` | function | any thrown value → an `AppError` (`ZodError` → `VALIDATION_ERROR` 400, else generic 500) — the framework's canonical classification, for a bespoke `onError` |
3250
+ | `normalizeError` | function | any thrown value → an `AppError` (`ZodError` → `VALIDATION_ERROR` 400 with structured `details.issues`, else generic 500) — the framework's canonical classification, for a bespoke `onError` |
3170
3251
  | `errorCode` | function | the stable error code for a thrown value (side-effect-free — for log attribution) |
3171
3252
  | `formatZodError` | function | a `ZodError` → a readable, field-summarised string |
3253
+ | `zodIssues` | function | a `ZodError` → structured `{ path, code, message }[]` — the machine-readable sibling of `formatZodError` |
3254
+ | `ZodIssueSummary` | _type_ | one structured validation issue (`{ path, code, message }`) |
3172
3255
  | `parseBody` | function | parse + Zod-validate a JSON body → `data` or `null` (no throw) |
3173
3256
  | `HandlerConfig` | _type_ | config for `createHandler` (runtime-agnostic) |
3174
3257
  | `BunServerConfig` | _type_ | config for `createServer` (Bun) |
@@ -3260,7 +3343,7 @@ Also re-exports the error helpers from `stitchkit/contract`.
3260
3343
  |--------|------|---------|
3261
3344
  | `generateOpenApiDocument` | function | an OpenAPI 3.1 document from contract services — [ADR 0018](../decisions/0018-openapi-generation.md) |
3262
3345
  | `openApiRoute` | function | a `RawRoute` that serves the document as JSON |
3263
- | `OpenApiConfig` | _type_ | config for `generateOpenApiDocument` |
3346
+ | `OpenApiConfig` | _type_ | config for `generateOpenApiDocument` (incl. `includeMethod` — curate a public subset) — [guide](../guide/server.md#curating-the-spec--includemethod) |
3264
3347
  | `OpenApiDocument` | _type_ | the generated document |
3265
3348
  | `OpenApiInfo` | _type_ | the spec `info` block |
3266
3349
  | `OpenApiServer` | _type_ | a spec `servers` entry |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "stitchkit",
3
- "version": "0.21.0",
3
+ "version": "0.23.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",