stitchkit 0.19.0 → 0.21.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.
@@ -73,13 +73,8 @@ async function parseMultipart(req, fileField, fieldsSchema, maxBytes = DEFAULT_M
73
73
  continue;
74
74
  if (isUnsafeKey(key))
75
75
  continue;
76
- if (typeof value === "string") {
77
- try {
78
- fields[key] = safeJsonParse(value);
79
- } catch {
80
- fields[key] = value;
81
- }
82
- }
76
+ if (typeof value === "string")
77
+ fields[key] = value;
83
78
  }
84
79
  return { file, fields: fieldsSchema ? fieldsSchema.parse(fields) : fields };
85
80
  }
package/dist/node.js CHANGED
@@ -3,7 +3,7 @@ import {
3
3
  createImplement,
4
4
  createSocketIOServer,
5
5
  implement
6
- } from "./index-wpbn133x.js";
6
+ } from "./index-f5e4efj1.js";
7
7
  import"./index-tje0q6gp.js";
8
8
  import {
9
9
  AppError,
@@ -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 } 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,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"}
@@ -10,7 +10,7 @@ import {
10
10
  socketIoLane,
11
11
  staticRoute,
12
12
  webSocketLane
13
- } from "../index-wpbn133x.js";
13
+ } from "../index-f5e4efj1.js";
14
14
  import {
15
15
  createAuthHook,
16
16
  createBearerResolver,
@@ -39,7 +39,9 @@ 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,
@@ -118,15 +120,14 @@ function cacheHeaders(maxAge, scope = "public") {
118
120
  // src/server/error-hook.ts
119
121
  function createErrorHook(config) {
120
122
  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;
123
+ const appErr = normalizeError(error);
124
+ const code = config.codeMap && isStitchErrorCode(appErr.code) ? config.codeMap[appErr.code] : appErr.code;
124
125
  const info = {
125
126
  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
127
+ status: appErr.status,
128
+ message: appErr.message,
129
+ details: appErr.details,
130
+ hint: appErr.hint
130
131
  };
131
132
  config.onError?.(error, info);
132
133
  return new Response(JSON.stringify(config.render(info)), {
@@ -639,15 +640,18 @@ export {
639
640
  parseBody,
640
641
  openApiRoute,
641
642
  notFound,
643
+ normalizeError,
642
644
  isStitchErrorCode,
643
645
  implement,
644
646
  getClientInfo,
645
647
  generateTraceId,
646
648
  generateOpenApiDocument,
649
+ formatZodError,
647
650
  forbidden,
648
651
  extractToken,
649
652
  extractIp,
650
653
  errorResponse2 as errorResponse,
654
+ errorCode,
651
655
  deriveCodeChallenge,
652
656
  defineCookie,
653
657
  createSocketIOServer,
@@ -7,9 +7,17 @@ export interface MultipartResult {
7
7
  }
8
8
  /**
9
9
  * Parse a `multipart/form-data` request — extract the file at `fileField` and
10
- * the remaining fields (each JSON-decoded, then validated by `fieldsSchema`
11
- * when given). Rejects with a 400 if the file is missing or the upload exceeds
12
- * `maxBytes` (default 25 MB).
10
+ * the remaining fields, then validate them with `fieldsSchema` when given.
11
+ * Rejects with a 400 if the file is missing or the upload exceeds `maxBytes`
12
+ * (default 25 MB).
13
+ *
14
+ * A multipart text field is always a **string** (per the spec) and is handed to
15
+ * the schema as one — the schema decides its type, exactly as with query params:
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.
19
+ * Content is never sniffed to guess a type — the contract owns the type, not the
20
+ * value (so an id like `'33111715'` never turns into a number under a `z.string()`).
13
21
  */
14
22
  export declare function parseMultipart(req: Request, fileField: string, fieldsSchema?: ZodType<unknown>, maxBytes?: number): Promise<MultipartResult>;
15
23
  //# sourceMappingURL=multipart.d.ts.map
@@ -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;;;;;GAKG;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,CA6B1B"}
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/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
 
@@ -383,6 +384,25 @@ upload: {
383
384
  The client sends a `multipart/form-data` request; the field value must be a
384
385
  `Blob`. See [HTTP server → multipart](./server.md#multipart).
385
386
 
387
+ ### Multipart text fields
388
+
389
+ Any non-file fields sent alongside the file are validated by the endpoint's
390
+ `input` schema. A multipart text field is **always a string** (per the spec) —
391
+ the schema owns its type, exactly as with [query input](#query-input-get--delete):
392
+
393
+ ```ts
394
+ input: z.object({
395
+ id: z.string(), // an id like '33111715' stays a string
396
+ count: z.coerce.number(), // '5' → 5
397
+ active: z.stringbool(), // 'true' → true, 'false' → false
398
+ meta: z.preprocess((v) => JSON.parse(String(v)), MetaSchema), // opt a field into JSON
399
+ })
400
+ ```
401
+
402
+ The content is never sniffed to guess a type — a field is a string until the
403
+ schema coerces it. Send a JSON blob as a stringified field and parse it with
404
+ `z.preprocess`; do not rely on the framework to auto-decode it.
405
+
386
406
  ## Pagination
387
407
 
388
408
  Every list endpoint should return the cursor envelope — one shape, one infinite-
@@ -2357,6 +2377,21 @@ createServer({ services, hooks: { onError } })
2357
2377
  Codes you threw yourself (not stitchkit's) pass through `codeMap` unchanged; the
2358
2378
  `satisfies Record<StitchErrorCode, …>` keeps the map exhaustive across upgrades.
2359
2379
 
2380
+ 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):
2385
+
2386
+ ```ts
2387
+ import { normalizeError } from 'stitchkit/server'
2388
+
2389
+ onError: (ctx, err) => {
2390
+ const e = normalizeError(err) // ZodError → VALIDATION_ERROR 400, else generic 500
2391
+ return jsonError(e.code, e.status, e.message)
2392
+ }
2393
+ ```
2394
+
2360
2395
 
2361
2396
  ==============================================================================
2362
2397
  # Guide: Observability (docs/guide/observability.md)
@@ -3131,6 +3166,9 @@ Also re-exports the error helpers from `stitchkit/contract`.
3131
3166
  | `ByteRange` | _type_ | an inclusive `{ start, end }` byte range |
3132
3167
  | `respondJson` | function | a raw route's JSON response (`204` for null/undefined) |
3133
3168
  | `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` |
3170
+ | `errorCode` | function | the stable error code for a thrown value (side-effect-free — for log attribution) |
3171
+ | `formatZodError` | function | a `ZodError` → a readable, field-summarised string |
3134
3172
  | `parseBody` | function | parse + Zod-validate a JSON body → `data` or `null` (no throw) |
3135
3173
  | `HandlerConfig` | _type_ | config for `createHandler` (runtime-agnostic) |
3136
3174
  | `BunServerConfig` | _type_ | config for `createServer` (Bun) |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "stitchkit",
3
- "version": "0.19.0",
3
+ "version": "0.21.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",