stitchkit 0.20.0 → 0.22.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";
@@ -9,6 +9,10 @@
9
9
  * of leaking stitchkit's code to the wire. The envelope stays app-owned via
10
10
  * `render`, so the core prescribes no domain shape (ADR 0002).
11
11
  *
12
+ * The thrown value is classified through the framework's own `normalizeError`
13
+ * first, so a `ZodError` (invalid input) is an honest `VALIDATION_ERROR` 400 —
14
+ * not a 500 — exactly as the framework default would render it.
15
+ *
12
16
  * ```ts
13
17
  * const onError = createErrorHook({
14
18
  * codeMap: {
@@ -31,7 +35,7 @@ export interface ResolvedError {
31
35
  code: string;
32
36
  /** HTTP status. */
33
37
  status: number;
34
- /** Safe message — the `AppError` message, or a generic string for a raw throw. */
38
+ /** Safe message — the `AppError` / `ZodError` summary, or a generic string for a raw throw. */
35
39
  message: string;
36
40
  /** Structured details, when the thrown `AppError` carried them. */
37
41
  details?: Record<string, unknown>;
@@ -1 +1 @@
1
- {"version":3,"file":"error-hook.d.ts","sourceRoot":"","sources":["../../src/server/error-hook.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,OAAO,EAA+B,KAAK,eAAe,EAAE,MAAM,aAAa,CAAC;AAChF,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,kFAAkF;IAClF,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,CAoBxC"}
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,4 +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, type ZodIssueSummary, zodIssues, } from '../internal/errors';
2
3
  export { cacheHeaders, createCache } from './cache';
3
4
  export { createHandler, createServer } from './create';
4
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;AACrB,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,
@@ -39,13 +39,16 @@ import {
39
39
  appError,
40
40
  badRequest,
41
41
  conflict,
42
+ errorCode,
42
43
  forbidden,
44
+ formatZodError,
43
45
  isStitchErrorCode,
44
46
  normalizeError,
45
47
  notFound,
46
48
  rateLimited,
47
- unauthorized
48
- } from "../index-ng2v2ts4.js";
49
+ unauthorized,
50
+ zodIssues
51
+ } from "../index-4gawbm74.js";
49
52
  import {
50
53
  extractIp,
51
54
  generateTraceId,
@@ -118,15 +121,14 @@ function cacheHeaders(maxAge, scope = "public") {
118
121
  // src/server/error-hook.ts
119
122
  function createErrorHook(config) {
120
123
  return (_ctx, error) => {
121
- const isApp = AppError.is(error);
122
- const rawCode = isApp ? error.code : "INTERNAL_SERVER_ERROR";
123
- const code = config.codeMap && isStitchErrorCode(rawCode) ? config.codeMap[rawCode] : rawCode;
124
+ const appErr = normalizeError(error);
125
+ const code = config.codeMap && isStitchErrorCode(appErr.code) ? config.codeMap[appErr.code] : appErr.code;
124
126
  const info = {
125
127
  code,
126
- status: isApp ? error.status : 500,
127
- message: isApp ? error.message : "Internal server error",
128
- details: isApp ? error.details : undefined,
129
- hint: isApp ? error.hint : undefined
128
+ status: appErr.status,
129
+ message: appErr.message,
130
+ details: appErr.details,
131
+ hint: appErr.hint
130
132
  };
131
133
  config.onError?.(error, info);
132
134
  return new Response(JSON.stringify(config.render(info)), {
@@ -617,6 +619,7 @@ async function* parseSSE(response, options) {
617
619
  }
618
620
  }
619
621
  export {
622
+ zodIssues,
620
623
  webSocketLane,
621
624
  weakETag,
622
625
  verifyPkce,
@@ -639,15 +642,18 @@ export {
639
642
  parseBody,
640
643
  openApiRoute,
641
644
  notFound,
645
+ normalizeError,
642
646
  isStitchErrorCode,
643
647
  implement,
644
648
  getClientInfo,
645
649
  generateTraceId,
646
650
  generateOpenApiDocument,
651
+ formatZodError,
647
652
  forbidden,
648
653
  extractToken,
649
654
  extractIp,
650
655
  errorResponse2 as errorResponse,
656
+ errorCode,
651
657
  deriveCodeChallenge,
652
658
  defineCookie,
653
659
  createSocketIOServer,
@@ -13,8 +13,9 @@ export interface MultipartResult {
13
13
  *
14
14
  * A multipart text field is always a **string** (per the spec) and is handed to
15
15
  * the schema as one — the schema decides its type, exactly as with query params:
16
- * `z.coerce.number()` for a number, `z.coerce.boolean()` for a boolean, and
17
- * `z.preprocess((v) => JSON.parse(String(v)), Schema)` to opt a field into JSON.
16
+ * `z.coerce.number()` for a number, `z.stringbool()` for a boolean (NOT
17
+ * `z.coerce.boolean()`, which is `Boolean(str)` — `'false'` would become `true`),
18
+ * and `z.preprocess((v) => JSON.parse(String(v)), Schema)` to opt a field into JSON.
18
19
  * Content is never sniffed to guess a type — the contract owns the type, not the
19
20
  * value (so an id like `'33111715'` never turns into a number under a `z.string()`).
20
21
  */
@@ -1 +1 @@
1
- {"version":3,"file":"multipart.d.ts","sourceRoot":"","sources":["../../src/server/multipart.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,KAAK,CAAC;AAInC,mFAAmF;AACnF,MAAM,WAAW,eAAe;IAC9B,IAAI,EAAE,IAAI,CAAC;IACX,8EAA8E;IAC9E,MAAM,EAAE,OAAO,CAAC;CACjB;AA0CD;;;;;;;;;;;;GAYG;AACH,wBAAsB,cAAc,CAClC,GAAG,EAAE,OAAO,EACZ,SAAS,EAAE,MAAM,EACjB,YAAY,CAAC,EAAE,OAAO,CAAC,OAAO,CAAC,EAC/B,QAAQ,SAA2B,GAClC,OAAO,CAAC,eAAe,CAAC,CAyB1B"}
1
+ {"version":3,"file":"multipart.d.ts","sourceRoot":"","sources":["../../src/server/multipart.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,KAAK,CAAC;AAInC,mFAAmF;AACnF,MAAM,WAAW,eAAe;IAC9B,IAAI,EAAE,IAAI,CAAC;IACX,8EAA8E;IAC9E,MAAM,EAAE,OAAO,CAAC;CACjB;AA0CD;;;;;;;;;;;;;GAaG;AACH,wBAAsB,cAAc,CAClC,GAAG,EAAE,OAAO,EACZ,SAAS,EAAE,MAAM,EACjB,YAAY,CAAC,EAAE,OAAO,CAAC,OAAO,CAAC,EAC/B,QAAQ,SAA2B,GAClC,OAAO,CAAC,eAAe,CAAC,CAyB1B"}
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
@@ -289,8 +289,9 @@ await api.search({ q: 'x', filter: { status: 'active' } })
289
289
 
290
290
  Flatten the field (`status: 'active'`) or move the operation to a body verb
291
291
  (`POST`). Remember the server parses query values from **strings** — use
292
- `z.coerce.number()` / `z.coerce.boolean()` in a query-input schema for
293
- non-string fields.
292
+ `z.coerce.number()` for numbers and `z.stringbool()` for booleans in a
293
+ query-input schema (not `z.coerce.boolean()`, which is `Boolean(str)`, so
294
+ `'false'` would become `true`).
294
295
 
295
296
  ## Transports
296
297
 
@@ -393,7 +394,7 @@ the schema owns its type, exactly as with [query input](#query-input-get--delete
393
394
  input: z.object({
394
395
  id: z.string(), // an id like '33111715' stays a string
395
396
  count: z.coerce.number(), // '5' → 5
396
- active: z.coerce.boolean(), // 'true' → true
397
+ active: z.stringbool(), // 'true' → true, 'false' → false
397
398
  meta: z.preprocess((v) => JSON.parse(String(v)), MetaSchema), // opt a field into JSON
398
399
  })
399
400
  ```
