@forinda/kickjs-swagger 4.0.0 → 4.1.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 CHANGED
@@ -97,14 +97,47 @@ interface SwaggerOptions {
97
97
  */
98
98
  schemaParser?: SchemaParser;
99
99
  }
100
- /** Register a controller for OpenAPI introspection (called by Application during route mounting) */
101
- declare function registerControllerForDocs(controllerClass: any, mountPath: string): void;
102
- /** Clear all registered routes (for HMR) */
103
- declare function clearRegisteredRoutes(): void;
104
- /** Build a full OpenAPI 3.0.3 spec from registered controllers and their decorators */
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
@@ -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;ECXyB;;;;EDiBlC,QAAA,CAAS,MAAA;ECdT;;;;AAIF;EDiBE,YAAA,CAAa,MAAA,YAAkB,MAAA;AAAA;;;;;cAOpB,eAAA,EAAiB,YAAA;;;UC/Bb,mBAAA;EACf,OAAA;EACA,WAAA;EACA,WAAA;EACA,UAAA;AAAA;AAAA,UAGe,kBAAA;EACf,MAAA;EACA,WAAA;EACA,MAAA;EDqB4B;ECnB5B,IAAA;AAAA;;iBAIc,YAAA,CAAa,OAAA,EAAS,mBAAA,GAAsB,eAAA;AAhB5D;AAAA,iBAuBgB,WAAA,CAAY,OAAA,EAAS,kBAAA,GAAqB,eAAA;;iBAY1C,OAAA,CAAA,GAAW,IAAA,aAAiB,cAAA,GAAiB,eAAA;;iBAW7C,aAAA,CAAc,IAAA,YAAsB,cAAA,GAAiB,eAAA;;iBAWrD,UAAA,CAAA,GAAc,cAAA,GAAiB,eAAA;;;UCzB9B,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;EFjBqC;;AAOvC;;;;;;;;AC/BA;;;ECuDE,YAAA,GAAe,YAAA;AAAA;;iBAWD,yBAAA,CAA0B,eAAA,OAAsB,SAAA;;iBAKhD,qBAAA,CAAA;;iBAKA,gBAAA,CAAiB,OAAA,GAAS,cAAA;;;UCjEzB,qBAAA,SAA8B,cAAA;;EAE7C,QAAA;EHJ2B;EGM3B,SAAA;EHSqC;EGPrC,QAAA;EHAA;EGEA,QAAA;EHKA;;;;;EGCA,aAAA;AAAA;;;;;;;AFzBF;;;;;;;;;;AAOA;;;;;;;cE4Ca,cAAA,EAAc,kBAAA,CAAA,cAAA,CAAA,qBAAA;;;;;;AH1C3B;;;;;;;;iBIAgB,aAAA,CAAc,OAAA,UAAiB,KAAA,WAAoB,UAAA;;;;AJsBnE;;;;iBIyCgB,SAAA,CAAU,OAAA,UAAiB,KAAA"}
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.0.0
2
+ * @forinda/kickjs-swagger v4.1.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: Symbol("kick:swagger:operation"),
34
- RESPONSES: Symbol("kick:swagger:responses"),
35
- TAGS: Symbol("kick:swagger:tags"),
36
- BEARER_AUTH: Symbol("kick:swagger:bearer"),
37
- EXCLUDE: Symbol("kick:swagger: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.getMetadataKeys !== "function") return void 0;
105
+ if (typeof R.getMetadata !== "function") return void 0;
77
106
  const proto = target.prototype ?? target;
78
- const sym = (propertyKey ? R.getMetadataKeys(proto, propertyKey) : R.getMetadataKeys(target)).find((k) => typeof k === "symbol" && k.description === key);
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("auth:authenticated", controllerClass, handlerName);
111
+ const val = getAuthMeta(AUTH_KEY_AUTHENTICATED, controllerClass, handlerName);
85
112
  if (val !== void 0) return !!val;
86
113
  }
87
- return !!getAuthMeta("auth:authenticated", controllerClass);
114
+ return !!getAuthMeta(AUTH_KEY_AUTHENTICATED, controllerClass);
88
115
  }
89
116
  function isAuthPublic(controllerClass, handlerName) {
90
- return !!getAuthMeta("auth:public", controllerClass, handlerName);
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
- const registeredRoutes = [];
93
- /** Register a controller for OpenAPI introspection (called by Application during route mounting) */
94
- function registerControllerForDocs(controllerClass, mountPath) {
95
- registeredRoutes.push({
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
- /** Clear all registered routes (for HMR) */
101
- function clearRegisteredRoutes() {
102
- registeredRoutes.length = 0;
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
- /** Build a full OpenAPI 3.0.3 spec from registered controllers and their decorators */
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 name = jsonSchema.title || jsonSchema.label || hint || "";
124
- if (!name) name = `Schema${++schemaCounter}`;
125
- name = name.replace(/[^a-zA-Z0-9]/g, "");
126
- if (!componentSchemas[name]) {
127
- const clean = { ...jsonSchema };
128
- delete clean.title;
129
- delete clean.label;
130
- delete clean.$schema;
131
- componentSchemas[name] = clean;
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
- for (const { controllerClass, mountPath } of registeredRoutes) {
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
- if (getMethodMetaOrUndefined(SWAGGER_KEYS.EXCLUDE, controllerClass, route.handlerName)) continue;
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(/:([a-zA-Z_]+)/g, "{$1}");
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 paramMatches = fullPath.match(/:([a-zA-Z_]+)/g) || [];
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
- op.parameters.push({
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)) op.parameters.push({
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) op.parameters.push({
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) op.parameters.push({
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) op.parameters.push({
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
- op.parameters.push({
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 (op.parameters.length === 0) delete op.parameters;
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) op.responses[String(resp.status)] = {
314
- description: resp.description || "",
315
- ...resp.schema ? (() => {
316
- const converted = typeof resp.schema === "function" || typeof resp.schema === "object" ? toJsonSchema(resp.schema) : null;
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) : typeof resp.schema === "object" ? resp.schema : void 0;
319
- return finalSchema ? { content: { "application/json": { schema: finalSchema } } } : {};
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 isDisabled = () => Boolean(config.disableInProd) && process.env.NODE_ENV === "production";
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
- if (!config.servers || config.servers.length === 0) config.servers = [{
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 || {})) config.servers?.push({
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
- "script-src 'self' 'unsafe-inline' https://unpkg.com https://cdn.redoc.ly https://cdn.jsdelivr.net",
536
- "style-src 'self' 'unsafe-inline' https://unpkg.com https://fonts.googleapis.com",
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
- "img-src 'self' data: https://unpkg.com",
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(swaggerUIHtml(specPath, config.info?.title, uiDistAvailable ? swaggerAssetsPath : void 0));
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(redocHtml(specPath, config.info?.title));
710
+ res.type("html").send(renderReDoc(specPath, config.info?.title));
552
711
  });
553
712
  app.use(docsRouter);
554
713
  log.info(`Swagger UI: ${docsPath}`);
@@ -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, '&amp;')\n .replace(/</g, '&lt;')\n .replace(/>/g, '&gt;')\n .replace(/\"/g, '&quot;')\n .replace(/'/g, '&#39;')\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, '&amp;')\n .replace(/</g, '&lt;')\n .replace(/>/g, '&gt;')\n .replace(/\"/g, '&quot;')\n .replace(/'/g, '&#39;')\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.0.0",
3
+ "version": "4.1.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.0.0"
89
+ "@forinda/kickjs": "4.1.0"
90
90
  },
91
91
  "publishConfig": {
92
92
  "access": "public"