@forinda/kickjs-swagger 3.2.0 → 4.0.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/README.md +9 -34
- package/dist/index.d.mts +5 -20
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +96 -95
- package/dist/index.mjs.map +1 -1
- package/package.json +7 -13
package/README.md
CHANGED
|
@@ -1,62 +1,37 @@
|
|
|
1
1
|
# @forinda/kickjs-swagger
|
|
2
2
|
|
|
3
|
-
Auto-generated OpenAPI spec from decorators
|
|
3
|
+
Auto-generated OpenAPI spec from decorators + Zod schemas. Serves Swagger UI at `/docs`, ReDoc at `/redoc`, raw JSON at `/openapi.json`.
|
|
4
4
|
|
|
5
5
|
## Install
|
|
6
6
|
|
|
7
7
|
```bash
|
|
8
|
-
# Using the KickJS CLI (recommended)
|
|
9
8
|
kick add swagger
|
|
10
|
-
|
|
11
|
-
# Manual install
|
|
12
|
-
pnpm add @forinda/kickjs-swagger @forinda/kickjs-core
|
|
13
9
|
```
|
|
14
10
|
|
|
15
|
-
## Features
|
|
16
|
-
|
|
17
|
-
- `SwaggerAdapter` — serves Swagger UI at `/docs`, ReDoc at `/redoc`, JSON at `/openapi.json`
|
|
18
|
-
- Decorators: `@ApiTags`, `@ApiOperation`, `@ApiResponse`, `@ApiBearerAuth`, `@ApiExclude`
|
|
19
|
-
- Auto-converts Zod validation schemas to OpenAPI JSON Schema
|
|
20
|
-
- Pluggable `SchemaParser` — use Joi, Yup, Valibot instead of Zod
|
|
21
|
-
- Schemas registered in `components.schemas` for the Models section
|
|
22
|
-
|
|
23
11
|
## Quick Example
|
|
24
12
|
|
|
25
|
-
```
|
|
13
|
+
```ts
|
|
14
|
+
import { bootstrap } from '@forinda/kickjs'
|
|
26
15
|
import { SwaggerAdapter } from '@forinda/kickjs-swagger'
|
|
16
|
+
import { modules } from './modules'
|
|
27
17
|
|
|
28
|
-
bootstrap({
|
|
18
|
+
export const app = await bootstrap({
|
|
29
19
|
modules,
|
|
30
20
|
adapters: [
|
|
31
|
-
|
|
21
|
+
SwaggerAdapter({
|
|
32
22
|
info: { title: 'My API', version: '1.0.0' },
|
|
33
23
|
bearerAuth: true,
|
|
34
|
-
disableInProd: true,
|
|
24
|
+
disableInProd: true,
|
|
35
25
|
}),
|
|
36
26
|
],
|
|
37
27
|
})
|
|
38
28
|
```
|
|
39
29
|
|
|
40
|
-
|
|
41
|
-
`NODE_ENV === 'production'`.
|
|
42
|
-
|
|
43
|
-
### Custom Schema Parser (Joi)
|
|
44
|
-
|
|
45
|
-
```typescript
|
|
46
|
-
import { type SchemaParser } from '@forinda/kickjs-swagger'
|
|
47
|
-
|
|
48
|
-
const joiParser: SchemaParser = {
|
|
49
|
-
name: 'joi',
|
|
50
|
-
supports: (schema) => Joi.isSchema(schema),
|
|
51
|
-
toJsonSchema: (schema) => joiToJson(schema),
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
new SwaggerAdapter({ schemaParser: joiParser })
|
|
55
|
-
```
|
|
30
|
+
Decorators (`@ApiTags`, `@ApiOperation`, `@ApiResponse`, `@ApiBearerAuth`, `@ApiExclude`) refine the generated spec. For non-Zod schemas, plug a custom `SchemaParser` (Joi, Yup, Valibot, etc.).
|
|
56
31
|
|
|
57
32
|
## Documentation
|
|
58
33
|
|
|
59
|
-
[
|
|
34
|
+
[forinda.github.io/kick-js/guide/swagger](https://forinda.github.io/kick-js/guide/swagger)
|
|
60
35
|
|
|
61
36
|
## License
|
|
62
37
|
|
package/dist/index.d.mts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
|
|
2
|
-
import
|
|
2
|
+
import * as _$_forinda_kickjs0 from "@forinda/kickjs";
|
|
3
3
|
|
|
4
4
|
//#region src/schema-parser.d.ts
|
|
5
5
|
/**
|
|
@@ -20,7 +20,7 @@ import { AdapterContext, AppAdapter } from "@forinda/kickjs";
|
|
|
20
20
|
* toJsonSchema: (schema) => joiToJson(schema),
|
|
21
21
|
* }
|
|
22
22
|
*
|
|
23
|
-
*
|
|
23
|
+
* SwaggerAdapter({ schemaParser: joiParser })
|
|
24
24
|
* ```
|
|
25
25
|
*/
|
|
26
26
|
interface SchemaParser {
|
|
@@ -90,7 +90,7 @@ interface SwaggerOptions {
|
|
|
90
90
|
*
|
|
91
91
|
* @example
|
|
92
92
|
* ```ts
|
|
93
|
-
*
|
|
93
|
+
* SwaggerAdapter({
|
|
94
94
|
* schemaParser: myYupParser,
|
|
95
95
|
* })
|
|
96
96
|
* ```
|
|
@@ -132,7 +132,7 @@ interface SwaggerAdapterOptions extends SwaggerOptions {
|
|
|
132
132
|
* bootstrap({
|
|
133
133
|
* modules,
|
|
134
134
|
* adapters: [
|
|
135
|
-
*
|
|
135
|
+
* SwaggerAdapter({
|
|
136
136
|
* info: { title: 'My API', version: '1.0.0' },
|
|
137
137
|
* }),
|
|
138
138
|
* ],
|
|
@@ -144,22 +144,7 @@ interface SwaggerAdapterOptions extends SwaggerOptions {
|
|
|
144
144
|
* GET /redoc — ReDoc (CDN — no local package available)
|
|
145
145
|
* GET /openapi.json — Raw OpenAPI 3.0.3 spec
|
|
146
146
|
*/
|
|
147
|
-
declare
|
|
148
|
-
private readonly options;
|
|
149
|
-
name: string;
|
|
150
|
-
constructor(options?: SwaggerAdapterOptions);
|
|
151
|
-
/** Whether the adapter should skip mounting in the current environment */
|
|
152
|
-
private get disabled();
|
|
153
|
-
/** Auto-detect server URLs from the running HTTP server and peer adapters */
|
|
154
|
-
afterStart({
|
|
155
|
-
server
|
|
156
|
-
}: AdapterContext): void;
|
|
157
|
-
/** Collect controller metadata as routes are mounted */
|
|
158
|
-
onRouteMount(controllerClass: any, mountPath: string): void;
|
|
159
|
-
beforeMount({
|
|
160
|
-
app
|
|
161
|
-
}: AdapterContext): void;
|
|
162
|
-
}
|
|
147
|
+
declare const SwaggerAdapter: _$_forinda_kickjs0.AdapterFactory<SwaggerAdapterOptions, unknown>;
|
|
163
148
|
//#endregion
|
|
164
149
|
//#region src/ui.d.ts
|
|
165
150
|
/**
|
package/dist/index.d.mts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.mts","names":[],"sources":["../src/schema-parser.ts","../src/decorators.ts","../src/openapi-builder.ts","../src/swagger.adapter.ts","../src/ui.ts"],"mappings":";;;;;;;AAqBA;;;;;;;;;;;;AAsBA;;;;;;UAtBiB,YAAA;;WAEN,IAAA;ECXyB;;;;EDiBlC,QAAA,CAAS,MAAA;ECdT;;;;AAIF;EDiBE,YAAA,CAAa,MAAA,YAAkB,MAAA;AAAA;;;;;cAOpB,eAAA,EAAiB,YAAA;;;UC/Bb,mBAAA;EACf,OAAA;EACA,WAAA;EACA,WAAA;EACA,UAAA;AAAA;AAAA,UAGe,kBAAA;EACf,MAAA;EACA,WAAA;EACA,MAAA;EDqB4B;ECnB5B,IAAA;AAAA;;iBAIc,YAAA,CAAa,OAAA,EAAS,mBAAA,GAAsB,eAAA;AAhB5D;AAAA,iBAuBgB,WAAA,CAAY,OAAA,EAAS,kBAAA,GAAqB,eAAA;;iBAY1C,OAAA,CAAA,GAAW,IAAA,aAAiB,cAAA,GAAiB,eAAA;;iBAW7C,aAAA,CAAc,IAAA,YAAsB,cAAA,GAAiB,eAAA;;iBAWrD,UAAA,CAAA,GAAc,cAAA,GAAiB,eAAA;;;UCzB9B,WAAA;EACf,KAAA;EACA,OAAA;EACA,WAAA;AAAA;AAAA,UAGe,cAAA;EACf,IAAA,GAAO,OAAA,CAAQ,WAAA;EACf,OAAA;IAAY,GAAA;IAAa,WAAA;EAAA;EACzB,UAAA;EFjBqC;;AAOvC;;;;;;;;AC/BA;;;ECuDE,YAAA,GAAe,YAAA;AAAA;;iBAWD,yBAAA,CAA0B,eAAA,OAAsB,SAAA;;iBAKhD,qBAAA,CAAA;;iBAKA,gBAAA,CAAiB,OAAA,GAAS,cAAA;;;UCjEzB,qBAAA,SAA8B,cAAA;
|
|
1
|
+
{"version":3,"file":"index.d.mts","names":[],"sources":["../src/schema-parser.ts","../src/decorators.ts","../src/openapi-builder.ts","../src/swagger.adapter.ts","../src/ui.ts"],"mappings":";;;;;;;AAqBA;;;;;;;;;;;;AAsBA;;;;;;UAtBiB,YAAA;;WAEN,IAAA;ECXyB;;;;EDiBlC,QAAA,CAAS,MAAA;ECdT;;;;AAIF;EDiBE,YAAA,CAAa,MAAA,YAAkB,MAAA;AAAA;;;;;cAOpB,eAAA,EAAiB,YAAA;;;UC/Bb,mBAAA;EACf,OAAA;EACA,WAAA;EACA,WAAA;EACA,UAAA;AAAA;AAAA,UAGe,kBAAA;EACf,MAAA;EACA,WAAA;EACA,MAAA;EDqB4B;ECnB5B,IAAA;AAAA;;iBAIc,YAAA,CAAa,OAAA,EAAS,mBAAA,GAAsB,eAAA;AAhB5D;AAAA,iBAuBgB,WAAA,CAAY,OAAA,EAAS,kBAAA,GAAqB,eAAA;;iBAY1C,OAAA,CAAA,GAAW,IAAA,aAAiB,cAAA,GAAiB,eAAA;;iBAW7C,aAAA,CAAc,IAAA,YAAsB,cAAA,GAAiB,eAAA;;iBAWrD,UAAA,CAAA,GAAc,cAAA,GAAiB,eAAA;;;UCzB9B,WAAA;EACf,KAAA;EACA,OAAA;EACA,WAAA;AAAA;AAAA,UAGe,cAAA;EACf,IAAA,GAAO,OAAA,CAAQ,WAAA;EACf,OAAA;IAAY,GAAA;IAAa,WAAA;EAAA;EACzB,UAAA;EFjBqC;;AAOvC;;;;;;;;AC/BA;;;ECuDE,YAAA,GAAe,YAAA;AAAA;;iBAWD,yBAAA,CAA0B,eAAA,OAAsB,SAAA;;iBAKhD,qBAAA,CAAA;;iBAKA,gBAAA,CAAiB,OAAA,GAAS,cAAA;;;UCjEzB,qBAAA,SAA8B,cAAA;;EAE7C,QAAA;EHJ2B;EGM3B,SAAA;EHSqC;EGPrC,QAAA;EHAA;EGEA,QAAA;EHKA;;;;;EGCA,aAAA;AAAA;;;;;;;AFzBF;;;;;;;;;;AAOA;;;;;;;cE4Ca,cAAA,EAAc,kBAAA,CAAA,cAAA,CAAA,qBAAA;;;;;;AH1C3B;;;;;;;;iBIAgB,aAAA,CAAc,OAAA,UAAiB,KAAA,WAAoB,UAAA;;;;AJsBnE;;;;iBIyCgB,SAAA,CAAU,OAAA,UAAiB,KAAA"}
|
package/dist/index.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @forinda/kickjs-swagger
|
|
2
|
+
* @forinda/kickjs-swagger v4.0.0
|
|
3
3
|
*
|
|
4
4
|
* Copyright (c) Felix Orinda
|
|
5
5
|
*
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
* @license MIT
|
|
10
10
|
*/
|
|
11
11
|
import { createRequire } from "node:module";
|
|
12
|
-
import { Logger, METADATA, getClassMeta, getClassMetaOrUndefined, getMethodMeta, getMethodMetaOrUndefined, hasClassMeta, joinPaths, pushMethodMeta, setClassMeta, setMethodMeta } from "@forinda/kickjs";
|
|
12
|
+
import { Logger, METADATA, defineAdapter, getClassMeta, getClassMetaOrUndefined, getMethodMeta, getMethodMetaOrUndefined, hasClassMeta, joinPaths, pushMethodMeta, setClassMeta, setMethodMeta } from "@forinda/kickjs";
|
|
13
13
|
import { dirname } from "node:path";
|
|
14
14
|
import express, { Router } from "express";
|
|
15
15
|
//#region src/schema-parser.ts
|
|
@@ -452,7 +452,7 @@ function getSwaggerUiDistPath() {
|
|
|
452
452
|
* bootstrap({
|
|
453
453
|
* modules,
|
|
454
454
|
* adapters: [
|
|
455
|
-
*
|
|
455
|
+
* SwaggerAdapter({
|
|
456
456
|
* info: { title: 'My API', version: '1.0.0' },
|
|
457
457
|
* }),
|
|
458
458
|
* ],
|
|
@@ -464,99 +464,100 @@ function getSwaggerUiDistPath() {
|
|
|
464
464
|
* GET /redoc — ReDoc (CDN — no local package available)
|
|
465
465
|
* GET /openapi.json — Raw OpenAPI 3.0.3 spec
|
|
466
466
|
*/
|
|
467
|
-
|
|
468
|
-
name
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
467
|
+
const SwaggerAdapter = defineAdapter({
|
|
468
|
+
name: "SwaggerAdapter",
|
|
469
|
+
defaults: {
|
|
470
|
+
docsPath: "/docs",
|
|
471
|
+
redocPath: "/redoc",
|
|
472
|
+
specPath: "/openapi.json"
|
|
473
|
+
},
|
|
474
|
+
build: (config) => {
|
|
475
|
+
const isDisabled = () => Boolean(config.disableInProd) && process.env.NODE_ENV === "production";
|
|
476
|
+
return {
|
|
477
|
+
onRouteMount(controllerClass, mountPath) {
|
|
478
|
+
if (isDisabled()) return;
|
|
479
|
+
registerControllerForDocs(controllerClass, mountPath);
|
|
480
|
+
},
|
|
481
|
+
afterStart({ server }) {
|
|
482
|
+
if (isDisabled()) return;
|
|
483
|
+
const addr = server?.address?.();
|
|
484
|
+
if (!addr || typeof addr !== "object") return;
|
|
485
|
+
const host = addr.address === "::" || addr.address === "0.0.0.0" ? "localhost" : addr.address;
|
|
486
|
+
if (!config.servers || config.servers.length === 0) config.servers = [{
|
|
487
|
+
url: `http://${host}:${addr.port}`,
|
|
488
|
+
description: "HTTP server"
|
|
489
|
+
}];
|
|
490
|
+
const wsAdapter = config.adapters?.find((a) => a.name === "WsAdapter" && typeof a.getStats === "function");
|
|
491
|
+
if (wsAdapter) {
|
|
492
|
+
const stats = wsAdapter.getStats();
|
|
493
|
+
for (const namespace of Object.keys(stats.namespaces || {})) config.servers?.push({
|
|
494
|
+
url: `ws://${host}:${addr.port}${namespace}`,
|
|
495
|
+
description: `WebSocket: ${namespace}`
|
|
496
|
+
});
|
|
497
|
+
}
|
|
498
|
+
},
|
|
499
|
+
beforeMount({ app }) {
|
|
500
|
+
if (isDisabled()) {
|
|
501
|
+
log.info("Swagger disabled in production (disableInProd=true)");
|
|
502
|
+
return;
|
|
503
|
+
}
|
|
504
|
+
clearRegisteredRoutes();
|
|
505
|
+
const docsPath = config.docsPath;
|
|
506
|
+
const redocPath = config.redocPath;
|
|
507
|
+
const specPath = config.specPath;
|
|
508
|
+
let uiDistAvailable = false;
|
|
509
|
+
const docsRouter = Router();
|
|
510
|
+
const swaggerAssetsPath = "/_swagger-assets";
|
|
511
|
+
try {
|
|
512
|
+
const swaggerDistDir = getSwaggerUiDistPath();
|
|
513
|
+
docsRouter.use(swaggerAssetsPath, express.static(swaggerDistDir));
|
|
514
|
+
uiDistAvailable = true;
|
|
515
|
+
} catch {
|
|
516
|
+
log.warn("swagger-ui-dist not found — Swagger UI will load from CDN (requires internet).");
|
|
517
|
+
}
|
|
518
|
+
docsRouter.use((_req, res, next) => {
|
|
519
|
+
const serverOrigins = /* @__PURE__ */ new Set();
|
|
520
|
+
for (const s of config.servers ?? []) try {
|
|
521
|
+
serverOrigins.add(new URL(s.url).origin);
|
|
522
|
+
} catch {}
|
|
523
|
+
const connectSrc = [
|
|
524
|
+
"'self'",
|
|
525
|
+
"http://localhost:*",
|
|
526
|
+
"http://127.0.0.1:*",
|
|
527
|
+
"https://localhost:*",
|
|
528
|
+
"https://127.0.0.1:*",
|
|
529
|
+
"ws://localhost:*",
|
|
530
|
+
"ws://127.0.0.1:*",
|
|
531
|
+
...serverOrigins
|
|
532
|
+
].join(" ");
|
|
533
|
+
res.setHeader("Content-Security-Policy", [
|
|
534
|
+
"default-src 'self'",
|
|
535
|
+
"script-src 'self' 'unsafe-inline' https://unpkg.com https://cdn.redoc.ly https://cdn.jsdelivr.net",
|
|
536
|
+
"style-src 'self' 'unsafe-inline' https://unpkg.com https://fonts.googleapis.com",
|
|
537
|
+
"font-src 'self' https://fonts.gstatic.com",
|
|
538
|
+
"img-src 'self' data: https://unpkg.com",
|
|
539
|
+
`connect-src ${connectSrc}`
|
|
540
|
+
].join("; "));
|
|
541
|
+
next();
|
|
542
|
+
});
|
|
543
|
+
docsRouter.get(specPath, (_req, res) => {
|
|
544
|
+
const spec = buildOpenAPISpec(config);
|
|
545
|
+
res.json(spec);
|
|
546
|
+
});
|
|
547
|
+
docsRouter.get(docsPath, (_req, res) => {
|
|
548
|
+
res.type("html").send(swaggerUIHtml(specPath, config.info?.title, uiDistAvailable ? swaggerAssetsPath : void 0));
|
|
549
|
+
});
|
|
550
|
+
docsRouter.get(redocPath, (_req, res) => {
|
|
551
|
+
res.type("html").send(redocHtml(specPath, config.info?.title));
|
|
552
|
+
});
|
|
553
|
+
app.use(docsRouter);
|
|
554
|
+
log.info(`Swagger UI: ${docsPath}`);
|
|
555
|
+
log.info(`ReDoc: ${redocPath}`);
|
|
556
|
+
log.info(`OpenAPI spec: ${specPath}`);
|
|
557
|
+
}
|
|
558
|
+
};
|
|
558
559
|
}
|
|
559
|
-
};
|
|
560
|
+
});
|
|
560
561
|
//#endregion
|
|
561
562
|
export { ApiBearerAuth, ApiExclude, ApiOperation, ApiResponse, ApiTags, SwaggerAdapter, buildOpenAPISpec, clearRegisteredRoutes, redocHtml, registerControllerForDocs, swaggerUIHtml, zodSchemaParser };
|
|
562
563
|
|
package/dist/index.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.mjs","names":[],"sources":["../src/schema-parser.ts","../src/decorators.ts","../src/openapi-builder.ts","../src/ui.ts","../src/swagger.adapter.ts"],"sourcesContent":["/**\n * Interface for converting validation library schemas to JSON Schema.\n *\n * KickJS ships with a Zod parser by default. To use a different validation\n * library (Yup, Joi, Valibot, ArkType, etc.), implement this interface and\n * pass it to the SwaggerAdapter.\n *\n * @example\n * ```ts\n * import Joi from 'joi'\n * import joiToJson from 'joi-to-json'\n *\n * const joiParser: SchemaParser = {\n * name: 'joi',\n * supports: (schema) => Joi.isSchema(schema),\n * toJsonSchema: (schema) => joiToJson(schema),\n * }\n *\n * new SwaggerAdapter({ schemaParser: joiParser })\n * ```\n */\nexport interface SchemaParser {\n /** Human-readable name for logging/debugging */\n readonly name: string\n\n /**\n * Return true if this parser can handle the given schema object.\n * Called before `toJsonSchema` to allow graceful fallback.\n */\n supports(schema: unknown): boolean\n\n /**\n * Convert a validation schema to a JSON Schema object.\n * Should return a plain object conforming to JSON Schema draft-07 or later.\n * Must not include the top-level `$schema` key — the builder adds it.\n */\n toJsonSchema(schema: unknown): Record<string, unknown>\n}\n\n/**\n * Default schema parser for Zod v4+.\n * Uses Zod's built-in `.toJSONSchema()` instance method.\n */\nexport const zodSchemaParser: SchemaParser = {\n name: 'zod',\n\n supports(schema: unknown): boolean {\n return (\n schema != null &&\n typeof schema === 'object' &&\n typeof (schema as any).safeParse === 'function' &&\n typeof (schema as any).toJSONSchema === 'function'\n )\n },\n\n toJsonSchema(schema: unknown): Record<string, unknown> {\n const { $schema: _, ...rest } = (schema as any).toJSONSchema() as Record<string, unknown>\n return rest\n },\n}\n","import { setMethodMeta, setClassMeta, pushMethodMeta } from '@forinda/kickjs'\n\nconst SWAGGER_KEYS = {\n OPERATION: Symbol('kick:swagger:operation'),\n RESPONSES: Symbol('kick:swagger:responses'),\n TAGS: Symbol('kick:swagger:tags'),\n BEARER_AUTH: Symbol('kick:swagger:bearer'),\n EXCLUDE: Symbol('kick:swagger:exclude'),\n}\n\nexport { SWAGGER_KEYS }\n\nexport interface ApiOperationOptions {\n summary?: string\n description?: string\n operationId?: string\n deprecated?: boolean\n}\n\nexport interface ApiResponseOptions {\n status: number\n description?: string\n schema?: any\n /** Schema name in components/schemas (e.g., 'UserResponse', 'ErrorBody'). Auto-generated from handler name if omitted. */\n name?: string\n}\n\n/** Attach operation metadata to a route handler */\nexport function ApiOperation(options: ApiOperationOptions): MethodDecorator {\n return (target, propertyKey) => {\n setMethodMeta(SWAGGER_KEYS.OPERATION, options, target.constructor, propertyKey as string)\n }\n}\n\n/** Document a response status. Can be stacked multiple times. */\nexport function ApiResponse(options: ApiResponseOptions): MethodDecorator {\n return (target, propertyKey) => {\n pushMethodMeta<ApiResponseOptions>(\n SWAGGER_KEYS.RESPONSES,\n target.constructor,\n propertyKey as string,\n options,\n )\n }\n}\n\n/** Apply OpenAPI tags at class or method level */\nexport function ApiTags(...tags: string[]): ClassDecorator & MethodDecorator {\n return (target: any, propertyKey?: string | symbol) => {\n if (propertyKey) {\n setMethodMeta(SWAGGER_KEYS.TAGS, tags, target.constructor, propertyKey as string)\n } else {\n setClassMeta(SWAGGER_KEYS.TAGS, tags, target)\n }\n }\n}\n\n/** Mark endpoint as requiring Bearer token auth */\nexport function ApiBearerAuth(name = 'BearerAuth'): ClassDecorator & MethodDecorator {\n return (target: any, propertyKey?: string | symbol) => {\n if (propertyKey) {\n setMethodMeta(SWAGGER_KEYS.BEARER_AUTH, name, target.constructor, propertyKey as string)\n } else {\n setClassMeta(SWAGGER_KEYS.BEARER_AUTH, name, target)\n }\n }\n}\n\n/** Exclude a controller or method from the OpenAPI spec */\nexport function ApiExclude(): ClassDecorator & MethodDecorator {\n return (target: any, propertyKey?: string | symbol) => {\n if (propertyKey) {\n setMethodMeta(SWAGGER_KEYS.EXCLUDE, true, target.constructor, propertyKey as string)\n } else {\n setClassMeta(SWAGGER_KEYS.EXCLUDE, true, target)\n }\n }\n}\n","import {\n METADATA,\n joinPaths,\n type RouteDefinition,\n getClassMeta,\n getClassMetaOrUndefined,\n getMethodMeta,\n getMethodMetaOrUndefined,\n hasClassMeta,\n} from '@forinda/kickjs'\nimport { SWAGGER_KEYS, type ApiOperationOptions, type ApiResponseOptions } from './decorators'\nimport { zodSchemaParser, type SchemaParser } from './schema-parser'\n\n// ── Auth metadata bridge ──────────────────────────────────────────────\n// Check @forinda/kickjs-auth decorators without importing the auth package.\n// Symbols are matched by description to avoid a hard dependency.\n\nconst R = Reflect as any\n\nfunction getAuthMeta(key: string, target: any, propertyKey?: string): any {\n if (typeof R.getMetadataKeys !== 'function') return undefined\n const proto = target.prototype ?? target\n const keys: any[] = propertyKey\n ? R.getMetadataKeys(proto, propertyKey)\n : R.getMetadataKeys(target)\n\n const sym = keys.find((k: any) => typeof k === 'symbol' && k.description === key)\n if (!sym) return undefined\n\n return propertyKey ? R.getMetadata(sym, proto, propertyKey) : R.getMetadata(sym, target)\n}\n\nfunction isAuthAuthenticated(controllerClass: any, handlerName?: string): boolean {\n if (handlerName) {\n const val = getAuthMeta('auth:authenticated', controllerClass, handlerName)\n if (val !== undefined) return !!val\n }\n return !!getAuthMeta('auth:authenticated', controllerClass)\n}\n\nfunction isAuthPublic(controllerClass: any, handlerName: string): boolean {\n return !!getAuthMeta('auth:public', controllerClass, handlerName)\n}\n\nexport interface OpenAPIInfo {\n title: string\n version: string\n description?: string\n}\n\nexport interface SwaggerOptions {\n info?: Partial<OpenAPIInfo>\n servers?: { url: string; description?: string }[]\n bearerAuth?: boolean\n /**\n * Pluggable schema parser for converting validation schemas to JSON Schema.\n * Defaults to `zodSchemaParser` which handles Zod v4+ schemas.\n *\n * Override this to use Yup, Joi, Valibot, ArkType, or any other library.\n *\n * @example\n * ```ts\n * new SwaggerAdapter({\n * schemaParser: myYupParser,\n * })\n * ```\n */\n schemaParser?: SchemaParser\n}\n\ninterface RegisteredRoute {\n controllerClass: any\n mountPath: string\n}\n\nconst registeredRoutes: RegisteredRoute[] = []\n\n/** Register a controller for OpenAPI introspection (called by Application during route mounting) */\nexport function registerControllerForDocs(controllerClass: any, mountPath: string): void {\n registeredRoutes.push({ controllerClass, mountPath })\n}\n\n/** Clear all registered routes (for HMR) */\nexport function clearRegisteredRoutes(): void {\n registeredRoutes.length = 0\n}\n\n/** Build a full OpenAPI 3.0.3 spec from registered controllers and their decorators */\nexport function buildOpenAPISpec(options: SwaggerOptions = {}): any {\n const parser = options.schemaParser ?? zodSchemaParser\n\n /** Convert a validation schema to JSON Schema using the configured parser */\n const toJsonSchema = (schema: unknown): Record<string, unknown> | null => {\n try {\n if (!parser.supports(schema)) return null\n return parser.toJsonSchema(schema)\n } catch {\n return null\n }\n }\n\n const componentSchemas: Record<string, any> = {}\n let schemaCounter = 0\n\n /**\n * Register a schema in components.schemas and return a $ref pointer.\n * If the schema has a title/label, use that as the name. Otherwise generate one.\n */\n const registerSchema = (jsonSchema: Record<string, unknown>, hint?: string): any => {\n // Try to extract a name from the schema\n let name = (jsonSchema.title as string) || (jsonSchema.label as string) || hint || ''\n if (!name) {\n name = `Schema${++schemaCounter}`\n }\n // Sanitize name for OpenAPI (remove spaces, special chars)\n name = name.replace(/[^a-zA-Z0-9]/g, '')\n\n // Avoid duplicates — if already registered with same name, reuse\n if (!componentSchemas[name]) {\n const clean = { ...jsonSchema }\n delete clean.title\n delete clean.label\n delete clean.$schema\n componentSchemas[name] = clean\n }\n return { $ref: `#/components/schemas/${name}` }\n }\n\n const spec: any = {\n openapi: '3.0.3',\n info: {\n title: options.info?.title || 'API',\n version: options.info?.version || '1.0.0',\n ...(options.info?.description ? { description: options.info.description } : {}),\n },\n paths: {},\n components: { schemas: {}, securitySchemes: {} },\n tags: [],\n }\n\n if (options.servers) {\n // Drop entries whose URL can't be parsed by the browser's URL\n // constructor. Swagger UI runs `new URL(server.url)` on the client\n // and crashes with `Failed to construct 'URL': Invalid URL` if any\n // entry is malformed — which can happen on Windows dev when an\n // adapter hook populates servers with a path that was never meant\n // to be a URL. Relative URLs (e.g. '/') are allowed through.\n const validServers = options.servers.filter((s) => {\n if (!s?.url || typeof s.url !== 'string') return false\n if (s.url.startsWith('/')) return true\n try {\n new URL(s.url)\n return true\n } catch {\n return false\n }\n })\n if (validServers.length > 0) {\n spec.servers = validServers\n }\n }\n\n const allTags = new Set<string>()\n const securitySchemes: Record<string, any> = {}\n\n for (const { controllerClass, mountPath } of registeredRoutes) {\n // Skip excluded controllers\n if (hasClassMeta(SWAGGER_KEYS.EXCLUDE, controllerClass)) continue\n\n const routes: RouteDefinition[] = getClassMeta<RouteDefinition[]>(\n METADATA.ROUTES,\n controllerClass,\n [],\n )\n const classTags: string[] = getClassMeta<string[]>(SWAGGER_KEYS.TAGS, controllerClass, [])\n const classAuth: string | undefined = getClassMetaOrUndefined<string>(\n SWAGGER_KEYS.BEARER_AUTH,\n controllerClass,\n )\n for (const route of routes) {\n // Skip excluded methods\n if (getMethodMetaOrUndefined(SWAGGER_KEYS.EXCLUDE, controllerClass, route.handlerName))\n continue\n\n // Build the full path — mountPath is the actual Express mount prefix (from onRouteMount),\n // and route.path is the method-level path. @Controller path is not included here\n // because buildRoutes does not bake it into the router.\n const fullPath = joinPaths(mountPath, route.path)\n\n // Convert Express :param to OpenAPI {param}\n const openApiPath = fullPath.replace(/:([a-zA-Z_]+)/g, '{$1}')\n const method = route.method.toLowerCase()\n\n // Gather metadata\n const operation: ApiOperationOptions = getMethodMeta<ApiOperationOptions>(\n SWAGGER_KEYS.OPERATION,\n controllerClass,\n route.handlerName,\n {} as ApiOperationOptions,\n )\n const responses: ApiResponseOptions[] = getMethodMeta<ApiResponseOptions[]>(\n SWAGGER_KEYS.RESPONSES,\n controllerClass,\n route.handlerName,\n [],\n )\n const methodTags: string[] = getMethodMeta<string[]>(\n SWAGGER_KEYS.TAGS,\n controllerClass,\n route.handlerName,\n [],\n )\n const methodAuth: string | undefined = getMethodMetaOrUndefined<string>(\n SWAGGER_KEYS.BEARER_AUTH,\n controllerClass,\n route.handlerName,\n )\n\n // Tags — method level overrides class level\n const tags = methodTags.length > 0 ? methodTags : classTags\n tags.forEach((t) => allTags.add(t))\n\n // Build operation object\n const op: any = {\n ...(tags.length > 0 ? { tags } : {}),\n ...(operation.summary ? { summary: operation.summary } : {}),\n ...(operation.description ? { description: operation.description } : {}),\n ...(operation.operationId ? { operationId: operation.operationId } : {}),\n ...(operation.deprecated ? { deprecated: true } : {}),\n parameters: [],\n responses: {},\n }\n\n // Path parameters\n const paramMatches = fullPath.match(/:([a-zA-Z_]+)/g) || []\n for (const match of paramMatches) {\n const paramName = match.slice(1)\n let schema: any = { type: 'string' }\n\n // Try to get type from params validation schema\n if (route.validation?.params) {\n const jsonSchema = toJsonSchema(route.validation.params)\n if (jsonSchema?.properties && typeof jsonSchema.properties === 'object') {\n const props = jsonSchema.properties as Record<string, any>\n if (props[paramName]) {\n schema = props[paramName]\n }\n }\n }\n\n op.parameters.push({\n name: paramName,\n in: 'path',\n required: true,\n schema,\n })\n }\n\n // Query parameters\n if (route.validation?.query) {\n const jsonSchema = toJsonSchema(route.validation.query)\n if (jsonSchema?.properties && typeof jsonSchema.properties === 'object') {\n const required = Array.isArray(jsonSchema.required) ? jsonSchema.required : []\n for (const [name, propSchema] of Object.entries(\n jsonSchema.properties as Record<string, any>,\n )) {\n op.parameters.push({\n name,\n in: 'query',\n required: required.includes(name),\n schema: propSchema,\n })\n }\n }\n }\n\n // @ApiQueryParams decorator — document filterable/sortable/searchable fields\n const queryParamsConfig = getMethodMetaOrUndefined<any>(\n METADATA.QUERY_PARAMS,\n controllerClass,\n route.handlerName,\n )\n if (queryParamsConfig) {\n if (queryParamsConfig.filterable?.length) {\n op.parameters.push({\n name: 'filter',\n in: 'query',\n required: false,\n description: `Filter fields: ${queryParamsConfig.filterable.join(', ')}. Format: \\`field:operator:value\\`. Operators: eq, neq, gt, gte, lt, lte, contains, starts, ends, in, between`,\n schema: { type: 'array', items: { type: 'string' } },\n style: 'form',\n explode: true,\n })\n }\n if (queryParamsConfig.sortable?.length) {\n op.parameters.push({\n name: 'sort',\n in: 'query',\n required: false,\n description: `Sort fields: ${queryParamsConfig.sortable.join(', ')}. Format: \\`field:asc\\` or \\`field:desc\\``,\n schema: { type: 'array', items: { type: 'string' } },\n style: 'form',\n explode: true,\n })\n }\n if (queryParamsConfig.searchable?.length) {\n op.parameters.push({\n name: 'q',\n in: 'query',\n required: false,\n description: `Search across: ${queryParamsConfig.searchable.join(', ')}`,\n schema: { type: 'string' },\n })\n }\n op.parameters.push(\n {\n name: 'page',\n in: 'query',\n required: false,\n description: 'Page number (default: 1)',\n schema: { type: 'integer', minimum: 1, default: 1 },\n },\n {\n name: 'limit',\n in: 'query',\n required: false,\n description: 'Items per page (default: 20, max: 100)',\n schema: { type: 'integer', minimum: 1, maximum: 100, default: 20 },\n },\n )\n }\n\n // Remove empty parameters array\n if (op.parameters.length === 0) delete op.parameters\n\n // Request body\n if (route.validation?.body && ['post', 'put', 'patch'].includes(method)) {\n const bodySchema = toJsonSchema(route.validation.body)\n if (bodySchema) {\n const bodyName = route.validation.name || `${route.handlerName}Body`\n const ref = registerSchema(bodySchema, bodyName)\n op.requestBody = {\n required: true,\n content: { 'application/json': { schema: ref } },\n }\n }\n }\n\n // File upload detection\n const fileUpload = getMethodMetaOrUndefined<any>(\n METADATA.FILE_UPLOAD,\n controllerClass,\n route.handlerName,\n )\n if (fileUpload) {\n const fieldName = fileUpload.fieldName ?? 'file'\n const properties: any = {}\n\n if (fileUpload.mode === 'array') {\n properties[fieldName] = {\n type: 'array',\n items: { type: 'string', format: 'binary' },\n }\n } else if (fileUpload.mode !== 'none') {\n properties[fieldName] = {\n type: 'string',\n format: 'binary',\n }\n }\n\n op.requestBody = {\n required: true,\n content: {\n 'multipart/form-data': {\n schema: { type: 'object', properties },\n },\n },\n }\n }\n\n // Responses\n if (responses.length > 0) {\n for (const resp of responses) {\n op.responses[String(resp.status)] = {\n description: resp.description || '',\n ...(resp.schema\n ? (() => {\n const converted =\n typeof resp.schema === 'function' || typeof resp.schema === 'object'\n ? toJsonSchema(resp.schema)\n : null\n const schemaName = resp.name || `${route.handlerName}Response${resp.status}`\n const finalSchema = converted\n ? registerSchema(converted, schemaName)\n : typeof resp.schema === 'object'\n ? resp.schema\n : undefined\n return finalSchema\n ? { content: { 'application/json': { schema: finalSchema } } }\n : {}\n })()\n : {}),\n }\n }\n } else {\n // Auto-generate default responses\n const defaultStatus = method === 'post' ? '201' : method === 'delete' ? '204' : '200'\n op.responses[defaultStatus] = { description: 'Successful operation' }\n\n if (route.validation?.body) {\n op.responses['422'] = { description: 'Validation error' }\n }\n }\n\n // Security — check Swagger @BearerAuth() first, then fall back to\n // @forinda/kickjs-auth decorators (@Authenticated, @Public, @Roles)\n const authName = methodAuth || classAuth\n const isPublicRoute = isAuthPublic(controllerClass, route.handlerName)\n const isAuthRequired =\n authName ||\n isAuthAuthenticated(controllerClass, route.handlerName) ||\n isAuthAuthenticated(controllerClass)\n\n if (!isPublicRoute && isAuthRequired) {\n const schemeName = authName || 'BearerAuth'\n op.security = [{ [schemeName]: [] }]\n securitySchemes[schemeName] = securitySchemes[schemeName] || {\n type: 'http',\n scheme: 'bearer',\n bearerFormat: 'JWT',\n }\n }\n\n // Mount\n if (!spec.paths[openApiPath]) spec.paths[openApiPath] = {}\n spec.paths[openApiPath][method] = op\n }\n }\n\n // Finalize\n spec.tags = Array.from(allTags).map((name) => ({ name }))\n spec.components.securitySchemes = securitySchemes\n\n if (options.bearerAuth) {\n if (!securitySchemes.BearerAuth) {\n spec.components.securitySchemes.BearerAuth = {\n type: 'http',\n scheme: 'bearer',\n bearerFormat: 'JWT',\n }\n }\n spec.security = [{ BearerAuth: [] }]\n }\n\n // Merge collected schemas into components\n spec.components.schemas = componentSchemas\n\n // Clean up empty components\n if (Object.keys(spec.components.schemas).length === 0) delete spec.components.schemas\n if (Object.keys(spec.components.securitySchemes).length === 0)\n delete spec.components.securitySchemes\n if (Object.keys(spec.components).length === 0) delete spec.components\n\n return spec\n}\n","/** Escape a string for safe HTML attribute/content interpolation */\nfunction escapeHtml(str: string): string {\n return str\n .replace(/&/g, '&')\n .replace(/</g, '<')\n .replace(/>/g, '>')\n .replace(/\"/g, '"')\n .replace(/'/g, ''')\n}\n\n/**\n * Generate Swagger UI HTML using local assets from swagger-ui-dist.\n *\n * Assets are served from `/_swagger-assets/` by the adapter's Express\n * static middleware. Falls back to CDN if the local path is not provided.\n * This ensures Swagger UI works fully offline in development.\n *\n * @param specUrl - Path to the OpenAPI JSON spec (e.g., '/openapi.json')\n * @param title - Page title\n * @param assetsPath - Base path for local swagger-ui-dist assets (e.g., '/_swagger-assets')\n */\nexport function swaggerUIHtml(specUrl: string, title = 'API Docs', assetsPath?: string): string {\n const safeTitle = escapeHtml(title)\n // JSON-stringify for safe inlining into the `<script>` block. The inline\n // script below resolves this to an absolute URL against\n // `window.location.origin` before passing it to SwaggerUIBundle —\n // some swagger-ui-dist builds call `new URL(url)` without a base and\n // crash with `Failed to construct 'URL': Invalid URL` when the value\n // is a bare path like `/openapi.json`.\n const safeUrl = JSON.stringify(specUrl).replace(/</g, '\\\\u003c')\n\n // Use local assets if available, CDN as fallback\n const cssHref = assetsPath\n ? `${assetsPath}/swagger-ui.css`\n : 'https://unpkg.com/swagger-ui-dist@5/swagger-ui.css'\n const bundleSrc = assetsPath\n ? `${assetsPath}/swagger-ui-bundle.js`\n : 'https://unpkg.com/swagger-ui-dist@5/swagger-ui-bundle.js'\n const presetSrc = assetsPath\n ? `${assetsPath}/swagger-ui-standalone-preset.js`\n : 'https://unpkg.com/swagger-ui-dist@5/swagger-ui-standalone-preset.js'\n\n return `<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n <meta charset=\"UTF-8\">\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n <title>${safeTitle}</title>\n <link rel=\"stylesheet\" href=\"${cssHref}\">\n</head>\n<body>\n <div id=\"swagger-ui\"></div>\n <script src=\"${bundleSrc}\"></script>\n <script src=\"${presetSrc}\"></script>\n <script>\n (function () {\n var rawUrl = ${safeUrl};\n var specUrl;\n try {\n specUrl = new URL(rawUrl, window.location.origin).href;\n } catch (_e) {\n specUrl = rawUrl;\n }\n SwaggerUIBundle({\n url: specUrl,\n dom_id: '#swagger-ui',\n deepLinking: true,\n presets: [SwaggerUIBundle.presets.apis, SwaggerUIStandalonePreset],\n plugins: [SwaggerUIBundle.plugins.DownloadUrl],\n layout: 'StandaloneLayout',\n });\n })();\n </script>\n</body>\n</html>`\n}\n\n/**\n * Generate ReDoc HTML.\n *\n * ReDoc doesn't publish a standalone npm package suitable for local serving,\n * so it still loads from CDN. If offline support for ReDoc is needed,\n * vendor the standalone bundle into the package's public/ directory.\n */\nexport function redocHtml(specUrl: string, title = 'API Docs'): string {\n const safeTitle = escapeHtml(title)\n const safeUrl = escapeHtml(specUrl)\n\n return `<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n <meta charset=\"UTF-8\">\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n <title>${safeTitle}</title>\n</head>\n<body>\n <redoc spec-url=\"${safeUrl}\"></redoc>\n <script src=\"https://cdn.redoc.ly/redoc/latest/bundles/redoc.standalone.js\"></script>\n</body>\n</html>`\n}\n","import { dirname } from 'node:path'\nimport { createRequire } from 'node:module'\nimport express, { Router } from 'express'\nimport { Logger, type AppAdapter, type AdapterContext } from '@forinda/kickjs'\nimport {\n buildOpenAPISpec,\n registerControllerForDocs,\n clearRegisteredRoutes,\n type SwaggerOptions,\n} from './openapi-builder'\nimport { swaggerUIHtml, redocHtml } from './ui'\n\nconst log = Logger.for('SwaggerAdapter')\n\n/**\n * Resolve the absolute path to swagger-ui-dist's static assets.\n * Uses createRequire to find it relative to this package (works with pnpm).\n */\nfunction getSwaggerUiDistPath(): string {\n const require = createRequire(import.meta.url)\n return dirname(require.resolve('swagger-ui-dist/package.json'))\n}\n\nexport interface SwaggerAdapterOptions extends SwaggerOptions {\n /** Path to serve Swagger UI (default: '/docs') */\n docsPath?: string\n /** Path to serve ReDoc (default: '/redoc') */\n redocPath?: string\n /** Path to serve the raw JSON spec (default: '/openapi.json') */\n specPath?: string\n /** Other adapters to discover (e.g., WsAdapter for WebSocket server URLs) */\n adapters?: any[]\n /**\n * When true, the adapter is a no-op while `NODE_ENV === 'production'` —\n * docs, spec, and assets are not mounted. Useful for keeping API docs\n * out of production builds without conditionally constructing the adapter.\n */\n disableInProd?: boolean\n}\n\n/**\n * Swagger adapter — auto-generates OpenAPI spec from decorators and serves docs.\n *\n * Assets are served locally from `swagger-ui-dist` (npm dependency) —\n * no CDN required, works fully offline.\n *\n * @example\n * ```ts\n * bootstrap({\n * modules,\n * adapters: [\n * new 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 class SwaggerAdapter implements AppAdapter {\n name = 'SwaggerAdapter'\n\n constructor(private readonly options: SwaggerAdapterOptions = {}) {}\n\n /** Whether the adapter should skip mounting in the current environment */\n private get disabled(): boolean {\n return Boolean(this.options.disableInProd) && process.env.NODE_ENV === 'production'\n }\n\n /** Auto-detect server URLs from the running HTTP server and peer adapters */\n afterStart({ server }: AdapterContext): void {\n if (this.disabled) return\n const addr = server?.address?.()\n if (!addr || typeof addr !== 'object') return\n\n const host = addr.address === '::' || addr.address === '0.0.0.0' ? 'localhost' : addr.address\n\n // Auto-add HTTP server URL if none configured\n if (!this.options.servers || this.options.servers.length === 0) {\n this.options.servers = [{ url: `http://${host}:${addr.port}`, description: 'HTTP server' }]\n }\n\n // Auto-add WebSocket server URLs from WsAdapter\n const wsAdapter = this.options.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 this.options.servers?.push({\n url: `ws://${host}:${addr.port}${namespace}`,\n description: `WebSocket: ${namespace}`,\n })\n }\n }\n }\n\n /** Collect controller metadata as routes are mounted */\n onRouteMount(controllerClass: any, mountPath: string): void {\n if (this.disabled) return\n registerControllerForDocs(controllerClass, mountPath)\n }\n\n beforeMount({ app }: AdapterContext): void {\n if (this.disabled) {\n log.info('Swagger disabled in production (disableInProd=true)')\n return\n }\n // Clear previous registrations (supports HMR rebuild)\n clearRegisteredRoutes()\n const docsPath = this.options.docsPath ?? '/docs'\n const redocPath = this.options.redocPath ?? '/redoc'\n const specPath = this.options.specPath ?? '/openapi.json'\n let uiDistAvailable = false\n\n const docsRouter = Router()\n\n // ── Serve swagger-ui-dist static assets locally ──────────────────\n // This makes Swagger UI work offline — no CDN needed.\n // Assets served at /_swagger-assets/ (CSS, JS, fonts, etc.)\n const swaggerAssetsPath = '/_swagger-assets'\n try {\n const swaggerDistDir = getSwaggerUiDistPath()\n docsRouter.use(swaggerAssetsPath, express.static(swaggerDistDir))\n uiDistAvailable = true\n } catch {\n log.warn('swagger-ui-dist not found — Swagger UI will load from CDN (requires internet).')\n }\n\n // Relax CSP for Swagger UI in both local and CDN modes (inline script is used in both)\n docsRouter.use((_req, res, next) => {\n // Build connect-src dynamically so \"Try it out\" can call any configured server URL.\n // Includes dev-friendly localhost/127.0.0.1 origins so docs served from one host\n // can call an API spec'd at the other (a common cross-origin gotcha).\n const serverOrigins = new Set<string>()\n for (const s of this.options.servers ?? []) {\n try {\n serverOrigins.add(new URL(s.url).origin)\n } catch {\n // ignore relative or malformed URLs\n }\n }\n const connectSrc = [\n \"'self'\",\n 'http://localhost:*',\n 'http://127.0.0.1:*',\n 'https://localhost:*',\n 'https://127.0.0.1:*',\n 'ws://localhost:*',\n 'ws://127.0.0.1:*',\n ...serverOrigins,\n ].join(' ')\n\n res.setHeader(\n 'Content-Security-Policy',\n [\n \"default-src 'self'\",\n \"script-src 'self' 'unsafe-inline' https://unpkg.com https://cdn.redoc.ly https://cdn.jsdelivr.net\",\n \"style-src 'self' 'unsafe-inline' https://unpkg.com https://fonts.googleapis.com\",\n \"font-src 'self' https://fonts.gstatic.com\",\n \"img-src 'self' data: https://unpkg.com\",\n `connect-src ${connectSrc}`,\n ].join('; '),\n )\n next()\n })\n\n // Spec endpoint (JSON)\n docsRouter.get(specPath, (_req, res) => {\n const spec = buildOpenAPISpec(this.options)\n res.json(spec)\n })\n\n // Swagger UI — uses local assets if available, CDN fallback\n docsRouter.get(docsPath, (_req, res) => {\n res\n .type('html')\n .send(\n swaggerUIHtml(\n specPath,\n this.options.info?.title,\n uiDistAvailable ? swaggerAssetsPath : undefined,\n ),\n )\n })\n\n // ReDoc — still CDN-based (no npm package for standalone bundle)\n docsRouter.get(redocPath, (_req, res) => {\n res.type('html').send(redocHtml(specPath, this.options.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// 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":[],"sources":["../src/schema-parser.ts","../src/decorators.ts","../src/openapi-builder.ts","../src/ui.ts","../src/swagger.adapter.ts"],"sourcesContent":["/**\n * Interface for converting validation library schemas to JSON Schema.\n *\n * KickJS ships with a Zod parser by default. To use a different validation\n * library (Yup, Joi, Valibot, ArkType, etc.), implement this interface and\n * pass it to the SwaggerAdapter.\n *\n * @example\n * ```ts\n * import Joi from 'joi'\n * import joiToJson from 'joi-to-json'\n *\n * const joiParser: SchemaParser = {\n * name: 'joi',\n * supports: (schema) => Joi.isSchema(schema),\n * toJsonSchema: (schema) => joiToJson(schema),\n * }\n *\n * SwaggerAdapter({ schemaParser: joiParser })\n * ```\n */\nexport interface SchemaParser {\n /** Human-readable name for logging/debugging */\n readonly name: string\n\n /**\n * Return true if this parser can handle the given schema object.\n * Called before `toJsonSchema` to allow graceful fallback.\n */\n supports(schema: unknown): boolean\n\n /**\n * Convert a validation schema to a JSON Schema object.\n * Should return a plain object conforming to JSON Schema draft-07 or later.\n * Must not include the top-level `$schema` key — the builder adds it.\n */\n toJsonSchema(schema: unknown): Record<string, unknown>\n}\n\n/**\n * Default schema parser for Zod v4+.\n * Uses Zod's built-in `.toJSONSchema()` instance method.\n */\nexport const zodSchemaParser: SchemaParser = {\n name: 'zod',\n\n supports(schema: unknown): boolean {\n return (\n schema != null &&\n typeof schema === 'object' &&\n typeof (schema as any).safeParse === 'function' &&\n typeof (schema as any).toJSONSchema === 'function'\n )\n },\n\n toJsonSchema(schema: unknown): Record<string, unknown> {\n const { $schema: _, ...rest } = (schema as any).toJSONSchema() as Record<string, unknown>\n return rest\n },\n}\n","import { setMethodMeta, setClassMeta, pushMethodMeta } from '@forinda/kickjs'\n\nconst SWAGGER_KEYS = {\n OPERATION: Symbol('kick:swagger:operation'),\n RESPONSES: Symbol('kick:swagger:responses'),\n TAGS: Symbol('kick:swagger:tags'),\n BEARER_AUTH: Symbol('kick:swagger:bearer'),\n EXCLUDE: Symbol('kick:swagger:exclude'),\n}\n\nexport { SWAGGER_KEYS }\n\nexport interface ApiOperationOptions {\n summary?: string\n description?: string\n operationId?: string\n deprecated?: boolean\n}\n\nexport interface ApiResponseOptions {\n status: number\n description?: string\n schema?: any\n /** Schema name in components/schemas (e.g., 'UserResponse', 'ErrorBody'). Auto-generated from handler name if omitted. */\n name?: string\n}\n\n/** Attach operation metadata to a route handler */\nexport function ApiOperation(options: ApiOperationOptions): MethodDecorator {\n return (target, propertyKey) => {\n setMethodMeta(SWAGGER_KEYS.OPERATION, options, target.constructor, propertyKey as string)\n }\n}\n\n/** Document a response status. Can be stacked multiple times. */\nexport function ApiResponse(options: ApiResponseOptions): MethodDecorator {\n return (target, propertyKey) => {\n pushMethodMeta<ApiResponseOptions>(\n SWAGGER_KEYS.RESPONSES,\n target.constructor,\n propertyKey as string,\n options,\n )\n }\n}\n\n/** Apply OpenAPI tags at class or method level */\nexport function ApiTags(...tags: string[]): ClassDecorator & MethodDecorator {\n return (target: any, propertyKey?: string | symbol) => {\n if (propertyKey) {\n setMethodMeta(SWAGGER_KEYS.TAGS, tags, target.constructor, propertyKey as string)\n } else {\n setClassMeta(SWAGGER_KEYS.TAGS, tags, target)\n }\n }\n}\n\n/** Mark endpoint as requiring Bearer token auth */\nexport function ApiBearerAuth(name = 'BearerAuth'): ClassDecorator & MethodDecorator {\n return (target: any, propertyKey?: string | symbol) => {\n if (propertyKey) {\n setMethodMeta(SWAGGER_KEYS.BEARER_AUTH, name, target.constructor, propertyKey as string)\n } else {\n setClassMeta(SWAGGER_KEYS.BEARER_AUTH, name, target)\n }\n }\n}\n\n/** Exclude a controller or method from the OpenAPI spec */\nexport function ApiExclude(): ClassDecorator & MethodDecorator {\n return (target: any, propertyKey?: string | symbol) => {\n if (propertyKey) {\n setMethodMeta(SWAGGER_KEYS.EXCLUDE, true, target.constructor, propertyKey as string)\n } else {\n setClassMeta(SWAGGER_KEYS.EXCLUDE, true, target)\n }\n }\n}\n","import {\n METADATA,\n joinPaths,\n type RouteDefinition,\n getClassMeta,\n getClassMetaOrUndefined,\n getMethodMeta,\n getMethodMetaOrUndefined,\n hasClassMeta,\n} from '@forinda/kickjs'\nimport { SWAGGER_KEYS, type ApiOperationOptions, type ApiResponseOptions } from './decorators'\nimport { zodSchemaParser, type SchemaParser } from './schema-parser'\n\n// ── Auth metadata bridge ──────────────────────────────────────────────\n// Check @forinda/kickjs-auth decorators without importing the auth package.\n// Symbols are matched by description to avoid a hard dependency.\n\nconst R = Reflect as any\n\nfunction getAuthMeta(key: string, target: any, propertyKey?: string): any {\n if (typeof R.getMetadataKeys !== 'function') return undefined\n const proto = target.prototype ?? target\n const keys: any[] = propertyKey\n ? R.getMetadataKeys(proto, propertyKey)\n : R.getMetadataKeys(target)\n\n const sym = keys.find((k: any) => typeof k === 'symbol' && k.description === key)\n if (!sym) return undefined\n\n return propertyKey ? R.getMetadata(sym, proto, propertyKey) : R.getMetadata(sym, target)\n}\n\nfunction isAuthAuthenticated(controllerClass: any, handlerName?: string): boolean {\n if (handlerName) {\n const val = getAuthMeta('auth:authenticated', controllerClass, handlerName)\n if (val !== undefined) return !!val\n }\n return !!getAuthMeta('auth:authenticated', controllerClass)\n}\n\nfunction isAuthPublic(controllerClass: any, handlerName: string): boolean {\n return !!getAuthMeta('auth:public', controllerClass, handlerName)\n}\n\nexport interface OpenAPIInfo {\n title: string\n version: string\n description?: string\n}\n\nexport interface SwaggerOptions {\n info?: Partial<OpenAPIInfo>\n servers?: { url: string; description?: string }[]\n bearerAuth?: boolean\n /**\n * Pluggable schema parser for converting validation schemas to JSON Schema.\n * Defaults to `zodSchemaParser` which handles Zod v4+ schemas.\n *\n * Override this to use Yup, Joi, Valibot, ArkType, or any other library.\n *\n * @example\n * ```ts\n * SwaggerAdapter({\n * schemaParser: myYupParser,\n * })\n * ```\n */\n schemaParser?: SchemaParser\n}\n\ninterface RegisteredRoute {\n controllerClass: any\n mountPath: string\n}\n\nconst registeredRoutes: RegisteredRoute[] = []\n\n/** Register a controller for OpenAPI introspection (called by Application during route mounting) */\nexport function registerControllerForDocs(controllerClass: any, mountPath: string): void {\n registeredRoutes.push({ controllerClass, mountPath })\n}\n\n/** Clear all registered routes (for HMR) */\nexport function clearRegisteredRoutes(): void {\n registeredRoutes.length = 0\n}\n\n/** Build a full OpenAPI 3.0.3 spec from registered controllers and their decorators */\nexport function buildOpenAPISpec(options: SwaggerOptions = {}): any {\n const parser = options.schemaParser ?? zodSchemaParser\n\n /** Convert a validation schema to JSON Schema using the configured parser */\n const toJsonSchema = (schema: unknown): Record<string, unknown> | null => {\n try {\n if (!parser.supports(schema)) return null\n return parser.toJsonSchema(schema)\n } catch {\n return null\n }\n }\n\n const componentSchemas: Record<string, any> = {}\n let schemaCounter = 0\n\n /**\n * Register a schema in components.schemas and return a $ref pointer.\n * If the schema has a title/label, use that as the name. Otherwise generate one.\n */\n const registerSchema = (jsonSchema: Record<string, unknown>, hint?: string): any => {\n // Try to extract a name from the schema\n let name = (jsonSchema.title as string) || (jsonSchema.label as string) || hint || ''\n if (!name) {\n name = `Schema${++schemaCounter}`\n }\n // Sanitize name for OpenAPI (remove spaces, special chars)\n name = name.replace(/[^a-zA-Z0-9]/g, '')\n\n // Avoid duplicates — if already registered with same name, reuse\n if (!componentSchemas[name]) {\n const clean = { ...jsonSchema }\n delete clean.title\n delete clean.label\n delete clean.$schema\n componentSchemas[name] = clean\n }\n return { $ref: `#/components/schemas/${name}` }\n }\n\n const spec: any = {\n openapi: '3.0.3',\n info: {\n title: options.info?.title || 'API',\n version: options.info?.version || '1.0.0',\n ...(options.info?.description ? { description: options.info.description } : {}),\n },\n paths: {},\n components: { schemas: {}, securitySchemes: {} },\n tags: [],\n }\n\n if (options.servers) {\n // Drop entries whose URL can't be parsed by the browser's URL\n // constructor. Swagger UI runs `new URL(server.url)` on the client\n // and crashes with `Failed to construct 'URL': Invalid URL` if any\n // entry is malformed — which can happen on Windows dev when an\n // adapter hook populates servers with a path that was never meant\n // to be a URL. Relative URLs (e.g. '/') are allowed through.\n const validServers = options.servers.filter((s) => {\n if (!s?.url || typeof s.url !== 'string') return false\n if (s.url.startsWith('/')) return true\n try {\n new URL(s.url)\n return true\n } catch {\n return false\n }\n })\n if (validServers.length > 0) {\n spec.servers = validServers\n }\n }\n\n const allTags = new Set<string>()\n const securitySchemes: Record<string, any> = {}\n\n for (const { controllerClass, mountPath } of registeredRoutes) {\n // Skip excluded controllers\n if (hasClassMeta(SWAGGER_KEYS.EXCLUDE, controllerClass)) continue\n\n const routes: RouteDefinition[] = getClassMeta<RouteDefinition[]>(\n METADATA.ROUTES,\n controllerClass,\n [],\n )\n const classTags: string[] = getClassMeta<string[]>(SWAGGER_KEYS.TAGS, controllerClass, [])\n const classAuth: string | undefined = getClassMetaOrUndefined<string>(\n SWAGGER_KEYS.BEARER_AUTH,\n controllerClass,\n )\n for (const route of routes) {\n // Skip excluded methods\n if (getMethodMetaOrUndefined(SWAGGER_KEYS.EXCLUDE, controllerClass, route.handlerName))\n continue\n\n // Build the full path — mountPath is the actual Express mount prefix (from onRouteMount),\n // and route.path is the method-level path. @Controller path is not included here\n // because buildRoutes does not bake it into the router.\n const fullPath = joinPaths(mountPath, route.path)\n\n // Convert Express :param to OpenAPI {param}\n const openApiPath = fullPath.replace(/:([a-zA-Z_]+)/g, '{$1}')\n const method = route.method.toLowerCase()\n\n // Gather metadata\n const operation: ApiOperationOptions = getMethodMeta<ApiOperationOptions>(\n SWAGGER_KEYS.OPERATION,\n controllerClass,\n route.handlerName,\n {} as ApiOperationOptions,\n )\n const responses: ApiResponseOptions[] = getMethodMeta<ApiResponseOptions[]>(\n SWAGGER_KEYS.RESPONSES,\n controllerClass,\n route.handlerName,\n [],\n )\n const methodTags: string[] = getMethodMeta<string[]>(\n SWAGGER_KEYS.TAGS,\n controllerClass,\n route.handlerName,\n [],\n )\n const methodAuth: string | undefined = getMethodMetaOrUndefined<string>(\n SWAGGER_KEYS.BEARER_AUTH,\n controllerClass,\n route.handlerName,\n )\n\n // Tags — method level overrides class level\n const tags = methodTags.length > 0 ? methodTags : classTags\n tags.forEach((t) => allTags.add(t))\n\n // Build operation object\n const op: any = {\n ...(tags.length > 0 ? { tags } : {}),\n ...(operation.summary ? { summary: operation.summary } : {}),\n ...(operation.description ? { description: operation.description } : {}),\n ...(operation.operationId ? { operationId: operation.operationId } : {}),\n ...(operation.deprecated ? { deprecated: true } : {}),\n parameters: [],\n responses: {},\n }\n\n // Path parameters\n const paramMatches = fullPath.match(/:([a-zA-Z_]+)/g) || []\n for (const match of paramMatches) {\n const paramName = match.slice(1)\n let schema: any = { type: 'string' }\n\n // Try to get type from params validation schema\n if (route.validation?.params) {\n const jsonSchema = toJsonSchema(route.validation.params)\n if (jsonSchema?.properties && typeof jsonSchema.properties === 'object') {\n const props = jsonSchema.properties as Record<string, any>\n if (props[paramName]) {\n schema = props[paramName]\n }\n }\n }\n\n op.parameters.push({\n name: paramName,\n in: 'path',\n required: true,\n schema,\n })\n }\n\n // Query parameters\n if (route.validation?.query) {\n const jsonSchema = toJsonSchema(route.validation.query)\n if (jsonSchema?.properties && typeof jsonSchema.properties === 'object') {\n const required = Array.isArray(jsonSchema.required) ? jsonSchema.required : []\n for (const [name, propSchema] of Object.entries(\n jsonSchema.properties as Record<string, any>,\n )) {\n op.parameters.push({\n name,\n in: 'query',\n required: required.includes(name),\n schema: propSchema,\n })\n }\n }\n }\n\n // @ApiQueryParams decorator — document filterable/sortable/searchable fields\n const queryParamsConfig = getMethodMetaOrUndefined<any>(\n METADATA.QUERY_PARAMS,\n controllerClass,\n route.handlerName,\n )\n if (queryParamsConfig) {\n if (queryParamsConfig.filterable?.length) {\n op.parameters.push({\n name: 'filter',\n in: 'query',\n required: false,\n description: `Filter fields: ${queryParamsConfig.filterable.join(', ')}. Format: \\`field:operator:value\\`. Operators: eq, neq, gt, gte, lt, lte, contains, starts, ends, in, between`,\n schema: { type: 'array', items: { type: 'string' } },\n style: 'form',\n explode: true,\n })\n }\n if (queryParamsConfig.sortable?.length) {\n op.parameters.push({\n name: 'sort',\n in: 'query',\n required: false,\n description: `Sort fields: ${queryParamsConfig.sortable.join(', ')}. Format: \\`field:asc\\` or \\`field:desc\\``,\n schema: { type: 'array', items: { type: 'string' } },\n style: 'form',\n explode: true,\n })\n }\n if (queryParamsConfig.searchable?.length) {\n op.parameters.push({\n name: 'q',\n in: 'query',\n required: false,\n description: `Search across: ${queryParamsConfig.searchable.join(', ')}`,\n schema: { type: 'string' },\n })\n }\n op.parameters.push(\n {\n name: 'page',\n in: 'query',\n required: false,\n description: 'Page number (default: 1)',\n schema: { type: 'integer', minimum: 1, default: 1 },\n },\n {\n name: 'limit',\n in: 'query',\n required: false,\n description: 'Items per page (default: 20, max: 100)',\n schema: { type: 'integer', minimum: 1, maximum: 100, default: 20 },\n },\n )\n }\n\n // Remove empty parameters array\n if (op.parameters.length === 0) delete op.parameters\n\n // Request body\n if (route.validation?.body && ['post', 'put', 'patch'].includes(method)) {\n const bodySchema = toJsonSchema(route.validation.body)\n if (bodySchema) {\n const bodyName = route.validation.name || `${route.handlerName}Body`\n const ref = registerSchema(bodySchema, bodyName)\n op.requestBody = {\n required: true,\n content: { 'application/json': { schema: ref } },\n }\n }\n }\n\n // File upload detection\n const fileUpload = getMethodMetaOrUndefined<any>(\n METADATA.FILE_UPLOAD,\n controllerClass,\n route.handlerName,\n )\n if (fileUpload) {\n const fieldName = fileUpload.fieldName ?? 'file'\n const properties: any = {}\n\n if (fileUpload.mode === 'array') {\n properties[fieldName] = {\n type: 'array',\n items: { type: 'string', format: 'binary' },\n }\n } else if (fileUpload.mode !== 'none') {\n properties[fieldName] = {\n type: 'string',\n format: 'binary',\n }\n }\n\n op.requestBody = {\n required: true,\n content: {\n 'multipart/form-data': {\n schema: { type: 'object', properties },\n },\n },\n }\n }\n\n // Responses\n if (responses.length > 0) {\n for (const resp of responses) {\n op.responses[String(resp.status)] = {\n description: resp.description || '',\n ...(resp.schema\n ? (() => {\n const converted =\n typeof resp.schema === 'function' || typeof resp.schema === 'object'\n ? toJsonSchema(resp.schema)\n : null\n const schemaName = resp.name || `${route.handlerName}Response${resp.status}`\n const finalSchema = converted\n ? registerSchema(converted, schemaName)\n : typeof resp.schema === 'object'\n ? resp.schema\n : undefined\n return finalSchema\n ? { content: { 'application/json': { schema: finalSchema } } }\n : {}\n })()\n : {}),\n }\n }\n } else {\n // Auto-generate default responses\n const defaultStatus = method === 'post' ? '201' : method === 'delete' ? '204' : '200'\n op.responses[defaultStatus] = { description: 'Successful operation' }\n\n if (route.validation?.body) {\n op.responses['422'] = { description: 'Validation error' }\n }\n }\n\n // Security — check Swagger @BearerAuth() first, then fall back to\n // @forinda/kickjs-auth decorators (@Authenticated, @Public, @Roles)\n const authName = methodAuth || classAuth\n const isPublicRoute = isAuthPublic(controllerClass, route.handlerName)\n const isAuthRequired =\n authName ||\n isAuthAuthenticated(controllerClass, route.handlerName) ||\n isAuthAuthenticated(controllerClass)\n\n if (!isPublicRoute && isAuthRequired) {\n const schemeName = authName || 'BearerAuth'\n op.security = [{ [schemeName]: [] }]\n securitySchemes[schemeName] = securitySchemes[schemeName] || {\n type: 'http',\n scheme: 'bearer',\n bearerFormat: 'JWT',\n }\n }\n\n // Mount\n if (!spec.paths[openApiPath]) spec.paths[openApiPath] = {}\n spec.paths[openApiPath][method] = op\n }\n }\n\n // Finalize\n spec.tags = Array.from(allTags).map((name) => ({ name }))\n spec.components.securitySchemes = securitySchemes\n\n if (options.bearerAuth) {\n if (!securitySchemes.BearerAuth) {\n spec.components.securitySchemes.BearerAuth = {\n type: 'http',\n scheme: 'bearer',\n bearerFormat: 'JWT',\n }\n }\n spec.security = [{ BearerAuth: [] }]\n }\n\n // Merge collected schemas into components\n spec.components.schemas = componentSchemas\n\n // Clean up empty components\n if (Object.keys(spec.components.schemas).length === 0) delete spec.components.schemas\n if (Object.keys(spec.components.securitySchemes).length === 0)\n delete spec.components.securitySchemes\n if (Object.keys(spec.components).length === 0) delete spec.components\n\n return spec\n}\n","/** Escape a string for safe HTML attribute/content interpolation */\nfunction escapeHtml(str: string): string {\n return str\n .replace(/&/g, '&')\n .replace(/</g, '<')\n .replace(/>/g, '>')\n .replace(/\"/g, '"')\n .replace(/'/g, ''')\n}\n\n/**\n * Generate Swagger UI HTML using local assets from swagger-ui-dist.\n *\n * Assets are served from `/_swagger-assets/` by the adapter's Express\n * static middleware. Falls back to CDN if the local path is not provided.\n * This ensures Swagger UI works fully offline in development.\n *\n * @param specUrl - Path to the OpenAPI JSON spec (e.g., '/openapi.json')\n * @param title - Page title\n * @param assetsPath - Base path for local swagger-ui-dist assets (e.g., '/_swagger-assets')\n */\nexport function swaggerUIHtml(specUrl: string, title = 'API Docs', assetsPath?: string): string {\n const safeTitle = escapeHtml(title)\n // JSON-stringify for safe inlining into the `<script>` block. The inline\n // script below resolves this to an absolute URL against\n // `window.location.origin` before passing it to SwaggerUIBundle —\n // some swagger-ui-dist builds call `new URL(url)` without a base and\n // crash with `Failed to construct 'URL': Invalid URL` when the value\n // is a bare path like `/openapi.json`.\n const safeUrl = JSON.stringify(specUrl).replace(/</g, '\\\\u003c')\n\n // Use local assets if available, CDN as fallback\n const cssHref = assetsPath\n ? `${assetsPath}/swagger-ui.css`\n : 'https://unpkg.com/swagger-ui-dist@5/swagger-ui.css'\n const bundleSrc = assetsPath\n ? `${assetsPath}/swagger-ui-bundle.js`\n : 'https://unpkg.com/swagger-ui-dist@5/swagger-ui-bundle.js'\n const presetSrc = assetsPath\n ? `${assetsPath}/swagger-ui-standalone-preset.js`\n : 'https://unpkg.com/swagger-ui-dist@5/swagger-ui-standalone-preset.js'\n\n return `<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n <meta charset=\"UTF-8\">\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n <title>${safeTitle}</title>\n <link rel=\"stylesheet\" href=\"${cssHref}\">\n</head>\n<body>\n <div id=\"swagger-ui\"></div>\n <script src=\"${bundleSrc}\"></script>\n <script src=\"${presetSrc}\"></script>\n <script>\n (function () {\n var rawUrl = ${safeUrl};\n var specUrl;\n try {\n specUrl = new URL(rawUrl, window.location.origin).href;\n } catch (_e) {\n specUrl = rawUrl;\n }\n SwaggerUIBundle({\n url: specUrl,\n dom_id: '#swagger-ui',\n deepLinking: true,\n presets: [SwaggerUIBundle.presets.apis, SwaggerUIStandalonePreset],\n plugins: [SwaggerUIBundle.plugins.DownloadUrl],\n layout: 'StandaloneLayout',\n });\n })();\n </script>\n</body>\n</html>`\n}\n\n/**\n * Generate ReDoc HTML.\n *\n * ReDoc doesn't publish a standalone npm package suitable for local serving,\n * so it still loads from CDN. If offline support for ReDoc is needed,\n * vendor the standalone bundle into the package's public/ directory.\n */\nexport function redocHtml(specUrl: string, title = 'API Docs'): string {\n const safeTitle = escapeHtml(title)\n const safeUrl = escapeHtml(specUrl)\n\n return `<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n <meta charset=\"UTF-8\">\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n <title>${safeTitle}</title>\n</head>\n<body>\n <redoc spec-url=\"${safeUrl}\"></redoc>\n <script src=\"https://cdn.redoc.ly/redoc/latest/bundles/redoc.standalone.js\"></script>\n</body>\n</html>`\n}\n","import { dirname } from 'node:path'\nimport { createRequire } from 'node:module'\nimport express, { Router } from 'express'\nimport { Logger, defineAdapter } from '@forinda/kickjs'\nimport {\n buildOpenAPISpec,\n registerControllerForDocs,\n clearRegisteredRoutes,\n type SwaggerOptions,\n} from './openapi-builder'\nimport { swaggerUIHtml, redocHtml } from './ui'\n\nconst log = Logger.for('SwaggerAdapter')\n\n/**\n * Resolve the absolute path to swagger-ui-dist's static assets.\n * Uses createRequire to find it relative to this package (works with pnpm).\n */\nfunction getSwaggerUiDistPath(): string {\n const require = createRequire(import.meta.url)\n return dirname(require.resolve('swagger-ui-dist/package.json'))\n}\n\nexport interface SwaggerAdapterOptions extends SwaggerOptions {\n /** Path to serve Swagger UI (default: '/docs') */\n docsPath?: string\n /** Path to serve ReDoc (default: '/redoc') */\n redocPath?: string\n /** Path to serve the raw JSON spec (default: '/openapi.json') */\n specPath?: string\n /** Other adapters to discover (e.g., WsAdapter for WebSocket server URLs) */\n adapters?: any[]\n /**\n * When true, the adapter is a no-op while `NODE_ENV === 'production'` —\n * docs, spec, and assets are not mounted. Useful for keeping API docs\n * out of production builds without conditionally constructing the adapter.\n */\n disableInProd?: boolean\n}\n\n/**\n * Swagger adapter — auto-generates OpenAPI spec from decorators and serves docs.\n *\n * Assets are served locally from `swagger-ui-dist` (npm dependency) —\n * no CDN required, works fully offline.\n *\n * @example\n * ```ts\n * bootstrap({\n * modules,\n * adapters: [\n * SwaggerAdapter({\n * info: { title: 'My API', version: '1.0.0' },\n * }),\n * ],\n * })\n * ```\n *\n * Endpoints:\n * GET /docs — Swagger UI (local assets, no CDN)\n * GET /redoc — ReDoc (CDN — no local package available)\n * GET /openapi.json — Raw OpenAPI 3.0.3 spec\n */\nexport const SwaggerAdapter = defineAdapter<SwaggerAdapterOptions>({\n name: 'SwaggerAdapter',\n defaults: {\n docsPath: '/docs',\n redocPath: '/redoc',\n specPath: '/openapi.json',\n },\n build: (config) => {\n const isDisabled = (): boolean =>\n Boolean(config.disableInProd) && process.env.NODE_ENV === 'production'\n\n return {\n onRouteMount(controllerClass, mountPath) {\n if (isDisabled()) return\n registerControllerForDocs(controllerClass, mountPath)\n },\n\n afterStart({ server }) {\n if (isDisabled()) return\n const addr = server?.address?.()\n if (!addr || typeof addr !== 'object') return\n\n const host =\n addr.address === '::' || addr.address === '0.0.0.0' ? 'localhost' : addr.address\n\n // Auto-add HTTP server URL if none configured\n if (!config.servers || config.servers.length === 0) {\n config.servers = [{ url: `http://${host}:${addr.port}`, description: 'HTTP server' }]\n }\n\n // Auto-add WebSocket server URLs from WsAdapter\n const wsAdapter = config.adapters?.find(\n (a) => a.name === 'WsAdapter' && typeof a.getStats === 'function',\n )\n if (wsAdapter) {\n const stats = wsAdapter.getStats()\n for (const namespace of Object.keys(stats.namespaces || {})) {\n config.servers?.push({\n url: `ws://${host}:${addr.port}${namespace}`,\n description: `WebSocket: ${namespace}`,\n })\n }\n }\n },\n\n beforeMount({ app }) {\n if (isDisabled()) {\n log.info('Swagger disabled in production (disableInProd=true)')\n return\n }\n // Clear previous registrations (supports HMR rebuild)\n clearRegisteredRoutes()\n const docsPath = config.docsPath!\n const redocPath = config.redocPath!\n const specPath = config.specPath!\n let uiDistAvailable = false\n\n const docsRouter = Router()\n\n // ── Serve swagger-ui-dist static assets locally ──────────────────\n // This makes Swagger UI work offline — no CDN needed.\n // Assets served at /_swagger-assets/ (CSS, JS, fonts, etc.)\n const swaggerAssetsPath = '/_swagger-assets'\n try {\n const swaggerDistDir = getSwaggerUiDistPath()\n docsRouter.use(swaggerAssetsPath, express.static(swaggerDistDir))\n uiDistAvailable = true\n } catch {\n log.warn('swagger-ui-dist not found — Swagger UI will load from CDN (requires internet).')\n }\n\n // Relax CSP for Swagger UI in both local and CDN modes (inline script is used in both)\n docsRouter.use((_req, res, next) => {\n // Build connect-src dynamically so \"Try it out\" can call any configured server URL.\n // Includes dev-friendly localhost/127.0.0.1 origins so docs served from one host\n // can call an API spec'd at the other (a common cross-origin gotcha).\n const serverOrigins = new Set<string>()\n for (const s of config.servers ?? []) {\n try {\n serverOrigins.add(new URL(s.url).origin)\n } catch {\n // ignore relative or malformed URLs\n }\n }\n const connectSrc = [\n \"'self'\",\n 'http://localhost:*',\n 'http://127.0.0.1:*',\n 'https://localhost:*',\n 'https://127.0.0.1:*',\n 'ws://localhost:*',\n 'ws://127.0.0.1:*',\n ...serverOrigins,\n ].join(' ')\n\n res.setHeader(\n 'Content-Security-Policy',\n [\n \"default-src 'self'\",\n \"script-src 'self' 'unsafe-inline' https://unpkg.com https://cdn.redoc.ly https://cdn.jsdelivr.net\",\n \"style-src 'self' 'unsafe-inline' https://unpkg.com https://fonts.googleapis.com\",\n \"font-src 'self' https://fonts.gstatic.com\",\n \"img-src 'self' data: https://unpkg.com\",\n `connect-src ${connectSrc}`,\n ].join('; '),\n )\n next()\n })\n\n // Spec endpoint (JSON)\n docsRouter.get(specPath, (_req, res) => {\n const spec = buildOpenAPISpec(config)\n res.json(spec)\n })\n\n // Swagger UI — uses local assets if available, CDN fallback\n docsRouter.get(docsPath, (_req, res) => {\n res\n .type('html')\n .send(\n swaggerUIHtml(\n specPath,\n config.info?.title,\n uiDistAvailable ? swaggerAssetsPath : undefined,\n ),\n )\n })\n\n // ReDoc — still CDN-based (no npm package for standalone bundle)\n docsRouter.get(redocPath, (_req, res) => {\n res.type('html').send(redocHtml(specPath, config.info?.title))\n })\n\n app.use(docsRouter)\n\n log.info(`Swagger UI: ${docsPath}`)\n log.info(`ReDoc: ${redocPath}`)\n log.info(`OpenAPI spec: ${specPath}`)\n },\n }\n },\n})\n\n// Re-export for use by Application when mounting module routes\nexport { registerControllerForDocs, clearRegisteredRoutes }\n"],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@forinda/kickjs-swagger",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "4.0.0",
|
|
4
4
|
"description": "OpenAPI spec generation from decorators, Swagger UI and ReDoc serving for KickJS",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"kickjs",
|
|
@@ -22,13 +22,10 @@
|
|
|
22
22
|
"@forinda/kickjs",
|
|
23
23
|
"@forinda/kickjs-auth",
|
|
24
24
|
"@forinda/kickjs-cli",
|
|
25
|
-
"@forinda/kickjs-config",
|
|
26
|
-
"@forinda/kickjs-core",
|
|
27
25
|
"@forinda/kickjs-cron",
|
|
28
26
|
"@forinda/kickjs-devtools",
|
|
29
27
|
"@forinda/kickjs-drizzle",
|
|
30
28
|
"@forinda/kickjs-graphql",
|
|
31
|
-
"@forinda/kickjs-http",
|
|
32
29
|
"@forinda/kickjs-mailer",
|
|
33
30
|
"@forinda/kickjs-multi-tenant",
|
|
34
31
|
"@forinda/kickjs-notifications",
|
|
@@ -64,10 +61,7 @@
|
|
|
64
61
|
"output": [
|
|
65
62
|
"dist/**"
|
|
66
63
|
],
|
|
67
|
-
"dependencies": [
|
|
68
|
-
"../core:build",
|
|
69
|
-
"../http:build"
|
|
70
|
-
]
|
|
64
|
+
"dependencies": []
|
|
71
65
|
}
|
|
72
66
|
},
|
|
73
67
|
"dependencies": {
|
|
@@ -85,14 +79,14 @@
|
|
|
85
79
|
}
|
|
86
80
|
},
|
|
87
81
|
"devDependencies": {
|
|
88
|
-
"@swc/core": "^1.15.
|
|
82
|
+
"@swc/core": "^1.15.30",
|
|
89
83
|
"@types/express": "^5.0.6",
|
|
90
|
-
"@types/node": "^25.
|
|
84
|
+
"@types/node": "^25.6.0",
|
|
91
85
|
"express": "^5.1.0",
|
|
92
|
-
"typescript": "^
|
|
93
|
-
"vitest": "^4.1.
|
|
86
|
+
"typescript": "^6.0.3",
|
|
87
|
+
"vitest": "^4.1.5",
|
|
94
88
|
"zod": "^4.3.6",
|
|
95
|
-
"@forinda/kickjs": "
|
|
89
|
+
"@forinda/kickjs": "4.0.0"
|
|
96
90
|
},
|
|
97
91
|
"publishConfig": {
|
|
98
92
|
"access": "public"
|