@@ -2292,7 +2293,27 @@ transport (HTTP, MCP, agent). → ADR 0032.
2292
2293
  `details` and `hint` are included when present. On HTTP this is the response
2293
2294
  body with the matching status; for an MCP or agent call the same `code` and
2294
2295
  `details` come back as a tool error. A schema-validation failure on the request
2295
- is turned into a `400 VALIDATION_ERROR` automatically.
2296
+ is turned into a `400 VALIDATION_ERROR` automatically — and it carries the
2297
+ offending fields as structured `details.issues`, so a machine client matches on
2298
+ them instead of parsing the text `message`:
2299
+
2300
+ ```json
2301
+ {
2302
+ "error": {
2303
+ "code": "VALIDATION_ERROR",
2304
+ "message": "name: Invalid input\nage: Invalid input",
2305
+ "details": {
2306
+ "issues": [
2307
+ { "path": "name", "code": "invalid_type", "message": "Invalid input" },
2308
+ { "path": "age", "code": "invalid_type", "message": "Invalid input" }
2309
+ ]
2310
+ }
2311
+ }
2312
+ }
2313
+ ```
2314
+
2315
+ Use the exported `zodIssues(error)` to build the same structured list from a
2316
+ `ZodError` in your own hook.
2296
2317
 
2297
2318
  ### On the client
