@forinda/kickjs-swagger 7.1.1 → 7.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.mts +21 -0
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +2 -2
- package/dist/index.mjs.map +1 -1
- package/package.json +2 -2
package/dist/index.d.mts
CHANGED
|
@@ -221,6 +221,27 @@ interface SwaggerOptions {
|
|
|
221
221
|
* })
|
|
222
222
|
* ```
|
|
223
223
|
*/
|
|
224
|
+
/**
|
|
225
|
+
* Name of the route flag that marks an endpoint public — the spec then reads
|
|
226
|
+
* the same declaration the runtime does, instead of asking for a second
|
|
227
|
+
* annotation that can drift from it.
|
|
228
|
+
*
|
|
229
|
+
* The name is configuration rather than a constant because the framework
|
|
230
|
+
* deliberately names no flags: one project's `auth.public` is another's
|
|
231
|
+
* `public` or `security.none`. Pass a list to accept several.
|
|
232
|
+
*
|
|
233
|
+
* ```ts
|
|
234
|
+
* // src/flags.ts
|
|
235
|
+
* export const Public = defineRouteFlag('auth.public')
|
|
236
|
+
*
|
|
237
|
+
* SwaggerAdapter({ bearerAuth: true, publicFlag: 'auth.public' })
|
|
238
|
+
* ```
|
|
239
|
+
*
|
|
240
|
+
* Checked after {@link ApiPublic} and {@link securityResolver}, before the
|
|
241
|
+
* `@ApiSecurity` / `@ApiBearerAuth` decorators — so an explicit resolver
|
|
242
|
+
* still wins, and a flag still overrides class-level security.
|
|
243
|
+
*/
|
|
244
|
+
publicFlag?: string | readonly string[];
|
|
224
245
|
securityResolver?: (ctx: SecurityResolverContext) => string | ApiSecurityRequirement | (string | ApiSecurityRequirement)[] | null | undefined;
|
|
225
246
|
/**
|
|
226
247
|
* Pluggable schema parser for converting validation schemas to JSON Schema.
|
package/dist/index.d.mts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.mts","names":[],"sources":["../src/schema-parser.ts","../src/decorators.ts","../src/openapi-builder.ts","../src/swagger.adapter.ts","../src/ui.ts"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;UAwBiB;;WAEN;;;;;EAMT,SAAS;;;;;;EAOT,aAAa,kBAAkB;;;;;;cAOpB,iBAAiB;;;;;;;;;;UCLb;EACf;EACA;;UAGe;EACf;EACA;EACA;EACA;;UAGe;EACf;EACA;EACA;;EAEA;;;iBAIc,aAAa,SAAS,sBAAsB;;iBAO5C,YAAY,SAAS,qBAAqB;;iBAY1C,WAAW,iBAAiB,iBAAiB;;iBAW7C,cAAc,gBAAsB,iBAAiB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBA0CrD,YACd,sBAAsB,mCAAmC,4BACxD,iBAAiB;;;;;;;;;;iBA2BJ,aAAa;;iBAOb,cAAc,iBAAiB;;;
|
|
1
|
+
{"version":3,"file":"index.d.mts","names":[],"sources":["../src/schema-parser.ts","../src/decorators.ts","../src/openapi-builder.ts","../src/swagger.adapter.ts","../src/ui.ts"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;UAwBiB;;WAEN;;;;;EAMT,SAAS;;;;;;EAOT,aAAa,kBAAkB;;;;;;cAOpB,iBAAiB;;;;;;;;;;UCLb;EACf;EACA;;UAGe;EACf;EACA;EACA;EACA;;UAGe;EACf;EACA;EACA;;EAEA;;;iBAIc,aAAa,SAAS,sBAAsB;;iBAO5C,YAAY,SAAS,qBAAqB;;iBAY1C,WAAW,iBAAiB,iBAAiB;;iBAW7C,cAAc,gBAAsB,iBAAiB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBA0CrD,YACd,sBAAsB,mCAAmC,4BACxD,iBAAiB;;;;;;;;;;iBA2BJ,aAAa;;iBAOb,cAAc,iBAAiB;;;UCjI9B;EACf;EACA;EACA;;;;;;;;;;;KAYU,wBAAwB;;;;;;;;UASnB;EACf;EACA;;UAGe;EACf,OAAO,QAAQ;EACf;IAAY;IAAa;;;;;;;EAMzB;;;;;;;;;;;;;;;;;;;;;;;;;;;;EA4BA,kBAAkB,eAAe;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAqDjC;EACA,oBACE,KAAK,qCACO,mCAAmC;;;;;;;;;;;;;;EAcjD,eAAe;;;;;;;;;;;iBA6FD,0BACd,sBACA,mBACA;;;;;;iBAWc,sBAAsB;;;;;;;;;;;;iBAsBtB,iBAAiB,UAAS;;;;;;;;;;;;;KChR9B,cAAc,iBAAiB,gBAAgB;UAE1C,8BAA8B;;EAE7C;;EAEA;;EAEA;;EAEA;;;;;;EAMA;;;;;;;;;;;;;;EAcA,kBAAkB;;;;;EAKlB,cAAc;;;;;;;;;;;;;;;;;;;;;;;;;cA0BH,0CAAc,eAAA;;;;;;;;;;;;;;iBCzEX,cAAc,iBAAiB,gBAAoB;;;;;;;;iBA+DnD,UAAU,iBAAiB"}
|
package/dist/index.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @forinda/kickjs-swagger v7.
|
|
2
|
+
* @forinda/kickjs-swagger v7.2.0
|
|
3
3
|
*
|
|
4
4
|
* Copyright (c) Felix Orinda
|
|
5
5
|
*
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
*
|
|
9
9
|
* @license MIT
|
|
10
10
|
*/
|
|
11
|
-
import{createRequire}from"node:module";import{detectSchema}from"@forinda/kickjs-schema";import{Logger,METADATA,defineAdapter,getClassMeta,getClassMetaOrUndefined,getMethodMeta,getMethodMetaOrUndefined,hasClassMeta,joinPaths,pushMethodMeta,setClassMeta,setMethodMeta}from"@forinda/kickjs";import{dirname}from"node:path";const zodSchemaParser={name:`kickjs-schema`,supports(schema){if(schema==null)return!1;try{return detectSchema(schema),!0}catch{return!1}},toJsonSchema(schema){return detectSchema(schema).toJsonSchema({target:`openapi-3.0`})}},SWAGGER_KEYS={OPERATION:`kick:swagger:operation`,RESPONSES:`kick:swagger:responses`,TAGS:`kick:swagger:tags`,BEARER_AUTH:`kick:swagger:bearer`,SECURITY:`kick:swagger:security`,PUBLIC:`kick:swagger:public`,EXCLUDE:`kick:swagger:exclude`};function ApiOperation(options){return(target,propertyKey)=>{setMethodMeta(SWAGGER_KEYS.OPERATION,options,target.constructor,propertyKey)}}function ApiResponse(options){return(target,propertyKey)=>{pushMethodMeta(SWAGGER_KEYS.RESPONSES,target.constructor,propertyKey,options)}}function ApiTags(...tags){return(target,propertyKey)=>{propertyKey?setMethodMeta(SWAGGER_KEYS.TAGS,tags,target.constructor,propertyKey):setClassMeta(SWAGGER_KEYS.TAGS,tags,target)}}function ApiBearerAuth(name=`BearerAuth`){return(target,propertyKey)=>{propertyKey?setMethodMeta(SWAGGER_KEYS.BEARER_AUTH,name,target.constructor,propertyKey):setClassMeta(SWAGGER_KEYS.BEARER_AUTH,name,target)}}function ApiSecurity(requirement){let requirements=(Array.isArray(requirement)?requirement:[requirement]).map(r=>typeof r==`string`?{name:r,scopes:[]}:{name:r.name,scopes:r.scopes??[]});return(target,propertyKey)=>{propertyKey?setMethodMeta(SWAGGER_KEYS.SECURITY,requirements,target.constructor,propertyKey):setClassMeta(SWAGGER_KEYS.SECURITY,requirements,target)}}function ApiPublic(){return(target,propertyKey)=>{setMethodMeta(SWAGGER_KEYS.PUBLIC,!0,target.constructor,propertyKey)}}function ApiExclude(){return(target,propertyKey)=>{propertyKey?setMethodMeta(SWAGGER_KEYS.EXCLUDE,!0,target.constructor,propertyKey):setClassMeta(SWAGGER_KEYS.EXCLUDE,!0,target)}}const log$1=Logger.for(`SwaggerSpec`),BODY_METHODS=new Set([`post`,`put`,`patch`]),warnedBodyOnReadMethod=new Set,EXPRESS_PARAM_RE=/:([A-Za-z_][A-Za-z0-9_]*)/g;function normaliseSecurity(raw){return(Array.isArray(raw)?raw:[raw]).map(entry=>typeof entry==`string`?{name:entry,scopes:[]}:{name:entry.name,scopes:entry.scopes??[]})}const DEFAULT_SCOPE=Symbol(`kick:swagger:default-scope`),routesByScope=new Map;routesByScope.set(DEFAULT_SCOPE,[]);function getScopeBag(scope){let key=scope??DEFAULT_SCOPE,bag=routesByScope.get(key);return bag||(bag=[],routesByScope.set(key,bag)),bag}const specCache=new WeakMap,cacheKeys=new Set;function invalidateSpecCache(scope){if(scope&&typeof scope==`object`){cacheKeys.has(scope)&&(specCache.delete(scope),cacheKeys.delete(scope));return}for(let key of cacheKeys)specCache.delete(key);cacheKeys.clear()}function registerControllerForDocs(controllerClass,mountPath,scope){getScopeBag(scope).push({controllerClass,mountPath}),invalidateSpecCache(scope)}function clearRegisteredRoutes(scope){if(scope&&typeof scope==`object`){routesByScope.delete(scope),invalidateSpecCache(scope);return}routesByScope.clear(),routesByScope.set(DEFAULT_SCOPE,[]),invalidateSpecCache()}function buildOpenAPISpec(options={}){let cacheKey=options,cached=specCache.get(cacheKey);if(cached!==void 0)return cached;let built=buildOpenAPISpecUncached(options);return specCache.set(cacheKey,built),cacheKeys.add(cacheKey),built}function buildOpenAPISpecUncached(options={}){let parser=options.schemaParser??zodSchemaParser,toJsonSchema=schema=>{try{return parser.supports(schema)?parser.toJsonSchema(schema):null}catch{return null}},componentSchemas={},schemaCounter=0,registerSchema=(jsonSchema,hint)=>{let baseName=jsonSchema.title||jsonSchema.label||hint||``;baseName||=`Schema${++schemaCounter}`,baseName=baseName.replace(/[^a-zA-Z0-9]/g,``);let clean={...jsonSchema};delete clean.title,delete clean.label,delete clean.$schema;let cleanJson=JSON.stringify(clean),name=baseName,suffix=2;for(;componentSchemas[name];){if(JSON.stringify(componentSchemas[name])===cleanJson)return{$ref:`#/components/schemas/${name}`};name=`${baseName}_${suffix++}`}return componentSchemas[name]=clean,{$ref:`#/components/schemas/${name}`}},spec={openapi:`3.0.3`,info:{title:options.info?.title||`API`,version:options.info?.version||`1.0.0`,...options.info?.description?{description:options.info.description}:{}},paths:{},components:{schemas:{},securitySchemes:{}},tags:[]};if(options.servers){let validServers=options.servers.filter(s=>{if(!s?.url||typeof s.url!=`string`)return!1;if(s.url.startsWith(`/`))return!0;try{return new URL(s.url),!0}catch{return!1}});validServers.length>0&&(spec.servers=validServers)}let allTags=new Set,securitySchemes={...options.securitySchemes},scopedRoutes=getScopeBag(options),defaultRoutes=options?getScopeBag(DEFAULT_SCOPE):[],routesToWalk=scopedRoutes.length>0?scopedRoutes:defaultRoutes;for(let{controllerClass,mountPath}of routesToWalk){if(hasClassMeta(SWAGGER_KEYS.EXCLUDE,controllerClass))continue;let routes=getClassMeta(METADATA.ROUTES,controllerClass,[]),classTags=getClassMeta(SWAGGER_KEYS.TAGS,controllerClass,[]),classAuth=getClassMetaOrUndefined(SWAGGER_KEYS.BEARER_AUTH,controllerClass),classSecurity=getClassMetaOrUndefined(SWAGGER_KEYS.SECURITY,controllerClass);for(let route of routes)try{emitRouteOperation(route)}catch(err){let openApiPath;try{openApiPath=joinPaths(mountPath,route.path).replace(EXPRESS_PARAM_RE,`{$1}`)}catch{openApiPath=`${mountPath}/__spec_error__`}let method=typeof route.method==`string`?route.method.toLowerCase():`get`;spec.paths[openApiPath]||(spec.paths[openApiPath]={}),spec.paths[openApiPath][method]={summary:`⚠ spec generation failed: ${err instanceof Error?err.message:String(err)}`,responses:{default:{description:`Spec generation failed for this operation.`}}}}function emitRouteOperation(route){if(getMethodMetaOrUndefined(SWAGGER_KEYS.EXCLUDE,controllerClass,route.handlerName))return;let fullPath=joinPaths(mountPath,route.path),openApiPath=fullPath.replace(EXPRESS_PARAM_RE,`{$1}`),method=route.method.toLowerCase(),operation=getMethodMeta(SWAGGER_KEYS.OPERATION,controllerClass,route.handlerName,{}),responses=getMethodMeta(SWAGGER_KEYS.RESPONSES,controllerClass,route.handlerName,[]),methodTags=getMethodMeta(SWAGGER_KEYS.TAGS,controllerClass,route.handlerName,[]),methodAuth=getMethodMetaOrUndefined(SWAGGER_KEYS.BEARER_AUTH,controllerClass,route.handlerName),tags=methodTags.length>0?methodTags:classTags;tags.forEach(t=>allTags.add(t));let op={...tags.length>0?{tags}:{},...operation.summary?{summary:operation.summary}:{},...operation.description?{description:operation.description}:{},...operation.operationId?{operationId:operation.operationId}:{},...operation.deprecated?{deprecated:!0}:{},responses:{}},parameters=[],paramMatches=fullPath.match(EXPRESS_PARAM_RE)||[];for(let match of paramMatches){let paramName=match.slice(1),schema={type:`string`};if(route.validation?.params){let jsonSchema=toJsonSchema(route.validation.params);if(jsonSchema?.properties&&typeof jsonSchema.properties==`object`){let props=jsonSchema.properties;props[paramName]&&(schema=props[paramName])}}parameters.push({name:paramName,in:`path`,required:!0,schema})}if(route.validation?.query){let jsonSchema=toJsonSchema(route.validation.query);if(jsonSchema?.properties&&typeof jsonSchema.properties==`object`){let required=Array.isArray(jsonSchema.required)?jsonSchema.required:[];for(let[name,propSchema]of Object.entries(jsonSchema.properties))parameters.push({name,in:`query`,required:required.includes(name),schema:propSchema})}}let queryParamsConfig=getMethodMetaOrUndefined(METADATA.QUERY_PARAMS,controllerClass,route.handlerName);if(queryParamsConfig&&(queryParamsConfig.filterable?.length&¶meters.push({name:`filter`,in:`query`,required:!1,description:`Filter fields: ${queryParamsConfig.filterable.join(`, `)}. Format: \`field:operator:value\`. Operators: eq, neq, gt, gte, lt, lte, contains, starts, ends, in, between`,schema:{type:`array`,items:{type:`string`}},style:`form`,explode:!0}),queryParamsConfig.sortable?.length&¶meters.push({name:`sort`,in:`query`,required:!1,description:`Sort fields: ${queryParamsConfig.sortable.join(`, `)}. Format: \`field:asc\` or \`field:desc\``,schema:{type:`array`,items:{type:`string`}},style:`form`,explode:!0}),queryParamsConfig.searchable?.length&¶meters.push({name:`q`,in:`query`,required:!1,description:`Search across: ${queryParamsConfig.searchable.join(`, `)}`,schema:{type:`string`}}),parameters.push({name:`page`,in:`query`,required:!1,description:`Page number (default: 1)`,schema:{type:`integer`,minimum:1,default:1}},{name:`limit`,in:`query`,required:!1,description:`Items per page (default: 20, max: 100)`,schema:{type:`integer`,minimum:1,maximum:100,default:20}})),parameters.length>0&&(op.parameters=parameters),route.validation?.body)if(BODY_METHODS.has(method)){let bodySchema=toJsonSchema(route.validation.body);if(bodySchema){let bodyName=route.validation.name||`${route.handlerName}Body`;op.requestBody={required:!0,content:{"application/json":{schema:registerSchema(bodySchema,bodyName)}}}}}else{let warnKey=`${controllerClass.name}.${route.handlerName}`;warnedBodyOnReadMethod.has(warnKey)||(warnedBodyOnReadMethod.add(warnKey),log$1.warn(`body validation on ${method.toUpperCase()} ${fullPath} (${warnKey}) is dropped from the OpenAPI spec — OpenAPI 3 does not allow a request body on ${method.toUpperCase()}. Move the schema to validation.query or change the route method.`))}let fileUpload=getMethodMetaOrUndefined(METADATA.FILE_UPLOAD,controllerClass,route.handlerName);if(fileUpload){let fieldName=fileUpload.fieldName??`file`,properties={};fileUpload.mode===`array`?properties[fieldName]={type:`array`,items:{type:`string`,format:`binary`}}:fileUpload.mode!==`none`&&(properties[fieldName]={type:`string`,format:`binary`}),op.requestBody={required:!0,content:{"multipart/form-data":{schema:{type:`object`,properties}}}}}if(responses.length>0)for(let resp of responses){let entry={description:resp.description||``};if(resp.schema&&typeof resp.schema==`object`){let converted=toJsonSchema(resp.schema),schemaName=resp.name||`${route.handlerName}Response${resp.status}`;entry.content={"application/json":{schema:converted?registerSchema(converted,schemaName):resp.schema}}}op.responses[String(resp.status)]=entry}else{let defaultStatus=method===`post`?`201`:method===`delete`?`204`:`200`,success={description:`Successful operation`};if(route.validation?.response&&defaultStatus!==`204`){let converted=toJsonSchema(route.validation.response);if(converted){let schemaName=`${route.validation.name||route.handlerName}Response`;success.content={"application/json":{schema:registerSchema(converted,schemaName)}}}}op.responses[defaultStatus]=success,route.validation?.body&&(op.responses[422]={description:`Validation error`})}let isPublicMethod=!!getMethodMetaOrUndefined(SWAGGER_KEYS.PUBLIC,controllerClass,route.handlerName),methodSecurity=getMethodMetaOrUndefined(SWAGGER_KEYS.SECURITY,controllerClass,route.handlerName),resolverOutput=isPublicMethod?void 0:options.securityResolver?.({controllerClass,handlerName:route.handlerName}),resolverSecurity=resolverOutput==null||resolverOutput===void 0?void 0:normaliseSecurity(resolverOutput),resolverPublic=resolverOutput===null,requirements,bearerAuthSourced=!1;if(isPublicMethod||resolverPublic?requirements=void 0:resolverSecurity&&resolverSecurity.length>0?requirements=resolverSecurity:methodSecurity&&methodSecurity.length>0?requirements=methodSecurity:methodAuth?(requirements=[{name:methodAuth,scopes:[]}],bearerAuthSourced=!0):classSecurity&&classSecurity.length>0?requirements=classSecurity:classAuth&&(requirements=[{name:classAuth,scopes:[]}],bearerAuthSourced=!0),requirements){op.security=requirements.map(r=>({[r.name]:r.scopes??[]}));for(let r of requirements)securitySchemes[r.name]||(bearerAuthSourced||r.name===`BearerAuth`)&&(securitySchemes[r.name]={type:`http`,scheme:`bearer`,bearerFormat:`JWT`})}spec.paths[openApiPath]||(spec.paths[openApiPath]={}),spec.paths[openApiPath][method]=op}}return spec.tags=Array.from(allTags).map(name=>({name})),spec.components.securitySchemes=securitySchemes,options.bearerAuth&&(securitySchemes.BearerAuth||(spec.components.securitySchemes.BearerAuth={type:`http`,scheme:`bearer`,bearerFormat:`JWT`}),spec.security=[{BearerAuth:[]}]),spec.components.schemas=componentSchemas,Object.keys(spec.components.schemas).length===0&&delete spec.components.schemas,Object.keys(spec.components.securitySchemes).length===0&&delete spec.components.securitySchemes,Object.keys(spec.components).length===0&&delete spec.components,spec}function escapeHtml(str){return str.replace(/&/g,`&`).replace(/</g,`<`).replace(/>/g,`>`).replace(/"/g,`"`).replace(/'/g,`'`)}function swaggerUIHtml(specUrl,title=`API Docs`,assetsPath){let safeTitle=escapeHtml(title),safeUrl=JSON.stringify(specUrl).replace(/</g,`\\u003c`);return`<!DOCTYPE html>
|
|
11
|
+
import{createRequire}from"node:module";import{detectSchema}from"@forinda/kickjs-schema";import{Logger,METADATA,defineAdapter,getClassMeta,getClassMetaOrUndefined,getMethodMeta,getMethodMetaOrUndefined,getRouteFlags,hasClassMeta,joinPaths,pushMethodMeta,setClassMeta,setMethodMeta}from"@forinda/kickjs";import{dirname}from"node:path";const zodSchemaParser={name:`kickjs-schema`,supports(schema){if(schema==null)return!1;try{return detectSchema(schema),!0}catch{return!1}},toJsonSchema(schema){return detectSchema(schema).toJsonSchema({target:`openapi-3.0`})}},SWAGGER_KEYS={OPERATION:`kick:swagger:operation`,RESPONSES:`kick:swagger:responses`,TAGS:`kick:swagger:tags`,BEARER_AUTH:`kick:swagger:bearer`,SECURITY:`kick:swagger:security`,PUBLIC:`kick:swagger:public`,EXCLUDE:`kick:swagger:exclude`};function ApiOperation(options){return(target,propertyKey)=>{setMethodMeta(SWAGGER_KEYS.OPERATION,options,target.constructor,propertyKey)}}function ApiResponse(options){return(target,propertyKey)=>{pushMethodMeta(SWAGGER_KEYS.RESPONSES,target.constructor,propertyKey,options)}}function ApiTags(...tags){return(target,propertyKey)=>{propertyKey?setMethodMeta(SWAGGER_KEYS.TAGS,tags,target.constructor,propertyKey):setClassMeta(SWAGGER_KEYS.TAGS,tags,target)}}function ApiBearerAuth(name=`BearerAuth`){return(target,propertyKey)=>{propertyKey?setMethodMeta(SWAGGER_KEYS.BEARER_AUTH,name,target.constructor,propertyKey):setClassMeta(SWAGGER_KEYS.BEARER_AUTH,name,target)}}function ApiSecurity(requirement){let requirements=(Array.isArray(requirement)?requirement:[requirement]).map(r=>typeof r==`string`?{name:r,scopes:[]}:{name:r.name,scopes:r.scopes??[]});return(target,propertyKey)=>{propertyKey?setMethodMeta(SWAGGER_KEYS.SECURITY,requirements,target.constructor,propertyKey):setClassMeta(SWAGGER_KEYS.SECURITY,requirements,target)}}function ApiPublic(){return(target,propertyKey)=>{setMethodMeta(SWAGGER_KEYS.PUBLIC,!0,target.constructor,propertyKey)}}function ApiExclude(){return(target,propertyKey)=>{propertyKey?setMethodMeta(SWAGGER_KEYS.EXCLUDE,!0,target.constructor,propertyKey):setClassMeta(SWAGGER_KEYS.EXCLUDE,!0,target)}}const log$1=Logger.for(`SwaggerSpec`),BODY_METHODS=new Set([`post`,`put`,`patch`]),warnedBodyOnReadMethod=new Set,EXPRESS_PARAM_RE=/:([A-Za-z_][A-Za-z0-9_]*)/g;function normaliseSecurity(raw){return(Array.isArray(raw)?raw:[raw]).map(entry=>typeof entry==`string`?{name:entry,scopes:[]}:{name:entry.name,scopes:entry.scopes??[]})}const DEFAULT_SCOPE=Symbol(`kick:swagger:default-scope`),routesByScope=new Map;routesByScope.set(DEFAULT_SCOPE,[]);function getScopeBag(scope){let key=scope??DEFAULT_SCOPE,bag=routesByScope.get(key);return bag||(bag=[],routesByScope.set(key,bag)),bag}const specCache=new WeakMap,cacheKeys=new Set;function invalidateSpecCache(scope){if(scope&&typeof scope==`object`){cacheKeys.has(scope)&&(specCache.delete(scope),cacheKeys.delete(scope));return}for(let key of cacheKeys)specCache.delete(key);cacheKeys.clear()}function registerControllerForDocs(controllerClass,mountPath,scope){getScopeBag(scope).push({controllerClass,mountPath}),invalidateSpecCache(scope)}function clearRegisteredRoutes(scope){if(scope&&typeof scope==`object`){routesByScope.delete(scope),invalidateSpecCache(scope);return}routesByScope.clear(),routesByScope.set(DEFAULT_SCOPE,[]),invalidateSpecCache()}function buildOpenAPISpec(options={}){let cacheKey=options,cached=specCache.get(cacheKey);if(cached!==void 0)return cached;let built=buildOpenAPISpecUncached(options);return specCache.set(cacheKey,built),cacheKeys.add(cacheKey),built}function buildOpenAPISpecUncached(options={}){let parser=options.schemaParser??zodSchemaParser,toJsonSchema=schema=>{try{return parser.supports(schema)?parser.toJsonSchema(schema):null}catch{return null}},componentSchemas={},schemaCounter=0,registerSchema=(jsonSchema,hint)=>{let baseName=jsonSchema.title||jsonSchema.label||hint||``;baseName||=`Schema${++schemaCounter}`,baseName=baseName.replace(/[^a-zA-Z0-9]/g,``);let clean={...jsonSchema};delete clean.title,delete clean.label,delete clean.$schema;let cleanJson=JSON.stringify(clean),name=baseName,suffix=2;for(;componentSchemas[name];){if(JSON.stringify(componentSchemas[name])===cleanJson)return{$ref:`#/components/schemas/${name}`};name=`${baseName}_${suffix++}`}return componentSchemas[name]=clean,{$ref:`#/components/schemas/${name}`}},spec={openapi:`3.0.3`,info:{title:options.info?.title||`API`,version:options.info?.version||`1.0.0`,...options.info?.description?{description:options.info.description}:{}},paths:{},components:{schemas:{},securitySchemes:{}},tags:[]};if(options.servers){let validServers=options.servers.filter(s=>{if(!s?.url||typeof s.url!=`string`)return!1;if(s.url.startsWith(`/`))return!0;try{return new URL(s.url),!0}catch{return!1}});validServers.length>0&&(spec.servers=validServers)}let allTags=new Set,securitySchemes={...options.securitySchemes},scopedRoutes=getScopeBag(options),defaultRoutes=options?getScopeBag(DEFAULT_SCOPE):[],routesToWalk=scopedRoutes.length>0?scopedRoutes:defaultRoutes;for(let{controllerClass,mountPath}of routesToWalk){if(hasClassMeta(SWAGGER_KEYS.EXCLUDE,controllerClass))continue;let routes=getClassMeta(METADATA.ROUTES,controllerClass,[]),classTags=getClassMeta(SWAGGER_KEYS.TAGS,controllerClass,[]),classAuth=getClassMetaOrUndefined(SWAGGER_KEYS.BEARER_AUTH,controllerClass),classSecurity=getClassMetaOrUndefined(SWAGGER_KEYS.SECURITY,controllerClass);for(let route of routes)try{emitRouteOperation(route)}catch(err){let openApiPath;try{openApiPath=joinPaths(mountPath,route.path).replace(EXPRESS_PARAM_RE,`{$1}`)}catch{openApiPath=`${mountPath}/__spec_error__`}let method=typeof route.method==`string`?route.method.toLowerCase():`get`;spec.paths[openApiPath]||(spec.paths[openApiPath]={}),spec.paths[openApiPath][method]={summary:`⚠ spec generation failed: ${err instanceof Error?err.message:String(err)}`,responses:{default:{description:`Spec generation failed for this operation.`}}}}function emitRouteOperation(route){if(getMethodMetaOrUndefined(SWAGGER_KEYS.EXCLUDE,controllerClass,route.handlerName))return;let fullPath=joinPaths(mountPath,route.path),openApiPath=fullPath.replace(EXPRESS_PARAM_RE,`{$1}`),method=route.method.toLowerCase(),operation=getMethodMeta(SWAGGER_KEYS.OPERATION,controllerClass,route.handlerName,{}),responses=getMethodMeta(SWAGGER_KEYS.RESPONSES,controllerClass,route.handlerName,[]),methodTags=getMethodMeta(SWAGGER_KEYS.TAGS,controllerClass,route.handlerName,[]),methodAuth=getMethodMetaOrUndefined(SWAGGER_KEYS.BEARER_AUTH,controllerClass,route.handlerName),tags=methodTags.length>0?methodTags:classTags;tags.forEach(t=>allTags.add(t));let op={...tags.length>0?{tags}:{},...operation.summary?{summary:operation.summary}:{},...operation.description?{description:operation.description}:{},...operation.operationId?{operationId:operation.operationId}:{},...operation.deprecated?{deprecated:!0}:{},responses:{}},parameters=[],paramMatches=fullPath.match(EXPRESS_PARAM_RE)||[];for(let match of paramMatches){let paramName=match.slice(1),schema={type:`string`};if(route.validation?.params){let jsonSchema=toJsonSchema(route.validation.params);if(jsonSchema?.properties&&typeof jsonSchema.properties==`object`){let props=jsonSchema.properties;props[paramName]&&(schema=props[paramName])}}parameters.push({name:paramName,in:`path`,required:!0,schema})}if(route.validation?.query){let jsonSchema=toJsonSchema(route.validation.query);if(jsonSchema?.properties&&typeof jsonSchema.properties==`object`){let required=Array.isArray(jsonSchema.required)?jsonSchema.required:[];for(let[name,propSchema]of Object.entries(jsonSchema.properties))parameters.push({name,in:`query`,required:required.includes(name),schema:propSchema})}}let queryParamsConfig=getMethodMetaOrUndefined(METADATA.QUERY_PARAMS,controllerClass,route.handlerName);if(queryParamsConfig&&(queryParamsConfig.filterable?.length&¶meters.push({name:`filter`,in:`query`,required:!1,description:`Filter fields: ${queryParamsConfig.filterable.join(`, `)}. Format: \`field:operator:value\`. Operators: eq, neq, gt, gte, lt, lte, contains, starts, ends, in, between`,schema:{type:`array`,items:{type:`string`}},style:`form`,explode:!0}),queryParamsConfig.sortable?.length&¶meters.push({name:`sort`,in:`query`,required:!1,description:`Sort fields: ${queryParamsConfig.sortable.join(`, `)}. Format: \`field:asc\` or \`field:desc\``,schema:{type:`array`,items:{type:`string`}},style:`form`,explode:!0}),queryParamsConfig.searchable?.length&¶meters.push({name:`q`,in:`query`,required:!1,description:`Search across: ${queryParamsConfig.searchable.join(`, `)}`,schema:{type:`string`}}),parameters.push({name:`page`,in:`query`,required:!1,description:`Page number (default: 1)`,schema:{type:`integer`,minimum:1,default:1}},{name:`limit`,in:`query`,required:!1,description:`Items per page (default: 20, max: 100)`,schema:{type:`integer`,minimum:1,maximum:100,default:20}})),parameters.length>0&&(op.parameters=parameters),route.validation?.body)if(BODY_METHODS.has(method)){let bodySchema=toJsonSchema(route.validation.body);if(bodySchema){let bodyName=route.validation.name||`${route.handlerName}Body`;op.requestBody={required:!0,content:{"application/json":{schema:registerSchema(bodySchema,bodyName)}}}}}else{let warnKey=`${controllerClass.name}.${route.handlerName}`;warnedBodyOnReadMethod.has(warnKey)||(warnedBodyOnReadMethod.add(warnKey),log$1.warn(`body validation on ${method.toUpperCase()} ${fullPath} (${warnKey}) is dropped from the OpenAPI spec — OpenAPI 3 does not allow a request body on ${method.toUpperCase()}. Move the schema to validation.query or change the route method.`))}let fileUpload=getMethodMetaOrUndefined(METADATA.FILE_UPLOAD,controllerClass,route.handlerName);if(fileUpload){let fieldName=fileUpload.fieldName??`file`,properties={};fileUpload.mode===`array`?properties[fieldName]={type:`array`,items:{type:`string`,format:`binary`}}:fileUpload.mode!==`none`&&(properties[fieldName]={type:`string`,format:`binary`}),op.requestBody={required:!0,content:{"multipart/form-data":{schema:{type:`object`,properties}}}}}if(responses.length>0)for(let resp of responses){let entry={description:resp.description||``};if(resp.schema&&typeof resp.schema==`object`){let converted=toJsonSchema(resp.schema),schemaName=resp.name||`${route.handlerName}Response${resp.status}`;entry.content={"application/json":{schema:converted?registerSchema(converted,schemaName):resp.schema}}}op.responses[String(resp.status)]=entry}else{let defaultStatus=method===`post`?`201`:method===`delete`?`204`:`200`,success={description:`Successful operation`};if(route.validation?.response&&defaultStatus!==`204`){let converted=toJsonSchema(route.validation.response);if(converted){let schemaName=`${route.validation.name||route.handlerName}Response`;success.content={"application/json":{schema:registerSchema(converted,schemaName)}}}}op.responses[defaultStatus]=success,route.validation?.body&&(op.responses[422]={description:`Validation error`})}let isPublicMethod=!!getMethodMetaOrUndefined(SWAGGER_KEYS.PUBLIC,controllerClass,route.handlerName),methodSecurity=getMethodMetaOrUndefined(SWAGGER_KEYS.SECURITY,controllerClass,route.handlerName),resolverOutput=isPublicMethod?void 0:options.securityResolver?.({controllerClass,handlerName:route.handlerName}),resolverSecurity=resolverOutput==null||resolverOutput===void 0?void 0:normaliseSecurity(resolverOutput),resolverPublic=resolverOutput===null,publicFlagNames=options.publicFlag===void 0?[]:typeof options.publicFlag==`string`?[options.publicFlag]:options.publicFlag,flagPublic=publicFlagNames.length>0&&(()=>{let flags=getRouteFlags(controllerClass,route.handlerName);return publicFlagNames.some(name=>flags.has(name))})(),requirements,bearerAuthSourced=!1;if(isPublicMethod||resolverPublic||flagPublic&&resolverSecurity===void 0?requirements=void 0:resolverSecurity&&resolverSecurity.length>0?requirements=resolverSecurity:methodSecurity&&methodSecurity.length>0?requirements=methodSecurity:methodAuth?(requirements=[{name:methodAuth,scopes:[]}],bearerAuthSourced=!0):classSecurity&&classSecurity.length>0?requirements=classSecurity:classAuth&&(requirements=[{name:classAuth,scopes:[]}],bearerAuthSourced=!0),requirements){op.security=requirements.map(r=>({[r.name]:r.scopes??[]}));for(let r of requirements)securitySchemes[r.name]||(bearerAuthSourced||r.name===`BearerAuth`)&&(securitySchemes[r.name]={type:`http`,scheme:`bearer`,bearerFormat:`JWT`})}spec.paths[openApiPath]||(spec.paths[openApiPath]={}),spec.paths[openApiPath][method]=op}}return spec.tags=Array.from(allTags).map(name=>({name})),spec.components.securitySchemes=securitySchemes,options.bearerAuth&&(securitySchemes.BearerAuth||(spec.components.securitySchemes.BearerAuth={type:`http`,scheme:`bearer`,bearerFormat:`JWT`}),spec.security=[{BearerAuth:[]}]),spec.components.schemas=componentSchemas,Object.keys(spec.components.schemas).length===0&&delete spec.components.schemas,Object.keys(spec.components.securitySchemes).length===0&&delete spec.components.securitySchemes,Object.keys(spec.components).length===0&&delete spec.components,spec}function escapeHtml(str){return str.replace(/&/g,`&`).replace(/</g,`<`).replace(/>/g,`>`).replace(/"/g,`"`).replace(/'/g,`'`)}function swaggerUIHtml(specUrl,title=`API Docs`,assetsPath){let safeTitle=escapeHtml(title),safeUrl=JSON.stringify(specUrl).replace(/</g,`\\u003c`);return`<!DOCTYPE html>
|
|
12
12
|
<html lang="en">
|
|
13
13
|
<head>
|
|
14
14
|
<meta charset="UTF-8">
|
package/dist/index.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.mjs","names":["log"],"sources":["../src/schema-parser.ts","../src/decorators.ts","../src/openapi-builder.ts","../src/ui.ts","../src/swagger.adapter.ts"],"sourcesContent":["import { detectSchema } from '@forinda/kickjs-schema'\n\n/**\n * Interface for converting validation library schemas to JSON Schema.\n *\n * @deprecated Use `@forinda/kickjs-schema` adapters instead. Schemas that\n * implement KickSchema or StandardSchemaV1 are auto-detected and converted\n * via `detectSchema().toJsonSchema()`. This interface is preserved for\n * backwards compatibility with custom parsers passed to SwaggerAdapter.\n *\n * @example\n * ```ts\n * import Joi from 'joi'\n * import joiToJson from 'joi-to-json'\n *\n * const joiParser: SchemaParser = {\n * name: 'joi',\n * supports: (schema) => Joi.isSchema(schema),\n * toJsonSchema: (schema) => joiToJson(schema),\n * }\n *\n * SwaggerAdapter({ schemaParser: joiParser })\n * ```\n */\nexport interface SchemaParser {\n /** Human-readable name for logging/debugging */\n readonly name: string\n\n /**\n * Return true if this parser can handle the given schema object.\n * Called before `toJsonSchema` to allow graceful fallback.\n */\n supports(schema: unknown): boolean\n\n /**\n * Convert a validation schema to a JSON Schema object.\n * Should return a plain object conforming to JSON Schema draft-07 or later.\n * Must not include the top-level `$schema` key — the builder adds it.\n */\n toJsonSchema(schema: unknown): Record<string, unknown>\n}\n\n/**\n * Default schema parser using @forinda/kickjs-schema auto-detection.\n * Supports Zod, Standard Schema v1, and any KickSchema adapter.\n */\nexport const zodSchemaParser: SchemaParser = {\n name: 'kickjs-schema',\n\n supports(schema: unknown): boolean {\n if (schema == null) return false\n try {\n detectSchema(schema)\n return true\n } catch {\n return false\n }\n },\n\n toJsonSchema(schema: unknown): Record<string, unknown> {\n const wrapped = detectSchema(schema)\n return wrapped.toJsonSchema({ target: 'openapi-3.0' })\n },\n}\n","import { setMethodMeta, setClassMeta, pushMethodMeta } from '@forinda/kickjs'\n\n/**\n * String metadata keys for the swagger decorators. Follows the §22\n * v4 'kick:area:thing' convention — survives JSON serialisation,\n * addressable by literal from cross-package consumers, visible in\n * DevTools snapshots.\n */\nconst SWAGGER_KEYS = {\n OPERATION: 'kick:swagger:operation',\n RESPONSES: 'kick:swagger:responses',\n TAGS: 'kick:swagger:tags',\n BEARER_AUTH: 'kick:swagger:bearer',\n /**\n * Generic security requirement(s) attached to a route. Replaces\n * the implicit `kick:auth:authenticated` cross-package bridge —\n * adopters now declare auth requirements explicitly via\n * `@ApiSecurity()` (single or multi-scheme, with optional OAuth\n * scopes) instead of having Swagger guess from a sibling\n * package's metadata.\n */\n SECURITY: 'kick:swagger:security',\n /**\n * Method-level opt-out from class-level security. Mirrors the\n * intent of `@Public` from auth packages but lives on Swagger's\n * own metadata namespace, so the spec builder doesn't need to\n * know about any specific auth library.\n */\n PUBLIC: 'kick:swagger:public',\n EXCLUDE: 'kick:swagger:exclude',\n} as const\n\nexport { SWAGGER_KEYS }\n\n/**\n * One entry in a route's OpenAPI security requirement list. Maps to\n * the `SecurityRequirementObject` in the OpenAPI 3 spec — `name`\n * references a scheme declared under `components.securitySchemes`,\n * and `scopes` is the optional OAuth2 / OpenID Connect scope list\n * (empty array for non-OAuth schemes).\n */\nexport interface ApiSecurityRequirement {\n name: string\n scopes?: string[]\n}\n\nexport interface ApiOperationOptions {\n summary?: string\n description?: string\n operationId?: string\n deprecated?: boolean\n}\n\nexport interface ApiResponseOptions {\n status: number\n description?: string\n schema?: any\n /** Schema name in components/schemas (e.g., 'UserResponse', 'ErrorBody'). Auto-generated from handler name if omitted. */\n name?: string\n}\n\n/** Attach operation metadata to a route handler */\nexport function ApiOperation(options: ApiOperationOptions): MethodDecorator {\n return (target, propertyKey) => {\n setMethodMeta(SWAGGER_KEYS.OPERATION, options, target.constructor, propertyKey as string)\n }\n}\n\n/** Document a response status. Can be stacked multiple times. */\nexport function ApiResponse(options: ApiResponseOptions): MethodDecorator {\n return (target, propertyKey) => {\n pushMethodMeta<ApiResponseOptions>(\n SWAGGER_KEYS.RESPONSES,\n target.constructor,\n propertyKey as string,\n options,\n )\n }\n}\n\n/** Apply OpenAPI tags at class or method level */\nexport function ApiTags(...tags: string[]): ClassDecorator & MethodDecorator {\n return (target: any, propertyKey?: string | symbol) => {\n if (propertyKey) {\n setMethodMeta(SWAGGER_KEYS.TAGS, tags, target.constructor, propertyKey as string)\n } else {\n setClassMeta(SWAGGER_KEYS.TAGS, tags, target)\n }\n }\n}\n\n/** Mark endpoint as requiring Bearer token auth */\nexport function ApiBearerAuth(name = 'BearerAuth'): ClassDecorator & MethodDecorator {\n return (target: any, propertyKey?: string | symbol) => {\n if (propertyKey) {\n setMethodMeta(SWAGGER_KEYS.BEARER_AUTH, name, target.constructor, propertyKey as string)\n } else {\n setClassMeta(SWAGGER_KEYS.BEARER_AUTH, name, target)\n }\n }\n}\n\n/**\n * Attach one or more OpenAPI security requirements to a class or\n * method. Generic alternative to {@link ApiBearerAuth} — pick this\n * when the scheme isn't bearer-shaped (API key, OAuth2 with scopes,\n * OpenID Connect) or when a route accepts multiple alternative\n * schemes (`SchemeA` OR `SchemeB`).\n *\n * Pass a string for the simple \"scheme by name, no scopes\" case;\n * pass an object `{ name, scopes }` to attach OAuth/OIDC scopes;\n * pass an array to declare multiple alternatives.\n *\n * The referenced scheme name **must** be declared under\n * `SwaggerOptions.securitySchemes` (or via the implicit BearerAuth\n * scheme generated when `bearerAuth: true` or `@ApiBearerAuth()`\n * is used) — Swagger doesn't synthesize schemes from `@ApiSecurity`\n * names alone.\n *\n * @example\n * ```ts\n * @Controller('/users')\n * @ApiSecurity('BearerAuth') // class-level default\n * class UsersController {\n * @Get('/me')\n * @ApiSecurity({ name: 'OAuth2', scopes: ['users:read'] }) // override\n * me() { ... }\n *\n * @Get('/health')\n * @ApiPublic() // opt out\n * health() { ... }\n * }\n * ```\n */\nexport function ApiSecurity(\n requirement: string | ApiSecurityRequirement | (string | ApiSecurityRequirement)[],\n): ClassDecorator & MethodDecorator {\n // Normalise everything to an `ApiSecurityRequirement[]` so the\n // builder reads a single shape. Strings become `{ name, scopes: [] }`.\n const requirements: ApiSecurityRequirement[] = (\n Array.isArray(requirement) ? requirement : [requirement]\n ).map((r) =>\n typeof r === 'string' ? { name: r, scopes: [] } : { name: r.name, scopes: r.scopes ?? [] },\n )\n\n return (target: any, propertyKey?: string | symbol) => {\n if (propertyKey) {\n setMethodMeta(SWAGGER_KEYS.SECURITY, requirements, target.constructor, propertyKey as string)\n } else {\n setClassMeta(SWAGGER_KEYS.SECURITY, requirements, target)\n }\n }\n}\n\n/**\n * Mark a method as publicly accessible — opts out of any\n * class-level security requirement (set via {@link ApiSecurity}\n * or {@link ApiBearerAuth}) for this one route.\n *\n * Use when the controller is mostly secured but exposes a\n * health-check / login / public-stats endpoint that shouldn't\n * carry the inherited security requirement in the OpenAPI spec.\n */\nexport function ApiPublic(): MethodDecorator {\n return (target, propertyKey) => {\n setMethodMeta(SWAGGER_KEYS.PUBLIC, true, target.constructor, propertyKey as string)\n }\n}\n\n/** Exclude a controller or method from the OpenAPI spec */\nexport function ApiExclude(): ClassDecorator & MethodDecorator {\n return (target: any, propertyKey?: string | symbol) => {\n if (propertyKey) {\n setMethodMeta(SWAGGER_KEYS.EXCLUDE, true, target.constructor, propertyKey as string)\n } else {\n setClassMeta(SWAGGER_KEYS.EXCLUDE, true, target)\n }\n }\n}\n","import {\n Logger,\n METADATA,\n joinPaths,\n type RouteDefinition,\n getClassMeta,\n getClassMetaOrUndefined,\n getMethodMeta,\n getMethodMetaOrUndefined,\n hasClassMeta,\n} from '@forinda/kickjs'\nimport {\n SWAGGER_KEYS,\n type ApiOperationOptions,\n type ApiResponseOptions,\n type ApiSecurityRequirement,\n} from './decorators'\nimport { zodSchemaParser, type SchemaParser } from './schema-parser'\n\nconst log = Logger.for('SwaggerSpec')\n\n/** HTTP methods that DO carry a request body in OpenAPI 3. */\nconst BODY_METHODS = new Set(['post', 'put', 'patch'])\n\n/**\n * One-time warning per (controller, handler) pair so a single\n * misconfigured route doesn't spam the boot log on every spec rebuild.\n */\nconst warnedBodyOnReadMethod = new Set<string>()\n\n/**\n * Express path-to-regexp param-name rule:\n * `[A-Za-z_][A-Za-z0-9_]*` (identifier-like; digits allowed after the\n * first char). Used in both directions — discovering params via\n * `match` and rewriting Express's `:name` to OpenAPI's `{name}` via\n * `replace`. Hyphens are NOT included because path-to-regexp uses\n * them as separators in patterns like `/:foo-:bar`.\n */\nconst EXPRESS_PARAM_RE = /:([A-Za-z_][A-Za-z0-9_]*)/g\n\nexport interface OpenAPIInfo {\n title: string\n version: string\n description?: string\n}\n\n/**\n * OpenAPI 3 SecuritySchemeObject — the shape adopters declare under\n * `SwaggerOptions.securitySchemes` and reference by name in\n * `@ApiSecurity('SchemeName')` / `@ApiBearerAuth('SchemeName')` /\n * `securityResolver()`. Loose `type: any` here matches the OpenAPI\n * union (`http` | `apiKey` | `oauth2` | `openIdConnect` | `mutualTLS`)\n * so adopters can declare any valid scheme without a re-export of\n * the full OpenAPI types.\n */\nexport type OpenAPISecurityScheme = Record<string, any>\n\n/**\n * Information passed to {@link SwaggerOptions.securityResolver} for\n * each route. The hook receives the raw controller class + method\n * name so adopters bridging their own auth layer (BYO decorators,\n * passport-style annotations, custom metadata) can read whatever\n * metadata they want via `Reflect.getMetadata`.\n */\nexport interface SecurityResolverContext {\n controllerClass: any\n handlerName: string\n}\n\nexport interface SwaggerOptions {\n info?: Partial<OpenAPIInfo>\n servers?: { url: string; description?: string }[]\n /**\n * Add the `BearerAuth` scheme to `components.securitySchemes` and\n * apply it as a global security requirement on the spec. Routes\n * that opt out via {@link ApiPublic} drop the global requirement.\n */\n bearerAuth?: boolean\n /**\n * Custom OpenAPI security schemes. Each entry is a\n * {@link OpenAPISecurityScheme} keyed by scheme name — the same\n * name `@ApiSecurity` / `@ApiBearerAuth` / `securityResolver`\n * reference. Schemes referenced by decorators but not declared\n * here are still emitted with a default `bearer` shape (back-compat\n * with the original `@ApiBearerAuth` flow).\n *\n * @example\n * ```ts\n * SwaggerAdapter({\n * securitySchemes: {\n * ApiKey: { type: 'apiKey', in: 'header', name: 'X-API-Key' },\n * OAuth2: {\n * type: 'oauth2',\n * flows: {\n * authorizationCode: {\n * authorizationUrl: 'https://example.com/oauth/authorize',\n * tokenUrl: 'https://example.com/oauth/token',\n * scopes: { 'users:read': 'Read user profile' },\n * },\n * },\n * },\n * },\n * })\n * ```\n */\n securitySchemes?: Record<string, OpenAPISecurityScheme>\n /**\n * Optional bridge for adopters who want their own auth library's\n * metadata to drive Swagger's security annotations without\n * reaching for `@ApiSecurity()` on every route.\n *\n * Returns one or more {@link ApiSecurityRequirement} entries (or\n * a bare scheme name string) when the route should be marked\n * secured; returns `null` to mark the route explicitly public\n * (overriding class-level security); returns `undefined` to fall\n * through to the decorator-driven path.\n *\n * The hook runs **after** {@link ApiPublic} (which short-circuits\n * to public) but **before** the decorator-driven `@ApiSecurity` /\n * `@ApiBearerAuth` lookups, so adopters who set both get the\n * resolver's verdict; this matches the historical behaviour of\n * the now-removed implicit `kick:auth:*` bridge.\n *\n * @example\n * ```ts\n * SwaggerAdapter({\n * securityResolver: ({ controllerClass, handlerName }) => {\n * // Bridge your own auth decorators' metadata without coupling.\n * const proto = controllerClass.prototype\n * if (Reflect.getMetadata('kick:auth:public', proto, handlerName)) return null\n * const secured =\n * Reflect.getMetadata('kick:auth:authenticated', controllerClass) ||\n * Reflect.getMetadata('kick:auth:authenticated', proto, handlerName)\n * return secured ? 'BearerAuth' : undefined\n * },\n * })\n * ```\n */\n securityResolver?: (\n ctx: SecurityResolverContext,\n ) => string | ApiSecurityRequirement | (string | ApiSecurityRequirement)[] | null | undefined\n /**\n * Pluggable schema parser for converting validation schemas to JSON Schema.\n * Defaults to `zodSchemaParser` which handles Zod v4+ schemas.\n *\n * Override this to use Yup, Joi, Valibot, ArkType, or any other library.\n *\n * @example\n * ```ts\n * SwaggerAdapter({\n * schemaParser: myYupParser,\n * })\n * ```\n */\n schemaParser?: SchemaParser\n}\n\n/**\n * Normalise any of the shapes accepted by `@ApiSecurity` /\n * `securityResolver` into a flat `ApiSecurityRequirement[]`.\n */\nfunction normaliseSecurity(\n raw: string | ApiSecurityRequirement | (string | ApiSecurityRequirement)[],\n): ApiSecurityRequirement[] {\n const arr = Array.isArray(raw) ? raw : [raw]\n return arr.map((entry) =>\n typeof entry === 'string'\n ? { name: entry, scopes: [] }\n : { name: entry.name, scopes: entry.scopes ?? [] },\n )\n}\n\ninterface RegisteredRoute {\n controllerClass: any\n mountPath: string\n}\n\n/**\n * Default route bag used when callers don't pass a config-scoped key.\n * Kept for back-compat with code that imports `registerControllerForDocs`\n * directly without going through SwaggerAdapter — those callers see the\n * legacy \"global single list\" behaviour.\n */\nconst DEFAULT_SCOPE = Symbol('kick:swagger:default-scope')\n\n/**\n * Per-adapter route storage. The adapter's `build` closure passes its\n * config object as the scope key so two SwaggerAdapter instances in\n * the same process (test harnesses, multi-tenant pre-fork) keep\n * independent route lists. Without this, two bootstraps in one process\n * cross-contaminate each other's specs.\n */\nconst routesByScope = new Map<object | symbol, RegisteredRoute[]>()\nroutesByScope.set(DEFAULT_SCOPE, [])\n\nfunction getScopeBag(scope: object | symbol | undefined): RegisteredRoute[] {\n const key = scope ?? DEFAULT_SCOPE\n let bag = routesByScope.get(key)\n if (!bag) {\n bag = []\n routesByScope.set(key, bag)\n }\n return bag\n}\n\n/**\n * Memoised spec — built lazily on the first {@link buildOpenAPISpec}\n * call after a registration change. Re-issued without rebuild on every\n * subsequent `/openapi.json` request until `clearRegisteredRoutes` or\n * `registerControllerForDocs` invalidates it.\n *\n * Keyed by reference equality on the options object so two adapters\n * with different `info.title` don't return each other's cached spec.\n * Application keeps the SwaggerAdapter config alive for the process\n * lifetime, so this is effectively a per-adapter memo cache. WeakMap\n * keeps the entries collectable when an adapter is disposed.\n *\n * `cacheKeys` is the iteration handle (WeakMap doesn't expose one) so\n * we can flush every cached spec on registration change without\n * tracking adapters individually.\n */\nconst specCache = new WeakMap<object, unknown>()\nconst cacheKeys = new Set<object>()\n\nfunction invalidateSpecCache(scope?: object | symbol): void {\n if (scope && typeof scope === 'object') {\n // Targeted invalidation — only the spec keyed on this config is stale.\n if (cacheKeys.has(scope)) {\n specCache.delete(scope)\n cacheKeys.delete(scope)\n }\n return\n }\n // Fallback: flush every cached spec (legacy untyped invalidation).\n for (const key of cacheKeys) specCache.delete(key)\n cacheKeys.clear()\n}\n\n/**\n * Register a controller for OpenAPI introspection. Called by Application\n * during route mounting via the adapter's onRouteMount hook.\n *\n * The optional `scope` argument keys the registration to a specific\n * adapter instance — pass the adapter's own config object as the key\n * (the SwaggerAdapter does this automatically). Omit for legacy\n * single-list behaviour, which is fine for single-bootstrap apps.\n */\nexport function registerControllerForDocs(\n controllerClass: any,\n mountPath: string,\n scope?: object,\n): void {\n getScopeBag(scope).push({ controllerClass, mountPath })\n invalidateSpecCache(scope)\n}\n\n/**\n * Clear registered routes — supports HMR rebuilds. Pass the adapter's\n * config object to clear only that adapter's routes; omit to clear\n * every scope (legacy/global behaviour).\n */\nexport function clearRegisteredRoutes(scope?: object): void {\n if (scope && typeof scope === 'object') {\n routesByScope.delete(scope)\n invalidateSpecCache(scope)\n return\n }\n routesByScope.clear()\n routesByScope.set(DEFAULT_SCOPE, [])\n invalidateSpecCache()\n}\n\n/**\n * Build a full OpenAPI 3.0.3 spec from registered controllers and\n * their decorators.\n *\n * Memoised — the first call for a given `options` object walks every\n * controller (~80–150ms for a 200-route app); subsequent calls return\n * the cached spec until {@link clearRegisteredRoutes} or\n * {@link registerControllerForDocs} invalidate. This matters because\n * Swagger UI re-fetches `/openapi.json` on every navigation; before\n * the cache, every fetch re-walked the entire controller graph.\n */\nexport function buildOpenAPISpec(options: SwaggerOptions = {}): any {\n const cacheKey = options as object\n const cached = specCache.get(cacheKey)\n if (cached !== undefined) return cached\n const built = buildOpenAPISpecUncached(options)\n specCache.set(cacheKey, built)\n cacheKeys.add(cacheKey)\n return built\n}\n\nfunction buildOpenAPISpecUncached(options: SwaggerOptions = {}): any {\n const parser = options.schemaParser ?? zodSchemaParser\n\n /** Convert a validation schema to JSON Schema using the configured parser */\n const toJsonSchema = (schema: unknown): Record<string, unknown> | null => {\n try {\n if (!parser.supports(schema)) return null\n return parser.toJsonSchema(schema)\n } catch {\n return null\n }\n }\n\n const componentSchemas: Record<string, any> = {}\n let schemaCounter = 0\n\n /**\n * Register a schema in components.schemas and return a $ref pointer.\n * If the schema has a title/label, use that as the name. Otherwise generate one.\n */\n const registerSchema = (jsonSchema: Record<string, unknown>, hint?: string): any => {\n // Try to extract a name from the schema\n let baseName = (jsonSchema.title as string) || (jsonSchema.label as string) || hint || ''\n if (!baseName) {\n baseName = `Schema${++schemaCounter}`\n }\n // Sanitize name for OpenAPI (remove spaces, special chars)\n baseName = baseName.replace(/[^a-zA-Z0-9]/g, '')\n\n const clean = { ...jsonSchema }\n delete clean.title\n delete clean.label\n delete clean.$schema\n const cleanJson = JSON.stringify(clean)\n\n // Resolve name collisions: if `baseName` already maps to a different\n // schema body, suffix with `_2`, `_3`, etc. until a free slot or a\n // structural duplicate is found. Two semantically-identical schemas\n // (`CreateUserDTO` registered twice) collapse to one entry by\n // JSON-equality, preserving the existing dedupe behaviour for the\n // common case while preventing the silent overwrite that produced\n // wrong-shape docs when two distinct DTOs hit the same hint.\n let name = baseName\n let suffix = 2\n while (componentSchemas[name]) {\n if (JSON.stringify(componentSchemas[name]) === cleanJson) {\n // Same schema body — reuse the existing slot.\n return { $ref: `#/components/schemas/${name}` }\n }\n name = `${baseName}_${suffix++}`\n }\n componentSchemas[name] = clean\n return { $ref: `#/components/schemas/${name}` }\n }\n\n const spec: any = {\n openapi: '3.0.3',\n info: {\n title: options.info?.title || 'API',\n version: options.info?.version || '1.0.0',\n ...(options.info?.description ? { description: options.info.description } : {}),\n },\n paths: {},\n components: { schemas: {}, securitySchemes: {} },\n tags: [],\n }\n\n if (options.servers) {\n // Drop entries whose URL can't be parsed by the browser's URL\n // constructor. Swagger UI runs `new URL(server.url)` on the client\n // and crashes with `Failed to construct 'URL': Invalid URL` if any\n // entry is malformed — which can happen on Windows dev when an\n // adapter hook populates servers with a path that was never meant\n // to be a URL. Relative URLs (e.g. '/') are allowed through.\n const validServers = options.servers.filter((s) => {\n if (!s?.url || typeof s.url !== 'string') return false\n if (s.url.startsWith('/')) return true\n try {\n void new URL(s.url)\n return true\n } catch {\n return false\n }\n })\n if (validServers.length > 0) {\n spec.servers = validServers\n }\n }\n\n const allTags = new Set<string>()\n // Pre-seed `securitySchemes` with adopter-declared schemes from\n // `options.securitySchemes` so `@ApiSecurity('OAuth2')` references\n // resolve without per-decorator scheme synthesis. The pre-seeded\n // entries take precedence over the implicit `BearerAuth` fallback\n // emitted in the route loop, so adopters who redefine `BearerAuth`\n // (e.g. with custom flows) get their version.\n const securitySchemes: Record<string, any> = { ...options.securitySchemes }\n\n // Routes scoped to this adapter's config (when adapter passed itself\n // as the scope) plus the legacy default-scope bag (for direct\n // registerControllerForDocs callers without a scope arg).\n const scopedRoutes = getScopeBag(options as object)\n const defaultRoutes = options ? getScopeBag(DEFAULT_SCOPE) : []\n const routesToWalk =\n scopedRoutes.length > 0\n ? scopedRoutes\n : defaultRoutes /* fall back to legacy single-list when adapter didn't scope */\n\n for (const { controllerClass, mountPath } of routesToWalk) {\n // Skip excluded controllers\n if (hasClassMeta(SWAGGER_KEYS.EXCLUDE, controllerClass)) continue\n\n const routes: RouteDefinition[] = getClassMeta<RouteDefinition[]>(\n METADATA.ROUTES,\n controllerClass,\n [],\n )\n const classTags: string[] = getClassMeta<string[]>(SWAGGER_KEYS.TAGS, controllerClass, [])\n const classAuth: string | undefined = getClassMetaOrUndefined<string>(\n SWAGGER_KEYS.BEARER_AUTH,\n controllerClass,\n )\n const classSecurity = getClassMetaOrUndefined<ApiSecurityRequirement[]>(\n SWAGGER_KEYS.SECURITY,\n controllerClass,\n )\n for (const route of routes) {\n try {\n emitRouteOperation(route)\n } catch (err) {\n // One bad operation must not blank the whole docs page. Emit a\n // marker summary so the broken op shows up in Swagger UI with\n // a visible warning, and the rest of the spec stays valid.\n // Defensive resolution — the same fields that crashed inside\n // emit may still be undefined here.\n let openApiPath: string\n try {\n openApiPath = joinPaths(mountPath, route.path).replace(EXPRESS_PARAM_RE, '{$1}')\n } catch {\n openApiPath = `${mountPath}/__spec_error__`\n }\n const method = typeof route.method === 'string' ? route.method.toLowerCase() : 'get'\n if (!spec.paths[openApiPath]) spec.paths[openApiPath] = {}\n spec.paths[openApiPath][method] = {\n summary: `⚠ spec generation failed: ${err instanceof Error ? err.message : String(err)}`,\n responses: { default: { description: 'Spec generation failed for this operation.' } },\n }\n }\n }\n\n // Per-route emit hoisted to a closure so the try/catch above can\n // wrap each route in isolation. Closes over loop-locals (operation,\n // routes, classTags, classAuth, etc.) so the body reads the same\n // way it did before the wrap.\n function emitRouteOperation(route: RouteDefinition): void {\n // Skip excluded methods\n if (getMethodMetaOrUndefined(SWAGGER_KEYS.EXCLUDE, controllerClass, route.handlerName)) return\n\n // Build the full path — mountPath is the actual Express mount prefix (from onRouteMount),\n // and route.path is the method-level path. @Controller path is not included here\n // because buildRoutes does not bake it into the router.\n const fullPath = joinPaths(mountPath, route.path)\n\n // Convert Express :param to OpenAPI {param}. Express's\n // path-to-regexp param-name rule is `[A-Za-z_][A-Za-z0-9_]*` —\n // identifier-like, digits allowed after the first char. The\n // previous regex (`[a-zA-Z_]+`) silently dropped digits, so\n // `:v2endpoint` became `:v` + literal `2endpoint` and the\n // generated docs missed the path-param entry entirely.\n const openApiPath = fullPath.replace(EXPRESS_PARAM_RE, '{$1}')\n const method = route.method.toLowerCase()\n\n // Gather metadata\n const operation: ApiOperationOptions = getMethodMeta<ApiOperationOptions>(\n SWAGGER_KEYS.OPERATION,\n controllerClass,\n route.handlerName,\n {} as ApiOperationOptions,\n )\n const responses: ApiResponseOptions[] = getMethodMeta<ApiResponseOptions[]>(\n SWAGGER_KEYS.RESPONSES,\n controllerClass,\n route.handlerName,\n [],\n )\n const methodTags: string[] = getMethodMeta<string[]>(\n SWAGGER_KEYS.TAGS,\n controllerClass,\n route.handlerName,\n [],\n )\n const methodAuth: string | undefined = getMethodMetaOrUndefined<string>(\n SWAGGER_KEYS.BEARER_AUTH,\n controllerClass,\n route.handlerName,\n )\n\n // Tags — method level overrides class level\n const tags = methodTags.length > 0 ? methodTags : classTags\n tags.forEach((t) => allTags.add(t))\n\n // Build operation object — `parameters` and `responses` are\n // attached below only when they have entries, so we don't emit\n // empty arrays/objects only to delete them later.\n const op: any = {\n ...(tags.length > 0 ? { tags } : {}),\n ...(operation.summary ? { summary: operation.summary } : {}),\n ...(operation.description ? { description: operation.description } : {}),\n ...(operation.operationId ? { operationId: operation.operationId } : {}),\n ...(operation.deprecated ? { deprecated: true } : {}),\n responses: {},\n }\n const parameters: any[] = []\n\n // Path parameters\n const paramMatches = fullPath.match(EXPRESS_PARAM_RE) || []\n for (const match of paramMatches) {\n const paramName = match.slice(1)\n let schema: any = { type: 'string' }\n\n // Try to get type from params validation schema\n if (route.validation?.params) {\n const jsonSchema = toJsonSchema(route.validation.params)\n if (jsonSchema?.properties && typeof jsonSchema.properties === 'object') {\n const props = jsonSchema.properties as Record<string, any>\n if (props[paramName]) {\n schema = props[paramName]\n }\n }\n }\n\n parameters.push({ name: paramName, in: 'path', required: true, schema })\n }\n\n // Query parameters\n if (route.validation?.query) {\n const jsonSchema = toJsonSchema(route.validation.query)\n if (jsonSchema?.properties && typeof jsonSchema.properties === 'object') {\n const required = Array.isArray(jsonSchema.required) ? jsonSchema.required : []\n for (const [name, propSchema] of Object.entries(\n jsonSchema.properties as Record<string, any>,\n )) {\n parameters.push({\n name,\n in: 'query',\n required: required.includes(name),\n schema: propSchema,\n })\n }\n }\n }\n\n // @ApiQueryParams decorator — document filterable/sortable/searchable fields\n const queryParamsConfig = getMethodMetaOrUndefined<any>(\n METADATA.QUERY_PARAMS,\n controllerClass,\n route.handlerName,\n )\n if (queryParamsConfig) {\n if (queryParamsConfig.filterable?.length) {\n parameters.push({\n name: 'filter',\n in: 'query',\n required: false,\n description: `Filter fields: ${queryParamsConfig.filterable.join(', ')}. Format: \\`field:operator:value\\`. Operators: eq, neq, gt, gte, lt, lte, contains, starts, ends, in, between`,\n schema: { type: 'array', items: { type: 'string' } },\n style: 'form',\n explode: true,\n })\n }\n if (queryParamsConfig.sortable?.length) {\n parameters.push({\n name: 'sort',\n in: 'query',\n required: false,\n description: `Sort fields: ${queryParamsConfig.sortable.join(', ')}. Format: \\`field:asc\\` or \\`field:desc\\``,\n schema: { type: 'array', items: { type: 'string' } },\n style: 'form',\n explode: true,\n })\n }\n if (queryParamsConfig.searchable?.length) {\n parameters.push({\n name: 'q',\n in: 'query',\n required: false,\n description: `Search across: ${queryParamsConfig.searchable.join(', ')}`,\n schema: { type: 'string' },\n })\n }\n parameters.push(\n {\n name: 'page',\n in: 'query',\n required: false,\n description: 'Page number (default: 1)',\n schema: { type: 'integer', minimum: 1, default: 1 },\n },\n {\n name: 'limit',\n in: 'query',\n required: false,\n description: 'Items per page (default: 20, max: 100)',\n schema: { type: 'integer', minimum: 1, maximum: 100, default: 20 },\n },\n )\n }\n\n if (parameters.length > 0) op.parameters = parameters\n\n // Request body\n if (route.validation?.body) {\n if (BODY_METHODS.has(method)) {\n const bodySchema = toJsonSchema(route.validation.body)\n if (bodySchema) {\n const bodyName = route.validation.name || `${route.handlerName}Body`\n const ref = registerSchema(bodySchema, bodyName)\n op.requestBody = {\n required: true,\n content: { 'application/json': { schema: ref } },\n }\n }\n } else {\n // Body validation on a method that OpenAPI 3 doesn't allow a\n // body for (GET / HEAD / DELETE / OPTIONS). Silently dropping\n // surprised adopters whose request schema vanished from docs;\n // warn once per route so they can switch to query validation\n // or rethink the route shape.\n const warnKey = `${controllerClass.name}.${route.handlerName}`\n if (!warnedBodyOnReadMethod.has(warnKey)) {\n warnedBodyOnReadMethod.add(warnKey)\n log.warn(\n `body validation on ${method.toUpperCase()} ${fullPath} (${warnKey}) is dropped from the OpenAPI spec — OpenAPI 3 does not allow a request body on ${method.toUpperCase()}. Move the schema to validation.query or change the route method.`,\n )\n }\n }\n }\n\n // File upload detection\n const fileUpload = getMethodMetaOrUndefined<any>(\n METADATA.FILE_UPLOAD,\n controllerClass,\n route.handlerName,\n )\n if (fileUpload) {\n const fieldName = fileUpload.fieldName ?? 'file'\n const properties: any = {}\n\n if (fileUpload.mode === 'array') {\n properties[fieldName] = {\n type: 'array',\n items: { type: 'string', format: 'binary' },\n }\n } else if (fileUpload.mode !== 'none') {\n properties[fieldName] = {\n type: 'string',\n format: 'binary',\n }\n }\n\n op.requestBody = {\n required: true,\n content: {\n 'multipart/form-data': {\n schema: { type: 'object', properties },\n },\n },\n }\n }\n\n // Responses\n if (responses.length > 0) {\n for (const resp of responses) {\n const entry: Record<string, unknown> = { description: resp.description || '' }\n if (resp.schema && typeof resp.schema === 'object') {\n // Try the validation parser first (Zod / Yup / etc.). If\n // that returns null the schema is plain JSON Schema and we\n // pass it through as-is — that's the escape hatch for\n // adopters who hand-write OpenAPI shapes without going\n // through the schema-parser layer.\n const converted = toJsonSchema(resp.schema)\n const schemaName = resp.name || `${route.handlerName}Response${resp.status}`\n const finalSchema = converted ? registerSchema(converted, schemaName) : resp.schema\n entry.content = { 'application/json': { schema: finalSchema } }\n }\n op.responses[String(resp.status)] = entry\n }\n } else {\n // Auto-generate default responses\n const defaultStatus = method === 'post' ? '201' : method === 'delete' ? '204' : '200'\n const success: Record<string, unknown> = { description: 'Successful operation' }\n // Declared response contract (`@Get('/', { response: schema })`) —\n // the same declaration `kick typegen` consumes, so docs and types\n // can't drift. Explicit @ApiResponse entries above still win.\n if (route.validation?.response && defaultStatus !== '204') {\n const converted = toJsonSchema(route.validation.response)\n if (converted) {\n const schemaName = `${route.validation.name || route.handlerName}Response`\n success.content = {\n 'application/json': { schema: registerSchema(converted, schemaName) },\n }\n }\n }\n op.responses[defaultStatus] = success\n\n if (route.validation?.body) {\n op.responses['422'] = { description: 'Validation error' }\n }\n }\n\n // Security resolution order (first match wins):\n // 1. @ApiPublic on the method — opt-out, no security emitted.\n // 2. options.securityResolver({controllerClass, handlerName})\n // — adopter-provided bridge for external auth libraries.\n // Returning `null` is \"explicitly public\" (same as\n // @ApiPublic); a value or array drives the requirements.\n // 3. @ApiSecurity / @ApiBearerAuth on the method.\n // 4. @ApiSecurity / @ApiBearerAuth on the class.\n const isPublicMethod = !!getMethodMetaOrUndefined<boolean>(\n SWAGGER_KEYS.PUBLIC,\n controllerClass,\n route.handlerName,\n )\n const methodSecurity = getMethodMetaOrUndefined<ApiSecurityRequirement[]>(\n SWAGGER_KEYS.SECURITY,\n controllerClass,\n route.handlerName,\n )\n const resolverOutput = !isPublicMethod\n ? options.securityResolver?.({ controllerClass, handlerName: route.handlerName })\n : undefined\n const resolverSecurity =\n resolverOutput == null || resolverOutput === undefined\n ? undefined\n : normaliseSecurity(resolverOutput)\n const resolverPublic = resolverOutput === null\n\n let requirements: ApiSecurityRequirement[] | undefined\n // Track whether the resolution path came from `@ApiBearerAuth`\n // (any name) so a bearer-shaped scheme gets auto-synthesised\n // for the named entry — preserves the original\n // `@ApiBearerAuth('CustomName')` ergonomics. `@ApiSecurity`\n // and the resolver hook DON'T auto-synth for arbitrary names\n // (only the literal `'BearerAuth'`) since their shapes are\n // generic — adopters must declare custom schemes via\n // `SwaggerOptions.securitySchemes` when using those paths.\n let bearerAuthSourced = false\n if (isPublicMethod || resolverPublic) {\n requirements = undefined\n } else if (resolverSecurity && resolverSecurity.length > 0) {\n requirements = resolverSecurity\n } else if (methodSecurity && methodSecurity.length > 0) {\n requirements = methodSecurity\n } else if (methodAuth) {\n requirements = [{ name: methodAuth, scopes: [] }]\n bearerAuthSourced = true\n } else if (classSecurity && classSecurity.length > 0) {\n requirements = classSecurity\n } else if (classAuth) {\n requirements = [{ name: classAuth, scopes: [] }]\n bearerAuthSourced = true\n }\n\n if (requirements) {\n op.security = requirements.map((r) => ({ [r.name]: r.scopes ?? [] }))\n for (const r of requirements) {\n if (!securitySchemes[r.name]) {\n // `@ApiBearerAuth('CustomName')` always emits a\n // bearer-shaped scheme under `CustomName`. The literal\n // `BearerAuth` name also auto-synths for back-compat\n // with `@ApiSecurity('BearerAuth')` and resolver hooks.\n if (bearerAuthSourced || r.name === 'BearerAuth') {\n securitySchemes[r.name] = {\n type: 'http',\n scheme: 'bearer',\n bearerFormat: 'JWT',\n }\n }\n }\n }\n }\n\n // Mount\n if (!spec.paths[openApiPath]) spec.paths[openApiPath] = {}\n spec.paths[openApiPath][method] = op\n }\n }\n\n // Finalize\n spec.tags = Array.from(allTags).map((name) => ({ name }))\n spec.components.securitySchemes = securitySchemes\n\n if (options.bearerAuth) {\n if (!securitySchemes.BearerAuth) {\n spec.components.securitySchemes.BearerAuth = {\n type: 'http',\n scheme: 'bearer',\n bearerFormat: 'JWT',\n }\n }\n spec.security = [{ BearerAuth: [] }]\n }\n\n // Merge collected schemas into components\n spec.components.schemas = componentSchemas\n\n // Clean up empty components\n if (Object.keys(spec.components.schemas).length === 0) delete spec.components.schemas\n if (Object.keys(spec.components.securitySchemes).length === 0)\n delete spec.components.securitySchemes\n if (Object.keys(spec.components).length === 0) delete spec.components\n\n return spec\n}\n","/** Escape a string for safe HTML attribute/content interpolation */\nfunction escapeHtml(str: string): string {\n return str\n .replace(/&/g, '&')\n .replace(/</g, '<')\n .replace(/>/g, '>')\n .replace(/\"/g, '"')\n .replace(/'/g, ''')\n}\n\n/**\n * Generate Swagger UI HTML using local assets from swagger-ui-dist.\n *\n * Assets are served from `/_swagger-assets/` by the adapter's Express\n * static middleware. Falls back to CDN if the local path is not provided.\n * This ensures Swagger UI works fully offline in development.\n *\n * @param specUrl - Path to the OpenAPI JSON spec (e.g., '/openapi.json')\n * @param title - Page title\n * @param assetsPath - Base path for local swagger-ui-dist assets (e.g., '/_swagger-assets')\n */\nexport function swaggerUIHtml(specUrl: string, title = 'API Docs', assetsPath?: string): string {\n const safeTitle = escapeHtml(title)\n // JSON-stringify for safe inlining into the `<script>` block. The inline\n // script below resolves this to an absolute URL against\n // `window.location.origin` before passing it to SwaggerUIBundle —\n // some swagger-ui-dist builds call `new URL(url)` without a base and\n // crash with `Failed to construct 'URL': Invalid URL` when the value\n // is a bare path like `/openapi.json`.\n const safeUrl = JSON.stringify(specUrl).replace(/</g, '\\\\u003c')\n\n // Use local assets if available, CDN as fallback\n const cssHref = assetsPath\n ? `${assetsPath}/swagger-ui.css`\n : 'https://unpkg.com/swagger-ui-dist@5/swagger-ui.css'\n const bundleSrc = assetsPath\n ? `${assetsPath}/swagger-ui-bundle.js`\n : 'https://unpkg.com/swagger-ui-dist@5/swagger-ui-bundle.js'\n const presetSrc = assetsPath\n ? `${assetsPath}/swagger-ui-standalone-preset.js`\n : 'https://unpkg.com/swagger-ui-dist@5/swagger-ui-standalone-preset.js'\n\n return `<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n <meta charset=\"UTF-8\">\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n <title>${safeTitle}</title>\n <link rel=\"stylesheet\" href=\"${cssHref}\">\n</head>\n<body>\n <div id=\"swagger-ui\"></div>\n <script src=\"${bundleSrc}\"></script>\n <script src=\"${presetSrc}\"></script>\n <script>\n (function () {\n var rawUrl = ${safeUrl};\n var specUrl;\n try {\n specUrl = new URL(rawUrl, window.location.origin).href;\n } catch (_e) {\n specUrl = rawUrl;\n }\n SwaggerUIBundle({\n url: specUrl,\n dom_id: '#swagger-ui',\n deepLinking: true,\n presets: [SwaggerUIBundle.presets.apis, SwaggerUIStandalonePreset],\n plugins: [SwaggerUIBundle.plugins.DownloadUrl],\n layout: 'StandaloneLayout',\n });\n })();\n </script>\n</body>\n</html>`\n}\n\n/**\n * Generate ReDoc HTML.\n *\n * ReDoc doesn't publish a standalone npm package suitable for local serving,\n * so it still loads from CDN. If offline support for ReDoc is needed,\n * vendor the standalone bundle into the package's public/ directory.\n */\nexport function redocHtml(specUrl: string, title = 'API Docs'): string {\n const safeTitle = escapeHtml(title)\n const safeUrl = escapeHtml(specUrl)\n\n return `<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n <meta charset=\"UTF-8\">\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n <title>${safeTitle}</title>\n</head>\n<body>\n <redoc spec-url=\"${safeUrl}\"></redoc>\n <script src=\"https://cdn.redoc.ly/redoc/latest/bundles/redoc.standalone.js\"></script>\n</body>\n</html>`\n}\n","import { dirname } from 'node:path'\nimport { createRequire } from 'node:module'\nimport type { Request, Response, NextFunction } from 'express'\nimport { Logger, defineAdapter } from '@forinda/kickjs'\nimport {\n buildOpenAPISpec,\n registerControllerForDocs,\n clearRegisteredRoutes,\n type SwaggerOptions,\n} from './openapi-builder'\nimport { swaggerUIHtml, redocHtml } from './ui'\n\nconst log = Logger.for('SwaggerAdapter')\n\n/**\n * Resolve the absolute path to swagger-ui-dist's static assets.\n * Uses createRequire to find it relative to this package (works with pnpm).\n */\nfunction getSwaggerUiDistPath(): string {\n const require = createRequire(import.meta.url)\n return dirname(require.resolve('swagger-ui-dist/package.json'))\n}\n\n/**\n * UI renderer signature — receives the spec URL and an optional title,\n * returns a complete HTML document. Both the built-in `swaggerUIHtml`\n * and `redocHtml` match this shape (the optional `assetsPath` arg\n * is opt-in for the offline-asset case and ignored by ReDoc).\n *\n * Adopters who want corporate branding, dark-mode default, custom\n * logos, or a third-party UI bundle (Stoplight Elements, RapiDoc,\n * Scalar) replace either renderer with their own.\n */\nexport type UIRenderer = (specUrl: string, title?: string, assetsPath?: string) => string\n\nexport interface SwaggerAdapterOptions extends SwaggerOptions {\n /** Path to serve Swagger UI (default: '/docs') */\n docsPath?: string\n /** Path to serve ReDoc (default: '/redoc') */\n redocPath?: string\n /** Path to serve the raw JSON spec (default: '/openapi.json') */\n specPath?: string\n /** Other adapters to discover (e.g., WsAdapter for WebSocket server URLs) */\n adapters?: any[]\n /**\n * When true, the adapter is a no-op while `NODE_ENV === 'production'` —\n * docs, spec, and assets are not mounted. Useful for keeping API docs\n * out of production builds without conditionally constructing the adapter.\n */\n disableInProd?: boolean\n /**\n * Override the Swagger UI HTML renderer. Defaults to the built-in\n * {@link swaggerUIHtml}. Useful for adopters who want corporate\n * branding, a custom theme, or to swap in a third-party UI bundle\n * (Stoplight Elements, RapiDoc, Scalar).\n *\n * @example\n * ```ts\n * SwaggerAdapter({\n * renderSwaggerUI: (specUrl, title) => myBrandedHtml(specUrl, title),\n * })\n * ```\n */\n renderSwaggerUI?: UIRenderer\n /**\n * Override the ReDoc HTML renderer. Defaults to the built-in\n * {@link redocHtml}. Same shape as {@link renderSwaggerUI}.\n */\n renderReDoc?: UIRenderer\n}\n\n/**\n * Swagger adapter — auto-generates OpenAPI spec from decorators and serves docs.\n *\n * Assets are served locally from `swagger-ui-dist` (npm dependency) —\n * no CDN required, works fully offline.\n *\n * @example\n * ```ts\n * bootstrap({\n * modules,\n * adapters: [\n * SwaggerAdapter({\n * info: { title: 'My API', version: '1.0.0' },\n * }),\n * ],\n * })\n * ```\n *\n * Endpoints:\n * GET /docs — Swagger UI (local assets, no CDN)\n * GET /redoc — ReDoc (CDN — no local package available)\n * GET /openapi.json — Raw OpenAPI 3.0.3 spec\n */\nexport const SwaggerAdapter = defineAdapter<SwaggerAdapterOptions>({\n name: 'SwaggerAdapter',\n defaults: {\n docsPath: '/docs',\n redocPath: '/redoc',\n specPath: '/openapi.json',\n },\n build: (config) => {\n // Resolved once at build time — config.disableInProd is set at\n // construction; NODE_ENV doesn't change at runtime. Checking on\n // every onRouteMount call (which fires per-controller) is noise.\n const disabled = Boolean(config.disableInProd) && process.env.NODE_ENV === 'production'\n const isDisabled = (): boolean => disabled\n\n // Snapshot the user-supplied servers list once per adapter instance\n // so subsequent afterStart runs (HMR reload, dev-mode restart loops,\n // multi-instance pre-fork in tests) re-derive the auto-detected\n // entries from a clean baseline instead of stacking duplicates onto\n // the previous run's accretion.\n const userSuppliedServers: ReadonlyArray<{ url: string; description?: string }> = config.servers\n ? [...config.servers]\n : []\n\n return {\n onRouteMount(controllerClass, mountPath) {\n if (isDisabled()) return\n // Pass `config` as the scope key so each SwaggerAdapter instance\n // owns its own route bag — two bootstraps in one process can't\n // cross-contaminate each other's specs.\n registerControllerForDocs(controllerClass, mountPath, config)\n },\n\n afterStart({ server }) {\n if (isDisabled()) return\n const addr = server?.address?.()\n if (!addr || typeof addr !== 'object') return\n\n const host =\n addr.address === '::' || addr.address === '0.0.0.0' ? 'localhost' : addr.address\n\n const autoDetected: { url: string; description?: string }[] = []\n // HTTP server URL is always auto-added — adopters who passed an\n // explicit HTTP URL keep their entry first because we restart\n // from the user snapshot above.\n autoDetected.push({ url: `http://${host}:${addr.port}`, description: 'HTTP server' })\n\n // Auto-add WebSocket server URLs from WsAdapter (one per namespace)\n const wsAdapter = config.adapters?.find(\n (a) => a.name === 'WsAdapter' && typeof a.getStats === 'function',\n )\n if (wsAdapter) {\n const stats = wsAdapter.getStats()\n for (const namespace of Object.keys(stats.namespaces || {})) {\n autoDetected.push({\n url: `ws://${host}:${addr.port}${namespace}`,\n description: `WebSocket: ${namespace}`,\n })\n }\n }\n\n // Always rebuild from the snapshot — replaces any leftover\n // auto-detected entries from a previous afterStart run.\n config.servers = [...userSuppliedServers, ...autoDetected]\n },\n\n beforeMount({ http }) {\n if (isDisabled()) {\n log.info('Swagger disabled in production (disableInProd=true)')\n return\n }\n // Clear previous registrations for THIS adapter (supports HMR\n // rebuild). Sibling adapters' route bags stay untouched.\n clearRegisteredRoutes(config)\n const docsPath = config.docsPath!\n const redocPath = config.redocPath!\n const specPath = config.specPath!\n let uiDistAvailable = false\n\n // ── Serve swagger-ui-dist static assets locally ──────────────────\n // This makes Swagger UI work offline — no CDN needed.\n // Assets served at /_swagger-assets/ (CSS, JS, fonts, etc.)\n const swaggerAssetsPath = '/_swagger-assets'\n try {\n const swaggerDistDir = getSwaggerUiDistPath()\n http.serveStatic(swaggerAssetsPath, swaggerDistDir)\n uiDistAvailable = true\n } catch {\n log.warn('swagger-ui-dist not found — Swagger UI will load from CDN (requires internet).')\n }\n\n // Tightened CSP: only whitelist the CDN entries we actually\n // need. The default Swagger UI renderer needs unpkg.com for\n // CDN fallback (when swagger-ui-dist isn't installed) AND for\n // the inline script. The default ReDoc renderer needs\n // cdn.redoc.ly for the standalone bundle. Custom renderers\n // (renderSwaggerUI / renderReDoc overrides) get only the\n // baseline policy — adopters set their own headers there.\n const customSwaggerRenderer = Boolean(config.renderSwaggerUI)\n const customReDocRenderer = Boolean(config.renderReDoc)\n const swaggerOrigins = uiDistAvailable || customSwaggerRenderer ? [] : ['https://unpkg.com']\n const redocOrigins = customReDocRenderer\n ? []\n : ['https://cdn.redoc.ly', 'https://cdn.jsdelivr.net']\n const scriptOrigins = [...swaggerOrigins, ...redocOrigins]\n const styleOrigins =\n uiDistAvailable || customSwaggerRenderer\n ? ['https://fonts.googleapis.com']\n : ['https://unpkg.com', 'https://fonts.googleapis.com']\n const imgOrigins = uiDistAvailable || customSwaggerRenderer ? [] : ['https://unpkg.com']\n\n http.use((_req: Request, res: Response, next: NextFunction) => {\n // Build connect-src dynamically so \"Try it out\" can call any configured server URL.\n // Includes dev-friendly localhost/127.0.0.1 origins so docs served from one host\n // can call an API spec'd at the other (a common cross-origin gotcha).\n const serverOrigins = new Set<string>()\n for (const s of config.servers ?? []) {\n try {\n serverOrigins.add(new URL(s.url).origin)\n } catch {\n // ignore relative or malformed URLs\n }\n }\n const connectSrc = [\n \"'self'\",\n 'http://localhost:*',\n 'http://127.0.0.1:*',\n 'https://localhost:*',\n 'https://127.0.0.1:*',\n 'ws://localhost:*',\n 'ws://127.0.0.1:*',\n ...serverOrigins,\n ].join(' ')\n\n // Inline script in swaggerUIHtml is required by SwaggerUIBundle's\n // bootstrapping pattern. We can't drop 'unsafe-inline' without\n // refactoring to a hashed/nonced inline script; until then, keep\n // 'unsafe-inline' but minimise CDN whitelist.\n res.setHeader(\n 'Content-Security-Policy',\n [\n \"default-src 'self'\",\n `script-src 'self' 'unsafe-inline'${scriptOrigins.length ? ' ' + scriptOrigins.join(' ') : ''}`,\n `style-src 'self' 'unsafe-inline'${styleOrigins.length ? ' ' + styleOrigins.join(' ') : ''}`,\n \"font-src 'self' https://fonts.gstatic.com\",\n `img-src 'self' data:${imgOrigins.length ? ' ' + imgOrigins.join(' ') : ''}`,\n `connect-src ${connectSrc}`,\n ].join('; '),\n )\n next()\n })\n\n // Spec endpoint (JSON)\n http.route('GET', specPath, (ctx) => {\n ctx.json(buildOpenAPISpec(config))\n })\n\n // Swagger UI — uses local assets if available, CDN fallback.\n // Adopters can override `renderSwaggerUI` to swap the bundle\n // (Stoplight Elements, RapiDoc, Scalar) or apply branding.\n const renderSwagger = config.renderSwaggerUI ?? swaggerUIHtml\n const renderReDoc = config.renderReDoc ?? redocHtml\n http.route('GET', docsPath, (ctx) => {\n ctx.html(\n renderSwagger(\n specPath,\n config.info?.title,\n uiDistAvailable ? swaggerAssetsPath : undefined,\n ),\n )\n })\n\n // ReDoc — still CDN-based for the default renderer (no npm\n // package for the standalone bundle). Custom renderers can\n // self-host whatever they like.\n http.route('GET', redocPath, (ctx) => {\n ctx.html(renderReDoc(specPath, config.info?.title))\n })\n\n log.info(`Swagger UI: ${docsPath}`)\n log.info(`ReDoc: ${redocPath}`)\n log.info(`OpenAPI spec: ${specPath}`)\n },\n }\n },\n})\n\n// Re-export for use by Application when mounting module routes\nexport { registerControllerForDocs, clearRegisteredRoutes }\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.mjs","names":["log"],"sources":["../src/schema-parser.ts","../src/decorators.ts","../src/openapi-builder.ts","../src/ui.ts","../src/swagger.adapter.ts"],"sourcesContent":["import { detectSchema } from '@forinda/kickjs-schema'\n\n/**\n * Interface for converting validation library schemas to JSON Schema.\n *\n * @deprecated Use `@forinda/kickjs-schema` adapters instead. Schemas that\n * implement KickSchema or StandardSchemaV1 are auto-detected and converted\n * via `detectSchema().toJsonSchema()`. This interface is preserved for\n * backwards compatibility with custom parsers passed to SwaggerAdapter.\n *\n * @example\n * ```ts\n * import Joi from 'joi'\n * import joiToJson from 'joi-to-json'\n *\n * const joiParser: SchemaParser = {\n * name: 'joi',\n * supports: (schema) => Joi.isSchema(schema),\n * toJsonSchema: (schema) => joiToJson(schema),\n * }\n *\n * SwaggerAdapter({ schemaParser: joiParser })\n * ```\n */\nexport interface SchemaParser {\n /** Human-readable name for logging/debugging */\n readonly name: string\n\n /**\n * Return true if this parser can handle the given schema object.\n * Called before `toJsonSchema` to allow graceful fallback.\n */\n supports(schema: unknown): boolean\n\n /**\n * Convert a validation schema to a JSON Schema object.\n * Should return a plain object conforming to JSON Schema draft-07 or later.\n * Must not include the top-level `$schema` key — the builder adds it.\n */\n toJsonSchema(schema: unknown): Record<string, unknown>\n}\n\n/**\n * Default schema parser using @forinda/kickjs-schema auto-detection.\n * Supports Zod, Standard Schema v1, and any KickSchema adapter.\n */\nexport const zodSchemaParser: SchemaParser = {\n name: 'kickjs-schema',\n\n supports(schema: unknown): boolean {\n if (schema == null) return false\n try {\n detectSchema(schema)\n return true\n } catch {\n return false\n }\n },\n\n toJsonSchema(schema: unknown): Record<string, unknown> {\n const wrapped = detectSchema(schema)\n return wrapped.toJsonSchema({ target: 'openapi-3.0' })\n },\n}\n","import { setMethodMeta, setClassMeta, pushMethodMeta } from '@forinda/kickjs'\n\n/**\n * String metadata keys for the swagger decorators. Follows the §22\n * v4 'kick:area:thing' convention — survives JSON serialisation,\n * addressable by literal from cross-package consumers, visible in\n * DevTools snapshots.\n */\nconst SWAGGER_KEYS = {\n OPERATION: 'kick:swagger:operation',\n RESPONSES: 'kick:swagger:responses',\n TAGS: 'kick:swagger:tags',\n BEARER_AUTH: 'kick:swagger:bearer',\n /**\n * Generic security requirement(s) attached to a route. Replaces\n * the implicit `kick:auth:authenticated` cross-package bridge —\n * adopters now declare auth requirements explicitly via\n * `@ApiSecurity()` (single or multi-scheme, with optional OAuth\n * scopes) instead of having Swagger guess from a sibling\n * package's metadata.\n */\n SECURITY: 'kick:swagger:security',\n /**\n * Method-level opt-out from class-level security. Mirrors the\n * intent of `@Public` from auth packages but lives on Swagger's\n * own metadata namespace, so the spec builder doesn't need to\n * know about any specific auth library.\n */\n PUBLIC: 'kick:swagger:public',\n EXCLUDE: 'kick:swagger:exclude',\n} as const\n\nexport { SWAGGER_KEYS }\n\n/**\n * One entry in a route's OpenAPI security requirement list. Maps to\n * the `SecurityRequirementObject` in the OpenAPI 3 spec — `name`\n * references a scheme declared under `components.securitySchemes`,\n * and `scopes` is the optional OAuth2 / OpenID Connect scope list\n * (empty array for non-OAuth schemes).\n */\nexport interface ApiSecurityRequirement {\n name: string\n scopes?: string[]\n}\n\nexport interface ApiOperationOptions {\n summary?: string\n description?: string\n operationId?: string\n deprecated?: boolean\n}\n\nexport interface ApiResponseOptions {\n status: number\n description?: string\n schema?: any\n /** Schema name in components/schemas (e.g., 'UserResponse', 'ErrorBody'). Auto-generated from handler name if omitted. */\n name?: string\n}\n\n/** Attach operation metadata to a route handler */\nexport function ApiOperation(options: ApiOperationOptions): MethodDecorator {\n return (target, propertyKey) => {\n setMethodMeta(SWAGGER_KEYS.OPERATION, options, target.constructor, propertyKey as string)\n }\n}\n\n/** Document a response status. Can be stacked multiple times. */\nexport function ApiResponse(options: ApiResponseOptions): MethodDecorator {\n return (target, propertyKey) => {\n pushMethodMeta<ApiResponseOptions>(\n SWAGGER_KEYS.RESPONSES,\n target.constructor,\n propertyKey as string,\n options,\n )\n }\n}\n\n/** Apply OpenAPI tags at class or method level */\nexport function ApiTags(...tags: string[]): ClassDecorator & MethodDecorator {\n return (target: any, propertyKey?: string | symbol) => {\n if (propertyKey) {\n setMethodMeta(SWAGGER_KEYS.TAGS, tags, target.constructor, propertyKey as string)\n } else {\n setClassMeta(SWAGGER_KEYS.TAGS, tags, target)\n }\n }\n}\n\n/** Mark endpoint as requiring Bearer token auth */\nexport function ApiBearerAuth(name = 'BearerAuth'): ClassDecorator & MethodDecorator {\n return (target: any, propertyKey?: string | symbol) => {\n if (propertyKey) {\n setMethodMeta(SWAGGER_KEYS.BEARER_AUTH, name, target.constructor, propertyKey as string)\n } else {\n setClassMeta(SWAGGER_KEYS.BEARER_AUTH, name, target)\n }\n }\n}\n\n/**\n * Attach one or more OpenAPI security requirements to a class or\n * method. Generic alternative to {@link ApiBearerAuth} — pick this\n * when the scheme isn't bearer-shaped (API key, OAuth2 with scopes,\n * OpenID Connect) or when a route accepts multiple alternative\n * schemes (`SchemeA` OR `SchemeB`).\n *\n * Pass a string for the simple \"scheme by name, no scopes\" case;\n * pass an object `{ name, scopes }` to attach OAuth/OIDC scopes;\n * pass an array to declare multiple alternatives.\n *\n * The referenced scheme name **must** be declared under\n * `SwaggerOptions.securitySchemes` (or via the implicit BearerAuth\n * scheme generated when `bearerAuth: true` or `@ApiBearerAuth()`\n * is used) — Swagger doesn't synthesize schemes from `@ApiSecurity`\n * names alone.\n *\n * @example\n * ```ts\n * @Controller('/users')\n * @ApiSecurity('BearerAuth') // class-level default\n * class UsersController {\n * @Get('/me')\n * @ApiSecurity({ name: 'OAuth2', scopes: ['users:read'] }) // override\n * me() { ... }\n *\n * @Get('/health')\n * @ApiPublic() // opt out\n * health() { ... }\n * }\n * ```\n */\nexport function ApiSecurity(\n requirement: string | ApiSecurityRequirement | (string | ApiSecurityRequirement)[],\n): ClassDecorator & MethodDecorator {\n // Normalise everything to an `ApiSecurityRequirement[]` so the\n // builder reads a single shape. Strings become `{ name, scopes: [] }`.\n const requirements: ApiSecurityRequirement[] = (\n Array.isArray(requirement) ? requirement : [requirement]\n ).map((r) =>\n typeof r === 'string' ? { name: r, scopes: [] } : { name: r.name, scopes: r.scopes ?? [] },\n )\n\n return (target: any, propertyKey?: string | symbol) => {\n if (propertyKey) {\n setMethodMeta(SWAGGER_KEYS.SECURITY, requirements, target.constructor, propertyKey as string)\n } else {\n setClassMeta(SWAGGER_KEYS.SECURITY, requirements, target)\n }\n }\n}\n\n/**\n * Mark a method as publicly accessible — opts out of any\n * class-level security requirement (set via {@link ApiSecurity}\n * or {@link ApiBearerAuth}) for this one route.\n *\n * Use when the controller is mostly secured but exposes a\n * health-check / login / public-stats endpoint that shouldn't\n * carry the inherited security requirement in the OpenAPI spec.\n */\nexport function ApiPublic(): MethodDecorator {\n return (target, propertyKey) => {\n setMethodMeta(SWAGGER_KEYS.PUBLIC, true, target.constructor, propertyKey as string)\n }\n}\n\n/** Exclude a controller or method from the OpenAPI spec */\nexport function ApiExclude(): ClassDecorator & MethodDecorator {\n return (target: any, propertyKey?: string | symbol) => {\n if (propertyKey) {\n setMethodMeta(SWAGGER_KEYS.EXCLUDE, true, target.constructor, propertyKey as string)\n } else {\n setClassMeta(SWAGGER_KEYS.EXCLUDE, true, target)\n }\n }\n}\n","import {\n Logger,\n METADATA,\n joinPaths,\n type RouteDefinition,\n getClassMeta,\n getClassMetaOrUndefined,\n getMethodMeta,\n getMethodMetaOrUndefined,\n hasClassMeta,\n getRouteFlags,\n} from '@forinda/kickjs'\nimport {\n SWAGGER_KEYS,\n type ApiOperationOptions,\n type ApiResponseOptions,\n type ApiSecurityRequirement,\n} from './decorators'\nimport { zodSchemaParser, type SchemaParser } from './schema-parser'\n\nconst log = Logger.for('SwaggerSpec')\n\n/** HTTP methods that DO carry a request body in OpenAPI 3. */\nconst BODY_METHODS = new Set(['post', 'put', 'patch'])\n\n/**\n * One-time warning per (controller, handler) pair so a single\n * misconfigured route doesn't spam the boot log on every spec rebuild.\n */\nconst warnedBodyOnReadMethod = new Set<string>()\n\n/**\n * Express path-to-regexp param-name rule:\n * `[A-Za-z_][A-Za-z0-9_]*` (identifier-like; digits allowed after the\n * first char). Used in both directions — discovering params via\n * `match` and rewriting Express's `:name` to OpenAPI's `{name}` via\n * `replace`. Hyphens are NOT included because path-to-regexp uses\n * them as separators in patterns like `/:foo-:bar`.\n */\nconst EXPRESS_PARAM_RE = /:([A-Za-z_][A-Za-z0-9_]*)/g\n\nexport interface OpenAPIInfo {\n title: string\n version: string\n description?: string\n}\n\n/**\n * OpenAPI 3 SecuritySchemeObject — the shape adopters declare under\n * `SwaggerOptions.securitySchemes` and reference by name in\n * `@ApiSecurity('SchemeName')` / `@ApiBearerAuth('SchemeName')` /\n * `securityResolver()`. Loose `type: any` here matches the OpenAPI\n * union (`http` | `apiKey` | `oauth2` | `openIdConnect` | `mutualTLS`)\n * so adopters can declare any valid scheme without a re-export of\n * the full OpenAPI types.\n */\nexport type OpenAPISecurityScheme = Record<string, any>\n\n/**\n * Information passed to {@link SwaggerOptions.securityResolver} for\n * each route. The hook receives the raw controller class + method\n * name so adopters bridging their own auth layer (BYO decorators,\n * passport-style annotations, custom metadata) can read whatever\n * metadata they want via `Reflect.getMetadata`.\n */\nexport interface SecurityResolverContext {\n controllerClass: any\n handlerName: string\n}\n\nexport interface SwaggerOptions {\n info?: Partial<OpenAPIInfo>\n servers?: { url: string; description?: string }[]\n /**\n * Add the `BearerAuth` scheme to `components.securitySchemes` and\n * apply it as a global security requirement on the spec. Routes\n * that opt out via {@link ApiPublic} drop the global requirement.\n */\n bearerAuth?: boolean\n /**\n * Custom OpenAPI security schemes. Each entry is a\n * {@link OpenAPISecurityScheme} keyed by scheme name — the same\n * name `@ApiSecurity` / `@ApiBearerAuth` / `securityResolver`\n * reference. Schemes referenced by decorators but not declared\n * here are still emitted with a default `bearer` shape (back-compat\n * with the original `@ApiBearerAuth` flow).\n *\n * @example\n * ```ts\n * SwaggerAdapter({\n * securitySchemes: {\n * ApiKey: { type: 'apiKey', in: 'header', name: 'X-API-Key' },\n * OAuth2: {\n * type: 'oauth2',\n * flows: {\n * authorizationCode: {\n * authorizationUrl: 'https://example.com/oauth/authorize',\n * tokenUrl: 'https://example.com/oauth/token',\n * scopes: { 'users:read': 'Read user profile' },\n * },\n * },\n * },\n * },\n * })\n * ```\n */\n securitySchemes?: Record<string, OpenAPISecurityScheme>\n /**\n * Optional bridge for adopters who want their own auth library's\n * metadata to drive Swagger's security annotations without\n * reaching for `@ApiSecurity()` on every route.\n *\n * Returns one or more {@link ApiSecurityRequirement} entries (or\n * a bare scheme name string) when the route should be marked\n * secured; returns `null` to mark the route explicitly public\n * (overriding class-level security); returns `undefined` to fall\n * through to the decorator-driven path.\n *\n * The hook runs **after** {@link ApiPublic} (which short-circuits\n * to public) but **before** the decorator-driven `@ApiSecurity` /\n * `@ApiBearerAuth` lookups, so adopters who set both get the\n * resolver's verdict; this matches the historical behaviour of\n * the now-removed implicit `kick:auth:*` bridge.\n *\n * @example\n * ```ts\n * SwaggerAdapter({\n * securityResolver: ({ controllerClass, handlerName }) => {\n * // Bridge your own auth decorators' metadata without coupling.\n * const proto = controllerClass.prototype\n * if (Reflect.getMetadata('kick:auth:public', proto, handlerName)) return null\n * const secured =\n * Reflect.getMetadata('kick:auth:authenticated', controllerClass) ||\n * Reflect.getMetadata('kick:auth:authenticated', proto, handlerName)\n * return secured ? 'BearerAuth' : undefined\n * },\n * })\n * ```\n */\n /**\n * Name of the route flag that marks an endpoint public — the spec then reads\n * the same declaration the runtime does, instead of asking for a second\n * annotation that can drift from it.\n *\n * The name is configuration rather than a constant because the framework\n * deliberately names no flags: one project's `auth.public` is another's\n * `public` or `security.none`. Pass a list to accept several.\n *\n * ```ts\n * // src/flags.ts\n * export const Public = defineRouteFlag('auth.public')\n *\n * SwaggerAdapter({ bearerAuth: true, publicFlag: 'auth.public' })\n * ```\n *\n * Checked after {@link ApiPublic} and {@link securityResolver}, before the\n * `@ApiSecurity` / `@ApiBearerAuth` decorators — so an explicit resolver\n * still wins, and a flag still overrides class-level security.\n */\n publicFlag?: string | readonly string[]\n securityResolver?: (\n ctx: SecurityResolverContext,\n ) => string | ApiSecurityRequirement | (string | ApiSecurityRequirement)[] | null | undefined\n /**\n * Pluggable schema parser for converting validation schemas to JSON Schema.\n * Defaults to `zodSchemaParser` which handles Zod v4+ schemas.\n *\n * Override this to use Yup, Joi, Valibot, ArkType, or any other library.\n *\n * @example\n * ```ts\n * SwaggerAdapter({\n * schemaParser: myYupParser,\n * })\n * ```\n */\n schemaParser?: SchemaParser\n}\n\n/**\n * Normalise any of the shapes accepted by `@ApiSecurity` /\n * `securityResolver` into a flat `ApiSecurityRequirement[]`.\n */\nfunction normaliseSecurity(\n raw: string | ApiSecurityRequirement | (string | ApiSecurityRequirement)[],\n): ApiSecurityRequirement[] {\n const arr = Array.isArray(raw) ? raw : [raw]\n return arr.map((entry) =>\n typeof entry === 'string'\n ? { name: entry, scopes: [] }\n : { name: entry.name, scopes: entry.scopes ?? [] },\n )\n}\n\ninterface RegisteredRoute {\n controllerClass: any\n mountPath: string\n}\n\n/**\n * Default route bag used when callers don't pass a config-scoped key.\n * Kept for back-compat with code that imports `registerControllerForDocs`\n * directly without going through SwaggerAdapter — those callers see the\n * legacy \"global single list\" behaviour.\n */\nconst DEFAULT_SCOPE = Symbol('kick:swagger:default-scope')\n\n/**\n * Per-adapter route storage. The adapter's `build` closure passes its\n * config object as the scope key so two SwaggerAdapter instances in\n * the same process (test harnesses, multi-tenant pre-fork) keep\n * independent route lists. Without this, two bootstraps in one process\n * cross-contaminate each other's specs.\n */\nconst routesByScope = new Map<object | symbol, RegisteredRoute[]>()\nroutesByScope.set(DEFAULT_SCOPE, [])\n\nfunction getScopeBag(scope: object | symbol | undefined): RegisteredRoute[] {\n const key = scope ?? DEFAULT_SCOPE\n let bag = routesByScope.get(key)\n if (!bag) {\n bag = []\n routesByScope.set(key, bag)\n }\n return bag\n}\n\n/**\n * Memoised spec — built lazily on the first {@link buildOpenAPISpec}\n * call after a registration change. Re-issued without rebuild on every\n * subsequent `/openapi.json` request until `clearRegisteredRoutes` or\n * `registerControllerForDocs` invalidates it.\n *\n * Keyed by reference equality on the options object so two adapters\n * with different `info.title` don't return each other's cached spec.\n * Application keeps the SwaggerAdapter config alive for the process\n * lifetime, so this is effectively a per-adapter memo cache. WeakMap\n * keeps the entries collectable when an adapter is disposed.\n *\n * `cacheKeys` is the iteration handle (WeakMap doesn't expose one) so\n * we can flush every cached spec on registration change without\n * tracking adapters individually.\n */\nconst specCache = new WeakMap<object, unknown>()\nconst cacheKeys = new Set<object>()\n\nfunction invalidateSpecCache(scope?: object | symbol): void {\n if (scope && typeof scope === 'object') {\n // Targeted invalidation — only the spec keyed on this config is stale.\n if (cacheKeys.has(scope)) {\n specCache.delete(scope)\n cacheKeys.delete(scope)\n }\n return\n }\n // Fallback: flush every cached spec (legacy untyped invalidation).\n for (const key of cacheKeys) specCache.delete(key)\n cacheKeys.clear()\n}\n\n/**\n * Register a controller for OpenAPI introspection. Called by Application\n * during route mounting via the adapter's onRouteMount hook.\n *\n * The optional `scope` argument keys the registration to a specific\n * adapter instance — pass the adapter's own config object as the key\n * (the SwaggerAdapter does this automatically). Omit for legacy\n * single-list behaviour, which is fine for single-bootstrap apps.\n */\nexport function registerControllerForDocs(\n controllerClass: any,\n mountPath: string,\n scope?: object,\n): void {\n getScopeBag(scope).push({ controllerClass, mountPath })\n invalidateSpecCache(scope)\n}\n\n/**\n * Clear registered routes — supports HMR rebuilds. Pass the adapter's\n * config object to clear only that adapter's routes; omit to clear\n * every scope (legacy/global behaviour).\n */\nexport function clearRegisteredRoutes(scope?: object): void {\n if (scope && typeof scope === 'object') {\n routesByScope.delete(scope)\n invalidateSpecCache(scope)\n return\n }\n routesByScope.clear()\n routesByScope.set(DEFAULT_SCOPE, [])\n invalidateSpecCache()\n}\n\n/**\n * Build a full OpenAPI 3.0.3 spec from registered controllers and\n * their decorators.\n *\n * Memoised — the first call for a given `options` object walks every\n * controller (~80–150ms for a 200-route app); subsequent calls return\n * the cached spec until {@link clearRegisteredRoutes} or\n * {@link registerControllerForDocs} invalidate. This matters because\n * Swagger UI re-fetches `/openapi.json` on every navigation; before\n * the cache, every fetch re-walked the entire controller graph.\n */\nexport function buildOpenAPISpec(options: SwaggerOptions = {}): any {\n const cacheKey = options as object\n const cached = specCache.get(cacheKey)\n if (cached !== undefined) return cached\n const built = buildOpenAPISpecUncached(options)\n specCache.set(cacheKey, built)\n cacheKeys.add(cacheKey)\n return built\n}\n\nfunction buildOpenAPISpecUncached(options: SwaggerOptions = {}): any {\n const parser = options.schemaParser ?? zodSchemaParser\n\n /** Convert a validation schema to JSON Schema using the configured parser */\n const toJsonSchema = (schema: unknown): Record<string, unknown> | null => {\n try {\n if (!parser.supports(schema)) return null\n return parser.toJsonSchema(schema)\n } catch {\n return null\n }\n }\n\n const componentSchemas: Record<string, any> = {}\n let schemaCounter = 0\n\n /**\n * Register a schema in components.schemas and return a $ref pointer.\n * If the schema has a title/label, use that as the name. Otherwise generate one.\n */\n const registerSchema = (jsonSchema: Record<string, unknown>, hint?: string): any => {\n // Try to extract a name from the schema\n let baseName = (jsonSchema.title as string) || (jsonSchema.label as string) || hint || ''\n if (!baseName) {\n baseName = `Schema${++schemaCounter}`\n }\n // Sanitize name for OpenAPI (remove spaces, special chars)\n baseName = baseName.replace(/[^a-zA-Z0-9]/g, '')\n\n const clean = { ...jsonSchema }\n delete clean.title\n delete clean.label\n delete clean.$schema\n const cleanJson = JSON.stringify(clean)\n\n // Resolve name collisions: if `baseName` already maps to a different\n // schema body, suffix with `_2`, `_3`, etc. until a free slot or a\n // structural duplicate is found. Two semantically-identical schemas\n // (`CreateUserDTO` registered twice) collapse to one entry by\n // JSON-equality, preserving the existing dedupe behaviour for the\n // common case while preventing the silent overwrite that produced\n // wrong-shape docs when two distinct DTOs hit the same hint.\n let name = baseName\n let suffix = 2\n while (componentSchemas[name]) {\n if (JSON.stringify(componentSchemas[name]) === cleanJson) {\n // Same schema body — reuse the existing slot.\n return { $ref: `#/components/schemas/${name}` }\n }\n name = `${baseName}_${suffix++}`\n }\n componentSchemas[name] = clean\n return { $ref: `#/components/schemas/${name}` }\n }\n\n const spec: any = {\n openapi: '3.0.3',\n info: {\n title: options.info?.title || 'API',\n version: options.info?.version || '1.0.0',\n ...(options.info?.description ? { description: options.info.description } : {}),\n },\n paths: {},\n components: { schemas: {}, securitySchemes: {} },\n tags: [],\n }\n\n if (options.servers) {\n // Drop entries whose URL can't be parsed by the browser's URL\n // constructor. Swagger UI runs `new URL(server.url)` on the client\n // and crashes with `Failed to construct 'URL': Invalid URL` if any\n // entry is malformed — which can happen on Windows dev when an\n // adapter hook populates servers with a path that was never meant\n // to be a URL. Relative URLs (e.g. '/') are allowed through.\n const validServers = options.servers.filter((s) => {\n if (!s?.url || typeof s.url !== 'string') return false\n if (s.url.startsWith('/')) return true\n try {\n void new URL(s.url)\n return true\n } catch {\n return false\n }\n })\n if (validServers.length > 0) {\n spec.servers = validServers\n }\n }\n\n const allTags = new Set<string>()\n // Pre-seed `securitySchemes` with adopter-declared schemes from\n // `options.securitySchemes` so `@ApiSecurity('OAuth2')` references\n // resolve without per-decorator scheme synthesis. The pre-seeded\n // entries take precedence over the implicit `BearerAuth` fallback\n // emitted in the route loop, so adopters who redefine `BearerAuth`\n // (e.g. with custom flows) get their version.\n const securitySchemes: Record<string, any> = { ...options.securitySchemes }\n\n // Routes scoped to this adapter's config (when adapter passed itself\n // as the scope) plus the legacy default-scope bag (for direct\n // registerControllerForDocs callers without a scope arg).\n const scopedRoutes = getScopeBag(options as object)\n const defaultRoutes = options ? getScopeBag(DEFAULT_SCOPE) : []\n const routesToWalk =\n scopedRoutes.length > 0\n ? scopedRoutes\n : defaultRoutes /* fall back to legacy single-list when adapter didn't scope */\n\n for (const { controllerClass, mountPath } of routesToWalk) {\n // Skip excluded controllers\n if (hasClassMeta(SWAGGER_KEYS.EXCLUDE, controllerClass)) continue\n\n const routes: RouteDefinition[] = getClassMeta<RouteDefinition[]>(\n METADATA.ROUTES,\n controllerClass,\n [],\n )\n const classTags: string[] = getClassMeta<string[]>(SWAGGER_KEYS.TAGS, controllerClass, [])\n const classAuth: string | undefined = getClassMetaOrUndefined<string>(\n SWAGGER_KEYS.BEARER_AUTH,\n controllerClass,\n )\n const classSecurity = getClassMetaOrUndefined<ApiSecurityRequirement[]>(\n SWAGGER_KEYS.SECURITY,\n controllerClass,\n )\n for (const route of routes) {\n try {\n emitRouteOperation(route)\n } catch (err) {\n // One bad operation must not blank the whole docs page. Emit a\n // marker summary so the broken op shows up in Swagger UI with\n // a visible warning, and the rest of the spec stays valid.\n // Defensive resolution — the same fields that crashed inside\n // emit may still be undefined here.\n let openApiPath: string\n try {\n openApiPath = joinPaths(mountPath, route.path).replace(EXPRESS_PARAM_RE, '{$1}')\n } catch {\n openApiPath = `${mountPath}/__spec_error__`\n }\n const method = typeof route.method === 'string' ? route.method.toLowerCase() : 'get'\n if (!spec.paths[openApiPath]) spec.paths[openApiPath] = {}\n spec.paths[openApiPath][method] = {\n summary: `⚠ spec generation failed: ${err instanceof Error ? err.message : String(err)}`,\n responses: { default: { description: 'Spec generation failed for this operation.' } },\n }\n }\n }\n\n // Per-route emit hoisted to a closure so the try/catch above can\n // wrap each route in isolation. Closes over loop-locals (operation,\n // routes, classTags, classAuth, etc.) so the body reads the same\n // way it did before the wrap.\n function emitRouteOperation(route: RouteDefinition): void {\n // Skip excluded methods\n if (getMethodMetaOrUndefined(SWAGGER_KEYS.EXCLUDE, controllerClass, route.handlerName)) return\n\n // Build the full path — mountPath is the actual Express mount prefix (from onRouteMount),\n // and route.path is the method-level path. @Controller path is not included here\n // because buildRoutes does not bake it into the router.\n const fullPath = joinPaths(mountPath, route.path)\n\n // Convert Express :param to OpenAPI {param}. Express's\n // path-to-regexp param-name rule is `[A-Za-z_][A-Za-z0-9_]*` —\n // identifier-like, digits allowed after the first char. The\n // previous regex (`[a-zA-Z_]+`) silently dropped digits, so\n // `:v2endpoint` became `:v` + literal `2endpoint` and the\n // generated docs missed the path-param entry entirely.\n const openApiPath = fullPath.replace(EXPRESS_PARAM_RE, '{$1}')\n const method = route.method.toLowerCase()\n\n // Gather metadata\n const operation: ApiOperationOptions = getMethodMeta<ApiOperationOptions>(\n SWAGGER_KEYS.OPERATION,\n controllerClass,\n route.handlerName,\n {} as ApiOperationOptions,\n )\n const responses: ApiResponseOptions[] = getMethodMeta<ApiResponseOptions[]>(\n SWAGGER_KEYS.RESPONSES,\n controllerClass,\n route.handlerName,\n [],\n )\n const methodTags: string[] = getMethodMeta<string[]>(\n SWAGGER_KEYS.TAGS,\n controllerClass,\n route.handlerName,\n [],\n )\n const methodAuth: string | undefined = getMethodMetaOrUndefined<string>(\n SWAGGER_KEYS.BEARER_AUTH,\n controllerClass,\n route.handlerName,\n )\n\n // Tags — method level overrides class level\n const tags = methodTags.length > 0 ? methodTags : classTags\n tags.forEach((t) => allTags.add(t))\n\n // Build operation object — `parameters` and `responses` are\n // attached below only when they have entries, so we don't emit\n // empty arrays/objects only to delete them later.\n const op: any = {\n ...(tags.length > 0 ? { tags } : {}),\n ...(operation.summary ? { summary: operation.summary } : {}),\n ...(operation.description ? { description: operation.description } : {}),\n ...(operation.operationId ? { operationId: operation.operationId } : {}),\n ...(operation.deprecated ? { deprecated: true } : {}),\n responses: {},\n }\n const parameters: any[] = []\n\n // Path parameters\n const paramMatches = fullPath.match(EXPRESS_PARAM_RE) || []\n for (const match of paramMatches) {\n const paramName = match.slice(1)\n let schema: any = { type: 'string' }\n\n // Try to get type from params validation schema\n if (route.validation?.params) {\n const jsonSchema = toJsonSchema(route.validation.params)\n if (jsonSchema?.properties && typeof jsonSchema.properties === 'object') {\n const props = jsonSchema.properties as Record<string, any>\n if (props[paramName]) {\n schema = props[paramName]\n }\n }\n }\n\n parameters.push({ name: paramName, in: 'path', required: true, schema })\n }\n\n // Query parameters\n if (route.validation?.query) {\n const jsonSchema = toJsonSchema(route.validation.query)\n if (jsonSchema?.properties && typeof jsonSchema.properties === 'object') {\n const required = Array.isArray(jsonSchema.required) ? jsonSchema.required : []\n for (const [name, propSchema] of Object.entries(\n jsonSchema.properties as Record<string, any>,\n )) {\n parameters.push({\n name,\n in: 'query',\n required: required.includes(name),\n schema: propSchema,\n })\n }\n }\n }\n\n // @ApiQueryParams decorator — document filterable/sortable/searchable fields\n const queryParamsConfig = getMethodMetaOrUndefined<any>(\n METADATA.QUERY_PARAMS,\n controllerClass,\n route.handlerName,\n )\n if (queryParamsConfig) {\n if (queryParamsConfig.filterable?.length) {\n parameters.push({\n name: 'filter',\n in: 'query',\n required: false,\n description: `Filter fields: ${queryParamsConfig.filterable.join(', ')}. Format: \\`field:operator:value\\`. Operators: eq, neq, gt, gte, lt, lte, contains, starts, ends, in, between`,\n schema: { type: 'array', items: { type: 'string' } },\n style: 'form',\n explode: true,\n })\n }\n if (queryParamsConfig.sortable?.length) {\n parameters.push({\n name: 'sort',\n in: 'query',\n required: false,\n description: `Sort fields: ${queryParamsConfig.sortable.join(', ')}. Format: \\`field:asc\\` or \\`field:desc\\``,\n schema: { type: 'array', items: { type: 'string' } },\n style: 'form',\n explode: true,\n })\n }\n if (queryParamsConfig.searchable?.length) {\n parameters.push({\n name: 'q',\n in: 'query',\n required: false,\n description: `Search across: ${queryParamsConfig.searchable.join(', ')}`,\n schema: { type: 'string' },\n })\n }\n parameters.push(\n {\n name: 'page',\n in: 'query',\n required: false,\n description: 'Page number (default: 1)',\n schema: { type: 'integer', minimum: 1, default: 1 },\n },\n {\n name: 'limit',\n in: 'query',\n required: false,\n description: 'Items per page (default: 20, max: 100)',\n schema: { type: 'integer', minimum: 1, maximum: 100, default: 20 },\n },\n )\n }\n\n if (parameters.length > 0) op.parameters = parameters\n\n // Request body\n if (route.validation?.body) {\n if (BODY_METHODS.has(method)) {\n const bodySchema = toJsonSchema(route.validation.body)\n if (bodySchema) {\n const bodyName = route.validation.name || `${route.handlerName}Body`\n const ref = registerSchema(bodySchema, bodyName)\n op.requestBody = {\n required: true,\n content: { 'application/json': { schema: ref } },\n }\n }\n } else {\n // Body validation on a method that OpenAPI 3 doesn't allow a\n // body for (GET / HEAD / DELETE / OPTIONS). Silently dropping\n // surprised adopters whose request schema vanished from docs;\n // warn once per route so they can switch to query validation\n // or rethink the route shape.\n const warnKey = `${controllerClass.name}.${route.handlerName}`\n if (!warnedBodyOnReadMethod.has(warnKey)) {\n warnedBodyOnReadMethod.add(warnKey)\n log.warn(\n `body validation on ${method.toUpperCase()} ${fullPath} (${warnKey}) is dropped from the OpenAPI spec — OpenAPI 3 does not allow a request body on ${method.toUpperCase()}. Move the schema to validation.query or change the route method.`,\n )\n }\n }\n }\n\n // File upload detection\n const fileUpload = getMethodMetaOrUndefined<any>(\n METADATA.FILE_UPLOAD,\n controllerClass,\n route.handlerName,\n )\n if (fileUpload) {\n const fieldName = fileUpload.fieldName ?? 'file'\n const properties: any = {}\n\n if (fileUpload.mode === 'array') {\n properties[fieldName] = {\n type: 'array',\n items: { type: 'string', format: 'binary' },\n }\n } else if (fileUpload.mode !== 'none') {\n properties[fieldName] = {\n type: 'string',\n format: 'binary',\n }\n }\n\n op.requestBody = {\n required: true,\n content: {\n 'multipart/form-data': {\n schema: { type: 'object', properties },\n },\n },\n }\n }\n\n // Responses\n if (responses.length > 0) {\n for (const resp of responses) {\n const entry: Record<string, unknown> = { description: resp.description || '' }\n if (resp.schema && typeof resp.schema === 'object') {\n // Try the validation parser first (Zod / Yup / etc.). If\n // that returns null the schema is plain JSON Schema and we\n // pass it through as-is — that's the escape hatch for\n // adopters who hand-write OpenAPI shapes without going\n // through the schema-parser layer.\n const converted = toJsonSchema(resp.schema)\n const schemaName = resp.name || `${route.handlerName}Response${resp.status}`\n const finalSchema = converted ? registerSchema(converted, schemaName) : resp.schema\n entry.content = { 'application/json': { schema: finalSchema } }\n }\n op.responses[String(resp.status)] = entry\n }\n } else {\n // Auto-generate default responses\n const defaultStatus = method === 'post' ? '201' : method === 'delete' ? '204' : '200'\n const success: Record<string, unknown> = { description: 'Successful operation' }\n // Declared response contract (`@Get('/', { response: schema })`) —\n // the same declaration `kick typegen` consumes, so docs and types\n // can't drift. Explicit @ApiResponse entries above still win.\n if (route.validation?.response && defaultStatus !== '204') {\n const converted = toJsonSchema(route.validation.response)\n if (converted) {\n const schemaName = `${route.validation.name || route.handlerName}Response`\n success.content = {\n 'application/json': { schema: registerSchema(converted, schemaName) },\n }\n }\n }\n op.responses[defaultStatus] = success\n\n if (route.validation?.body) {\n op.responses['422'] = { description: 'Validation error' }\n }\n }\n\n // Security resolution order (first match wins):\n // 1. @ApiPublic on the method — opt-out, no security emitted.\n // 2. options.securityResolver({controllerClass, handlerName})\n // — adopter-provided bridge for external auth libraries.\n // Returning `null` is \"explicitly public\" (same as\n // @ApiPublic); a value or array drives the requirements.\n // 3. options.publicFlag — a route flag naming this endpoint public.\n // 4. @ApiSecurity / @ApiBearerAuth on the method.\n // 5. @ApiSecurity / @ApiBearerAuth on the class.\n const isPublicMethod = !!getMethodMetaOrUndefined<boolean>(\n SWAGGER_KEYS.PUBLIC,\n controllerClass,\n route.handlerName,\n )\n const methodSecurity = getMethodMetaOrUndefined<ApiSecurityRequirement[]>(\n SWAGGER_KEYS.SECURITY,\n controllerClass,\n route.handlerName,\n )\n const resolverOutput = !isPublicMethod\n ? options.securityResolver?.({ controllerClass, handlerName: route.handlerName })\n : undefined\n const resolverSecurity =\n resolverOutput == null || resolverOutput === undefined\n ? undefined\n : normaliseSecurity(resolverOutput)\n const resolverPublic = resolverOutput === null\n\n // A route flag naming this endpoint public. Resolved from the controller\n // metadata, so it is the same method-over-class value `ctx.route.flags`\n // carries at request time.\n const publicFlagNames =\n options.publicFlag === undefined\n ? []\n : typeof options.publicFlag === 'string'\n ? [options.publicFlag]\n : options.publicFlag\n const flagPublic =\n publicFlagNames.length > 0 &&\n (() => {\n const flags = getRouteFlags(controllerClass, route.handlerName)\n return publicFlagNames.some((name) => flags.has(name))\n })()\n\n let requirements: ApiSecurityRequirement[] | undefined\n // Track whether the resolution path came from `@ApiBearerAuth`\n // (any name) so a bearer-shaped scheme gets auto-synthesised\n // for the named entry — preserves the original\n // `@ApiBearerAuth('CustomName')` ergonomics. `@ApiSecurity`\n // and the resolver hook DON'T auto-synth for arbitrary names\n // (only the literal `'BearerAuth'`) since their shapes are\n // generic — adopters must declare custom schemes via\n // `SwaggerOptions.securitySchemes` when using those paths.\n let bearerAuthSourced = false\n if (isPublicMethod || resolverPublic || (flagPublic && resolverSecurity === undefined)) {\n requirements = undefined\n } else if (resolverSecurity && resolverSecurity.length > 0) {\n requirements = resolverSecurity\n } else if (methodSecurity && methodSecurity.length > 0) {\n requirements = methodSecurity\n } else if (methodAuth) {\n requirements = [{ name: methodAuth, scopes: [] }]\n bearerAuthSourced = true\n } else if (classSecurity && classSecurity.length > 0) {\n requirements = classSecurity\n } else if (classAuth) {\n requirements = [{ name: classAuth, scopes: [] }]\n bearerAuthSourced = true\n }\n\n if (requirements) {\n op.security = requirements.map((r) => ({ [r.name]: r.scopes ?? [] }))\n for (const r of requirements) {\n if (!securitySchemes[r.name]) {\n // `@ApiBearerAuth('CustomName')` always emits a\n // bearer-shaped scheme under `CustomName`. The literal\n // `BearerAuth` name also auto-synths for back-compat\n // with `@ApiSecurity('BearerAuth')` and resolver hooks.\n if (bearerAuthSourced || r.name === 'BearerAuth') {\n securitySchemes[r.name] = {\n type: 'http',\n scheme: 'bearer',\n bearerFormat: 'JWT',\n }\n }\n }\n }\n }\n\n // Mount\n if (!spec.paths[openApiPath]) spec.paths[openApiPath] = {}\n spec.paths[openApiPath][method] = op\n }\n }\n\n // Finalize\n spec.tags = Array.from(allTags).map((name) => ({ name }))\n spec.components.securitySchemes = securitySchemes\n\n if (options.bearerAuth) {\n if (!securitySchemes.BearerAuth) {\n spec.components.securitySchemes.BearerAuth = {\n type: 'http',\n scheme: 'bearer',\n bearerFormat: 'JWT',\n }\n }\n spec.security = [{ BearerAuth: [] }]\n }\n\n // Merge collected schemas into components\n spec.components.schemas = componentSchemas\n\n // Clean up empty components\n if (Object.keys(spec.components.schemas).length === 0) delete spec.components.schemas\n if (Object.keys(spec.components.securitySchemes).length === 0)\n delete spec.components.securitySchemes\n if (Object.keys(spec.components).length === 0) delete spec.components\n\n return spec\n}\n","/** Escape a string for safe HTML attribute/content interpolation */\nfunction escapeHtml(str: string): string {\n return str\n .replace(/&/g, '&')\n .replace(/</g, '<')\n .replace(/>/g, '>')\n .replace(/\"/g, '"')\n .replace(/'/g, ''')\n}\n\n/**\n * Generate Swagger UI HTML using local assets from swagger-ui-dist.\n *\n * Assets are served from `/_swagger-assets/` by the adapter's Express\n * static middleware. Falls back to CDN if the local path is not provided.\n * This ensures Swagger UI works fully offline in development.\n *\n * @param specUrl - Path to the OpenAPI JSON spec (e.g., '/openapi.json')\n * @param title - Page title\n * @param assetsPath - Base path for local swagger-ui-dist assets (e.g., '/_swagger-assets')\n */\nexport function swaggerUIHtml(specUrl: string, title = 'API Docs', assetsPath?: string): string {\n const safeTitle = escapeHtml(title)\n // JSON-stringify for safe inlining into the `<script>` block. The inline\n // script below resolves this to an absolute URL against\n // `window.location.origin` before passing it to SwaggerUIBundle —\n // some swagger-ui-dist builds call `new URL(url)` without a base and\n // crash with `Failed to construct 'URL': Invalid URL` when the value\n // is a bare path like `/openapi.json`.\n const safeUrl = JSON.stringify(specUrl).replace(/</g, '\\\\u003c')\n\n // Use local assets if available, CDN as fallback\n const cssHref = assetsPath\n ? `${assetsPath}/swagger-ui.css`\n : 'https://unpkg.com/swagger-ui-dist@5/swagger-ui.css'\n const bundleSrc = assetsPath\n ? `${assetsPath}/swagger-ui-bundle.js`\n : 'https://unpkg.com/swagger-ui-dist@5/swagger-ui-bundle.js'\n const presetSrc = assetsPath\n ? `${assetsPath}/swagger-ui-standalone-preset.js`\n : 'https://unpkg.com/swagger-ui-dist@5/swagger-ui-standalone-preset.js'\n\n return `<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n <meta charset=\"UTF-8\">\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n <title>${safeTitle}</title>\n <link rel=\"stylesheet\" href=\"${cssHref}\">\n</head>\n<body>\n <div id=\"swagger-ui\"></div>\n <script src=\"${bundleSrc}\"></script>\n <script src=\"${presetSrc}\"></script>\n <script>\n (function () {\n var rawUrl = ${safeUrl};\n var specUrl;\n try {\n specUrl = new URL(rawUrl, window.location.origin).href;\n } catch (_e) {\n specUrl = rawUrl;\n }\n SwaggerUIBundle({\n url: specUrl,\n dom_id: '#swagger-ui',\n deepLinking: true,\n presets: [SwaggerUIBundle.presets.apis, SwaggerUIStandalonePreset],\n plugins: [SwaggerUIBundle.plugins.DownloadUrl],\n layout: 'StandaloneLayout',\n });\n })();\n </script>\n</body>\n</html>`\n}\n\n/**\n * Generate ReDoc HTML.\n *\n * ReDoc doesn't publish a standalone npm package suitable for local serving,\n * so it still loads from CDN. If offline support for ReDoc is needed,\n * vendor the standalone bundle into the package's public/ directory.\n */\nexport function redocHtml(specUrl: string, title = 'API Docs'): string {\n const safeTitle = escapeHtml(title)\n const safeUrl = escapeHtml(specUrl)\n\n return `<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n <meta charset=\"UTF-8\">\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n <title>${safeTitle}</title>\n</head>\n<body>\n <redoc spec-url=\"${safeUrl}\"></redoc>\n <script src=\"https://cdn.redoc.ly/redoc/latest/bundles/redoc.standalone.js\"></script>\n</body>\n</html>`\n}\n","import { dirname } from 'node:path'\nimport { createRequire } from 'node:module'\nimport type { Request, Response, NextFunction } from 'express'\nimport { Logger, defineAdapter } from '@forinda/kickjs'\nimport {\n buildOpenAPISpec,\n registerControllerForDocs,\n clearRegisteredRoutes,\n type SwaggerOptions,\n} from './openapi-builder'\nimport { swaggerUIHtml, redocHtml } from './ui'\n\nconst log = Logger.for('SwaggerAdapter')\n\n/**\n * Resolve the absolute path to swagger-ui-dist's static assets.\n * Uses createRequire to find it relative to this package (works with pnpm).\n */\nfunction getSwaggerUiDistPath(): string {\n const require = createRequire(import.meta.url)\n return dirname(require.resolve('swagger-ui-dist/package.json'))\n}\n\n/**\n * UI renderer signature — receives the spec URL and an optional title,\n * returns a complete HTML document. Both the built-in `swaggerUIHtml`\n * and `redocHtml` match this shape (the optional `assetsPath` arg\n * is opt-in for the offline-asset case and ignored by ReDoc).\n *\n * Adopters who want corporate branding, dark-mode default, custom\n * logos, or a third-party UI bundle (Stoplight Elements, RapiDoc,\n * Scalar) replace either renderer with their own.\n */\nexport type UIRenderer = (specUrl: string, title?: string, assetsPath?: string) => string\n\nexport interface SwaggerAdapterOptions extends SwaggerOptions {\n /** Path to serve Swagger UI (default: '/docs') */\n docsPath?: string\n /** Path to serve ReDoc (default: '/redoc') */\n redocPath?: string\n /** Path to serve the raw JSON spec (default: '/openapi.json') */\n specPath?: string\n /** Other adapters to discover (e.g., WsAdapter for WebSocket server URLs) */\n adapters?: any[]\n /**\n * When true, the adapter is a no-op while `NODE_ENV === 'production'` —\n * docs, spec, and assets are not mounted. Useful for keeping API docs\n * out of production builds without conditionally constructing the adapter.\n */\n disableInProd?: boolean\n /**\n * Override the Swagger UI HTML renderer. Defaults to the built-in\n * {@link swaggerUIHtml}. Useful for adopters who want corporate\n * branding, a custom theme, or to swap in a third-party UI bundle\n * (Stoplight Elements, RapiDoc, Scalar).\n *\n * @example\n * ```ts\n * SwaggerAdapter({\n * renderSwaggerUI: (specUrl, title) => myBrandedHtml(specUrl, title),\n * })\n * ```\n */\n renderSwaggerUI?: UIRenderer\n /**\n * Override the ReDoc HTML renderer. Defaults to the built-in\n * {@link redocHtml}. Same shape as {@link renderSwaggerUI}.\n */\n renderReDoc?: UIRenderer\n}\n\n/**\n * Swagger adapter — auto-generates OpenAPI spec from decorators and serves docs.\n *\n * Assets are served locally from `swagger-ui-dist` (npm dependency) —\n * no CDN required, works fully offline.\n *\n * @example\n * ```ts\n * bootstrap({\n * modules,\n * adapters: [\n * SwaggerAdapter({\n * info: { title: 'My API', version: '1.0.0' },\n * }),\n * ],\n * })\n * ```\n *\n * Endpoints:\n * GET /docs — Swagger UI (local assets, no CDN)\n * GET /redoc — ReDoc (CDN — no local package available)\n * GET /openapi.json — Raw OpenAPI 3.0.3 spec\n */\nexport const SwaggerAdapter = defineAdapter<SwaggerAdapterOptions>({\n name: 'SwaggerAdapter',\n defaults: {\n docsPath: '/docs',\n redocPath: '/redoc',\n specPath: '/openapi.json',\n },\n build: (config) => {\n // Resolved once at build time — config.disableInProd is set at\n // construction; NODE_ENV doesn't change at runtime. Checking on\n // every onRouteMount call (which fires per-controller) is noise.\n const disabled = Boolean(config.disableInProd) && process.env.NODE_ENV === 'production'\n const isDisabled = (): boolean => disabled\n\n // Snapshot the user-supplied servers list once per adapter instance\n // so subsequent afterStart runs (HMR reload, dev-mode restart loops,\n // multi-instance pre-fork in tests) re-derive the auto-detected\n // entries from a clean baseline instead of stacking duplicates onto\n // the previous run's accretion.\n const userSuppliedServers: ReadonlyArray<{ url: string; description?: string }> = config.servers\n ? [...config.servers]\n : []\n\n return {\n onRouteMount(controllerClass, mountPath) {\n if (isDisabled()) return\n // Pass `config` as the scope key so each SwaggerAdapter instance\n // owns its own route bag — two bootstraps in one process can't\n // cross-contaminate each other's specs.\n registerControllerForDocs(controllerClass, mountPath, config)\n },\n\n afterStart({ server }) {\n if (isDisabled()) return\n const addr = server?.address?.()\n if (!addr || typeof addr !== 'object') return\n\n const host =\n addr.address === '::' || addr.address === '0.0.0.0' ? 'localhost' : addr.address\n\n const autoDetected: { url: string; description?: string }[] = []\n // HTTP server URL is always auto-added — adopters who passed an\n // explicit HTTP URL keep their entry first because we restart\n // from the user snapshot above.\n autoDetected.push({ url: `http://${host}:${addr.port}`, description: 'HTTP server' })\n\n // Auto-add WebSocket server URLs from WsAdapter (one per namespace)\n const wsAdapter = config.adapters?.find(\n (a) => a.name === 'WsAdapter' && typeof a.getStats === 'function',\n )\n if (wsAdapter) {\n const stats = wsAdapter.getStats()\n for (const namespace of Object.keys(stats.namespaces || {})) {\n autoDetected.push({\n url: `ws://${host}:${addr.port}${namespace}`,\n description: `WebSocket: ${namespace}`,\n })\n }\n }\n\n // Always rebuild from the snapshot — replaces any leftover\n // auto-detected entries from a previous afterStart run.\n config.servers = [...userSuppliedServers, ...autoDetected]\n },\n\n beforeMount({ http }) {\n if (isDisabled()) {\n log.info('Swagger disabled in production (disableInProd=true)')\n return\n }\n // Clear previous registrations for THIS adapter (supports HMR\n // rebuild). Sibling adapters' route bags stay untouched.\n clearRegisteredRoutes(config)\n const docsPath = config.docsPath!\n const redocPath = config.redocPath!\n const specPath = config.specPath!\n let uiDistAvailable = false\n\n // ── Serve swagger-ui-dist static assets locally ──────────────────\n // This makes Swagger UI work offline — no CDN needed.\n // Assets served at /_swagger-assets/ (CSS, JS, fonts, etc.)\n const swaggerAssetsPath = '/_swagger-assets'\n try {\n const swaggerDistDir = getSwaggerUiDistPath()\n http.serveStatic(swaggerAssetsPath, swaggerDistDir)\n uiDistAvailable = true\n } catch {\n log.warn('swagger-ui-dist not found — Swagger UI will load from CDN (requires internet).')\n }\n\n // Tightened CSP: only whitelist the CDN entries we actually\n // need. The default Swagger UI renderer needs unpkg.com for\n // CDN fallback (when swagger-ui-dist isn't installed) AND for\n // the inline script. The default ReDoc renderer needs\n // cdn.redoc.ly for the standalone bundle. Custom renderers\n // (renderSwaggerUI / renderReDoc overrides) get only the\n // baseline policy — adopters set their own headers there.\n const customSwaggerRenderer = Boolean(config.renderSwaggerUI)\n const customReDocRenderer = Boolean(config.renderReDoc)\n const swaggerOrigins = uiDistAvailable || customSwaggerRenderer ? [] : ['https://unpkg.com']\n const redocOrigins = customReDocRenderer\n ? []\n : ['https://cdn.redoc.ly', 'https://cdn.jsdelivr.net']\n const scriptOrigins = [...swaggerOrigins, ...redocOrigins]\n const styleOrigins =\n uiDistAvailable || customSwaggerRenderer\n ? ['https://fonts.googleapis.com']\n : ['https://unpkg.com', 'https://fonts.googleapis.com']\n const imgOrigins = uiDistAvailable || customSwaggerRenderer ? [] : ['https://unpkg.com']\n\n http.use((_req: Request, res: Response, next: NextFunction) => {\n // Build connect-src dynamically so \"Try it out\" can call any configured server URL.\n // Includes dev-friendly localhost/127.0.0.1 origins so docs served from one host\n // can call an API spec'd at the other (a common cross-origin gotcha).\n const serverOrigins = new Set<string>()\n for (const s of config.servers ?? []) {\n try {\n serverOrigins.add(new URL(s.url).origin)\n } catch {\n // ignore relative or malformed URLs\n }\n }\n const connectSrc = [\n \"'self'\",\n 'http://localhost:*',\n 'http://127.0.0.1:*',\n 'https://localhost:*',\n 'https://127.0.0.1:*',\n 'ws://localhost:*',\n 'ws://127.0.0.1:*',\n ...serverOrigins,\n ].join(' ')\n\n // Inline script in swaggerUIHtml is required by SwaggerUIBundle's\n // bootstrapping pattern. We can't drop 'unsafe-inline' without\n // refactoring to a hashed/nonced inline script; until then, keep\n // 'unsafe-inline' but minimise CDN whitelist.\n res.setHeader(\n 'Content-Security-Policy',\n [\n \"default-src 'self'\",\n `script-src 'self' 'unsafe-inline'${scriptOrigins.length ? ' ' + scriptOrigins.join(' ') : ''}`,\n `style-src 'self' 'unsafe-inline'${styleOrigins.length ? ' ' + styleOrigins.join(' ') : ''}`,\n \"font-src 'self' https://fonts.gstatic.com\",\n `img-src 'self' data:${imgOrigins.length ? ' ' + imgOrigins.join(' ') : ''}`,\n `connect-src ${connectSrc}`,\n ].join('; '),\n )\n next()\n })\n\n // Spec endpoint (JSON)\n http.route('GET', specPath, (ctx) => {\n ctx.json(buildOpenAPISpec(config))\n })\n\n // Swagger UI — uses local assets if available, CDN fallback.\n // Adopters can override `renderSwaggerUI` to swap the bundle\n // (Stoplight Elements, RapiDoc, Scalar) or apply branding.\n const renderSwagger = config.renderSwaggerUI ?? swaggerUIHtml\n const renderReDoc = config.renderReDoc ?? redocHtml\n http.route('GET', docsPath, (ctx) => {\n ctx.html(\n renderSwagger(\n specPath,\n config.info?.title,\n uiDistAvailable ? swaggerAssetsPath : undefined,\n ),\n )\n })\n\n // ReDoc — still CDN-based for the default renderer (no npm\n // package for the standalone bundle). Custom renderers can\n // self-host whatever they like.\n http.route('GET', redocPath, (ctx) => {\n ctx.html(renderReDoc(specPath, config.info?.title))\n })\n\n log.info(`Swagger UI: ${docsPath}`)\n log.info(`ReDoc: ${redocPath}`)\n log.info(`OpenAPI spec: ${specPath}`)\n },\n }\n },\n})\n\n// Re-export for use by Application when mounting module routes\nexport { registerControllerForDocs, clearRegisteredRoutes }\n"],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@forinda/kickjs-swagger",
|
|
3
|
-
"version": "7.
|
|
3
|
+
"version": "7.2.0",
|
|
4
4
|
"description": "OpenAPI spec generation from decorators, Swagger UI and ReDoc serving for KickJS",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"kickjs",
|
|
@@ -53,7 +53,7 @@
|
|
|
53
53
|
"vitest": "^4.1.11",
|
|
54
54
|
"yup": "^1.7.1",
|
|
55
55
|
"zod": "^4.5.1",
|
|
56
|
-
"@forinda/kickjs": "8.
|
|
56
|
+
"@forinda/kickjs": "8.2.0"
|
|
57
57
|
},
|
|
58
58
|
"publishConfig": {
|
|
59
59
|
"access": "public"
|