@cleverbrush/server-openapi 0.0.0-beta-20260413195755
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 +141 -0
- package/dist/cli.d.ts +26 -0
- package/dist/generateOpenApiSpec.d.ts +48 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -0
- package/dist/openApiEndpoint.d.ts +40 -0
- package/dist/pathUtils.d.ts +24 -0
- package/dist/schemaConverter.d.ts +8 -0
- package/dist/securityMapper.d.ts +26 -0
- package/dist/serveOpenApi.d.ts +27 -0
- package/package.json +49 -0
package/README.md
ADDED
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
# @cleverbrush/server-openapi
|
|
2
|
+
|
|
3
|
+
[](https://github.com/cleverbrush/framework/actions/workflows/ci.yml)
|
|
4
|
+
[](../../LICENSE)
|
|
5
|
+
|
|
6
|
+
OpenAPI 3.1 specification generation for [`@cleverbrush/server`](../server). Converts endpoint registrations, schema definitions, and authentication configuration into a fully-formed OpenAPI document — no annotations, no decorators.
|
|
7
|
+
|
|
8
|
+
## Features
|
|
9
|
+
|
|
10
|
+
- **`generateOpenApiSpec()`** — converts `@cleverbrush/server` endpoint registrations into an OpenAPI 3.1 document.
|
|
11
|
+
- **Schema conversion** — maps `@cleverbrush/schema` builders to JSON Schema Draft 2020-12 via `@cleverbrush/schema-json`.
|
|
12
|
+
- **Path resolution** — converts both colon-style paths (`:id`) and `ParseStringSchemaBuilder` templates to OpenAPI `{param}` format with per-parameter schemas.
|
|
13
|
+
- **Security mapping** — translates `@cleverbrush/auth` authentication schemes to OpenAPI `securitySchemes`; maps per-endpoint `authorize()` to `security` arrays.
|
|
14
|
+
- **`serveOpenApi()`** — middleware that lazily generates and caches the spec; serves it at a configurable path (default: `/openapi.json`).
|
|
15
|
+
- **`createOpenApiEndpoint()`** — returns a typed endpoint + handler pair for use with `ServerBuilder.handle()`.
|
|
16
|
+
- **CLI / build script** — `writeOpenApiSpec()` writes the spec to a file.
|
|
17
|
+
|
|
18
|
+
## Installation
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
npm install @cleverbrush/server-openapi @cleverbrush/server @cleverbrush/schema
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## Quick Start
|
|
25
|
+
|
|
26
|
+
```ts
|
|
27
|
+
import { ServerBuilder, endpoint } from '@cleverbrush/server';
|
|
28
|
+
import { serveOpenApi } from '@cleverbrush/server-openapi';
|
|
29
|
+
import { object, string, number } from '@cleverbrush/schema';
|
|
30
|
+
|
|
31
|
+
const GetUser = endpoint
|
|
32
|
+
.get('/api/users/:id')
|
|
33
|
+
.summary('Get a user by ID')
|
|
34
|
+
.tags('users');
|
|
35
|
+
|
|
36
|
+
const server = new ServerBuilder();
|
|
37
|
+
|
|
38
|
+
server
|
|
39
|
+
.use(serveOpenApi({
|
|
40
|
+
getRegistrations: () => server.getRegistrations(),
|
|
41
|
+
info: { title: 'My API', version: '1.0.0' }
|
|
42
|
+
}))
|
|
43
|
+
.handle(GetUser, ({ params }) => ({ id: params.id }));
|
|
44
|
+
|
|
45
|
+
await server.listen(3000);
|
|
46
|
+
// GET /openapi.json → OpenAPI 3.1 document
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## Generating the Spec
|
|
50
|
+
|
|
51
|
+
### As middleware (recommended)
|
|
52
|
+
|
|
53
|
+
```ts
|
|
54
|
+
import { serveOpenApi } from '@cleverbrush/server-openapi';
|
|
55
|
+
|
|
56
|
+
server.use(serveOpenApi({
|
|
57
|
+
getRegistrations: () => server.getRegistrations(),
|
|
58
|
+
info: { title: 'My API', version: '1.0.0' },
|
|
59
|
+
servers: [{ url: 'https://api.example.com', description: 'Production' }],
|
|
60
|
+
path: '/openapi.json' // default
|
|
61
|
+
}));
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
### As a registered endpoint
|
|
65
|
+
|
|
66
|
+
```ts
|
|
67
|
+
import { createOpenApiEndpoint } from '@cleverbrush/server-openapi';
|
|
68
|
+
|
|
69
|
+
const { endpoint: openApiEp, handler } = createOpenApiEndpoint({
|
|
70
|
+
getRegistrations: () => server.getRegistrations(),
|
|
71
|
+
info: { title: 'My API', version: '1.0.0' }
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
server.handle(openApiEp, handler);
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
### Generating to a file (build scripts)
|
|
78
|
+
|
|
79
|
+
```ts
|
|
80
|
+
import { writeOpenApiSpec } from '@cleverbrush/server-openapi';
|
|
81
|
+
|
|
82
|
+
await writeOpenApiSpec({
|
|
83
|
+
registrations: server.getRegistrations(),
|
|
84
|
+
info: { title: 'My API', version: '1.0.0' },
|
|
85
|
+
outputPath: './openapi.json'
|
|
86
|
+
});
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
## Authentication & Security Schemes
|
|
90
|
+
|
|
91
|
+
Pass the server's `AuthenticationConfig` to automatically generate `securitySchemes` and per-operation `security` arrays:
|
|
92
|
+
|
|
93
|
+
```ts
|
|
94
|
+
import { jwtScheme } from '@cleverbrush/auth';
|
|
95
|
+
|
|
96
|
+
const authConfig = {
|
|
97
|
+
defaultScheme: 'jwt',
|
|
98
|
+
schemes: [jwtScheme({ secret: '...', mapClaims: c => c })]
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
server.use(serveOpenApi({
|
|
102
|
+
getRegistrations: () => server.getRegistrations(),
|
|
103
|
+
info: { title: 'My API', version: '1.0.0' },
|
|
104
|
+
authConfig
|
|
105
|
+
}));
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
JWT schemes generate `{ type: 'http', scheme: 'bearer', bearerFormat: 'JWT' }`; cookie schemes generate `{ type: 'apiKey', in: 'cookie' }`.
|
|
109
|
+
|
|
110
|
+
## OpenAPI Info
|
|
111
|
+
|
|
112
|
+
```ts
|
|
113
|
+
const info: OpenApiInfo = {
|
|
114
|
+
title: 'My API',
|
|
115
|
+
version: '2.0.0',
|
|
116
|
+
description: 'Full description of my API.',
|
|
117
|
+
termsOfService: 'https://example.com/tos',
|
|
118
|
+
contact: { name: 'Support', email: 'support@example.com' },
|
|
119
|
+
license: { name: 'MIT', url: 'https://opensource.org/licenses/MIT' }
|
|
120
|
+
};
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
## Path Parameters
|
|
124
|
+
|
|
125
|
+
Both path styles are supported:
|
|
126
|
+
|
|
127
|
+
```ts
|
|
128
|
+
// Colon style — converted to {id}
|
|
129
|
+
endpoint.get('/api/users/:id');
|
|
130
|
+
|
|
131
|
+
// ParseStringSchemaBuilder — type-safe, with schema
|
|
132
|
+
import { route } from '@cleverbrush/server';
|
|
133
|
+
import { object, number } from '@cleverbrush/schema';
|
|
134
|
+
|
|
135
|
+
endpoint.get(route(object({ id: number().coerce() }), $t => $t`/api/users/${t => t.id}`));
|
|
136
|
+
// produces path "/api/users/{id}" with schema { type: 'number' }
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
## License
|
|
140
|
+
|
|
141
|
+
BSD-3-Clause — see [LICENSE](../../LICENSE).
|
package/dist/cli.d.ts
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { type OpenApiDocument, type OpenApiOptions } from './generateOpenApiSpec.js';
|
|
2
|
+
/**
|
|
3
|
+
* Generate an OpenAPI spec and write it to a file.
|
|
4
|
+
*
|
|
5
|
+
* @param options - Same options as `generateOpenApiSpec()`.
|
|
6
|
+
* @param outputPath - File path to write the JSON spec to.
|
|
7
|
+
* @returns The generated spec document.
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* ```ts
|
|
11
|
+
* import { writeOpenApiSpec } from '@cleverbrush/server-openapi';
|
|
12
|
+
* import { createServer, endpoint } from '@cleverbrush/server';
|
|
13
|
+
*
|
|
14
|
+
* const builder = createServer()
|
|
15
|
+
* .handle(endpoint.get('/api/health'), () => ({ ok: true }));
|
|
16
|
+
*
|
|
17
|
+
* writeOpenApiSpec(
|
|
18
|
+
* {
|
|
19
|
+
* registrations: builder.getRegistrations(),
|
|
20
|
+
* info: { title: 'My API', version: '1.0.0' }
|
|
21
|
+
* },
|
|
22
|
+
* './openapi.json'
|
|
23
|
+
* );
|
|
24
|
+
* ```
|
|
25
|
+
*/
|
|
26
|
+
export declare function writeOpenApiSpec(options: OpenApiOptions, outputPath: string): OpenApiDocument;
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import type { AuthenticationConfig, EndpointRegistration } from '@cleverbrush/server';
|
|
2
|
+
import { type OpenApiSecurityScheme } from './securityMapper.js';
|
|
3
|
+
/**
|
|
4
|
+
* API metadata included in the OpenAPI `info` object.
|
|
5
|
+
* Maps directly to the OpenAPI 3.1 Info Object.
|
|
6
|
+
*/
|
|
7
|
+
export interface OpenApiInfo {
|
|
8
|
+
readonly title: string;
|
|
9
|
+
readonly version: string;
|
|
10
|
+
readonly description?: string;
|
|
11
|
+
readonly termsOfService?: string;
|
|
12
|
+
readonly contact?: {
|
|
13
|
+
readonly name?: string;
|
|
14
|
+
readonly url?: string;
|
|
15
|
+
readonly email?: string;
|
|
16
|
+
};
|
|
17
|
+
readonly license?: {
|
|
18
|
+
readonly name: string;
|
|
19
|
+
readonly url?: string;
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* A server entry in the OpenAPI `servers` array.
|
|
24
|
+
* Describes a base URL where the API is accessible.
|
|
25
|
+
*/
|
|
26
|
+
export interface OpenApiServer {
|
|
27
|
+
readonly url: string;
|
|
28
|
+
readonly description?: string;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Options passed to {@link generateOpenApiSpec}.
|
|
32
|
+
*/
|
|
33
|
+
export interface OpenApiOptions {
|
|
34
|
+
readonly registrations: readonly EndpointRegistration[];
|
|
35
|
+
readonly info: OpenApiInfo;
|
|
36
|
+
readonly servers?: readonly OpenApiServer[];
|
|
37
|
+
readonly authConfig?: AuthenticationConfig | null;
|
|
38
|
+
readonly securitySchemes?: Record<string, OpenApiSecurityScheme>;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* A generated OpenAPI 3.1 document. Typed as a plain object map to allow
|
|
42
|
+
* any extension fields without requiring a full OpenAPI type library.
|
|
43
|
+
*/
|
|
44
|
+
export type OpenApiDocument = Record<string, unknown>;
|
|
45
|
+
/**
|
|
46
|
+
* Generate an OpenAPI 3.1 specification document from registered endpoints.
|
|
47
|
+
*/
|
|
48
|
+
export declare function generateOpenApiSpec(options: OpenApiOptions): OpenApiDocument;
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export { writeOpenApiSpec } from './cli.js';
|
|
2
|
+
export { generateOpenApiSpec, type OpenApiDocument, type OpenApiInfo, type OpenApiOptions, type OpenApiServer } from './generateOpenApiSpec.js';
|
|
3
|
+
export { createOpenApiEndpoint, type OpenApiEndpointOptions } from './openApiEndpoint.js';
|
|
4
|
+
export { type PathParameterInfo, type ResolvedPath, resolvePath } from './pathUtils.js';
|
|
5
|
+
export { convertSchema } from './schemaConverter.js';
|
|
6
|
+
export { mapOperationSecurity, mapSecuritySchemes, type OpenApiSecurityScheme } from './securityMapper.js';
|
|
7
|
+
export { type ServeOpenApiOptions, serveOpenApi } from './serveOpenApi.js';
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import*as h from"fs";import*as v from"path";import{toJsonSchema as b}from"@cleverbrush/schema-json";function I(e){return typeof e!="string"&&typeof e.validate=="function"}function x(e){let t=[];return{converted:e.replace(/:([a-zA-Z_][a-zA-Z0-9_]*)/g,(n,o)=>(t.push(o),`{${o}}`)),paramNames:t}}function w(e){let t=e.introspect(),r=t.templateDefinition,s=t.objectSchema.introspect().properties??{},a="";for(let i=0;i<r.segments.length;i++)a+=r.literals[i]+`{${r.segments[i].path}}`;a+=r.literals[r.segments.length]??"";let u=r.segments.map(i=>({name:i.path,schema:s[i.path]?b(s[i.path],{$schema:!1}):{type:"string"}}));return{pathString:a,parameters:u}}function f(e){let t=e.basePath.replace(/\/$/,""),r=e.pathTemplate;if(I(r)){let{pathString:c,parameters:p}=w(r);return{path:A(t+c),parameters:p}}let o=t+(r==="/"?"":r),{converted:s,paramNames:a}=x(o),u=A(s),i=a.map(c=>({name:c,schema:{type:"string"}}));return{path:u,parameters:i}}function A(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 k}from"@cleverbrush/schema-json";function l(e){return e==null?{}:k(e,{$schema:!1,draft:"2020-12"})}function g(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 s=r.cookieName??r._options?.cookieName??"session";t[n]={type:"apiKey",in:"cookie",name:s}}else o?t[n]={type:"http",scheme:o.headerValue.split(" ")[0].toLowerCase()}:t[n]={type:"http",scheme:n}}return t}function S(e,t){return e===null?[]:t.length===0?[]:t.map(r=>({[r]:[...e]}))}function O(e,t,r,n,o){let s={name:e,in:t,schema:r};return n&&(s.required=!0),o&&(s.description=o),s}function C(e){let t=l(e),r=e.introspect(),n={required:r.isRequired!==!1,content:{"application/json":{schema:t}}};return typeof r.description=="string"&&r.description!==""&&(n.description=r.description),n}function E(e,t){if(e){let r=l(e),n=e.introspect();return{200:{description:typeof n.description=="string"&&n.description!==""?n.description:"Successful response",content:{"application/json":{schema:r}}}}}return t==="DELETE"||t==="HEAD"?{204:{description:"No content"}}:{200:{description:"Successful response"}}}function j(e,t,r){let n={};e.summary&&(n.summary=e.summary),e.description&&(n.description=e.description),e.tags.length>0&&(n.tags=[...e.tags]),e.operationId&&(n.operationId=e.operationId),e.deprecated&&(n.deprecated=!0);let o=[];for(let a of t)o.push(O(a.name,"path",a.schema,!0));if(e.querySchema){let u=e.querySchema.introspect().properties??{};for(let[i,c]of Object.entries(u)){let p=c.introspect(),d=p.isRequired!==!1,m=typeof p.description=="string"&&p.description!==""?p.description:void 0;o.push(O(i,"query",l(c),d,m))}}if(e.headerSchema){let u=e.headerSchema.introspect().properties??{};for(let[i,c]of Object.entries(u)){let p=c.introspect(),d=p.isRequired!==!1,m=typeof p.description=="string"&&p.description!==""?p.description:void 0;o.push(O(i,"header",l(c),d,m))}}o.length>0&&(n.parameters=o),e.bodySchema&&(n.requestBody=C(e.bodySchema)),n.responses=E(e.responseSchema,e.method.toUpperCase());let s=S(q(e),r);return s.length>0&&(n.security=s),n}function q(e){return e.authRoles}function y(e){let{registrations:t,info:r,servers:n,authConfig:o,securitySchemes:s}=e,a=s??g(o),u=Object.keys(a),i={};for(let p of t){let d=p.endpoint,{path:m,parameters:R}=f(d),P=d.method.toLowerCase();i[m]||(i[m]={}),i[m][P]=j(d,R,u)}let c={openapi:"3.1.0",info:{...r}};return n&&n.length>0&&(c.servers=n.map(p=>({...p}))),c.paths=i,u.length>0&&(c.components={securitySchemes:{...a}}),c}function B(e,t){let r=y(e),n=v.dirname(t);return n&&!h.existsSync(n)&&h.mkdirSync(n,{recursive:!0}),h.writeFileSync(t,JSON.stringify(r,null,2),"utf-8"),r}import{endpoint as D}from"@cleverbrush/server";function N(e){let t=e.path??"/openapi.json",r=D.get(t).summary("OpenAPI specification").tags("OpenAPI").operationId("getOpenApiSpec"),n=null;return{endpoint:r,handler:()=>(n||(n=y({registrations:e.getRegistrations(),info:e.info,servers:e.servers,authConfig:e.authConfig,securitySchemes:e.securitySchemes})),n)}}function M(e){let t=e.path??"/openapi.json",r=null;return async(n,o)=>{let a=n.url.pathname;if(n.method.toUpperCase()==="GET"&&a===t){r||(r=y({registrations:e.getRegistrations(),info:e.info,servers:e.servers,authConfig:e.authConfig,securitySchemes:e.securitySchemes}));let i=JSON.stringify(r);n.response.writeHead(200,{"content-type":"application/json","content-length":Buffer.byteLength(i).toString()}),n.response.end(i);return}await o()}}export{l as convertSchema,N as createOpenApiEndpoint,y as generateOpenApiSpec,S as mapOperationSecurity,g as mapSecuritySchemes,f as resolvePath,M as serveOpenApi,B as writeOpenApiSpec};
|
|
2
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/cli.ts","../src/pathUtils.ts","../src/schemaConverter.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';\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 */\nexport function convertSchema(\n schema: SchemaBuilder<any, any, any, any, any> | null | undefined\n): Record<string, unknown> {\n if (schema == null) return {};\n return toJsonSchema(schema, { $schema: false, draft: '2020-12' });\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 {\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 * 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\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): Record<string, unknown> {\n const jsonSchema = convertSchema(bodySchema);\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\nfunction buildResponses(\n responseSchema: SchemaBuilder<any, any, any, any, any> | null,\n method: string\n): Record<string, unknown> {\n if (responseSchema) {\n const jsonSchema = convertSchema(responseSchema);\n const respInfo = responseSchema.introspect() as any;\n const desc =\n typeof respInfo.description === 'string' &&\n respInfo.description !== ''\n ? respInfo.description\n : 'Successful response';\n return {\n '200': {\n description: desc,\n content: {\n 'application/json': { schema: jsonSchema }\n }\n }\n };\n }\n\n // No response schema — use 204 for methods that typically don't return content\n if (method === 'DELETE' || method === 'HEAD') {\n return { '204': { description: 'No content' } };\n }\n\n return { '200': { description: 'Successful response' } };\n}\n\nfunction buildOperation(\n meta: EndpointMetadata,\n pathParams: { name: string; schema: Record<string, unknown> }[],\n securitySchemeNames: string[]\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),\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),\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);\n }\n\n // Responses\n operation['responses'] = buildResponses(\n meta.responseSchema,\n meta.method.toUpperCase()\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 } =\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 // 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 );\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 doc['paths'] = paths;\n\n // Components\n if (securitySchemeNames.length > 0) {\n doc['components'] = {\n securitySchemes: { ...resolvedSchemes }\n };\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":"AAAA,UAAYA,MAAQ,KACpB,UAAYC,MAAU,OCGtB,OAAS,gBAAAC,MAAoB,2BAK7B,SAASC,EACLC,EACsD,CACtD,OAAO,OAAOA,GAAM,UAAY,OAAQA,EAAU,UAAa,UACnE,CA0BA,SAASC,EAAmBC,EAG1B,CACE,IAAMC,EAAuB,CAAC,EAK9B,MAAO,CAAE,UAJSD,EAAK,QAAQ,6BAA8B,CAACE,EAAGC,KAC7DF,EAAW,KAAKE,CAAI,EACb,IAAIA,CAAI,IAClB,EACmB,WAAAF,CAAW,CACnC,CAMA,SAASG,EACLC,EACuD,CACvD,IAAMC,EAAOD,EAAa,WAAW,EAC/BE,EAAcD,EAAK,mBAMnBE,EAFeF,EAAK,aACG,WAAW,EAI5B,YAAc,CAAC,EAEvBG,EAAa,GACjB,QAAS,EAAI,EAAG,EAAIF,EAAY,SAAS,OAAQ,IAC7CE,GACIF,EAAY,SAAS,CAAC,EAAI,IAAIA,EAAY,SAAS,CAAC,EAAE,IAAI,IAElEE,GAAcF,EAAY,SAASA,EAAY,SAAS,MAAM,GAAK,GAEnE,IAAMG,EAAkCH,EAAY,SAAS,IAAII,IAAQ,CACrE,KAAMA,EAAI,KACV,OAAQH,EAAMG,EAAI,IAAI,EAChBf,EAAaY,EAAMG,EAAI,IAAI,EAAG,CAAE,QAAS,EAAM,CAAC,EAChD,CAAE,KAAM,QAAS,CAC3B,EAAE,EAEF,MAAO,CAAE,WAAAF,EAAY,WAAAC,CAAW,CACpC,CAMO,SAASE,EAAYC,EAAsC,CAC9D,IAAMC,EAAWD,EAAK,SAAS,QAAQ,MAAO,EAAE,EAC1CR,EAAeQ,EAAK,aAE1B,GAAIhB,EAAoBQ,CAAY,EAAG,CACnC,GAAM,CAAE,WAAAI,EAAY,WAAAC,CAAW,EAAIN,EAAuBC,CAAY,EAEtE,MAAO,CAAE,KADQU,EAAiBD,EAAWL,CAAU,EAC9B,WAAAC,CAAW,CACxC,CAIA,IAAMM,EAAWF,GADGT,IAAiB,IAAM,GAAKA,GAE1C,CAAE,UAAAY,EAAW,WAAAhB,CAAW,EAAIF,EAAmBiB,CAAQ,EACvDE,EAAWH,EAAiBE,CAAS,EAErCP,EAAkCT,EAAW,IAAIE,IAAS,CAC5D,KAAAA,EACA,OAAQ,CAAE,KAAM,QAAS,CAC7B,EAAE,EAEF,MAAO,CAAE,KAAMe,EAAU,WAAAR,CAAW,CACxC,CAEA,SAASK,EAAiBf,EAAsB,CAE5C,IAAImB,EAASnB,EAAK,QAAQ,OAAQ,GAAG,EACrC,OAAKmB,EAAO,WAAW,GAAG,IAAGA,EAAS,IAAMA,GACxCA,EAAO,OAAS,GAAKA,EAAO,SAAS,GAAG,IAAGA,EAASA,EAAO,MAAM,EAAG,EAAE,GACnEA,GAAU,GACrB,CCxHA,OAAS,gBAAAC,MAAoB,2BAQtB,SAASC,EACZC,EACuB,CACvB,OAAIA,GAAU,KAAa,CAAC,EACrBF,EAAaE,EAAQ,CAAE,QAAS,GAAO,MAAO,SAAU,CAAC,CACpE,CCKO,SAASC,EACZC,EACqC,CACrC,GAAI,CAACA,EAAY,MAAO,CAAC,EAEzB,IAAMC,EAAgD,CAAC,EACvD,QAAWC,KAAUF,EAAW,QAAS,CACrC,IAAMG,EAAOD,EAAO,KACdE,EAAYF,EAAO,YAAY,EAErC,GACIE,GAAW,aAAa,YAAY,EAAE,WAAW,QAAQ,GACzDD,IAAS,MAETF,EAAOE,CAAI,EAAI,CACX,KAAM,OACN,OAAQ,SACR,aAAc,KAClB,UACOA,IAAS,UAAaD,EAAe,WAAY,CAExD,IAAMG,EACDH,EAAe,YACfA,EAAe,UAAU,YAC1B,UACJD,EAAOE,CAAI,EAAI,CACX,KAAM,SACN,GAAI,SACJ,KAAME,CACV,CACJ,MAAWD,EAEPH,EAAOE,CAAI,EAAI,CACX,KAAM,OACN,OAAQC,EAAU,YAAY,MAAM,GAAG,EAAE,CAAC,EAAE,YAAY,CAC5D,EAGAH,EAAOE,CAAI,EAAI,CAAE,KAAM,OAAQ,OAAQA,CAAK,CAEpD,CACA,OAAOF,CACX,CASO,SAASK,EACZC,EACAC,EAC0B,CAC1B,OAAID,IAAc,KAAa,CAAC,EAC5BC,EAAoB,SAAW,EAAU,CAAC,EAGvCA,EAAoB,IAAIL,IAAS,CACpC,CAACA,CAAI,EAAG,CAAC,GAAGI,CAAS,CACzB,EAAE,CACN,CCTA,SAASE,EACLC,EACAC,EACAC,EACAC,EACAC,EACuB,CACvB,IAAMC,EAAiC,CACnC,KAAAL,EACA,GAAIC,EACJ,OAAAC,CACJ,EACA,OAAIC,IAAUE,EAAM,SAAc,IAC9BD,IAAaC,EAAM,YAAiBD,GACjCC,CACX,CAEA,SAASC,EACLC,EACuB,CACvB,IAAMC,EAAaC,EAAcF,CAAU,EACrCG,EAAWH,EAAW,WAAW,EACjCI,EAAgC,CAClC,SAAUD,EAAS,aAAe,GAClC,QAAS,CACL,mBAAoB,CAAE,OAAQF,CAAW,CAC7C,CACJ,EACA,OAAI,OAAOE,EAAS,aAAgB,UAAYA,EAAS,cAAgB,KACrEC,EAAK,YAAiBD,EAAS,aAC5BC,CACX,CAEA,SAASC,EACLC,EACAC,EACuB,CACvB,GAAID,EAAgB,CAChB,IAAML,EAAaC,EAAcI,CAAc,EACzCE,EAAWF,EAAe,WAAW,EAM3C,MAAO,CACH,IAAO,CACH,YANJ,OAAOE,EAAS,aAAgB,UAChCA,EAAS,cAAgB,GACnBA,EAAS,YACT,sBAIF,QAAS,CACL,mBAAoB,CAAE,OAAQP,CAAW,CAC7C,CACJ,CACJ,CACJ,CAGA,OAAIM,IAAW,UAAYA,IAAW,OAC3B,CAAE,IAAO,CAAE,YAAa,YAAa,CAAE,EAG3C,CAAE,IAAO,CAAE,YAAa,qBAAsB,CAAE,CAC3D,CAEA,SAASE,EACLC,EACAC,EACAC,EACuB,CACvB,IAAMC,EAAqC,CAAC,EAGxCH,EAAK,UAASG,EAAU,QAAaH,EAAK,SAC1CA,EAAK,cAAaG,EAAU,YAAiBH,EAAK,aAClDA,EAAK,KAAK,OAAS,IAAGG,EAAU,KAAU,CAAC,GAAGH,EAAK,IAAI,GACvDA,EAAK,cAAaG,EAAU,YAAiBH,EAAK,aAClDA,EAAK,aAAYG,EAAU,WAAgB,IAG/C,IAAMC,EAAwC,CAAC,EAG/C,QAAWC,KAAMJ,EACbG,EAAW,KAAKtB,EAAqBuB,EAAG,KAAM,OAAQA,EAAG,OAAQ,EAAI,CAAC,EAI1E,GAAIL,EAAK,YAAa,CAElB,IAAMM,EADYN,EAAK,YAAY,WAAW,EAIhC,YAAc,CAAC,EAC7B,OAAW,CAACjB,EAAMwB,CAAU,IAAK,OAAO,QAAQD,CAAK,EAAG,CACpD,IAAME,EAAWD,EAAW,WAAW,EACjCE,EAAaD,EAAS,aAAe,GACrCrB,EACF,OAAOqB,EAAS,aAAgB,UAChCA,EAAS,cAAgB,GACnBA,EAAS,YACT,OACVJ,EAAW,KACPtB,EACIC,EACA,QACAS,EAAce,CAAU,EACxBE,EACAtB,CACJ,CACJ,CACJ,CACJ,CAGA,GAAIa,EAAK,aAAc,CAEnB,IAAMM,EADaN,EAAK,aAAa,WAAW,EAIjC,YAAc,CAAC,EAC9B,OAAW,CAACjB,EAAMwB,CAAU,IAAK,OAAO,QAAQD,CAAK,EAAG,CACpD,IAAME,EAAWD,EAAW,WAAW,EACjCE,EAAaD,EAAS,aAAe,GACrCrB,EACF,OAAOqB,EAAS,aAAgB,UAChCA,EAAS,cAAgB,GACnBA,EAAS,YACT,OACVJ,EAAW,KACPtB,EACIC,EACA,SACAS,EAAce,CAAU,EACxBE,EACAtB,CACJ,CACJ,CACJ,CACJ,CAEIiB,EAAW,OAAS,IAAGD,EAAU,WAAgBC,GAGjDJ,EAAK,aACLG,EAAU,YAAiBd,EAAiBW,EAAK,UAAU,GAI/DG,EAAU,UAAeR,EACrBK,EAAK,eACLA,EAAK,OAAO,YAAY,CAC5B,EAGA,IAAMU,EAAWC,EAAqBC,EAAUZ,CAAI,EAAGE,CAAmB,EAC1E,OAAIQ,EAAS,OAAS,IAAGP,EAAU,SAAcO,GAE1CP,CACX,CAEA,SAASS,EAAUZ,EAAkD,CACjE,OAAOA,EAAK,SAChB,CAKO,SAASa,EAAoBC,EAA0C,CAC1E,GAAM,CAAE,cAAAC,EAAe,KAAAC,EAAM,QAAAC,EAAS,WAAAC,EAAY,gBAAAC,CAAgB,EAC9DL,EAGEM,EACFD,GAAmBE,EAAmBH,CAAU,EAC9ChB,EAAsB,OAAO,KAAKkB,CAAe,EAGjDE,EAAiD,CAAC,EAExD,QAAWC,KAAOR,EAAe,CAC7B,IAAMf,EAAOuB,EAAI,SACX,CAAE,KAAAC,EAAM,WAAYvB,CAAW,EAAIwB,EAAYzB,CAAI,EACnDH,EAASG,EAAK,OAAO,YAAY,EAElCsB,EAAME,CAAI,IAAGF,EAAME,CAAI,EAAI,CAAC,GACjCF,EAAME,CAAI,EAAE3B,CAAM,EAAIE,EAClBC,EACAC,EACAC,CACJ,CACJ,CAGA,IAAMwB,EAAuB,CACzB,QAAS,QACT,KAAM,CAAE,GAAGV,CAAK,CACpB,EAEA,OAAIC,GAAWA,EAAQ,OAAS,IAC5BS,EAAI,QAAaT,EAAQ,IAAIU,IAAM,CAAE,GAAGA,CAAE,EAAE,GAGhDD,EAAI,MAAWJ,EAGXpB,EAAoB,OAAS,IAC7BwB,EAAI,WAAgB,CAChB,gBAAiB,CAAE,GAAGN,CAAgB,CAC1C,GAGGM,CACX,CJ3PO,SAASE,EACZC,EACAC,EACe,CACf,IAAMC,EAAOC,EAAoBH,CAAO,EAClCI,EAAW,UAAQH,CAAU,EACnC,OAAIG,GAAO,CAAI,aAAWA,CAAG,GACtB,YAAUA,EAAK,CAAE,UAAW,EAAK,CAAC,EAEtC,gBAAcH,EAAY,KAAK,UAAUC,EAAM,KAAM,CAAC,EAAG,OAAO,EAC5DA,CACX,CKvCA,OAAS,YAAAG,MAAgB,sBA0ClB,SAASC,EAAsBC,EAGpC,CACE,IAAMC,EAAYD,EAAQ,MAAQ,gBAE5BE,EAAKC,EACN,IAAIF,CAAS,EACb,QAAQ,uBAAuB,EAC/B,KAAK,SAAS,EACd,YAAY,gBAAgB,EAE7BG,EAAqC,KAezC,MAAO,CAAE,SAAUF,EAAI,QAbP,KACPE,IACDA,EAAaC,EAAoB,CAC7B,cAAeL,EAAQ,iBAAiB,EACxC,KAAMA,EAAQ,KACd,QAASA,EAAQ,QACjB,WAAYA,EAAQ,WACpB,gBAAiBA,EAAQ,eAC7B,CAAC,GAEEI,EAGoB,CACnC,CCrCO,SAASE,EACZC,EACqE,CACrE,IAAMC,EAAYD,EAAQ,MAAQ,gBAC9BE,EAAqC,KAEzC,MAAO,OAAOC,EAASC,IAAS,CAE5B,IAAMC,EADMF,EAAQ,IACC,SAGrB,GAFeA,EAAQ,OAAO,YAAY,IAE3B,OAASE,IAAaJ,EAAW,CACvCC,IACDA,EAAaI,EAAoB,CAC7B,cAAeN,EAAQ,iBAAiB,EACxC,KAAMA,EAAQ,KACd,QAASA,EAAQ,QACjB,WAAYA,EAAQ,WACpB,gBAAiBA,EAAQ,eAC7B,CAAC,GAEL,IAAMO,EAAO,KAAK,UAAUL,CAAU,EACtCC,EAAQ,SAAS,UAAU,IAAK,CAC5B,eAAgB,mBAChB,iBAAkB,OAAO,WAAWI,CAAI,EAAE,SAAS,CACvD,CAAC,EACDJ,EAAQ,SAAS,IAAII,CAAI,EACzB,MACJ,CAEA,MAAMH,EAAK,CACf,CACJ","names":["fs","path","toJsonSchema","isParseStringSchema","p","convertColonParams","path","paramNames","_","name","convertParseStringPath","pathTemplate","info","templateDef","props","pathString","parameters","seg","resolvePath","meta","basePath","normalizeSlashes","combined","converted","fullPath","result","toJsonSchema","convertSchema","schema","mapSecuritySchemes","authConfig","result","scheme","name","challenge","cookieName","mapOperationSecurity","authRoles","securitySchemeNames","buildParameterObject","name","location","schema","required","description","param","buildRequestBody","bodySchema","jsonSchema","convertSchema","bodyInfo","body","buildResponses","responseSchema","method","respInfo","buildOperation","meta","pathParams","securitySchemeNames","operation","parameters","pp","props","propSchema","propInfo","isRequired","security","mapOperationSecurity","authRoles","generateOpenApiSpec","options","registrations","info","servers","authConfig","securitySchemes","resolvedSchemes","mapSecuritySchemes","paths","reg","path","resolvePath","doc","s","writeOpenApiSpec","options","outputPath","spec","generateOpenApiSpec","dir","endpoint","createOpenApiEndpoint","options","servePath","ep","endpoint","cachedSpec","generateOpenApiSpec","serveOpenApi","options","servePath","cachedSpec","context","next","pathname","generateOpenApiSpec","body"]}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type { AuthenticationConfig, EndpointRegistration } from '@cleverbrush/server';
|
|
2
|
+
import { endpoint } from '@cleverbrush/server';
|
|
3
|
+
import { type OpenApiDocument, type OpenApiInfo, type OpenApiServer } from './generateOpenApiSpec.js';
|
|
4
|
+
import type { OpenApiSecurityScheme } from './securityMapper.js';
|
|
5
|
+
/**
|
|
6
|
+
* Options for {@link createOpenApiEndpoint}.
|
|
7
|
+
*/
|
|
8
|
+
export interface OpenApiEndpointOptions {
|
|
9
|
+
/** Function that returns endpoint registrations. */
|
|
10
|
+
readonly getRegistrations: () => readonly EndpointRegistration[];
|
|
11
|
+
/** OpenAPI info metadata. */
|
|
12
|
+
readonly info: OpenApiInfo;
|
|
13
|
+
/** Optional server entries. */
|
|
14
|
+
readonly servers?: readonly OpenApiServer[];
|
|
15
|
+
/** Optional auth config for security scheme generation. */
|
|
16
|
+
readonly authConfig?: AuthenticationConfig | null;
|
|
17
|
+
/** Override security schemes manually. */
|
|
18
|
+
readonly securitySchemes?: Record<string, OpenApiSecurityScheme>;
|
|
19
|
+
/** Path to serve the spec at (default: `/openapi.json`). */
|
|
20
|
+
readonly path?: string;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Creates an endpoint definition and handler that serves the OpenAPI spec
|
|
24
|
+
* as JSON. Register it with `builder.handle(ep, handler)`.
|
|
25
|
+
*
|
|
26
|
+
* The spec is lazily generated on first request and cached.
|
|
27
|
+
*
|
|
28
|
+
* @example
|
|
29
|
+
* ```ts
|
|
30
|
+
* const { endpoint: openApiEp, handler } = createOpenApiEndpoint({
|
|
31
|
+
* getRegistrations: () => builder.getRegistrations(),
|
|
32
|
+
* info: { title: 'My API', version: '1.0.0' }
|
|
33
|
+
* });
|
|
34
|
+
* builder.handle(openApiEp, handler);
|
|
35
|
+
* ```
|
|
36
|
+
*/
|
|
37
|
+
export declare function createOpenApiEndpoint(options: OpenApiEndpointOptions): {
|
|
38
|
+
endpoint: ReturnType<(typeof endpoint)['get']>;
|
|
39
|
+
handler: () => OpenApiDocument;
|
|
40
|
+
};
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { EndpointMetadata } from '@cleverbrush/server';
|
|
2
|
+
/**
|
|
3
|
+
* Describes a single path parameter extracted from a route template,
|
|
4
|
+
* including its name and the JSON Schema representation of its type.
|
|
5
|
+
*/
|
|
6
|
+
export interface PathParameterInfo {
|
|
7
|
+
readonly name: string;
|
|
8
|
+
readonly schema: Record<string, unknown>;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* The output of resolving a route path template to an OpenAPI-compatible
|
|
12
|
+
* path string and its parameter list.
|
|
13
|
+
*/
|
|
14
|
+
export interface ResolvedPath {
|
|
15
|
+
/** OpenAPI-formatted path, e.g. `/api/users/{id}` */
|
|
16
|
+
readonly path: string;
|
|
17
|
+
/** Extracted path parameters with their JSON Schema */
|
|
18
|
+
readonly parameters: readonly PathParameterInfo[];
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Combines `basePath` and `pathTemplate` from an endpoint into an
|
|
22
|
+
* OpenAPI-formatted path with extracted parameter information.
|
|
23
|
+
*/
|
|
24
|
+
export declare function resolvePath(meta: EndpointMetadata): ResolvedPath;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { SchemaBuilder } from '@cleverbrush/schema';
|
|
2
|
+
/**
|
|
3
|
+
* Converts a `@cleverbrush/schema` builder to a JSON Schema object suitable
|
|
4
|
+
* for embedding in an OpenAPI 3.1 spec (no `$schema` header, Draft 2020-12).
|
|
5
|
+
*
|
|
6
|
+
* Returns an empty schema `{}` when the input is `null` or `undefined`.
|
|
7
|
+
*/
|
|
8
|
+
export declare function convertSchema(schema: SchemaBuilder<any, any, any, any, any> | null | undefined): Record<string, unknown>;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { AuthenticationConfig } from '@cleverbrush/server';
|
|
2
|
+
/**
|
|
3
|
+
* An OpenAPI 3.1 Security Scheme Object.
|
|
4
|
+
* Describes how requests to the API are authenticated.
|
|
5
|
+
*
|
|
6
|
+
* @see {@link https://spec.openapis.org/oas/v3.1.0#security-scheme-object OpenAPI Security Scheme Object}
|
|
7
|
+
*/
|
|
8
|
+
export interface OpenApiSecurityScheme {
|
|
9
|
+
readonly type: string;
|
|
10
|
+
readonly scheme?: string;
|
|
11
|
+
readonly bearerFormat?: string;
|
|
12
|
+
readonly in?: string;
|
|
13
|
+
readonly name?: string;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Map `@cleverbrush/auth` authentication schemes to OpenAPI security scheme objects.
|
|
17
|
+
*/
|
|
18
|
+
export declare function mapSecuritySchemes(authConfig: AuthenticationConfig | null | undefined): Record<string, OpenApiSecurityScheme>;
|
|
19
|
+
/**
|
|
20
|
+
* Map endpoint `authRoles` to an OpenAPI operation-level `security` array.
|
|
21
|
+
*
|
|
22
|
+
* - `null` → empty array (public endpoint, no security)
|
|
23
|
+
* - `[]` → `[{ <schemeName>: [] }]` (any authenticated user)
|
|
24
|
+
* - `['admin']` → `[{ <schemeName>: ['admin'] }]` (require specific roles)
|
|
25
|
+
*/
|
|
26
|
+
export declare function mapOperationSecurity(authRoles: readonly string[] | null, securitySchemeNames: string[]): Record<string, string[]>[];
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { AuthenticationConfig, EndpointRegistration, RequestContext } from '@cleverbrush/server';
|
|
2
|
+
import { type OpenApiInfo, type OpenApiServer } from './generateOpenApiSpec.js';
|
|
3
|
+
import type { OpenApiSecurityScheme } from './securityMapper.js';
|
|
4
|
+
/**
|
|
5
|
+
* Options for the {@link serveOpenApi} middleware.
|
|
6
|
+
*/
|
|
7
|
+
export interface ServeOpenApiOptions {
|
|
8
|
+
/** Function that returns endpoint registrations. */
|
|
9
|
+
readonly getRegistrations: () => readonly EndpointRegistration[];
|
|
10
|
+
/** OpenAPI info metadata. */
|
|
11
|
+
readonly info: OpenApiInfo;
|
|
12
|
+
/** Optional server entries. */
|
|
13
|
+
readonly servers?: readonly OpenApiServer[];
|
|
14
|
+
/** Optional auth config for security scheme generation. */
|
|
15
|
+
readonly authConfig?: AuthenticationConfig | null;
|
|
16
|
+
/** Override security schemes manually. */
|
|
17
|
+
readonly securitySchemes?: Record<string, OpenApiSecurityScheme>;
|
|
18
|
+
/** Path to serve the spec at (default: `/openapi.json`). */
|
|
19
|
+
readonly path?: string;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Returns a server middleware that serves the OpenAPI spec as JSON at
|
|
23
|
+
* the configured path (default: `/openapi.json`).
|
|
24
|
+
*
|
|
25
|
+
* The spec is lazily generated on first request and cached.
|
|
26
|
+
*/
|
|
27
|
+
export declare function serveOpenApi(options: ServeOpenApiOptions): (context: RequestContext, next: () => Promise<void>) => Promise<void>;
|
package/package.json
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
{
|
|
2
|
+
"author": "Andrew Zolotukhin <andrew_zol@cleverbrush.com>",
|
|
3
|
+
"bugs": {
|
|
4
|
+
"url": "https://github.com/cleverbrush/framework/issues",
|
|
5
|
+
"email": "andrew_zol@cleverbrush.com"
|
|
6
|
+
},
|
|
7
|
+
"peerDependencies": {
|
|
8
|
+
"@cleverbrush/server": "0.0.0-beta-20260413195755",
|
|
9
|
+
"@cleverbrush/schema": "0.0.0-beta-20260413195755",
|
|
10
|
+
"@cleverbrush/schema-json": "0.0.0-beta-20260413195755",
|
|
11
|
+
"@cleverbrush/auth": "0.0.0-beta-20260413195755"
|
|
12
|
+
},
|
|
13
|
+
"description": "OpenAPI 3.1 spec generation for @cleverbrush/server — automatic endpoint documentation",
|
|
14
|
+
"files": [
|
|
15
|
+
"dist"
|
|
16
|
+
],
|
|
17
|
+
"homepage": "https://docs.cleverbrush.com/server-openapi",
|
|
18
|
+
"keywords": [
|
|
19
|
+
"openapi",
|
|
20
|
+
"swagger",
|
|
21
|
+
"server",
|
|
22
|
+
"schema",
|
|
23
|
+
"documentation",
|
|
24
|
+
"cleverbrush"
|
|
25
|
+
],
|
|
26
|
+
"license": "BSD 3-Clause",
|
|
27
|
+
"main": "./dist/index.js",
|
|
28
|
+
"exports": {
|
|
29
|
+
".": {
|
|
30
|
+
"types": "./dist/index.d.ts",
|
|
31
|
+
"import": "./dist/index.js"
|
|
32
|
+
}
|
|
33
|
+
},
|
|
34
|
+
"sideEffects": false,
|
|
35
|
+
"name": "@cleverbrush/server-openapi",
|
|
36
|
+
"readme": "https://github.com/cleverbrush/framework/tree/master/libs/server-openapi#readme",
|
|
37
|
+
"repository": {
|
|
38
|
+
"type": "git",
|
|
39
|
+
"url": "github:cleverbrush/framework"
|
|
40
|
+
},
|
|
41
|
+
"scripts": {
|
|
42
|
+
"watch": "tsc --build tsconfig.build.json --watch",
|
|
43
|
+
"build": "tsup && tsc --project tsconfig.build.json --emitDeclarationOnly",
|
|
44
|
+
"clean": "rm -rf dist tsconfig.build.tsbuildinfo"
|
|
45
|
+
},
|
|
46
|
+
"type": "module",
|
|
47
|
+
"types": "./dist/index.d.ts",
|
|
48
|
+
"version": "0.0.0-beta-20260413195755"
|
|
49
|
+
}
|