@forinda/kickjs-swagger 4.0.0 → 4.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 +58 -6
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +226 -67
- package/dist/index.mjs.map +1 -1
- package/package.json +2 -2
package/dist/index.d.mts
CHANGED
|
@@ -97,14 +97,47 @@ interface SwaggerOptions {
|
|
|
97
97
|
*/
|
|
98
98
|
schemaParser?: SchemaParser;
|
|
99
99
|
}
|
|
100
|
-
/**
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
100
|
+
/**
|
|
101
|
+
* Register a controller for OpenAPI introspection. Called by Application
|
|
102
|
+
* during route mounting via the adapter's onRouteMount hook.
|
|
103
|
+
*
|
|
104
|
+
* The optional `scope` argument keys the registration to a specific
|
|
105
|
+
* adapter instance — pass the adapter's own config object as the key
|
|
106
|
+
* (the SwaggerAdapter does this automatically). Omit for legacy
|
|
107
|
+
* single-list behaviour, which is fine for single-bootstrap apps.
|
|
108
|
+
*/
|
|
109
|
+
declare function registerControllerForDocs(controllerClass: any, mountPath: string, scope?: object): void;
|
|
110
|
+
/**
|
|
111
|
+
* Clear registered routes — supports HMR rebuilds. Pass the adapter's
|
|
112
|
+
* config object to clear only that adapter's routes; omit to clear
|
|
113
|
+
* every scope (legacy/global behaviour).
|
|
114
|
+
*/
|
|
115
|
+
declare function clearRegisteredRoutes(scope?: object): void;
|
|
116
|
+
/**
|
|
117
|
+
* Build a full OpenAPI 3.0.3 spec from registered controllers and
|
|
118
|
+
* their decorators.
|
|
119
|
+
*
|
|
120
|
+
* Memoised — the first call for a given `options` object walks every
|
|
121
|
+
* controller (~80–150ms for a 200-route app); subsequent calls return
|
|
122
|
+
* the cached spec until {@link clearRegisteredRoutes} or
|
|
123
|
+
* {@link registerControllerForDocs} invalidate. This matters because
|
|
124
|
+
* Swagger UI re-fetches `/openapi.json` on every navigation; before
|
|
125
|
+
* the cache, every fetch re-walked the entire controller graph.
|
|
126
|
+
*/
|
|
105
127
|
declare function buildOpenAPISpec(options?: SwaggerOptions): any;
|
|
106
128
|
//#endregion
|
|
107
129
|
//#region src/swagger.adapter.d.ts
|
|
130
|
+
/**
|
|
131
|
+
* UI renderer signature — receives the spec URL and an optional title,
|
|
132
|
+
* returns a complete HTML document. Both the built-in `swaggerUIHtml`
|
|
133
|
+
* and `redocHtml` match this shape (the optional `assetsPath` arg
|
|
134
|
+
* is opt-in for the offline-asset case and ignored by ReDoc).
|
|
135
|
+
*
|
|
136
|
+
* Adopters who want corporate branding, dark-mode default, custom
|
|
137
|
+
* logos, or a third-party UI bundle (Stoplight Elements, RapiDoc,
|
|
138
|
+
* Scalar) replace either renderer with their own.
|
|
139
|
+
*/
|
|
140
|
+
type UIRenderer = (specUrl: string, title?: string, assetsPath?: string) => string;
|
|
108
141
|
interface SwaggerAdapterOptions extends SwaggerOptions {
|
|
109
142
|
/** Path to serve Swagger UI (default: '/docs') */
|
|
110
143
|
docsPath?: string;
|
|
@@ -120,6 +153,25 @@ interface SwaggerAdapterOptions extends SwaggerOptions {
|
|
|
120
153
|
* out of production builds without conditionally constructing the adapter.
|
|
121
154
|
*/
|
|
122
155
|
disableInProd?: boolean;
|
|
156
|
+
/**
|
|
157
|
+
* Override the Swagger UI HTML renderer. Defaults to the built-in
|
|
158
|
+
* {@link swaggerUIHtml}. Useful for adopters who want corporate
|
|
159
|
+
* branding, a custom theme, or to swap in a third-party UI bundle
|
|
160
|
+
* (Stoplight Elements, RapiDoc, Scalar).
|
|
161
|
+
*
|
|
162
|
+
* @example
|
|
163
|
+
* ```ts
|
|
164
|
+
* SwaggerAdapter({
|
|
165
|
+
* renderSwaggerUI: (specUrl, title) => myBrandedHtml(specUrl, title),
|
|
166
|
+
* })
|
|
167
|
+
* ```
|
|
168
|
+
*/
|
|
169
|
+
renderSwaggerUI?: UIRenderer;
|
|
170
|
+
/**
|
|
171
|
+
* Override the ReDoc HTML renderer. Defaults to the built-in
|
|
172
|
+
* {@link redocHtml}. Same shape as {@link renderSwaggerUI}.
|
|
173
|
+
*/
|
|
174
|
+
renderReDoc?: UIRenderer;
|
|
123
175
|
}
|
|
124
176
|
/**
|
|
125
177
|
* Swagger adapter — auto-generates OpenAPI spec from decorators and serves docs.
|
|
@@ -168,5 +220,5 @@ declare function swaggerUIHtml(specUrl: string, title?: string, assetsPath?: str
|
|
|
168
220
|
*/
|
|
169
221
|
declare function redocHtml(specUrl: string, title?: string): string;
|
|
170
222
|
//#endregion
|
|
171
|
-
export { ApiBearerAuth, ApiExclude, ApiOperation, type ApiOperationOptions, ApiResponse, type ApiResponseOptions, ApiTags, type OpenAPIInfo, type SchemaParser, SwaggerAdapter, type SwaggerAdapterOptions, type SwaggerOptions, buildOpenAPISpec, clearRegisteredRoutes, redocHtml, registerControllerForDocs, swaggerUIHtml, zodSchemaParser };
|
|
223
|
+
export { ApiBearerAuth, ApiExclude, ApiOperation, type ApiOperationOptions, ApiResponse, type ApiResponseOptions, ApiTags, type OpenAPIInfo, type SchemaParser, SwaggerAdapter, type SwaggerAdapterOptions, type SwaggerOptions, type UIRenderer, buildOpenAPISpec, clearRegisteredRoutes, redocHtml, registerControllerForDocs, swaggerUIHtml, zodSchemaParser };
|
|
172
224
|
//# sourceMappingURL=index.d.mts.map
|
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":";;;;;;;AAqBA;;;;;;;;;;;;AAsBA;;;;;;UAtBiB,YAAA;;WAEN,IAAA;
|
|
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":";;;;;;;AAqBA;;;;;;;;;;;;AAsBA;;;;;;UAtBiB,YAAA;;WAEN,IAAA;ECLyB;;;;EDWlC,QAAA,CAAS,MAAA;ECRT;;;;AAIF;EDWE,YAAA,CAAa,MAAA,YAAkB,MAAA;AAAA;;;;;cAOpB,eAAA,EAAiB,YAAA;;;UCzBb,mBAAA;EACf,OAAA;EACA,WAAA;EACA,WAAA;EACA,UAAA;AAAA;AAAA,UAGe,kBAAA;EACf,MAAA;EACA,WAAA;EACA,MAAA;EAVkC;EAYlC,IAAA;AAAA;;iBAIc,YAAA,CAAa,OAAA,EAAS,mBAAA,GAAsB,eAAA;;iBAO5C,WAAA,CAAY,OAAA,EAAS,kBAAA,GAAqB,eAAA;;iBAY1C,OAAA,CAAA,GAAW,IAAA,aAAiB,cAAA,GAAiB,eAAA;AA5B7D;AAAA,iBAuCgB,aAAA,CAAc,IAAA,YAAsB,cAAA,GAAiB,eAAA;;iBAWrD,UAAA,CAAA,GAAc,cAAA,GAAiB,eAAA;;;UCR9B,WAAA;EACf,KAAA;EACA,OAAA;EACA,WAAA;AAAA;AAAA,UAGe,cAAA;EACf,IAAA,GAAO,OAAA,CAAQ,WAAA;EACf,OAAA;IAAY,GAAA;IAAa,WAAA;EAAA;EACzB,UAAA;EFxCqC;;AAOvC;;;;;;;;ACzBA;;;ECwEE,YAAA,GAAe,YAAA;AAAA;;;;;;ADjEjB;;;;iBC+IgB,yBAAA,CACd,eAAA,OACA,SAAA,UACA,KAAA;;;;;;iBAWc,qBAAA,CAAsB,KAAA;;;;;;;;;AD7ItC;;;iBCmKgB,gBAAA,CAAiB,OAAA,GAAS,cAAA;;;;;AFvL1C;;;;;;;;KGYY,UAAA,IAAc,OAAA,UAAiB,KAAA,WAAgB,UAAA;AAAA,UAE1C,qBAAA,SAA8B,cAAA;EHCR;EGCrC,QAAA;EHMW;EGJX,SAAA;;EAEA,QAAA;EHkBD;EGhBC,QAAA;;;AFzBF;;;EE+BE,aAAA;EF9BA;;;;;;AAMF;;;;;;;EEsCE,eAAA,GAAkB,UAAA;EFjCd;;AAIN;;EEkCE,WAAA,GAAc,UAAA;AAAA;;;;;;AF3BhB;;;;;;;;;AAYA;;;;;;;;;cEyCa,cAAA,EAAc,kBAAA,CAAA,cAAA,CAAA,qBAAA;;;;;;AHzE3B;;;;;;;;iBIAgB,aAAA,CAAc,OAAA,UAAiB,KAAA,WAAoB,UAAA;;;;AJsBnE;;;;iBIyCgB,SAAA,CAAU,OAAA,UAAiB,KAAA"}
|
package/dist/index.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @forinda/kickjs-swagger v4.
|
|
2
|
+
* @forinda/kickjs-swagger v4.2.0
|
|
3
3
|
*
|
|
4
4
|
* Copyright (c) Felix Orinda
|
|
5
5
|
*
|
|
@@ -29,12 +29,18 @@ const zodSchemaParser = {
|
|
|
29
29
|
};
|
|
30
30
|
//#endregion
|
|
31
31
|
//#region src/decorators.ts
|
|
32
|
+
/**
|
|
33
|
+
* String metadata keys for the swagger decorators. Follows the §22
|
|
34
|
+
* v4 'kick:area:thing' convention — survives JSON serialisation,
|
|
35
|
+
* addressable by literal from cross-package consumers, visible in
|
|
36
|
+
* DevTools snapshots.
|
|
37
|
+
*/
|
|
32
38
|
const SWAGGER_KEYS = {
|
|
33
|
-
OPERATION:
|
|
34
|
-
RESPONSES:
|
|
35
|
-
TAGS:
|
|
36
|
-
BEARER_AUTH:
|
|
37
|
-
EXCLUDE:
|
|
39
|
+
OPERATION: "kick:swagger:operation",
|
|
40
|
+
RESPONSES: "kick:swagger:responses",
|
|
41
|
+
TAGS: "kick:swagger:tags",
|
|
42
|
+
BEARER_AUTH: "kick:swagger:bearer",
|
|
43
|
+
EXCLUDE: "kick:swagger:exclude"
|
|
38
44
|
};
|
|
39
45
|
/** Attach operation metadata to a route handler */
|
|
40
46
|
function ApiOperation(options) {
|
|
@@ -71,38 +77,151 @@ function ApiExclude() {
|
|
|
71
77
|
}
|
|
72
78
|
//#endregion
|
|
73
79
|
//#region src/openapi-builder.ts
|
|
80
|
+
const log$1 = Logger.for("SwaggerSpec");
|
|
81
|
+
/** HTTP methods that DO carry a request body in OpenAPI 3. */
|
|
82
|
+
const BODY_METHODS = new Set([
|
|
83
|
+
"post",
|
|
84
|
+
"put",
|
|
85
|
+
"patch"
|
|
86
|
+
]);
|
|
87
|
+
/**
|
|
88
|
+
* One-time warning per (controller, handler) pair so a single
|
|
89
|
+
* misconfigured route doesn't spam the boot log on every spec rebuild.
|
|
90
|
+
*/
|
|
91
|
+
const warnedBodyOnReadMethod = /* @__PURE__ */ new Set();
|
|
92
|
+
/**
|
|
93
|
+
* Express path-to-regexp param-name rule:
|
|
94
|
+
* `[A-Za-z_][A-Za-z0-9_]*` (identifier-like; digits allowed after the
|
|
95
|
+
* first char). Used in both directions — discovering params via
|
|
96
|
+
* `match` and rewriting Express's `:name` to OpenAPI's `{name}` via
|
|
97
|
+
* `replace`. Hyphens are NOT included because path-to-regexp uses
|
|
98
|
+
* them as separators in patterns like `/:foo-:bar`.
|
|
99
|
+
*/
|
|
100
|
+
const EXPRESS_PARAM_RE = /:([A-Za-z_][A-Za-z0-9_]*)/g;
|
|
101
|
+
const AUTH_KEY_AUTHENTICATED = "kick:auth:authenticated";
|
|
102
|
+
const AUTH_KEY_PUBLIC = "kick:auth:public";
|
|
74
103
|
const R = Reflect;
|
|
75
104
|
function getAuthMeta(key, target, propertyKey) {
|
|
76
|
-
if (typeof R.
|
|
105
|
+
if (typeof R.getMetadata !== "function") return void 0;
|
|
77
106
|
const proto = target.prototype ?? target;
|
|
78
|
-
|
|
79
|
-
if (!sym) return void 0;
|
|
80
|
-
return propertyKey ? R.getMetadata(sym, proto, propertyKey) : R.getMetadata(sym, target);
|
|
107
|
+
return propertyKey ? R.getMetadata(key, proto, propertyKey) : R.getMetadata(key, target);
|
|
81
108
|
}
|
|
82
109
|
function isAuthAuthenticated(controllerClass, handlerName) {
|
|
83
110
|
if (handlerName) {
|
|
84
|
-
const val = getAuthMeta(
|
|
111
|
+
const val = getAuthMeta(AUTH_KEY_AUTHENTICATED, controllerClass, handlerName);
|
|
85
112
|
if (val !== void 0) return !!val;
|
|
86
113
|
}
|
|
87
|
-
return !!getAuthMeta(
|
|
114
|
+
return !!getAuthMeta(AUTH_KEY_AUTHENTICATED, controllerClass);
|
|
88
115
|
}
|
|
89
116
|
function isAuthPublic(controllerClass, handlerName) {
|
|
90
|
-
return !!getAuthMeta(
|
|
117
|
+
return !!getAuthMeta(AUTH_KEY_PUBLIC, controllerClass, handlerName);
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Default route bag used when callers don't pass a config-scoped key.
|
|
121
|
+
* Kept for back-compat with code that imports `registerControllerForDocs`
|
|
122
|
+
* directly without going through SwaggerAdapter — those callers see the
|
|
123
|
+
* legacy "global single list" behaviour.
|
|
124
|
+
*/
|
|
125
|
+
const DEFAULT_SCOPE = Symbol("kick:swagger:default-scope");
|
|
126
|
+
/**
|
|
127
|
+
* Per-adapter route storage. The adapter's `build` closure passes its
|
|
128
|
+
* config object as the scope key so two SwaggerAdapter instances in
|
|
129
|
+
* the same process (test harnesses, multi-tenant pre-fork) keep
|
|
130
|
+
* independent route lists. Without this, two bootstraps in one process
|
|
131
|
+
* cross-contaminate each other's specs.
|
|
132
|
+
*/
|
|
133
|
+
const routesByScope = /* @__PURE__ */ new Map();
|
|
134
|
+
routesByScope.set(DEFAULT_SCOPE, []);
|
|
135
|
+
function getScopeBag(scope) {
|
|
136
|
+
const key = scope ?? DEFAULT_SCOPE;
|
|
137
|
+
let bag = routesByScope.get(key);
|
|
138
|
+
if (!bag) {
|
|
139
|
+
bag = [];
|
|
140
|
+
routesByScope.set(key, bag);
|
|
141
|
+
}
|
|
142
|
+
return bag;
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* Memoised spec — built lazily on the first {@link buildOpenAPISpec}
|
|
146
|
+
* call after a registration change. Re-issued without rebuild on every
|
|
147
|
+
* subsequent `/openapi.json` request until `clearRegisteredRoutes` or
|
|
148
|
+
* `registerControllerForDocs` invalidates it.
|
|
149
|
+
*
|
|
150
|
+
* Keyed by reference equality on the options object so two adapters
|
|
151
|
+
* with different `info.title` don't return each other's cached spec.
|
|
152
|
+
* Application keeps the SwaggerAdapter config alive for the process
|
|
153
|
+
* lifetime, so this is effectively a per-adapter memo cache. WeakMap
|
|
154
|
+
* keeps the entries collectable when an adapter is disposed.
|
|
155
|
+
*
|
|
156
|
+
* `cacheKeys` is the iteration handle (WeakMap doesn't expose one) so
|
|
157
|
+
* we can flush every cached spec on registration change without
|
|
158
|
+
* tracking adapters individually.
|
|
159
|
+
*/
|
|
160
|
+
const specCache = /* @__PURE__ */ new WeakMap();
|
|
161
|
+
const cacheKeys = /* @__PURE__ */ new Set();
|
|
162
|
+
function invalidateSpecCache(scope) {
|
|
163
|
+
if (scope && typeof scope === "object") {
|
|
164
|
+
if (cacheKeys.has(scope)) {
|
|
165
|
+
specCache.delete(scope);
|
|
166
|
+
cacheKeys.delete(scope);
|
|
167
|
+
}
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
for (const key of cacheKeys) specCache.delete(key);
|
|
171
|
+
cacheKeys.clear();
|
|
91
172
|
}
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
173
|
+
/**
|
|
174
|
+
* Register a controller for OpenAPI introspection. Called by Application
|
|
175
|
+
* during route mounting via the adapter's onRouteMount hook.
|
|
176
|
+
*
|
|
177
|
+
* The optional `scope` argument keys the registration to a specific
|
|
178
|
+
* adapter instance — pass the adapter's own config object as the key
|
|
179
|
+
* (the SwaggerAdapter does this automatically). Omit for legacy
|
|
180
|
+
* single-list behaviour, which is fine for single-bootstrap apps.
|
|
181
|
+
*/
|
|
182
|
+
function registerControllerForDocs(controllerClass, mountPath, scope) {
|
|
183
|
+
getScopeBag(scope).push({
|
|
96
184
|
controllerClass,
|
|
97
185
|
mountPath
|
|
98
186
|
});
|
|
187
|
+
invalidateSpecCache(scope);
|
|
99
188
|
}
|
|
100
|
-
/**
|
|
101
|
-
|
|
102
|
-
|
|
189
|
+
/**
|
|
190
|
+
* Clear registered routes — supports HMR rebuilds. Pass the adapter's
|
|
191
|
+
* config object to clear only that adapter's routes; omit to clear
|
|
192
|
+
* every scope (legacy/global behaviour).
|
|
193
|
+
*/
|
|
194
|
+
function clearRegisteredRoutes(scope) {
|
|
195
|
+
if (scope && typeof scope === "object") {
|
|
196
|
+
routesByScope.delete(scope);
|
|
197
|
+
invalidateSpecCache(scope);
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
routesByScope.clear();
|
|
201
|
+
routesByScope.set(DEFAULT_SCOPE, []);
|
|
202
|
+
invalidateSpecCache();
|
|
103
203
|
}
|
|
104
|
-
/**
|
|
204
|
+
/**
|
|
205
|
+
* Build a full OpenAPI 3.0.3 spec from registered controllers and
|
|
206
|
+
* their decorators.
|
|
207
|
+
*
|
|
208
|
+
* Memoised — the first call for a given `options` object walks every
|
|
209
|
+
* controller (~80–150ms for a 200-route app); subsequent calls return
|
|
210
|
+
* the cached spec until {@link clearRegisteredRoutes} or
|
|
211
|
+
* {@link registerControllerForDocs} invalidate. This matters because
|
|
212
|
+
* Swagger UI re-fetches `/openapi.json` on every navigation; before
|
|
213
|
+
* the cache, every fetch re-walked the entire controller graph.
|
|
214
|
+
*/
|
|
105
215
|
function buildOpenAPISpec(options = {}) {
|
|
216
|
+
const cacheKey = options;
|
|
217
|
+
const cached = specCache.get(cacheKey);
|
|
218
|
+
if (cached !== void 0) return cached;
|
|
219
|
+
const built = buildOpenAPISpecUncached(options);
|
|
220
|
+
specCache.set(cacheKey, built);
|
|
221
|
+
cacheKeys.add(cacheKey);
|
|
222
|
+
return built;
|
|
223
|
+
}
|
|
224
|
+
function buildOpenAPISpecUncached(options = {}) {
|
|
106
225
|
const parser = options.schemaParser ?? zodSchemaParser;
|
|
107
226
|
/** Convert a validation schema to JSON Schema using the configured parser */
|
|
108
227
|
const toJsonSchema = (schema) => {
|
|
@@ -120,16 +239,21 @@ function buildOpenAPISpec(options = {}) {
|
|
|
120
239
|
* If the schema has a title/label, use that as the name. Otherwise generate one.
|
|
121
240
|
*/
|
|
122
241
|
const registerSchema = (jsonSchema, hint) => {
|
|
123
|
-
let
|
|
124
|
-
if (!
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
242
|
+
let baseName = jsonSchema.title || jsonSchema.label || hint || "";
|
|
243
|
+
if (!baseName) baseName = `Schema${++schemaCounter}`;
|
|
244
|
+
baseName = baseName.replace(/[^a-zA-Z0-9]/g, "");
|
|
245
|
+
const clean = { ...jsonSchema };
|
|
246
|
+
delete clean.title;
|
|
247
|
+
delete clean.label;
|
|
248
|
+
delete clean.$schema;
|
|
249
|
+
const cleanJson = JSON.stringify(clean);
|
|
250
|
+
let name = baseName;
|
|
251
|
+
let suffix = 2;
|
|
252
|
+
while (componentSchemas[name]) {
|
|
253
|
+
if (JSON.stringify(componentSchemas[name]) === cleanJson) return { $ref: `#/components/schemas/${name}` };
|
|
254
|
+
name = `${baseName}_${suffix++}`;
|
|
132
255
|
}
|
|
256
|
+
componentSchemas[name] = clean;
|
|
133
257
|
return { $ref: `#/components/schemas/${name}` };
|
|
134
258
|
};
|
|
135
259
|
const spec = {
|
|
@@ -161,15 +285,34 @@ function buildOpenAPISpec(options = {}) {
|
|
|
161
285
|
}
|
|
162
286
|
const allTags = /* @__PURE__ */ new Set();
|
|
163
287
|
const securitySchemes = {};
|
|
164
|
-
|
|
288
|
+
const scopedRoutes = getScopeBag(options);
|
|
289
|
+
const defaultRoutes = options ? getScopeBag(DEFAULT_SCOPE) : [];
|
|
290
|
+
const routesToWalk = scopedRoutes.length > 0 ? scopedRoutes : defaultRoutes;
|
|
291
|
+
for (const { controllerClass, mountPath } of routesToWalk) {
|
|
165
292
|
if (hasClassMeta(SWAGGER_KEYS.EXCLUDE, controllerClass)) continue;
|
|
166
293
|
const routes = getClassMeta(METADATA.ROUTES, controllerClass, []);
|
|
167
294
|
const classTags = getClassMeta(SWAGGER_KEYS.TAGS, controllerClass, []);
|
|
168
295
|
const classAuth = getClassMetaOrUndefined(SWAGGER_KEYS.BEARER_AUTH, controllerClass);
|
|
169
|
-
for (const route of routes) {
|
|
170
|
-
|
|
296
|
+
for (const route of routes) try {
|
|
297
|
+
emitRouteOperation(route);
|
|
298
|
+
} catch (err) {
|
|
299
|
+
let openApiPath;
|
|
300
|
+
try {
|
|
301
|
+
openApiPath = joinPaths(mountPath, route.path).replace(EXPRESS_PARAM_RE, "{$1}");
|
|
302
|
+
} catch {
|
|
303
|
+
openApiPath = `${mountPath}/__spec_error__`;
|
|
304
|
+
}
|
|
305
|
+
const method = typeof route.method === "string" ? route.method.toLowerCase() : "get";
|
|
306
|
+
if (!spec.paths[openApiPath]) spec.paths[openApiPath] = {};
|
|
307
|
+
spec.paths[openApiPath][method] = {
|
|
308
|
+
summary: `⚠ spec generation failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
309
|
+
responses: { default: { description: "Spec generation failed for this operation." } }
|
|
310
|
+
};
|
|
311
|
+
}
|
|
312
|
+
function emitRouteOperation(route) {
|
|
313
|
+
if (getMethodMetaOrUndefined(SWAGGER_KEYS.EXCLUDE, controllerClass, route.handlerName)) return;
|
|
171
314
|
const fullPath = joinPaths(mountPath, route.path);
|
|
172
|
-
const openApiPath = fullPath.replace(
|
|
315
|
+
const openApiPath = fullPath.replace(EXPRESS_PARAM_RE, "{$1}");
|
|
173
316
|
const method = route.method.toLowerCase();
|
|
174
317
|
const operation = getMethodMeta(SWAGGER_KEYS.OPERATION, controllerClass, route.handlerName, {});
|
|
175
318
|
const responses = getMethodMeta(SWAGGER_KEYS.RESPONSES, controllerClass, route.handlerName, []);
|
|
@@ -183,10 +326,10 @@ function buildOpenAPISpec(options = {}) {
|
|
|
183
326
|
...operation.description ? { description: operation.description } : {},
|
|
184
327
|
...operation.operationId ? { operationId: operation.operationId } : {},
|
|
185
328
|
...operation.deprecated ? { deprecated: true } : {},
|
|
186
|
-
parameters: [],
|
|
187
329
|
responses: {}
|
|
188
330
|
};
|
|
189
|
-
const
|
|
331
|
+
const parameters = [];
|
|
332
|
+
const paramMatches = fullPath.match(EXPRESS_PARAM_RE) || [];
|
|
190
333
|
for (const match of paramMatches) {
|
|
191
334
|
const paramName = match.slice(1);
|
|
192
335
|
let schema = { type: "string" };
|
|
@@ -197,7 +340,7 @@ function buildOpenAPISpec(options = {}) {
|
|
|
197
340
|
if (props[paramName]) schema = props[paramName];
|
|
198
341
|
}
|
|
199
342
|
}
|
|
200
|
-
|
|
343
|
+
parameters.push({
|
|
201
344
|
name: paramName,
|
|
202
345
|
in: "path",
|
|
203
346
|
required: true,
|
|
@@ -208,7 +351,7 @@ function buildOpenAPISpec(options = {}) {
|
|
|
208
351
|
const jsonSchema = toJsonSchema(route.validation.query);
|
|
209
352
|
if (jsonSchema?.properties && typeof jsonSchema.properties === "object") {
|
|
210
353
|
const required = Array.isArray(jsonSchema.required) ? jsonSchema.required : [];
|
|
211
|
-
for (const [name, propSchema] of Object.entries(jsonSchema.properties))
|
|
354
|
+
for (const [name, propSchema] of Object.entries(jsonSchema.properties)) parameters.push({
|
|
212
355
|
name,
|
|
213
356
|
in: "query",
|
|
214
357
|
required: required.includes(name),
|
|
@@ -218,7 +361,7 @@ function buildOpenAPISpec(options = {}) {
|
|
|
218
361
|
}
|
|
219
362
|
const queryParamsConfig = getMethodMetaOrUndefined(METADATA.QUERY_PARAMS, controllerClass, route.handlerName);
|
|
220
363
|
if (queryParamsConfig) {
|
|
221
|
-
if (queryParamsConfig.filterable?.length)
|
|
364
|
+
if (queryParamsConfig.filterable?.length) parameters.push({
|
|
222
365
|
name: "filter",
|
|
223
366
|
in: "query",
|
|
224
367
|
required: false,
|
|
@@ -230,7 +373,7 @@ function buildOpenAPISpec(options = {}) {
|
|
|
230
373
|
style: "form",
|
|
231
374
|
explode: true
|
|
232
375
|
});
|
|
233
|
-
if (queryParamsConfig.sortable?.length)
|
|
376
|
+
if (queryParamsConfig.sortable?.length) parameters.push({
|
|
234
377
|
name: "sort",
|
|
235
378
|
in: "query",
|
|
236
379
|
required: false,
|
|
@@ -242,14 +385,14 @@ function buildOpenAPISpec(options = {}) {
|
|
|
242
385
|
style: "form",
|
|
243
386
|
explode: true
|
|
244
387
|
});
|
|
245
|
-
if (queryParamsConfig.searchable?.length)
|
|
388
|
+
if (queryParamsConfig.searchable?.length) parameters.push({
|
|
246
389
|
name: "q",
|
|
247
390
|
in: "query",
|
|
248
391
|
required: false,
|
|
249
392
|
description: `Search across: ${queryParamsConfig.searchable.join(", ")}`,
|
|
250
393
|
schema: { type: "string" }
|
|
251
394
|
});
|
|
252
|
-
|
|
395
|
+
parameters.push({
|
|
253
396
|
name: "page",
|
|
254
397
|
in: "query",
|
|
255
398
|
required: false,
|
|
@@ -272,12 +415,8 @@ function buildOpenAPISpec(options = {}) {
|
|
|
272
415
|
}
|
|
273
416
|
});
|
|
274
417
|
}
|
|
275
|
-
if (
|
|
276
|
-
if (route.validation?.body
|
|
277
|
-
"post",
|
|
278
|
-
"put",
|
|
279
|
-
"patch"
|
|
280
|
-
].includes(method)) {
|
|
418
|
+
if (parameters.length > 0) op.parameters = parameters;
|
|
419
|
+
if (route.validation?.body) if (BODY_METHODS.has(method)) {
|
|
281
420
|
const bodySchema = toJsonSchema(route.validation.body);
|
|
282
421
|
if (bodySchema) {
|
|
283
422
|
const ref = registerSchema(bodySchema, route.validation.name || `${route.handlerName}Body`);
|
|
@@ -286,6 +425,12 @@ function buildOpenAPISpec(options = {}) {
|
|
|
286
425
|
content: { "application/json": { schema: ref } }
|
|
287
426
|
};
|
|
288
427
|
}
|
|
428
|
+
} else {
|
|
429
|
+
const warnKey = `${controllerClass.name}.${route.handlerName}`;
|
|
430
|
+
if (!warnedBodyOnReadMethod.has(warnKey)) {
|
|
431
|
+
warnedBodyOnReadMethod.add(warnKey);
|
|
432
|
+
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.`);
|
|
433
|
+
}
|
|
289
434
|
}
|
|
290
435
|
const fileUpload = getMethodMetaOrUndefined(METADATA.FILE_UPLOAD, controllerClass, route.handlerName);
|
|
291
436
|
if (fileUpload) {
|
|
@@ -310,15 +455,16 @@ function buildOpenAPISpec(options = {}) {
|
|
|
310
455
|
} } }
|
|
311
456
|
};
|
|
312
457
|
}
|
|
313
|
-
if (responses.length > 0) for (const resp of responses)
|
|
314
|
-
description: resp.description || ""
|
|
315
|
-
|
|
316
|
-
const converted =
|
|
458
|
+
if (responses.length > 0) for (const resp of responses) {
|
|
459
|
+
const entry = { description: resp.description || "" };
|
|
460
|
+
if (resp.schema && typeof resp.schema === "object") {
|
|
461
|
+
const converted = toJsonSchema(resp.schema);
|
|
317
462
|
const schemaName = resp.name || `${route.handlerName}Response${resp.status}`;
|
|
318
|
-
const finalSchema = converted ? registerSchema(converted, schemaName) :
|
|
319
|
-
|
|
320
|
-
}
|
|
321
|
-
|
|
463
|
+
const finalSchema = converted ? registerSchema(converted, schemaName) : resp.schema;
|
|
464
|
+
entry.content = { "application/json": { schema: finalSchema } };
|
|
465
|
+
}
|
|
466
|
+
op.responses[String(resp.status)] = entry;
|
|
467
|
+
}
|
|
322
468
|
else {
|
|
323
469
|
const defaultStatus = method === "post" ? "201" : method === "delete" ? "204" : "200";
|
|
324
470
|
op.responses[defaultStatus] = { description: "Successful operation" };
|
|
@@ -472,36 +618,40 @@ const SwaggerAdapter = defineAdapter({
|
|
|
472
618
|
specPath: "/openapi.json"
|
|
473
619
|
},
|
|
474
620
|
build: (config) => {
|
|
475
|
-
const
|
|
621
|
+
const disabled = Boolean(config.disableInProd) && process.env.NODE_ENV === "production";
|
|
622
|
+
const isDisabled = () => disabled;
|
|
623
|
+
const userSuppliedServers = config.servers ? [...config.servers] : [];
|
|
476
624
|
return {
|
|
477
625
|
onRouteMount(controllerClass, mountPath) {
|
|
478
626
|
if (isDisabled()) return;
|
|
479
|
-
registerControllerForDocs(controllerClass, mountPath);
|
|
627
|
+
registerControllerForDocs(controllerClass, mountPath, config);
|
|
480
628
|
},
|
|
481
629
|
afterStart({ server }) {
|
|
482
630
|
if (isDisabled()) return;
|
|
483
631
|
const addr = server?.address?.();
|
|
484
632
|
if (!addr || typeof addr !== "object") return;
|
|
485
633
|
const host = addr.address === "::" || addr.address === "0.0.0.0" ? "localhost" : addr.address;
|
|
486
|
-
|
|
634
|
+
const autoDetected = [];
|
|
635
|
+
autoDetected.push({
|
|
487
636
|
url: `http://${host}:${addr.port}`,
|
|
488
637
|
description: "HTTP server"
|
|
489
|
-
}
|
|
638
|
+
});
|
|
490
639
|
const wsAdapter = config.adapters?.find((a) => a.name === "WsAdapter" && typeof a.getStats === "function");
|
|
491
640
|
if (wsAdapter) {
|
|
492
641
|
const stats = wsAdapter.getStats();
|
|
493
|
-
for (const namespace of Object.keys(stats.namespaces || {}))
|
|
642
|
+
for (const namespace of Object.keys(stats.namespaces || {})) autoDetected.push({
|
|
494
643
|
url: `ws://${host}:${addr.port}${namespace}`,
|
|
495
644
|
description: `WebSocket: ${namespace}`
|
|
496
645
|
});
|
|
497
646
|
}
|
|
647
|
+
config.servers = [...userSuppliedServers, ...autoDetected];
|
|
498
648
|
},
|
|
499
649
|
beforeMount({ app }) {
|
|
500
650
|
if (isDisabled()) {
|
|
501
651
|
log.info("Swagger disabled in production (disableInProd=true)");
|
|
502
652
|
return;
|
|
503
653
|
}
|
|
504
|
-
clearRegisteredRoutes();
|
|
654
|
+
clearRegisteredRoutes(config);
|
|
505
655
|
const docsPath = config.docsPath;
|
|
506
656
|
const redocPath = config.redocPath;
|
|
507
657
|
const specPath = config.specPath;
|
|
@@ -515,6 +665,13 @@ const SwaggerAdapter = defineAdapter({
|
|
|
515
665
|
} catch {
|
|
516
666
|
log.warn("swagger-ui-dist not found — Swagger UI will load from CDN (requires internet).");
|
|
517
667
|
}
|
|
668
|
+
const customSwaggerRenderer = Boolean(config.renderSwaggerUI);
|
|
669
|
+
const customReDocRenderer = Boolean(config.renderReDoc);
|
|
670
|
+
const swaggerOrigins = uiDistAvailable || customSwaggerRenderer ? [] : ["https://unpkg.com"];
|
|
671
|
+
const redocOrigins = customReDocRenderer ? [] : ["https://cdn.redoc.ly", "https://cdn.jsdelivr.net"];
|
|
672
|
+
const scriptOrigins = [...swaggerOrigins, ...redocOrigins];
|
|
673
|
+
const styleOrigins = uiDistAvailable || customSwaggerRenderer ? ["https://fonts.googleapis.com"] : ["https://unpkg.com", "https://fonts.googleapis.com"];
|
|
674
|
+
const imgOrigins = uiDistAvailable || customSwaggerRenderer ? [] : ["https://unpkg.com"];
|
|
518
675
|
docsRouter.use((_req, res, next) => {
|
|
519
676
|
const serverOrigins = /* @__PURE__ */ new Set();
|
|
520
677
|
for (const s of config.servers ?? []) try {
|
|
@@ -532,10 +689,10 @@ const SwaggerAdapter = defineAdapter({
|
|
|
532
689
|
].join(" ");
|
|
533
690
|
res.setHeader("Content-Security-Policy", [
|
|
534
691
|
"default-src 'self'",
|
|
535
|
-
|
|
536
|
-
|
|
692
|
+
`script-src 'self' 'unsafe-inline'${scriptOrigins.length ? " " + scriptOrigins.join(" ") : ""}`,
|
|
693
|
+
`style-src 'self' 'unsafe-inline'${styleOrigins.length ? " " + styleOrigins.join(" ") : ""}`,
|
|
537
694
|
"font-src 'self' https://fonts.gstatic.com",
|
|
538
|
-
|
|
695
|
+
`img-src 'self' data:${imgOrigins.length ? " " + imgOrigins.join(" ") : ""}`,
|
|
539
696
|
`connect-src ${connectSrc}`
|
|
540
697
|
].join("; "));
|
|
541
698
|
next();
|
|
@@ -544,11 +701,13 @@ const SwaggerAdapter = defineAdapter({
|
|
|
544
701
|
const spec = buildOpenAPISpec(config);
|
|
545
702
|
res.json(spec);
|
|
546
703
|
});
|
|
704
|
+
const renderSwagger = config.renderSwaggerUI ?? swaggerUIHtml;
|
|
705
|
+
const renderReDoc = config.renderReDoc ?? redocHtml;
|
|
547
706
|
docsRouter.get(docsPath, (_req, res) => {
|
|
548
|
-
res.type("html").send(
|
|
707
|
+
res.type("html").send(renderSwagger(specPath, config.info?.title, uiDistAvailable ? swaggerAssetsPath : void 0));
|
|
549
708
|
});
|
|
550
709
|
docsRouter.get(redocPath, (_req, res) => {
|
|
551
|
-
res.type("html").send(
|
|
710
|
+
res.type("html").send(renderReDoc(specPath, config.info?.title));
|
|
552
711
|
});
|
|
553
712
|
app.use(docsRouter);
|
|
554
713
|
log.info(`Swagger UI: ${docsPath}`);
|
package/dist/index.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.mjs","names":[],"sources":["../src/schema-parser.ts","../src/decorators.ts","../src/openapi-builder.ts","../src/ui.ts","../src/swagger.adapter.ts"],"sourcesContent":["/**\n * Interface for converting validation library schemas to JSON Schema.\n *\n * KickJS ships with a Zod parser by default. To use a different validation\n * library (Yup, Joi, Valibot, ArkType, etc.), implement this interface and\n * pass it to the 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 for Zod v4+.\n * Uses Zod's built-in `.toJSONSchema()` instance method.\n */\nexport const zodSchemaParser: SchemaParser = {\n name: 'zod',\n\n supports(schema: unknown): boolean {\n return (\n schema != null &&\n typeof schema === 'object' &&\n typeof (schema as any).safeParse === 'function' &&\n typeof (schema as any).toJSONSchema === 'function'\n )\n },\n\n toJsonSchema(schema: unknown): Record<string, unknown> {\n const { $schema: _, ...rest } = (schema as any).toJSONSchema() as Record<string, unknown>\n return rest\n },\n}\n","import { setMethodMeta, setClassMeta, pushMethodMeta } from '@forinda/kickjs'\n\nconst SWAGGER_KEYS = {\n OPERATION: Symbol('kick:swagger:operation'),\n RESPONSES: Symbol('kick:swagger:responses'),\n TAGS: Symbol('kick:swagger:tags'),\n BEARER_AUTH: Symbol('kick:swagger:bearer'),\n EXCLUDE: Symbol('kick:swagger:exclude'),\n}\n\nexport { SWAGGER_KEYS }\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/** 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 METADATA,\n joinPaths,\n type RouteDefinition,\n getClassMeta,\n getClassMetaOrUndefined,\n getMethodMeta,\n getMethodMetaOrUndefined,\n hasClassMeta,\n} from '@forinda/kickjs'\nimport { SWAGGER_KEYS, type ApiOperationOptions, type ApiResponseOptions } from './decorators'\nimport { zodSchemaParser, type SchemaParser } from './schema-parser'\n\n// ── Auth metadata bridge ──────────────────────────────────────────────\n// Check @forinda/kickjs-auth decorators without importing the auth package.\n// Symbols are matched by description to avoid a hard dependency.\n\nconst R = Reflect as any\n\nfunction getAuthMeta(key: string, target: any, propertyKey?: string): any {\n if (typeof R.getMetadataKeys !== 'function') return undefined\n const proto = target.prototype ?? target\n const keys: any[] = propertyKey\n ? R.getMetadataKeys(proto, propertyKey)\n : R.getMetadataKeys(target)\n\n const sym = keys.find((k: any) => typeof k === 'symbol' && k.description === key)\n if (!sym) return undefined\n\n return propertyKey ? R.getMetadata(sym, proto, propertyKey) : R.getMetadata(sym, target)\n}\n\nfunction isAuthAuthenticated(controllerClass: any, handlerName?: string): boolean {\n if (handlerName) {\n const val = getAuthMeta('auth:authenticated', controllerClass, handlerName)\n if (val !== undefined) return !!val\n }\n return !!getAuthMeta('auth:authenticated', controllerClass)\n}\n\nfunction isAuthPublic(controllerClass: any, handlerName: string): boolean {\n return !!getAuthMeta('auth:public', controllerClass, handlerName)\n}\n\nexport interface OpenAPIInfo {\n title: string\n version: string\n description?: string\n}\n\nexport interface SwaggerOptions {\n info?: Partial<OpenAPIInfo>\n servers?: { url: string; description?: string }[]\n bearerAuth?: boolean\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\ninterface RegisteredRoute {\n controllerClass: any\n mountPath: string\n}\n\nconst registeredRoutes: RegisteredRoute[] = []\n\n/** Register a controller for OpenAPI introspection (called by Application during route mounting) */\nexport function registerControllerForDocs(controllerClass: any, mountPath: string): void {\n registeredRoutes.push({ controllerClass, mountPath })\n}\n\n/** Clear all registered routes (for HMR) */\nexport function clearRegisteredRoutes(): void {\n registeredRoutes.length = 0\n}\n\n/** Build a full OpenAPI 3.0.3 spec from registered controllers and their decorators */\nexport function buildOpenAPISpec(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 name = (jsonSchema.title as string) || (jsonSchema.label as string) || hint || ''\n if (!name) {\n name = `Schema${++schemaCounter}`\n }\n // Sanitize name for OpenAPI (remove spaces, special chars)\n name = name.replace(/[^a-zA-Z0-9]/g, '')\n\n // Avoid duplicates — if already registered with same name, reuse\n if (!componentSchemas[name]) {\n const clean = { ...jsonSchema }\n delete clean.title\n delete clean.label\n delete clean.$schema\n componentSchemas[name] = clean\n }\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 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 const securitySchemes: Record<string, any> = {}\n\n for (const { controllerClass, mountPath } of registeredRoutes) {\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 for (const route of routes) {\n // Skip excluded methods\n if (getMethodMetaOrUndefined(SWAGGER_KEYS.EXCLUDE, controllerClass, route.handlerName))\n continue\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}\n const openApiPath = fullPath.replace(/:([a-zA-Z_]+)/g, '{$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\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 parameters: [],\n responses: {},\n }\n\n // Path parameters\n const paramMatches = fullPath.match(/:([a-zA-Z_]+)/g) || []\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 op.parameters.push({\n name: paramName,\n in: 'path',\n required: true,\n schema,\n })\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 op.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 op.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 op.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 op.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 op.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 // Remove empty parameters array\n if (op.parameters.length === 0) delete op.parameters\n\n // Request body\n if (route.validation?.body && ['post', 'put', 'patch'].includes(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 }\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 op.responses[String(resp.status)] = {\n description: resp.description || '',\n ...(resp.schema\n ? (() => {\n const converted =\n typeof resp.schema === 'function' || typeof resp.schema === 'object'\n ? toJsonSchema(resp.schema)\n : null\n const schemaName = resp.name || `${route.handlerName}Response${resp.status}`\n const finalSchema = converted\n ? registerSchema(converted, schemaName)\n : typeof resp.schema === 'object'\n ? resp.schema\n : undefined\n return finalSchema\n ? { content: { 'application/json': { schema: finalSchema } } }\n : {}\n })()\n : {}),\n }\n }\n } else {\n // Auto-generate default responses\n const defaultStatus = method === 'post' ? '201' : method === 'delete' ? '204' : '200'\n op.responses[defaultStatus] = { description: 'Successful operation' }\n\n if (route.validation?.body) {\n op.responses['422'] = { description: 'Validation error' }\n }\n }\n\n // Security — check Swagger @BearerAuth() first, then fall back to\n // @forinda/kickjs-auth decorators (@Authenticated, @Public, @Roles)\n const authName = methodAuth || classAuth\n const isPublicRoute = isAuthPublic(controllerClass, route.handlerName)\n const isAuthRequired =\n authName ||\n isAuthAuthenticated(controllerClass, route.handlerName) ||\n isAuthAuthenticated(controllerClass)\n\n if (!isPublicRoute && isAuthRequired) {\n const schemeName = authName || 'BearerAuth'\n op.security = [{ [schemeName]: [] }]\n securitySchemes[schemeName] = securitySchemes[schemeName] || {\n type: 'http',\n scheme: 'bearer',\n bearerFormat: 'JWT',\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 express, { Router } 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\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\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 const isDisabled = (): boolean =>\n Boolean(config.disableInProd) && process.env.NODE_ENV === 'production'\n\n return {\n onRouteMount(controllerClass, mountPath) {\n if (isDisabled()) return\n registerControllerForDocs(controllerClass, mountPath)\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 // Auto-add HTTP server URL if none configured\n if (!config.servers || config.servers.length === 0) {\n config.servers = [{ url: `http://${host}:${addr.port}`, description: 'HTTP server' }]\n }\n\n // Auto-add WebSocket server URLs from WsAdapter\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 config.servers?.push({\n url: `ws://${host}:${addr.port}${namespace}`,\n description: `WebSocket: ${namespace}`,\n })\n }\n }\n },\n\n beforeMount({ app }) {\n if (isDisabled()) {\n log.info('Swagger disabled in production (disableInProd=true)')\n return\n }\n // Clear previous registrations (supports HMR rebuild)\n clearRegisteredRoutes()\n const docsPath = config.docsPath!\n const redocPath = config.redocPath!\n const specPath = config.specPath!\n let uiDistAvailable = false\n\n const docsRouter = Router()\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 docsRouter.use(swaggerAssetsPath, express.static(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 // Relax CSP for Swagger UI in both local and CDN modes (inline script is used in both)\n docsRouter.use((_req, res, next) => {\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 res.setHeader(\n 'Content-Security-Policy',\n [\n \"default-src 'self'\",\n \"script-src 'self' 'unsafe-inline' https://unpkg.com https://cdn.redoc.ly https://cdn.jsdelivr.net\",\n \"style-src 'self' 'unsafe-inline' https://unpkg.com https://fonts.googleapis.com\",\n \"font-src 'self' https://fonts.gstatic.com\",\n \"img-src 'self' data: https://unpkg.com\",\n `connect-src ${connectSrc}`,\n ].join('; '),\n )\n next()\n })\n\n // Spec endpoint (JSON)\n docsRouter.get(specPath, (_req, res) => {\n const spec = buildOpenAPISpec(config)\n res.json(spec)\n })\n\n // Swagger UI — uses local assets if available, CDN fallback\n docsRouter.get(docsPath, (_req, res) => {\n res\n .type('html')\n .send(\n swaggerUIHtml(\n specPath,\n config.info?.title,\n uiDistAvailable ? swaggerAssetsPath : undefined,\n ),\n )\n })\n\n // ReDoc — still CDN-based (no npm package for standalone bundle)\n docsRouter.get(redocPath, (_req, res) => {\n res.type('html').send(redocHtml(specPath, config.info?.title))\n })\n\n app.use(docsRouter)\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":["/**\n * Interface for converting validation library schemas to JSON Schema.\n *\n * KickJS ships with a Zod parser by default. To use a different validation\n * library (Yup, Joi, Valibot, ArkType, etc.), implement this interface and\n * pass it to the 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 for Zod v4+.\n * Uses Zod's built-in `.toJSONSchema()` instance method.\n */\nexport const zodSchemaParser: SchemaParser = {\n name: 'zod',\n\n supports(schema: unknown): boolean {\n return (\n schema != null &&\n typeof schema === 'object' &&\n typeof (schema as any).safeParse === 'function' &&\n typeof (schema as any).toJSONSchema === 'function'\n )\n },\n\n toJsonSchema(schema: unknown): Record<string, unknown> {\n const { $schema: _, ...rest } = (schema as any).toJSONSchema() as Record<string, unknown>\n return rest\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 EXCLUDE: 'kick:swagger:exclude',\n} as const\n\nexport { SWAGGER_KEYS }\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/** 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 { SWAGGER_KEYS, type ApiOperationOptions, type ApiResponseOptions } 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\n// ── Auth metadata bridge ──────────────────────────────────────────────\n// Check @forinda/kickjs-auth decorators without importing the auth\n// package. Auth's metadata keys (AUTH_META.AUTHENTICATED etc.) are\n// string literals under the §22 'kick:auth:*' convention; we read them\n// here via Reflect.getMetadata directly. The previous Symbol-by-\n// description shim broke silently when either side migrated; string\n// literals are byte-stable across packages.\nconst AUTH_KEY_AUTHENTICATED = 'kick:auth:authenticated'\nconst AUTH_KEY_PUBLIC = 'kick:auth:public'\n\nconst R = Reflect as {\n getMetadata?: (key: string, target: object, propertyKey?: string) => unknown\n}\n\nfunction getAuthMeta(key: string, target: any, propertyKey?: string): unknown {\n if (typeof R.getMetadata !== 'function') return undefined\n const proto = target.prototype ?? target\n return propertyKey ? R.getMetadata(key, proto, propertyKey) : R.getMetadata(key, target)\n}\n\nfunction isAuthAuthenticated(controllerClass: any, handlerName?: string): boolean {\n if (handlerName) {\n const val = getAuthMeta(AUTH_KEY_AUTHENTICATED, controllerClass, handlerName)\n if (val !== undefined) return !!val\n }\n return !!getAuthMeta(AUTH_KEY_AUTHENTICATED, controllerClass)\n}\n\nfunction isAuthPublic(controllerClass: any, handlerName: string): boolean {\n return !!getAuthMeta(AUTH_KEY_PUBLIC, controllerClass, handlerName)\n}\n\nexport interface OpenAPIInfo {\n title: string\n version: string\n description?: string\n}\n\nexport interface SwaggerOptions {\n info?: Partial<OpenAPIInfo>\n servers?: { url: string; description?: string }[]\n bearerAuth?: boolean\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\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 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 const securitySchemes: Record<string, any> = {}\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 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 op.responses[defaultStatus] = { description: 'Successful operation' }\n\n if (route.validation?.body) {\n op.responses['422'] = { description: 'Validation error' }\n }\n }\n\n // Security — check Swagger @BearerAuth() first, then fall back to\n // @forinda/kickjs-auth decorators (@Authenticated, @Public, @Roles)\n const authName = methodAuth || classAuth\n const isPublicRoute = isAuthPublic(controllerClass, route.handlerName)\n const isAuthRequired =\n authName ||\n isAuthAuthenticated(controllerClass, route.handlerName) ||\n isAuthAuthenticated(controllerClass)\n\n if (!isPublicRoute && isAuthRequired) {\n const schemeName = authName || 'BearerAuth'\n op.security = [{ [schemeName]: [] }]\n securitySchemes[schemeName] = securitySchemes[schemeName] || {\n type: 'http',\n scheme: 'bearer',\n bearerFormat: 'JWT',\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 express, { Router } 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({ app }) {\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 const docsRouter = Router()\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 docsRouter.use(swaggerAssetsPath, express.static(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 docsRouter.use((_req, res, next) => {\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 docsRouter.get(specPath, (_req, res) => {\n const spec = buildOpenAPISpec(config)\n res.json(spec)\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 docsRouter.get(docsPath, (_req, res) => {\n res\n .type('html')\n .send(\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 docsRouter.get(redocPath, (_req, res) => {\n res.type('html').send(renderReDoc(specPath, config.info?.title))\n })\n\n app.use(docsRouter)\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": "4.
|
|
3
|
+
"version": "4.2.0",
|
|
4
4
|
"description": "OpenAPI spec generation from decorators, Swagger UI and ReDoc serving for KickJS",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"kickjs",
|
|
@@ -86,7 +86,7 @@
|
|
|
86
86
|
"typescript": "^6.0.3",
|
|
87
87
|
"vitest": "^4.1.5",
|
|
88
88
|
"zod": "^4.3.6",
|
|
89
|
-
"@forinda/kickjs": "4.
|
|
89
|
+
"@forinda/kickjs": "4.2.0"
|
|
90
90
|
},
|
|
91
91
|
"publishConfig": {
|
|
92
92
|
"access": "public"
|