@minisylar/express-typed-router 1.9.7 → 1.9.9

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/README.md CHANGED
@@ -127,6 +127,182 @@ router.get("/static/*", handler); // { "0": string }
127
127
 
128
128
  ---
129
129
 
130
+ ## Validation failure hooks
131
+
132
+ `bodySchema`/`querySchema`/`paramsSchema` already reject a bad request with a 400. Hooks let you react to that failure, and for three of them, change what happens next. There are two kinds:
133
+
134
+ - **`onValidationFailure`** (route or global) — a side effect only: logging, metrics, alerts. It never changes the response.
135
+ - **`onBodyValidationFailure`, `onQueryValidationFailure`, `onParamsValidationFailure`** — one per schema. These *can* change the response: keep the default 400, send something else, or let the request through anyway. Covered below.
136
+
137
+ ### The `info` object
138
+
139
+ Every hook is called with one object:
140
+
141
+ | field | type | what it is |
142
+ | --- | --- | --- |
143
+ | `source` | `"body" \| "query" \| "params"` | which schema failed — only on `onValidationFailure`; the schema-specific hooks already know from their own name |
144
+ | `error` | `string` | short error message |
145
+ | `details` | that schema's own issue shape once narrowed | the validation issues themselves. On the route's `onValidationFailure`, narrow on `source` first (it's a discriminated union). On the global `onValidationFailure`, always `any[]`, it isn't tied to one route's schemas. |
146
+ | `method` | `string` | the route's HTTP method, e.g. `"post"` |
147
+ | `path` | `string` | the route's path, readable even for a `RegExp` route |
148
+ | `req` | Express `Request` | the request; also sees this route's `middleware`-added properties (route-level hooks) or the router's `useMiddleware()`-added properties (global), see below |
149
+ | `res` | Express `Response` | the response — schema-specific hooks only, that's how they change it |
150
+ | `next` | Express `NextFunction` | continues the request — schema-specific hooks only |
151
+
152
+ ### Just reacting to a failure
153
+
154
+ ```ts
155
+ router.post(
156
+ "/users",
157
+ {
158
+ bodySchema: UserSchema,
159
+ querySchema: QuerySchema,
160
+ hooks: {
161
+ onValidationFailure: (info) => {
162
+ if (info.source === "body") {
163
+ logger.warn({ issues: info.details }, "user body validation failed"); // UserSchema's own issue shape
164
+ } else if (info.source === "query") {
165
+ logger.warn({ issues: info.details }, "user query validation failed"); // QuerySchema's own issue shape
166
+ }
167
+ },
168
+ },
169
+ },
170
+ handler,
171
+ );
172
+
173
+ // Or globally, for every route on the router — details is always any[] here,
174
+ // a route's own onValidationFailure above is the one that narrows per schema.
175
+ createTypedRouter().onValidationFailure(({ source, method, path }) => metrics.increment(source));
176
+ createTypedRouterWithConfig({ hooks: { onValidationFailure: (info) => metrics.increment(info.source) } });
177
+ ```
178
+
179
+ Narrowing on `info.source` is what gives you the precise `details` type per branch; unnarrowed, it's whatever's common to all three. The 400 is sent regardless of what this hook does. It can be `async`; a throw or rejection is swallowed, the response never waits on it. `router.onValidationFailure()` can be called any time, read fresh per request, and picks up whatever `.useMiddleware()` already added by that point in the chain, no cast needed:
180
+
181
+ ```ts
182
+ const router = createTypedRouter()
183
+ .useMiddleware(requireAuth) // adds req.userId
184
+ .onValidationFailure((info) => {
185
+ metrics.increment(`validation_failure.${info.source}`, { userId: info.req.userId });
186
+ });
187
+ ```
188
+
189
+ `createTypedRouterWithConfig<Req>({ hooks: { onValidationFailure } })` works the same way, `Req` just has to be spelled out explicitly since there's no `.useMiddleware()` call for it to infer from:
190
+
191
+ ```ts
192
+ const router = createTypedRouterWithConfig<{ userId: string }>({
193
+ hooks: { onValidationFailure: (info) => metrics.increment(info.source, { userId: info.req.userId }) },
194
+ });
195
+ ```
196
+
197
+ The global hook doesn't inherit into a `.mount()`ed sub-router, pass the same function to each router if you want it everywhere.
198
+
199
+ The route's own `onValidationFailure` gets this too, same as its schema-specific siblings, from that route's `middleware` option:
200
+
201
+ ```ts
202
+ router.post(
203
+ "/orders",
204
+ {
205
+ bodySchema: OrderSchema,
206
+ middleware: [requireAuth], // adds req.userId
207
+ hooks: {
208
+ onValidationFailure: (info) => logger.warn({ userId: info.req.userId, source: info.source }, "validation failed"),
209
+ },
210
+ },
211
+ handler,
212
+ );
213
+ ```
214
+
215
+ ### Changing the response
216
+
217
+ `onBodyValidationFailure`, `onQueryValidationFailure`, `onParamsValidationFailure` are typed to that one schema (`details` is its own issue shape, not a blanket `any[]`), and the router waits for them before responding:
218
+
219
+ ```ts
220
+ import { defaultValidationHandler } from "@minisylar/express-typed-router";
221
+ // ^ the library's own default 400 response, exported so a hook can run a
222
+ // side effect and still fall back to it, instead of re-implementing it.
223
+
224
+ router.post(
225
+ "/orders",
226
+ {
227
+ bodySchema: OrderSchema,
228
+ hooks: {
229
+ onBodyValidationFailure: (info) => {
230
+ logger.warn({ issues: info.details }, "order validation failed");
231
+ return defaultValidationHandler(info); // side effect above, still the normal 400
232
+ },
233
+ },
234
+ },
235
+ handler,
236
+ );
237
+ ```
238
+
239
+ What a hook does decides the outcome:
240
+
241
+ | does this | result |
242
+ | --- | --- |
243
+ | nothing | default 400, same as if there were no hook |
244
+ | `defaultValidationHandler(info)` | default 400, after whatever the hook did first |
245
+ | `info.res.status(422).json(...)` | that response instead |
246
+ | `info.next()` | request continues unvalidated; `req.body` stays typed as `OrderSchema`'s output either way, you're trusting it yourself |
247
+
248
+ ### Order
249
+
250
+ A request only ever fails one schema, whichever is checked first (body, then query, then params). So on the `/users` route above, a request with both an invalid body *and* an invalid query string still only fails on `body`, query is never even checked:
251
+
252
+ - `onBodyValidationFailure` fires (if set)
253
+ - `onValidationFailure` fires once, with `source: "body"` — not twice, and `onQueryValidationFailure` does not fire
254
+ - the global `onValidationFailure` fires once, same `source: "body"`
255
+
256
+ Fix the body and resend, and *then* you'd see the query failure. Whichever hooks apply to that one failure run in this order: the schema-specific one, then the route's `onValidationFailure`, then the global one.
257
+
258
+ ### Reading middleware-added properties
259
+
260
+ A route's own `middleware` (see [Middleware typing](#middleware-typing) below) runs before validation, and what it adds to `req` is visible, typed, inside a schema-specific hook too, no cast needed, as long as the hook is written inline:
261
+
262
+ ```ts
263
+ router.post(
264
+ "/orders",
265
+ {
266
+ bodySchema: OrderSchema,
267
+ middleware: [requireAuth], // adds req.userId
268
+ hooks: {
269
+ onBodyValidationFailure: (info) => {
270
+ if (info.req.userId === "trusted-internal-service") return info.next();
271
+ info.res.status(422).json({ error: "Invalid order payload" });
272
+ },
273
+ },
274
+ },
275
+ handler,
276
+ );
277
+ ```
278
+
279
+ A hook pulled out into a standalone `const` can't know which route's `middleware` it'll be attached to, so it falls back to plain `req` there, see reuse below.
280
+
281
+ ### Reuse outside the route
282
+
283
+ `SchemaValidationFailureHook<S>` / `ValidationFailureHook` are exported, so a hook doesn't have to be written inline:
284
+
285
+ ```ts
286
+ const logUserBodyFailure: SchemaValidationFailureHook<typeof UserSchema> = ({ details }) => {
287
+ logger.warn({ issues: details }, "user body validation failed");
288
+ };
289
+
290
+ router.post("/users", { bodySchema: UserSchema, hooks: { onBodyValidationFailure: logUserBodyFailure } }, handler);
291
+ router.put("/users/:id", { bodySchema: UserSchema, hooks: { onBodyValidationFailure: logUserBodyFailure } }, handler);
292
+ ```
293
+
294
+ ### At a glance
295
+
296
+ | hook | fires when | can change the response | `details` |
297
+ | --- | --- | --- | --- |
298
+ | `onBodyValidationFailure` | `bodySchema` rejects | yes | `bodySchema`'s own issue shape |
299
+ | `onQueryValidationFailure` | `querySchema` rejects | yes | `querySchema`'s own issue shape |
300
+ | `onParamsValidationFailure` | `paramsSchema` rejects | yes | `paramsSchema`'s own issue shape |
301
+ | `onValidationFailure` (route option) | any of the three, on this route | no | matching schema's issue shape, once narrowed on `source` |
302
+ | `onValidationFailure` (`router.onValidationFailure()` / `createTypedRouterWithConfig`) | any of the three, on any route on this router | no | `any[]` |
303
+
304
+ ---
305
+
130
306
  ## Middleware typing
131
307
 
132
308
  Declare what a middleware adds to `req`, and that type flows into every handler that uses it.
@@ -329,7 +505,32 @@ app.use(
329
505
 
330
506
  **How it persists:** schemas fill in as traffic flows. They're held in memory and, when `specOutputPath` is set, written to the file (debounced) as new shapes are observed. On startup the library **reloads** the existing file, so a restart doesn't reset what was already learned — the file is the durable store.
331
507
 
332
- **Gotchas:**
508
+ ### Gotchas
509
+
510
+ - **Keep middleware variables as tuples** - inline middleware arrays preserve
511
+ their tuple type automatically, but assigning the array to a variable widens
512
+ it to `TypedMiddleware[]`. The simplest option is to pass the tuple directly
513
+ in the route's `middleware` object:
514
+
515
+ ```ts
516
+ router.get("/something", { middleware: [a, b, c] }, handler);
517
+ ```
518
+
519
+ If you need to reuse the middleware variable with `InferSchemaHandler`, wrap
520
+ it in `defineMiddleware`. It keeps each middleware's specific type instead
521
+ of widening:
522
+
523
+ ```ts
524
+ const middleware = defineMiddleware(a, b, c);
525
+ type Handler = InferSchemaHandler<{ middleware: typeof middleware }>;
526
+ ```
527
+
528
+ `as const` works too, if you'd rather not import a helper:
529
+
530
+ ```ts
531
+ const middleware = [a, b, c] as const;
532
+ type Handler = InferSchemaHandler<{ middleware: typeof middleware }>;
533
+ ```
333
534
 
334
535
  - **Reset accumulated schemas** — inference is merge-only, so a field you _remove_ from a response lingers in the docs. To clear it, delete `openapi.json` and let it rebuild from current traffic.
335
536
  - **`responseSchema` beats inference** — declare it on routes you want guaranteed-correct (and leak-proof); it overrides whatever traffic suggests.
@@ -413,6 +614,46 @@ Edit a route, save, and your client types update on their own.
413
614
 
414
615
  > ⚠️ **Avoid a restart loop.** Write the generated `api.d.ts` **outside** the path your server watcher restarts on (or add it to the watcher's ignore list). If your server watches `*.ts` in `src/` and you output the types _into_ `src/`, you get: type-gen writes `api.d.ts` → server restarts → spec rewrites → type-gen runs again → ♻️. Putting it in a separate folder (e.g. `shared/`, `generated/`) avoids this.
415
616
 
617
+ ### Generate the spec without running a server
618
+
619
+ `specOutputPath` above writes the file as a side effect of starting your app. That's fine for local dev, but in CI you don't want to start a server just to get a file. `generateOpenApiSpec` builds the spec object directly: no Express app, no `app.listen()`, no HTTP request.
620
+
621
+ ```ts
622
+ import { createTypedRouter, generateOpenApiSpec } from "@minisylar/express-typed-router";
623
+ import { writeFile } from "node:fs/promises";
624
+
625
+ const router = createTypedRouter();
626
+ router.get("/users/:id", handler);
627
+ // ... define the rest of your routes
628
+
629
+ const spec = await generateOpenApiSpec(router, { title: "My API", version: "1.0.0" });
630
+ await writeFile("./openapi.json", JSON.stringify(spec, null, 2));
631
+ ```
632
+
633
+ Run that as a script in CI and feed the output to whatever reads a static spec: `openapi-typescript`, a docs site generator, a linter, anything that takes a `.json` file.
634
+
635
+ It accepts the same router shapes as [`createDocs`](#per-feature-routers-one-doc-endpoint) — a single router, a single prefixed router, or an array mixing either:
636
+
637
+ ```ts
638
+ // Single router, no options
639
+ await generateOpenApiSpec(usersRouter);
640
+
641
+ // Single router with a prefix
642
+ await generateOpenApiSpec({ prefix: "/api/users", router: usersRouter });
643
+
644
+ // Multiple routers, all prefixed
645
+ await generateOpenApiSpec([
646
+ { prefix: "/api/users", router: usersRouter },
647
+ { prefix: "/api/orders", router: ordersRouter },
648
+ ]);
649
+
650
+ // Mixed — some prefixed, some not
651
+ await generateOpenApiSpec([
652
+ usersRouter,
653
+ { prefix: "/api/orders", router: ordersRouter },
654
+ ]);
655
+ ```
656
+
416
657
  ### Use with `openapi-fetch`
417
658
 
418
659
  ```ts
@@ -596,12 +837,19 @@ Use a schema that actually parses the text:
596
837
  | ---------------------------------------- | ------------------------------------------------ |
597
838
  | `createTypedRouter()` | Create a router |
598
839
  | `createTypedRouterWithMiddleware(...mw)` | Create a router pre-configured with middleware |
599
- | `createTypedRouterWithConfig(config)` | Create a router with custom error handling |
840
+ | `createTypedRouterWithConfig(config)` | Create a router with an error handler and/or global config |
600
841
  | `router.useMiddleware(mw)` | Add typed global middleware (returns new router) |
842
+ | `router.onValidationFailure(hook)` | Set the global validation failure hook (returns same router) |
601
843
  | `router.use(prefix, subRouter)` | Mount a sub-router |
602
844
  | `router.getRouter()` | Get the underlying Express router |
603
845
  | `router.docs(options)` | Get the docs + OpenAPI spec router |
846
+ | `generateOpenApiSpec(routers, options)` | Build the spec object with no server involved |
604
847
  | `TypedMiddleware<T>` | Type helper for middleware that extends `req` |
848
+ | `defineMiddleware(...mw)` | Keep a middleware array's tuple type when reused |
849
+ | `SchemaValidationFailureHook<S>` | Type a reusable `onBodyValidationFailure`/`onQueryValidationFailure`/`onParamsValidationFailure` outside the route |
850
+ | `RouteValidationFailureHook<Body, Query, Params>` | Type a reusable route-level `onValidationFailure` outside the route, `details` still narrows on `source` |
851
+ | `ValidationFailureHook` | Type a reusable global `onValidationFailure` outside the route |
852
+ | `defaultValidationHandler(info)` | The library's own default 400 response, callable from a schema-specific hook |
605
853
 
606
854
  ---
607
855
 
@@ -1,12 +1,12 @@
1
- Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`});var e=Object.create,t=Object.defineProperty,n=Object.getOwnPropertyDescriptor,r=Object.getOwnPropertyNames,i=Object.getPrototypeOf,a=Object.prototype.hasOwnProperty,o=(e,i,o,s)=>{if(i&&typeof i==`object`||typeof i==`function`)for(var c=r(i),l=0,u=c.length,d;l<u;l++)d=c[l],!a.call(e,d)&&d!==o&&t(e,d,{get:(e=>i[e]).bind(null,d),enumerable:!(s=n(i,d))||s.enumerable});return e},s=(n,r,s)=>(s=n==null?{}:e(i(n)),o(r||!n||!n.__esModule||!a.call(n,`default`)?t(s,`default`,{value:n,enumerable:!0}):s,n));let c=require("express");c=s(c,1);let l=require("@standard-schema/utils");function u(e,t){let n=e;if(n&&n[`~standard`]&&typeof n[`~standard`].validate==`function`){let e=n[`~standard`].validate(t);if(e instanceof Promise)throw TypeError(`Async schema validation is not supported by parseSchema`);if(e.issues)throw new l.SchemaError(e.issues);return e.value}throw TypeError(`Unsupported schema shape for parseSchema`)}function d(e,t){let n=e;if(n&&n[`~standard`]&&typeof n[`~standard`].validate==`function`)return n[`~standard`].validate(t);if(n&&typeof n.safeParse==`function`)return n.safeParse(t);if(n&&typeof n.parse==`function`)try{return{value:n.parse(t)}}catch(e){return{issues:[{message:e?.message??String(e)}]}}if(n&&typeof n.validate==`function`){let e=n.validate(t);return e&&e.then&&typeof e.then==`function`?e.then(e=>e.error?{issues:[{message:e.error.message}]}:e.issues?{issues:e.issues}:{value:e.value??e}):e&&e.error?{issues:[{message:e.error.message}]}:e&&e.issues?{issues:e.issues}:{value:e.value??e}}return{issues:[{message:`Unsupported schema shape`}]}}function f(e){return typeof e==`object`&&!!e&&`issues`in e&&Array.isArray(e.issues)}const p=Function(`m`,`return import(m)`);let m,h;async function g(e){m??=await p(`module`),h??=await p(`url`);let t=[],n=globalThis.process?.argv?.[1];n&&t.push(h.pathToFileURL(n).href);let r=globalThis.process?.cwd?.()??``;r&&t.push(h.pathToFileURL(r+`/`).href);for(let n of t)try{let t=m.createRequire(n).resolve(e);return await p(h.pathToFileURL(t).href)}catch{}return p(e)}const _=new WeakMap,v=/\(\?<[^>]+>/;function y(e){return typeof e==`string`&&v.test(e)?new RegExp(e):e}function b(e){if(typeof e.path==`string`)return e.path;if(e.pathExample)return e.pathExample;let t=0;return e.path.source.replace(/\\\//g,`/`).replace(/\((?!\?)[^()]*\)/g,()=>`:${t++}`)}function x(e){let t=[],n=0,r=0,i=!1,a=!1;for(let o=0;o<e.length;o++){let s=e[o];if(a){a=!1;continue}if(s===`\\`){a=!0;continue}if(s===`[`){i=!0;continue}if(s===`]`){i=!1;continue}i||(s===`(`?r++:s===`)`?r=Math.max(0,r-1):s===`|`&&r===0&&(t.push(e.slice(n,o)),n=o+1))}return t.push(e.slice(n)),t.length>1?t:[e]}function S(e){if(!e.startsWith(`(`)||!e.endsWith(`)`))return e;let t=0,n=!1,r=!1;for(let i=0;i<e.length;i++){let a=e[i];if(r){r=!1;continue}if(a===`\\`){r=!0;continue}if(a===`[`)n=!0;else if(a===`]`)n=!1;else if(!n&&a===`(`)t++;else if(!n&&a===`)`&&(t--,t===0&&i!==e.length-1))return e}return e.slice(1,-1).replace(/^\?:/,``)}function C(e){let t=0;return S(e).replace(/^\^|\$$/g,``).replace(/\\\//g,`/`).replace(/\.\*|\.\+/g,`/:path`).replace(/\/?\?$/,``).replace(/\(\?<([A-Za-z0-9_]+)>[^()]*\)/g,`:$1`).replace(/\((?!\?)[^()]*\)/g,()=>`:${t++}`)}function w(e){if(typeof e.path==`string`||e.pathExample)return[b(e)];let t=x(e.path.source);return t.length===1?[b(e)]:t.map(C)}function T(e){return e.replace(/\{([^{}]*)\}/g,`$1`).replace(/:([A-Za-z0-9_]+)(?:\([^)]*\))?[?+*]?/g,`{$1}`).replace(/\(\?<([A-Za-z0-9_]+)>[^)]*\)/g,`{$1}`).replace(/^\^|\$$/g,``).replace(/\/{2,}/g,`/`)}function E(e){let t=e.replace(/\{([^{}]*)\}/g,`$1`);return[...t.matchAll(/:([A-Za-z0-9_]+)/g),...t.matchAll(/\(\?<([A-Za-z0-9_]+)>/g)].map(e=>e[1])}function D(e){return e.startsWith(`:`)||e.startsWith(`*`)||e.includes(`(?<`)}function O(e){return e.replace(/^\^|\$$/g,``).replace(/[{}]/g,``).split(`/`).filter(Boolean).find(e=>!D(e))??`default`}function k(e,t){let n=t.replace(/^\^|\$$/g,``).replace(/[{}]/g,``).split(`/`).filter(e=>e&&!D(e)),r=n[n.length-1]??`resource`;return`${{get:`Get`,post:`Create`,put:`Update`,patch:`Patch`,delete:`Delete`,head:`Head`,options:`Options`}[e]??e} ${r}`}async function A(e){let t=_.get(e);if(t)return t;let n=await j(e);return _.set(e,n),n}async function j(e){if(typeof e.toJsonSchema==`function`)try{return e.toJsonSchema()}catch{}let t=e[`~standard`]?.vendor;if(t===`zod`){try{let t=await g(`zod`);if(typeof t.toJSONSchema==`function`)return t.toJSONSchema(e)}catch{}try{let t=await g(`zod-to-json-schema`),n=t.zodToJsonSchema??t.default?.zodToJsonSchema;if(typeof n==`function`)return n(e)}catch{}}if(t===`valibot`)try{let t=await g(`@valibot/to-json-schema`),n=t.toJsonSchema??t.default?.toJsonSchema;if(typeof n==`function`)return n(e)}catch{}if(t===`effect`)try{let t=await g(`effect`),n=t.JSONSchema?.make??t.default?.JSONSchema?.make;if(typeof n==`function`)return n(e)}catch{}return{}}function M(e){return N(e,0,new WeakSet)}function N(e,t,n){if(e==null)return{type:`null`};if(t>=12)return{};if(e instanceof Date)return{type:`string`,format:`date-time`};if(typeof e==`bigint`)return{type:`integer`};if(typeof e==`object`&&typeof e.toJSON==`function`)return N(e.toJSON(),t,n);if(Array.isArray(e)){if(e.length===0||n.has(e))return{type:`array`,items:{}};n.add(e);let r=Math.min(e.length,20),i=N(e[0],t+1,n);for(let a=1;a<r;a++)i=I(i,N(e[a],t+1,n));return n.delete(e),{type:`array`,items:i}}switch(typeof e){case`string`:return{type:`string`};case`boolean`:return{type:`boolean`};case`number`:return Number.isFinite(e)?{type:Number.isInteger(e)?`integer`:`number`}:{type:`null`};case`object`:{if(n.has(e))return{type:`object`};n.add(e);let r={},i=[];for(let[a,o]of Object.entries(e))typeof o!=`function`&&o!==void 0&&(r[a]=N(o,t+1,n),i.push(a));n.delete(e);let a={type:`object`,properties:r};return i.length&&(a.required=i),a}default:return{}}}function P(e){return!e||e.type===void 0?new Set:new Set(Array.isArray(e.type)?e.type:[e.type])}function F(e){e.has(`integer`)&&e.has(`number`)&&e.delete(`integer`);let t=[...e];if(t.length!==0)return t.length===1?t[0]:t}function I(e,t){if(!e||Object.keys(e).length===0)return t??{};if(!t||Object.keys(t).length===0)return e??{};let n=new Set([...P(e),...P(t)]),r={},i=F(n);if(i!==void 0&&(r.type=i),n.has(`object`)&&(e.properties||t.properties)){let n=e.properties??{},i=t.properties??{},a={};for(let e of new Set([...Object.keys(n),...Object.keys(i)]))a[e]=I(n[e],i[e]);r.properties=a;let o=e.required??[],s=t.required??[],c=o.filter(e=>s.includes(e));c.length&&(r.required=c)}if(n.has(`array`)){let n=I(e.items,t.items);n&&Object.keys(n).length&&(r.items=n)}return r}function L(e){return e.replace(/&/g,`&amp;`).replace(/</g,`&lt;`).replace(/>/g,`&gt;`).replace(/"/g,`&quot;`)}const R=`https://cdn.jsdelivr.net/npm/@scalar/api-reference`;function z(e,t,n){return`<!doctype html>
1
+ Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`});var e=Object.create,t=Object.defineProperty,n=Object.getOwnPropertyDescriptor,r=Object.getOwnPropertyNames,i=Object.getPrototypeOf,a=Object.prototype.hasOwnProperty,o=(e,i,o,s)=>{if(i&&typeof i==`object`||typeof i==`function`)for(var c=r(i),l=0,u=c.length,d;l<u;l++)d=c[l],!a.call(e,d)&&d!==o&&t(e,d,{get:(e=>i[e]).bind(null,d),enumerable:!(s=n(i,d))||s.enumerable});return e},s=(n,r,s)=>(s=n==null?{}:e(i(n)),o(r||!n||!n.__esModule||!a.call(n,`default`)?t(s,`default`,{value:n,enumerable:!0}):s,n));let c=require("express");c=s(c,1);let l=require("@standard-schema/utils");function u(e,t){let n=e;if(n&&n[`~standard`]&&typeof n[`~standard`].validate==`function`){let e=n[`~standard`].validate(t);if(e instanceof Promise)throw TypeError(`Async schema validation is not supported by parseSchema`);if(e.issues)throw new l.SchemaError(e.issues);return e.value}throw TypeError(`Unsupported schema shape for parseSchema`)}function d(e,t){let n=e;if(n&&n[`~standard`]&&typeof n[`~standard`].validate==`function`)return n[`~standard`].validate(t);if(n&&typeof n.safeParse==`function`)return n.safeParse(t);if(n&&typeof n.parse==`function`)try{return{value:n.parse(t)}}catch(e){return{issues:[{message:e?.message??String(e)}]}}if(n&&typeof n.validate==`function`){let e=n.validate(t);return e&&e.then&&typeof e.then==`function`?e.then(e=>e.error?{issues:[{message:e.error.message}]}:e.issues?{issues:e.issues}:{value:e.value??e}):e&&e.error?{issues:[{message:e.error.message}]}:e&&e.issues?{issues:e.issues}:{value:e.value??e}}return{issues:[{message:`Unsupported schema shape`}]}}function f(e){return typeof e==`object`&&!!e&&`issues`in e&&Array.isArray(e.issues)}function p(...e){return e}function m(e){e.res.status(400).json({error:`Validation failed`,details:e.details})}function h(e,t){if(e)try{e(t)?.catch?.(()=>{})}catch{}}const g=Function(`m`,`return import(m)`);let _,v;async function y(e){_??=await g(`module`),v??=await g(`url`);let t=[],n=globalThis.process?.argv?.[1];n&&t.push(v.pathToFileURL(n).href);let r=globalThis.process?.cwd?.()??``;r&&t.push(v.pathToFileURL(r+`/`).href);for(let n of t)try{let t=_.createRequire(n).resolve(e);return await g(v.pathToFileURL(t).href)}catch{}return g(e)}const b=new WeakMap,x=/\(\?<[^>]+>/;function S(e){return typeof e==`string`&&x.test(e)?new RegExp(e):e}function C(e){if(typeof e.path==`string`)return e.path;if(e.pathExample)return e.pathExample;let t=0;return e.path.source.replace(/\\\//g,`/`).replace(/\((?!\?)[^()]*\)/g,()=>`:${t++}`)}function w(e){let t=[],n=0,r=0,i=!1,a=!1;for(let o=0;o<e.length;o++){let s=e[o];if(a){a=!1;continue}if(s===`\\`){a=!0;continue}if(s===`[`){i=!0;continue}if(s===`]`){i=!1;continue}i||(s===`(`?r++:s===`)`?r=Math.max(0,r-1):s===`|`&&r===0&&(t.push(e.slice(n,o)),n=o+1))}return t.push(e.slice(n)),t.length>1?t:[e]}function T(e){if(!e.startsWith(`(`)||!e.endsWith(`)`))return e;let t=0,n=!1,r=!1;for(let i=0;i<e.length;i++){let a=e[i];if(r){r=!1;continue}if(a===`\\`){r=!0;continue}if(a===`[`)n=!0;else if(a===`]`)n=!1;else if(!n&&a===`(`)t++;else if(!n&&a===`)`&&(t--,t===0&&i!==e.length-1))return e}return e.slice(1,-1).replace(/^\?:/,``)}function E(e){let t=0;return T(e).replace(/^\^|\$$/g,``).replace(/\\\//g,`/`).replace(/\.\*|\.\+/g,`/:path`).replace(/\/?\?$/,``).replace(/\(\?<([A-Za-z0-9_]+)>[^()]*\)/g,`:$1`).replace(/\((?!\?)[^()]*\)/g,()=>`:${t++}`)}function D(e){if(typeof e.path==`string`||e.pathExample)return[C(e)];let t=w(e.path.source);return t.length===1?[C(e)]:t.map(E)}function O(e){let t=e.replace(/\{([^{}]*)\}/g,`$1`).replace(/:([A-Za-z0-9_]+)(?:\([^)]*\))?[?+*]?/g,`{$1}`).replace(/\(\?<([A-Za-z0-9_]+)>[^)]*\)/g,`{$1}`).replace(/\*([A-Za-z0-9_]+)/g,`{$1}`).replace(/^\^|\$$/g,``),n=0;return t.replace(/\*/g,()=>`{${n++}}`).replace(/\/{2,}/g,`/`)}function k(e){let t=e.replace(/\{([^{}]*)\}/g,`$1`).replace(/^\^|\$$/g,``),n=[...t.matchAll(/:([A-Za-z0-9_]+)/g),...t.matchAll(/\(\?<([A-Za-z0-9_]+)>/g),...t.matchAll(/\*([A-Za-z0-9_]+)/g)].map(e=>e[1]),r=(t.replace(/\*[A-Za-z0-9_]+/g,``).match(/\*/g)??[]).length;for(let e=0;e<r;e++)n.push(String(e));return n}function A(e){return e.startsWith(`:`)||e.includes(`*`)||e.includes(`(?<`)}function j(e){return e.replace(/^\^|\$$/g,``).replace(/[{}]/g,``).split(`/`).filter(Boolean).find(e=>!A(e))??`default`}function M(e,t){let n=t.replace(/^\^|\$$/g,``).replace(/[{}]/g,``).split(`/`).filter(e=>e&&!A(e)),r=n[n.length-1]??`resource`;return`${{get:`Get`,post:`Create`,put:`Update`,patch:`Patch`,delete:`Delete`,head:`Head`,options:`Options`}[e]??e} ${r}`}async function N(e){let t=b.get(e);if(t)return t;let n=await P(e);return b.set(e,n),n}async function P(e){if(typeof e.toJsonSchema==`function`)try{return e.toJsonSchema()}catch{}let t=e[`~standard`]?.vendor;if(t===`zod`){try{let t=await y(`zod`);if(typeof t.toJSONSchema==`function`)return t.toJSONSchema(e)}catch{}try{let t=await y(`zod-to-json-schema`),n=t.zodToJsonSchema??t.default?.zodToJsonSchema;if(typeof n==`function`)return n(e)}catch{}}if(t===`valibot`)try{let t=await y(`@valibot/to-json-schema`),n=t.toJsonSchema??t.default?.toJsonSchema;if(typeof n==`function`)return n(e)}catch{}if(t===`effect`)try{let t=await y(`effect`),n=t.JSONSchema?.make??t.default?.JSONSchema?.make;if(typeof n==`function`)return n(e)}catch{}return{}}function F(e){return I(e,0,new WeakSet)}function I(e,t,n){if(e==null)return{type:`null`};if(t>=12)return{};if(e instanceof Date)return{type:`string`,format:`date-time`};if(typeof e==`bigint`)return{type:`integer`};if(typeof e==`object`&&typeof e.toJSON==`function`)return I(e.toJSON(),t,n);if(Array.isArray(e)){if(e.length===0||n.has(e))return{type:`array`,items:{}};n.add(e);let r=Math.min(e.length,20),i=I(e[0],t+1,n);for(let a=1;a<r;a++)i=z(i,I(e[a],t+1,n));return n.delete(e),{type:`array`,items:i}}switch(typeof e){case`string`:return{type:`string`};case`boolean`:return{type:`boolean`};case`number`:return Number.isFinite(e)?{type:Number.isInteger(e)?`integer`:`number`}:{type:`null`};case`object`:{if(n.has(e))return{type:`object`};n.add(e);let r={},i=[];for(let[a,o]of Object.entries(e))typeof o!=`function`&&o!==void 0&&(r[a]=I(o,t+1,n),i.push(a));n.delete(e);let a={type:`object`,properties:r};return i.length&&(a.required=i),a}default:return{}}}function L(e){return!e||e.type===void 0?new Set:new Set(Array.isArray(e.type)?e.type:[e.type])}function R(e){e.has(`integer`)&&e.has(`number`)&&e.delete(`integer`);let t=[...e];if(t.length!==0)return t.length===1?t[0]:t}function z(e,t){if(!e||Object.keys(e).length===0)return t??{};if(!t||Object.keys(t).length===0)return e??{};let n=new Set([...L(e),...L(t)]),r={},i=R(n);if(i!==void 0&&(r.type=i),n.has(`object`)&&(e.properties||t.properties)){let n=e.properties??{},i=t.properties??{},a={};for(let e of new Set([...Object.keys(n),...Object.keys(i)]))a[e]=z(n[e],i[e]);r.properties=a;let o=e.required??[],s=t.required??[],c=o.filter(e=>s.includes(e));c.length&&(r.required=c)}if(n.has(`array`)){let n=z(e.items,t.items);n&&Object.keys(n).length&&(r.items=n)}return r}function B(e){return e.replace(/&/g,`&amp;`).replace(/</g,`&lt;`).replace(/>/g,`&gt;`).replace(/"/g,`&quot;`)}const V=`https://cdn.jsdelivr.net/npm/@scalar/api-reference`;function H(e,t,n){return`<!doctype html>
2
2
  <html>
3
3
  <head>
4
- <title>${L(e)}</title>
4
+ <title>${B(e)}</title>
5
5
  <meta charset="utf-8" />
6
6
  <meta name="viewport" content="width=device-width, initial-scale=1" />
7
7
  </head>
8
8
  <body>
9
- <script id="api-reference" data-url="${L(t)}"><\/script>
10
- <script src="${L(n)}"><\/script>
9
+ <script id="api-reference" data-url="${B(t)}"><\/script>
10
+ <script src="${B(n)}"><\/script>
11
11
  </body>
12
- </html>`}async function B(e,t){let n=Object.create(null);for(let t of e)if(!(t.method===`all`||t.hidden))for(let e of w(t)){let r=T(e);n[r]||(n[r]={});let i;if(t.paramsSchema){let e=await A(t.paramsSchema),n=e.properties??{},r=e.required??[];i=Object.entries(n).map(([e,t])=>({name:e,in:`path`,required:r.includes(e),schema:t}))}else i=E(e).map(e=>({name:e,in:`path`,required:!0,schema:{type:`string`}}));if(t.querySchema){let e=await A(t.querySchema),n=e.properties??{},r=e.required??[];for(let[e,t]of Object.entries(n))i.push({name:e,in:`query`,required:r.includes(e),schema:t})}let a={summary:t.summary??k(t.method,e),tags:t.tags??[O(e)],parameters:i};t.description&&(a.description=t.description),t.deprecated&&(a.deprecated=!0),t.bodySchema&&(a.requestBody={required:!0,content:{"application/json":{schema:await A(t.bodySchema)}}});let o={};for(let[e,n]of t.responseSamples){let t={schema:n.schema};n.example!==void 0&&(t.example=n.example),o[String(e)]={description:e<400?`Success`:`Error`,content:{"application/json":t}}}if(t.responseSchema){let e=await A(t.responseSchema);if(e&&Object.keys(e).length){let n=`200`;for(let e of t.responseSamples.keys())if(e>=200&&e<300){n=String(e);break}o[n]={description:`Success`,content:{"application/json":{schema:e}}}}}Object.keys(o).length===0&&(o[200]={description:`Success`}),a.responses=o,n[r][t.method]=a}return{openapi:`3.1.0`,info:{title:t.title??`API`,version:t.version??`1.0.0`,...t.description?{description:t.description}:{}},...t.servers?{servers:t.servers}:{},paths:n}}const V=new WeakMap;var H=class e{router;routes=[];mountedRouters=[];sampleMode=`off`;scheduleSpecWrite;constructor(){this.router=c.default.Router(),V.set(this.router,this)}useMiddleware(e){return this.router.use(e),this}getRouter(){return this.router}use(t,...n){let r=typeof t==`string`,i=r?t:``,a=(r?n:[t,...n]).map(t=>{if(t instanceof e)return this.trackMounted(i,t),t.getRouter();let n=V.get(t);return n&&this.trackMounted(i,n),t});return r?this.router.use(t,...a):this.router.use(...a),this}mount(e,t){if(typeof e==`string`){let n=t;this.router.use(e,n.getRouter()),this.trackMounted(e,n)}else this.router.use(e.getRouter()),this.trackMounted(``,e);return this}getRouteMetadata(){let e=this.mountedRouters.flatMap(({prefix:e,router:t})=>t.getRouteMetadata().map(t=>({...t,...typeof t.path==`string`?{path:e+t.path}:{path:t.path,pathExample:e+b(t)}})));return[...this.routes,...e]}enableSampling(e=`redacted`,t,n=new Set){if(!n.has(this)){n.add(this),this.sampleMode=e,this.scheduleSpecWrite=t;for(let{router:r}of this.mountedRouters)r.enableSampling(e,t,n)}}trackMounted(e,t){this.mountedRouters.some(n=>n.router===t&&n.prefix===e)||(this.mountedRouters.push({prefix:e,router:t}),this.sampleMode!==`off`&&t.enableSampling(this.sampleMode,this.scheduleSpecWrite))}hydrateResponses(e,t=``,n=new Set){if(n.has(this))return;n.add(this);let r=e?.paths??{};for(let e of this.routes)for(let n of w(e)){let i=r[T(t+n)]?.[e.method]?.responses;if(i)for(let t of Object.keys(i)){let n=Number(t);if(Number.isNaN(n)||e.responseSamples.has(n))continue;let r=i[t]?.content?.[`application/json`];r?.schema&&e.responseSamples.set(n,r.example===void 0?{schema:r.schema}:{schema:r.schema,example:r.example})}}for(let{prefix:r,router:i}of this.mountedRouters)i.hydrateResponses(e,t+r,n)}docs(e={}){let t=c.default.Router(),n;if(e.specOutputPath){let t=e.specOutputPath,r=!1,i=!1,a=async()=>{if(r){i=!0;return}r=!0;try{let n=await B(this.getRouteMetadata(),e),r=await p(`fs/promises`),i=t.replace(/[/\\][^/\\]*$/,``);i&&i!==t&&await r.mkdir(i,{recursive:!0}).catch(()=>{});let a=globalThis.process?.pid??`0`,o=`${t}.${a}.tmp`;await r.writeFile(o,JSON.stringify(n,null,2),`utf8`),await r.rename(o,t)}catch{}finally{r=!1,i&&(i=!1,a())}},o;n=()=>{o&&clearTimeout(o),o=setTimeout(a,300),o.unref?.()},setImmediate(async()=>{try{let e=await(await p(`fs/promises`)).readFile(t,`utf8`).catch(()=>null);if(e)try{this.hydrateResponses(JSON.parse(e))}catch{}}catch{}await a()})}return e.sampleResponses!==!1&&this.enableSampling(e.sampleResponses===`live`?`live`:`redacted`,n),t.get(`/openapi.json`,async(t,n)=>{try{let t=await B(this.getRouteMetadata(),e);n.json(t)}catch(e){n.status(500).json({error:`Failed to generate spec`,details:String(e)})}}),t.get(`/`,(t,n)=>{let r=`${t.baseUrl}/openapi.json`,i=e.cdnUrl??R;n.setHeader(`Content-Type`,`text/html; charset=utf-8`),n.send(z(e.title??`API`,r,i))}),t}get(e,t,n){return this.registerRoute(`get`,e,t,n)}post(e,t,n){return this.registerRoute(`post`,e,t,n)}put(e,t,n){return this.registerRoute(`put`,e,t,n)}patch(e,t,n){return this.registerRoute(`patch`,e,t,n)}delete(e,t,n){return this.registerRoute(`delete`,e,t,n)}options(e,t,n){return this.registerRoute(`options`,e,t,n)}head(e,t,n){return this.registerRoute(`head`,e,t,n)}all(e,t,n){return this.registerRoute(`all`,e,t,n)}registerRoute(e,t,n,r){let i=[],a={method:e,path:t,responseSamples:new Map};if(this.routes.push(a),typeof n==`object`){let e=n;a.bodySchema=e.bodySchema,a.querySchema=e.querySchema,a.paramsSchema=e.paramsSchema,a.tags=e.tags,a.description=e.description,a.summary=e.summary,a.deprecated=e.deprecated,a.responseSchema=e.responseSchema,a.hidden=e.hidden,a.pathExample=e.pathExample,e.middleware&&i.push(...e.middleware),e.bodySchema&&i.push(this.createBodyValidationMiddleware(e.bodySchema)),e.querySchema&&i.push(this.createQueryValidationMiddleware(e.querySchema)),e.paramsSchema&&i.push(this.createParamsValidationMiddleware(e.paramsSchema)),i.push(r)}else i.push(n);let o=0,s=(e,t)=>{let n=e.statusCode,r=M(t),i=a.responseSamples.get(n),o=i?I(i.schema,r):r,s=this.sampleMode===`live`?i?.example??t:void 0,c=!i||JSON.stringify(i.schema)!==JSON.stringify(o);a.responseSamples.set(n,{schema:o,example:s}),c&&this.scheduleSpecWrite?.()};return this.router[e](y(t),(e,t,n)=>{if(this.sampleMode===`off`||a.hidden||o>=50||a.responseSamples.size>=10){n();return}o++;let r=t.json;t.json=function(e){return s(t,e),r.call(this,e)};let i=t.send;t.send=function(e){return typeof e==`object`&&e&&!Buffer.isBuffer(e)&&s(t,e),i.call(this,e)},n()},...i),this}createBodyValidationMiddleware(e){return async(t,n,r)=>{try{let i=d(e,t.body),a=i&&typeof i.then==`function`?await i:i;if(a&&`issues`in a&&a.issues){n.status(400).json({error:`Validation failed`,details:a.errors||a.issues});return}t.body=a&&`value`in a?a.value:a,r()}catch(e){f(e)?n.status(400).json({error:`Validation failed`,details:e.errors||e.issues}):r(e)}}}createParamsValidationMiddleware(e){return async(t,n,r)=>{try{let i=d(e,t.params),a=i&&typeof i.then==`function`?await i:i;if(a&&`issues`in a&&a.issues){n.status(400).json({error:`Validation failed`,details:a.errors||a.issues});return}t.params=a&&`value`in a?a.value:a,r()}catch(e){f(e)?n.status(400).json({error:`Validation failed`,details:e.errors||e.issues}):r(e)}}}createQueryValidationMiddleware(e){return async(t,n,r)=>{try{let i=d(e,t.query),a=i&&typeof i.then==`function`?await i:i;if(a&&`issues`in a&&a.issues){n.status(400).json({error:`Validation failed`,details:a.errors||a.issues});return}let o=a&&`value`in a?a.value:a;Object.defineProperty(t,"query",{value:o,writable:!1,enumerable:!0,configurable:!0}),r()}catch(e){f(e)?n.status(400).json({error:`Validation failed`,details:e.errors||e.issues}):r(e)}}}};function U(){return new H}function W(e){let t=new H;return e?.errorHandler&&t.getRouter().use(e.errorHandler),t}function G(...e){let t=new H;for(let n of e)t=t.useMiddleware(n);return t}function K(e,t={}){let n=(Array.isArray(e)?e:[e]).map(e=>`prefix`in e?e:{prefix:``,router:e});if(t.sampleResponses!==!1){let e=t.sampleResponses===`live`?`live`:`redacted`;for(let{router:t}of n)t.enableSampling(e)}let r=c.default.Router();return r.get(`/openapi.json`,async(e,r)=>{try{let e=await B(n.flatMap(({prefix:e,router:t})=>t.getRouteMetadata().map(t=>({...t,...typeof t.path==`string`?{path:e+t.path}:{path:t.path,pathExample:e+b(t)}}))),t);r.json(e)}catch(e){r.status(500).json({error:`Failed to generate spec`,details:String(e)})}}),r.get(`/`,(e,n)=>{let r=`${e.baseUrl}/openapi.json`,i=t.cdnUrl??R;n.setHeader(`Content-Type`,`text/html; charset=utf-8`),n.send(z(t.title??`API`,r,i))}),r}exports.TypedRouter=H,exports.createDocs=K,exports.createTypedRouter=U,exports.createTypedRouterWithConfig=W,exports.createTypedRouterWithMiddleware=G,exports.inferJsonSchema=M,exports.isSchemaError=f,exports.parseSchema=u,exports.safeParseSchema=d;
12
+ </html>`}async function U(e,t){let n=Object.create(null);for(let t of e)if(!(t.method===`all`||t.hidden))for(let e of D(t)){let r=O(e);n[r]||(n[r]={});let i;if(t.paramsSchema){let e=await N(t.paramsSchema),n=e.properties??{},r=e.required??[];i=Object.entries(n).map(([e,t])=>({name:e,in:`path`,required:r.includes(e),schema:t}))}else i=k(e).map(e=>({name:e,in:`path`,required:!0,schema:{type:`string`}}));if(t.querySchema){let e=await N(t.querySchema),n=e.properties??{},r=e.required??[];for(let[e,t]of Object.entries(n))i.push({name:e,in:`query`,required:r.includes(e),schema:t})}let a={summary:t.summary??M(t.method,e),tags:t.tags??[j(e)],parameters:i};t.description&&(a.description=t.description),t.deprecated&&(a.deprecated=!0),t.bodySchema&&(a.requestBody={required:!0,content:{"application/json":{schema:await N(t.bodySchema)}}});let o={};for(let[e,n]of t.responseSamples){let t={schema:n.schema};n.example!==void 0&&(t.example=n.example),o[String(e)]={description:e<400?`Success`:`Error`,content:{"application/json":t}}}if(t.responseSchema){let e=await N(t.responseSchema);if(e&&Object.keys(e).length){let n=`200`;for(let e of t.responseSamples.keys())if(e>=200&&e<300){n=String(e);break}o[n]={description:`Success`,content:{"application/json":{schema:e}}}}}Object.keys(o).length===0&&(o[200]={description:`Success`}),a.responses=o,n[r][t.method]=a}return{openapi:`3.1.0`,info:{title:t.title??`API`,version:t.version??`1.0.0`,...t.description?{description:t.description}:{}},...t.servers?{servers:t.servers}:{},paths:n}}const W=new WeakMap;var G=class e{router;routes=[];mountedRouters=[];sampleMode=`off`;scheduleSpecWrite;globalValidationFailureHook;constructor(){this.router=c.default.Router(),W.set(this.router,this)}onValidationFailure(e){return this.globalValidationFailureHook=e,this}useMiddleware(e){return this.router.use(e),this}getRouter(){return this.router}use(t,...n){let r=typeof t==`string`,i=r?t:``,a=(r?n:[t,...n]).map(t=>{if(t instanceof e)return this.trackMounted(i,t),t.getRouter();let n=W.get(t);return n&&this.trackMounted(i,n),t});return r?this.router.use(t,...a):this.router.use(...a),this}mount(e,t){if(typeof e==`string`){let n=t;this.router.use(e,n.getRouter()),this.trackMounted(e,n)}else this.router.use(e.getRouter()),this.trackMounted(``,e);return this}getRouteMetadata(){let e=this.mountedRouters.flatMap(({prefix:e,router:t})=>t.getRouteMetadata().map(t=>({...t,...typeof t.path==`string`?{path:e+t.path}:{path:t.path,pathExample:e+C(t)}})));return[...this.routes,...e]}enableSampling(e=`redacted`,t,n=new Set){if(!n.has(this)){n.add(this),this.sampleMode=e,this.scheduleSpecWrite=t;for(let{router:r}of this.mountedRouters)r.enableSampling(e,t,n)}}trackMounted(e,t){this.mountedRouters.some(n=>n.router===t&&n.prefix===e)||(this.mountedRouters.push({prefix:e,router:t}),this.sampleMode!==`off`&&t.enableSampling(this.sampleMode,this.scheduleSpecWrite))}hydrateResponses(e,t=``,n=new Set){if(n.has(this))return;n.add(this);let r=e?.paths??{};for(let e of this.routes)for(let n of D(e)){let i=r[O(t+n)]?.[e.method]?.responses;if(i)for(let t of Object.keys(i)){let n=Number(t);if(Number.isNaN(n)||e.responseSamples.has(n))continue;let r=i[t]?.content?.[`application/json`];r?.schema&&e.responseSamples.set(n,r.example===void 0?{schema:r.schema}:{schema:r.schema,example:r.example})}}for(let{prefix:r,router:i}of this.mountedRouters)i.hydrateResponses(e,t+r,n)}docs(e={}){let t=c.default.Router(),n;if(e.specOutputPath){let t=e.specOutputPath,r=!1,i=!1,a=async()=>{if(r){i=!0;return}r=!0;try{let n=await U(this.getRouteMetadata(),e),r=await g(`fs/promises`),i=t.replace(/[/\\][^/\\]*$/,``);i&&i!==t&&await r.mkdir(i,{recursive:!0}).catch(()=>{});let a=globalThis.process?.pid??`0`,o=`${t}.${a}.tmp`;await r.writeFile(o,JSON.stringify(n,null,2),`utf8`),await r.rename(o,t)}catch{}finally{r=!1,i&&(i=!1,a())}},o;n=()=>{o&&clearTimeout(o),o=setTimeout(a,300),o.unref?.()},setImmediate(async()=>{try{let e=await(await g(`fs/promises`)).readFile(t,`utf8`).catch(()=>null);if(e)try{this.hydrateResponses(JSON.parse(e))}catch{}}catch{}await a()})}return e.sampleResponses!==!1&&this.enableSampling(e.sampleResponses===`live`?`live`:`redacted`,n),t.get(`/openapi.json`,async(t,n)=>{try{let t=await U(this.getRouteMetadata(),e);n.json(t)}catch(e){n.status(500).json({error:`Failed to generate spec`,details:String(e)})}}),t.get(`/`,(t,n)=>{let r=`${t.baseUrl}/openapi.json`,i=e.cdnUrl??V;n.setHeader(`Content-Type`,`text/html; charset=utf-8`),n.send(H(e.title??`API`,r,i))}),t}get(e,t,n){return this.registerRoute(`get`,e,t,n)}post(e,t,n){return this.registerRoute(`post`,e,t,n)}put(e,t,n){return this.registerRoute(`put`,e,t,n)}patch(e,t,n){return this.registerRoute(`patch`,e,t,n)}delete(e,t,n){return this.registerRoute(`delete`,e,t,n)}options(e,t,n){return this.registerRoute(`options`,e,t,n)}head(e,t,n){return this.registerRoute(`head`,e,t,n)}all(e,t,n){return this.registerRoute(`all`,e,t,n)}registerRoute(e,t,n,r){let i=[],a={method:e,path:t,responseSamples:new Map};if(this.routes.push(a),typeof n==`object`){let o=n;a.bodySchema=o.bodySchema,a.querySchema=o.querySchema,a.paramsSchema=o.paramsSchema,a.tags=o.tags,a.description=o.description,a.summary=o.summary,a.deprecated=o.deprecated,a.responseSchema=o.responseSchema,a.hidden=o.hidden,a.pathExample=o.pathExample,o.middleware&&i.push(...o.middleware);let s=o.hooks?.onValidationFailure,c=o.hooks?.onBodyValidationFailure,l=o.hooks?.onQueryValidationFailure,u=o.hooks?.onParamsValidationFailure;o.bodySchema&&i.push(this.createBodyValidationMiddleware(o.bodySchema,e,t,o.pathExample,s,c)),o.querySchema&&i.push(this.createQueryValidationMiddleware(o.querySchema,e,t,o.pathExample,s,l)),o.paramsSchema&&i.push(this.createParamsValidationMiddleware(o.paramsSchema,e,t,o.pathExample,s,u)),i.push(r)}else i.push(n);let o=0,s=(e,t)=>{let n=e.statusCode,r=F(t),i=a.responseSamples.get(n),o=i?z(i.schema,r):r,s=this.sampleMode===`live`?i?.example??t:void 0,c=!i||JSON.stringify(i.schema)!==JSON.stringify(o);a.responseSamples.set(n,{schema:o,example:s}),c&&this.scheduleSpecWrite?.()};return this.router[e](S(t),(e,t,n)=>{if(this.sampleMode===`off`||a.hidden||o>=50||a.responseSamples.size>=10){n();return}o++;let r=t.json;t.json=function(e){return s(t,e),r.call(this,e)};let i=t.send;t.send=function(e){return typeof e==`object`&&e&&!Buffer.isBuffer(e)&&s(t,e),i.call(this,e)},n()},...i),this}notifyValidationFailure(e,t,n,r,i,a,o){if(!o&&!this.globalValidationFailureHook)return;let s={source:e,error:t,details:n,method:r,path:i,req:a};h(o,s),h(this.globalValidationFailureHook,s)}async runSchemaValidationHook(e,t,n,r,i,a,o,s){if(!e)return!1;let c=!1,l={error:t,details:n,method:r,path:i,req:a,res:o,next:((...e)=>{c=!0,s(...e)})};try{await e(l)}catch{}return c||o.headersSent}createBodyValidationMiddleware(e,t,n,r,i,a){return async(o,s,c)=>{try{let l=d(e,o.body),u=l&&typeof l.then==`function`?await l:l;if(u&&`issues`in u&&u.issues){let e=u.errors||u.issues,l=C({path:n,pathExample:r}),d=await this.runSchemaValidationHook(a,`Validation failed`,e,t,l,o,s,c);this.notifyValidationFailure(`body`,`Validation failed`,e,t,l,o,i),d||m({res:s,details:e});return}o.body=u&&`value`in u?u.value:u,c()}catch(e){if(f(e)){let l=e.errors||e.issues,u=C({path:n,pathExample:r}),d=await this.runSchemaValidationHook(a,`Validation failed`,l,t,u,o,s,c);this.notifyValidationFailure(`body`,`Validation failed`,l,t,u,o,i),d||m({res:s,details:l})}else c(e)}}}createParamsValidationMiddleware(e,t,n,r,i,a){return async(o,s,c)=>{try{let l=d(e,o.params),u=l&&typeof l.then==`function`?await l:l;if(u&&`issues`in u&&u.issues){let e=u.errors||u.issues,l=C({path:n,pathExample:r}),d=await this.runSchemaValidationHook(a,`Validation failed`,e,t,l,o,s,c);this.notifyValidationFailure(`params`,`Validation failed`,e,t,l,o,i),d||m({res:s,details:e});return}o.params=u&&`value`in u?u.value:u,c()}catch(e){if(f(e)){let l=e.errors||e.issues,u=C({path:n,pathExample:r}),d=await this.runSchemaValidationHook(a,`Validation failed`,l,t,u,o,s,c);this.notifyValidationFailure(`params`,`Validation failed`,l,t,u,o,i),d||m({res:s,details:l})}else c(e)}}}createQueryValidationMiddleware(e,t,n,r,i,a){return async(o,s,c)=>{try{let l=d(e,o.query),u=l&&typeof l.then==`function`?await l:l;if(u&&`issues`in u&&u.issues){let e=u.errors||u.issues,l=C({path:n,pathExample:r}),d=await this.runSchemaValidationHook(a,`Validation failed`,e,t,l,o,s,c);this.notifyValidationFailure(`query`,`Validation failed`,e,t,l,o,i),d||m({res:s,details:e});return}let f=u&&`value`in u?u.value:u;Object.defineProperty(o,"query",{value:f,writable:!1,enumerable:!0,configurable:!0}),c()}catch(e){if(f(e)){let l=e.errors||e.issues,u=C({path:n,pathExample:r}),d=await this.runSchemaValidationHook(a,`Validation failed`,l,t,u,o,s,c);this.notifyValidationFailure(`query`,`Validation failed`,l,t,u,o,i),d||m({res:s,details:l})}else c(e)}}}};function K(){return new G}function q(e){let t=new G;return e?.errorHandler&&t.getRouter().use(e.errorHandler),e?.hooks?.onValidationFailure&&t.onValidationFailure(e.hooks.onValidationFailure),t}function J(...e){let t=new G;for(let n of e)t=t.useMiddleware(n);return t}function Y(e){return(Array.isArray(e)?e:[e]).map(e=>`prefix`in e?e:{prefix:``,router:e}).flatMap(({prefix:e,router:t})=>t.getRouteMetadata().map(t=>({...t,...typeof t.path==`string`?{path:e+t.path}:{path:t.path,pathExample:e+C(t)}})))}async function X(e,t={}){return U(Y(e),t)}function Z(e,t={}){if(t.sampleResponses!==!1){let n=t.sampleResponses===`live`?`live`:`redacted`,r=Array.isArray(e)?e:[e];for(let e of r)(`prefix`in e?e.router:e).enableSampling(n)}let n=c.default.Router();return n.get(`/openapi.json`,async(n,r)=>{try{let n=await X(e,t);r.json(n)}catch(e){r.status(500).json({error:`Failed to generate spec`,details:String(e)})}}),n.get(`/`,(e,n)=>{let r=`${e.baseUrl}/openapi.json`,i=t.cdnUrl??V;n.setHeader(`Content-Type`,`text/html; charset=utf-8`),n.send(H(t.title??`API`,r,i))}),n}exports.TypedRouter=G,exports.createDocs=Z,exports.createTypedRouter=K,exports.createTypedRouterWithConfig=q,exports.createTypedRouterWithMiddleware=J,exports.defaultValidationHandler=m,exports.defineMiddleware=p,exports.generateOpenApiSpec=X,exports.inferJsonSchema=F,exports.isSchemaError=f,exports.parseSchema=u,exports.safeParseSchema=d;
@@ -112,6 +112,18 @@ type RequestOnlyMiddleware<TReq extends Record<string, any>> = TypedMiddleware<T
112
112
  * Simplified TypedMiddleware for response locals-only extensions
113
113
  */
114
114
  type LocalsOnlyMiddleware<TLocals extends Record<string, any>> = TypedMiddleware<{}, TLocals>;
115
+ /**
116
+ * Assigning a middleware array to a variable widens it to
117
+ * TypedMiddleware<any, any>[], losing the per-middleware types that
118
+ * InferSchemaHandler needs. The usual fix is `as const` on the array;
119
+ * defineMiddleware does the same thing without it, since the `const` type
120
+ * parameter keeps each argument's specific type instead of widening.
121
+ *
122
+ * @example
123
+ * const middleware = defineMiddleware(auth, logging);
124
+ * type Handler = InferSchemaHandler<{ middleware: typeof middleware }>;
125
+ */
126
+ declare function defineMiddleware<const M extends readonly TypedMiddleware<any, any>[]>(...mw: M): [...M];
115
127
  type InferMiddlewareProps<T extends readonly TypedMiddleware<any, any>[]> = T extends readonly [infer First, ...infer Rest] ? First extends TypedMiddleware<infer FirstReq, any> ? Rest extends readonly TypedMiddleware<any, any>[] ? FirstReq & InferMiddlewareProps<Rest> : FirstReq : {} : {};
116
128
  type InferMiddlewareLocals<T extends readonly TypedMiddleware<any, any>[]> = T extends readonly [infer First, ...infer Rest] ? First extends TypedMiddleware<any, infer FirstLocals> ? Rest extends readonly TypedMiddleware<any, any>[] ? FirstLocals & InferMiddlewareLocals<Rest> : FirstLocals : {} : {};
117
129
  type SchemaRequest<Path extends string = string, BodySchema extends SchemaLike | undefined = undefined, QuerySchema extends SchemaLike | undefined = undefined, MiddlewareProps extends Record<string, any> = {}, ParamsSchema extends SchemaLike | undefined = undefined, ParamsOverride extends Record<string, any> | undefined = undefined> = Omit<Request, "params" | "query" | "body"> & {
@@ -131,6 +143,112 @@ type SchemaRouteHandler<Path extends string = string, BodySchema extends SchemaL
131
143
  * @property paramsSchema - Optional schema for validating route params.
132
144
  * @property middleware - Optional array of TypedMiddleware for this route.
133
145
  */
146
+ /** Which validated part of the request failed. */
147
+ type ValidationFailureSource = "body" | "query" | "params";
148
+ /**
149
+ * Passed to a validation failure hook when bodySchema/querySchema/paramsSchema
150
+ * rejects a request. `req` includes whatever `.useMiddleware()` added, when
151
+ * set via `router.onValidationFailure()` (that method's `Req` type parameter
152
+ * is the router's own, already widened by any `.useMiddleware()` calls
153
+ * before it in the chain). Not the case for `createTypedRouterWithConfig`'s
154
+ * `hooks.onValidationFailure`, called before any middleware exists.
155
+ */
156
+ interface ValidationFailureInfo<MiddlewareProps extends Record<string, any> = {}> {
157
+ source: ValidationFailureSource;
158
+ error: string;
159
+ details: any[];
160
+ method: HttpMethod;
161
+ path: string;
162
+ req: Request & MiddlewareProps;
163
+ }
164
+ /**
165
+ * Called when validation fails, in addition to the 400 response that's
166
+ * already sent, for side effects like metrics/logging/alerting, not for
167
+ * changing the response (see the schema-specific hooks in RouteHooks for
168
+ * that). May be async (e.g. to log to an external service); the response is sent
169
+ * without waiting for it either way. Both a synchronous throw and a rejected
170
+ * promise are caught and ignored, so a broken hook can't take down request
171
+ * handling or surface as an unhandled rejection.
172
+ */
173
+ type ValidationFailureHook<MiddlewareProps extends Record<string, any> = {}> = (info: ValidationFailureInfo<MiddlewareProps>) => void | Promise<void>;
174
+ type SchemaValidationDetails<S> = S extends AnyStandardSchema ? readonly StandardSchemaV1.Issue[] : any[];
175
+ /**
176
+ * Passed to a route's onValidationFailure. A discriminated union on `source`
177
+ * (unlike {@link ValidationFailureInfo}, which is only ever `any[]`): narrow
178
+ * on `source` and `details` is that schema's own issue shape.
179
+ */
180
+ type RouteValidationFailureInfo<BodySchema extends SchemaLike | undefined = undefined, QuerySchema extends SchemaLike | undefined = undefined, ParamsSchema extends SchemaLike | undefined = undefined, MiddlewareProps extends Record<string, any> = {}> = {
181
+ source: "body";
182
+ error: string;
183
+ details: SchemaValidationDetails<BodySchema>;
184
+ method: HttpMethod;
185
+ path: string;
186
+ req: Request & MiddlewareProps;
187
+ } | {
188
+ source: "query";
189
+ error: string;
190
+ details: SchemaValidationDetails<QuerySchema>;
191
+ method: HttpMethod;
192
+ path: string;
193
+ req: Request & MiddlewareProps;
194
+ } | {
195
+ source: "params";
196
+ error: string;
197
+ details: SchemaValidationDetails<ParamsSchema>;
198
+ method: HttpMethod;
199
+ path: string;
200
+ req: Request & MiddlewareProps;
201
+ };
202
+ type RouteValidationFailureHook<BodySchema extends SchemaLike | undefined = undefined, QuerySchema extends SchemaLike | undefined = undefined, ParamsSchema extends SchemaLike | undefined = undefined, MiddlewareProps extends Record<string, any> = {}> = (info: RouteValidationFailureInfo<BodySchema, QuerySchema, ParamsSchema, MiddlewareProps>) => void | Promise<void>;
203
+ /**
204
+ * Passed to a schema-specific validation failure hook. Do nothing and the
205
+ * default 400 still fires. Call `next()` to continue with the raw,
206
+ * unvalidated value, or respond via `res` yourself (e.g. a custom status).
207
+ * `req` includes whatever the route's own `middleware` option adds, same as
208
+ * the handler's `req`.
209
+ */
210
+ interface SchemaValidationFailureInfo<S extends SchemaLike | undefined, MiddlewareProps extends Record<string, any> = {}> {
211
+ error: string;
212
+ details: SchemaValidationDetails<S>;
213
+ method: HttpMethod;
214
+ path: string;
215
+ req: Request & MiddlewareProps;
216
+ res: Response;
217
+ next: NextFunction;
218
+ }
219
+ type SchemaValidationFailureHook<S extends SchemaLike | undefined, MiddlewareProps extends Record<string, any> = {}> = (info: SchemaValidationFailureInfo<S, MiddlewareProps>) => void | Promise<void>;
220
+ /** The default 400 response for a validation failure. Call it from a schema-specific hook to log first and still fall back to the normal response. */
221
+ declare function defaultValidationHandler(info: {
222
+ res: Response;
223
+ details: unknown;
224
+ }): void;
225
+ /**
226
+ * Router-wide validation failure hook. Schema-specific hooks aren't
227
+ * available here: a router spans many routes, each with its own (possibly
228
+ * different) bodySchema/querySchema/paramsSchema, so there's no single
229
+ * schema to type this against.
230
+ */
231
+ interface RouterHooks<Req extends Record<string, any> = {}> {
232
+ /** Runs for every route on this router, after any per-route hooks below. */
233
+ onValidationFailure?: ValidationFailureHook<Req>;
234
+ }
235
+ /**
236
+ * Per-route lifecycle hooks, grouped under one `hooks` option so adding a
237
+ * future hook doesn't grow the flat option list. `onBodyValidationFailure`/
238
+ * `onQueryValidationFailure`/`onParamsValidationFailure` are typed to that
239
+ * route's own schema; `onValidationFailure` covers all three sources with
240
+ * `details: any[]`, for narrowing on `source` yourself instead.
241
+ */
242
+ interface RouteHooks<BodySchema extends SchemaLike | undefined = undefined, QuerySchema extends SchemaLike | undefined = undefined, ParamsSchema extends SchemaLike | undefined = undefined, MiddlewareProps extends Record<string, any> = {}> {
243
+ /** Runs when bodySchema, querySchema, or paramsSchema rejects the request. Fires after the schema-specific hook for that one, before the router's global hook. Can't change the response, use the schema-specific hooks for that; good for side effects (metrics, logging, alerts) across all three sources at once. `details` is typed to the matching schema once you narrow on `source`; `req` includes this route's `middleware` props, same as the schema-specific hooks below. */
244
+ onValidationFailure?: RouteValidationFailureHook<BodySchema, QuerySchema, ParamsSchema, MiddlewareProps>;
245
+ /** Runs when bodySchema rejects. `details` typed to bodySchema, `req` includes this route's `middleware` props. Do nothing for the default 400, call `next()` to continue with the raw body, or respond via `res` yourself. */
246
+ onBodyValidationFailure?: SchemaValidationFailureHook<BodySchema, MiddlewareProps>;
247
+ /** Runs when querySchema rejects. `details` typed to querySchema, `req` includes this route's `middleware` props. Do nothing for the default 400, call `next()` to continue with the raw query, or respond via `res` yourself. */
248
+ onQueryValidationFailure?: SchemaValidationFailureHook<QuerySchema, MiddlewareProps>;
249
+ /** Runs when paramsSchema rejects. `details` typed to paramsSchema, `req` includes this route's `middleware` props. Do nothing for the default 400, call `next()` to continue with the raw params, or respond via `res` yourself. */
250
+ onParamsValidationFailure?: SchemaValidationFailureHook<ParamsSchema, MiddlewareProps>;
251
+ }
134
252
  interface RouteOptions<BodySchema extends SchemaLike | undefined = undefined, QuerySchema extends SchemaLike | undefined = undefined, ParamsSchema extends SchemaLike | undefined = undefined> {
135
253
  bodySchema?: BodySchema;
136
254
  /**
@@ -147,6 +265,8 @@ interface RouteOptions<BodySchema extends SchemaLike | undefined = undefined, Qu
147
265
  * string-arrival caveat as `querySchema` applies; see its docs above.
148
266
  */
149
267
  paramsSchema?: ParamsSchema;
268
+ /** hooks.onValidationFailure runs in addition to the router's global hook (set via createTypedRouterWithConfig or router.onValidationFailure()), if both are set. */
269
+ hooks?: RouteHooks<BodySchema, QuerySchema, ParamsSchema>;
150
270
  middleware?: TypedMiddleware<any, any>[];
151
271
  tags?: string[];
152
272
  description?: string;
@@ -297,7 +417,21 @@ declare class TypedRouter<Req extends Record<string, any> = AdditionalReqProps,
297
417
  private mountedRouters;
298
418
  private sampleMode;
299
419
  private scheduleSpecWrite?;
420
+ private globalValidationFailureHook?;
300
421
  constructor();
422
+ /**
423
+ * Set the global validation failure hook. Called for every route
424
+ * registered directly on this router when bodySchema/querySchema/
425
+ * paramsSchema rejects a request. Works the same whether the router came
426
+ * from createTypedRouter() or createTypedRouterWithConfig({ onValidationFailure }),
427
+ * so you're not locked into the config-taking factory just to add this later.
428
+ *
429
+ * @example
430
+ * const router = createTypedRouter().onValidationFailure((info) => {
431
+ * logger.warn(info, 'request validation failed');
432
+ * });
433
+ */
434
+ onValidationFailure(hook: ValidationFailureHook<Req>): TypedRouter<Req, Locals>;
301
435
  /**
302
436
  * Add typed middleware that extends the request with additional properties
303
437
  * and/or adds properties to response.locals
@@ -391,6 +525,7 @@ declare class TypedRouter<Req extends Record<string, any> = AdditionalReqProps,
391
525
  bodySchema?: BodySchema;
392
526
  querySchema?: QuerySchema;
393
527
  paramsSchema?: ParamsSchema;
528
+ hooks?: RouteHooks<BodySchema, QuerySchema, ParamsSchema, InferMiddlewareProps<[...M]>>;
394
529
  middleware?: [...M];
395
530
  }, handler: RouteHandlerFromOptions<Path, BodySchema, QuerySchema, ParamsSchema, Req, Locals, M>): TypedRouter<Req, Locals>;
396
531
  get(path: RegExp, handler: SchemaRouteHandler<string, undefined, undefined, Req, Locals>): TypedRouter<Req, Locals>;
@@ -398,6 +533,7 @@ declare class TypedRouter<Req extends Record<string, any> = AdditionalReqProps,
398
533
  bodySchema?: BodySchema;
399
534
  querySchema?: QuerySchema;
400
535
  paramsSchema?: ParamsSchema;
536
+ hooks?: RouteHooks<BodySchema, QuerySchema, ParamsSchema, InferMiddlewareProps<[...M]>>;
401
537
  middleware?: [...M];
402
538
  }, handler: RouteHandlerFromOptions<string, BodySchema, QuerySchema, ParamsSchema, Req, Locals, M>): TypedRouter<Req, Locals>;
403
539
  post<Path extends string>(path: Path, handler: SchemaRouteHandler<Path, undefined, undefined, Req, Locals>): TypedRouter<Req, Locals>;
@@ -405,6 +541,7 @@ declare class TypedRouter<Req extends Record<string, any> = AdditionalReqProps,
405
541
  bodySchema?: BodySchema;
406
542
  querySchema?: QuerySchema;
407
543
  paramsSchema?: ParamsSchema;
544
+ hooks?: RouteHooks<BodySchema, QuerySchema, ParamsSchema, InferMiddlewareProps<[...M]>>;
408
545
  middleware?: [...M];
409
546
  }, handler: RouteHandlerFromOptions<Path, BodySchema, QuerySchema, ParamsSchema, Req, Locals, M>): TypedRouter<Req, Locals>;
410
547
  post(path: RegExp, handler: SchemaRouteHandler<string, undefined, undefined, Req, Locals>): TypedRouter<Req, Locals>;
@@ -412,6 +549,7 @@ declare class TypedRouter<Req extends Record<string, any> = AdditionalReqProps,
412
549
  bodySchema?: BodySchema;
413
550
  querySchema?: QuerySchema;
414
551
  paramsSchema?: ParamsSchema;
552
+ hooks?: RouteHooks<BodySchema, QuerySchema, ParamsSchema, InferMiddlewareProps<[...M]>>;
415
553
  middleware?: [...M];
416
554
  }, handler: RouteHandlerFromOptions<string, BodySchema, QuerySchema, ParamsSchema, Req, Locals, M>): TypedRouter<Req, Locals>;
417
555
  put<Path extends string>(path: Path, handler: SchemaRouteHandler<Path, undefined, undefined, Req, Locals>): TypedRouter<Req, Locals>;
@@ -419,6 +557,7 @@ declare class TypedRouter<Req extends Record<string, any> = AdditionalReqProps,
419
557
  bodySchema?: BodySchema;
420
558
  querySchema?: QuerySchema;
421
559
  paramsSchema?: ParamsSchema;
560
+ hooks?: RouteHooks<BodySchema, QuerySchema, ParamsSchema, InferMiddlewareProps<[...M]>>;
422
561
  middleware?: [...M];
423
562
  }, handler: RouteHandlerFromOptions<Path, BodySchema, QuerySchema, ParamsSchema, Req, Locals, M>): TypedRouter<Req, Locals>;
424
563
  put(path: RegExp, handler: SchemaRouteHandler<string, undefined, undefined, Req, Locals>): TypedRouter<Req, Locals>;
@@ -426,6 +565,7 @@ declare class TypedRouter<Req extends Record<string, any> = AdditionalReqProps,
426
565
  bodySchema?: BodySchema;
427
566
  querySchema?: QuerySchema;
428
567
  paramsSchema?: ParamsSchema;
568
+ hooks?: RouteHooks<BodySchema, QuerySchema, ParamsSchema, InferMiddlewareProps<[...M]>>;
429
569
  middleware?: [...M];
430
570
  }, handler: RouteHandlerFromOptions<string, BodySchema, QuerySchema, ParamsSchema, Req, Locals, M>): TypedRouter<Req, Locals>;
431
571
  patch<Path extends string>(path: Path, handler: SchemaRouteHandler<Path, undefined, undefined, Req, Locals>): TypedRouter<Req, Locals>;
@@ -433,6 +573,7 @@ declare class TypedRouter<Req extends Record<string, any> = AdditionalReqProps,
433
573
  bodySchema?: BodySchema;
434
574
  querySchema?: QuerySchema;
435
575
  paramsSchema?: ParamsSchema;
576
+ hooks?: RouteHooks<BodySchema, QuerySchema, ParamsSchema, InferMiddlewareProps<[...M]>>;
436
577
  middleware?: [...M];
437
578
  }, handler: RouteHandlerFromOptions<Path, BodySchema, QuerySchema, ParamsSchema, Req, Locals, M>): TypedRouter<Req, Locals>;
438
579
  patch(path: RegExp, handler: SchemaRouteHandler<string, undefined, undefined, Req, Locals>): TypedRouter<Req, Locals>;
@@ -440,42 +581,49 @@ declare class TypedRouter<Req extends Record<string, any> = AdditionalReqProps,
440
581
  bodySchema?: BodySchema;
441
582
  querySchema?: QuerySchema;
442
583
  paramsSchema?: ParamsSchema;
584
+ hooks?: RouteHooks<BodySchema, QuerySchema, ParamsSchema, InferMiddlewareProps<[...M]>>;
443
585
  middleware?: [...M];
444
586
  }, handler: RouteHandlerFromOptions<string, BodySchema, QuerySchema, ParamsSchema, Req, Locals, M>): TypedRouter<Req, Locals>;
445
587
  delete<Path extends string>(path: Path, handler: SchemaRouteHandler<Path, undefined, undefined, Req, Locals>): TypedRouter<Req, Locals>;
446
588
  delete<Path extends string, QuerySchema extends SchemaLike | undefined = undefined, ParamsSchema extends SchemaLike | undefined = undefined, M extends TypedMiddleware<any, any>[] = []>(path: Path, options: DocMeta & {
447
589
  querySchema?: QuerySchema;
448
590
  paramsSchema?: ParamsSchema;
591
+ hooks?: RouteHooks<undefined, QuerySchema, ParamsSchema, InferMiddlewareProps<[...M]>>;
449
592
  middleware?: [...M];
450
593
  }, handler: RouteHandlerFromOptions<Path, undefined, QuerySchema, ParamsSchema, Req, Locals, M>): TypedRouter<Req, Locals>;
451
594
  delete(path: RegExp, handler: SchemaRouteHandler<string, undefined, undefined, Req, Locals>): TypedRouter<Req, Locals>;
452
595
  delete<QuerySchema extends SchemaLike | undefined = undefined, ParamsSchema extends SchemaLike | undefined = undefined, M extends TypedMiddleware<any, any>[] = []>(path: RegExp, options: DocMeta & {
453
596
  querySchema?: QuerySchema;
454
597
  paramsSchema?: ParamsSchema;
598
+ hooks?: RouteHooks<undefined, QuerySchema, ParamsSchema, InferMiddlewareProps<[...M]>>;
455
599
  middleware?: [...M];
456
600
  }, handler: RouteHandlerFromOptions<string, undefined, QuerySchema, ParamsSchema, Req, Locals, M>): TypedRouter<Req, Locals>;
457
601
  options<Path extends string>(path: Path, handler: SchemaRouteHandler<Path, undefined, undefined, Req, Locals>): TypedRouter<Req, Locals>;
458
602
  options<Path extends string, QuerySchema extends SchemaLike | undefined = undefined, ParamsSchema extends SchemaLike | undefined = undefined, M extends TypedMiddleware<any, any>[] = []>(path: Path, options: DocMeta & {
459
603
  querySchema?: QuerySchema;
460
604
  paramsSchema?: ParamsSchema;
605
+ hooks?: RouteHooks<undefined, QuerySchema, ParamsSchema, InferMiddlewareProps<[...M]>>;
461
606
  middleware?: [...M];
462
607
  }, handler: RouteHandlerFromOptions<Path, undefined, QuerySchema, ParamsSchema, Req, Locals, M>): TypedRouter<Req, Locals>;
463
608
  options(path: RegExp, handler: SchemaRouteHandler<string, undefined, undefined, Req, Locals>): TypedRouter<Req, Locals>;
464
609
  options<QuerySchema extends SchemaLike | undefined = undefined, ParamsSchema extends SchemaLike | undefined = undefined, M extends TypedMiddleware<any, any>[] = []>(path: RegExp, options: DocMeta & {
465
610
  querySchema?: QuerySchema;
466
611
  paramsSchema?: ParamsSchema;
612
+ hooks?: RouteHooks<undefined, QuerySchema, ParamsSchema, InferMiddlewareProps<[...M]>>;
467
613
  middleware?: [...M];
468
614
  }, handler: RouteHandlerFromOptions<string, undefined, QuerySchema, ParamsSchema, Req, Locals, M>): TypedRouter<Req, Locals>;
469
615
  head<Path extends string>(path: Path, handler: SchemaRouteHandler<Path, undefined, undefined, Req, Locals>): TypedRouter<Req, Locals>;
470
616
  head<Path extends string, QuerySchema extends SchemaLike | undefined = undefined, ParamsSchema extends SchemaLike | undefined = undefined, M extends TypedMiddleware<any, any>[] = []>(path: Path, options: DocMeta & {
471
617
  querySchema?: QuerySchema;
472
618
  paramsSchema?: ParamsSchema;
619
+ hooks?: RouteHooks<undefined, QuerySchema, ParamsSchema, InferMiddlewareProps<[...M]>>;
473
620
  middleware?: [...M];
474
621
  }, handler: RouteHandlerFromOptions<Path, undefined, QuerySchema, ParamsSchema, Req, Locals, M>): TypedRouter<Req, Locals>;
475
622
  head(path: RegExp, handler: SchemaRouteHandler<string, undefined, undefined, Req, Locals>): TypedRouter<Req, Locals>;
476
623
  head<QuerySchema extends SchemaLike | undefined = undefined, ParamsSchema extends SchemaLike | undefined = undefined, M extends TypedMiddleware<any, any>[] = []>(path: RegExp, options: DocMeta & {
477
624
  querySchema?: QuerySchema;
478
625
  paramsSchema?: ParamsSchema;
626
+ hooks?: RouteHooks<undefined, QuerySchema, ParamsSchema, InferMiddlewareProps<[...M]>>;
479
627
  middleware?: [...M];
480
628
  }, handler: RouteHandlerFromOptions<string, undefined, QuerySchema, ParamsSchema, Req, Locals, M>): TypedRouter<Req, Locals>;
481
629
  all<Path extends string>(path: Path, handler: SchemaRouteHandler<Path, undefined, undefined, Req, Locals>): TypedRouter<Req, Locals>;
@@ -483,6 +631,7 @@ declare class TypedRouter<Req extends Record<string, any> = AdditionalReqProps,
483
631
  bodySchema?: BodySchema;
484
632
  querySchema?: QuerySchema;
485
633
  paramsSchema?: ParamsSchema;
634
+ hooks?: RouteHooks<BodySchema, QuerySchema, ParamsSchema, InferMiddlewareProps<[...M]>>;
486
635
  middleware?: [...M];
487
636
  }, handler: RouteHandlerFromOptions<Path, BodySchema, QuerySchema, ParamsSchema, Req, Locals, M>): TypedRouter<Req, Locals>;
488
637
  all(path: RegExp, handler: SchemaRouteHandler<string, undefined, undefined, Req, Locals>): TypedRouter<Req, Locals>;
@@ -490,9 +639,12 @@ declare class TypedRouter<Req extends Record<string, any> = AdditionalReqProps,
490
639
  bodySchema?: BodySchema;
491
640
  querySchema?: QuerySchema;
492
641
  paramsSchema?: ParamsSchema;
642
+ hooks?: RouteHooks<BodySchema, QuerySchema, ParamsSchema, InferMiddlewareProps<[...M]>>;
493
643
  middleware?: [...M];
494
644
  }, handler: RouteHandlerFromOptions<string, BodySchema, QuerySchema, ParamsSchema, Req, Locals, M>): TypedRouter<Req, Locals>;
495
645
  private registerRoute;
646
+ private notifyValidationFailure;
647
+ private runSchemaValidationHook;
496
648
  private createBodyValidationMiddleware;
497
649
  private createParamsValidationMiddleware;
498
650
  private createQueryValidationMiddleware;
@@ -523,10 +675,12 @@ declare function createTypedRouter<Req extends Record<string, any> = AdditionalR
523
675
  *
524
676
  * @property validateInput - (Future) Whether to enable global input validation.
525
677
  * @property errorHandler - Optional global error handler middleware for the router.
678
+ * @property hooks.onValidationFailure - Called for every validation failure on this router, in addition to any per-route hook.
526
679
  */
527
- interface RouterConfig {
680
+ interface RouterConfig<Req extends Record<string, any> = {}> {
528
681
  validateInput?: boolean;
529
682
  errorHandler?: (error: any, req: Request, res: Response, next: NextFunction) => void;
683
+ hooks?: RouterHooks<Req>;
530
684
  }
531
685
  /**
532
686
  * Create a new typed router with optional configuration.
@@ -545,7 +699,7 @@ interface RouterConfig {
545
699
  * }
546
700
  * });
547
701
  */
548
- declare function createTypedRouterWithConfig<Req extends Record<string, any> = AdditionalReqProps, Locals extends Record<string, any> = AdditionalLocals>(config?: RouterConfig): TypedRouter<Req, Locals>;
702
+ declare function createTypedRouterWithConfig<Req extends Record<string, any> = AdditionalReqProps, Locals extends Record<string, any> = AdditionalLocals>(config?: RouterConfig<Req>): TypedRouter<Req, Locals>;
549
703
  /**
550
704
  * Create a new typed router with pre-configured middleware.
551
705
  *
@@ -576,27 +730,16 @@ type RouterDocEntry = TypedRouter<any, any> | {
576
730
  router: TypedRouter<any, any>;
577
731
  };
578
732
  /**
579
- * Create a unified OpenAPI docs endpoint that merges routes from multiple
580
- * TypedRouter instances. Use this when routes are split across files.
733
+ * Build the OpenAPI spec object directly, without mounting an Express router
734
+ * or making an HTTP request. For generating openapi.json at build/CI time,
735
+ * separately from running the app. Accepts the same router(s) shape as
736
+ * createDocs.
581
737
  *
582
738
  * @example
583
- * // users.router.ts routes like /users, /users/:id
584
- * export const usersRouter = createTypedRouter();
585
- *
586
- * // auth.router.ts — routes like /login, /logout
587
- * export const authRouter = createTypedRouter();
588
- *
589
- * // app.ts
590
- * app.use('/api', usersRouter.getRouter());
591
- * app.use('/api', authRouter.getRouter());
592
- * app.use('/docs', createDocs(
593
- * [
594
- * { prefix: '/api', router: usersRouter },
595
- * { prefix: '/api', router: authRouter },
596
- * ],
597
- * { title: 'My API', version: '1.0.0' }
598
- * ));
739
+ * const spec = await generateOpenApiSpec(router, { title: 'My API' });
740
+ * await fs.writeFile('./openapi.json', JSON.stringify(spec, null, 2));
599
741
  */
742
+ declare function generateOpenApiSpec(routers: RouterDocEntry | RouterDocEntry[], options?: DocsOptions): Promise<Record<string, any>>;
600
743
  declare function createDocs(routers: RouterDocEntry | RouterDocEntry[], options?: DocsOptions): express.Router & express.RequestHandler;
601
744
  //#endregion
602
- export { AdditionalLocals, AdditionalReqProps, AnyStandardSchema, DocsOptions, ExtractRouteParams, HttpMethod, InferInput, InferOutput, InferSchemaHandler, InferSchemaHandlerOptions, InferSchemaOutput, LocalsOnlyMiddleware, RequestOnlyMiddleware, RouteOptions, RouterConfig, RouterDocEntry, SafeParseResult, SchemaLike, SchemaRequest, SchemaRouteHandler, TypedMiddleware, TypedRouter, createDocs, createTypedRouter, createTypedRouterWithConfig, createTypedRouterWithMiddleware, inferJsonSchema, isSchemaError, parseSchema, safeParseSchema };
745
+ export { AdditionalLocals, AdditionalReqProps, AnyStandardSchema, DocsOptions, ExtractRouteParams, HttpMethod, InferInput, InferOutput, InferSchemaHandler, InferSchemaHandlerOptions, InferSchemaOutput, LocalsOnlyMiddleware, RequestOnlyMiddleware, RouteHooks, RouteOptions, RouteValidationFailureHook, RouteValidationFailureInfo, RouterConfig, RouterDocEntry, RouterHooks, SafeParseResult, SchemaLike, SchemaRequest, SchemaRouteHandler, SchemaValidationFailureHook, SchemaValidationFailureInfo, TypedMiddleware, TypedRouter, ValidationFailureHook, ValidationFailureInfo, ValidationFailureSource, createDocs, createTypedRouter, createTypedRouterWithConfig, createTypedRouterWithMiddleware, defaultValidationHandler, defineMiddleware, generateOpenApiSpec, inferJsonSchema, isSchemaError, parseSchema, safeParseSchema };
@@ -112,6 +112,18 @@ type RequestOnlyMiddleware<TReq extends Record<string, any>> = TypedMiddleware<T
112
112
  * Simplified TypedMiddleware for response locals-only extensions
113
113
  */
114
114
  type LocalsOnlyMiddleware<TLocals extends Record<string, any>> = TypedMiddleware<{}, TLocals>;
115
+ /**
116
+ * Assigning a middleware array to a variable widens it to
117
+ * TypedMiddleware<any, any>[], losing the per-middleware types that
118
+ * InferSchemaHandler needs. The usual fix is `as const` on the array;
119
+ * defineMiddleware does the same thing without it, since the `const` type
120
+ * parameter keeps each argument's specific type instead of widening.
121
+ *
122
+ * @example
123
+ * const middleware = defineMiddleware(auth, logging);
124
+ * type Handler = InferSchemaHandler<{ middleware: typeof middleware }>;
125
+ */
126
+ declare function defineMiddleware<const M extends readonly TypedMiddleware<any, any>[]>(...mw: M): [...M];
115
127
  type InferMiddlewareProps<T extends readonly TypedMiddleware<any, any>[]> = T extends readonly [infer First, ...infer Rest] ? First extends TypedMiddleware<infer FirstReq, any> ? Rest extends readonly TypedMiddleware<any, any>[] ? FirstReq & InferMiddlewareProps<Rest> : FirstReq : {} : {};
116
128
  type InferMiddlewareLocals<T extends readonly TypedMiddleware<any, any>[]> = T extends readonly [infer First, ...infer Rest] ? First extends TypedMiddleware<any, infer FirstLocals> ? Rest extends readonly TypedMiddleware<any, any>[] ? FirstLocals & InferMiddlewareLocals<Rest> : FirstLocals : {} : {};
117
129
  type SchemaRequest<Path extends string = string, BodySchema extends SchemaLike | undefined = undefined, QuerySchema extends SchemaLike | undefined = undefined, MiddlewareProps extends Record<string, any> = {}, ParamsSchema extends SchemaLike | undefined = undefined, ParamsOverride extends Record<string, any> | undefined = undefined> = Omit<Request, "params" | "query" | "body"> & {
@@ -131,6 +143,112 @@ type SchemaRouteHandler<Path extends string = string, BodySchema extends SchemaL
131
143
  * @property paramsSchema - Optional schema for validating route params.
132
144
  * @property middleware - Optional array of TypedMiddleware for this route.
133
145
  */
146
+ /** Which validated part of the request failed. */
147
+ type ValidationFailureSource = "body" | "query" | "params";
148
+ /**
149
+ * Passed to a validation failure hook when bodySchema/querySchema/paramsSchema
150
+ * rejects a request. `req` includes whatever `.useMiddleware()` added, when
151
+ * set via `router.onValidationFailure()` (that method's `Req` type parameter
152
+ * is the router's own, already widened by any `.useMiddleware()` calls
153
+ * before it in the chain). Not the case for `createTypedRouterWithConfig`'s
154
+ * `hooks.onValidationFailure`, called before any middleware exists.
155
+ */
156
+ interface ValidationFailureInfo<MiddlewareProps extends Record<string, any> = {}> {
157
+ source: ValidationFailureSource;
158
+ error: string;
159
+ details: any[];
160
+ method: HttpMethod;
161
+ path: string;
162
+ req: Request & MiddlewareProps;
163
+ }
164
+ /**
165
+ * Called when validation fails, in addition to the 400 response that's
166
+ * already sent, for side effects like metrics/logging/alerting, not for
167
+ * changing the response (see the schema-specific hooks in RouteHooks for
168
+ * that). May be async (e.g. to log to an external service); the response is sent
169
+ * without waiting for it either way. Both a synchronous throw and a rejected
170
+ * promise are caught and ignored, so a broken hook can't take down request
171
+ * handling or surface as an unhandled rejection.
172
+ */
173
+ type ValidationFailureHook<MiddlewareProps extends Record<string, any> = {}> = (info: ValidationFailureInfo<MiddlewareProps>) => void | Promise<void>;
174
+ type SchemaValidationDetails<S> = S extends AnyStandardSchema ? readonly StandardSchemaV1.Issue[] : any[];
175
+ /**
176
+ * Passed to a route's onValidationFailure. A discriminated union on `source`
177
+ * (unlike {@link ValidationFailureInfo}, which is only ever `any[]`): narrow
178
+ * on `source` and `details` is that schema's own issue shape.
179
+ */
180
+ type RouteValidationFailureInfo<BodySchema extends SchemaLike | undefined = undefined, QuerySchema extends SchemaLike | undefined = undefined, ParamsSchema extends SchemaLike | undefined = undefined, MiddlewareProps extends Record<string, any> = {}> = {
181
+ source: "body";
182
+ error: string;
183
+ details: SchemaValidationDetails<BodySchema>;
184
+ method: HttpMethod;
185
+ path: string;
186
+ req: Request & MiddlewareProps;
187
+ } | {
188
+ source: "query";
189
+ error: string;
190
+ details: SchemaValidationDetails<QuerySchema>;
191
+ method: HttpMethod;
192
+ path: string;
193
+ req: Request & MiddlewareProps;
194
+ } | {
195
+ source: "params";
196
+ error: string;
197
+ details: SchemaValidationDetails<ParamsSchema>;
198
+ method: HttpMethod;
199
+ path: string;
200
+ req: Request & MiddlewareProps;
201
+ };
202
+ type RouteValidationFailureHook<BodySchema extends SchemaLike | undefined = undefined, QuerySchema extends SchemaLike | undefined = undefined, ParamsSchema extends SchemaLike | undefined = undefined, MiddlewareProps extends Record<string, any> = {}> = (info: RouteValidationFailureInfo<BodySchema, QuerySchema, ParamsSchema, MiddlewareProps>) => void | Promise<void>;
203
+ /**
204
+ * Passed to a schema-specific validation failure hook. Do nothing and the
205
+ * default 400 still fires. Call `next()` to continue with the raw,
206
+ * unvalidated value, or respond via `res` yourself (e.g. a custom status).
207
+ * `req` includes whatever the route's own `middleware` option adds, same as
208
+ * the handler's `req`.
209
+ */
210
+ interface SchemaValidationFailureInfo<S extends SchemaLike | undefined, MiddlewareProps extends Record<string, any> = {}> {
211
+ error: string;
212
+ details: SchemaValidationDetails<S>;
213
+ method: HttpMethod;
214
+ path: string;
215
+ req: Request & MiddlewareProps;
216
+ res: Response;
217
+ next: NextFunction;
218
+ }
219
+ type SchemaValidationFailureHook<S extends SchemaLike | undefined, MiddlewareProps extends Record<string, any> = {}> = (info: SchemaValidationFailureInfo<S, MiddlewareProps>) => void | Promise<void>;
220
+ /** The default 400 response for a validation failure. Call it from a schema-specific hook to log first and still fall back to the normal response. */
221
+ declare function defaultValidationHandler(info: {
222
+ res: Response;
223
+ details: unknown;
224
+ }): void;
225
+ /**
226
+ * Router-wide validation failure hook. Schema-specific hooks aren't
227
+ * available here: a router spans many routes, each with its own (possibly
228
+ * different) bodySchema/querySchema/paramsSchema, so there's no single
229
+ * schema to type this against.
230
+ */
231
+ interface RouterHooks<Req extends Record<string, any> = {}> {
232
+ /** Runs for every route on this router, after any per-route hooks below. */
233
+ onValidationFailure?: ValidationFailureHook<Req>;
234
+ }
235
+ /**
236
+ * Per-route lifecycle hooks, grouped under one `hooks` option so adding a
237
+ * future hook doesn't grow the flat option list. `onBodyValidationFailure`/
238
+ * `onQueryValidationFailure`/`onParamsValidationFailure` are typed to that
239
+ * route's own schema; `onValidationFailure` covers all three sources with
240
+ * `details: any[]`, for narrowing on `source` yourself instead.
241
+ */
242
+ interface RouteHooks<BodySchema extends SchemaLike | undefined = undefined, QuerySchema extends SchemaLike | undefined = undefined, ParamsSchema extends SchemaLike | undefined = undefined, MiddlewareProps extends Record<string, any> = {}> {
243
+ /** Runs when bodySchema, querySchema, or paramsSchema rejects the request. Fires after the schema-specific hook for that one, before the router's global hook. Can't change the response, use the schema-specific hooks for that; good for side effects (metrics, logging, alerts) across all three sources at once. `details` is typed to the matching schema once you narrow on `source`; `req` includes this route's `middleware` props, same as the schema-specific hooks below. */
244
+ onValidationFailure?: RouteValidationFailureHook<BodySchema, QuerySchema, ParamsSchema, MiddlewareProps>;
245
+ /** Runs when bodySchema rejects. `details` typed to bodySchema, `req` includes this route's `middleware` props. Do nothing for the default 400, call `next()` to continue with the raw body, or respond via `res` yourself. */
246
+ onBodyValidationFailure?: SchemaValidationFailureHook<BodySchema, MiddlewareProps>;
247
+ /** Runs when querySchema rejects. `details` typed to querySchema, `req` includes this route's `middleware` props. Do nothing for the default 400, call `next()` to continue with the raw query, or respond via `res` yourself. */
248
+ onQueryValidationFailure?: SchemaValidationFailureHook<QuerySchema, MiddlewareProps>;
249
+ /** Runs when paramsSchema rejects. `details` typed to paramsSchema, `req` includes this route's `middleware` props. Do nothing for the default 400, call `next()` to continue with the raw params, or respond via `res` yourself. */
250
+ onParamsValidationFailure?: SchemaValidationFailureHook<ParamsSchema, MiddlewareProps>;
251
+ }
134
252
  interface RouteOptions<BodySchema extends SchemaLike | undefined = undefined, QuerySchema extends SchemaLike | undefined = undefined, ParamsSchema extends SchemaLike | undefined = undefined> {
135
253
  bodySchema?: BodySchema;
136
254
  /**
@@ -147,6 +265,8 @@ interface RouteOptions<BodySchema extends SchemaLike | undefined = undefined, Qu
147
265
  * string-arrival caveat as `querySchema` applies; see its docs above.
148
266
  */
149
267
  paramsSchema?: ParamsSchema;
268
+ /** hooks.onValidationFailure runs in addition to the router's global hook (set via createTypedRouterWithConfig or router.onValidationFailure()), if both are set. */
269
+ hooks?: RouteHooks<BodySchema, QuerySchema, ParamsSchema>;
150
270
  middleware?: TypedMiddleware<any, any>[];
151
271
  tags?: string[];
152
272
  description?: string;
@@ -297,7 +417,21 @@ declare class TypedRouter<Req extends Record<string, any> = AdditionalReqProps,
297
417
  private mountedRouters;
298
418
  private sampleMode;
299
419
  private scheduleSpecWrite?;
420
+ private globalValidationFailureHook?;
300
421
  constructor();
422
+ /**
423
+ * Set the global validation failure hook. Called for every route
424
+ * registered directly on this router when bodySchema/querySchema/
425
+ * paramsSchema rejects a request. Works the same whether the router came
426
+ * from createTypedRouter() or createTypedRouterWithConfig({ onValidationFailure }),
427
+ * so you're not locked into the config-taking factory just to add this later.
428
+ *
429
+ * @example
430
+ * const router = createTypedRouter().onValidationFailure((info) => {
431
+ * logger.warn(info, 'request validation failed');
432
+ * });
433
+ */
434
+ onValidationFailure(hook: ValidationFailureHook<Req>): TypedRouter<Req, Locals>;
301
435
  /**
302
436
  * Add typed middleware that extends the request with additional properties
303
437
  * and/or adds properties to response.locals
@@ -391,6 +525,7 @@ declare class TypedRouter<Req extends Record<string, any> = AdditionalReqProps,
391
525
  bodySchema?: BodySchema;
392
526
  querySchema?: QuerySchema;
393
527
  paramsSchema?: ParamsSchema;
528
+ hooks?: RouteHooks<BodySchema, QuerySchema, ParamsSchema, InferMiddlewareProps<[...M]>>;
394
529
  middleware?: [...M];
395
530
  }, handler: RouteHandlerFromOptions<Path, BodySchema, QuerySchema, ParamsSchema, Req, Locals, M>): TypedRouter<Req, Locals>;
396
531
  get(path: RegExp, handler: SchemaRouteHandler<string, undefined, undefined, Req, Locals>): TypedRouter<Req, Locals>;
@@ -398,6 +533,7 @@ declare class TypedRouter<Req extends Record<string, any> = AdditionalReqProps,
398
533
  bodySchema?: BodySchema;
399
534
  querySchema?: QuerySchema;
400
535
  paramsSchema?: ParamsSchema;
536
+ hooks?: RouteHooks<BodySchema, QuerySchema, ParamsSchema, InferMiddlewareProps<[...M]>>;
401
537
  middleware?: [...M];
402
538
  }, handler: RouteHandlerFromOptions<string, BodySchema, QuerySchema, ParamsSchema, Req, Locals, M>): TypedRouter<Req, Locals>;
403
539
  post<Path extends string>(path: Path, handler: SchemaRouteHandler<Path, undefined, undefined, Req, Locals>): TypedRouter<Req, Locals>;
@@ -405,6 +541,7 @@ declare class TypedRouter<Req extends Record<string, any> = AdditionalReqProps,
405
541
  bodySchema?: BodySchema;
406
542
  querySchema?: QuerySchema;
407
543
  paramsSchema?: ParamsSchema;
544
+ hooks?: RouteHooks<BodySchema, QuerySchema, ParamsSchema, InferMiddlewareProps<[...M]>>;
408
545
  middleware?: [...M];
409
546
  }, handler: RouteHandlerFromOptions<Path, BodySchema, QuerySchema, ParamsSchema, Req, Locals, M>): TypedRouter<Req, Locals>;
410
547
  post(path: RegExp, handler: SchemaRouteHandler<string, undefined, undefined, Req, Locals>): TypedRouter<Req, Locals>;
@@ -412,6 +549,7 @@ declare class TypedRouter<Req extends Record<string, any> = AdditionalReqProps,
412
549
  bodySchema?: BodySchema;
413
550
  querySchema?: QuerySchema;
414
551
  paramsSchema?: ParamsSchema;
552
+ hooks?: RouteHooks<BodySchema, QuerySchema, ParamsSchema, InferMiddlewareProps<[...M]>>;
415
553
  middleware?: [...M];
416
554
  }, handler: RouteHandlerFromOptions<string, BodySchema, QuerySchema, ParamsSchema, Req, Locals, M>): TypedRouter<Req, Locals>;
417
555
  put<Path extends string>(path: Path, handler: SchemaRouteHandler<Path, undefined, undefined, Req, Locals>): TypedRouter<Req, Locals>;
@@ -419,6 +557,7 @@ declare class TypedRouter<Req extends Record<string, any> = AdditionalReqProps,
419
557
  bodySchema?: BodySchema;
420
558
  querySchema?: QuerySchema;
421
559
  paramsSchema?: ParamsSchema;
560
+ hooks?: RouteHooks<BodySchema, QuerySchema, ParamsSchema, InferMiddlewareProps<[...M]>>;
422
561
  middleware?: [...M];
423
562
  }, handler: RouteHandlerFromOptions<Path, BodySchema, QuerySchema, ParamsSchema, Req, Locals, M>): TypedRouter<Req, Locals>;
424
563
  put(path: RegExp, handler: SchemaRouteHandler<string, undefined, undefined, Req, Locals>): TypedRouter<Req, Locals>;
@@ -426,6 +565,7 @@ declare class TypedRouter<Req extends Record<string, any> = AdditionalReqProps,
426
565
  bodySchema?: BodySchema;
427
566
  querySchema?: QuerySchema;
428
567
  paramsSchema?: ParamsSchema;
568
+ hooks?: RouteHooks<BodySchema, QuerySchema, ParamsSchema, InferMiddlewareProps<[...M]>>;
429
569
  middleware?: [...M];
430
570
  }, handler: RouteHandlerFromOptions<string, BodySchema, QuerySchema, ParamsSchema, Req, Locals, M>): TypedRouter<Req, Locals>;
431
571
  patch<Path extends string>(path: Path, handler: SchemaRouteHandler<Path, undefined, undefined, Req, Locals>): TypedRouter<Req, Locals>;
@@ -433,6 +573,7 @@ declare class TypedRouter<Req extends Record<string, any> = AdditionalReqProps,
433
573
  bodySchema?: BodySchema;
434
574
  querySchema?: QuerySchema;
435
575
  paramsSchema?: ParamsSchema;
576
+ hooks?: RouteHooks<BodySchema, QuerySchema, ParamsSchema, InferMiddlewareProps<[...M]>>;
436
577
  middleware?: [...M];
437
578
  }, handler: RouteHandlerFromOptions<Path, BodySchema, QuerySchema, ParamsSchema, Req, Locals, M>): TypedRouter<Req, Locals>;
438
579
  patch(path: RegExp, handler: SchemaRouteHandler<string, undefined, undefined, Req, Locals>): TypedRouter<Req, Locals>;
@@ -440,42 +581,49 @@ declare class TypedRouter<Req extends Record<string, any> = AdditionalReqProps,
440
581
  bodySchema?: BodySchema;
441
582
  querySchema?: QuerySchema;
442
583
  paramsSchema?: ParamsSchema;
584
+ hooks?: RouteHooks<BodySchema, QuerySchema, ParamsSchema, InferMiddlewareProps<[...M]>>;
443
585
  middleware?: [...M];
444
586
  }, handler: RouteHandlerFromOptions<string, BodySchema, QuerySchema, ParamsSchema, Req, Locals, M>): TypedRouter<Req, Locals>;
445
587
  delete<Path extends string>(path: Path, handler: SchemaRouteHandler<Path, undefined, undefined, Req, Locals>): TypedRouter<Req, Locals>;
446
588
  delete<Path extends string, QuerySchema extends SchemaLike | undefined = undefined, ParamsSchema extends SchemaLike | undefined = undefined, M extends TypedMiddleware<any, any>[] = []>(path: Path, options: DocMeta & {
447
589
  querySchema?: QuerySchema;
448
590
  paramsSchema?: ParamsSchema;
591
+ hooks?: RouteHooks<undefined, QuerySchema, ParamsSchema, InferMiddlewareProps<[...M]>>;
449
592
  middleware?: [...M];
450
593
  }, handler: RouteHandlerFromOptions<Path, undefined, QuerySchema, ParamsSchema, Req, Locals, M>): TypedRouter<Req, Locals>;
451
594
  delete(path: RegExp, handler: SchemaRouteHandler<string, undefined, undefined, Req, Locals>): TypedRouter<Req, Locals>;
452
595
  delete<QuerySchema extends SchemaLike | undefined = undefined, ParamsSchema extends SchemaLike | undefined = undefined, M extends TypedMiddleware<any, any>[] = []>(path: RegExp, options: DocMeta & {
453
596
  querySchema?: QuerySchema;
454
597
  paramsSchema?: ParamsSchema;
598
+ hooks?: RouteHooks<undefined, QuerySchema, ParamsSchema, InferMiddlewareProps<[...M]>>;
455
599
  middleware?: [...M];
456
600
  }, handler: RouteHandlerFromOptions<string, undefined, QuerySchema, ParamsSchema, Req, Locals, M>): TypedRouter<Req, Locals>;
457
601
  options<Path extends string>(path: Path, handler: SchemaRouteHandler<Path, undefined, undefined, Req, Locals>): TypedRouter<Req, Locals>;
458
602
  options<Path extends string, QuerySchema extends SchemaLike | undefined = undefined, ParamsSchema extends SchemaLike | undefined = undefined, M extends TypedMiddleware<any, any>[] = []>(path: Path, options: DocMeta & {
459
603
  querySchema?: QuerySchema;
460
604
  paramsSchema?: ParamsSchema;
605
+ hooks?: RouteHooks<undefined, QuerySchema, ParamsSchema, InferMiddlewareProps<[...M]>>;
461
606
  middleware?: [...M];
462
607
  }, handler: RouteHandlerFromOptions<Path, undefined, QuerySchema, ParamsSchema, Req, Locals, M>): TypedRouter<Req, Locals>;
463
608
  options(path: RegExp, handler: SchemaRouteHandler<string, undefined, undefined, Req, Locals>): TypedRouter<Req, Locals>;
464
609
  options<QuerySchema extends SchemaLike | undefined = undefined, ParamsSchema extends SchemaLike | undefined = undefined, M extends TypedMiddleware<any, any>[] = []>(path: RegExp, options: DocMeta & {
465
610
  querySchema?: QuerySchema;
466
611
  paramsSchema?: ParamsSchema;
612
+ hooks?: RouteHooks<undefined, QuerySchema, ParamsSchema, InferMiddlewareProps<[...M]>>;
467
613
  middleware?: [...M];
468
614
  }, handler: RouteHandlerFromOptions<string, undefined, QuerySchema, ParamsSchema, Req, Locals, M>): TypedRouter<Req, Locals>;
469
615
  head<Path extends string>(path: Path, handler: SchemaRouteHandler<Path, undefined, undefined, Req, Locals>): TypedRouter<Req, Locals>;
470
616
  head<Path extends string, QuerySchema extends SchemaLike | undefined = undefined, ParamsSchema extends SchemaLike | undefined = undefined, M extends TypedMiddleware<any, any>[] = []>(path: Path, options: DocMeta & {
471
617
  querySchema?: QuerySchema;
472
618
  paramsSchema?: ParamsSchema;
619
+ hooks?: RouteHooks<undefined, QuerySchema, ParamsSchema, InferMiddlewareProps<[...M]>>;
473
620
  middleware?: [...M];
474
621
  }, handler: RouteHandlerFromOptions<Path, undefined, QuerySchema, ParamsSchema, Req, Locals, M>): TypedRouter<Req, Locals>;
475
622
  head(path: RegExp, handler: SchemaRouteHandler<string, undefined, undefined, Req, Locals>): TypedRouter<Req, Locals>;
476
623
  head<QuerySchema extends SchemaLike | undefined = undefined, ParamsSchema extends SchemaLike | undefined = undefined, M extends TypedMiddleware<any, any>[] = []>(path: RegExp, options: DocMeta & {
477
624
  querySchema?: QuerySchema;
478
625
  paramsSchema?: ParamsSchema;
626
+ hooks?: RouteHooks<undefined, QuerySchema, ParamsSchema, InferMiddlewareProps<[...M]>>;
479
627
  middleware?: [...M];
480
628
  }, handler: RouteHandlerFromOptions<string, undefined, QuerySchema, ParamsSchema, Req, Locals, M>): TypedRouter<Req, Locals>;
481
629
  all<Path extends string>(path: Path, handler: SchemaRouteHandler<Path, undefined, undefined, Req, Locals>): TypedRouter<Req, Locals>;
@@ -483,6 +631,7 @@ declare class TypedRouter<Req extends Record<string, any> = AdditionalReqProps,
483
631
  bodySchema?: BodySchema;
484
632
  querySchema?: QuerySchema;
485
633
  paramsSchema?: ParamsSchema;
634
+ hooks?: RouteHooks<BodySchema, QuerySchema, ParamsSchema, InferMiddlewareProps<[...M]>>;
486
635
  middleware?: [...M];
487
636
  }, handler: RouteHandlerFromOptions<Path, BodySchema, QuerySchema, ParamsSchema, Req, Locals, M>): TypedRouter<Req, Locals>;
488
637
  all(path: RegExp, handler: SchemaRouteHandler<string, undefined, undefined, Req, Locals>): TypedRouter<Req, Locals>;
@@ -490,9 +639,12 @@ declare class TypedRouter<Req extends Record<string, any> = AdditionalReqProps,
490
639
  bodySchema?: BodySchema;
491
640
  querySchema?: QuerySchema;
492
641
  paramsSchema?: ParamsSchema;
642
+ hooks?: RouteHooks<BodySchema, QuerySchema, ParamsSchema, InferMiddlewareProps<[...M]>>;
493
643
  middleware?: [...M];
494
644
  }, handler: RouteHandlerFromOptions<string, BodySchema, QuerySchema, ParamsSchema, Req, Locals, M>): TypedRouter<Req, Locals>;
495
645
  private registerRoute;
646
+ private notifyValidationFailure;
647
+ private runSchemaValidationHook;
496
648
  private createBodyValidationMiddleware;
497
649
  private createParamsValidationMiddleware;
498
650
  private createQueryValidationMiddleware;
@@ -523,10 +675,12 @@ declare function createTypedRouter<Req extends Record<string, any> = AdditionalR
523
675
  *
524
676
  * @property validateInput - (Future) Whether to enable global input validation.
525
677
  * @property errorHandler - Optional global error handler middleware for the router.
678
+ * @property hooks.onValidationFailure - Called for every validation failure on this router, in addition to any per-route hook.
526
679
  */
527
- interface RouterConfig {
680
+ interface RouterConfig<Req extends Record<string, any> = {}> {
528
681
  validateInput?: boolean;
529
682
  errorHandler?: (error: any, req: Request, res: Response, next: NextFunction) => void;
683
+ hooks?: RouterHooks<Req>;
530
684
  }
531
685
  /**
532
686
  * Create a new typed router with optional configuration.
@@ -545,7 +699,7 @@ interface RouterConfig {
545
699
  * }
546
700
  * });
547
701
  */
548
- declare function createTypedRouterWithConfig<Req extends Record<string, any> = AdditionalReqProps, Locals extends Record<string, any> = AdditionalLocals>(config?: RouterConfig): TypedRouter<Req, Locals>;
702
+ declare function createTypedRouterWithConfig<Req extends Record<string, any> = AdditionalReqProps, Locals extends Record<string, any> = AdditionalLocals>(config?: RouterConfig<Req>): TypedRouter<Req, Locals>;
549
703
  /**
550
704
  * Create a new typed router with pre-configured middleware.
551
705
  *
@@ -576,27 +730,16 @@ type RouterDocEntry = TypedRouter<any, any> | {
576
730
  router: TypedRouter<any, any>;
577
731
  };
578
732
  /**
579
- * Create a unified OpenAPI docs endpoint that merges routes from multiple
580
- * TypedRouter instances. Use this when routes are split across files.
733
+ * Build the OpenAPI spec object directly, without mounting an Express router
734
+ * or making an HTTP request. For generating openapi.json at build/CI time,
735
+ * separately from running the app. Accepts the same router(s) shape as
736
+ * createDocs.
581
737
  *
582
738
  * @example
583
- * // users.router.ts routes like /users, /users/:id
584
- * export const usersRouter = createTypedRouter();
585
- *
586
- * // auth.router.ts — routes like /login, /logout
587
- * export const authRouter = createTypedRouter();
588
- *
589
- * // app.ts
590
- * app.use('/api', usersRouter.getRouter());
591
- * app.use('/api', authRouter.getRouter());
592
- * app.use('/docs', createDocs(
593
- * [
594
- * { prefix: '/api', router: usersRouter },
595
- * { prefix: '/api', router: authRouter },
596
- * ],
597
- * { title: 'My API', version: '1.0.0' }
598
- * ));
739
+ * const spec = await generateOpenApiSpec(router, { title: 'My API' });
740
+ * await fs.writeFile('./openapi.json', JSON.stringify(spec, null, 2));
599
741
  */
742
+ declare function generateOpenApiSpec(routers: RouterDocEntry | RouterDocEntry[], options?: DocsOptions): Promise<Record<string, any>>;
600
743
  declare function createDocs(routers: RouterDocEntry | RouterDocEntry[], options?: DocsOptions): express.Router & express.RequestHandler;
601
744
  //#endregion
602
- export { AdditionalLocals, AdditionalReqProps, AnyStandardSchema, DocsOptions, ExtractRouteParams, HttpMethod, InferInput, InferOutput, InferSchemaHandler, InferSchemaHandlerOptions, InferSchemaOutput, LocalsOnlyMiddleware, RequestOnlyMiddleware, RouteOptions, RouterConfig, RouterDocEntry, SafeParseResult, SchemaLike, SchemaRequest, SchemaRouteHandler, TypedMiddleware, TypedRouter, createDocs, createTypedRouter, createTypedRouterWithConfig, createTypedRouterWithMiddleware, inferJsonSchema, isSchemaError, parseSchema, safeParseSchema };
745
+ export { AdditionalLocals, AdditionalReqProps, AnyStandardSchema, DocsOptions, ExtractRouteParams, HttpMethod, InferInput, InferOutput, InferSchemaHandler, InferSchemaHandlerOptions, InferSchemaOutput, LocalsOnlyMiddleware, RequestOnlyMiddleware, RouteHooks, RouteOptions, RouteValidationFailureHook, RouteValidationFailureInfo, RouterConfig, RouterDocEntry, RouterHooks, SafeParseResult, SchemaLike, SchemaRequest, SchemaRouteHandler, SchemaValidationFailureHook, SchemaValidationFailureInfo, TypedMiddleware, TypedRouter, ValidationFailureHook, ValidationFailureInfo, ValidationFailureSource, createDocs, createTypedRouter, createTypedRouterWithConfig, createTypedRouterWithMiddleware, defaultValidationHandler, defineMiddleware, generateOpenApiSpec, inferJsonSchema, isSchemaError, parseSchema, safeParseSchema };
@@ -1,12 +1,12 @@
1
- import e from"express";import{SchemaError as t}from"@standard-schema/utils";function n(e,n){let r=e;if(r&&r[`~standard`]&&typeof r[`~standard`].validate==`function`){let e=r[`~standard`].validate(n);if(e instanceof Promise)throw TypeError(`Async schema validation is not supported by parseSchema`);if(e.issues)throw new t(e.issues);return e.value}throw TypeError(`Unsupported schema shape for parseSchema`)}function r(e,t){let n=e;if(n&&n[`~standard`]&&typeof n[`~standard`].validate==`function`)return n[`~standard`].validate(t);if(n&&typeof n.safeParse==`function`)return n.safeParse(t);if(n&&typeof n.parse==`function`)try{return{value:n.parse(t)}}catch(e){return{issues:[{message:e?.message??String(e)}]}}if(n&&typeof n.validate==`function`){let e=n.validate(t);return e&&e.then&&typeof e.then==`function`?e.then(e=>e.error?{issues:[{message:e.error.message}]}:e.issues?{issues:e.issues}:{value:e.value??e}):e&&e.error?{issues:[{message:e.error.message}]}:e&&e.issues?{issues:e.issues}:{value:e.value??e}}return{issues:[{message:`Unsupported schema shape`}]}}function i(e){return typeof e==`object`&&!!e&&`issues`in e&&Array.isArray(e.issues)}const a=Function(`m`,`return import(m)`);let o,s;async function c(e){o??=await a(`module`),s??=await a(`url`);let t=[],n=globalThis.process?.argv?.[1];n&&t.push(s.pathToFileURL(n).href);let r=globalThis.process?.cwd?.()??``;r&&t.push(s.pathToFileURL(r+`/`).href);for(let n of t)try{let t=o.createRequire(n).resolve(e);return await a(s.pathToFileURL(t).href)}catch{}return a(e)}const l=new WeakMap,u=/\(\?<[^>]+>/;function d(e){return typeof e==`string`&&u.test(e)?new RegExp(e):e}function f(e){if(typeof e.path==`string`)return e.path;if(e.pathExample)return e.pathExample;let t=0;return e.path.source.replace(/\\\//g,`/`).replace(/\((?!\?)[^()]*\)/g,()=>`:${t++}`)}function p(e){let t=[],n=0,r=0,i=!1,a=!1;for(let o=0;o<e.length;o++){let s=e[o];if(a){a=!1;continue}if(s===`\\`){a=!0;continue}if(s===`[`){i=!0;continue}if(s===`]`){i=!1;continue}i||(s===`(`?r++:s===`)`?r=Math.max(0,r-1):s===`|`&&r===0&&(t.push(e.slice(n,o)),n=o+1))}return t.push(e.slice(n)),t.length>1?t:[e]}function m(e){if(!e.startsWith(`(`)||!e.endsWith(`)`))return e;let t=0,n=!1,r=!1;for(let i=0;i<e.length;i++){let a=e[i];if(r){r=!1;continue}if(a===`\\`){r=!0;continue}if(a===`[`)n=!0;else if(a===`]`)n=!1;else if(!n&&a===`(`)t++;else if(!n&&a===`)`&&(t--,t===0&&i!==e.length-1))return e}return e.slice(1,-1).replace(/^\?:/,``)}function h(e){let t=0;return m(e).replace(/^\^|\$$/g,``).replace(/\\\//g,`/`).replace(/\.\*|\.\+/g,`/:path`).replace(/\/?\?$/,``).replace(/\(\?<([A-Za-z0-9_]+)>[^()]*\)/g,`:$1`).replace(/\((?!\?)[^()]*\)/g,()=>`:${t++}`)}function g(e){if(typeof e.path==`string`||e.pathExample)return[f(e)];let t=p(e.path.source);return t.length===1?[f(e)]:t.map(h)}function _(e){return e.replace(/\{([^{}]*)\}/g,`$1`).replace(/:([A-Za-z0-9_]+)(?:\([^)]*\))?[?+*]?/g,`{$1}`).replace(/\(\?<([A-Za-z0-9_]+)>[^)]*\)/g,`{$1}`).replace(/^\^|\$$/g,``).replace(/\/{2,}/g,`/`)}function v(e){let t=e.replace(/\{([^{}]*)\}/g,`$1`);return[...t.matchAll(/:([A-Za-z0-9_]+)/g),...t.matchAll(/\(\?<([A-Za-z0-9_]+)>/g)].map(e=>e[1])}function y(e){return e.startsWith(`:`)||e.startsWith(`*`)||e.includes(`(?<`)}function b(e){return e.replace(/^\^|\$$/g,``).replace(/[{}]/g,``).split(`/`).filter(Boolean).find(e=>!y(e))??`default`}function x(e,t){let n=t.replace(/^\^|\$$/g,``).replace(/[{}]/g,``).split(`/`).filter(e=>e&&!y(e)),r=n[n.length-1]??`resource`;return`${{get:`Get`,post:`Create`,put:`Update`,patch:`Patch`,delete:`Delete`,head:`Head`,options:`Options`}[e]??e} ${r}`}async function S(e){let t=l.get(e);if(t)return t;let n=await C(e);return l.set(e,n),n}async function C(e){if(typeof e.toJsonSchema==`function`)try{return e.toJsonSchema()}catch{}let t=e[`~standard`]?.vendor;if(t===`zod`){try{let t=await c(`zod`);if(typeof t.toJSONSchema==`function`)return t.toJSONSchema(e)}catch{}try{let t=await c(`zod-to-json-schema`),n=t.zodToJsonSchema??t.default?.zodToJsonSchema;if(typeof n==`function`)return n(e)}catch{}}if(t===`valibot`)try{let t=await c(`@valibot/to-json-schema`),n=t.toJsonSchema??t.default?.toJsonSchema;if(typeof n==`function`)return n(e)}catch{}if(t===`effect`)try{let t=await c(`effect`),n=t.JSONSchema?.make??t.default?.JSONSchema?.make;if(typeof n==`function`)return n(e)}catch{}return{}}function w(e){return T(e,0,new WeakSet)}function T(e,t,n){if(e==null)return{type:`null`};if(t>=12)return{};if(e instanceof Date)return{type:`string`,format:`date-time`};if(typeof e==`bigint`)return{type:`integer`};if(typeof e==`object`&&typeof e.toJSON==`function`)return T(e.toJSON(),t,n);if(Array.isArray(e)){if(e.length===0||n.has(e))return{type:`array`,items:{}};n.add(e);let r=Math.min(e.length,20),i=T(e[0],t+1,n);for(let a=1;a<r;a++)i=O(i,T(e[a],t+1,n));return n.delete(e),{type:`array`,items:i}}switch(typeof e){case`string`:return{type:`string`};case`boolean`:return{type:`boolean`};case`number`:return Number.isFinite(e)?{type:Number.isInteger(e)?`integer`:`number`}:{type:`null`};case`object`:{if(n.has(e))return{type:`object`};n.add(e);let r={},i=[];for(let[a,o]of Object.entries(e))typeof o!=`function`&&o!==void 0&&(r[a]=T(o,t+1,n),i.push(a));n.delete(e);let a={type:`object`,properties:r};return i.length&&(a.required=i),a}default:return{}}}function E(e){return!e||e.type===void 0?new Set:new Set(Array.isArray(e.type)?e.type:[e.type])}function D(e){e.has(`integer`)&&e.has(`number`)&&e.delete(`integer`);let t=[...e];if(t.length!==0)return t.length===1?t[0]:t}function O(e,t){if(!e||Object.keys(e).length===0)return t??{};if(!t||Object.keys(t).length===0)return e??{};let n=new Set([...E(e),...E(t)]),r={},i=D(n);if(i!==void 0&&(r.type=i),n.has(`object`)&&(e.properties||t.properties)){let n=e.properties??{},i=t.properties??{},a={};for(let e of new Set([...Object.keys(n),...Object.keys(i)]))a[e]=O(n[e],i[e]);r.properties=a;let o=e.required??[],s=t.required??[],c=o.filter(e=>s.includes(e));c.length&&(r.required=c)}if(n.has(`array`)){let n=O(e.items,t.items);n&&Object.keys(n).length&&(r.items=n)}return r}function k(e){return e.replace(/&/g,`&amp;`).replace(/</g,`&lt;`).replace(/>/g,`&gt;`).replace(/"/g,`&quot;`)}const A=`https://cdn.jsdelivr.net/npm/@scalar/api-reference`;function j(e,t,n){return`<!doctype html>
1
+ import e from"express";import{SchemaError as t}from"@standard-schema/utils";function n(e,n){let r=e;if(r&&r[`~standard`]&&typeof r[`~standard`].validate==`function`){let e=r[`~standard`].validate(n);if(e instanceof Promise)throw TypeError(`Async schema validation is not supported by parseSchema`);if(e.issues)throw new t(e.issues);return e.value}throw TypeError(`Unsupported schema shape for parseSchema`)}function r(e,t){let n=e;if(n&&n[`~standard`]&&typeof n[`~standard`].validate==`function`)return n[`~standard`].validate(t);if(n&&typeof n.safeParse==`function`)return n.safeParse(t);if(n&&typeof n.parse==`function`)try{return{value:n.parse(t)}}catch(e){return{issues:[{message:e?.message??String(e)}]}}if(n&&typeof n.validate==`function`){let e=n.validate(t);return e&&e.then&&typeof e.then==`function`?e.then(e=>e.error?{issues:[{message:e.error.message}]}:e.issues?{issues:e.issues}:{value:e.value??e}):e&&e.error?{issues:[{message:e.error.message}]}:e&&e.issues?{issues:e.issues}:{value:e.value??e}}return{issues:[{message:`Unsupported schema shape`}]}}function i(e){return typeof e==`object`&&!!e&&`issues`in e&&Array.isArray(e.issues)}function a(...e){return e}function o(e){e.res.status(400).json({error:`Validation failed`,details:e.details})}function s(e,t){if(e)try{e(t)?.catch?.(()=>{})}catch{}}const c=Function(`m`,`return import(m)`);let l,u;async function d(e){l??=await c(`module`),u??=await c(`url`);let t=[],n=globalThis.process?.argv?.[1];n&&t.push(u.pathToFileURL(n).href);let r=globalThis.process?.cwd?.()??``;r&&t.push(u.pathToFileURL(r+`/`).href);for(let n of t)try{let t=l.createRequire(n).resolve(e);return await c(u.pathToFileURL(t).href)}catch{}return c(e)}const f=new WeakMap,p=/\(\?<[^>]+>/;function m(e){return typeof e==`string`&&p.test(e)?new RegExp(e):e}function h(e){if(typeof e.path==`string`)return e.path;if(e.pathExample)return e.pathExample;let t=0;return e.path.source.replace(/\\\//g,`/`).replace(/\((?!\?)[^()]*\)/g,()=>`:${t++}`)}function g(e){let t=[],n=0,r=0,i=!1,a=!1;for(let o=0;o<e.length;o++){let s=e[o];if(a){a=!1;continue}if(s===`\\`){a=!0;continue}if(s===`[`){i=!0;continue}if(s===`]`){i=!1;continue}i||(s===`(`?r++:s===`)`?r=Math.max(0,r-1):s===`|`&&r===0&&(t.push(e.slice(n,o)),n=o+1))}return t.push(e.slice(n)),t.length>1?t:[e]}function _(e){if(!e.startsWith(`(`)||!e.endsWith(`)`))return e;let t=0,n=!1,r=!1;for(let i=0;i<e.length;i++){let a=e[i];if(r){r=!1;continue}if(a===`\\`){r=!0;continue}if(a===`[`)n=!0;else if(a===`]`)n=!1;else if(!n&&a===`(`)t++;else if(!n&&a===`)`&&(t--,t===0&&i!==e.length-1))return e}return e.slice(1,-1).replace(/^\?:/,``)}function v(e){let t=0;return _(e).replace(/^\^|\$$/g,``).replace(/\\\//g,`/`).replace(/\.\*|\.\+/g,`/:path`).replace(/\/?\?$/,``).replace(/\(\?<([A-Za-z0-9_]+)>[^()]*\)/g,`:$1`).replace(/\((?!\?)[^()]*\)/g,()=>`:${t++}`)}function y(e){if(typeof e.path==`string`||e.pathExample)return[h(e)];let t=g(e.path.source);return t.length===1?[h(e)]:t.map(v)}function b(e){let t=e.replace(/\{([^{}]*)\}/g,`$1`).replace(/:([A-Za-z0-9_]+)(?:\([^)]*\))?[?+*]?/g,`{$1}`).replace(/\(\?<([A-Za-z0-9_]+)>[^)]*\)/g,`{$1}`).replace(/\*([A-Za-z0-9_]+)/g,`{$1}`).replace(/^\^|\$$/g,``),n=0;return t.replace(/\*/g,()=>`{${n++}}`).replace(/\/{2,}/g,`/`)}function x(e){let t=e.replace(/\{([^{}]*)\}/g,`$1`).replace(/^\^|\$$/g,``),n=[...t.matchAll(/:([A-Za-z0-9_]+)/g),...t.matchAll(/\(\?<([A-Za-z0-9_]+)>/g),...t.matchAll(/\*([A-Za-z0-9_]+)/g)].map(e=>e[1]),r=(t.replace(/\*[A-Za-z0-9_]+/g,``).match(/\*/g)??[]).length;for(let e=0;e<r;e++)n.push(String(e));return n}function S(e){return e.startsWith(`:`)||e.includes(`*`)||e.includes(`(?<`)}function C(e){return e.replace(/^\^|\$$/g,``).replace(/[{}]/g,``).split(`/`).filter(Boolean).find(e=>!S(e))??`default`}function w(e,t){let n=t.replace(/^\^|\$$/g,``).replace(/[{}]/g,``).split(`/`).filter(e=>e&&!S(e)),r=n[n.length-1]??`resource`;return`${{get:`Get`,post:`Create`,put:`Update`,patch:`Patch`,delete:`Delete`,head:`Head`,options:`Options`}[e]??e} ${r}`}async function T(e){let t=f.get(e);if(t)return t;let n=await E(e);return f.set(e,n),n}async function E(e){if(typeof e.toJsonSchema==`function`)try{return e.toJsonSchema()}catch{}let t=e[`~standard`]?.vendor;if(t===`zod`){try{let t=await d(`zod`);if(typeof t.toJSONSchema==`function`)return t.toJSONSchema(e)}catch{}try{let t=await d(`zod-to-json-schema`),n=t.zodToJsonSchema??t.default?.zodToJsonSchema;if(typeof n==`function`)return n(e)}catch{}}if(t===`valibot`)try{let t=await d(`@valibot/to-json-schema`),n=t.toJsonSchema??t.default?.toJsonSchema;if(typeof n==`function`)return n(e)}catch{}if(t===`effect`)try{let t=await d(`effect`),n=t.JSONSchema?.make??t.default?.JSONSchema?.make;if(typeof n==`function`)return n(e)}catch{}return{}}function D(e){return O(e,0,new WeakSet)}function O(e,t,n){if(e==null)return{type:`null`};if(t>=12)return{};if(e instanceof Date)return{type:`string`,format:`date-time`};if(typeof e==`bigint`)return{type:`integer`};if(typeof e==`object`&&typeof e.toJSON==`function`)return O(e.toJSON(),t,n);if(Array.isArray(e)){if(e.length===0||n.has(e))return{type:`array`,items:{}};n.add(e);let r=Math.min(e.length,20),i=O(e[0],t+1,n);for(let a=1;a<r;a++)i=j(i,O(e[a],t+1,n));return n.delete(e),{type:`array`,items:i}}switch(typeof e){case`string`:return{type:`string`};case`boolean`:return{type:`boolean`};case`number`:return Number.isFinite(e)?{type:Number.isInteger(e)?`integer`:`number`}:{type:`null`};case`object`:{if(n.has(e))return{type:`object`};n.add(e);let r={},i=[];for(let[a,o]of Object.entries(e))typeof o!=`function`&&o!==void 0&&(r[a]=O(o,t+1,n),i.push(a));n.delete(e);let a={type:`object`,properties:r};return i.length&&(a.required=i),a}default:return{}}}function k(e){return!e||e.type===void 0?new Set:new Set(Array.isArray(e.type)?e.type:[e.type])}function A(e){e.has(`integer`)&&e.has(`number`)&&e.delete(`integer`);let t=[...e];if(t.length!==0)return t.length===1?t[0]:t}function j(e,t){if(!e||Object.keys(e).length===0)return t??{};if(!t||Object.keys(t).length===0)return e??{};let n=new Set([...k(e),...k(t)]),r={},i=A(n);if(i!==void 0&&(r.type=i),n.has(`object`)&&(e.properties||t.properties)){let n=e.properties??{},i=t.properties??{},a={};for(let e of new Set([...Object.keys(n),...Object.keys(i)]))a[e]=j(n[e],i[e]);r.properties=a;let o=e.required??[],s=t.required??[],c=o.filter(e=>s.includes(e));c.length&&(r.required=c)}if(n.has(`array`)){let n=j(e.items,t.items);n&&Object.keys(n).length&&(r.items=n)}return r}function M(e){return e.replace(/&/g,`&amp;`).replace(/</g,`&lt;`).replace(/>/g,`&gt;`).replace(/"/g,`&quot;`)}const N=`https://cdn.jsdelivr.net/npm/@scalar/api-reference`;function P(e,t,n){return`<!doctype html>
2
2
  <html>
3
3
  <head>
4
- <title>${k(e)}</title>
4
+ <title>${M(e)}</title>
5
5
  <meta charset="utf-8" />
6
6
  <meta name="viewport" content="width=device-width, initial-scale=1" />
7
7
  </head>
8
8
  <body>
9
- <script id="api-reference" data-url="${k(t)}"><\/script>
10
- <script src="${k(n)}"><\/script>
9
+ <script id="api-reference" data-url="${M(t)}"><\/script>
10
+ <script src="${M(n)}"><\/script>
11
11
  </body>
12
- </html>`}async function M(e,t){let n=Object.create(null);for(let t of e)if(!(t.method===`all`||t.hidden))for(let e of g(t)){let r=_(e);n[r]||(n[r]={});let i;if(t.paramsSchema){let e=await S(t.paramsSchema),n=e.properties??{},r=e.required??[];i=Object.entries(n).map(([e,t])=>({name:e,in:`path`,required:r.includes(e),schema:t}))}else i=v(e).map(e=>({name:e,in:`path`,required:!0,schema:{type:`string`}}));if(t.querySchema){let e=await S(t.querySchema),n=e.properties??{},r=e.required??[];for(let[e,t]of Object.entries(n))i.push({name:e,in:`query`,required:r.includes(e),schema:t})}let a={summary:t.summary??x(t.method,e),tags:t.tags??[b(e)],parameters:i};t.description&&(a.description=t.description),t.deprecated&&(a.deprecated=!0),t.bodySchema&&(a.requestBody={required:!0,content:{"application/json":{schema:await S(t.bodySchema)}}});let o={};for(let[e,n]of t.responseSamples){let t={schema:n.schema};n.example!==void 0&&(t.example=n.example),o[String(e)]={description:e<400?`Success`:`Error`,content:{"application/json":t}}}if(t.responseSchema){let e=await S(t.responseSchema);if(e&&Object.keys(e).length){let n=`200`;for(let e of t.responseSamples.keys())if(e>=200&&e<300){n=String(e);break}o[n]={description:`Success`,content:{"application/json":{schema:e}}}}}Object.keys(o).length===0&&(o[200]={description:`Success`}),a.responses=o,n[r][t.method]=a}return{openapi:`3.1.0`,info:{title:t.title??`API`,version:t.version??`1.0.0`,...t.description?{description:t.description}:{}},...t.servers?{servers:t.servers}:{},paths:n}}const N=new WeakMap;var P=class t{router;routes=[];mountedRouters=[];sampleMode=`off`;scheduleSpecWrite;constructor(){this.router=e.Router(),N.set(this.router,this)}useMiddleware(e){return this.router.use(e),this}getRouter(){return this.router}use(e,...n){let r=typeof e==`string`,i=r?e:``,a=(r?n:[e,...n]).map(e=>{if(e instanceof t)return this.trackMounted(i,e),e.getRouter();let n=N.get(e);return n&&this.trackMounted(i,n),e});return r?this.router.use(e,...a):this.router.use(...a),this}mount(e,t){if(typeof e==`string`){let n=t;this.router.use(e,n.getRouter()),this.trackMounted(e,n)}else this.router.use(e.getRouter()),this.trackMounted(``,e);return this}getRouteMetadata(){let e=this.mountedRouters.flatMap(({prefix:e,router:t})=>t.getRouteMetadata().map(t=>({...t,...typeof t.path==`string`?{path:e+t.path}:{path:t.path,pathExample:e+f(t)}})));return[...this.routes,...e]}enableSampling(e=`redacted`,t,n=new Set){if(!n.has(this)){n.add(this),this.sampleMode=e,this.scheduleSpecWrite=t;for(let{router:r}of this.mountedRouters)r.enableSampling(e,t,n)}}trackMounted(e,t){this.mountedRouters.some(n=>n.router===t&&n.prefix===e)||(this.mountedRouters.push({prefix:e,router:t}),this.sampleMode!==`off`&&t.enableSampling(this.sampleMode,this.scheduleSpecWrite))}hydrateResponses(e,t=``,n=new Set){if(n.has(this))return;n.add(this);let r=e?.paths??{};for(let e of this.routes)for(let n of g(e)){let i=r[_(t+n)]?.[e.method]?.responses;if(i)for(let t of Object.keys(i)){let n=Number(t);if(Number.isNaN(n)||e.responseSamples.has(n))continue;let r=i[t]?.content?.[`application/json`];r?.schema&&e.responseSamples.set(n,r.example===void 0?{schema:r.schema}:{schema:r.schema,example:r.example})}}for(let{prefix:r,router:i}of this.mountedRouters)i.hydrateResponses(e,t+r,n)}docs(t={}){let n=e.Router(),r;if(t.specOutputPath){let e=t.specOutputPath,n=!1,i=!1,o=async()=>{if(n){i=!0;return}n=!0;try{let n=await M(this.getRouteMetadata(),t),r=await a(`fs/promises`),i=e.replace(/[/\\][^/\\]*$/,``);i&&i!==e&&await r.mkdir(i,{recursive:!0}).catch(()=>{});let o=globalThis.process?.pid??`0`,s=`${e}.${o}.tmp`;await r.writeFile(s,JSON.stringify(n,null,2),`utf8`),await r.rename(s,e)}catch{}finally{n=!1,i&&(i=!1,o())}},s;r=()=>{s&&clearTimeout(s),s=setTimeout(o,300),s.unref?.()},setImmediate(async()=>{try{let t=await(await a(`fs/promises`)).readFile(e,`utf8`).catch(()=>null);if(t)try{this.hydrateResponses(JSON.parse(t))}catch{}}catch{}await o()})}return t.sampleResponses!==!1&&this.enableSampling(t.sampleResponses===`live`?`live`:`redacted`,r),n.get(`/openapi.json`,async(e,n)=>{try{let e=await M(this.getRouteMetadata(),t);n.json(e)}catch(e){n.status(500).json({error:`Failed to generate spec`,details:String(e)})}}),n.get(`/`,(e,n)=>{let r=`${e.baseUrl}/openapi.json`,i=t.cdnUrl??A;n.setHeader(`Content-Type`,`text/html; charset=utf-8`),n.send(j(t.title??`API`,r,i))}),n}get(e,t,n){return this.registerRoute(`get`,e,t,n)}post(e,t,n){return this.registerRoute(`post`,e,t,n)}put(e,t,n){return this.registerRoute(`put`,e,t,n)}patch(e,t,n){return this.registerRoute(`patch`,e,t,n)}delete(e,t,n){return this.registerRoute(`delete`,e,t,n)}options(e,t,n){return this.registerRoute(`options`,e,t,n)}head(e,t,n){return this.registerRoute(`head`,e,t,n)}all(e,t,n){return this.registerRoute(`all`,e,t,n)}registerRoute(e,t,n,r){let i=[],a={method:e,path:t,responseSamples:new Map};if(this.routes.push(a),typeof n==`object`){let e=n;a.bodySchema=e.bodySchema,a.querySchema=e.querySchema,a.paramsSchema=e.paramsSchema,a.tags=e.tags,a.description=e.description,a.summary=e.summary,a.deprecated=e.deprecated,a.responseSchema=e.responseSchema,a.hidden=e.hidden,a.pathExample=e.pathExample,e.middleware&&i.push(...e.middleware),e.bodySchema&&i.push(this.createBodyValidationMiddleware(e.bodySchema)),e.querySchema&&i.push(this.createQueryValidationMiddleware(e.querySchema)),e.paramsSchema&&i.push(this.createParamsValidationMiddleware(e.paramsSchema)),i.push(r)}else i.push(n);let o=0,s=(e,t)=>{let n=e.statusCode,r=w(t),i=a.responseSamples.get(n),o=i?O(i.schema,r):r,s=this.sampleMode===`live`?i?.example??t:void 0,c=!i||JSON.stringify(i.schema)!==JSON.stringify(o);a.responseSamples.set(n,{schema:o,example:s}),c&&this.scheduleSpecWrite?.()};return this.router[e](d(t),(e,t,n)=>{if(this.sampleMode===`off`||a.hidden||o>=50||a.responseSamples.size>=10){n();return}o++;let r=t.json;t.json=function(e){return s(t,e),r.call(this,e)};let i=t.send;t.send=function(e){return typeof e==`object`&&e&&!Buffer.isBuffer(e)&&s(t,e),i.call(this,e)},n()},...i),this}createBodyValidationMiddleware(e){return async(t,n,a)=>{try{let i=r(e,t.body),o=i&&typeof i.then==`function`?await i:i;if(o&&`issues`in o&&o.issues){n.status(400).json({error:`Validation failed`,details:o.errors||o.issues});return}t.body=o&&`value`in o?o.value:o,a()}catch(e){i(e)?n.status(400).json({error:`Validation failed`,details:e.errors||e.issues}):a(e)}}}createParamsValidationMiddleware(e){return async(t,n,a)=>{try{let i=r(e,t.params),o=i&&typeof i.then==`function`?await i:i;if(o&&`issues`in o&&o.issues){n.status(400).json({error:`Validation failed`,details:o.errors||o.issues});return}t.params=o&&`value`in o?o.value:o,a()}catch(e){i(e)?n.status(400).json({error:`Validation failed`,details:e.errors||e.issues}):a(e)}}}createQueryValidationMiddleware(e){return async(t,n,a)=>{try{let i=r(e,t.query),o=i&&typeof i.then==`function`?await i:i;if(o&&`issues`in o&&o.issues){n.status(400).json({error:`Validation failed`,details:o.errors||o.issues});return}let s=o&&`value`in o?o.value:o;Object.defineProperty(t,"query",{value:s,writable:!1,enumerable:!0,configurable:!0}),a()}catch(e){i(e)?n.status(400).json({error:`Validation failed`,details:e.errors||e.issues}):a(e)}}}};function F(){return new P}function I(e){let t=new P;return e?.errorHandler&&t.getRouter().use(e.errorHandler),t}function L(...e){let t=new P;for(let n of e)t=t.useMiddleware(n);return t}function R(t,n={}){let r=(Array.isArray(t)?t:[t]).map(e=>`prefix`in e?e:{prefix:``,router:e});if(n.sampleResponses!==!1){let e=n.sampleResponses===`live`?`live`:`redacted`;for(let{router:t}of r)t.enableSampling(e)}let i=e.Router();return i.get(`/openapi.json`,async(e,t)=>{try{let e=await M(r.flatMap(({prefix:e,router:t})=>t.getRouteMetadata().map(t=>({...t,...typeof t.path==`string`?{path:e+t.path}:{path:t.path,pathExample:e+f(t)}}))),n);t.json(e)}catch(e){t.status(500).json({error:`Failed to generate spec`,details:String(e)})}}),i.get(`/`,(e,t)=>{let r=`${e.baseUrl}/openapi.json`,i=n.cdnUrl??A;t.setHeader(`Content-Type`,`text/html; charset=utf-8`),t.send(j(n.title??`API`,r,i))}),i}export{P as TypedRouter,R as createDocs,F as createTypedRouter,I as createTypedRouterWithConfig,L as createTypedRouterWithMiddleware,w as inferJsonSchema,i as isSchemaError,n as parseSchema,r as safeParseSchema};
12
+ </html>`}async function F(e,t){let n=Object.create(null);for(let t of e)if(!(t.method===`all`||t.hidden))for(let e of y(t)){let r=b(e);n[r]||(n[r]={});let i;if(t.paramsSchema){let e=await T(t.paramsSchema),n=e.properties??{},r=e.required??[];i=Object.entries(n).map(([e,t])=>({name:e,in:`path`,required:r.includes(e),schema:t}))}else i=x(e).map(e=>({name:e,in:`path`,required:!0,schema:{type:`string`}}));if(t.querySchema){let e=await T(t.querySchema),n=e.properties??{},r=e.required??[];for(let[e,t]of Object.entries(n))i.push({name:e,in:`query`,required:r.includes(e),schema:t})}let a={summary:t.summary??w(t.method,e),tags:t.tags??[C(e)],parameters:i};t.description&&(a.description=t.description),t.deprecated&&(a.deprecated=!0),t.bodySchema&&(a.requestBody={required:!0,content:{"application/json":{schema:await T(t.bodySchema)}}});let o={};for(let[e,n]of t.responseSamples){let t={schema:n.schema};n.example!==void 0&&(t.example=n.example),o[String(e)]={description:e<400?`Success`:`Error`,content:{"application/json":t}}}if(t.responseSchema){let e=await T(t.responseSchema);if(e&&Object.keys(e).length){let n=`200`;for(let e of t.responseSamples.keys())if(e>=200&&e<300){n=String(e);break}o[n]={description:`Success`,content:{"application/json":{schema:e}}}}}Object.keys(o).length===0&&(o[200]={description:`Success`}),a.responses=o,n[r][t.method]=a}return{openapi:`3.1.0`,info:{title:t.title??`API`,version:t.version??`1.0.0`,...t.description?{description:t.description}:{}},...t.servers?{servers:t.servers}:{},paths:n}}const I=new WeakMap;var L=class t{router;routes=[];mountedRouters=[];sampleMode=`off`;scheduleSpecWrite;globalValidationFailureHook;constructor(){this.router=e.Router(),I.set(this.router,this)}onValidationFailure(e){return this.globalValidationFailureHook=e,this}useMiddleware(e){return this.router.use(e),this}getRouter(){return this.router}use(e,...n){let r=typeof e==`string`,i=r?e:``,a=(r?n:[e,...n]).map(e=>{if(e instanceof t)return this.trackMounted(i,e),e.getRouter();let n=I.get(e);return n&&this.trackMounted(i,n),e});return r?this.router.use(e,...a):this.router.use(...a),this}mount(e,t){if(typeof e==`string`){let n=t;this.router.use(e,n.getRouter()),this.trackMounted(e,n)}else this.router.use(e.getRouter()),this.trackMounted(``,e);return this}getRouteMetadata(){let e=this.mountedRouters.flatMap(({prefix:e,router:t})=>t.getRouteMetadata().map(t=>({...t,...typeof t.path==`string`?{path:e+t.path}:{path:t.path,pathExample:e+h(t)}})));return[...this.routes,...e]}enableSampling(e=`redacted`,t,n=new Set){if(!n.has(this)){n.add(this),this.sampleMode=e,this.scheduleSpecWrite=t;for(let{router:r}of this.mountedRouters)r.enableSampling(e,t,n)}}trackMounted(e,t){this.mountedRouters.some(n=>n.router===t&&n.prefix===e)||(this.mountedRouters.push({prefix:e,router:t}),this.sampleMode!==`off`&&t.enableSampling(this.sampleMode,this.scheduleSpecWrite))}hydrateResponses(e,t=``,n=new Set){if(n.has(this))return;n.add(this);let r=e?.paths??{};for(let e of this.routes)for(let n of y(e)){let i=r[b(t+n)]?.[e.method]?.responses;if(i)for(let t of Object.keys(i)){let n=Number(t);if(Number.isNaN(n)||e.responseSamples.has(n))continue;let r=i[t]?.content?.[`application/json`];r?.schema&&e.responseSamples.set(n,r.example===void 0?{schema:r.schema}:{schema:r.schema,example:r.example})}}for(let{prefix:r,router:i}of this.mountedRouters)i.hydrateResponses(e,t+r,n)}docs(t={}){let n=e.Router(),r;if(t.specOutputPath){let e=t.specOutputPath,n=!1,i=!1,a=async()=>{if(n){i=!0;return}n=!0;try{let n=await F(this.getRouteMetadata(),t),r=await c(`fs/promises`),i=e.replace(/[/\\][^/\\]*$/,``);i&&i!==e&&await r.mkdir(i,{recursive:!0}).catch(()=>{});let a=globalThis.process?.pid??`0`,o=`${e}.${a}.tmp`;await r.writeFile(o,JSON.stringify(n,null,2),`utf8`),await r.rename(o,e)}catch{}finally{n=!1,i&&(i=!1,a())}},o;r=()=>{o&&clearTimeout(o),o=setTimeout(a,300),o.unref?.()},setImmediate(async()=>{try{let t=await(await c(`fs/promises`)).readFile(e,`utf8`).catch(()=>null);if(t)try{this.hydrateResponses(JSON.parse(t))}catch{}}catch{}await a()})}return t.sampleResponses!==!1&&this.enableSampling(t.sampleResponses===`live`?`live`:`redacted`,r),n.get(`/openapi.json`,async(e,n)=>{try{let e=await F(this.getRouteMetadata(),t);n.json(e)}catch(e){n.status(500).json({error:`Failed to generate spec`,details:String(e)})}}),n.get(`/`,(e,n)=>{let r=`${e.baseUrl}/openapi.json`,i=t.cdnUrl??N;n.setHeader(`Content-Type`,`text/html; charset=utf-8`),n.send(P(t.title??`API`,r,i))}),n}get(e,t,n){return this.registerRoute(`get`,e,t,n)}post(e,t,n){return this.registerRoute(`post`,e,t,n)}put(e,t,n){return this.registerRoute(`put`,e,t,n)}patch(e,t,n){return this.registerRoute(`patch`,e,t,n)}delete(e,t,n){return this.registerRoute(`delete`,e,t,n)}options(e,t,n){return this.registerRoute(`options`,e,t,n)}head(e,t,n){return this.registerRoute(`head`,e,t,n)}all(e,t,n){return this.registerRoute(`all`,e,t,n)}registerRoute(e,t,n,r){let i=[],a={method:e,path:t,responseSamples:new Map};if(this.routes.push(a),typeof n==`object`){let o=n;a.bodySchema=o.bodySchema,a.querySchema=o.querySchema,a.paramsSchema=o.paramsSchema,a.tags=o.tags,a.description=o.description,a.summary=o.summary,a.deprecated=o.deprecated,a.responseSchema=o.responseSchema,a.hidden=o.hidden,a.pathExample=o.pathExample,o.middleware&&i.push(...o.middleware);let s=o.hooks?.onValidationFailure,c=o.hooks?.onBodyValidationFailure,l=o.hooks?.onQueryValidationFailure,u=o.hooks?.onParamsValidationFailure;o.bodySchema&&i.push(this.createBodyValidationMiddleware(o.bodySchema,e,t,o.pathExample,s,c)),o.querySchema&&i.push(this.createQueryValidationMiddleware(o.querySchema,e,t,o.pathExample,s,l)),o.paramsSchema&&i.push(this.createParamsValidationMiddleware(o.paramsSchema,e,t,o.pathExample,s,u)),i.push(r)}else i.push(n);let o=0,s=(e,t)=>{let n=e.statusCode,r=D(t),i=a.responseSamples.get(n),o=i?j(i.schema,r):r,s=this.sampleMode===`live`?i?.example??t:void 0,c=!i||JSON.stringify(i.schema)!==JSON.stringify(o);a.responseSamples.set(n,{schema:o,example:s}),c&&this.scheduleSpecWrite?.()};return this.router[e](m(t),(e,t,n)=>{if(this.sampleMode===`off`||a.hidden||o>=50||a.responseSamples.size>=10){n();return}o++;let r=t.json;t.json=function(e){return s(t,e),r.call(this,e)};let i=t.send;t.send=function(e){return typeof e==`object`&&e&&!Buffer.isBuffer(e)&&s(t,e),i.call(this,e)},n()},...i),this}notifyValidationFailure(e,t,n,r,i,a,o){if(!o&&!this.globalValidationFailureHook)return;let c={source:e,error:t,details:n,method:r,path:i,req:a};s(o,c),s(this.globalValidationFailureHook,c)}async runSchemaValidationHook(e,t,n,r,i,a,o,s){if(!e)return!1;let c=!1,l={error:t,details:n,method:r,path:i,req:a,res:o,next:((...e)=>{c=!0,s(...e)})};try{await e(l)}catch{}return c||o.headersSent}createBodyValidationMiddleware(e,t,n,a,s,c){return async(l,u,d)=>{try{let i=r(e,l.body),f=i&&typeof i.then==`function`?await i:i;if(f&&`issues`in f&&f.issues){let e=f.errors||f.issues,r=h({path:n,pathExample:a}),i=await this.runSchemaValidationHook(c,`Validation failed`,e,t,r,l,u,d);this.notifyValidationFailure(`body`,`Validation failed`,e,t,r,l,s),i||o({res:u,details:e});return}l.body=f&&`value`in f?f.value:f,d()}catch(e){if(i(e)){let r=e.errors||e.issues,i=h({path:n,pathExample:a}),f=await this.runSchemaValidationHook(c,`Validation failed`,r,t,i,l,u,d);this.notifyValidationFailure(`body`,`Validation failed`,r,t,i,l,s),f||o({res:u,details:r})}else d(e)}}}createParamsValidationMiddleware(e,t,n,a,s,c){return async(l,u,d)=>{try{let i=r(e,l.params),f=i&&typeof i.then==`function`?await i:i;if(f&&`issues`in f&&f.issues){let e=f.errors||f.issues,r=h({path:n,pathExample:a}),i=await this.runSchemaValidationHook(c,`Validation failed`,e,t,r,l,u,d);this.notifyValidationFailure(`params`,`Validation failed`,e,t,r,l,s),i||o({res:u,details:e});return}l.params=f&&`value`in f?f.value:f,d()}catch(e){if(i(e)){let r=e.errors||e.issues,i=h({path:n,pathExample:a}),f=await this.runSchemaValidationHook(c,`Validation failed`,r,t,i,l,u,d);this.notifyValidationFailure(`params`,`Validation failed`,r,t,i,l,s),f||o({res:u,details:r})}else d(e)}}}createQueryValidationMiddleware(e,t,n,a,s,c){return async(l,u,d)=>{try{let i=r(e,l.query),f=i&&typeof i.then==`function`?await i:i;if(f&&`issues`in f&&f.issues){let e=f.errors||f.issues,r=h({path:n,pathExample:a}),i=await this.runSchemaValidationHook(c,`Validation failed`,e,t,r,l,u,d);this.notifyValidationFailure(`query`,`Validation failed`,e,t,r,l,s),i||o({res:u,details:e});return}let p=f&&`value`in f?f.value:f;Object.defineProperty(l,"query",{value:p,writable:!1,enumerable:!0,configurable:!0}),d()}catch(e){if(i(e)){let r=e.errors||e.issues,i=h({path:n,pathExample:a}),f=await this.runSchemaValidationHook(c,`Validation failed`,r,t,i,l,u,d);this.notifyValidationFailure(`query`,`Validation failed`,r,t,i,l,s),f||o({res:u,details:r})}else d(e)}}}};function R(){return new L}function z(e){let t=new L;return e?.errorHandler&&t.getRouter().use(e.errorHandler),e?.hooks?.onValidationFailure&&t.onValidationFailure(e.hooks.onValidationFailure),t}function B(...e){let t=new L;for(let n of e)t=t.useMiddleware(n);return t}function V(e){return(Array.isArray(e)?e:[e]).map(e=>`prefix`in e?e:{prefix:``,router:e}).flatMap(({prefix:e,router:t})=>t.getRouteMetadata().map(t=>({...t,...typeof t.path==`string`?{path:e+t.path}:{path:t.path,pathExample:e+h(t)}})))}async function H(e,t={}){return F(V(e),t)}function U(t,n={}){if(n.sampleResponses!==!1){let e=n.sampleResponses===`live`?`live`:`redacted`,r=Array.isArray(t)?t:[t];for(let t of r)(`prefix`in t?t.router:t).enableSampling(e)}let r=e.Router();return r.get(`/openapi.json`,async(e,r)=>{try{let e=await H(t,n);r.json(e)}catch(e){r.status(500).json({error:`Failed to generate spec`,details:String(e)})}}),r.get(`/`,(e,t)=>{let r=`${e.baseUrl}/openapi.json`,i=n.cdnUrl??N;t.setHeader(`Content-Type`,`text/html; charset=utf-8`),t.send(P(n.title??`API`,r,i))}),r}export{L as TypedRouter,U as createDocs,R as createTypedRouter,z as createTypedRouterWithConfig,B as createTypedRouterWithMiddleware,o as defaultValidationHandler,a as defineMiddleware,H as generateOpenApiSpec,D as inferJsonSchema,i as isSchemaError,n as parseSchema,r as safeParseSchema};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@minisylar/express-typed-router",
3
- "version": "1.9.7",
3
+ "version": "1.9.9",
4
4
  "description": "A strongly-typed Express router with Zod validation and automatic type inference for params, body, query, and middleware",
5
5
  "type": "module",
6
6
  "main": "dist/schema-router.cjs",