@cleverbrush/server-openapi 0.0.0-beta-20260415102753 → 0.0.0-beta-20260415113443

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
@@ -181,6 +181,53 @@ const CreatePet = endpoint.post('/api/pets').body(PetBody);
181
181
 
182
182
  Code generators like openapi-generator and orval use the `discriminator` to produce proper tagged union types.
183
183
 
184
+ ## Recursive Schemas
185
+
186
+ Self-referential schemas (tree nodes, nested menus, threaded comments) are
187
+ supported via `lazy()` from `@cleverbrush/schema`. Call `.schemaName()` on the
188
+ root schema and `generateOpenApiSpec` will:
189
+
190
+ 1. Register the named schema in `components.schemas`, expanding its definition
191
+ exactly once.
192
+ 2. Replace every recursive reference inside the definition with the appropriate
193
+ `$ref` pointer — breaking the cycle automatically.
194
+
195
+ ```ts
196
+ import { object, number, array, lazy } from '@cleverbrush/schema';
197
+
198
+ type TreeNode = { value: number; children: TreeNode[] };
199
+
200
+ // TypeScript needs an explicit annotation for recursive types
201
+ const treeNode: ReturnType<typeof object> = object({
202
+ value: number(),
203
+ children: array(lazy(() => treeNode))
204
+ }).schemaName('TreeNode');
205
+
206
+ // Use treeNode as a body / response schema — no extra configuration needed
207
+ const CreateTree = endpoint.post('/api/tree').body(treeNode);
208
+ ```
209
+
210
+ Generated spec (abbreviated):
211
+
212
+ ```yaml
213
+ components:
214
+ schemas:
215
+ TreeNode:
216
+ type: object
217
+ properties:
218
+ value: { type: integer }
219
+ children:
220
+ type: array
221
+ items: { $ref: '#/components/schemas/TreeNode' }
222
+ paths:
223
+ /api/tree:
224
+ post:
225
+ requestBody:
226
+ content:
227
+ application/json:
228
+ schema: { $ref: '#/components/schemas/TreeNode' }
229
+ ```
230
+
184
231
  ## Authentication & Security Schemes
185
232
 
186
233
  Pass the server's `AuthenticationConfig` to automatically generate `securitySchemes` and per-operation `security` arrays:
