stitchkit 0.20.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.
@@ -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"}
@@ -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,
@@ -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/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
  ```
@@ -2376,6 +2377,21 @@ createServer({ services, hooks: { onError } })
2376
2377
  Codes you threw yourself (not stitchkit's) pass through `codeMap` unchanged; the
2377
2378
  `satisfies Record<StitchErrorCode, …>` keeps the map exhaustive across upgrades.
2378
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
+
2379
2395
 
2380
2396
  ==============================================================================
2381
2397
  # Guide: Observability (docs/guide/observability.md)
@@ -3150,6 +3166,9 @@ Also re-exports the error helpers from `stitchkit/contract`.
3150
3166
  | `ByteRange` | _type_ | an inclusive `{ start, end }` byte range |
3151
3167
  | `respondJson` | function | a raw route's JSON response (`204` for null/undefined) |
3152
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 |
3153
3172
  | `parseBody` | function | parse + Zod-validate a JSON body → `data` or `null` (no throw) |
3154
3173
  | `HandlerConfig` | _type_ | config for `createHandler` (runtime-agnostic) |
3155
3174
  | `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.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",