@minisylar/express-typed-router 1.8.1 → 1.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -195,7 +195,7 @@ app.use(
195
195
 
196
196
  - route paths, methods, and path parameters
197
197
  - query and body schemas (from `querySchema` / `bodySchema`)
198
- - response examplescaptured from real `res.json()` calls, no manual input needed
198
+ - **response schemas**inferred from real traffic (see [Response schemas](#response-schemas-from-live-traffic) below)
199
199
  - tags and summaries — inferred from route paths, or set manually
200
200
 
201
201
  **Custom route metadata:**
@@ -225,6 +225,49 @@ app.use("/docs", api.docs({ title: "My API", version: "1.0.0" }));
225
225
  // Discovers all sub-routers and merges routes with correct prefixes
226
226
  ```
227
227
 
228
+ ### Response schemas from live traffic
229
+
230
+ You don't have to declare what your routes return. The library **observes real responses** (`res.json` / `res.send`), **infers a JSON Schema** from them, and **merges across samples** — so it learns field types, which fields are nullable, and which are optional. This drives both the docs UI and `openapi-typescript` (real response types instead of `unknown`).
231
+
232
+ By default this runs in **redacted** mode: only the shape is kept, never the values — so no real user data is ever stored or shown.
233
+
234
+ ```ts
235
+ // A few responses like { id: 1, email: "a@b.com", nickname: "Al" } and
236
+ // { id: 2, email: null } are observed and merged into:
237
+ {
238
+ type: "object",
239
+ properties: {
240
+ id: { type: "integer" },
241
+ email: { type: ["string", "null"] }, // nullable — seen as null sometimes
242
+ nickname: { type: "string" } // optional — missing in some responses
243
+ },
244
+ required: ["id", "email"] // nickname excluded
245
+ }
246
+ ```
247
+
248
+ Control it with `sampleResponses`:
249
+
250
+ | Value | Behavior |
251
+ |---|---|
252
+ | `true` _(default)_ | **Redacted** — infer schema only. Real values discarded at capture time. Safe to expose. |
253
+ | `"live"` | Infer schema **and** attach one real captured response as an example. ⚠️ Examples contain actual data — use only for trusted/internal docs. |
254
+ | `false` | Don't observe responses at all. |
255
+
256
+ ```ts
257
+ // Safe default — schema only, no real data
258
+ app.use("/docs", api.docs({ title: "My API", version: "1.0.0" }));
259
+
260
+ // Show real example payloads (internal docs only)
261
+ app.use("/docs", api.docs({ title: "My API", version: "1.0.0", sampleResponses: "live" }));
262
+
263
+ // Disable entirely
264
+ app.use("/docs", api.docs({ title: "My API", version: "1.0.0", sampleResponses: false }));
265
+ ```
266
+
267
+ > Exclude an individual sensitive route from docs with `hidden: true` in its route options — works in any mode.
268
+
269
+ > **Note:** observed schemas live in memory and fill in as traffic flows (they reset on server restart). The live `/docs/openapi.json` endpoint always reflects the latest — so to get inferred **response** types in your generated client, point `openapi-typescript` at the running URL after exercising your routes. The `specOutputPath` file is written once at startup (before traffic), so it carries request schemas only.
270
+
228
271
  ### Schema library support for docs
229
272
 
230
273
  All validators work for **request validation**. For **OpenAPI schema generation** (showing field names and types in the spec), some libraries need an extra converter package installed in your project. This library auto-detects them at runtime — install the one you need and it just works, no config required.
@@ -240,7 +283,7 @@ All validators work for **request validation**. For **OpenAPI schema generation*
240
283
  | Joi | ✅ | ⚠️ | not supported — no official JSON Schema converter |
241
284
  | Decoders / ts.data.json / unhoax | ✅ | ⚠️ | not supported — no schema introspection |
242
285
 
243
- > **⚠️ Partial docs** means routes still appear in the spec with paths, methods, and captured response examples — only the request body/query field shapes are missing.
286
+ > **⚠️ Partial docs** means routes still appear in the spec with paths, methods, and inferred response schemas — only the request body/query field shapes are missing.
244
287
 
245
288
  ---
246
289
 
@@ -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,a)=>(a=n==null?{}:e(i(n)),o(r||!n||!n.__esModule?t(a,`default`,{value:n,enumerable:!0}):a,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){try{m??=await p(`module`),h??=await p(`url`);let t=globalThis.process?.cwd?.()??``,n=m.createRequire(h.pathToFileURL(t+`/`).href).resolve(e);return p(h.pathToFileURL(n).href)}catch{return p(e)}}const _=new WeakMap;function v(e){return e.replace(/:([^/?*.()]+)\??/g,`{$1}`)}function y(e){return[...e.matchAll(/:([^/?*.()]+)\??/g)].map(e=>e[1])}function b(e){let t=e.split(`/`).filter(Boolean)[0];return t&&!t.startsWith(`:`)?t:`default`}function x(e,t){let n=t.split(`/`).filter(e=>e&&!e.startsWith(`:`)),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=_.get(e);if(t)return t;let n=await C(e);return _.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 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 w(e){return e.replace(/&/g,`&amp;`).replace(/</g,`&lt;`).replace(/>/g,`&gt;`).replace(/"/g,`&quot;`)}const T=`https://cdn.jsdelivr.net/npm/@scalar/api-reference`;function E(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,a)=>(a=n==null?{}:e(i(n)),o(r||!n||!n.__esModule?t(a,`default`,{value:n,enumerable:!0}):a,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){try{m??=await p(`module`),h??=await p(`url`);let t=globalThis.process?.cwd?.()??``,n=m.createRequire(h.pathToFileURL(t+`/`).href).resolve(e);return p(h.pathToFileURL(n).href)}catch{return p(e)}}const _=new WeakMap;function v(e){return e.replace(/:([^/?*.()]+)\??/g,`{$1}`)}function y(e){return[...e.matchAll(/:([^/?*.()]+)\??/g)].map(e=>e[1])}function b(e){let t=e.split(`/`).filter(Boolean)[0];return t&&!t.startsWith(`:`)?t:`default`}function x(e,t){let n=t.split(`/`).filter(e=>e&&!e.startsWith(`:`)),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=_.get(e);if(t)return t;let n=await C(e);return _.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 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 w(e){if(e==null)return{type:`null`};if(Array.isArray(e)){if(e.length===0)return{type:`array`,items:{}};let t=w(e[0]);for(let n=1;n<e.length;n++)t=D(t,w(e[n]));return{type:`array`,items:t}}switch(typeof e){case`string`:return{type:`string`};case`boolean`:return{type:`boolean`};case`number`:return{type:Number.isInteger(e)?`integer`:`number`};case`object`:{let t={},n=[];for(let[r,i]of Object.entries(e))t[r]=w(i),n.push(r);let r={type:`object`,properties:t};return n.length&&(r.required=n),r}default:return{}}}function T(e){return!e||e.type===void 0?new Set:new Set(Array.isArray(e.type)?e.type:[e.type])}function E(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 D(e,t){if(!e||Object.keys(e).length===0)return t??{};if(!t||Object.keys(t).length===0)return e??{};let n=new Set([...T(e),...T(t)]),r={},i=E(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]=D(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=D(e.items,t.items);n&&Object.keys(n).length&&(r.items=n)}return r}function O(e){return e.replace(/&/g,`&amp;`).replace(/</g,`&lt;`).replace(/>/g,`&gt;`).replace(/"/g,`&quot;`)}const k=`https://cdn.jsdelivr.net/npm/@scalar/api-reference`;function A(e,t,n){return`<!doctype html>
2
2
  <html>
3
3
  <head>
4
- <title>${w(e)}</title>
4
+ <title>${O(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="${w(t)}"><\/script>
10
- <script src="${w(n)}"><\/script>
9
+ <script id="api-reference" data-url="${O(t)}"><\/script>
10
+ <script src="${O(n)}"><\/script>
11
11
  </body>
12
- </html>`}async function D(e,t){let n=Object.create(null);for(let t of e){if(t.method===`all`||t.hidden)continue;let e=v(t.path);n[e]||(n[e]={});let r=y(t.path).map(e=>({name:e,in:`path`,required:!0,schema:{type:`string`}}));if(t.querySchema){let e=await S(t.querySchema),n=e.properties??{},i=e.required??[];for(let[e,t]of Object.entries(n))r.push({name:e,in:`query`,required:i.includes(e),schema:t})}let i={summary:t.summary??x(t.method,t.path),tags:t.tags??[b(t.path)],parameters:r};t.description&&(i.description=t.description),t.deprecated&&(i.deprecated=!0),t.bodySchema&&(i.requestBody={required:!0,content:{"application/json":{schema:await S(t.bodySchema)}}});let a={};if(t.responseSamples.size>0)for(let[e,n]of t.responseSamples)a[String(e)]={description:e<400?`Success`:`Error`,content:{"application/json":{example:n}}};else a[200]={description:`Success`};i.responses=a,n[e][t.method]=i}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 O=new WeakMap;var k=class e{router;routes=[];mountedRouters=[];samplingEnabled=!1;constructor(){this.router=c.default.Router(),O.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=O.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,path:e+t.path})));return[...this.routes,...e]}enableSampling(e=new Set){if(!e.has(this)){e.add(this),this.samplingEnabled=!0;for(let{router:t}of this.mountedRouters)t.enableSampling(e)}}trackMounted(e,t){this.mountedRouters.some(n=>n.router===t&&n.prefix===e)||(this.mountedRouters.push({prefix:e,router:t}),this.samplingEnabled&&t.enableSampling())}docs(e={}){this.enableSampling();let t=c.default.Router();if(e.specOutputPath){let t=this,n=e.specOutputPath;setImmediate(async()=>{try{let r=await D(t.getRouteMetadata(),e);await(await p(`fs/promises`)).writeFile(n,JSON.stringify(r,null,2),`utf8`)}catch{}})}return t.get(`/openapi.json`,async(t,n)=>{try{let t=await D(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??T;n.setHeader(`Content-Type`,`text/html; charset=utf-8`),n.send(E(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.tags=e.tags,a.description=e.description,a.summary=e.summary,a.deprecated=e.deprecated,a.responseSchema=e.responseSchema,a.hidden=e.hidden,e.middleware&&i.push(...e.middleware),e.bodySchema&&i.push(this.createBodyValidationMiddleware(e.bodySchema)),e.querySchema&&i.push(this.createQueryValidationMiddleware(e.querySchema)),i.push(r)}else i.push(n);let o=0;return this.router[e](t,(e,t,n)=>{if(!this.samplingEnabled||a.hidden||o>=50||a.responseSamples.size>=10){n();return}o++;let r=t.json;t.json=e=>(a.responseSamples.has(t.statusCode)||a.responseSamples.set(t.statusCode,e),r.call(t,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)}}}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 A(){return new k}function j(e){let t=new k;return e?.errorHandler&&t.getRouter().use(e.errorHandler),t}function M(...e){let t=new k;for(let n of e)t=t.useMiddleware(n);return t}function N(e,t={}){let n=(Array.isArray(e)?e:[e]).map(e=>`prefix`in e?e:{prefix:``,router:e});for(let{router:e}of n)e.enableSampling();let r=c.default.Router();return r.get(`/openapi.json`,async(e,r)=>{try{let e=await D(n.flatMap(({prefix:e,router:t})=>t.getRouteMetadata().map(t=>({...t,path:e+t.path}))),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??T;n.setHeader(`Content-Type`,`text/html; charset=utf-8`),n.send(E(t.title??`API`,r,i))}),r}exports.TypedRouter=k,exports.createDocs=N,exports.createTypedRouter=A,exports.createTypedRouterWithConfig=j,exports.createTypedRouterWithMiddleware=M,exports.isSchemaError=f,exports.parseSchema=u,exports.safeParseSchema=d;
12
+ </html>`}async function j(e,t){let n=Object.create(null);for(let t of e){if(t.method===`all`||t.hidden)continue;let e=v(t.path);n[e]||(n[e]={});let r=y(t.path).map(e=>({name:e,in:`path`,required:!0,schema:{type:`string`}}));if(t.querySchema){let e=await S(t.querySchema),n=e.properties??{},i=e.required??[];for(let[e,t]of Object.entries(n))r.push({name:e,in:`query`,required:i.includes(e),schema:t})}let i={summary:t.summary??x(t.method,t.path),tags:t.tags??[b(t.path)],parameters:r};t.description&&(i.description=t.description),t.deprecated&&(i.deprecated=!0),t.bodySchema&&(i.requestBody={required:!0,content:{"application/json":{schema:await S(t.bodySchema)}}});let a={};if(t.responseSamples.size>0)for(let[e,n]of t.responseSamples){let t={schema:n.schema};n.example!==void 0&&(t.example=n.example),a[String(e)]={description:e<400?`Success`:`Error`,content:{"application/json":t}}}else a[200]={description:`Success`};i.responses=a,n[e][t.method]=i}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 M=new WeakMap;var N=class e{router;routes=[];mountedRouters=[];sampleMode=`off`;scheduleSpecWrite;constructor(){this.router=c.default.Router(),M.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=M.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,path:e+t.path})));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))}docs(e={}){let t=c.default.Router(),n;if(e.specOutputPath){let t=e.specOutputPath,r=async()=>{try{let n=await j(this.getRouteMetadata(),e);await(await p(`fs/promises`)).writeFile(t,JSON.stringify(n,null,2),`utf8`)}catch{}},i;n=()=>{i&&clearTimeout(i),i=setTimeout(r,300),i.unref?.()},setImmediate(r)}return e.sampleResponses!==!1&&this.enableSampling(e.sampleResponses===`live`?`live`:`redacted`,n),t.get(`/openapi.json`,async(t,n)=>{try{let t=await j(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??k;n.setHeader(`Content-Type`,`text/html; charset=utf-8`),n.send(A(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.tags=e.tags,a.description=e.description,a.summary=e.summary,a.deprecated=e.deprecated,a.responseSchema=e.responseSchema,a.hidden=e.hidden,e.middleware&&i.push(...e.middleware),e.bodySchema&&i.push(this.createBodyValidationMiddleware(e.bodySchema)),e.querySchema&&i.push(this.createQueryValidationMiddleware(e.querySchema)),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?D(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](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)}}}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 P(){return new N}function F(e){let t=new N;return e?.errorHandler&&t.getRouter().use(e.errorHandler),t}function I(...e){let t=new N;for(let n of e)t=t.useMiddleware(n);return t}function L(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 j(n.flatMap(({prefix:e,router:t})=>t.getRouteMetadata().map(t=>({...t,path:e+t.path}))),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??k;n.setHeader(`Content-Type`,`text/html; charset=utf-8`),n.send(A(t.title??`API`,r,i))}),r}exports.TypedRouter=N,exports.createDocs=L,exports.createTypedRouter=P,exports.createTypedRouterWithConfig=F,exports.createTypedRouterWithMiddleware=I,exports.isSchemaError=f,exports.parseSchema=u,exports.safeParseSchema=d;
@@ -148,6 +148,10 @@ interface DocsOptions {
148
148
  * Enables `openapi-typescript --watch` in development — the tool watches
149
149
  * the file and regenerates your client types automatically as routes change.
150
150
  *
151
+ * The file is written once at startup, so it contains route **schemas only**
152
+ * — never captured response examples. This keeps real response data (which
153
+ * may include PII) out of any file you might commit or share.
154
+ *
151
155
  * @example
152
156
  * // docs options
153
157
  * { specOutputPath: './openapi.json' }
@@ -156,6 +160,27 @@ interface DocsOptions {
156
160
  * // npx openapi-typescript ./openapi.json -o ./src/client.d.ts --watch
157
161
  */
158
162
  specOutputPath?: string;
163
+ /**
164
+ * Learn response shapes from live traffic and add them to the docs. The
165
+ * library observes real responses and **infers a JSON Schema** from them
166
+ * (field names, types, nullability, required vs optional) — so the docs and
167
+ * generated client types reflect what your API actually returns.
168
+ *
169
+ * Modes:
170
+ * - `true` (default) — **redacted**: infer the schema only. Real values are
171
+ * discarded at capture time, so no user data is ever stored or shown. Safe
172
+ * to expose.
173
+ * - `"live"` — infer the schema **and** attach a real captured response as an
174
+ * example. ⚠️ Examples contain actual data (emails, tokens, IDs). Only use
175
+ * for trusted/internal docs.
176
+ * - `false` — don't observe responses at all.
177
+ *
178
+ * Use the per-route `hidden: true` option to exclude individual sensitive
179
+ * routes regardless of mode.
180
+ *
181
+ * @default true
182
+ */
183
+ sampleResponses?: boolean | "live";
159
184
  }
160
185
  interface RouteMetadata {
161
186
  method: HttpMethod;
@@ -168,7 +193,10 @@ interface RouteMetadata {
168
193
  deprecated?: boolean;
169
194
  responseSchema?: AnyStandardSchema;
170
195
  hidden?: boolean;
171
- responseSamples: Map<number, unknown>;
196
+ responseSamples: Map<number, {
197
+ schema: Record<string, any>;
198
+ example?: unknown;
199
+ }>;
172
200
  }
173
201
  /**
174
202
  * Extra properties that middleware has added to the Express `req` object.
@@ -198,7 +226,8 @@ declare class TypedRouter<Req extends Record<string, any> = AdditionalReqProps,
198
226
  private router;
199
227
  private routes;
200
228
  private mountedRouters;
201
- private samplingEnabled;
229
+ private sampleMode;
230
+ private scheduleSpecWrite?;
202
231
  constructor();
203
232
  /**
204
233
  * Add typed middleware that extends the request with additional properties
@@ -260,15 +289,15 @@ declare class TypedRouter<Req extends Record<string, any> = AdditionalReqProps,
260
289
  */
261
290
  getRouteMetadata(): RouteMetadata[];
262
291
  /**
263
- * Turn on response sampling for this router and every router mounted under
264
- * it. Called by .docs() and createDocs() so examples are captured only when
265
- * docs are actually generated. The visited set guards against mount cycles.
292
+ * Turn on response observation for this router and every router mounted under
293
+ * it. Called by .docs() and createDocs() so it happens only when docs are
294
+ * actually generated. The visited set guards against mount cycles.
266
295
  * @internal Public only so createDocs() can reach it; not part of the API.
267
296
  */
268
- enableSampling(visited?: Set<TypedRouter<any, any>>): void;
297
+ enableSampling(mode?: "redacted" | "live", writer?: () => void, visited?: Set<TypedRouter<any, any>>): void;
269
298
  /**
270
299
  * Record a sub-router for docs, de-duplicating identical (prefix, router)
271
- * pairs and propagating sampling if docs were already requested.
300
+ * pairs and propagating the sample mode if docs were already requested.
272
301
  */
273
302
  private trackMounted;
274
303
  /**
@@ -148,6 +148,10 @@ interface DocsOptions {
148
148
  * Enables `openapi-typescript --watch` in development — the tool watches
149
149
  * the file and regenerates your client types automatically as routes change.
150
150
  *
151
+ * The file is written once at startup, so it contains route **schemas only**
152
+ * — never captured response examples. This keeps real response data (which
153
+ * may include PII) out of any file you might commit or share.
154
+ *
151
155
  * @example
152
156
  * // docs options
153
157
  * { specOutputPath: './openapi.json' }
@@ -156,6 +160,27 @@ interface DocsOptions {
156
160
  * // npx openapi-typescript ./openapi.json -o ./src/client.d.ts --watch
157
161
  */
158
162
  specOutputPath?: string;
163
+ /**
164
+ * Learn response shapes from live traffic and add them to the docs. The
165
+ * library observes real responses and **infers a JSON Schema** from them
166
+ * (field names, types, nullability, required vs optional) — so the docs and
167
+ * generated client types reflect what your API actually returns.
168
+ *
169
+ * Modes:
170
+ * - `true` (default) — **redacted**: infer the schema only. Real values are
171
+ * discarded at capture time, so no user data is ever stored or shown. Safe
172
+ * to expose.
173
+ * - `"live"` — infer the schema **and** attach a real captured response as an
174
+ * example. ⚠️ Examples contain actual data (emails, tokens, IDs). Only use
175
+ * for trusted/internal docs.
176
+ * - `false` — don't observe responses at all.
177
+ *
178
+ * Use the per-route `hidden: true` option to exclude individual sensitive
179
+ * routes regardless of mode.
180
+ *
181
+ * @default true
182
+ */
183
+ sampleResponses?: boolean | "live";
159
184
  }
160
185
  interface RouteMetadata {
161
186
  method: HttpMethod;
@@ -168,7 +193,10 @@ interface RouteMetadata {
168
193
  deprecated?: boolean;
169
194
  responseSchema?: AnyStandardSchema;
170
195
  hidden?: boolean;
171
- responseSamples: Map<number, unknown>;
196
+ responseSamples: Map<number, {
197
+ schema: Record<string, any>;
198
+ example?: unknown;
199
+ }>;
172
200
  }
173
201
  /**
174
202
  * Extra properties that middleware has added to the Express `req` object.
@@ -198,7 +226,8 @@ declare class TypedRouter<Req extends Record<string, any> = AdditionalReqProps,
198
226
  private router;
199
227
  private routes;
200
228
  private mountedRouters;
201
- private samplingEnabled;
229
+ private sampleMode;
230
+ private scheduleSpecWrite?;
202
231
  constructor();
203
232
  /**
204
233
  * Add typed middleware that extends the request with additional properties
@@ -260,15 +289,15 @@ declare class TypedRouter<Req extends Record<string, any> = AdditionalReqProps,
260
289
  */
261
290
  getRouteMetadata(): RouteMetadata[];
262
291
  /**
263
- * Turn on response sampling for this router and every router mounted under
264
- * it. Called by .docs() and createDocs() so examples are captured only when
265
- * docs are actually generated. The visited set guards against mount cycles.
292
+ * Turn on response observation for this router and every router mounted under
293
+ * it. Called by .docs() and createDocs() so it happens only when docs are
294
+ * actually generated. The visited set guards against mount cycles.
266
295
  * @internal Public only so createDocs() can reach it; not part of the API.
267
296
  */
268
- enableSampling(visited?: Set<TypedRouter<any, any>>): void;
297
+ enableSampling(mode?: "redacted" | "live", writer?: () => void, visited?: Set<TypedRouter<any, any>>): void;
269
298
  /**
270
299
  * Record a sub-router for docs, de-duplicating identical (prefix, router)
271
- * pairs and propagating sampling if docs were already requested.
300
+ * pairs and propagating the sample mode if docs were already requested.
272
301
  */
273
302
  private trackMounted;
274
303
  /**
@@ -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){try{o??=await a(`module`),s??=await a(`url`);let t=globalThis.process?.cwd?.()??``,n=o.createRequire(s.pathToFileURL(t+`/`).href).resolve(e);return a(s.pathToFileURL(n).href)}catch{return a(e)}}const l=new WeakMap;function u(e){return e.replace(/:([^/?*.()]+)\??/g,`{$1}`)}function d(e){return[...e.matchAll(/:([^/?*.()]+)\??/g)].map(e=>e[1])}function f(e){let t=e.split(`/`).filter(Boolean)[0];return t&&!t.startsWith(`:`)?t:`default`}function p(e,t){let n=t.split(`/`).filter(e=>e&&!e.startsWith(`:`)),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 m(e){let t=l.get(e);if(t)return t;let n=await h(e);return l.set(e,n),n}async function h(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 g(e){return e.replace(/&/g,`&amp;`).replace(/</g,`&lt;`).replace(/>/g,`&gt;`).replace(/"/g,`&quot;`)}const _=`https://cdn.jsdelivr.net/npm/@scalar/api-reference`;function v(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)}const a=Function(`m`,`return import(m)`);let o,s;async function c(e){try{o??=await a(`module`),s??=await a(`url`);let t=globalThis.process?.cwd?.()??``,n=o.createRequire(s.pathToFileURL(t+`/`).href).resolve(e);return a(s.pathToFileURL(n).href)}catch{return a(e)}}const l=new WeakMap;function u(e){return e.replace(/:([^/?*.()]+)\??/g,`{$1}`)}function d(e){return[...e.matchAll(/:([^/?*.()]+)\??/g)].map(e=>e[1])}function f(e){let t=e.split(`/`).filter(Boolean)[0];return t&&!t.startsWith(`:`)?t:`default`}function p(e,t){let n=t.split(`/`).filter(e=>e&&!e.startsWith(`:`)),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 m(e){let t=l.get(e);if(t)return t;let n=await h(e);return l.set(e,n),n}async function h(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 g(e){if(e==null)return{type:`null`};if(Array.isArray(e)){if(e.length===0)return{type:`array`,items:{}};let t=g(e[0]);for(let n=1;n<e.length;n++)t=y(t,g(e[n]));return{type:`array`,items:t}}switch(typeof e){case`string`:return{type:`string`};case`boolean`:return{type:`boolean`};case`number`:return{type:Number.isInteger(e)?`integer`:`number`};case`object`:{let t={},n=[];for(let[r,i]of Object.entries(e))t[r]=g(i),n.push(r);let r={type:`object`,properties:t};return n.length&&(r.required=n),r}default:return{}}}function _(e){return!e||e.type===void 0?new Set:new Set(Array.isArray(e.type)?e.type:[e.type])}function v(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 y(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),..._(t)]),r={},i=v(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]=y(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=y(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 x=`https://cdn.jsdelivr.net/npm/@scalar/api-reference`;function S(e,t,n){return`<!doctype html>
2
2
  <html>
3
3
  <head>
4
- <title>${g(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="${g(t)}"><\/script>
10
- <script src="${g(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 y(e,t){let n=Object.create(null);for(let t of e){if(t.method===`all`||t.hidden)continue;let e=u(t.path);n[e]||(n[e]={});let r=d(t.path).map(e=>({name:e,in:`path`,required:!0,schema:{type:`string`}}));if(t.querySchema){let e=await m(t.querySchema),n=e.properties??{},i=e.required??[];for(let[e,t]of Object.entries(n))r.push({name:e,in:`query`,required:i.includes(e),schema:t})}let i={summary:t.summary??p(t.method,t.path),tags:t.tags??[f(t.path)],parameters:r};t.description&&(i.description=t.description),t.deprecated&&(i.deprecated=!0),t.bodySchema&&(i.requestBody={required:!0,content:{"application/json":{schema:await m(t.bodySchema)}}});let a={};if(t.responseSamples.size>0)for(let[e,n]of t.responseSamples)a[String(e)]={description:e<400?`Success`:`Error`,content:{"application/json":{example:n}}};else a[200]={description:`Success`};i.responses=a,n[e][t.method]=i}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 b=new WeakMap;var x=class t{router;routes=[];mountedRouters=[];samplingEnabled=!1;constructor(){this.router=e.Router(),b.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=b.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,path:e+t.path})));return[...this.routes,...e]}enableSampling(e=new Set){if(!e.has(this)){e.add(this),this.samplingEnabled=!0;for(let{router:t}of this.mountedRouters)t.enableSampling(e)}}trackMounted(e,t){this.mountedRouters.some(n=>n.router===t&&n.prefix===e)||(this.mountedRouters.push({prefix:e,router:t}),this.samplingEnabled&&t.enableSampling())}docs(t={}){this.enableSampling();let n=e.Router();if(t.specOutputPath){let e=this,n=t.specOutputPath;setImmediate(async()=>{try{let r=await y(e.getRouteMetadata(),t);await(await a(`fs/promises`)).writeFile(n,JSON.stringify(r,null,2),`utf8`)}catch{}})}return n.get(`/openapi.json`,async(e,n)=>{try{let e=await y(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.setHeader(`Content-Type`,`text/html; charset=utf-8`),n.send(v(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.tags=e.tags,a.description=e.description,a.summary=e.summary,a.deprecated=e.deprecated,a.responseSchema=e.responseSchema,a.hidden=e.hidden,e.middleware&&i.push(...e.middleware),e.bodySchema&&i.push(this.createBodyValidationMiddleware(e.bodySchema)),e.querySchema&&i.push(this.createQueryValidationMiddleware(e.querySchema)),i.push(r)}else i.push(n);let o=0;return this.router[e](t,(e,t,n)=>{if(!this.samplingEnabled||a.hidden||o>=50||a.responseSamples.size>=10){n();return}o++;let r=t.json;t.json=e=>(a.responseSamples.has(t.statusCode)||a.responseSamples.set(t.statusCode,e),r.call(t,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)}}}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 S(){return new x}function C(e){let t=new x;return e?.errorHandler&&t.getRouter().use(e.errorHandler),t}function w(...e){let t=new x;for(let n of e)t=t.useMiddleware(n);return t}function T(t,n={}){let r=(Array.isArray(t)?t:[t]).map(e=>`prefix`in e?e:{prefix:``,router:e});for(let{router:e}of r)e.enableSampling();let i=e.Router();return i.get(`/openapi.json`,async(e,t)=>{try{let e=await y(r.flatMap(({prefix:e,router:t})=>t.getRouteMetadata().map(t=>({...t,path:e+t.path}))),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??_;t.setHeader(`Content-Type`,`text/html; charset=utf-8`),t.send(v(n.title??`API`,r,i))}),i}export{x as TypedRouter,T as createDocs,S as createTypedRouter,C as createTypedRouterWithConfig,w as createTypedRouterWithMiddleware,i as isSchemaError,n as parseSchema,r as safeParseSchema};
12
+ </html>`}async function C(e,t){let n=Object.create(null);for(let t of e){if(t.method===`all`||t.hidden)continue;let e=u(t.path);n[e]||(n[e]={});let r=d(t.path).map(e=>({name:e,in:`path`,required:!0,schema:{type:`string`}}));if(t.querySchema){let e=await m(t.querySchema),n=e.properties??{},i=e.required??[];for(let[e,t]of Object.entries(n))r.push({name:e,in:`query`,required:i.includes(e),schema:t})}let i={summary:t.summary??p(t.method,t.path),tags:t.tags??[f(t.path)],parameters:r};t.description&&(i.description=t.description),t.deprecated&&(i.deprecated=!0),t.bodySchema&&(i.requestBody={required:!0,content:{"application/json":{schema:await m(t.bodySchema)}}});let a={};if(t.responseSamples.size>0)for(let[e,n]of t.responseSamples){let t={schema:n.schema};n.example!==void 0&&(t.example=n.example),a[String(e)]={description:e<400?`Success`:`Error`,content:{"application/json":t}}}else a[200]={description:`Success`};i.responses=a,n[e][t.method]=i}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 T=class t{router;routes=[];mountedRouters=[];sampleMode=`off`;scheduleSpecWrite;constructor(){this.router=e.Router(),w.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=w.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,path:e+t.path})));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))}docs(t={}){let n=e.Router(),r;if(t.specOutputPath){let e=t.specOutputPath,n=async()=>{try{let n=await C(this.getRouteMetadata(),t);await(await a(`fs/promises`)).writeFile(e,JSON.stringify(n,null,2),`utf8`)}catch{}},i;r=()=>{i&&clearTimeout(i),i=setTimeout(n,300),i.unref?.()},setImmediate(n)}return t.sampleResponses!==!1&&this.enableSampling(t.sampleResponses===`live`?`live`:`redacted`,r),n.get(`/openapi.json`,async(e,n)=>{try{let e=await C(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??x;n.setHeader(`Content-Type`,`text/html; charset=utf-8`),n.send(S(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.tags=e.tags,a.description=e.description,a.summary=e.summary,a.deprecated=e.deprecated,a.responseSchema=e.responseSchema,a.hidden=e.hidden,e.middleware&&i.push(...e.middleware),e.bodySchema&&i.push(this.createBodyValidationMiddleware(e.bodySchema)),e.querySchema&&i.push(this.createQueryValidationMiddleware(e.querySchema)),i.push(r)}else i.push(n);let o=0,s=(e,t)=>{let n=e.statusCode,r=g(t),i=a.responseSamples.get(n),o=i?y(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](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)}}}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 E(){return new T}function D(e){let t=new T;return e?.errorHandler&&t.getRouter().use(e.errorHandler),t}function O(...e){let t=new T;for(let n of e)t=t.useMiddleware(n);return t}function k(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 C(r.flatMap(({prefix:e,router:t})=>t.getRouteMetadata().map(t=>({...t,path:e+t.path}))),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??x;t.setHeader(`Content-Type`,`text/html; charset=utf-8`),t.send(S(n.title??`API`,r,i))}),i}export{T as TypedRouter,k as createDocs,E as createTypedRouter,D as createTypedRouterWithConfig,O as createTypedRouterWithMiddleware,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.8.1",
3
+ "version": "1.9.0",
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",