@forinda/kickjs-swagger 5.2.0 → 5.3.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
@@ -45,6 +45,17 @@ interface SchemaParser {
45
45
  declare const zodSchemaParser: SchemaParser;
46
46
  //#endregion
47
47
  //#region src/decorators.d.ts
48
+ /**
49
+ * One entry in a route's OpenAPI security requirement list. Maps to
50
+ * the `SecurityRequirementObject` in the OpenAPI 3 spec — `name`
51
+ * references a scheme declared under `components.securitySchemes`,
52
+ * and `scopes` is the optional OAuth2 / OpenID Connect scope list
53
+ * (empty array for non-OAuth schemes).
54
+ */
55
+ interface ApiSecurityRequirement {
56
+ name: string;
57
+ scopes?: string[];
58
+ }
48
59
  interface ApiOperationOptions {
49
60
  summary?: string;
50
61
  description?: string;
@@ -66,6 +77,49 @@ declare function ApiResponse(options: ApiResponseOptions): MethodDecorator;
66
77
  declare function ApiTags(...tags: string[]): ClassDecorator & MethodDecorator;
67
78
  /** Mark endpoint as requiring Bearer token auth */
68
79
  declare function ApiBearerAuth(name?: string): ClassDecorator & MethodDecorator;
80
+ /**
81
+ * Attach one or more OpenAPI security requirements to a class or
82
+ * method. Generic alternative to {@link ApiBearerAuth} — pick this
83
+ * when the scheme isn't bearer-shaped (API key, OAuth2 with scopes,
84
+ * OpenID Connect) or when a route accepts multiple alternative
85
+ * schemes (`SchemeA` OR `SchemeB`).
86
+ *
87
+ * Pass a string for the simple "scheme by name, no scopes" case;
88
+ * pass an object `{ name, scopes }` to attach OAuth/OIDC scopes;
89
+ * pass an array to declare multiple alternatives.
90
+ *
91
+ * The referenced scheme name **must** be declared under
92
+ * `SwaggerOptions.securitySchemes` (or via the implicit BearerAuth
93
+ * scheme generated when `bearerAuth: true` or `@ApiBearerAuth()`
94
+ * is used) — Swagger doesn't synthesize schemes from `@ApiSecurity`
95
+ * names alone.
96
+ *
97
+ * @example
98
+ * ```ts
99
+ * @Controller('/users')
100
+ * @ApiSecurity('BearerAuth') // class-level default
101
+ * class UsersController {
102
+ * @Get('/me')
103
+ * @ApiSecurity({ name: 'OAuth2', scopes: ['users:read'] }) // override
104
+ * me() { ... }
105
+ *
106
+ * @Get('/health')
107
+ * @ApiPublic() // opt out
108
+ * health() { ... }
109
+ * }
110
+ * ```
111
+ */
112
+ declare function ApiSecurity(requirement: string | ApiSecurityRequirement | (string | ApiSecurityRequirement)[]): ClassDecorator & MethodDecorator;
113
+ /**
114
+ * Mark a method as publicly accessible — opts out of any
115
+ * class-level security requirement (set via {@link ApiSecurity}
116
+ * or {@link ApiBearerAuth}) for this one route.
117
+ *
118
+ * Use when the controller is mostly secured but exposes a
119
+ * health-check / login / public-stats endpoint that shouldn't
120
+ * carry the inherited security requirement in the OpenAPI spec.
121
+ */
122
+ declare function ApiPublic(): MethodDecorator;
69
123
  /** Exclude a controller or method from the OpenAPI spec */
70
124
  declare function ApiExclude(): ClassDecorator & MethodDecorator;
71
125
  //#endregion
@@ -75,13 +129,100 @@ interface OpenAPIInfo {
75
129
  version: string;
76
130
  description?: string;
77
131
  }
132
+ /**
133
+ * OpenAPI 3 SecuritySchemeObject — the shape adopters declare under
134
+ * `SwaggerOptions.securitySchemes` and reference by name in
135
+ * `@ApiSecurity('SchemeName')` / `@ApiBearerAuth('SchemeName')` /
136
+ * `securityResolver()`. Loose `type: any` here matches the OpenAPI
137
+ * union (`http` | `apiKey` | `oauth2` | `openIdConnect` | `mutualTLS`)
138
+ * so adopters can declare any valid scheme without a re-export of
139
+ * the full OpenAPI types.
140
+ */
141
+ type OpenAPISecurityScheme = Record<string, any>;
142
+ /**
143
+ * Information passed to {@link SwaggerOptions.securityResolver} for
144
+ * each route. The hook receives the raw controller class + method
145
+ * name so adopters bridging another auth library (kickjs-auth,
146
+ * passport-style decorators, custom annotations) can read whatever
147
+ * metadata they want via `Reflect.getMetadata`.
148
+ */
149
+ interface SecurityResolverContext {
150
+ controllerClass: any;
151
+ handlerName: string;
152
+ }
78
153
  interface SwaggerOptions {
79
154
  info?: Partial<OpenAPIInfo>;
80
155
  servers?: {
81
156
  url: string;
82
157
  description?: string;
83
158
  }[];
159
+ /**
160
+ * Add the `BearerAuth` scheme to `components.securitySchemes` and
161
+ * apply it as a global security requirement on the spec. Routes
162
+ * that opt out via {@link ApiPublic} drop the global requirement.
163
+ */
84
164
  bearerAuth?: boolean;
165
+ /**
166
+ * Custom OpenAPI security schemes. Each entry is a
167
+ * {@link OpenAPISecurityScheme} keyed by scheme name — the same
168
+ * name `@ApiSecurity` / `@ApiBearerAuth` / `securityResolver`
169
+ * reference. Schemes referenced by decorators but not declared
170
+ * here are still emitted with a default `bearer` shape (back-compat
171
+ * with the original `@ApiBearerAuth` flow).
172
+ *
173
+ * @example
174
+ * ```ts
175
+ * SwaggerAdapter({
176
+ * securitySchemes: {
177
+ * ApiKey: { type: 'apiKey', in: 'header', name: 'X-API-Key' },
178
+ * OAuth2: {
179
+ * type: 'oauth2',
180
+ * flows: {
181
+ * authorizationCode: {
182
+ * authorizationUrl: 'https://example.com/oauth/authorize',
183
+ * tokenUrl: 'https://example.com/oauth/token',
184
+ * scopes: { 'users:read': 'Read user profile' },
185
+ * },
186
+ * },
187
+ * },
188
+ * },
189
+ * })
190
+ * ```
191
+ */
192
+ securitySchemes?: Record<string, OpenAPISecurityScheme>;
193
+ /**
194
+ * Optional bridge for adopters who want their own auth library's
195
+ * metadata to drive Swagger's security annotations without
196
+ * reaching for `@ApiSecurity()` on every route.
197
+ *
198
+ * Returns one or more {@link ApiSecurityRequirement} entries (or
199
+ * a bare scheme name string) when the route should be marked
200
+ * secured; returns `null` to mark the route explicitly public
201
+ * (overriding class-level security); returns `undefined` to fall
202
+ * through to the decorator-driven path.
203
+ *
204
+ * The hook runs **after** {@link ApiPublic} (which short-circuits
205
+ * to public) but **before** the decorator-driven `@ApiSecurity` /
206
+ * `@ApiBearerAuth` lookups, so adopters who set both get the
207
+ * resolver's verdict; this matches the historical behaviour of
208
+ * the now-removed implicit `kick:auth:*` bridge.
209
+ *
210
+ * @example
211
+ * ```ts
212
+ * SwaggerAdapter({
213
+ * securityResolver: ({ controllerClass, handlerName }) => {
214
+ * // Bridge `@forinda/kickjs-auth`'s metadata without coupling.
215
+ * const proto = controllerClass.prototype
216
+ * if (Reflect.getMetadata('kick:auth:public', proto, handlerName)) return null
217
+ * const secured =
218
+ * Reflect.getMetadata('kick:auth:authenticated', controllerClass) ||
219
+ * Reflect.getMetadata('kick:auth:authenticated', proto, handlerName)
220
+ * return secured ? 'BearerAuth' : undefined
221
+ * },
222
+ * })
223
+ * ```
224
+ */
225
+ securityResolver?: (ctx: SecurityResolverContext) => string | ApiSecurityRequirement | (string | ApiSecurityRequirement)[] | null | undefined;
85
226
  /**
86
227
  * Pluggable schema parser for converting validation schemas to JSON Schema.
87
228
  * Defaults to `zodSchemaParser` which handles Zod v4+ schemas.
@@ -220,5 +361,5 @@ declare function swaggerUIHtml(specUrl: string, title?: string, assetsPath?: str
220
361
  */
221
362
  declare function redocHtml(specUrl: string, title?: string): string;
222
363
  //#endregion
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 };
364
+ export { ApiBearerAuth, ApiExclude, ApiOperation, type ApiOperationOptions, ApiPublic, ApiResponse, type ApiResponseOptions, ApiSecurity, type ApiSecurityRequirement, ApiTags, type OpenAPIInfo, type OpenAPISecurityScheme, type SchemaParser, type SecurityResolverContext, SwaggerAdapter, type SwaggerAdapterOptions, type SwaggerOptions, type UIRenderer, buildOpenAPISpec, clearRegisteredRoutes, redocHtml, registerControllerForDocs, swaggerUIHtml, zodSchemaParser };
224
365
  //# 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;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"}
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;ECkB4B;;;;EDZrC,QAAA,CAAS,MAAA;ECiByB;;;;;EDVlC,YAAA,CAAa,MAAA,YAAkB,MAAA;AAAA;;;ACiBjC;;cDVa,eAAA,EAAiB,YAAA;;;;;;;;;;UCFb,sBAAA;EACf,IAAA;EACA,MAAA;AAAA;AAAA,UAGe,mBAAA;EACf,OAAA;EACA,WAAA;EACA,WAAA;EACA,UAAA;AAAA;AAAA,UAGe,kBAAA;EACf,MAAA;EACA,WAAA;EACA,MAAA;EAMyE;EAJzE,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;;iBAW7C,aAAA,CAAc,IAAA,YAAsB,cAAA,GAAiB,eAAA;;;;;AAXrE;;;;;;;;;AAWA;;;;;;;;;AA0CA;;;;;;;;;;iBAAgB,WAAA,CACd,WAAA,WAAsB,sBAAA,aAAmC,sBAAA,MACxD,cAAA,GAAiB,eAAA;;;;;;AAyBpB;;;;iBAAgB,SAAA,CAAA,GAAa,eAAA;AAO7B;AAAA,iBAAgB,UAAA,CAAA,GAAc,cAAA,GAAiB,eAAA;;;UChI9B,WAAA;EACf,KAAA;EACA,OAAA;EACA,WAAA;AAAA;;;;;;;;;AFAF;KEYY,qBAAA,GAAwB,MAAA;;;;;;;ADdpC;UCuBiB,uBAAA;EACf,eAAA;EACA,WAAA;AAAA;AAAA,UAGe,cAAA;EACf,IAAA,GAAO,OAAA,CAAQ,WAAA;EACf,OAAA;IAAY,GAAA;IAAa,WAAA;EAAA;EDtBzB;;;;AAIF;ECwBE,UAAA;;;;;;;;;ADfF;;;;;;;;;AAOA;;;;;;;;;AAYA;ECwBE,eAAA,GAAkB,MAAA,SAAe,qBAAA;;;;;;;;ADbnC;;;;;;;;;AA0CA;;;;;;;;;;;;;;;;ECIE,gBAAA,IACE,GAAA,EAAK,uBAAA,cACO,sBAAA,aAAmC,sBAAA;EDqB1B;;;;AAOzB;;;;;;;;AChIA;EAkHE,YAAA,GAAe,YAAA;AAAA;;;;;;;AAnGjB;;;iBA8LgB,yBAAA,CACd,eAAA,OACA,SAAA,UACA,KAAA;;AAxLF;;;;iBAmMgB,qBAAA,CAAsB,KAAA;AA9LtC;;;;;;;;;;;AAAA,iBAoNgB,gBAAA,CAAiB,OAAA,GAAS,cAAA;;;;;AFpQ1C;;;;;;;;KGYY,UAAA,IAAc,OAAA,UAAiB,KAAA,WAAgB,UAAA;AAAA,UAE1C,qBAAA,SAA8B,cAAA;EHCR;EGCrC,QAAA;EHMW;EGJX,SAAA;;EAEA,QAAA;EHkBD;EGhBC,QAAA;;;AFFF;;;EEQE,aAAA;EFNM;AAGR;;;;;;;;;;AAOA;;EEUE,eAAA,GAAkB,UAAA;EFVe;;;;EEejC,WAAA,GAAc,UAAA;AAAA;;AFNhB;;;;;;;;;AAOA;;;;;;;;;AAYA;;;;cEaa,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 v5.2.0
2
+ * @forinda/kickjs-swagger v5.3.0
3
3
  *
4
4
  * Copyright (c) Felix Orinda
5
5
  *
@@ -8,532 +8,18 @@
8
8
  *
9
9
  * @license MIT
10
10
  */
11
- import { createRequire } from "node:module";
12
- import { Logger, METADATA, defineAdapter, getClassMeta, getClassMetaOrUndefined, getMethodMeta, getMethodMetaOrUndefined, hasClassMeta, joinPaths, pushMethodMeta, setClassMeta, setMethodMeta } from "@forinda/kickjs";
13
- import { dirname } from "node:path";
14
- import express, { Router } from "express";
15
- //#region src/schema-parser.ts
16
- /**
17
- * Default schema parser for Zod v4+.
18
- * Uses Zod's built-in `.toJSONSchema()` instance method.
19
- */
20
- const zodSchemaParser = {
21
- name: "zod",
22
- supports(schema) {
23
- return schema != null && typeof schema === "object" && typeof schema.safeParse === "function" && typeof schema.toJSONSchema === "function";
24
- },
25
- toJsonSchema(schema) {
26
- const { $schema: _, ...rest } = schema.toJSONSchema();
27
- return rest;
28
- }
29
- };
30
- //#endregion
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
- */
38
- const SWAGGER_KEYS = {
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"
44
- };
45
- /** Attach operation metadata to a route handler */
46
- function ApiOperation(options) {
47
- return (target, propertyKey) => {
48
- setMethodMeta(SWAGGER_KEYS.OPERATION, options, target.constructor, propertyKey);
49
- };
50
- }
51
- /** Document a response status. Can be stacked multiple times. */
52
- function ApiResponse(options) {
53
- return (target, propertyKey) => {
54
- pushMethodMeta(SWAGGER_KEYS.RESPONSES, target.constructor, propertyKey, options);
55
- };
56
- }
57
- /** Apply OpenAPI tags at class or method level */
58
- function ApiTags(...tags) {
59
- return (target, propertyKey) => {
60
- if (propertyKey) setMethodMeta(SWAGGER_KEYS.TAGS, tags, target.constructor, propertyKey);
61
- else setClassMeta(SWAGGER_KEYS.TAGS, tags, target);
62
- };
63
- }
64
- /** Mark endpoint as requiring Bearer token auth */
65
- function ApiBearerAuth(name = "BearerAuth") {
66
- return (target, propertyKey) => {
67
- if (propertyKey) setMethodMeta(SWAGGER_KEYS.BEARER_AUTH, name, target.constructor, propertyKey);
68
- else setClassMeta(SWAGGER_KEYS.BEARER_AUTH, name, target);
69
- };
70
- }
71
- /** Exclude a controller or method from the OpenAPI spec */
72
- function ApiExclude() {
73
- return (target, propertyKey) => {
74
- if (propertyKey) setMethodMeta(SWAGGER_KEYS.EXCLUDE, true, target.constructor, propertyKey);
75
- else setClassMeta(SWAGGER_KEYS.EXCLUDE, true, target);
76
- };
77
- }
78
- //#endregion
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";
103
- const R = Reflect;
104
- function getAuthMeta(key, target, propertyKey) {
105
- if (typeof R.getMetadata !== "function") return void 0;
106
- const proto = target.prototype ?? target;
107
- return propertyKey ? R.getMetadata(key, proto, propertyKey) : R.getMetadata(key, target);
108
- }
109
- function isAuthAuthenticated(controllerClass, handlerName) {
110
- if (handlerName) {
111
- const val = getAuthMeta(AUTH_KEY_AUTHENTICATED, controllerClass, handlerName);
112
- if (val !== void 0) return !!val;
113
- }
114
- return !!getAuthMeta(AUTH_KEY_AUTHENTICATED, controllerClass);
115
- }
116
- function isAuthPublic(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();
172
- }
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({
184
- controllerClass,
185
- mountPath
186
- });
187
- invalidateSpecCache(scope);
188
- }
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();
203
- }
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
- */
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 = {}) {
225
- const parser = options.schemaParser ?? zodSchemaParser;
226
- /** Convert a validation schema to JSON Schema using the configured parser */
227
- const toJsonSchema = (schema) => {
228
- try {
229
- if (!parser.supports(schema)) return null;
230
- return parser.toJsonSchema(schema);
231
- } catch {
232
- return null;
233
- }
234
- };
235
- const componentSchemas = {};
236
- let schemaCounter = 0;
237
- /**
238
- * Register a schema in components.schemas and return a $ref pointer.
239
- * If the schema has a title/label, use that as the name. Otherwise generate one.
240
- */
241
- const registerSchema = (jsonSchema, hint) => {
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++}`;
255
- }
256
- componentSchemas[name] = clean;
257
- return { $ref: `#/components/schemas/${name}` };
258
- };
259
- const spec = {
260
- openapi: "3.0.3",
261
- info: {
262
- title: options.info?.title || "API",
263
- version: options.info?.version || "1.0.0",
264
- ...options.info?.description ? { description: options.info.description } : {}
265
- },
266
- paths: {},
267
- components: {
268
- schemas: {},
269
- securitySchemes: {}
270
- },
271
- tags: []
272
- };
273
- if (options.servers) {
274
- const validServers = options.servers.filter((s) => {
275
- if (!s?.url || typeof s.url !== "string") return false;
276
- if (s.url.startsWith("/")) return true;
277
- try {
278
- new URL(s.url);
279
- return true;
280
- } catch {
281
- return false;
282
- }
283
- });
284
- if (validServers.length > 0) spec.servers = validServers;
285
- }
286
- const allTags = /* @__PURE__ */ new Set();
287
- const securitySchemes = {};
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) {
292
- if (hasClassMeta(SWAGGER_KEYS.EXCLUDE, controllerClass)) continue;
293
- const routes = getClassMeta(METADATA.ROUTES, controllerClass, []);
294
- const classTags = getClassMeta(SWAGGER_KEYS.TAGS, controllerClass, []);
295
- const classAuth = getClassMetaOrUndefined(SWAGGER_KEYS.BEARER_AUTH, controllerClass);
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;
314
- const fullPath = joinPaths(mountPath, route.path);
315
- const openApiPath = fullPath.replace(EXPRESS_PARAM_RE, "{$1}");
316
- const method = route.method.toLowerCase();
317
- const operation = getMethodMeta(SWAGGER_KEYS.OPERATION, controllerClass, route.handlerName, {});
318
- const responses = getMethodMeta(SWAGGER_KEYS.RESPONSES, controllerClass, route.handlerName, []);
319
- const methodTags = getMethodMeta(SWAGGER_KEYS.TAGS, controllerClass, route.handlerName, []);
320
- const methodAuth = getMethodMetaOrUndefined(SWAGGER_KEYS.BEARER_AUTH, controllerClass, route.handlerName);
321
- const tags = methodTags.length > 0 ? methodTags : classTags;
322
- tags.forEach((t) => allTags.add(t));
323
- const op = {
324
- ...tags.length > 0 ? { tags } : {},
325
- ...operation.summary ? { summary: operation.summary } : {},
326
- ...operation.description ? { description: operation.description } : {},
327
- ...operation.operationId ? { operationId: operation.operationId } : {},
328
- ...operation.deprecated ? { deprecated: true } : {},
329
- responses: {}
330
- };
331
- const parameters = [];
332
- const paramMatches = fullPath.match(EXPRESS_PARAM_RE) || [];
333
- for (const match of paramMatches) {
334
- const paramName = match.slice(1);
335
- let schema = { type: "string" };
336
- if (route.validation?.params) {
337
- const jsonSchema = toJsonSchema(route.validation.params);
338
- if (jsonSchema?.properties && typeof jsonSchema.properties === "object") {
339
- const props = jsonSchema.properties;
340
- if (props[paramName]) schema = props[paramName];
341
- }
342
- }
343
- parameters.push({
344
- name: paramName,
345
- in: "path",
346
- required: true,
347
- schema
348
- });
349
- }
350
- if (route.validation?.query) {
351
- const jsonSchema = toJsonSchema(route.validation.query);
352
- if (jsonSchema?.properties && typeof jsonSchema.properties === "object") {
353
- const required = Array.isArray(jsonSchema.required) ? jsonSchema.required : [];
354
- for (const [name, propSchema] of Object.entries(jsonSchema.properties)) parameters.push({
355
- name,
356
- in: "query",
357
- required: required.includes(name),
358
- schema: propSchema
359
- });
360
- }
361
- }
362
- const queryParamsConfig = getMethodMetaOrUndefined(METADATA.QUERY_PARAMS, controllerClass, route.handlerName);
363
- if (queryParamsConfig) {
364
- if (queryParamsConfig.filterable?.length) parameters.push({
365
- name: "filter",
366
- in: "query",
367
- required: false,
368
- description: `Filter fields: ${queryParamsConfig.filterable.join(", ")}. Format: \`field:operator:value\`. Operators: eq, neq, gt, gte, lt, lte, contains, starts, ends, in, between`,
369
- schema: {
370
- type: "array",
371
- items: { type: "string" }
372
- },
373
- style: "form",
374
- explode: true
375
- });
376
- if (queryParamsConfig.sortable?.length) parameters.push({
377
- name: "sort",
378
- in: "query",
379
- required: false,
380
- description: `Sort fields: ${queryParamsConfig.sortable.join(", ")}. Format: \`field:asc\` or \`field:desc\``,
381
- schema: {
382
- type: "array",
383
- items: { type: "string" }
384
- },
385
- style: "form",
386
- explode: true
387
- });
388
- if (queryParamsConfig.searchable?.length) parameters.push({
389
- name: "q",
390
- in: "query",
391
- required: false,
392
- description: `Search across: ${queryParamsConfig.searchable.join(", ")}`,
393
- schema: { type: "string" }
394
- });
395
- parameters.push({
396
- name: "page",
397
- in: "query",
398
- required: false,
399
- description: "Page number (default: 1)",
400
- schema: {
401
- type: "integer",
402
- minimum: 1,
403
- default: 1
404
- }
405
- }, {
406
- name: "limit",
407
- in: "query",
408
- required: false,
409
- description: "Items per page (default: 20, max: 100)",
410
- schema: {
411
- type: "integer",
412
- minimum: 1,
413
- maximum: 100,
414
- default: 20
415
- }
416
- });
417
- }
418
- if (parameters.length > 0) op.parameters = parameters;
419
- if (route.validation?.body) if (BODY_METHODS.has(method)) {
420
- const bodySchema = toJsonSchema(route.validation.body);
421
- if (bodySchema) {
422
- const ref = registerSchema(bodySchema, route.validation.name || `${route.handlerName}Body`);
423
- op.requestBody = {
424
- required: true,
425
- content: { "application/json": { schema: ref } }
426
- };
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
- }
434
- }
435
- const fileUpload = getMethodMetaOrUndefined(METADATA.FILE_UPLOAD, controllerClass, route.handlerName);
436
- if (fileUpload) {
437
- const fieldName = fileUpload.fieldName ?? "file";
438
- const properties = {};
439
- if (fileUpload.mode === "array") properties[fieldName] = {
440
- type: "array",
441
- items: {
442
- type: "string",
443
- format: "binary"
444
- }
445
- };
446
- else if (fileUpload.mode !== "none") properties[fieldName] = {
447
- type: "string",
448
- format: "binary"
449
- };
450
- op.requestBody = {
451
- required: true,
452
- content: { "multipart/form-data": { schema: {
453
- type: "object",
454
- properties
455
- } } }
456
- };
457
- }
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);
462
- const schemaName = resp.name || `${route.handlerName}Response${resp.status}`;
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
- }
468
- else {
469
- const defaultStatus = method === "post" ? "201" : method === "delete" ? "204" : "200";
470
- op.responses[defaultStatus] = { description: "Successful operation" };
471
- if (route.validation?.body) op.responses["422"] = { description: "Validation error" };
472
- }
473
- const authName = methodAuth || classAuth;
474
- const isPublicRoute = isAuthPublic(controllerClass, route.handlerName);
475
- const isAuthRequired = authName || isAuthAuthenticated(controllerClass, route.handlerName) || isAuthAuthenticated(controllerClass);
476
- if (!isPublicRoute && isAuthRequired) {
477
- const schemeName = authName || "BearerAuth";
478
- op.security = [{ [schemeName]: [] }];
479
- securitySchemes[schemeName] = securitySchemes[schemeName] || {
480
- type: "http",
481
- scheme: "bearer",
482
- bearerFormat: "JWT"
483
- };
484
- }
485
- if (!spec.paths[openApiPath]) spec.paths[openApiPath] = {};
486
- spec.paths[openApiPath][method] = op;
487
- }
488
- }
489
- spec.tags = Array.from(allTags).map((name) => ({ name }));
490
- spec.components.securitySchemes = securitySchemes;
491
- if (options.bearerAuth) {
492
- if (!securitySchemes.BearerAuth) spec.components.securitySchemes.BearerAuth = {
493
- type: "http",
494
- scheme: "bearer",
495
- bearerFormat: "JWT"
496
- };
497
- spec.security = [{ BearerAuth: [] }];
498
- }
499
- spec.components.schemas = componentSchemas;
500
- if (Object.keys(spec.components.schemas).length === 0) delete spec.components.schemas;
501
- if (Object.keys(spec.components.securitySchemes).length === 0) delete spec.components.securitySchemes;
502
- if (Object.keys(spec.components).length === 0) delete spec.components;
503
- return spec;
504
- }
505
- //#endregion
506
- //#region src/ui.ts
507
- /** Escape a string for safe HTML attribute/content interpolation */
508
- function escapeHtml(str) {
509
- return str.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
510
- }
511
- /**
512
- * Generate Swagger UI HTML using local assets from swagger-ui-dist.
513
- *
514
- * Assets are served from `/_swagger-assets/` by the adapter's Express
515
- * static middleware. Falls back to CDN if the local path is not provided.
516
- * This ensures Swagger UI works fully offline in development.
517
- *
518
- * @param specUrl - Path to the OpenAPI JSON spec (e.g., '/openapi.json')
519
- * @param title - Page title
520
- * @param assetsPath - Base path for local swagger-ui-dist assets (e.g., '/_swagger-assets')
521
- */
522
- function swaggerUIHtml(specUrl, title = "API Docs", assetsPath) {
523
- const safeTitle = escapeHtml(title);
524
- const safeUrl = JSON.stringify(specUrl).replace(/</g, "\\u003c");
525
- return `<!DOCTYPE html>
11
+ import{createRequire}from"node:module";import{Logger,METADATA,defineAdapter,getClassMeta,getClassMetaOrUndefined,getMethodMeta,getMethodMetaOrUndefined,hasClassMeta,joinPaths,pushMethodMeta,setClassMeta,setMethodMeta}from"@forinda/kickjs";import{dirname}from"node:path";import express,{Router}from"express";const zodSchemaParser={name:`zod`,supports(schema){return typeof schema==`object`&&!!schema&&typeof schema.safeParse==`function`&&typeof schema.toJSONSchema==`function`},toJsonSchema(schema){let{$schema:_,...rest}=schema.toJSONSchema();return rest}},SWAGGER_KEYS={OPERATION:`kick:swagger:operation`,RESPONSES:`kick:swagger:responses`,TAGS:`kick:swagger:tags`,BEARER_AUTH:`kick:swagger:bearer`,SECURITY:`kick:swagger:security`,PUBLIC:`kick:swagger:public`,EXCLUDE:`kick:swagger:exclude`};function ApiOperation(options){return(target,propertyKey)=>{setMethodMeta(SWAGGER_KEYS.OPERATION,options,target.constructor,propertyKey)}}function ApiResponse(options){return(target,propertyKey)=>{pushMethodMeta(SWAGGER_KEYS.RESPONSES,target.constructor,propertyKey,options)}}function ApiTags(...tags){return(target,propertyKey)=>{propertyKey?setMethodMeta(SWAGGER_KEYS.TAGS,tags,target.constructor,propertyKey):setClassMeta(SWAGGER_KEYS.TAGS,tags,target)}}function ApiBearerAuth(name=`BearerAuth`){return(target,propertyKey)=>{propertyKey?setMethodMeta(SWAGGER_KEYS.BEARER_AUTH,name,target.constructor,propertyKey):setClassMeta(SWAGGER_KEYS.BEARER_AUTH,name,target)}}function ApiSecurity(requirement){let requirements=(Array.isArray(requirement)?requirement:[requirement]).map(r=>typeof r==`string`?{name:r,scopes:[]}:{scopes:[],...r});return(target,propertyKey)=>{propertyKey?setMethodMeta(SWAGGER_KEYS.SECURITY,requirements,target.constructor,propertyKey):setClassMeta(SWAGGER_KEYS.SECURITY,requirements,target)}}function ApiPublic(){return(target,propertyKey)=>{setMethodMeta(SWAGGER_KEYS.PUBLIC,!0,target.constructor,propertyKey)}}function ApiExclude(){return(target,propertyKey)=>{propertyKey?setMethodMeta(SWAGGER_KEYS.EXCLUDE,!0,target.constructor,propertyKey):setClassMeta(SWAGGER_KEYS.EXCLUDE,!0,target)}}const log$1=Logger.for(`SwaggerSpec`),BODY_METHODS=new Set([`post`,`put`,`patch`]),warnedBodyOnReadMethod=new Set,EXPRESS_PARAM_RE=/:([A-Za-z_][A-Za-z0-9_]*)/g;function normaliseSecurity(raw){return(Array.isArray(raw)?raw:[raw]).map(entry=>typeof entry==`string`?{name:entry,scopes:[]}:{scopes:[],...entry})}const DEFAULT_SCOPE=Symbol(`kick:swagger:default-scope`),routesByScope=new Map;routesByScope.set(DEFAULT_SCOPE,[]);function getScopeBag(scope){let key=scope??DEFAULT_SCOPE,bag=routesByScope.get(key);return bag||(bag=[],routesByScope.set(key,bag)),bag}const specCache=new WeakMap,cacheKeys=new Set;function invalidateSpecCache(scope){if(scope&&typeof scope==`object`){cacheKeys.has(scope)&&(specCache.delete(scope),cacheKeys.delete(scope));return}for(let key of cacheKeys)specCache.delete(key);cacheKeys.clear()}function registerControllerForDocs(controllerClass,mountPath,scope){getScopeBag(scope).push({controllerClass,mountPath}),invalidateSpecCache(scope)}function clearRegisteredRoutes(scope){if(scope&&typeof scope==`object`){routesByScope.delete(scope),invalidateSpecCache(scope);return}routesByScope.clear(),routesByScope.set(DEFAULT_SCOPE,[]),invalidateSpecCache()}function buildOpenAPISpec(options={}){let cacheKey=options,cached=specCache.get(cacheKey);if(cached!==void 0)return cached;let built=buildOpenAPISpecUncached(options);return specCache.set(cacheKey,built),cacheKeys.add(cacheKey),built}function buildOpenAPISpecUncached(options={}){let parser=options.schemaParser??zodSchemaParser,toJsonSchema=schema=>{try{return parser.supports(schema)?parser.toJsonSchema(schema):null}catch{return null}},componentSchemas={},schemaCounter=0,registerSchema=(jsonSchema,hint)=>{let baseName=jsonSchema.title||jsonSchema.label||hint||``;baseName||=`Schema${++schemaCounter}`,baseName=baseName.replace(/[^a-zA-Z0-9]/g,``);let clean={...jsonSchema};delete clean.title,delete clean.label,delete clean.$schema;let cleanJson=JSON.stringify(clean),name=baseName,suffix=2;for(;componentSchemas[name];){if(JSON.stringify(componentSchemas[name])===cleanJson)return{$ref:`#/components/schemas/${name}`};name=`${baseName}_${suffix++}`}return componentSchemas[name]=clean,{$ref:`#/components/schemas/${name}`}},spec={openapi:`3.0.3`,info:{title:options.info?.title||`API`,version:options.info?.version||`1.0.0`,...options.info?.description?{description:options.info.description}:{}},paths:{},components:{schemas:{},securitySchemes:{}},tags:[]};if(options.servers){let validServers=options.servers.filter(s=>{if(!s?.url||typeof s.url!=`string`)return!1;if(s.url.startsWith(`/`))return!0;try{return new URL(s.url),!0}catch{return!1}});validServers.length>0&&(spec.servers=validServers)}let allTags=new Set,securitySchemes={...options.securitySchemes},scopedRoutes=getScopeBag(options),defaultRoutes=options?getScopeBag(DEFAULT_SCOPE):[],routesToWalk=scopedRoutes.length>0?scopedRoutes:defaultRoutes;for(let{controllerClass,mountPath}of routesToWalk){if(hasClassMeta(SWAGGER_KEYS.EXCLUDE,controllerClass))continue;let routes=getClassMeta(METADATA.ROUTES,controllerClass,[]),classTags=getClassMeta(SWAGGER_KEYS.TAGS,controllerClass,[]),classAuth=getClassMetaOrUndefined(SWAGGER_KEYS.BEARER_AUTH,controllerClass),classSecurity=getClassMetaOrUndefined(SWAGGER_KEYS.SECURITY,controllerClass);for(let route of routes)try{emitRouteOperation(route)}catch(err){let openApiPath;try{openApiPath=joinPaths(mountPath,route.path).replace(EXPRESS_PARAM_RE,`{$1}`)}catch{openApiPath=`${mountPath}/__spec_error__`}let method=typeof route.method==`string`?route.method.toLowerCase():`get`;spec.paths[openApiPath]||(spec.paths[openApiPath]={}),spec.paths[openApiPath][method]={summary:`⚠ spec generation failed: ${err instanceof Error?err.message:String(err)}`,responses:{default:{description:`Spec generation failed for this operation.`}}}}function emitRouteOperation(route){if(getMethodMetaOrUndefined(SWAGGER_KEYS.EXCLUDE,controllerClass,route.handlerName))return;let fullPath=joinPaths(mountPath,route.path),openApiPath=fullPath.replace(EXPRESS_PARAM_RE,`{$1}`),method=route.method.toLowerCase(),operation=getMethodMeta(SWAGGER_KEYS.OPERATION,controllerClass,route.handlerName,{}),responses=getMethodMeta(SWAGGER_KEYS.RESPONSES,controllerClass,route.handlerName,[]),methodTags=getMethodMeta(SWAGGER_KEYS.TAGS,controllerClass,route.handlerName,[]),methodAuth=getMethodMetaOrUndefined(SWAGGER_KEYS.BEARER_AUTH,controllerClass,route.handlerName),tags=methodTags.length>0?methodTags:classTags;tags.forEach(t=>allTags.add(t));let op={...tags.length>0?{tags}:{},...operation.summary?{summary:operation.summary}:{},...operation.description?{description:operation.description}:{},...operation.operationId?{operationId:operation.operationId}:{},...operation.deprecated?{deprecated:!0}:{},responses:{}},parameters=[],paramMatches=fullPath.match(EXPRESS_PARAM_RE)||[];for(let match of paramMatches){let paramName=match.slice(1),schema={type:`string`};if(route.validation?.params){let jsonSchema=toJsonSchema(route.validation.params);if(jsonSchema?.properties&&typeof jsonSchema.properties==`object`){let props=jsonSchema.properties;props[paramName]&&(schema=props[paramName])}}parameters.push({name:paramName,in:`path`,required:!0,schema})}if(route.validation?.query){let jsonSchema=toJsonSchema(route.validation.query);if(jsonSchema?.properties&&typeof jsonSchema.properties==`object`){let required=Array.isArray(jsonSchema.required)?jsonSchema.required:[];for(let[name,propSchema]of Object.entries(jsonSchema.properties))parameters.push({name,in:`query`,required:required.includes(name),schema:propSchema})}}let queryParamsConfig=getMethodMetaOrUndefined(METADATA.QUERY_PARAMS,controllerClass,route.handlerName);if(queryParamsConfig&&(queryParamsConfig.filterable?.length&&parameters.push({name:`filter`,in:`query`,required:!1,description:`Filter fields: ${queryParamsConfig.filterable.join(`, `)}. Format: \`field:operator:value\`. Operators: eq, neq, gt, gte, lt, lte, contains, starts, ends, in, between`,schema:{type:`array`,items:{type:`string`}},style:`form`,explode:!0}),queryParamsConfig.sortable?.length&&parameters.push({name:`sort`,in:`query`,required:!1,description:`Sort fields: ${queryParamsConfig.sortable.join(`, `)}. Format: \`field:asc\` or \`field:desc\``,schema:{type:`array`,items:{type:`string`}},style:`form`,explode:!0}),queryParamsConfig.searchable?.length&&parameters.push({name:`q`,in:`query`,required:!1,description:`Search across: ${queryParamsConfig.searchable.join(`, `)}`,schema:{type:`string`}}),parameters.push({name:`page`,in:`query`,required:!1,description:`Page number (default: 1)`,schema:{type:`integer`,minimum:1,default:1}},{name:`limit`,in:`query`,required:!1,description:`Items per page (default: 20, max: 100)`,schema:{type:`integer`,minimum:1,maximum:100,default:20}})),parameters.length>0&&(op.parameters=parameters),route.validation?.body)if(BODY_METHODS.has(method)){let bodySchema=toJsonSchema(route.validation.body);bodySchema&&(op.requestBody={required:!0,content:{"application/json":{schema:registerSchema(bodySchema,route.validation.name||`${route.handlerName}Body`)}}})}else{let warnKey=`${controllerClass.name}.${route.handlerName}`;warnedBodyOnReadMethod.has(warnKey)||(warnedBodyOnReadMethod.add(warnKey),log$1.warn(`body validation on ${method.toUpperCase()} ${fullPath} (${warnKey}) is dropped from the OpenAPI spec — OpenAPI 3 does not allow a request body on ${method.toUpperCase()}. Move the schema to validation.query or change the route method.`))}let fileUpload=getMethodMetaOrUndefined(METADATA.FILE_UPLOAD,controllerClass,route.handlerName);if(fileUpload){let fieldName=fileUpload.fieldName??`file`,properties={};fileUpload.mode===`array`?properties[fieldName]={type:`array`,items:{type:`string`,format:`binary`}}:fileUpload.mode!==`none`&&(properties[fieldName]={type:`string`,format:`binary`}),op.requestBody={required:!0,content:{"multipart/form-data":{schema:{type:`object`,properties}}}}}if(responses.length>0)for(let resp of responses){let entry={description:resp.description||``};if(resp.schema&&typeof resp.schema==`object`){let converted=toJsonSchema(resp.schema),schemaName=resp.name||`${route.handlerName}Response${resp.status}`;entry.content={"application/json":{schema:converted?registerSchema(converted,schemaName):resp.schema}}}op.responses[String(resp.status)]=entry}else{let defaultStatus=method===`post`?`201`:method===`delete`?`204`:`200`;op.responses[defaultStatus]={description:`Successful operation`},route.validation?.body&&(op.responses[422]={description:`Validation error`})}let isPublicMethod=!!getMethodMetaOrUndefined(SWAGGER_KEYS.PUBLIC,controllerClass,route.handlerName),methodSecurity=getMethodMetaOrUndefined(SWAGGER_KEYS.SECURITY,controllerClass,route.handlerName),resolverOutput=isPublicMethod?void 0:options.securityResolver?.({controllerClass,handlerName:route.handlerName}),resolverSecurity=resolverOutput==null||resolverOutput===void 0?void 0:normaliseSecurity(resolverOutput),resolverPublic=resolverOutput===null,requirements,bearerAuthSourced=!1;if(isPublicMethod||resolverPublic?requirements=void 0:resolverSecurity&&resolverSecurity.length>0?requirements=resolverSecurity:methodSecurity&&methodSecurity.length>0?requirements=methodSecurity:methodAuth?(requirements=[{name:methodAuth,scopes:[]}],bearerAuthSourced=!0):classSecurity&&classSecurity.length>0?requirements=classSecurity:classAuth&&(requirements=[{name:classAuth,scopes:[]}],bearerAuthSourced=!0),requirements){op.security=requirements.map(r=>({[r.name]:r.scopes??[]}));for(let r of requirements)securitySchemes[r.name]||(bearerAuthSourced||r.name===`BearerAuth`)&&(securitySchemes[r.name]={type:`http`,scheme:`bearer`,bearerFormat:`JWT`})}spec.paths[openApiPath]||(spec.paths[openApiPath]={}),spec.paths[openApiPath][method]=op}}return spec.tags=Array.from(allTags).map(name=>({name})),spec.components.securitySchemes=securitySchemes,options.bearerAuth&&(securitySchemes.BearerAuth||(spec.components.securitySchemes.BearerAuth={type:`http`,scheme:`bearer`,bearerFormat:`JWT`}),spec.security=[{BearerAuth:[]}]),spec.components.schemas=componentSchemas,Object.keys(spec.components.schemas).length===0&&delete spec.components.schemas,Object.keys(spec.components.securitySchemes).length===0&&delete spec.components.securitySchemes,Object.keys(spec.components).length===0&&delete spec.components,spec}function escapeHtml(str){return str.replace(/&/g,`&amp;`).replace(/</g,`&lt;`).replace(/>/g,`&gt;`).replace(/"/g,`&quot;`).replace(/'/g,`&#39;`)}function swaggerUIHtml(specUrl,title=`API Docs`,assetsPath){let safeTitle=escapeHtml(title),safeUrl=JSON.stringify(specUrl).replace(/</g,`\\u003c`);return`<!DOCTYPE html>
526
12
  <html lang="en">
527
13
  <head>
528
14
  <meta charset="UTF-8">
529
15
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
530
16
  <title>${safeTitle}</title>
531
- <link rel="stylesheet" href="${assetsPath ? `${assetsPath}/swagger-ui.css` : "https://unpkg.com/swagger-ui-dist@5/swagger-ui.css"}">
17
+ <link rel="stylesheet" href="${assetsPath?`${assetsPath}/swagger-ui.css`:`https://unpkg.com/swagger-ui-dist@5/swagger-ui.css`}">
532
18
  </head>
533
19
  <body>
534
20
  <div id="swagger-ui"></div>
535
- <script src="${assetsPath ? `${assetsPath}/swagger-ui-bundle.js` : "https://unpkg.com/swagger-ui-dist@5/swagger-ui-bundle.js"}"><\/script>
536
- <script src="${assetsPath ? `${assetsPath}/swagger-ui-standalone-preset.js` : "https://unpkg.com/swagger-ui-dist@5/swagger-ui-standalone-preset.js"}"><\/script>
21
+ <script src="${assetsPath?`${assetsPath}/swagger-ui-bundle.js`:`https://unpkg.com/swagger-ui-dist@5/swagger-ui-bundle.js`}"><\/script>
22
+ <script src="${assetsPath?`${assetsPath}/swagger-ui-standalone-preset.js`:`https://unpkg.com/swagger-ui-dist@5/swagger-ui-standalone-preset.js`}"><\/script>
537
23
  <script>
538
24
  (function () {
539
25
  var rawUrl = ${safeUrl};
@@ -554,17 +40,7 @@ function swaggerUIHtml(specUrl, title = "API Docs", assetsPath) {
554
40
  })();
555
41
  <\/script>
556
42
  </body>
557
- </html>`;
558
- }
559
- /**
560
- * Generate ReDoc HTML.
561
- *
562
- * ReDoc doesn't publish a standalone npm package suitable for local serving,
563
- * so it still loads from CDN. If offline support for ReDoc is needed,
564
- * vendor the standalone bundle into the package's public/ directory.
565
- */
566
- function redocHtml(specUrl, title = "API Docs") {
567
- return `<!DOCTYPE html>
43
+ </html>`}function redocHtml(specUrl,title=`API Docs`){return`<!DOCTYPE html>
568
44
  <html lang="en">
569
45
  <head>
570
46
  <meta charset="UTF-8">
@@ -575,149 +51,5 @@ function redocHtml(specUrl, title = "API Docs") {
575
51
  <redoc spec-url="${escapeHtml(specUrl)}"></redoc>
576
52
  <script src="https://cdn.redoc.ly/redoc/latest/bundles/redoc.standalone.js"><\/script>
577
53
  </body>
578
- </html>`;
579
- }
580
- //#endregion
581
- //#region src/swagger.adapter.ts
582
- const log = Logger.for("SwaggerAdapter");
583
- /**
584
- * Resolve the absolute path to swagger-ui-dist's static assets.
585
- * Uses createRequire to find it relative to this package (works with pnpm).
586
- */
587
- function getSwaggerUiDistPath() {
588
- return dirname(createRequire(import.meta.url).resolve("swagger-ui-dist/package.json"));
589
- }
590
- /**
591
- * Swagger adapter — auto-generates OpenAPI spec from decorators and serves docs.
592
- *
593
- * Assets are served locally from `swagger-ui-dist` (npm dependency) —
594
- * no CDN required, works fully offline.
595
- *
596
- * @example
597
- * ```ts
598
- * bootstrap({
599
- * modules,
600
- * adapters: [
601
- * SwaggerAdapter({
602
- * info: { title: 'My API', version: '1.0.0' },
603
- * }),
604
- * ],
605
- * })
606
- * ```
607
- *
608
- * Endpoints:
609
- * GET /docs — Swagger UI (local assets, no CDN)
610
- * GET /redoc — ReDoc (CDN — no local package available)
611
- * GET /openapi.json — Raw OpenAPI 3.0.3 spec
612
- */
613
- const SwaggerAdapter = defineAdapter({
614
- name: "SwaggerAdapter",
615
- defaults: {
616
- docsPath: "/docs",
617
- redocPath: "/redoc",
618
- specPath: "/openapi.json"
619
- },
620
- build: (config) => {
621
- const disabled = Boolean(config.disableInProd) && process.env.NODE_ENV === "production";
622
- const isDisabled = () => disabled;
623
- const userSuppliedServers = config.servers ? [...config.servers] : [];
624
- return {
625
- onRouteMount(controllerClass, mountPath) {
626
- if (isDisabled()) return;
627
- registerControllerForDocs(controllerClass, mountPath, config);
628
- },
629
- afterStart({ server }) {
630
- if (isDisabled()) return;
631
- const addr = server?.address?.();
632
- if (!addr || typeof addr !== "object") return;
633
- const host = addr.address === "::" || addr.address === "0.0.0.0" ? "localhost" : addr.address;
634
- const autoDetected = [];
635
- autoDetected.push({
636
- url: `http://${host}:${addr.port}`,
637
- description: "HTTP server"
638
- });
639
- const wsAdapter = config.adapters?.find((a) => a.name === "WsAdapter" && typeof a.getStats === "function");
640
- if (wsAdapter) {
641
- const stats = wsAdapter.getStats();
642
- for (const namespace of Object.keys(stats.namespaces || {})) autoDetected.push({
643
- url: `ws://${host}:${addr.port}${namespace}`,
644
- description: `WebSocket: ${namespace}`
645
- });
646
- }
647
- config.servers = [...userSuppliedServers, ...autoDetected];
648
- },
649
- beforeMount({ app }) {
650
- if (isDisabled()) {
651
- log.info("Swagger disabled in production (disableInProd=true)");
652
- return;
653
- }
654
- clearRegisteredRoutes(config);
655
- const docsPath = config.docsPath;
656
- const redocPath = config.redocPath;
657
- const specPath = config.specPath;
658
- let uiDistAvailable = false;
659
- const docsRouter = Router();
660
- const swaggerAssetsPath = "/_swagger-assets";
661
- try {
662
- const swaggerDistDir = getSwaggerUiDistPath();
663
- docsRouter.use(swaggerAssetsPath, express.static(swaggerDistDir));
664
- uiDistAvailable = true;
665
- } catch {
666
- log.warn("swagger-ui-dist not found — Swagger UI will load from CDN (requires internet).");
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"];
675
- docsRouter.use((_req, res, next) => {
676
- const serverOrigins = /* @__PURE__ */ new Set();
677
- for (const s of config.servers ?? []) try {
678
- serverOrigins.add(new URL(s.url).origin);
679
- } catch {}
680
- const connectSrc = [
681
- "'self'",
682
- "http://localhost:*",
683
- "http://127.0.0.1:*",
684
- "https://localhost:*",
685
- "https://127.0.0.1:*",
686
- "ws://localhost:*",
687
- "ws://127.0.0.1:*",
688
- ...serverOrigins
689
- ].join(" ");
690
- res.setHeader("Content-Security-Policy", [
691
- "default-src 'self'",
692
- `script-src 'self' 'unsafe-inline'${scriptOrigins.length ? " " + scriptOrigins.join(" ") : ""}`,
693
- `style-src 'self' 'unsafe-inline'${styleOrigins.length ? " " + styleOrigins.join(" ") : ""}`,
694
- "font-src 'self' https://fonts.gstatic.com",
695
- `img-src 'self' data:${imgOrigins.length ? " " + imgOrigins.join(" ") : ""}`,
696
- `connect-src ${connectSrc}`
697
- ].join("; "));
698
- next();
699
- });
700
- docsRouter.get(specPath, (_req, res) => {
701
- const spec = buildOpenAPISpec(config);
702
- res.json(spec);
703
- });
704
- const renderSwagger = config.renderSwaggerUI ?? swaggerUIHtml;
705
- const renderReDoc = config.renderReDoc ?? redocHtml;
706
- docsRouter.get(docsPath, (_req, res) => {
707
- res.type("html").send(renderSwagger(specPath, config.info?.title, uiDistAvailable ? swaggerAssetsPath : void 0));
708
- });
709
- docsRouter.get(redocPath, (_req, res) => {
710
- res.type("html").send(renderReDoc(specPath, config.info?.title));
711
- });
712
- app.use(docsRouter);
713
- log.info(`Swagger UI: ${docsPath}`);
714
- log.info(`ReDoc: ${redocPath}`);
715
- log.info(`OpenAPI spec: ${specPath}`);
716
- }
717
- };
718
- }
719
- });
720
- //#endregion
721
- export { ApiBearerAuth, ApiExclude, ApiOperation, ApiResponse, ApiTags, SwaggerAdapter, buildOpenAPISpec, clearRegisteredRoutes, redocHtml, registerControllerForDocs, swaggerUIHtml, zodSchemaParser };
722
-
54
+ </html>`}const log=Logger.for(`SwaggerAdapter`);function getSwaggerUiDistPath(){return dirname(createRequire(import.meta.url).resolve(`swagger-ui-dist/package.json`))}const SwaggerAdapter=defineAdapter({name:`SwaggerAdapter`,defaults:{docsPath:`/docs`,redocPath:`/redoc`,specPath:`/openapi.json`},build:config=>{let disabled=!!config.disableInProd&&process.env.NODE_ENV===`production`,isDisabled=()=>disabled,userSuppliedServers=config.servers?[...config.servers]:[];return{onRouteMount(controllerClass,mountPath){isDisabled()||registerControllerForDocs(controllerClass,mountPath,config)},afterStart({server}){if(isDisabled())return;let addr=server?.address?.();if(!addr||typeof addr!=`object`)return;let host=addr.address===`::`||addr.address===`0.0.0.0`?`localhost`:addr.address,autoDetected=[];autoDetected.push({url:`http://${host}:${addr.port}`,description:`HTTP server`});let wsAdapter=config.adapters?.find(a=>a.name===`WsAdapter`&&typeof a.getStats==`function`);if(wsAdapter){let stats=wsAdapter.getStats();for(let namespace of Object.keys(stats.namespaces||{}))autoDetected.push({url:`ws://${host}:${addr.port}${namespace}`,description:`WebSocket: ${namespace}`})}config.servers=[...userSuppliedServers,...autoDetected]},beforeMount({app}){if(isDisabled()){log.info(`Swagger disabled in production (disableInProd=true)`);return}clearRegisteredRoutes(config);let docsPath=config.docsPath,redocPath=config.redocPath,specPath=config.specPath,uiDistAvailable=!1,docsRouter=Router(),swaggerAssetsPath=`/_swagger-assets`;try{let swaggerDistDir=getSwaggerUiDistPath();docsRouter.use(swaggerAssetsPath,express.static(swaggerDistDir)),uiDistAvailable=!0}catch{log.warn(`swagger-ui-dist not found — Swagger UI will load from CDN (requires internet).`)}let customSwaggerRenderer=!!config.renderSwaggerUI,customReDocRenderer=!!config.renderReDoc,swaggerOrigins=uiDistAvailable||customSwaggerRenderer?[]:[`https://unpkg.com`],redocOrigins=customReDocRenderer?[]:[`https://cdn.redoc.ly`,`https://cdn.jsdelivr.net`],scriptOrigins=[...swaggerOrigins,...redocOrigins],styleOrigins=uiDistAvailable||customSwaggerRenderer?[`https://fonts.googleapis.com`]:[`https://unpkg.com`,`https://fonts.googleapis.com`],imgOrigins=uiDistAvailable||customSwaggerRenderer?[]:[`https://unpkg.com`];docsRouter.use((_req,res,next)=>{let serverOrigins=new Set;for(let s of config.servers??[])try{serverOrigins.add(new URL(s.url).origin)}catch{}let connectSrc=[`'self'`,`http://localhost:*`,`http://127.0.0.1:*`,`https://localhost:*`,`https://127.0.0.1:*`,`ws://localhost:*`,`ws://127.0.0.1:*`,...serverOrigins].join(` `);res.setHeader(`Content-Security-Policy`,[`default-src 'self'`,`script-src 'self' 'unsafe-inline'${scriptOrigins.length?` `+scriptOrigins.join(` `):``}`,`style-src 'self' 'unsafe-inline'${styleOrigins.length?` `+styleOrigins.join(` `):``}`,`font-src 'self' https://fonts.gstatic.com`,`img-src 'self' data:${imgOrigins.length?` `+imgOrigins.join(` `):``}`,`connect-src ${connectSrc}`].join(`; `)),next()}),docsRouter.get(specPath,(_req,res)=>{let spec=buildOpenAPISpec(config);res.json(spec)});let renderSwagger=config.renderSwaggerUI??swaggerUIHtml,renderReDoc=config.renderReDoc??redocHtml;docsRouter.get(docsPath,(_req,res)=>{res.type(`html`).send(renderSwagger(specPath,config.info?.title,uiDistAvailable?swaggerAssetsPath:void 0))}),docsRouter.get(redocPath,(_req,res)=>{res.type(`html`).send(renderReDoc(specPath,config.info?.title))}),app.use(docsRouter),log.info(`Swagger UI: ${docsPath}`),log.info(`ReDoc: ${redocPath}`),log.info(`OpenAPI spec: ${specPath}`)}}}});export{ApiBearerAuth,ApiExclude,ApiOperation,ApiPublic,ApiResponse,ApiSecurity,ApiTags,SwaggerAdapter,buildOpenAPISpec,clearRegisteredRoutes,redocHtml,registerControllerForDocs,swaggerUIHtml,zodSchemaParser};
723
55
  //# sourceMappingURL=index.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.mjs","names":["log"],"sources":["../src/schema-parser.ts","../src/decorators.ts","../src/openapi-builder.ts","../src/ui.ts","../src/swagger.adapter.ts"],"sourcesContent":["/**\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 void new URL(s.url)\n return true\n } catch {\n return false\n }\n })\n if (validServers.length > 0) {\n spec.servers = validServers\n }\n }\n\n const allTags = new Set<string>()\n 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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"}
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 /**\n * Generic security requirement(s) attached to a route. Replaces\n * the implicit `kick:auth:authenticated` cross-package bridge —\n * adopters now declare auth requirements explicitly via\n * `@ApiSecurity()` (single or multi-scheme, with optional OAuth\n * scopes) instead of having Swagger guess from a sibling\n * package's metadata.\n */\n SECURITY: 'kick:swagger:security',\n /**\n * Method-level opt-out from class-level security. Mirrors the\n * intent of `@Public` from auth packages but lives on Swagger's\n * own metadata namespace, so the spec builder doesn't need to\n * know about any specific auth library.\n */\n PUBLIC: 'kick:swagger:public',\n EXCLUDE: 'kick:swagger:exclude',\n} as const\n\nexport { SWAGGER_KEYS }\n\n/**\n * One entry in a route's OpenAPI security requirement list. Maps to\n * the `SecurityRequirementObject` in the OpenAPI 3 spec — `name`\n * references a scheme declared under `components.securitySchemes`,\n * and `scopes` is the optional OAuth2 / OpenID Connect scope list\n * (empty array for non-OAuth schemes).\n */\nexport interface ApiSecurityRequirement {\n name: string\n scopes?: string[]\n}\n\nexport interface ApiOperationOptions {\n summary?: string\n description?: string\n operationId?: string\n deprecated?: boolean\n}\n\nexport interface ApiResponseOptions {\n status: number\n description?: string\n schema?: any\n /** Schema name in components/schemas (e.g., 'UserResponse', 'ErrorBody'). Auto-generated from handler name if omitted. */\n name?: string\n}\n\n/** Attach operation metadata to a route handler */\nexport function ApiOperation(options: ApiOperationOptions): MethodDecorator {\n return (target, propertyKey) => {\n setMethodMeta(SWAGGER_KEYS.OPERATION, options, target.constructor, propertyKey as string)\n }\n}\n\n/** Document a response status. Can be stacked multiple times. */\nexport function ApiResponse(options: ApiResponseOptions): MethodDecorator {\n return (target, propertyKey) => {\n pushMethodMeta<ApiResponseOptions>(\n SWAGGER_KEYS.RESPONSES,\n target.constructor,\n propertyKey as string,\n options,\n )\n }\n}\n\n/** Apply OpenAPI tags at class or method level */\nexport function ApiTags(...tags: string[]): ClassDecorator & MethodDecorator {\n return (target: any, propertyKey?: string | symbol) => {\n if (propertyKey) {\n setMethodMeta(SWAGGER_KEYS.TAGS, tags, target.constructor, propertyKey as string)\n } else {\n setClassMeta(SWAGGER_KEYS.TAGS, tags, target)\n }\n }\n}\n\n/** Mark endpoint as requiring Bearer token auth */\nexport function ApiBearerAuth(name = 'BearerAuth'): ClassDecorator & MethodDecorator {\n return (target: any, propertyKey?: string | symbol) => {\n if (propertyKey) {\n setMethodMeta(SWAGGER_KEYS.BEARER_AUTH, name, target.constructor, propertyKey as string)\n } else {\n setClassMeta(SWAGGER_KEYS.BEARER_AUTH, name, target)\n }\n }\n}\n\n/**\n * Attach one or more OpenAPI security requirements to a class or\n * method. Generic alternative to {@link ApiBearerAuth} — pick this\n * when the scheme isn't bearer-shaped (API key, OAuth2 with scopes,\n * OpenID Connect) or when a route accepts multiple alternative\n * schemes (`SchemeA` OR `SchemeB`).\n *\n * Pass a string for the simple \"scheme by name, no scopes\" case;\n * pass an object `{ name, scopes }` to attach OAuth/OIDC scopes;\n * pass an array to declare multiple alternatives.\n *\n * The referenced scheme name **must** be declared under\n * `SwaggerOptions.securitySchemes` (or via the implicit BearerAuth\n * scheme generated when `bearerAuth: true` or `@ApiBearerAuth()`\n * is used) — Swagger doesn't synthesize schemes from `@ApiSecurity`\n * names alone.\n *\n * @example\n * ```ts\n * @Controller('/users')\n * @ApiSecurity('BearerAuth') // class-level default\n * class UsersController {\n * @Get('/me')\n * @ApiSecurity({ name: 'OAuth2', scopes: ['users:read'] }) // override\n * me() { ... }\n *\n * @Get('/health')\n * @ApiPublic() // opt out\n * health() { ... }\n * }\n * ```\n */\nexport function ApiSecurity(\n requirement: string | ApiSecurityRequirement | (string | ApiSecurityRequirement)[],\n): ClassDecorator & MethodDecorator {\n // Normalise everything to an `ApiSecurityRequirement[]` so the\n // builder reads a single shape. Strings become `{ name, scopes: [] }`.\n const requirements: ApiSecurityRequirement[] = (\n Array.isArray(requirement) ? requirement : [requirement]\n ).map((r) => (typeof r === 'string' ? { name: r, scopes: [] } : { scopes: [], ...r }))\n\n return (target: any, propertyKey?: string | symbol) => {\n if (propertyKey) {\n setMethodMeta(SWAGGER_KEYS.SECURITY, requirements, target.constructor, propertyKey as string)\n } else {\n setClassMeta(SWAGGER_KEYS.SECURITY, requirements, target)\n }\n }\n}\n\n/**\n * Mark a method as publicly accessible — opts out of any\n * class-level security requirement (set via {@link ApiSecurity}\n * or {@link ApiBearerAuth}) for this one route.\n *\n * Use when the controller is mostly secured but exposes a\n * health-check / login / public-stats endpoint that shouldn't\n * carry the inherited security requirement in the OpenAPI spec.\n */\nexport function ApiPublic(): MethodDecorator {\n return (target, propertyKey) => {\n setMethodMeta(SWAGGER_KEYS.PUBLIC, true, target.constructor, propertyKey as string)\n }\n}\n\n/** Exclude a controller or method from the OpenAPI spec */\nexport function ApiExclude(): ClassDecorator & MethodDecorator {\n return (target: any, propertyKey?: string | symbol) => {\n if (propertyKey) {\n setMethodMeta(SWAGGER_KEYS.EXCLUDE, true, target.constructor, propertyKey as string)\n } else {\n setClassMeta(SWAGGER_KEYS.EXCLUDE, true, target)\n }\n }\n}\n","import {\n Logger,\n METADATA,\n joinPaths,\n type RouteDefinition,\n getClassMeta,\n getClassMetaOrUndefined,\n getMethodMeta,\n getMethodMetaOrUndefined,\n hasClassMeta,\n} from '@forinda/kickjs'\nimport {\n SWAGGER_KEYS,\n type ApiOperationOptions,\n type ApiResponseOptions,\n type ApiSecurityRequirement,\n} from './decorators'\nimport { zodSchemaParser, type SchemaParser } from './schema-parser'\n\nconst log = Logger.for('SwaggerSpec')\n\n/** HTTP methods that DO carry a request body in OpenAPI 3. */\nconst BODY_METHODS = new Set(['post', 'put', 'patch'])\n\n/**\n * One-time warning per (controller, handler) pair so a single\n * misconfigured route doesn't spam the boot log on every spec rebuild.\n */\nconst warnedBodyOnReadMethod = new Set<string>()\n\n/**\n * Express path-to-regexp param-name rule:\n * `[A-Za-z_][A-Za-z0-9_]*` (identifier-like; digits allowed after the\n * first char). Used in both directions — discovering params via\n * `match` and rewriting Express's `:name` to OpenAPI's `{name}` via\n * `replace`. Hyphens are NOT included because path-to-regexp uses\n * them as separators in patterns like `/:foo-:bar`.\n */\nconst EXPRESS_PARAM_RE = /:([A-Za-z_][A-Za-z0-9_]*)/g\n\nexport interface OpenAPIInfo {\n title: string\n version: string\n description?: string\n}\n\n/**\n * OpenAPI 3 SecuritySchemeObject — the shape adopters declare under\n * `SwaggerOptions.securitySchemes` and reference by name in\n * `@ApiSecurity('SchemeName')` / `@ApiBearerAuth('SchemeName')` /\n * `securityResolver()`. Loose `type: any` here matches the OpenAPI\n * union (`http` | `apiKey` | `oauth2` | `openIdConnect` | `mutualTLS`)\n * so adopters can declare any valid scheme without a re-export of\n * the full OpenAPI types.\n */\nexport type OpenAPISecurityScheme = Record<string, any>\n\n/**\n * Information passed to {@link SwaggerOptions.securityResolver} for\n * each route. The hook receives the raw controller class + method\n * name so adopters bridging another auth library (kickjs-auth,\n * passport-style decorators, custom annotations) can read whatever\n * metadata they want via `Reflect.getMetadata`.\n */\nexport interface SecurityResolverContext {\n controllerClass: any\n handlerName: string\n}\n\nexport interface SwaggerOptions {\n info?: Partial<OpenAPIInfo>\n servers?: { url: string; description?: string }[]\n /**\n * Add the `BearerAuth` scheme to `components.securitySchemes` and\n * apply it as a global security requirement on the spec. Routes\n * that opt out via {@link ApiPublic} drop the global requirement.\n */\n bearerAuth?: boolean\n /**\n * Custom OpenAPI security schemes. Each entry is a\n * {@link OpenAPISecurityScheme} keyed by scheme name — the same\n * name `@ApiSecurity` / `@ApiBearerAuth` / `securityResolver`\n * reference. Schemes referenced by decorators but not declared\n * here are still emitted with a default `bearer` shape (back-compat\n * with the original `@ApiBearerAuth` flow).\n *\n * @example\n * ```ts\n * SwaggerAdapter({\n * securitySchemes: {\n * ApiKey: { type: 'apiKey', in: 'header', name: 'X-API-Key' },\n * OAuth2: {\n * type: 'oauth2',\n * flows: {\n * authorizationCode: {\n * authorizationUrl: 'https://example.com/oauth/authorize',\n * tokenUrl: 'https://example.com/oauth/token',\n * scopes: { 'users:read': 'Read user profile' },\n * },\n * },\n * },\n * },\n * })\n * ```\n */\n securitySchemes?: Record<string, OpenAPISecurityScheme>\n /**\n * Optional bridge for adopters who want their own auth library's\n * metadata to drive Swagger's security annotations without\n * reaching for `@ApiSecurity()` on every route.\n *\n * Returns one or more {@link ApiSecurityRequirement} entries (or\n * a bare scheme name string) when the route should be marked\n * secured; returns `null` to mark the route explicitly public\n * (overriding class-level security); returns `undefined` to fall\n * through to the decorator-driven path.\n *\n * The hook runs **after** {@link ApiPublic} (which short-circuits\n * to public) but **before** the decorator-driven `@ApiSecurity` /\n * `@ApiBearerAuth` lookups, so adopters who set both get the\n * resolver's verdict; this matches the historical behaviour of\n * the now-removed implicit `kick:auth:*` bridge.\n *\n * @example\n * ```ts\n * SwaggerAdapter({\n * securityResolver: ({ controllerClass, handlerName }) => {\n * // Bridge `@forinda/kickjs-auth`'s metadata without coupling.\n * const proto = controllerClass.prototype\n * if (Reflect.getMetadata('kick:auth:public', proto, handlerName)) return null\n * const secured =\n * Reflect.getMetadata('kick:auth:authenticated', controllerClass) ||\n * Reflect.getMetadata('kick:auth:authenticated', proto, handlerName)\n * return secured ? 'BearerAuth' : undefined\n * },\n * })\n * ```\n */\n securityResolver?: (\n ctx: SecurityResolverContext,\n ) => string | ApiSecurityRequirement | (string | ApiSecurityRequirement)[] | null | undefined\n /**\n * Pluggable schema parser for converting validation schemas to JSON Schema.\n * Defaults to `zodSchemaParser` which handles Zod v4+ schemas.\n *\n * Override this to use Yup, Joi, Valibot, ArkType, or any other library.\n *\n * @example\n * ```ts\n * SwaggerAdapter({\n * schemaParser: myYupParser,\n * })\n * ```\n */\n schemaParser?: SchemaParser\n}\n\n/**\n * Normalise any of the shapes accepted by `@ApiSecurity` /\n * `securityResolver` into a flat `ApiSecurityRequirement[]`.\n */\nfunction normaliseSecurity(\n raw: string | ApiSecurityRequirement | (string | ApiSecurityRequirement)[],\n): ApiSecurityRequirement[] {\n const arr = Array.isArray(raw) ? raw : [raw]\n return arr.map((entry) =>\n typeof entry === 'string' ? { name: entry, scopes: [] } : { scopes: [], ...entry },\n )\n}\n\ninterface RegisteredRoute {\n controllerClass: any\n mountPath: string\n}\n\n/**\n * Default route bag used when callers don't pass a config-scoped key.\n * Kept for back-compat with code that imports `registerControllerForDocs`\n * directly without going through SwaggerAdapter — those callers see the\n * legacy \"global single list\" behaviour.\n */\nconst DEFAULT_SCOPE = Symbol('kick:swagger:default-scope')\n\n/**\n * Per-adapter route storage. The adapter's `build` closure passes its\n * config object as the scope key so two SwaggerAdapter instances in\n * the same process (test harnesses, multi-tenant pre-fork) keep\n * independent route lists. Without this, two bootstraps in one process\n * cross-contaminate each other's specs.\n */\nconst routesByScope = new Map<object | symbol, RegisteredRoute[]>()\nroutesByScope.set(DEFAULT_SCOPE, [])\n\nfunction getScopeBag(scope: object | symbol | undefined): RegisteredRoute[] {\n const key = scope ?? DEFAULT_SCOPE\n let bag = routesByScope.get(key)\n if (!bag) {\n bag = []\n routesByScope.set(key, bag)\n }\n return bag\n}\n\n/**\n * Memoised spec — built lazily on the first {@link buildOpenAPISpec}\n * call after a registration change. Re-issued without rebuild on every\n * subsequent `/openapi.json` request until `clearRegisteredRoutes` or\n * `registerControllerForDocs` invalidates it.\n *\n * Keyed by reference equality on the options object so two adapters\n * with different `info.title` don't return each other's cached spec.\n * Application keeps the SwaggerAdapter config alive for the process\n * lifetime, so this is effectively a per-adapter memo cache. WeakMap\n * keeps the entries collectable when an adapter is disposed.\n *\n * `cacheKeys` is the iteration handle (WeakMap doesn't expose one) so\n * we can flush every cached spec on registration change without\n * tracking adapters individually.\n */\nconst specCache = new WeakMap<object, unknown>()\nconst cacheKeys = new Set<object>()\n\nfunction invalidateSpecCache(scope?: object | symbol): void {\n if (scope && typeof scope === 'object') {\n // Targeted invalidation — only the spec keyed on this config is stale.\n if (cacheKeys.has(scope)) {\n specCache.delete(scope)\n cacheKeys.delete(scope)\n }\n return\n }\n // Fallback: flush every cached spec (legacy untyped invalidation).\n for (const key of cacheKeys) specCache.delete(key)\n cacheKeys.clear()\n}\n\n/**\n * Register a controller for OpenAPI introspection. Called by Application\n * during route mounting via the adapter's onRouteMount hook.\n *\n * The optional `scope` argument keys the registration to a specific\n * adapter instance — pass the adapter's own config object as the key\n * (the SwaggerAdapter does this automatically). Omit for legacy\n * single-list behaviour, which is fine for single-bootstrap apps.\n */\nexport function registerControllerForDocs(\n controllerClass: any,\n mountPath: string,\n scope?: object,\n): void {\n getScopeBag(scope).push({ controllerClass, mountPath })\n invalidateSpecCache(scope)\n}\n\n/**\n * Clear registered routes — supports HMR rebuilds. Pass the adapter's\n * config object to clear only that adapter's routes; omit to clear\n * every scope (legacy/global behaviour).\n */\nexport function clearRegisteredRoutes(scope?: object): void {\n if (scope && typeof scope === 'object') {\n routesByScope.delete(scope)\n invalidateSpecCache(scope)\n return\n }\n routesByScope.clear()\n routesByScope.set(DEFAULT_SCOPE, [])\n invalidateSpecCache()\n}\n\n/**\n * Build a full OpenAPI 3.0.3 spec from registered controllers and\n * their decorators.\n *\n * Memoised — the first call for a given `options` object walks every\n * controller (~80–150ms for a 200-route app); subsequent calls return\n * the cached spec until {@link clearRegisteredRoutes} or\n * {@link registerControllerForDocs} invalidate. This matters because\n * Swagger UI re-fetches `/openapi.json` on every navigation; before\n * the cache, every fetch re-walked the entire controller graph.\n */\nexport function buildOpenAPISpec(options: SwaggerOptions = {}): any {\n const cacheKey = options as object\n const cached = specCache.get(cacheKey)\n if (cached !== undefined) return cached\n const built = buildOpenAPISpecUncached(options)\n specCache.set(cacheKey, built)\n cacheKeys.add(cacheKey)\n return built\n}\n\nfunction buildOpenAPISpecUncached(options: SwaggerOptions = {}): any {\n const parser = options.schemaParser ?? zodSchemaParser\n\n /** Convert a validation schema to JSON Schema using the configured parser */\n const toJsonSchema = (schema: unknown): Record<string, unknown> | null => {\n try {\n if (!parser.supports(schema)) return null\n return parser.toJsonSchema(schema)\n } catch {\n return null\n }\n }\n\n const componentSchemas: Record<string, any> = {}\n let schemaCounter = 0\n\n /**\n * Register a schema in components.schemas and return a $ref pointer.\n * If the schema has a title/label, use that as the name. Otherwise generate one.\n */\n const registerSchema = (jsonSchema: Record<string, unknown>, hint?: string): any => {\n // Try to extract a name from the schema\n let baseName = (jsonSchema.title as string) || (jsonSchema.label as string) || hint || ''\n if (!baseName) {\n baseName = `Schema${++schemaCounter}`\n }\n // Sanitize name for OpenAPI (remove spaces, special chars)\n baseName = baseName.replace(/[^a-zA-Z0-9]/g, '')\n\n const clean = { ...jsonSchema }\n delete clean.title\n delete clean.label\n delete clean.$schema\n const cleanJson = JSON.stringify(clean)\n\n // Resolve name collisions: if `baseName` already maps to a different\n // schema body, suffix with `_2`, `_3`, etc. until a free slot or a\n // structural duplicate is found. Two semantically-identical schemas\n // (`CreateUserDTO` registered twice) collapse to one entry by\n // JSON-equality, preserving the existing dedupe behaviour for the\n // common case while preventing the silent overwrite that produced\n // wrong-shape docs when two distinct DTOs hit the same hint.\n let name = baseName\n let suffix = 2\n while (componentSchemas[name]) {\n if (JSON.stringify(componentSchemas[name]) === cleanJson) {\n // Same schema body — reuse the existing slot.\n return { $ref: `#/components/schemas/${name}` }\n }\n name = `${baseName}_${suffix++}`\n }\n componentSchemas[name] = clean\n return { $ref: `#/components/schemas/${name}` }\n }\n\n const spec: any = {\n openapi: '3.0.3',\n info: {\n title: options.info?.title || 'API',\n version: options.info?.version || '1.0.0',\n ...(options.info?.description ? { description: options.info.description } : {}),\n },\n paths: {},\n components: { schemas: {}, securitySchemes: {} },\n tags: [],\n }\n\n if (options.servers) {\n // Drop entries whose URL can't be parsed by the browser's URL\n // constructor. Swagger UI runs `new URL(server.url)` on the client\n // and crashes with `Failed to construct 'URL': Invalid URL` if any\n // entry is malformed — which can happen on Windows dev when an\n // adapter hook populates servers with a path that was never meant\n // to be a URL. Relative URLs (e.g. '/') are allowed through.\n const validServers = options.servers.filter((s) => {\n if (!s?.url || typeof s.url !== 'string') return false\n if (s.url.startsWith('/')) return true\n try {\n void new URL(s.url)\n return true\n } catch {\n return false\n }\n })\n if (validServers.length > 0) {\n spec.servers = validServers\n }\n }\n\n const allTags = new Set<string>()\n // Pre-seed `securitySchemes` with adopter-declared schemes from\n // `options.securitySchemes` so `@ApiSecurity('OAuth2')` references\n // resolve without per-decorator scheme synthesis. The pre-seeded\n // entries take precedence over the implicit `BearerAuth` fallback\n // emitted in the route loop, so adopters who redefine `BearerAuth`\n // (e.g. with custom flows) get their version.\n const securitySchemes: Record<string, any> = { ...options.securitySchemes }\n\n // Routes scoped to this adapter's config (when adapter passed itself\n // as the scope) plus the legacy default-scope bag (for direct\n // registerControllerForDocs callers without a scope arg).\n const scopedRoutes = getScopeBag(options as object)\n const defaultRoutes = options ? getScopeBag(DEFAULT_SCOPE) : []\n const routesToWalk =\n scopedRoutes.length > 0\n ? scopedRoutes\n : defaultRoutes /* fall back to legacy single-list when adapter didn't scope */\n\n for (const { controllerClass, mountPath } of routesToWalk) {\n // Skip excluded controllers\n if (hasClassMeta(SWAGGER_KEYS.EXCLUDE, controllerClass)) continue\n\n const routes: RouteDefinition[] = getClassMeta<RouteDefinition[]>(\n METADATA.ROUTES,\n controllerClass,\n [],\n )\n const classTags: string[] = getClassMeta<string[]>(SWAGGER_KEYS.TAGS, controllerClass, [])\n const classAuth: string | undefined = getClassMetaOrUndefined<string>(\n SWAGGER_KEYS.BEARER_AUTH,\n controllerClass,\n )\n const classSecurity = getClassMetaOrUndefined<ApiSecurityRequirement[]>(\n SWAGGER_KEYS.SECURITY,\n controllerClass,\n )\n for (const route of routes) {\n try {\n emitRouteOperation(route)\n } catch (err) {\n // One bad operation must not blank the whole docs page. Emit a\n // marker summary so the broken op shows up in Swagger UI with\n // a visible warning, and the rest of the spec stays valid.\n // Defensive resolution — the same fields that crashed inside\n // emit may still be undefined here.\n let openApiPath: string\n try {\n openApiPath = joinPaths(mountPath, route.path).replace(EXPRESS_PARAM_RE, '{$1}')\n } catch {\n openApiPath = `${mountPath}/__spec_error__`\n }\n const method = typeof route.method === 'string' ? route.method.toLowerCase() : 'get'\n if (!spec.paths[openApiPath]) spec.paths[openApiPath] = {}\n spec.paths[openApiPath][method] = {\n summary: `⚠ spec generation failed: ${err instanceof Error ? err.message : String(err)}`,\n responses: { default: { description: 'Spec generation failed for this operation.' } },\n }\n }\n }\n\n // Per-route emit hoisted to a closure so the try/catch above can\n // wrap each route in isolation. Closes over loop-locals (operation,\n // routes, classTags, classAuth, etc.) so the body reads the same\n // way it did before the wrap.\n function emitRouteOperation(route: RouteDefinition): void {\n // Skip excluded methods\n if (getMethodMetaOrUndefined(SWAGGER_KEYS.EXCLUDE, controllerClass, route.handlerName)) return\n\n // Build the full path — mountPath is the actual Express mount prefix (from onRouteMount),\n // and route.path is the method-level path. @Controller path is not included here\n // because buildRoutes does not bake it into the router.\n const fullPath = joinPaths(mountPath, route.path)\n\n // Convert Express :param to OpenAPI {param}. Express's\n // path-to-regexp param-name rule is `[A-Za-z_][A-Za-z0-9_]*` —\n // identifier-like, digits allowed after the first char. The\n // previous regex (`[a-zA-Z_]+`) silently dropped digits, so\n // `:v2endpoint` became `:v` + literal `2endpoint` and the\n // generated docs missed the path-param entry entirely.\n const openApiPath = fullPath.replace(EXPRESS_PARAM_RE, '{$1}')\n const method = route.method.toLowerCase()\n\n // Gather metadata\n const operation: ApiOperationOptions = getMethodMeta<ApiOperationOptions>(\n SWAGGER_KEYS.OPERATION,\n controllerClass,\n route.handlerName,\n {} as ApiOperationOptions,\n )\n const responses: ApiResponseOptions[] = getMethodMeta<ApiResponseOptions[]>(\n SWAGGER_KEYS.RESPONSES,\n controllerClass,\n route.handlerName,\n [],\n )\n const methodTags: string[] = getMethodMeta<string[]>(\n SWAGGER_KEYS.TAGS,\n controllerClass,\n route.handlerName,\n [],\n )\n const methodAuth: string | undefined = getMethodMetaOrUndefined<string>(\n SWAGGER_KEYS.BEARER_AUTH,\n controllerClass,\n route.handlerName,\n )\n\n // Tags — method level overrides class level\n const tags = methodTags.length > 0 ? methodTags : classTags\n tags.forEach((t) => allTags.add(t))\n\n // Build operation object — `parameters` and `responses` are\n // attached below only when they have entries, so we don't emit\n // empty arrays/objects only to delete them later.\n const op: any = {\n ...(tags.length > 0 ? { tags } : {}),\n ...(operation.summary ? { summary: operation.summary } : {}),\n ...(operation.description ? { description: operation.description } : {}),\n ...(operation.operationId ? { operationId: operation.operationId } : {}),\n ...(operation.deprecated ? { deprecated: true } : {}),\n responses: {},\n }\n const parameters: any[] = []\n\n // Path parameters\n const paramMatches = fullPath.match(EXPRESS_PARAM_RE) || []\n for (const match of paramMatches) {\n const paramName = match.slice(1)\n let schema: any = { type: 'string' }\n\n // Try to get type from params validation schema\n if (route.validation?.params) {\n const jsonSchema = toJsonSchema(route.validation.params)\n if (jsonSchema?.properties && typeof jsonSchema.properties === 'object') {\n const props = jsonSchema.properties as Record<string, any>\n if (props[paramName]) {\n schema = props[paramName]\n }\n }\n }\n\n parameters.push({ name: paramName, in: 'path', required: true, schema })\n }\n\n // Query parameters\n if (route.validation?.query) {\n const jsonSchema = toJsonSchema(route.validation.query)\n if (jsonSchema?.properties && typeof jsonSchema.properties === 'object') {\n const required = Array.isArray(jsonSchema.required) ? jsonSchema.required : []\n for (const [name, propSchema] of Object.entries(\n jsonSchema.properties as Record<string, any>,\n )) {\n parameters.push({\n name,\n in: 'query',\n required: required.includes(name),\n schema: propSchema,\n })\n }\n }\n }\n\n // @ApiQueryParams decorator — document filterable/sortable/searchable fields\n const queryParamsConfig = getMethodMetaOrUndefined<any>(\n METADATA.QUERY_PARAMS,\n controllerClass,\n route.handlerName,\n )\n if (queryParamsConfig) {\n if (queryParamsConfig.filterable?.length) {\n parameters.push({\n name: 'filter',\n in: 'query',\n required: false,\n description: `Filter fields: ${queryParamsConfig.filterable.join(', ')}. Format: \\`field:operator:value\\`. Operators: eq, neq, gt, gte, lt, lte, contains, starts, ends, in, between`,\n schema: { type: 'array', items: { type: 'string' } },\n style: 'form',\n explode: true,\n })\n }\n if (queryParamsConfig.sortable?.length) {\n parameters.push({\n name: 'sort',\n in: 'query',\n required: false,\n description: `Sort fields: ${queryParamsConfig.sortable.join(', ')}. Format: \\`field:asc\\` or \\`field:desc\\``,\n schema: { type: 'array', items: { type: 'string' } },\n style: 'form',\n explode: true,\n })\n }\n if (queryParamsConfig.searchable?.length) {\n parameters.push({\n name: 'q',\n in: 'query',\n required: false,\n description: `Search across: ${queryParamsConfig.searchable.join(', ')}`,\n schema: { type: 'string' },\n })\n }\n parameters.push(\n {\n name: 'page',\n in: 'query',\n required: false,\n description: 'Page number (default: 1)',\n schema: { type: 'integer', minimum: 1, default: 1 },\n },\n {\n name: 'limit',\n in: 'query',\n required: false,\n description: 'Items per page (default: 20, max: 100)',\n schema: { type: 'integer', minimum: 1, maximum: 100, default: 20 },\n },\n )\n }\n\n if (parameters.length > 0) op.parameters = parameters\n\n // Request body\n if (route.validation?.body) {\n if (BODY_METHODS.has(method)) {\n const bodySchema = toJsonSchema(route.validation.body)\n if (bodySchema) {\n const bodyName = route.validation.name || `${route.handlerName}Body`\n const ref = registerSchema(bodySchema, bodyName)\n op.requestBody = {\n required: true,\n content: { 'application/json': { schema: ref } },\n }\n }\n } else {\n // Body validation on a method that OpenAPI 3 doesn't allow a\n // body for (GET / HEAD / DELETE / OPTIONS). Silently dropping\n // surprised adopters whose request schema vanished from docs;\n // warn once per route so they can switch to query validation\n // or rethink the route shape.\n const warnKey = `${controllerClass.name}.${route.handlerName}`\n if (!warnedBodyOnReadMethod.has(warnKey)) {\n warnedBodyOnReadMethod.add(warnKey)\n log.warn(\n `body validation on ${method.toUpperCase()} ${fullPath} (${warnKey}) is dropped from the OpenAPI spec — OpenAPI 3 does not allow a request body on ${method.toUpperCase()}. Move the schema to validation.query or change the route method.`,\n )\n }\n }\n }\n\n // File upload detection\n const fileUpload = getMethodMetaOrUndefined<any>(\n METADATA.FILE_UPLOAD,\n controllerClass,\n route.handlerName,\n )\n if (fileUpload) {\n const fieldName = fileUpload.fieldName ?? 'file'\n const properties: any = {}\n\n if (fileUpload.mode === 'array') {\n properties[fieldName] = {\n type: 'array',\n items: { type: 'string', format: 'binary' },\n }\n } else if (fileUpload.mode !== 'none') {\n properties[fieldName] = {\n type: 'string',\n format: 'binary',\n }\n }\n\n op.requestBody = {\n required: true,\n content: {\n 'multipart/form-data': {\n schema: { type: 'object', properties },\n },\n },\n }\n }\n\n // Responses\n if (responses.length > 0) {\n for (const resp of responses) {\n const entry: Record<string, unknown> = { description: resp.description || '' }\n if (resp.schema && typeof resp.schema === 'object') {\n // Try the validation parser first (Zod / Yup / etc.). If\n // that returns null the schema is plain JSON Schema and we\n // pass it through as-is — that's the escape hatch for\n // adopters who hand-write OpenAPI shapes without going\n // through the schema-parser layer.\n const converted = toJsonSchema(resp.schema)\n const schemaName = resp.name || `${route.handlerName}Response${resp.status}`\n const finalSchema = converted ? registerSchema(converted, schemaName) : resp.schema\n entry.content = { 'application/json': { schema: finalSchema } }\n }\n op.responses[String(resp.status)] = entry\n }\n } else {\n // Auto-generate default responses\n const defaultStatus = method === 'post' ? '201' : method === 'delete' ? '204' : '200'\n op.responses[defaultStatus] = { description: 'Successful operation' }\n\n if (route.validation?.body) {\n op.responses['422'] = { description: 'Validation error' }\n }\n }\n\n // Security resolution order (first match wins):\n // 1. @ApiPublic on the method — opt-out, no security emitted.\n // 2. options.securityResolver({controllerClass, handlerName})\n // — adopter-provided bridge for external auth libraries.\n // Returning `null` is \"explicitly public\" (same as\n // @ApiPublic); a value or array drives the requirements.\n // 3. @ApiSecurity / @ApiBearerAuth on the method.\n // 4. @ApiSecurity / @ApiBearerAuth on the class.\n const isPublicMethod = !!getMethodMetaOrUndefined<boolean>(\n SWAGGER_KEYS.PUBLIC,\n controllerClass,\n route.handlerName,\n )\n const methodSecurity = getMethodMetaOrUndefined<ApiSecurityRequirement[]>(\n SWAGGER_KEYS.SECURITY,\n controllerClass,\n route.handlerName,\n )\n const resolverOutput = !isPublicMethod\n ? options.securityResolver?.({ controllerClass, handlerName: route.handlerName })\n : undefined\n const resolverSecurity =\n resolverOutput == null || resolverOutput === undefined\n ? undefined\n : normaliseSecurity(resolverOutput)\n const resolverPublic = resolverOutput === null\n\n let requirements: ApiSecurityRequirement[] | undefined\n // Track whether the resolution path came from `@ApiBearerAuth`\n // (any name) so a bearer-shaped scheme gets auto-synthesised\n // for the named entry — preserves the original\n // `@ApiBearerAuth('CustomName')` ergonomics. `@ApiSecurity`\n // and the resolver hook DON'T auto-synth for arbitrary names\n // (only the literal `'BearerAuth'`) since their shapes are\n // generic — adopters must declare custom schemes via\n // `SwaggerOptions.securitySchemes` when using those paths.\n let bearerAuthSourced = false\n if (isPublicMethod || resolverPublic) {\n requirements = undefined\n } else if (resolverSecurity && resolverSecurity.length > 0) {\n requirements = resolverSecurity\n } else if (methodSecurity && methodSecurity.length > 0) {\n requirements = methodSecurity\n } else if (methodAuth) {\n requirements = [{ name: methodAuth, scopes: [] }]\n bearerAuthSourced = true\n } else if (classSecurity && classSecurity.length > 0) {\n requirements = classSecurity\n } else if (classAuth) {\n requirements = [{ name: classAuth, scopes: [] }]\n bearerAuthSourced = true\n }\n\n if (requirements) {\n op.security = requirements.map((r) => ({ [r.name]: r.scopes ?? [] }))\n for (const r of requirements) {\n if (!securitySchemes[r.name]) {\n // `@ApiBearerAuth('CustomName')` always emits a\n // bearer-shaped scheme under `CustomName`. The literal\n // `BearerAuth` name also auto-synths for back-compat\n // with `@ApiSecurity('BearerAuth')` and resolver hooks.\n if (bearerAuthSourced || r.name === 'BearerAuth') {\n securitySchemes[r.name] = {\n type: 'http',\n scheme: 'bearer',\n bearerFormat: 'JWT',\n }\n }\n }\n }\n }\n\n // Mount\n if (!spec.paths[openApiPath]) spec.paths[openApiPath] = {}\n spec.paths[openApiPath][method] = op\n }\n }\n\n // Finalize\n spec.tags = Array.from(allTags).map((name) => ({ name }))\n spec.components.securitySchemes = securitySchemes\n\n if (options.bearerAuth) {\n if (!securitySchemes.BearerAuth) {\n spec.components.securitySchemes.BearerAuth = {\n type: 'http',\n scheme: 'bearer',\n bearerFormat: 'JWT',\n }\n }\n spec.security = [{ BearerAuth: [] }]\n }\n\n // Merge collected schemas into components\n spec.components.schemas = componentSchemas\n\n // Clean up empty components\n if (Object.keys(spec.components.schemas).length === 0) delete spec.components.schemas\n if (Object.keys(spec.components.securitySchemes).length === 0)\n delete spec.components.securitySchemes\n if (Object.keys(spec.components).length === 0) delete spec.components\n\n return spec\n}\n","/** Escape a string for safe HTML attribute/content interpolation */\nfunction escapeHtml(str: string): string {\n return str\n .replace(/&/g, '&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": "5.2.0",
3
+ "version": "5.3.0",
4
4
  "description": "OpenAPI spec generation from decorators, Swagger UI and ReDoc serving for KickJS",
5
5
  "keywords": [
6
6
  "kickjs",
@@ -73,14 +73,14 @@
73
73
  }
74
74
  },
75
75
  "devDependencies": {
76
- "@swc/core": "^1.15.30",
76
+ "@swc/core": "^1.15.33",
77
77
  "@types/express": "^5.0.6",
78
78
  "@types/node": "^25.6.0",
79
79
  "express": "^5.1.0",
80
80
  "typescript": "^6.0.3",
81
81
  "vitest": "^4.1.5",
82
82
  "zod": "^4.3.6",
83
- "@forinda/kickjs": "5.2.0"
83
+ "@forinda/kickjs": "5.5.0"
84
84
  },
85
85
  "publishConfig": {
86
86
  "access": "public"