2298
2319
 
@@ -2376,6 +2397,25 @@ createServer({ services, hooks: { onError } })
2376
2397
  Codes you threw yourself (not stitchkit's) pass through `codeMap` unchanged; the
2377
2398
  `satisfies Record<StitchErrorCode, …>` keeps the map exhaustive across upgrades.
2378
2399
 
2400
+ Invalid input (a `ZodError`) is classified as `VALIDATION_ERROR` 400 before it
2401
+ reaches `render` — a client fault is an honest 400, not a 500 — and the
2402
+ offending fields arrive as structured `info.details.issues`, so your `render`
2403
+ can surface them to a machine client without parsing the message.
2404
+
2405
+ If you write a **bespoke** `onError` instead of using `createErrorHook`, run the
2406
+ thrown value through the exported `normalizeError` to get the same classification
2407
+ (a raw `ZodError` reaches your hook untouched, so the framework can inspect it —
2408
+ call `zodIssues(err)` yourself if you want the structured fields):
2409
+
2410
+ ```ts
2411
+ import { normalizeError } from 'stitchkit/server'
2412
+
2413
+ onError: (ctx, err) => {
2414
+ const e = normalizeError(err) // ZodError → VALIDATION_ERROR 400, else generic 500
2415
+ return jsonError(e.code, e.status, e.message)
2416
+ }
2417
+ ```
2418
+
2379
2419
 
2380
2420
  ==============================================================================
2381
2421
  # Guide: Observability (docs/guide/observability.md)
@@ -3150,6 +3190,11 @@ Also re-exports the error helpers from `stitchkit/contract`.
3150
3190
  | `ByteRange` | _type_ | an inclusive `{ start, end }` byte range |
3151
3191
  | `respondJson` | function | a raw route's JSON response (`204` for null/undefined) |
3152
3192
  | `errorResponse` | function | any thrown value → the framework error envelope + `x-request-id` |
3193
+ | `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` |
3194
+ | `errorCode` | function | the stable error code for a thrown value (side-effect-free — for log attribution) |
3195
+ | `formatZodError` | function | a `ZodError` → a readable, field-summarised string |
3196
+ | `zodIssues` | function | a `ZodError` → structured `{ path, code, message }[]` — the machine-readable sibling of `formatZodError` |
3197
+ | `ZodIssueSummary` | _type_ | one structured validation issue (`{ path, code, message }`) |
3153
3198
  | `parseBody` | function | parse + Zod-validate a JSON body → `data` or `null` (no throw) |
3154
3199
  | `HandlerConfig` | _type_ | config for `createHandler` (runtime-agnostic) |
3155
3200
  | `BunServerConfig` | _type_ | config for `createServer` (Bun) |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "stitchkit",
3
- "version": "0.20.0",
3
+ "version": "0.22.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",