package/dist/index.js CHANGED
@@ -1,2 +1,2 @@
1
- import*as O from"fs";import*as q from"path";import{toJsonSchema as T}from"@cleverbrush/schema-json";function M(e){return typeof e!="string"&&typeof e.validate=="function"}function _(e){let t=[];return{converted:e.replace(/:([a-zA-Z_][a-zA-Z0-9_]*)/g,(n,o)=>(t.push(o),`{${o}}`)),paramNames:t}}function L(e){let t=e.introspect(),r=t.templateDefinition,a=t.objectSchema.introspect().properties??{},m="";for(let s=0;s<r.segments.length;s++)m+=r.literals[s]+`{${r.segments[s].path}}`;m+=r.literals[r.segments.length]??"";let d=r.segments.map(s=>({name:s.path,schema:a[s.path]?T(a[s.path],{$schema:!1}):{type:"string"}}));return{pathString:m,parameters:d}}function w(e){let t=e.basePath.replace(/\/$/,""),r=e.pathTemplate;if(M(r)){let{pathString:p,parameters:y}=L(r);return{path:N(t+p),parameters:y}}let o=t+(r==="/"?"":r),{converted:a,paramNames:m}=_(o),d=N(a),s=m.map(p=>({name:p,schema:{type:"string"}}));return{path:d,parameters:s}}function N(e){let t=e.replace(/\/+/g,"/");return t.startsWith("/")||(t="/"+t),t.length>1&&t.endsWith("/")&&(t=t.slice(0,-1)),t||"/"}import{toJsonSchema as $}from"@cleverbrush/schema-json";function g(e,t){if(e==null)return{};let r;return t&&(typeof t=="function"?r=n=>t(n)??null:r=n=>t.getName(n)),$(e,{$schema:!1,draft:"2020-12",nameResolver:r})}var v=class{byInstance=new Map;byName=new Map;register(t){let r=t.introspect().schemaName;if(typeof r!="string")return;let n=this.byName.get(r);if(n!==void 0){if(n===t)return;throw new Error(`Schema name "${r}" is already registered by a different schema instance. Each named schema must be a single, reused constant. If you intended to register the same schema, ensure you are passing the same object reference (not a rebuilt schema).`)}this.byInstance.set(t,r),this.byName.set(r,t)}getName(t){return this.byInstance.get(t)??null}entries(){return this.byName.entries()}get isEmpty(){return this.byName.size===0}};function u(e,t,r=new Set){if(r.has(e))return;r.add(e),t.register(e);let n=e.introspect();switch(n.type){case"object":{let o=n.properties;if(o)for(let a of Object.values(o))u(a,t,r);break}case"array":n.elementSchema&&u(n.elementSchema,t,r);break;case"tuple":{let o=n.elements??[];for(let a of o)u(a,t,r);n.restSchema&&u(n.restSchema,t,r);break}case"union":{let o=n.options??[];for(let a of o)u(a,t,r);break}case"record":n.keySchema&&u(n.keySchema,t,r),n.valueSchema&&u(n.valueSchema,t,r);break;default:break}}function k(e){if(!e)return{};let t={};for(let r of e.schemes){let n=r.name,o=r.challenge?.();if(o?.headerValue?.toLowerCase().startsWith("bearer")||n==="jwt")t[n]={type:"http",scheme:"bearer",bearerFormat:"JWT"};else if(n==="cookie"||r.cookieName){let a=r.cookieName??r._options?.cookieName??"session";t[n]={type:"apiKey",in:"cookie",name:a}}else o?t[n]={type:"http",scheme:o.headerValue.split(" ")[0].toLowerCase()}:t[n]={type:"http",scheme:n}}return t}function B(e,t){return e===null?[]:t.length===0?[]:t.map(r=>({[r]:[...e]}))}function j(e,t,r,n,o){let a={name:e,in:t,schema:r};return n&&(a.required=!0),o&&(a.description=o),a}function z(e,t){let r=g(e,t),n=e.introspect(),o={required:n.isRequired!==!1,content:{"application/json":{schema:r}}};return typeof n.description=="string"&&n.description!==""&&(o.description=n.description),o}var F={200:"OK",201:"Created",202:"Accepted",204:"No Content",400:"Bad Request",401:"Unauthorized",403:"Forbidden",404:"Not Found",409:"Conflict",422:"Unprocessable Entity",500:"Internal Server Error"},x={type:"object",properties:{status:{type:"integer"},title:{type:"string"},detail:{type:"string"}}};function U(e,t,r){let n={};if(e.responsesSchemas)for(let[o,a]of Object.entries(e.responsesSchemas)){let m=Number(o),d=F[m]??`Response ${o}`;if(a){let s=g(a,r),p=a.introspect(),y=typeof p.description=="string"&&p.description!==""?p.description:d;n[o]={description:y,content:{"application/json":{schema:s}}}}else n[o]={description:d}}else if(e.responseSchema){let o=g(e.responseSchema,r),a=e.responseSchema.introspect(),m=typeof a.description=="string"&&a.description!==""?a.description:"Successful response";n[200]={description:m,content:{"application/json":{schema:o}}}}else t==="DELETE"||t==="HEAD"?n[204]={description:"No content"}:n[200]={description:"Successful response"};return e.bodySchema&&!n[422]&&(n[422]={description:"Validation error",content:{"application/problem+json":{schema:x}}}),e.authRoles!==null&&(n[401]||(n[401]={description:"Unauthorized",content:{"application/problem+json":{schema:x}}}),n[403]||(n[403]={description:"Forbidden",content:{"application/problem+json":{schema:x}}})),n}function J(e,t,r,n){let o={};e.summary&&(o.summary=e.summary),e.description&&(o.description=e.description),e.tags.length>0&&(o.tags=[...e.tags]),e.operationId&&(o.operationId=e.operationId),e.deprecated&&(o.deprecated=!0);let a=[];for(let d of t)a.push(j(d.name,"path",d.schema,!0));if(e.querySchema){let s=e.querySchema.introspect().properties??{};for(let[p,y]of Object.entries(s)){let h=y.introspect(),f=h.isRequired!==!1,A=typeof h.description=="string"&&h.description!==""?h.description:void 0;a.push(j(p,"query",g(y,n),f,A))}}if(e.headerSchema){let s=e.headerSchema.introspect().properties??{};for(let[p,y]of Object.entries(s)){let h=y.introspect(),f=h.isRequired!==!1,A=typeof h.description=="string"&&h.description!==""?h.description:void 0;a.push(j(p,"header",g(y,n),f,A))}}a.length>0&&(o.parameters=a),e.bodySchema&&(o.requestBody=z(e.bodySchema,n)),o.responses=U(e,e.method.toUpperCase(),n);let m=B(H(e),r);return m.length>0&&(o.security=m),o}function H(e){return e.authRoles}function S(e){let{registrations:t,info:r,servers:n,authConfig:o,securitySchemes:a,tags:m}=e,d=a??k(o),s=Object.keys(d),p=new v,y=new Set;for(let c of t){let i=c.endpoint;if(i.bodySchema&&u(i.bodySchema,p,y),i.responseSchema&&u(i.responseSchema,p,y),i.responsesSchemas)for(let l of Object.values(i.responsesSchemas))l&&u(l,p,y);if(i.querySchema){let l=i.querySchema.introspect().properties??{};for(let R of Object.values(l))u(R,p,y)}if(i.headerSchema){let l=i.headerSchema.introspect().properties??{};for(let R of Object.values(l))u(R,p,y)}}let h=c=>i=>i===c?void 0:p.getName(i),f={};for(let c of t){let i=c.endpoint,{path:l,parameters:R}=w(i),D=i.method.toLowerCase();f[l]||(f[l]={}),f[l][D]=J(i,R,s,p)}let A=new Set((m??[]).map(c=>c.name)),P=[];for(let c of t)for(let i of c.endpoint.tags)!A.has(i)&&!P.includes(i)&&P.push(i);P.sort();let E=[...m??[],...P.map(c=>({name:c}))],b={openapi:"3.1.0",info:{...r}};n&&n.length>0&&(b.servers=n.map(c=>({...c}))),E.length>0&&(b.tags=E.map(c=>({...c}))),b.paths=f;let I={};for(let[c,i]of p.entries())I[c]=g(i,h(i));let C=Object.keys(I).length>0;if(s.length>0||C){let c={};s.length>0&&(c.securitySchemes={...d}),C&&(c.schemas=I),b.components=c}return b}function W(e,t){let r=S(e),n=q.dirname(t);return n&&!O.existsSync(n)&&O.mkdirSync(n,{recursive:!0}),O.writeFileSync(t,JSON.stringify(r,null,2),"utf-8"),r}import{endpoint as V}from"@cleverbrush/server";function K(e){let t=e.path??"/openapi.json",r=V.get(t).summary("OpenAPI specification").tags("OpenAPI").operationId("getOpenApiSpec"),n=null;return{endpoint:r,handler:()=>(n||(n=S({registrations:e.getRegistrations(),info:e.info,servers:e.servers,authConfig:e.authConfig,securitySchemes:e.securitySchemes})),n)}}function Z(e){let t=e.path??"/openapi.json",r=null;return async(n,o)=>{let m=n.url.pathname;if(n.method.toUpperCase()==="GET"&&m===t){r||(r=S({registrations:e.getRegistrations(),info:e.info,servers:e.servers,authConfig:e.authConfig,securitySchemes:e.securitySchemes}));let s=JSON.stringify(r);n.response.writeHead(200,{"content-type":"application/json","content-length":Buffer.byteLength(s).toString()}),n.response.end(s);return}await o()}}export{v as SchemaRegistry,g as convertSchema,K as createOpenApiEndpoint,S as generateOpenApiSpec,B as mapOperationSecurity,k as mapSecuritySchemes,w as resolvePath,Z as serveOpenApi,u as walkSchemas,W as writeOpenApiSpec};
1
+ import*as O from"fs";import*as q from"path";import{toJsonSchema as T}from"@cleverbrush/schema-json";function M(e){return typeof e!="string"&&typeof e.validate=="function"}function _(e){let t=[];return{converted:e.replace(/:([a-zA-Z_][a-zA-Z0-9_]*)/g,(n,o)=>(t.push(o),`{${o}}`)),paramNames:t}}function z(e){let t=e.introspect(),r=t.templateDefinition,a=t.objectSchema.introspect().properties??{},m="";for(let s=0;s<r.segments.length;s++)m+=r.literals[s]+`{${r.segments[s].path}}`;m+=r.literals[r.segments.length]??"";let d=r.segments.map(s=>({name:s.path,schema:a[s.path]?T(a[s.path],{$schema:!1}):{type:"string"}}));return{pathString:m,parameters:d}}function k(e){let t=e.basePath.replace(/\/$/,""),r=e.pathTemplate;if(M(r)){let{pathString:p,parameters:y}=z(r);return{path:N(t+p),parameters:y}}let o=t+(r==="/"?"":r),{converted:a,paramNames:m}=_(o),d=N(a),s=m.map(p=>({name:p,schema:{type:"string"}}));return{path:d,parameters:s}}function N(e){let t=e.replace(/\/+/g,"/");return t.startsWith("/")||(t="/"+t),t.length>1&&t.endsWith("/")&&(t=t.slice(0,-1)),t||"/"}import{toJsonSchema as L}from"@cleverbrush/schema-json";function g(e,t){if(e==null)return{};let r;return t&&(typeof t=="function"?r=n=>t(n)??null:r=n=>t.getName(n)),L(e,{$schema:!1,draft:"2020-12",nameResolver:r})}var v=class{byInstance=new Map;byName=new Map;register(t){let r=t.introspect().schemaName;if(typeof r!="string")return;let n=this.byName.get(r);if(n!==void 0){if(n===t)return;throw new Error(`Schema name "${r}" is already registered by a different schema instance. Each named schema must be a single, reused constant. If you intended to register the same schema, ensure you are passing the same object reference (not a rebuilt schema).`)}this.byInstance.set(t,r),this.byName.set(r,t)}getName(t){return this.byInstance.get(t)??null}entries(){return this.byName.entries()}get isEmpty(){return this.byName.size===0}};function u(e,t,r=new Set){if(r.has(e))return;r.add(e),t.register(e);let n=e.introspect();switch(n.type){case"object":{let o=n.properties;if(o)for(let a of Object.values(o))u(a,t,r);break}case"array":n.elementSchema&&u(n.elementSchema,t,r);break;case"tuple":{let o=n.elements??[];for(let a of o)u(a,t,r);n.restSchema&&u(n.restSchema,t,r);break}case"union":{let o=n.options??[];for(let a of o)u(a,t,r);break}case"record":n.keySchema&&u(n.keySchema,t,r),n.valueSchema&&u(n.valueSchema,t,r);break;case"lazy":{let o=e.resolve();u(o,t,r);break}default:break}}function w(e){if(!e)return{};let t={};for(let r of e.schemes){let n=r.name,o=r.challenge?.();if(o?.headerValue?.toLowerCase().startsWith("bearer")||n==="jwt")t[n]={type:"http",scheme:"bearer",bearerFormat:"JWT"};else if(n==="cookie"||r.cookieName){let a=r.cookieName??r._options?.cookieName??"session";t[n]={type:"apiKey",in:"cookie",name:a}}else o?t[n]={type:"http",scheme:o.headerValue.split(" ")[0].toLowerCase()}:t[n]={type:"http",scheme:n}}return t}function B(e,t){return e===null?[]:t.length===0?[]:t.map(r=>({[r]:[...e]}))}function j(e,t,r,n,o){let a={name:e,in:t,schema:r};return n&&(a.required=!0),o&&(a.description=o),a}function $(e,t){let r=g(e,t),n=e.introspect(),o={required:n.isRequired!==!1,content:{"application/json":{schema:r}}};return typeof n.description=="string"&&n.description!==""&&(o.description=n.description),o}var F={200:"OK",201:"Created",202:"Accepted",204:"No Content",400:"Bad Request",401:"Unauthorized",403:"Forbidden",404:"Not Found",409:"Conflict",422:"Unprocessable Entity",500:"Internal Server Error"},x={type:"object",properties:{status:{type:"integer"},title:{type:"string"},detail:{type:"string"}}};function U(e,t,r){let n={};if(e.responsesSchemas)for(let[o,a]of Object.entries(e.responsesSchemas)){let m=Number(o),d=F[m]??`Response ${o}`;if(a){let s=g(a,r),p=a.introspect(),y=typeof p.description=="string"&&p.description!==""?p.description:d;n[o]={description:y,content:{"application/json":{schema:s}}}}else n[o]={description:d}}else if(e.responseSchema){let o=g(e.responseSchema,r),a=e.responseSchema.introspect(),m=typeof a.description=="string"&&a.description!==""?a.description:"Successful response";n[200]={description:m,content:{"application/json":{schema:o}}}}else t==="DELETE"||t==="HEAD"?n[204]={description:"No content"}:n[200]={description:"Successful response"};return e.bodySchema&&!n[422]&&(n[422]={description:"Validation error",content:{"application/problem+json":{schema:x}}}),e.authRoles!==null&&(n[401]||(n[401]={description:"Unauthorized",content:{"application/problem+json":{schema:x}}}),n[403]||(n[403]={description:"Forbidden",content:{"application/problem+json":{schema:x}}})),n}function J(e,t,r,n){let o={};e.summary&&(o.summary=e.summary),e.description&&(o.description=e.description),e.tags.length>0&&(o.tags=[...e.tags]),e.operationId&&(o.operationId=e.operationId),e.deprecated&&(o.deprecated=!0);let a=[];for(let d of t)a.push(j(d.name,"path",d.schema,!0));if(e.querySchema){let s=e.querySchema.introspect().properties??{};for(let[p,y]of Object.entries(s)){let h=y.introspect(),f=h.isRequired!==!1,A=typeof h.description=="string"&&h.description!==""?h.description:void 0;a.push(j(p,"query",g(y,n),f,A))}}if(e.headerSchema){let s=e.headerSchema.introspect().properties??{};for(let[p,y]of Object.entries(s)){let h=y.introspect(),f=h.isRequired!==!1,A=typeof h.description=="string"&&h.description!==""?h.description:void 0;a.push(j(p,"header",g(y,n),f,A))}}a.length>0&&(o.parameters=a),e.bodySchema&&(o.requestBody=$(e.bodySchema,n)),o.responses=U(e,e.method.toUpperCase(),n);let m=B(H(e),r);return m.length>0&&(o.security=m),o}function H(e){return e.authRoles}function S(e){let{registrations:t,info:r,servers:n,authConfig:o,securitySchemes:a,tags:m}=e,d=a??w(o),s=Object.keys(d),p=new v,y=new Set;for(let c of t){let i=c.endpoint;if(i.bodySchema&&u(i.bodySchema,p,y),i.responseSchema&&u(i.responseSchema,p,y),i.responsesSchemas)for(let l of Object.values(i.responsesSchemas))l&&u(l,p,y);if(i.querySchema){let l=i.querySchema.introspect().properties??{};for(let R of Object.values(l))u(R,p,y)}if(i.headerSchema){let l=i.headerSchema.introspect().properties??{};for(let R of Object.values(l))u(R,p,y)}}let h=c=>{let i=!1;return l=>{if(l===c&&!i){i=!0;return}return p.getName(l)??void 0}},f={};for(let c of t){let i=c.endpoint,{path:l,parameters:R}=k(i),D=i.method.toLowerCase();f[l]||(f[l]={}),f[l][D]=J(i,R,s,p)}let A=new Set((m??[]).map(c=>c.name)),P=[];for(let c of t)for(let i of c.endpoint.tags)!A.has(i)&&!P.includes(i)&&P.push(i);P.sort();let E=[...m??[],...P.map(c=>({name:c}))],b={openapi:"3.1.0",info:{...r}};n&&n.length>0&&(b.servers=n.map(c=>({...c}))),E.length>0&&(b.tags=E.map(c=>({...c}))),b.paths=f;let I={};for(let[c,i]of p.entries())I[c]=g(i,h(i));let C=Object.keys(I).length>0;if(s.length>0||C){let c={};s.length>0&&(c.securitySchemes={...d}),C&&(c.schemas=I),b.components=c}return b}function W(e,t){let r=S(e),n=q.dirname(t);return n&&!O.existsSync(n)&&O.mkdirSync(n,{recursive:!0}),O.writeFileSync(t,JSON.stringify(r,null,2),"utf-8"),r}import{endpoint as V}from"@cleverbrush/server";function K(e){let t=e.path??"/openapi.json",r=V.get(t).summary("OpenAPI specification").tags("OpenAPI").operationId("getOpenApiSpec"),n=null;return{endpoint:r,handler:()=>(n||(n=S({registrations:e.getRegistrations(),info:e.info,servers:e.servers,authConfig:e.authConfig,securitySchemes:e.securitySchemes})),n)}}function Z(e){let t=e.path??"/openapi.json",r=null;return async(n,o)=>{let m=n.url.pathname;if(n.method.toUpperCase()==="GET"&&m===t){r||(r=S({registrations:e.getRegistrations(),info:e.info,servers:e.servers,authConfig:e.authConfig,securitySchemes:e.securitySchemes}));let s=JSON.stringify(r);n.response.writeHead(200,{"content-type":"application/json","content-length":Buffer.byteLength(s).toString()}),n.response.end(s);return}await o()}}export{v as SchemaRegistry,g as convertSchema,K as createOpenApiEndpoint,S as generateOpenApiSpec,B as mapOperationSecurity,w as mapSecuritySchemes,k as resolvePath,Z as serveOpenApi,u as walkSchemas,W as writeOpenApiSpec};
2
2
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/cli.ts","../src/pathUtils.ts","../src/schemaConverter.ts","../src/schemaRegistry.ts","../src/securityMapper.ts","../src/generateOpenApiSpec.ts","../src/openApiEndpoint.ts","../src/serveOpenApi.ts"],"sourcesContent":["import * as fs from 'node:fs';\nimport * as path from 'node:path';\nimport {\n generateOpenApiSpec,\n type OpenApiDocument,\n type OpenApiOptions\n} from './generateOpenApiSpec.js';\n\n/**\n * Generate an OpenAPI spec and write it to a file.\n *\n * @param options - Same options as `generateOpenApiSpec()`.\n * @param outputPath - File path to write the JSON spec to.\n * @returns The generated spec document.\n *\n * @example\n * ```ts\n * import { writeOpenApiSpec } from '@cleverbrush/server-openapi';\n * import { createServer, endpoint } from '@cleverbrush/server';\n *\n * const builder = createServer()\n * .handle(endpoint.get('/api/health'), () => ({ ok: true }));\n *\n * writeOpenApiSpec(\n * {\n * registrations: builder.getRegistrations(),\n * info: { title: 'My API', version: '1.0.0' }\n * },\n * './openapi.json'\n * );\n * ```\n */\nexport function writeOpenApiSpec(\n options: OpenApiOptions,\n outputPath: string\n): OpenApiDocument {\n const spec = generateOpenApiSpec(options);\n const dir = path.dirname(outputPath);\n if (dir && !fs.existsSync(dir)) {\n fs.mkdirSync(dir, { recursive: true });\n }\n fs.writeFileSync(outputPath, JSON.stringify(spec, null, 2), 'utf-8');\n return spec;\n}\n","import type {\n ParseStringSchemaBuilder,\n SchemaBuilder\n} from '@cleverbrush/schema';\nimport { toJsonSchema } from '@cleverbrush/schema-json';\nimport type { EndpointMetadata } from '@cleverbrush/server';\n\ntype RoutePath = string | ParseStringSchemaBuilder<any, any, any, any, any>;\n\nfunction isParseStringSchema(\n p: RoutePath\n): p is ParseStringSchemaBuilder<any, any, any, any, any> {\n return typeof p !== 'string' && typeof (p as any).validate === 'function';\n}\n\n/**\n * Describes a single path parameter extracted from a route template,\n * including its name and the JSON Schema representation of its type.\n */\nexport interface PathParameterInfo {\n readonly name: string;\n readonly schema: Record<string, unknown>;\n}\n\n/**\n * The output of resolving a route path template to an OpenAPI-compatible\n * path string and its parameter list.\n */\nexport interface ResolvedPath {\n /** OpenAPI-formatted path, e.g. `/api/users/{id}` */\n readonly path: string;\n /** Extracted path parameters with their JSON Schema */\n readonly parameters: readonly PathParameterInfo[];\n}\n\n/**\n * Convert a colon-style static path to OpenAPI `{param}` format.\n * E.g. `/users/:id/posts/:pid` → `/users/{id}/posts/{pid}`\n */\nfunction convertColonParams(path: string): {\n converted: string;\n paramNames: string[];\n} {\n const paramNames: string[] = [];\n const converted = path.replace(/:([a-zA-Z_][a-zA-Z0-9_]*)/g, (_, name) => {\n paramNames.push(name);\n return `{${name}}`;\n });\n return { converted, paramNames };\n}\n\n/**\n * Convert a `ParseStringSchemaBuilder` path template to an OpenAPI-formatted\n * path string and extract parameter schemas.\n */\nfunction convertParseStringPath(\n pathTemplate: ParseStringSchemaBuilder<any, any, any, any, any>\n): { pathString: string; parameters: PathParameterInfo[] } {\n const info = pathTemplate.introspect() as any;\n const templateDef = info.templateDefinition as {\n literals: string[];\n segments: { path: string }[];\n };\n const objectSchema = info.objectSchema;\n const objInfo = objectSchema.introspect() as any;\n const props: Record<\n string,\n SchemaBuilder<any, any, any>\n > = objInfo.properties ?? {};\n\n let pathString = '';\n for (let i = 0; i < templateDef.segments.length; i++) {\n pathString +=\n templateDef.literals[i] + `{${templateDef.segments[i].path}}`;\n }\n pathString += templateDef.literals[templateDef.segments.length] ?? '';\n\n const parameters: PathParameterInfo[] = templateDef.segments.map(seg => ({\n name: seg.path,\n schema: props[seg.path]\n ? toJsonSchema(props[seg.path], { $schema: false })\n : { type: 'string' }\n }));\n\n return { pathString, parameters };\n}\n\n/**\n * Combines `basePath` and `pathTemplate` from an endpoint into an\n * OpenAPI-formatted path with extracted parameter information.\n */\nexport function resolvePath(meta: EndpointMetadata): ResolvedPath {\n const basePath = meta.basePath.replace(/\\/$/, '');\n const pathTemplate = meta.pathTemplate;\n\n if (isParseStringSchema(pathTemplate)) {\n const { pathString, parameters } = convertParseStringPath(pathTemplate);\n const fullPath = normalizeSlashes(basePath + pathString);\n return { path: fullPath, parameters };\n }\n\n // Static string path template\n const templateStr = pathTemplate === '/' ? '' : pathTemplate;\n const combined = basePath + templateStr;\n const { converted, paramNames } = convertColonParams(combined);\n const fullPath = normalizeSlashes(converted);\n\n const parameters: PathParameterInfo[] = paramNames.map(name => ({\n name,\n schema: { type: 'string' }\n }));\n\n return { path: fullPath, parameters };\n}\n\nfunction normalizeSlashes(path: string): string {\n // Replace double slashes with single, ensure leading slash\n let result = path.replace(/\\/+/g, '/');\n if (!result.startsWith('/')) result = '/' + result;\n if (result.length > 1 && result.endsWith('/')) result = result.slice(0, -1);\n return result || '/';\n}\n","import type { SchemaBuilder } from '@cleverbrush/schema';\nimport { toJsonSchema } from '@cleverbrush/schema-json';\nimport type { SchemaRegistry } from './schemaRegistry.js';\n\ntype NameResolver = (\n schema: SchemaBuilder<any, any, any>\n) => string | null | undefined;\n\n/**\n * Converts a `@cleverbrush/schema` builder to a JSON Schema object suitable\n * for embedding in an OpenAPI 3.1 spec (no `$schema` header, Draft 2020-12).\n *\n * Returns an empty schema `{}` when the input is `null` or `undefined`.\n *\n * When a {@link SchemaRegistry} or a custom resolver function is provided, any\n * schema instance that resolves to a name will be emitted as a\n * `$ref: '#/components/schemas/<name>'` pointer instead of being inlined.\n *\n * @param schema - The schema to convert, or `null`/`undefined`.\n * @param registry - Optional registry or resolver function for `$ref` deduplication.\n */\nexport function convertSchema(\n schema: SchemaBuilder<any, any, any, any, any> | null | undefined,\n registry?: SchemaRegistry | NameResolver\n): Record<string, unknown> {\n if (schema == null) return {};\n\n let nameResolver:\n | ((s: SchemaBuilder<any, any, any>) => string | null)\n | undefined;\n if (registry) {\n if (typeof registry === 'function') {\n nameResolver = s => registry(s) ?? null;\n } else {\n nameResolver = s => registry.getName(s);\n }\n }\n\n return toJsonSchema(schema, {\n $schema: false,\n draft: '2020-12',\n nameResolver\n });\n}\n","import type { SchemaBuilder } from '@cleverbrush/schema';\n\n// ---------------------------------------------------------------------------\n// SchemaRegistry\n// ---------------------------------------------------------------------------\n\n/**\n * Collects schemas that carry an explicit component name (set via\n * `.schemaName()`) and provides a reference-based lookup used during OpenAPI\n * spec generation to replace inline schema objects with\n * `$ref: '#/components/schemas/<name>'` pointers.\n *\n * **Conflict rule**: registering two *different* schema instances (different\n * object references) under the same name throws immediately. Re-registering\n * the same instance is a no-op.\n *\n * @example\n * ```ts\n * const registry = new SchemaRegistry();\n * registry.register(UserSchema); // UserSchema.schemaName('User')\n * registry.getName(UserSchema); // 'User'\n * registry.getName(someOtherSchema); // null\n * ```\n */\nexport class SchemaRegistry {\n /** schema instance → registered name */\n private readonly byInstance = new Map<\n SchemaBuilder<any, any, any>,\n string\n >();\n /** name → first-registered schema instance */\n private readonly byName = new Map<string, SchemaBuilder<any, any, any>>();\n\n /**\n * Attempts to register `schema` in the registry.\n *\n * - If the schema has no `schemaName` in its introspect output, it is\n * silently skipped.\n * - If the same instance is already registered, this is a no-op.\n * - If a **different** instance is already registered under the same name,\n * an error is thrown.\n *\n * @param schema - The schema builder to register.\n * @throws {Error} When two distinct schema instances share the same name.\n */\n register(schema: SchemaBuilder<any, any, any>): void {\n const name = (schema.introspect() as any).schemaName as\n | string\n | undefined;\n if (typeof name !== 'string') return;\n\n const existing = this.byName.get(name);\n if (existing !== undefined) {\n // Same instance → idempotent, nothing to do\n if (existing === schema) return;\n // Different instance → conflict\n throw new Error(\n `Schema name \"${name}\" is already registered by a different schema instance. ` +\n `Each named schema must be a single, reused constant. ` +\n `If you intended to register the same schema, ensure you are passing ` +\n `the same object reference (not a rebuilt schema).`\n );\n }\n\n this.byInstance.set(schema, name);\n this.byName.set(name, schema);\n }\n\n /**\n * Returns the component name for a given schema instance, or `null` if it\n * was not registered.\n *\n * @param schema - The schema builder to look up.\n * @returns The registered name, or `null`.\n */\n getName(schema: SchemaBuilder<any, any, any>): string | null {\n return this.byInstance.get(schema) ?? null;\n }\n\n /**\n * Iterates over all registered `[name, schema]` pairs in insertion order.\n *\n * Used to emit the `components.schemas` section of an OpenAPI document.\n */\n entries(): IterableIterator<[string, SchemaBuilder<any, any, any>]> {\n return this.byName.entries();\n }\n\n /** Returns `true` when at least one schema has been registered. */\n get isEmpty(): boolean {\n return this.byName.size === 0;\n }\n}\n\n// ---------------------------------------------------------------------------\n// walkSchemas\n// ---------------------------------------------------------------------------\n\n/**\n * Recursively visits every {@link SchemaBuilder} reachable from `schema` and\n * calls {@link SchemaRegistry.register} on each node.\n *\n * Cycle detection is performed via a `visited` `Set` of object references, so\n * schemas may safely be shared across multiple branches without causing\n * infinite recursion.\n *\n * **Excluded schema types**\n * - `lazy` — deferred resolution would require calling the getter, which may\n * itself reference the parent schema; lazy schemas are handled separately.\n *\n * @param schema - Root schema to start the walk from.\n * @param registry - Registry to register named schemas into.\n * @param visited - Shared set for cycle detection; pass a new `Set()` for the\n * top-level call.\n */\nexport function walkSchemas(\n schema: SchemaBuilder<any, any, any>,\n registry: SchemaRegistry,\n visited: Set<SchemaBuilder<any, any, any>> = new Set()\n): void {\n if (visited.has(schema)) return;\n visited.add(schema);\n\n registry.register(schema);\n\n const info = schema.introspect() as any;\n\n switch (info.type) {\n case 'object': {\n const props = info.properties as\n | Record<string, SchemaBuilder<any, any, any>>\n | undefined;\n if (props) {\n for (const child of Object.values(props)) {\n walkSchemas(child, registry, visited);\n }\n }\n break;\n }\n case 'array':\n if (info.elementSchema) {\n walkSchemas(info.elementSchema, registry, visited);\n }\n break;\n case 'tuple': {\n const elements: SchemaBuilder<any, any, any>[] =\n info.elements ?? [];\n for (const el of elements) {\n walkSchemas(el, registry, visited);\n }\n if (info.restSchema) {\n walkSchemas(info.restSchema, registry, visited);\n }\n break;\n }\n case 'union': {\n const options: SchemaBuilder<any, any, any>[] = info.options ?? [];\n for (const opt of options) {\n walkSchemas(opt, registry, visited);\n }\n break;\n }\n case 'record':\n if (info.keySchema) {\n walkSchemas(info.keySchema, registry, visited);\n }\n if (info.valueSchema) {\n walkSchemas(info.valueSchema, registry, visited);\n }\n break;\n // 'lazy' intentionally excluded — resolved lazily, handled separately\n default:\n break;\n }\n}\n","import type { AuthenticationConfig } from '@cleverbrush/server';\n\n/**\n * An OpenAPI 3.1 Security Scheme Object.\n * Describes how requests to the API are authenticated.\n *\n * @see {@link https://spec.openapis.org/oas/v3.1.0#security-scheme-object OpenAPI Security Scheme Object}\n */\nexport interface OpenApiSecurityScheme {\n readonly type: string;\n readonly scheme?: string;\n readonly bearerFormat?: string;\n readonly in?: string;\n readonly name?: string;\n}\n\n/**\n * Map `@cleverbrush/auth` authentication schemes to OpenAPI security scheme objects.\n */\nexport function mapSecuritySchemes(\n authConfig: AuthenticationConfig | null | undefined\n): Record<string, OpenApiSecurityScheme> {\n if (!authConfig) return {};\n\n const result: Record<string, OpenApiSecurityScheme> = {};\n for (const scheme of authConfig.schemes) {\n const name = scheme.name;\n const challenge = scheme.challenge?.();\n\n if (\n challenge?.headerValue?.toLowerCase().startsWith('bearer') ||\n name === 'jwt'\n ) {\n result[name] = {\n type: 'http',\n scheme: 'bearer',\n bearerFormat: 'JWT'\n };\n } else if (name === 'cookie' || (scheme as any).cookieName) {\n // Cookie scheme — extract cookie name from options if available\n const cookieName =\n (scheme as any).cookieName ??\n (scheme as any)._options?.cookieName ??\n 'session';\n result[name] = {\n type: 'apiKey',\n in: 'cookie',\n name: cookieName\n };\n } else if (challenge) {\n // Generic scheme with a challenge header\n result[name] = {\n type: 'http',\n scheme: challenge.headerValue.split(' ')[0].toLowerCase()\n };\n } else {\n // Fallback — treat as generic HTTP scheme\n result[name] = { type: 'http', scheme: name };\n }\n }\n return result;\n}\n\n/**\n * Map endpoint `authRoles` to an OpenAPI operation-level `security` array.\n *\n * - `null` → empty array (public endpoint, no security)\n * - `[]` → `[{ <schemeName>: [] }]` (any authenticated user)\n * - `['admin']` → `[{ <schemeName>: ['admin'] }]` (require specific roles)\n */\nexport function mapOperationSecurity(\n authRoles: readonly string[] | null,\n securitySchemeNames: string[]\n): Record<string, string[]>[] {\n if (authRoles === null) return [];\n if (securitySchemeNames.length === 0) return [];\n\n // Each security scheme listed as an option (OR semantics in OpenAPI)\n return securitySchemeNames.map(name => ({\n [name]: [...authRoles]\n }));\n}\n","import type { SchemaBuilder } from '@cleverbrush/schema';\nimport type {\n AuthenticationConfig,\n EndpointMetadata,\n EndpointRegistration\n} from '@cleverbrush/server';\nimport { resolvePath } from './pathUtils.js';\nimport { convertSchema } from './schemaConverter.js';\nimport { SchemaRegistry, walkSchemas } from './schemaRegistry.js';\nimport {\n mapOperationSecurity,\n mapSecuritySchemes,\n type OpenApiSecurityScheme\n} from './securityMapper.js';\n\n// ---------------------------------------------------------------------------\n// Options\n// ---------------------------------------------------------------------------\n\n/**\n * API metadata included in the OpenAPI `info` object.\n * Maps directly to the OpenAPI 3.1 Info Object.\n */\nexport interface OpenApiInfo {\n readonly title: string;\n readonly version: string;\n readonly description?: string;\n readonly termsOfService?: string;\n readonly contact?: {\n readonly name?: string;\n readonly url?: string;\n readonly email?: string;\n };\n readonly license?: {\n readonly name: string;\n readonly url?: string;\n };\n}\n\n/**\n * A server entry in the OpenAPI `servers` array.\n * Describes a base URL where the API is accessible.\n */\nexport interface OpenApiServer {\n readonly url: string;\n readonly description?: string;\n}\n\n/**\n * A tag entry in the OpenAPI top-level `tags` array.\n * Provides a name, optional description, and optional external documentation\n * for a tag group.\n *\n * @see https://spec.openapis.org/oas/v3.1.0#tag-object\n */\nexport interface OpenApiTag {\n /** Tag name. Must match the tag strings used on individual operations. */\n readonly name: string;\n /** Short description for the tag group, displayed in Swagger UI / Redoc. */\n readonly description?: string;\n /** Link to external documentation for this tag. */\n readonly externalDocs?: {\n readonly url: string;\n readonly description?: string;\n };\n}\n\n/**\n * Options passed to {@link generateOpenApiSpec}.\n */\nexport interface OpenApiOptions {\n readonly registrations: readonly EndpointRegistration[];\n readonly info: OpenApiInfo;\n readonly servers?: readonly OpenApiServer[];\n readonly authConfig?: AuthenticationConfig | null;\n readonly securitySchemes?: Record<string, OpenApiSecurityScheme>;\n /**\n * Top-level tag definitions with optional descriptions and external docs.\n *\n * When provided, these entries are emitted as the top-level `tags` array.\n * Any tag names used by registered endpoints but absent from this list are\n * automatically appended as name-only entries (sorted alphabetically).\n *\n * When omitted, unique tag names are still auto-collected from all\n * registered endpoints and emitted as name-only entries.\n *\n * @see https://spec.openapis.org/oas/v3.1.0#tag-object\n */\n readonly tags?: readonly OpenApiTag[];\n}\n\n// ---------------------------------------------------------------------------\n// OpenAPI Document (partial typing — plain objects for flexibility)\n// ---------------------------------------------------------------------------\n\n/**\n * A generated OpenAPI 3.1 document. Typed as a plain object map to allow\n * any extension fields without requiring a full OpenAPI type library.\n */\nexport type OpenApiDocument = Record<string, unknown>;\n\n// ---------------------------------------------------------------------------\n// Generator\n// ---------------------------------------------------------------------------\n\nfunction buildParameterObject(\n name: string,\n location: 'query' | 'header' | 'path',\n schema: Record<string, unknown>,\n required: boolean,\n description?: string\n): Record<string, unknown> {\n const param: Record<string, unknown> = {\n name,\n in: location,\n schema\n };\n if (required) param['required'] = true;\n if (description) param['description'] = description;\n return param;\n}\n\nfunction buildRequestBody(\n bodySchema: SchemaBuilder<any, any, any, any, any>,\n registry: SchemaRegistry\n): Record<string, unknown> {\n const jsonSchema = convertSchema(bodySchema, registry);\n const bodyInfo = bodySchema.introspect() as any;\n const body: Record<string, unknown> = {\n required: bodyInfo.isRequired !== false,\n content: {\n 'application/json': { schema: jsonSchema }\n }\n };\n if (typeof bodyInfo.description === 'string' && bodyInfo.description !== '')\n body['description'] = bodyInfo.description;\n return body;\n}\n\n// Default descriptions for common HTTP status codes\nconst HTTP_STATUS_DESCRIPTIONS: Record<number, string> = {\n 200: 'OK',\n 201: 'Created',\n 202: 'Accepted',\n 204: 'No Content',\n 400: 'Bad Request',\n 401: 'Unauthorized',\n 403: 'Forbidden',\n 404: 'Not Found',\n 409: 'Conflict',\n 422: 'Unprocessable Entity',\n 500: 'Internal Server Error'\n};\n\n// Minimal inline schema for ProblemDetails error responses\nconst PROBLEM_DETAILS_SCHEMA = {\n type: 'object',\n properties: {\n status: { type: 'integer' },\n title: { type: 'string' },\n detail: { type: 'string' }\n }\n};\n\nfunction buildResponses(\n meta: EndpointMetadata,\n method: string,\n registry: SchemaRegistry\n): Record<string, unknown> {\n const result: Record<string, unknown> = {};\n\n // Multi-code path — .responses() was called\n if (meta.responsesSchemas) {\n for (const [codeStr, schema] of Object.entries(meta.responsesSchemas)) {\n const code = Number(codeStr);\n const desc =\n HTTP_STATUS_DESCRIPTIONS[code] ?? `Response ${codeStr}`;\n if (schema) {\n const jsonSchema = convertSchema(schema, registry);\n const respInfo = schema.introspect() as any;\n const customDesc =\n typeof respInfo.description === 'string' &&\n respInfo.description !== ''\n ? respInfo.description\n : desc;\n result[codeStr] = {\n description: customDesc,\n content: { 'application/json': { schema: jsonSchema } }\n };\n } else {\n result[codeStr] = { description: desc };\n }\n }\n } else if (meta.responseSchema) {\n // Legacy single-code path — .returns() was called\n const jsonSchema = convertSchema(meta.responseSchema, registry);\n const respInfo = meta.responseSchema.introspect() as any;\n const desc =\n typeof respInfo.description === 'string' &&\n respInfo.description !== ''\n ? respInfo.description\n : 'Successful response';\n result['200'] = {\n description: desc,\n content: { 'application/json': { schema: jsonSchema } }\n };\n } else if (method === 'DELETE' || method === 'HEAD') {\n result['204'] = { description: 'No content' };\n } else {\n result['200'] = { description: 'Successful response' };\n }\n\n // Auto-add framework-generated error responses\n if (meta.bodySchema && !result['422']) {\n result['422'] = {\n description: 'Validation error',\n content: {\n 'application/problem+json': { schema: PROBLEM_DETAILS_SCHEMA }\n }\n };\n }\n\n if (meta.authRoles !== null) {\n if (!result['401']) {\n result['401'] = {\n description: 'Unauthorized',\n content: {\n 'application/problem+json': {\n schema: PROBLEM_DETAILS_SCHEMA\n }\n }\n };\n }\n if (!result['403']) {\n result['403'] = {\n description: 'Forbidden',\n content: {\n 'application/problem+json': {\n schema: PROBLEM_DETAILS_SCHEMA\n }\n }\n };\n }\n }\n\n return result;\n}\n\nfunction buildOperation(\n meta: EndpointMetadata,\n pathParams: { name: string; schema: Record<string, unknown> }[],\n securitySchemeNames: string[],\n registry: SchemaRegistry\n): Record<string, unknown> {\n const operation: Record<string, unknown> = {};\n\n // Metadata\n if (meta.summary) operation['summary'] = meta.summary;\n if (meta.description) operation['description'] = meta.description;\n if (meta.tags.length > 0) operation['tags'] = [...meta.tags];\n if (meta.operationId) operation['operationId'] = meta.operationId;\n if (meta.deprecated) operation['deprecated'] = true;\n\n // Parameters\n const parameters: Record<string, unknown>[] = [];\n\n // Path parameters\n for (const pp of pathParams) {\n parameters.push(buildParameterObject(pp.name, 'path', pp.schema, true));\n }\n\n // Query parameters\n if (meta.querySchema) {\n const queryInfo = meta.querySchema.introspect() as any;\n const props: Record<\n string,\n SchemaBuilder<any, any, any>\n > = queryInfo.properties ?? {};\n for (const [name, propSchema] of Object.entries(props)) {\n const propInfo = propSchema.introspect() as any;\n const isRequired = propInfo.isRequired !== false;\n const description =\n typeof propInfo.description === 'string' &&\n propInfo.description !== ''\n ? propInfo.description\n : undefined;\n parameters.push(\n buildParameterObject(\n name,\n 'query',\n convertSchema(propSchema, registry),\n isRequired,\n description\n )\n );\n }\n }\n\n // Header parameters\n if (meta.headerSchema) {\n const headerInfo = meta.headerSchema.introspect() as any;\n const props: Record<\n string,\n SchemaBuilder<any, any, any>\n > = headerInfo.properties ?? {};\n for (const [name, propSchema] of Object.entries(props)) {\n const propInfo = propSchema.introspect() as any;\n const isRequired = propInfo.isRequired !== false;\n const description =\n typeof propInfo.description === 'string' &&\n propInfo.description !== ''\n ? propInfo.description\n : undefined;\n parameters.push(\n buildParameterObject(\n name,\n 'header',\n convertSchema(propSchema, registry),\n isRequired,\n description\n )\n );\n }\n }\n\n if (parameters.length > 0) operation['parameters'] = parameters;\n\n // Request body\n if (meta.bodySchema) {\n operation['requestBody'] = buildRequestBody(meta.bodySchema, registry);\n }\n\n // Responses\n operation['responses'] = buildResponses(\n meta,\n meta.method.toUpperCase(),\n registry\n );\n\n // Security\n const security = mapOperationSecurity(authRoles(meta), securitySchemeNames);\n if (security.length > 0) operation['security'] = security;\n\n return operation;\n}\n\nfunction authRoles(meta: EndpointMetadata): readonly string[] | null {\n return meta.authRoles;\n}\n\n/**\n * Generate an OpenAPI 3.1 specification document from registered endpoints.\n */\nexport function generateOpenApiSpec(options: OpenApiOptions): OpenApiDocument {\n const { registrations, info, servers, authConfig, securitySchemes, tags } =\n options;\n\n // Security schemes — from explicit config or auto-mapped\n const resolvedSchemes: Record<string, OpenApiSecurityScheme> =\n securitySchemes ?? mapSecuritySchemes(authConfig);\n const securitySchemeNames = Object.keys(resolvedSchemes);\n\n // Pre-pass: collect all named schemas from every endpoint into a registry.\n // Walking happens before path generation so that $ref pointers are emitted\n // correctly at every call site within buildOperation.\n const registry = new SchemaRegistry();\n const visited = new Set<SchemaBuilder<any, any, any>>();\n for (const reg of registrations) {\n const meta = reg.endpoint;\n if (meta.bodySchema) walkSchemas(meta.bodySchema, registry, visited);\n if (meta.responseSchema)\n walkSchemas(meta.responseSchema, registry, visited);\n if (meta.responsesSchemas) {\n for (const schema of Object.values(meta.responsesSchemas)) {\n if (schema) walkSchemas(schema, registry, visited);\n }\n }\n if (meta.querySchema) {\n const queryProps =\n (meta.querySchema.introspect() as any).properties ?? {};\n for (const propSchema of Object.values<\n SchemaBuilder<any, any, any>\n >(queryProps)) {\n walkSchemas(propSchema, registry, visited);\n }\n }\n if (meta.headerSchema) {\n const headerProps =\n (meta.headerSchema.introspect() as any).properties ?? {};\n for (const propSchema of Object.values<\n SchemaBuilder<any, any, any>\n >(headerProps)) {\n walkSchemas(propSchema, registry, visited);\n }\n }\n }\n\n const resolveComponentSchemaName =\n (rootSchema: SchemaBuilder<any, any, any>) =>\n (candidate: SchemaBuilder<any, any, any>) =>\n candidate === rootSchema ? undefined : registry.getName(candidate);\n\n // Build paths\n const paths: Record<string, Record<string, unknown>> = {};\n\n for (const reg of registrations) {\n const meta = reg.endpoint;\n const { path, parameters: pathParams } = resolvePath(meta);\n const method = meta.method.toLowerCase();\n\n if (!paths[path]) paths[path] = {};\n paths[path][method] = buildOperation(\n meta,\n pathParams as { name: string; schema: Record<string, unknown> }[],\n securitySchemeNames,\n registry\n );\n }\n\n // Build top-level tags array: explicit entries first, then auto-collected\n // tag names from endpoints that are not already covered.\n const explicitNames = new Set((tags ?? []).map(t => t.name));\n const autoNames: string[] = [];\n for (const reg of registrations) {\n for (const tag of reg.endpoint.tags) {\n if (!explicitNames.has(tag) && !autoNames.includes(tag)) {\n autoNames.push(tag);\n }\n }\n }\n autoNames.sort();\n const mergedTags: OpenApiTag[] = [\n ...(tags ?? []),\n ...autoNames.map(name => ({ name }))\n ];\n\n // Assemble document\n const doc: OpenApiDocument = {\n openapi: '3.1.0',\n info: { ...info }\n };\n\n if (servers && servers.length > 0) {\n doc['servers'] = servers.map(s => ({ ...s }));\n }\n\n if (mergedTags.length > 0) {\n doc['tags'] = mergedTags.map(t => ({ ...t }));\n }\n\n doc['paths'] = paths;\n\n // Components — security schemes + named component schemas\n const componentSchemas: Record<string, unknown> = {};\n for (const [name, schema] of registry.entries()) {\n // Inline the root schema to avoid a self-referential $ref, but resolve\n // nested named schemas through the shared registry so component\n // definitions can still deduplicate via $ref.\n componentSchemas[name] = convertSchema(\n schema,\n resolveComponentSchemaName(schema)\n );\n }\n const hasSchemas = Object.keys(componentSchemas).length > 0;\n\n if (securitySchemeNames.length > 0 || hasSchemas) {\n const components: Record<string, unknown> = {};\n if (securitySchemeNames.length > 0)\n components['securitySchemes'] = { ...resolvedSchemes };\n if (hasSchemas) components['schemas'] = componentSchemas;\n doc['components'] = components;\n }\n\n return doc;\n}\n","import type {\n AuthenticationConfig,\n EndpointRegistration\n} from '@cleverbrush/server';\nimport { endpoint } from '@cleverbrush/server';\nimport {\n generateOpenApiSpec,\n type OpenApiDocument,\n type OpenApiInfo,\n type OpenApiServer\n} from './generateOpenApiSpec.js';\nimport type { OpenApiSecurityScheme } from './securityMapper.js';\n\n/**\n * Options for {@link createOpenApiEndpoint}.\n */\nexport interface OpenApiEndpointOptions {\n /** Function that returns endpoint registrations. */\n readonly getRegistrations: () => readonly EndpointRegistration[];\n /** OpenAPI info metadata. */\n readonly info: OpenApiInfo;\n /** Optional server entries. */\n readonly servers?: readonly OpenApiServer[];\n /** Optional auth config for security scheme generation. */\n readonly authConfig?: AuthenticationConfig | null;\n /** Override security schemes manually. */\n readonly securitySchemes?: Record<string, OpenApiSecurityScheme>;\n /** Path to serve the spec at (default: `/openapi.json`). */\n readonly path?: string;\n}\n\n/**\n * Creates an endpoint definition and handler that serves the OpenAPI spec\n * as JSON. Register it with `builder.handle(ep, handler)`.\n *\n * The spec is lazily generated on first request and cached.\n *\n * @example\n * ```ts\n * const { endpoint: openApiEp, handler } = createOpenApiEndpoint({\n * getRegistrations: () => builder.getRegistrations(),\n * info: { title: 'My API', version: '1.0.0' }\n * });\n * builder.handle(openApiEp, handler);\n * ```\n */\nexport function createOpenApiEndpoint(options: OpenApiEndpointOptions): {\n endpoint: ReturnType<(typeof endpoint)['get']>;\n handler: () => OpenApiDocument;\n} {\n const servePath = options.path ?? '/openapi.json';\n\n const ep = endpoint\n .get(servePath)\n .summary('OpenAPI specification')\n .tags('OpenAPI')\n .operationId('getOpenApiSpec');\n\n let cachedSpec: OpenApiDocument | null = null;\n\n const handler = (): OpenApiDocument => {\n if (!cachedSpec) {\n cachedSpec = generateOpenApiSpec({\n registrations: options.getRegistrations(),\n info: options.info,\n servers: options.servers,\n authConfig: options.authConfig,\n securitySchemes: options.securitySchemes\n });\n }\n return cachedSpec;\n };\n\n return { endpoint: ep, handler };\n}\n","import type {\n AuthenticationConfig,\n EndpointRegistration,\n RequestContext\n} from '@cleverbrush/server';\nimport {\n generateOpenApiSpec,\n type OpenApiDocument,\n type OpenApiInfo,\n type OpenApiServer\n} from './generateOpenApiSpec.js';\nimport type { OpenApiSecurityScheme } from './securityMapper.js';\n\n/**\n * Options for the {@link serveOpenApi} middleware.\n */\nexport interface ServeOpenApiOptions {\n /** Function that returns endpoint registrations. */\n readonly getRegistrations: () => readonly EndpointRegistration[];\n /** OpenAPI info metadata. */\n readonly info: OpenApiInfo;\n /** Optional server entries. */\n readonly servers?: readonly OpenApiServer[];\n /** Optional auth config for security scheme generation. */\n readonly authConfig?: AuthenticationConfig | null;\n /** Override security schemes manually. */\n readonly securitySchemes?: Record<string, OpenApiSecurityScheme>;\n /** Path to serve the spec at (default: `/openapi.json`). */\n readonly path?: string;\n}\n\n/**\n * Returns a server middleware that serves the OpenAPI spec as JSON at\n * the configured path (default: `/openapi.json`).\n *\n * The spec is lazily generated on first request and cached.\n */\nexport function serveOpenApi(\n options: ServeOpenApiOptions\n): (context: RequestContext, next: () => Promise<void>) => Promise<void> {\n const servePath = options.path ?? '/openapi.json';\n let cachedSpec: OpenApiDocument | null = null;\n\n return async (context, next) => {\n const url = context.url;\n const pathname = url.pathname;\n const method = context.method.toUpperCase();\n\n if (method === 'GET' && pathname === servePath) {\n if (!cachedSpec) {\n cachedSpec = generateOpenApiSpec({\n registrations: options.getRegistrations(),\n info: options.info,\n servers: options.servers,\n authConfig: options.authConfig,\n securitySchemes: options.securitySchemes\n });\n }\n const body = JSON.stringify(cachedSpec);\n context.response.writeHead(200, {\n 'content-type': 'application/json',\n 'content-length': Buffer.byteLength(body).toString()\n });\n context.response.end(body);\n return;\n }\n\n await next();\n };\n}\n"],"mappings":"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","names":["fs","path","toJsonSchema","isParseStringSchema","p","convertColonParams","path","paramNames","_","name","convertParseStringPath","pathTemplate","info","templateDef","props","pathString","i","parameters","seg","resolvePath","meta","basePath","normalizeSlashes","combined","converted","fullPath","result","toJsonSchema","convertSchema","schema","registry","nameResolver","s","SchemaRegistry","schema","name","existing","walkSchemas","registry","visited","info","props","child","elements","el","options","opt","mapSecuritySchemes","authConfig","result","scheme","name","challenge","cookieName","mapOperationSecurity","authRoles","securitySchemeNames","buildParameterObject","name","location","schema","required","description","param","buildRequestBody","bodySchema","registry","jsonSchema","convertSchema","bodyInfo","body","HTTP_STATUS_DESCRIPTIONS","PROBLEM_DETAILS_SCHEMA","buildResponses","meta","method","result","codeStr","code","desc","respInfo","customDesc","buildOperation","pathParams","securitySchemeNames","operation","parameters","pp","props","propSchema","propInfo","isRequired","security","mapOperationSecurity","authRoles","generateOpenApiSpec","options","registrations","info","servers","authConfig","securitySchemes","tags","resolvedSchemes","mapSecuritySchemes","SchemaRegistry","visited","reg","walkSchemas","queryProps","headerProps","resolveComponentSchemaName","rootSchema","candidate","paths","path","resolvePath","explicitNames","t","autoNames","tag","mergedTags","doc","s","componentSchemas","hasSchemas","components","writeOpenApiSpec","options","outputPath","spec","generateOpenApiSpec","dir","endpoint","createOpenApiEndpoint","options","servePath","ep","endpoint","cachedSpec","generateOpenApiSpec","serveOpenApi","options","servePath","cachedSpec","context","next","pathname","generateOpenApiSpec","body"]}
1
+ {"version":3,"sources":["../src/cli.ts","../src/pathUtils.ts","../src/schemaConverter.ts","../src/schemaRegistry.ts","../src/securityMapper.ts","../src/generateOpenApiSpec.ts","../src/openApiEndpoint.ts","../src/serveOpenApi.ts"],"sourcesContent":["import * as fs from 'node:fs';\nimport * as path from 'node:path';\nimport {\n generateOpenApiSpec,\n type OpenApiDocument,\n type OpenApiOptions\n} from './generateOpenApiSpec.js';\n\n/**\n * Generate an OpenAPI spec and write it to a file.\n *\n * @param options - Same options as `generateOpenApiSpec()`.\n * @param outputPath - File path to write the JSON spec to.\n * @returns The generated spec document.\n *\n * @example\n * ```ts\n * import { writeOpenApiSpec } from '@cleverbrush/server-openapi';\n * import { createServer, endpoint } from '@cleverbrush/server';\n *\n * const builder = createServer()\n * .handle(endpoint.get('/api/health'), () => ({ ok: true }));\n *\n * writeOpenApiSpec(\n * {\n * registrations: builder.getRegistrations(),\n * info: { title: 'My API', version: '1.0.0' }\n * },\n * './openapi.json'\n * );\n * ```\n */\nexport function writeOpenApiSpec(\n options: OpenApiOptions,\n outputPath: string\n): OpenApiDocument {\n const spec = generateOpenApiSpec(options);\n const dir = path.dirname(outputPath);\n if (dir && !fs.existsSync(dir)) {\n fs.mkdirSync(dir, { recursive: true });\n }\n fs.writeFileSync(outputPath, JSON.stringify(spec, null, 2), 'utf-8');\n return spec;\n}\n","import type {\n ParseStringSchemaBuilder,\n SchemaBuilder\n} from '@cleverbrush/schema';\nimport { toJsonSchema } from '@cleverbrush/schema-json';\nimport type { EndpointMetadata } from '@cleverbrush/server';\n\ntype RoutePath = string | ParseStringSchemaBuilder<any, any, any, any, any>;\n\nfunction isParseStringSchema(\n p: RoutePath\n): p is ParseStringSchemaBuilder<any, any, any, any, any> {\n return typeof p !== 'string' && typeof (p as any).validate === 'function';\n}\n\n/**\n * Describes a single path parameter extracted from a route template,\n * including its name and the JSON Schema representation of its type.\n */\nexport interface PathParameterInfo {\n readonly name: string;\n readonly schema: Record<string, unknown>;\n}\n\n/**\n * The output of resolving a route path template to an OpenAPI-compatible\n * path string and its parameter list.\n */\nexport interface ResolvedPath {\n /** OpenAPI-formatted path, e.g. `/api/users/{id}` */\n readonly path: string;\n /** Extracted path parameters with their JSON Schema */\n readonly parameters: readonly PathParameterInfo[];\n}\n\n/**\n * Convert a colon-style static path to OpenAPI `{param}` format.\n * E.g. `/users/:id/posts/:pid` → `/users/{id}/posts/{pid}`\n */\nfunction convertColonParams(path: string): {\n converted: string;\n paramNames: string[];\n} {\n const paramNames: string[] = [];\n const converted = path.replace(/:([a-zA-Z_][a-zA-Z0-9_]*)/g, (_, name) => {\n paramNames.push(name);\n return `{${name}}`;\n });\n return { converted, paramNames };\n}\n\n/**\n * Convert a `ParseStringSchemaBuilder` path template to an OpenAPI-formatted\n * path string and extract parameter schemas.\n */\nfunction convertParseStringPath(\n pathTemplate: ParseStringSchemaBuilder<any, any, any, any, any>\n): { pathString: string; parameters: PathParameterInfo[] } {\n const info = pathTemplate.introspect() as any;\n const templateDef = info.templateDefinition as {\n literals: string[];\n segments: { path: string }[];\n };\n const objectSchema = info.objectSchema;\n const objInfo = objectSchema.introspect() as any;\n const props: Record<\n string,\n SchemaBuilder<any, any, any>\n > = objInfo.properties ?? {};\n\n let pathString = '';\n for (let i = 0; i < templateDef.segments.length; i++) {\n pathString +=\n templateDef.literals[i] + `{${templateDef.segments[i].path}}`;\n }\n pathString += templateDef.literals[templateDef.segments.length] ?? '';\n\n const parameters: PathParameterInfo[] = templateDef.segments.map(seg => ({\n name: seg.path,\n schema: props[seg.path]\n ? toJsonSchema(props[seg.path], { $schema: false })\n : { type: 'string' }\n }));\n\n return { pathString, parameters };\n}\n\n/**\n * Combines `basePath` and `pathTemplate` from an endpoint into an\n * OpenAPI-formatted path with extracted parameter information.\n */\nexport function resolvePath(meta: EndpointMetadata): ResolvedPath {\n const basePath = meta.basePath.replace(/\\/$/, '');\n const pathTemplate = meta.pathTemplate;\n\n if (isParseStringSchema(pathTemplate)) {\n const { pathString, parameters } = convertParseStringPath(pathTemplate);\n const fullPath = normalizeSlashes(basePath + pathString);\n return { path: fullPath, parameters };\n }\n\n // Static string path template\n const templateStr = pathTemplate === '/' ? '' : pathTemplate;\n const combined = basePath + templateStr;\n const { converted, paramNames } = convertColonParams(combined);\n const fullPath = normalizeSlashes(converted);\n\n const parameters: PathParameterInfo[] = paramNames.map(name => ({\n name,\n schema: { type: 'string' }\n }));\n\n return { path: fullPath, parameters };\n}\n\nfunction normalizeSlashes(path: string): string {\n // Replace double slashes with single, ensure leading slash\n let result = path.replace(/\\/+/g, '/');\n if (!result.startsWith('/')) result = '/' + result;\n if (result.length > 1 && result.endsWith('/')) result = result.slice(0, -1);\n return result || '/';\n}\n","import type { SchemaBuilder } from '@cleverbrush/schema';\nimport { toJsonSchema } from '@cleverbrush/schema-json';\nimport type { SchemaRegistry } from './schemaRegistry.js';\n\ntype NameResolver = (\n schema: SchemaBuilder<any, any, any>\n) => string | null | undefined;\n\n/**\n * Converts a `@cleverbrush/schema` builder to a JSON Schema object suitable\n * for embedding in an OpenAPI 3.1 spec (no `$schema` header, Draft 2020-12).\n *\n * Returns an empty schema `{}` when the input is `null` or `undefined`.\n *\n * When a {@link SchemaRegistry} or a custom resolver function is provided, any\n * schema instance that resolves to a name will be emitted as a\n * `$ref: '#/components/schemas/<name>'` pointer instead of being inlined.\n *\n * @param schema - The schema to convert, or `null`/`undefined`.\n * @param registry - Optional registry or resolver function for `$ref` deduplication.\n */\nexport function convertSchema(\n schema: SchemaBuilder<any, any, any, any, any> | null | undefined,\n registry?: SchemaRegistry | NameResolver\n): Record<string, unknown> {\n if (schema == null) return {};\n\n let nameResolver:\n | ((s: SchemaBuilder<any, any, any>) => string | null)\n | undefined;\n if (registry) {\n if (typeof registry === 'function') {\n nameResolver = s => registry(s) ?? null;\n } else {\n nameResolver = s => registry.getName(s);\n }\n }\n\n return toJsonSchema(schema, {\n $schema: false,\n draft: '2020-12',\n nameResolver\n });\n}\n","import type { SchemaBuilder } from '@cleverbrush/schema';\n\n// ---------------------------------------------------------------------------\n// SchemaRegistry\n// ---------------------------------------------------------------------------\n\n/**\n * Collects schemas that carry an explicit component name (set via\n * `.schemaName()`) and provides a reference-based lookup used during OpenAPI\n * spec generation to replace inline schema objects with\n * `$ref: '#/components/schemas/<name>'` pointers.\n *\n * **Conflict rule**: registering two *different* schema instances (different\n * object references) under the same name throws immediately. Re-registering\n * the same instance is a no-op.\n *\n * @example\n * ```ts\n * const registry = new SchemaRegistry();\n * registry.register(UserSchema); // UserSchema.schemaName('User')\n * registry.getName(UserSchema); // 'User'\n * registry.getName(someOtherSchema); // null\n * ```\n */\nexport class SchemaRegistry {\n /** schema instance → registered name */\n private readonly byInstance = new Map<\n SchemaBuilder<any, any, any>,\n string\n >();\n /** name → first-registered schema instance */\n private readonly byName = new Map<string, SchemaBuilder<any, any, any>>();\n\n /**\n * Attempts to register `schema` in the registry.\n *\n * - If the schema has no `schemaName` in its introspect output, it is\n * silently skipped.\n * - If the same instance is already registered, this is a no-op.\n * - If a **different** instance is already registered under the same name,\n * an error is thrown.\n *\n * @param schema - The schema builder to register.\n * @throws {Error} When two distinct schema instances share the same name.\n */\n register(schema: SchemaBuilder<any, any, any>): void {\n const name = (schema.introspect() as any).schemaName as\n | string\n | undefined;\n if (typeof name !== 'string') return;\n\n const existing = this.byName.get(name);\n if (existing !== undefined) {\n // Same instance → idempotent, nothing to do\n if (existing === schema) return;\n // Different instance → conflict\n throw new Error(\n `Schema name \"${name}\" is already registered by a different schema instance. ` +\n `Each named schema must be a single, reused constant. ` +\n `If you intended to register the same schema, ensure you are passing ` +\n `the same object reference (not a rebuilt schema).`\n );\n }\n\n this.byInstance.set(schema, name);\n this.byName.set(name, schema);\n }\n\n /**\n * Returns the component name for a given schema instance, or `null` if it\n * was not registered.\n *\n * @param schema - The schema builder to look up.\n * @returns The registered name, or `null`.\n */\n getName(schema: SchemaBuilder<any, any, any>): string | null {\n return this.byInstance.get(schema) ?? null;\n }\n\n /**\n * Iterates over all registered `[name, schema]` pairs in insertion order.\n *\n * Used to emit the `components.schemas` section of an OpenAPI document.\n */\n entries(): IterableIterator<[string, SchemaBuilder<any, any, any>]> {\n return this.byName.entries();\n }\n\n /** Returns `true` when at least one schema has been registered. */\n get isEmpty(): boolean {\n return this.byName.size === 0;\n }\n}\n\n// ---------------------------------------------------------------------------\n// walkSchemas\n// ---------------------------------------------------------------------------\n\n/**\n * Recursively visits every {@link SchemaBuilder} reachable from `schema` and\n * calls {@link SchemaRegistry.register} on each node.\n *\n * Cycle detection is performed via a `visited` `Set` of object references, so\n * schemas may safely be shared across multiple branches without causing\n * infinite recursion.\n *\n * **Excluded schema types**\n * - `lazy` — deferred resolution would require calling the getter, which may\n * itself reference the parent schema; lazy schemas are handled separately.\n *\n * @param schema - Root schema to start the walk from.\n * @param registry - Registry to register named schemas into.\n * @param visited - Shared set for cycle detection; pass a new `Set()` for the\n * top-level call.\n */\nexport function walkSchemas(\n schema: SchemaBuilder<any, any, any>,\n registry: SchemaRegistry,\n visited: Set<SchemaBuilder<any, any, any>> = new Set()\n): void {\n if (visited.has(schema)) return;\n visited.add(schema);\n\n registry.register(schema);\n\n const info = schema.introspect() as any;\n\n switch (info.type) {\n case 'object': {\n const props = info.properties as\n | Record<string, SchemaBuilder<any, any, any>>\n | undefined;\n if (props) {\n for (const child of Object.values(props)) {\n walkSchemas(child, registry, visited);\n }\n }\n break;\n }\n case 'array':\n if (info.elementSchema) {\n walkSchemas(info.elementSchema, registry, visited);\n }\n break;\n case 'tuple': {\n const elements: SchemaBuilder<any, any, any>[] =\n info.elements ?? [];\n for (const el of elements) {\n walkSchemas(el, registry, visited);\n }\n if (info.restSchema) {\n walkSchemas(info.restSchema, registry, visited);\n }\n break;\n }\n case 'union': {\n const options: SchemaBuilder<any, any, any>[] = info.options ?? [];\n for (const opt of options) {\n walkSchemas(opt, registry, visited);\n }\n break;\n }\n case 'record':\n if (info.keySchema) {\n walkSchemas(info.keySchema, registry, visited);\n }\n if (info.valueSchema) {\n walkSchemas(info.valueSchema, registry, visited);\n }\n break;\n case 'lazy': {\n // Resolve the inner schema and walk it so that any named schema\n // reachable through a lazy boundary is registered. Cycle-safety is\n // provided by the `visited` set — the lazy wrapper itself was\n // already added above, preventing infinite recursion on\n // self-referential schemas.\n const resolved = (schema as any).resolve() as SchemaBuilder<\n any,\n any,\n any\n >;\n walkSchemas(resolved, registry, visited);\n break;\n }\n default:\n break;\n }\n}\n","import type { AuthenticationConfig } from '@cleverbrush/server';\n\n/**\n * An OpenAPI 3.1 Security Scheme Object.\n * Describes how requests to the API are authenticated.\n *\n * @see {@link https://spec.openapis.org/oas/v3.1.0#security-scheme-object OpenAPI Security Scheme Object}\n */\nexport interface OpenApiSecurityScheme {\n readonly type: string;\n readonly scheme?: string;\n readonly bearerFormat?: string;\n readonly in?: string;\n readonly name?: string;\n}\n\n/**\n * Map `@cleverbrush/auth` authentication schemes to OpenAPI security scheme objects.\n */\nexport function mapSecuritySchemes(\n authConfig: AuthenticationConfig | null | undefined\n): Record<string, OpenApiSecurityScheme> {\n if (!authConfig) return {};\n\n const result: Record<string, OpenApiSecurityScheme> = {};\n for (const scheme of authConfig.schemes) {\n const name = scheme.name;\n const challenge = scheme.challenge?.();\n\n if (\n challenge?.headerValue?.toLowerCase().startsWith('bearer') ||\n name === 'jwt'\n ) {\n result[name] = {\n type: 'http',\n scheme: 'bearer',\n bearerFormat: 'JWT'\n };\n } else if (name === 'cookie' || (scheme as any).cookieName) {\n // Cookie scheme — extract cookie name from options if available\n const cookieName =\n (scheme as any).cookieName ??\n (scheme as any)._options?.cookieName ??\n 'session';\n result[name] = {\n type: 'apiKey',\n in: 'cookie',\n name: cookieName\n };\n } else if (challenge) {\n // Generic scheme with a challenge header\n result[name] = {\n type: 'http',\n scheme: challenge.headerValue.split(' ')[0].toLowerCase()\n };\n } else {\n // Fallback — treat as generic HTTP scheme\n result[name] = { type: 'http', scheme: name };\n }\n }\n return result;\n}\n\n/**\n * Map endpoint `authRoles` to an OpenAPI operation-level `security` array.\n *\n * - `null` → empty array (public endpoint, no security)\n * - `[]` → `[{ <schemeName>: [] }]` (any authenticated user)\n * - `['admin']` → `[{ <schemeName>: ['admin'] }]` (require specific roles)\n */\nexport function mapOperationSecurity(\n authRoles: readonly string[] | null,\n securitySchemeNames: string[]\n): Record<string, string[]>[] {\n if (authRoles === null) return [];\n if (securitySchemeNames.length === 0) return [];\n\n // Each security scheme listed as an option (OR semantics in OpenAPI)\n return securitySchemeNames.map(name => ({\n [name]: [...authRoles]\n }));\n}\n","import type { SchemaBuilder } from '@cleverbrush/schema';\nimport type {\n AuthenticationConfig,\n EndpointMetadata,\n EndpointRegistration\n} from '@cleverbrush/server';\nimport { resolvePath } from './pathUtils.js';\nimport { convertSchema } from './schemaConverter.js';\nimport { SchemaRegistry, walkSchemas } from './schemaRegistry.js';\nimport {\n mapOperationSecurity,\n mapSecuritySchemes,\n type OpenApiSecurityScheme\n} from './securityMapper.js';\n\n// ---------------------------------------------------------------------------\n// Options\n// ---------------------------------------------------------------------------\n\n/**\n * API metadata included in the OpenAPI `info` object.\n * Maps directly to the OpenAPI 3.1 Info Object.\n */\nexport interface OpenApiInfo {\n readonly title: string;\n readonly version: string;\n readonly description?: string;\n readonly termsOfService?: string;\n readonly contact?: {\n readonly name?: string;\n readonly url?: string;\n readonly email?: string;\n };\n readonly license?: {\n readonly name: string;\n readonly url?: string;\n };\n}\n\n/**\n * A server entry in the OpenAPI `servers` array.\n * Describes a base URL where the API is accessible.\n */\nexport interface OpenApiServer {\n readonly url: string;\n readonly description?: string;\n}\n\n/**\n * A tag entry in the OpenAPI top-level `tags` array.\n * Provides a name, optional description, and optional external documentation\n * for a tag group.\n *\n * @see https://spec.openapis.org/oas/v3.1.0#tag-object\n */\nexport interface OpenApiTag {\n /** Tag name. Must match the tag strings used on individual operations. */\n readonly name: string;\n /** Short description for the tag group, displayed in Swagger UI / Redoc. */\n readonly description?: string;\n /** Link to external documentation for this tag. */\n readonly externalDocs?: {\n readonly url: string;\n readonly description?: string;\n };\n}\n\n/**\n * Options passed to {@link generateOpenApiSpec}.\n */\nexport interface OpenApiOptions {\n readonly registrations: readonly EndpointRegistration[];\n readonly info: OpenApiInfo;\n readonly servers?: readonly OpenApiServer[];\n readonly authConfig?: AuthenticationConfig | null;\n readonly securitySchemes?: Record<string, OpenApiSecurityScheme>;\n /**\n * Top-level tag definitions with optional descriptions and external docs.\n *\n * When provided, these entries are emitted as the top-level `tags` array.\n * Any tag names used by registered endpoints but absent from this list are\n * automatically appended as name-only entries (sorted alphabetically).\n *\n * When omitted, unique tag names are still auto-collected from all\n * registered endpoints and emitted as name-only entries.\n *\n * @see https://spec.openapis.org/oas/v3.1.0#tag-object\n */\n readonly tags?: readonly OpenApiTag[];\n}\n\n// ---------------------------------------------------------------------------\n// OpenAPI Document (partial typing — plain objects for flexibility)\n// ---------------------------------------------------------------------------\n\n/**\n * A generated OpenAPI 3.1 document. Typed as a plain object map to allow\n * any extension fields without requiring a full OpenAPI type library.\n */\nexport type OpenApiDocument = Record<string, unknown>;\n\n// ---------------------------------------------------------------------------\n// Generator\n// ---------------------------------------------------------------------------\n\nfunction buildParameterObject(\n name: string,\n location: 'query' | 'header' | 'path',\n schema: Record<string, unknown>,\n required: boolean,\n description?: string\n): Record<string, unknown> {\n const param: Record<string, unknown> = {\n name,\n in: location,\n schema\n };\n if (required) param['required'] = true;\n if (description) param['description'] = description;\n return param;\n}\n\nfunction buildRequestBody(\n bodySchema: SchemaBuilder<any, any, any, any, any>,\n registry: SchemaRegistry\n): Record<string, unknown> {\n const jsonSchema = convertSchema(bodySchema, registry);\n const bodyInfo = bodySchema.introspect() as any;\n const body: Record<string, unknown> = {\n required: bodyInfo.isRequired !== false,\n content: {\n 'application/json': { schema: jsonSchema }\n }\n };\n if (typeof bodyInfo.description === 'string' && bodyInfo.description !== '')\n body['description'] = bodyInfo.description;\n return body;\n}\n\n// Default descriptions for common HTTP status codes\nconst HTTP_STATUS_DESCRIPTIONS: Record<number, string> = {\n 200: 'OK',\n 201: 'Created',\n 202: 'Accepted',\n 204: 'No Content',\n 400: 'Bad Request',\n 401: 'Unauthorized',\n 403: 'Forbidden',\n 404: 'Not Found',\n 409: 'Conflict',\n 422: 'Unprocessable Entity',\n 500: 'Internal Server Error'\n};\n\n// Minimal inline schema for ProblemDetails error responses\nconst PROBLEM_DETAILS_SCHEMA = {\n type: 'object',\n properties: {\n status: { type: 'integer' },\n title: { type: 'string' },\n detail: { type: 'string' }\n }\n};\n\nfunction buildResponses(\n meta: EndpointMetadata,\n method: string,\n registry: SchemaRegistry\n): Record<string, unknown> {\n const result: Record<string, unknown> = {};\n\n // Multi-code path — .responses() was called\n if (meta.responsesSchemas) {\n for (const [codeStr, schema] of Object.entries(meta.responsesSchemas)) {\n const code = Number(codeStr);\n const desc =\n HTTP_STATUS_DESCRIPTIONS[code] ?? `Response ${codeStr}`;\n if (schema) {\n const jsonSchema = convertSchema(schema, registry);\n const respInfo = schema.introspect() as any;\n const customDesc =\n typeof respInfo.description === 'string' &&\n respInfo.description !== ''\n ? respInfo.description\n : desc;\n result[codeStr] = {\n description: customDesc,\n content: { 'application/json': { schema: jsonSchema } }\n };\n } else {\n result[codeStr] = { description: desc };\n }\n }\n } else if (meta.responseSchema) {\n // Legacy single-code path — .returns() was called\n const jsonSchema = convertSchema(meta.responseSchema, registry);\n const respInfo = meta.responseSchema.introspect() as any;\n const desc =\n typeof respInfo.description === 'string' &&\n respInfo.description !== ''\n ? respInfo.description\n : 'Successful response';\n result['200'] = {\n description: desc,\n content: { 'application/json': { schema: jsonSchema } }\n };\n } else if (method === 'DELETE' || method === 'HEAD') {\n result['204'] = { description: 'No content' };\n } else {\n result['200'] = { description: 'Successful response' };\n }\n\n // Auto-add framework-generated error responses\n if (meta.bodySchema && !result['422']) {\n result['422'] = {\n description: 'Validation error',\n content: {\n 'application/problem+json': { schema: PROBLEM_DETAILS_SCHEMA }\n }\n };\n }\n\n if (meta.authRoles !== null) {\n if (!result['401']) {\n result['401'] = {\n description: 'Unauthorized',\n content: {\n 'application/problem+json': {\n schema: PROBLEM_DETAILS_SCHEMA\n }\n }\n };\n }\n if (!result['403']) {\n result['403'] = {\n description: 'Forbidden',\n content: {\n 'application/problem+json': {\n schema: PROBLEM_DETAILS_SCHEMA\n }\n }\n };\n }\n }\n\n return result;\n}\n\nfunction buildOperation(\n meta: EndpointMetadata,\n pathParams: { name: string; schema: Record<string, unknown> }[],\n securitySchemeNames: string[],\n registry: SchemaRegistry\n): Record<string, unknown> {\n const operation: Record<string, unknown> = {};\n\n // Metadata\n if (meta.summary) operation['summary'] = meta.summary;\n if (meta.description) operation['description'] = meta.description;\n if (meta.tags.length > 0) operation['tags'] = [...meta.tags];\n if (meta.operationId) operation['operationId'] = meta.operationId;\n if (meta.deprecated) operation['deprecated'] = true;\n\n // Parameters\n const parameters: Record<string, unknown>[] = [];\n\n // Path parameters\n for (const pp of pathParams) {\n parameters.push(buildParameterObject(pp.name, 'path', pp.schema, true));\n }\n\n // Query parameters\n if (meta.querySchema) {\n const queryInfo = meta.querySchema.introspect() as any;\n const props: Record<\n string,\n SchemaBuilder<any, any, any>\n > = queryInfo.properties ?? {};\n for (const [name, propSchema] of Object.entries(props)) {\n const propInfo = propSchema.introspect() as any;\n const isRequired = propInfo.isRequired !== false;\n const description =\n typeof propInfo.description === 'string' &&\n propInfo.description !== ''\n ? propInfo.description\n : undefined;\n parameters.push(\n buildParameterObject(\n name,\n 'query',\n convertSchema(propSchema, registry),\n isRequired,\n description\n )\n );\n }\n }\n\n // Header parameters\n if (meta.headerSchema) {\n const headerInfo = meta.headerSchema.introspect() as any;\n const props: Record<\n string,\n SchemaBuilder<any, any, any>\n > = headerInfo.properties ?? {};\n for (const [name, propSchema] of Object.entries(props)) {\n const propInfo = propSchema.introspect() as any;\n const isRequired = propInfo.isRequired !== false;\n const description =\n typeof propInfo.description === 'string' &&\n propInfo.description !== ''\n ? propInfo.description\n : undefined;\n parameters.push(\n buildParameterObject(\n name,\n 'header',\n convertSchema(propSchema, registry),\n isRequired,\n description\n )\n );\n }\n }\n\n if (parameters.length > 0) operation['parameters'] = parameters;\n\n // Request body\n if (meta.bodySchema) {\n operation['requestBody'] = buildRequestBody(meta.bodySchema, registry);\n }\n\n // Responses\n operation['responses'] = buildResponses(\n meta,\n meta.method.toUpperCase(),\n registry\n );\n\n // Security\n const security = mapOperationSecurity(authRoles(meta), securitySchemeNames);\n if (security.length > 0) operation['security'] = security;\n\n return operation;\n}\n\nfunction authRoles(meta: EndpointMetadata): readonly string[] | null {\n return meta.authRoles;\n}\n\n/**\n * Generate an OpenAPI 3.1 specification document from registered endpoints.\n */\nexport function generateOpenApiSpec(options: OpenApiOptions): OpenApiDocument {\n const { registrations, info, servers, authConfig, securitySchemes, tags } =\n options;\n\n // Security schemes — from explicit config or auto-mapped\n const resolvedSchemes: Record<string, OpenApiSecurityScheme> =\n securitySchemes ?? mapSecuritySchemes(authConfig);\n const securitySchemeNames = Object.keys(resolvedSchemes);\n\n // Pre-pass: collect all named schemas from every endpoint into a registry.\n // Walking happens before path generation so that $ref pointers are emitted\n // correctly at every call site within buildOperation.\n const registry = new SchemaRegistry();\n const visited = new Set<SchemaBuilder<any, any, any>>();\n for (const reg of registrations) {\n const meta = reg.endpoint;\n if (meta.bodySchema) walkSchemas(meta.bodySchema, registry, visited);\n if (meta.responseSchema)\n walkSchemas(meta.responseSchema, registry, visited);\n if (meta.responsesSchemas) {\n for (const schema of Object.values(meta.responsesSchemas)) {\n if (schema) walkSchemas(schema, registry, visited);\n }\n }\n if (meta.querySchema) {\n const queryProps =\n (meta.querySchema.introspect() as any).properties ?? {};\n for (const propSchema of Object.values<\n SchemaBuilder<any, any, any>\n >(queryProps)) {\n walkSchemas(propSchema, registry, visited);\n }\n }\n if (meta.headerSchema) {\n const headerProps =\n (meta.headerSchema.introspect() as any).properties ?? {};\n for (const propSchema of Object.values<\n SchemaBuilder<any, any, any>\n >(headerProps)) {\n walkSchemas(propSchema, registry, visited);\n }\n }\n }\n\n const resolveComponentSchemaName = (\n rootSchema: SchemaBuilder<any, any, any>\n ) => {\n // The root schema must be inlined exactly once — for the component\n // definition itself. Any subsequent encounter (e.g. through a lazy\n // self-reference) should emit a $ref instead of inlining again,\n // which would otherwise cause infinite recursion.\n let inlinedRoot = false;\n return (\n candidate: SchemaBuilder<any, any, any>\n ): string | undefined => {\n if (candidate === rootSchema && !inlinedRoot) {\n inlinedRoot = true;\n return undefined; // inline the root definition once\n }\n return registry.getName(candidate) ?? undefined;\n };\n };\n\n // Build paths\n const paths: Record<string, Record<string, unknown>> = {};\n\n for (const reg of registrations) {\n const meta = reg.endpoint;\n const { path, parameters: pathParams } = resolvePath(meta);\n const method = meta.method.toLowerCase();\n\n if (!paths[path]) paths[path] = {};\n paths[path][method] = buildOperation(\n meta,\n pathParams as { name: string; schema: Record<string, unknown> }[],\n securitySchemeNames,\n registry\n );\n }\n\n // Build top-level tags array: explicit entries first, then auto-collected\n // tag names from endpoints that are not already covered.\n const explicitNames = new Set((tags ?? []).map(t => t.name));\n const autoNames: string[] = [];\n for (const reg of registrations) {\n for (const tag of reg.endpoint.tags) {\n if (!explicitNames.has(tag) && !autoNames.includes(tag)) {\n autoNames.push(tag);\n }\n }\n }\n autoNames.sort();\n const mergedTags: OpenApiTag[] = [\n ...(tags ?? []),\n ...autoNames.map(name => ({ name }))\n ];\n\n // Assemble document\n const doc: OpenApiDocument = {\n openapi: '3.1.0',\n info: { ...info }\n };\n\n if (servers && servers.length > 0) {\n doc['servers'] = servers.map(s => ({ ...s }));\n }\n\n if (mergedTags.length > 0) {\n doc['tags'] = mergedTags.map(t => ({ ...t }));\n }\n\n doc['paths'] = paths;\n\n // Components — security schemes + named component schemas\n const componentSchemas: Record<string, unknown> = {};\n for (const [name, schema] of registry.entries()) {\n // Inline the root schema to avoid a self-referential $ref, but resolve\n // nested named schemas through the shared registry so component\n // definitions can still deduplicate via $ref.\n componentSchemas[name] = convertSchema(\n schema,\n resolveComponentSchemaName(schema)\n );\n }\n const hasSchemas = Object.keys(componentSchemas).length > 0;\n\n if (securitySchemeNames.length > 0 || hasSchemas) {\n const components: Record<string, unknown> = {};\n if (securitySchemeNames.length > 0)\n components['securitySchemes'] = { ...resolvedSchemes };\n if (hasSchemas) components['schemas'] = componentSchemas;\n doc['components'] = components;\n }\n\n return doc;\n}\n","import type {\n AuthenticationConfig,\n EndpointRegistration\n} from '@cleverbrush/server';\nimport { endpoint } from '@cleverbrush/server';\nimport {\n generateOpenApiSpec,\n type OpenApiDocument,\n type OpenApiInfo,\n type OpenApiServer\n} from './generateOpenApiSpec.js';\nimport type { OpenApiSecurityScheme } from './securityMapper.js';\n\n/**\n * Options for {@link createOpenApiEndpoint}.\n */\nexport interface OpenApiEndpointOptions {\n /** Function that returns endpoint registrations. */\n readonly getRegistrations: () => readonly EndpointRegistration[];\n /** OpenAPI info metadata. */\n readonly info: OpenApiInfo;\n /** Optional server entries. */\n readonly servers?: readonly OpenApiServer[];\n /** Optional auth config for security scheme generation. */\n readonly authConfig?: AuthenticationConfig | null;\n /** Override security schemes manually. */\n readonly securitySchemes?: Record<string, OpenApiSecurityScheme>;\n /** Path to serve the spec at (default: `/openapi.json`). */\n readonly path?: string;\n}\n\n/**\n * Creates an endpoint definition and handler that serves the OpenAPI spec\n * as JSON. Register it with `builder.handle(ep, handler)`.\n *\n * The spec is lazily generated on first request and cached.\n *\n * @example\n * ```ts\n * const { endpoint: openApiEp, handler } = createOpenApiEndpoint({\n * getRegistrations: () => builder.getRegistrations(),\n * info: { title: 'My API', version: '1.0.0' }\n * });\n * builder.handle(openApiEp, handler);\n * ```\n */\nexport function createOpenApiEndpoint(options: OpenApiEndpointOptions): {\n endpoint: ReturnType<(typeof endpoint)['get']>;\n handler: () => OpenApiDocument;\n} {\n const servePath = options.path ?? '/openapi.json';\n\n const ep = endpoint\n .get(servePath)\n .summary('OpenAPI specification')\n .tags('OpenAPI')\n .operationId('getOpenApiSpec');\n\n let cachedSpec: OpenApiDocument | null = null;\n\n const handler = (): OpenApiDocument => {\n if (!cachedSpec) {\n cachedSpec = generateOpenApiSpec({\n registrations: options.getRegistrations(),\n info: options.info,\n servers: options.servers,\n authConfig: options.authConfig,\n securitySchemes: options.securitySchemes\n });\n }\n return cachedSpec;\n };\n\n return { endpoint: ep, handler };\n}\n","import type {\n AuthenticationConfig,\n EndpointRegistration,\n RequestContext\n} from '@cleverbrush/server';\nimport {\n generateOpenApiSpec,\n type OpenApiDocument,\n type OpenApiInfo,\n type OpenApiServer\n} from './generateOpenApiSpec.js';\nimport type { OpenApiSecurityScheme } from './securityMapper.js';\n\n/**\n * Options for the {@link serveOpenApi} middleware.\n */\nexport interface ServeOpenApiOptions {\n /** Function that returns endpoint registrations. */\n readonly getRegistrations: () => readonly EndpointRegistration[];\n /** OpenAPI info metadata. */\n readonly info: OpenApiInfo;\n /** Optional server entries. */\n readonly servers?: readonly OpenApiServer[];\n /** Optional auth config for security scheme generation. */\n readonly authConfig?: AuthenticationConfig | null;\n /** Override security schemes manually. */\n readonly securitySchemes?: Record<string, OpenApiSecurityScheme>;\n /** Path to serve the spec at (default: `/openapi.json`). */\n readonly path?: string;\n}\n\n/**\n * Returns a server middleware that serves the OpenAPI spec as JSON at\n * the configured path (default: `/openapi.json`).\n *\n * The spec is lazily generated on first request and cached.\n */\nexport function serveOpenApi(\n options: ServeOpenApiOptions\n): (context: RequestContext, next: () => Promise<void>) => Promise<void> {\n const servePath = options.path ?? '/openapi.json';\n let cachedSpec: OpenApiDocument | null = null;\n\n return async (context, next) => {\n const url = context.url;\n const pathname = url.pathname;\n const method = context.method.toUpperCase();\n\n if (method === 'GET' && pathname === servePath) {\n if (!cachedSpec) {\n cachedSpec = generateOpenApiSpec({\n registrations: options.getRegistrations(),\n info: options.info,\n servers: options.servers,\n authConfig: options.authConfig,\n securitySchemes: options.securitySchemes\n });\n }\n const body = JSON.stringify(cachedSpec);\n context.response.writeHead(200, {\n 'content-type': 'application/json',\n 'content-length': Buffer.byteLength(body).toString()\n });\n context.response.end(body);\n return;\n }\n\n await next();\n };\n}\n"],"mappings":"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","names":["fs","path","toJsonSchema","isParseStringSchema","p","convertColonParams","path","paramNames","_","name","convertParseStringPath","pathTemplate","info","templateDef","props","pathString","i","parameters","seg","resolvePath","meta","basePath","normalizeSlashes","combined","converted","fullPath","result","toJsonSchema","convertSchema","schema","registry","nameResolver","s","SchemaRegistry","schema","name","existing","walkSchemas","registry","visited","info","props","child","elements","el","options","opt","resolved","mapSecuritySchemes","authConfig","result","scheme","name","challenge","cookieName","mapOperationSecurity","authRoles","securitySchemeNames","buildParameterObject","name","location","schema","required","description","param","buildRequestBody","bodySchema","registry","jsonSchema","convertSchema","bodyInfo","body","HTTP_STATUS_DESCRIPTIONS","PROBLEM_DETAILS_SCHEMA","buildResponses","meta","method","result","codeStr","code","desc","respInfo","customDesc","buildOperation","pathParams","securitySchemeNames","operation","parameters","pp","props","propSchema","propInfo","isRequired","security","mapOperationSecurity","authRoles","generateOpenApiSpec","options","registrations","info","servers","authConfig","securitySchemes","tags","resolvedSchemes","mapSecuritySchemes","SchemaRegistry","visited","reg","walkSchemas","queryProps","headerProps","resolveComponentSchemaName","rootSchema","inlinedRoot","candidate","paths","path","resolvePath","explicitNames","t","autoNames","tag","mergedTags","doc","s","componentSchemas","hasSchemas","components","writeOpenApiSpec","options","outputPath","spec","generateOpenApiSpec","dir","endpoint","createOpenApiEndpoint","options","servePath","ep","endpoint","cachedSpec","generateOpenApiSpec","serveOpenApi","options","servePath","cachedSpec","context","next","pathname","generateOpenApiSpec","body"]}
package/package.json CHANGED
@@ -5,10 +5,10 @@
5
5
  "email": "andrew_zol@cleverbrush.com"
6
6
  },
7
7
  "peerDependencies": {
8
- "@cleverbrush/server": "0.0.0-beta-20260415102753",
9
- "@cleverbrush/schema": "0.0.0-beta-20260415102753",
10
- "@cleverbrush/schema-json": "0.0.0-beta-20260415102753",
11
- "@cleverbrush/auth": "0.0.0-beta-20260415102753"
8
+ "@cleverbrush/server": "0.0.0-beta-20260415113443",
9
+ "@cleverbrush/schema": "0.0.0-beta-20260415113443",
10
+ "@cleverbrush/schema-json": "0.0.0-beta-20260415113443",
11
+ "@cleverbrush/auth": "0.0.0-beta-20260415113443"
12
12
  },
13
13
  "description": "OpenAPI 3.1 spec generation for @cleverbrush/server — automatic endpoint documentation",
14
14
  "files": [
@@ -45,5 +45,5 @@
45
45
  },
46
46
  "type": "module",
47
47
  "types": "./dist/index.d.ts",
48
- "version": "0.0.0-beta-20260415102753"
48
+ "version": "0.0.0-beta-20260415113443"
49
49
  }