@axiomify/cli 5.0.0 → 6.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 CHANGED
@@ -1,89 +1,158 @@
1
1
  # @axiomify/cli
2
2
 
3
- The Axiomify command-line interface — scaffold projects, run the dev server, and inspect routes.
3
+
4
+ [![npm version](https://img.shields.io/npm/v/@axiomify/cli.svg)](https://npmjs.com/package//@axiomify/cli)
5
+ [![codecov](https://codecov.io/github/otopman/axiomify/graph/badge.svg)](https://codecov.io/github/otopman/axiomify)
6
+ [![OpenSSF Scorecard](https://api.securityscorecards.dev/projects/github.com/OTopman/axiomify/badge)](https://securityscorecards.dev/viewer/?uri=github.com/OTopman/axiomify)
7
+ [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT)
8
+
9
+ The official CLI for the Axiomify framework — scaffold projects, run the
10
+ dev server, build production bundles, inspect routes, generate OpenAPI
11
+ specs, and audit production readiness.
4
12
 
5
13
  ## Install
6
14
 
7
15
  ```bash
8
- npm install -g @axiomify/cli
9
- # or use without installing:
16
+ npm install -D @axiomify/cli
17
+ # or invoke without installing:
10
18
  npx @axiomify/cli init my-api
11
19
  ```
12
20
 
13
- ## Commands
21
+ Per-project install is recommended so the CLI version stays pinned to
22
+ the same major as your `@axiomify/*` runtime packages.
23
+
24
+ ## Commands at a glance
14
25
 
15
- ### `axiomify init <name>`
26
+ | Command | Purpose |
27
+ |---|---|
28
+ | `axiomify init [directory]` | Bootstrap a new project |
29
+ | `axiomify dev [entry]` | Hot-reload dev server (esbuild watch) |
30
+ | `axiomify build [entry]` | Compile a production bundle to `dist/` |
31
+ | `axiomify routes [entry]` | Inspect every HTTP + WebSocket route |
32
+ | `axiomify openapi [entry]` | Generate the OpenAPI 3.0.3 spec |
33
+ | `axiomify check [entry]` | Static production-readiness audit |
34
+ | `axiomify doctor` | Diagnose the host environment |
16
35
 
17
- Scaffolds a new Axiomify project with your chosen adapter.
36
+ `[entry]` defaults to `src/index.ts` everywhere it's accepted.
37
+
38
+ For the full reference (flags, exit codes, CI examples), see
39
+ [`[./docs/packages/cli.md](./docs/packages/cli.md)`](https://github.com/OTopman/axiomify/blob/main/docs/packages/cli.md).
40
+
41
+ ## `axiomify init`
18
42
 
19
43
  ```bash
20
44
  axiomify init my-api
21
45
  ```
22
46
 
23
- Prompts:
47
+ Interactive scaffolder. Prompts for project name (when no directory
48
+ argument is given), description, package manager (npm / pnpm / yarn),
49
+ optional ESLint + Prettier + EditorConfig, git initialisation, and
50
+ whether to run install automatically.
51
+
52
+ The generated `src/index.ts` registers `helmet`, `cors`, `security`,
53
+ `rate-limit`, `fingerprint`, and `logger` with sane defaults. Pass
54
+ `-f, --force` to overwrite existing files.
24
55
 
25
- 1. **Adapter** Native (uWS, fastest) *(default)*, Fastify, Express, Hapi, or Node HTTP
26
- 2. **Plugins** — Auth, CORS, Helmet, Rate Limit, Metrics, Logger, OpenAPI (multi-select)
27
- 3. **Language** — TypeScript *(default)* or JavaScript
56
+ ## `axiomify dev` / `axiomify build`
28
57
 
29
- Generates:
58
+ ```bash
59
+ axiomify dev # watches src/, restarts on change
60
+ axiomify build # bundles to dist/index.js
30
61
  ```
31
- my-api/
32
- ├── src/
33
- │ ├── index.ts # Entry point with chosen adapter
34
- │ ├── routes/ # Example routes
35
- │ └── plugins/ # Plugin configuration
36
- ├── package.json
37
- ├── tsconfig.json
38
- └── README.md
62
+
63
+ Both use esbuild. `dev` sends SIGTERM first so your `gracefulShutdown`
64
+ hooks can drain, with a SIGKILL fallback after 3 seconds.
65
+
66
+ ## `axiomify routes`
67
+
68
+ Inspects the app *without* booting a listener. Prints a
69
+ Unicode-bordered table with colour-coded HTTP methods, validation
70
+ badges, OpenAPI tags + `operationId`, plugin count, timeout, and
71
+ deprecation marker.
72
+
39
73
  ```
74
+ 🧭 Axiomify routes
75
+
76
+ ┌─────────┬──────────────────────┬───────────────┬───────────────────────────────────────┐
77
+ │ METHOD │ PATH │ VALIDATION │ META │
78
+ ├─────────┼──────────────────────┼───────────────┼───────────────────────────────────────┤
79
+ │ WS │ /chat │ Message │ — │
80
+ │ GET │ /health │ — │ — │
81
+ │ POST │ /users ⊘ DEPRECATED │ Body,Response │ op:createUser #Users 5000ms +1 plugin │
82
+ │ GET │ /users/:id │ Params │ op:getUser #Users │
83
+ │ DELETE │ /users/:id │ Params │ — │
84
+ └─────────┴──────────────────────┴───────────────┴───────────────────────────────────────┘
85
+
86
+ ✓ 5 routes DELETE 1 · GET 2 · POST 1 · WS 1
87
+ └ 1 WebSocket route included
88
+ ```
89
+
90
+ Flags: `--json`, `--method GET,POST,WS`, `--filter "/api/v1/*"`,
91
+ `--sort path|method`.
40
92
 
41
- ### `axiomify dev`
93
+ WebSocket routes (`app.ws(...)`) appear under the `WS` pseudo-method
94
+ alongside HTTP routes — earlier CLI versions silently omitted them.
42
95
 
43
- Starts the development server with hot-reload powered by esbuild.
96
+ ## `axiomify openapi`
44
97
 
45
98
  ```bash
46
- axiomify dev # watches src/, rebuilds on change
47
- axiomify dev --port 4000 # custom port
48
- axiomify dev --debug # verbose logging
99
+ axiomify openapi # stdout, pretty JSON
100
+ axiomify openapi -o openapi.json
101
+ axiomify openapi --format yaml -o api.yml
102
+ axiomify openapi --minify > spec.min.json
103
+ axiomify openapi --title "My API" --spec-version "$(git describe)"
49
104
  ```
50
105
 
51
- ### `axiomify build`
106
+ Generates the OpenAPI 3.0.3 spec from the app's registered routes.
107
+ Useful in CI for client codegen pipelines (`openapi-typescript`,
108
+ `openapi-generator`, `oazapfts`) without booting an HTTP listener.
109
+
110
+ Requires `@axiomify/openapi` to be installed; dynamic-imports it at
111
+ runtime and prints a clean error if missing.
52
112
 
53
- Compiles the application for production.
113
+ ## `axiomify check`
54
114
 
55
115
  ```bash
56
- axiomify build # outputs to dist/
57
- axiomify build --minify # minified output
58
- axiomify build --sourcemap # include source maps
116
+ axiomify check
59
117
  ```
60
118
 
61
- ### `axiomify routes`
119
+ Static production-readiness audit. Loads the app (no listener) and
120
+ flags:
121
+
122
+ - ✓ pass — configuration is correct
123
+ - ⚠ warn — non-fatal smell
124
+ - ✗ fail — real defect that blocks ship
125
+
126
+ Checks include: `enableRequestId()` called, env vars referenced in
127
+ source actually set, routes with body schemas declare response schemas,
128
+ no deprecated `meta:` field usage, health check registered, OpenAPI docs
129
+ protected, security plugins active.
62
130
 
63
- Visualises all registered routes in a table.
131
+ Exit code 1 on any fail — wire into CI to gate deploys.
132
+
133
+ ## `axiomify doctor`
64
134
 
65
135
  ```bash
66
- axiomify routes
67
-
68
- ┌─────────────────────────────────────┬────────┬───────────────────────────────┐
69
- │ Path │ Method │ Plugins │
70
- ├─────────────────────────────────────┼────────┼───────────────────────────────┤
71
- │ /users │ GET │ requireAuth, rateLimiter │
72
- │ /users │ POST │ requireAuth │
73
- │ /users/:id │ GET │ requireAuth │
74
- │ /auth/login │ POST │ loginRateLimit │
75
- │ /auth/refresh │ POST │ refreshRateLimit │
76
- │ /health │ GET │ — │
77
- │ /metrics │ GET │ — │
78
- └─────────────────────────────────────┴────────┴───────────────────────────────┘
136
+ axiomify doctor
79
137
  ```
80
138
 
81
- ## Adapter choices in `axiomify init`
139
+ Diagnoses the host environment: Node version vs uWS prebuilt support,
140
+ platform (Linux ✓ for `SO_REUSEPORT` clustering), `@axiomify/*` package
141
+ alignment, uWS bindings load successfully, recent build artefact, port
142
+ 3000 (or `$PORT`) availability.
143
+
144
+ Run on a fresh clone or new CI runner before chasing test failures that
145
+ turn out to be Node-version mismatches.
146
+
147
+ ## CI example
148
+
149
+ ```yaml
150
+ - run: npx axiomify doctor # environment sanity
151
+ - run: npx axiomify check # static readiness audit
152
+ - run: npx axiomify build
153
+ - run: npx axiomify openapi -o ./openapi.json --spec-version "$GITHUB_SHA"
154
+ - run: npx axiomify routes --json > routes.json # surface snapshot
155
+ ```
82
156
 
83
- | Adapter | Req/s (1 core) | Use case |
84
- |---|---:|---|
85
- | **Native (uWS)** | ~50k | Maximum throughput, production APIs |
86
- | **Fastify** | ~10k | Recommended default — best ecosystem balance |
87
- | **Express** | ~5k | Legacy migration, Express middleware ecosystem |
88
- | **Hapi** | ~5k | Enterprise Hapi plugin ecosystem |
89
- | **Node HTTP** | ~10k | Zero dependencies, edge/serverless |
157
+ Diff `routes.json` between commits to detect accidental API changes
158
+ before they reach production.
@@ -0,0 +1,41 @@
1
+ var __create = Object.create;
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __getProtoOf = Object.getPrototypeOf;
6
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
7
+ var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
8
+ get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
9
+ }) : x)(function(x) {
10
+ if (typeof require !== "undefined") return require.apply(this, arguments);
11
+ throw Error('Dynamic require of "' + x + '" is not supported');
12
+ });
13
+ var __glob = (map) => (path) => {
14
+ var fn = map[path];
15
+ if (fn) return fn();
16
+ throw new Error("Module not found in bundle: " + path);
17
+ };
18
+ var __esm = (fn, res) => function __init() {
19
+ return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
20
+ };
21
+ var __commonJS = (cb, mod) => function __require2() {
22
+ return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
23
+ };
24
+ var __copyProps = (to, from, except, desc) => {
25
+ if (from && typeof from === "object" || typeof from === "function") {
26
+ for (let key of __getOwnPropNames(from))
27
+ if (!__hasOwnProp.call(to, key) && key !== except)
28
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
29
+ }
30
+ return to;
31
+ };
32
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
33
+ // If the importer is in node compatibility mode or this is not an ESM
34
+ // file that has been converted to a CommonJS file using a Babel-
35
+ // compatible transform (i.e. "__esModule" has not been set), then set
36
+ // "default" to the CommonJS "module.exports" for node compatibility.
37
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
38
+ mod
39
+ ));
40
+
41
+ export { __commonJS, __esm, __glob, __require, __toESM };
@@ -0,0 +1,344 @@
1
+ import { __require } from './chunk-YZPZCUKZ.mjs';
2
+
3
+ // ../openapi/dist/index.mjs
4
+ var __require2 = /* @__PURE__ */ ((x) => typeof __require !== "undefined" ? __require : typeof Proxy !== "undefined" ? new Proxy(x, {
5
+ get: (a, b) => (typeof __require !== "undefined" ? __require : a)[b]
6
+ }) : x)(function(x) {
7
+ if (typeof __require !== "undefined") return __require.apply(this, arguments);
8
+ throw Error('Dynamic require of "' + x + '" is not supported');
9
+ });
10
+ function zodSchemaToOpenApi(schema) {
11
+ const s = schema;
12
+ if (typeof s.toJSONSchema === "function") {
13
+ const full = s.toJSONSchema({ target: "openApi3_1" });
14
+ const { $schema: _dropped, ...rest } = full;
15
+ return rest;
16
+ }
17
+ try {
18
+ const { zodToJsonSchema } = __require2("zod-to-json-schema");
19
+ return zodToJsonSchema(schema, { target: "openApi3" });
20
+ } catch {
21
+ return { type: "object" };
22
+ }
23
+ }
24
+ function isZodSchema(value) {
25
+ return typeof value === "object" && value !== null && typeof value.safeParse === "function";
26
+ }
27
+ var OpenApiGenerator = class {
28
+ constructor(app, options) {
29
+ this.app = app;
30
+ this.options = options;
31
+ }
32
+ app;
33
+ options;
34
+ generate() {
35
+ const spec = {
36
+ openapi: "3.1.0",
37
+ info: this.options.info,
38
+ paths: {}
39
+ };
40
+ if (this.options.components) spec.components = this.options.components;
41
+ if (this.options.security) spec.security = this.options.security;
42
+ for (const route of this.app.registeredRoutes) {
43
+ const openApiPath = this.formatPath(route.path);
44
+ const method = route.method.toLowerCase();
45
+ const paths = spec.paths;
46
+ if (!paths[openApiPath]) paths[openApiPath] = {};
47
+ const s = route.schema ?? {};
48
+ const operation = {
49
+ // OAS §4.8.10.2 — summary. Defaults to `${method} ${path}`.
50
+ summary: s.summary ?? `${route.method} ${route.path}`,
51
+ // OAS §4.8.10.5 — operationId for client codegen tools.
52
+ // Auto-synthesised from method+path when omitted:
53
+ // GET /users/:id → "getUsersById" POST /users → "postUsers"
54
+ operationId: s.operationId ?? this.synthesiseOperationId(route.method, route.path),
55
+ parameters: this.extractParameters(route),
56
+ responses: this.extractResponses(route)
57
+ };
58
+ if (s.description) operation.description = s.description;
59
+ if (s.tags) operation.tags = s.tags;
60
+ if (s.security !== void 0) operation.security = s.security;
61
+ if (s.deprecated) operation.deprecated = true;
62
+ if (s.externalDocs) operation.externalDocs = s.externalDocs;
63
+ if (s.servers) operation.servers = s.servers;
64
+ if (s.callbacks) operation.callbacks = s.callbacks;
65
+ const body = this.extractBody(route);
66
+ if (body) {
67
+ if (s.requestBodyDescription) body.description = s.requestBodyDescription;
68
+ operation.requestBody = body;
69
+ }
70
+ paths[openApiPath][method] = operation;
71
+ }
72
+ return spec;
73
+ }
74
+ /** Translates Axiomify path syntax to OpenAPI: `/users/:id` → `/users/{id}` */
75
+ formatPath(path) {
76
+ return path.replace(/:([a-zA-Z0-9_]+)/g, "{$1}");
77
+ }
78
+ extractParameters(route) {
79
+ const parameters = [];
80
+ if (route.schema?.params) {
81
+ const paramSchema = zodSchemaToOpenApi(route.schema.params);
82
+ const properties = paramSchema.properties ?? {};
83
+ for (const [key, prop] of Object.entries(properties)) {
84
+ parameters.push({ name: key, in: "path", required: true, schema: prop });
85
+ }
86
+ }
87
+ if (route.schema?.query) {
88
+ const querySchema = zodSchemaToOpenApi(route.schema.query);
89
+ const properties = querySchema.properties ?? {};
90
+ const required = querySchema.required ?? [];
91
+ for (const [key, prop] of Object.entries(properties)) {
92
+ parameters.push({
93
+ name: key,
94
+ in: "query",
95
+ required: required.includes(key),
96
+ schema: prop
97
+ });
98
+ }
99
+ }
100
+ return parameters;
101
+ }
102
+ /**
103
+ * Synthesise a stable, codegen-friendly operationId from method+path
104
+ * when the route definition doesn't supply one. Example outputs:
105
+ * GET /users/:id → getUsersById
106
+ * POST /users → postUsers
107
+ * GET /users/:id/posts/:pid → getUsersByIdPostsByPid
108
+ *
109
+ * Determinism matters here — client codegen produces the same function
110
+ * names on every run as long as method+path are stable.
111
+ */
112
+ synthesiseOperationId(method, path) {
113
+ const verb = method.toLowerCase();
114
+ const parts = [];
115
+ for (const seg of path.split("/")) {
116
+ if (!seg) continue;
117
+ if (seg.startsWith(":")) {
118
+ const name = seg.slice(1);
119
+ parts.push("By", name.charAt(0).toUpperCase() + name.slice(1));
120
+ } else if (seg === "*") {
121
+ parts.push("All");
122
+ } else {
123
+ parts.push(seg.charAt(0).toUpperCase() + seg.slice(1));
124
+ }
125
+ }
126
+ return verb + parts.join("");
127
+ }
128
+ extractBody(route) {
129
+ if (!route.schema?.body && !route.schema?.files) return void 0;
130
+ const hasFiles = !!route.schema.files;
131
+ const contentType = hasFiles ? "multipart/form-data" : "application/json";
132
+ let finalSchema = { type: "object", properties: {} };
133
+ if (route.schema.body) {
134
+ const bodySchema = zodSchemaToOpenApi(route.schema.body);
135
+ if (bodySchema.type === "object") {
136
+ finalSchema.properties = { ...bodySchema.properties };
137
+ if (bodySchema.required) finalSchema.required = bodySchema.required;
138
+ if (bodySchema.additionalProperties !== void 0) {
139
+ finalSchema.additionalProperties = bodySchema.additionalProperties;
140
+ }
141
+ } else {
142
+ finalSchema = hasFiles ? { type: "object", properties: { payload: bodySchema } } : bodySchema;
143
+ }
144
+ }
145
+ if (hasFiles) {
146
+ const files = route.schema.files;
147
+ const props = finalSchema.properties ?? {};
148
+ for (const [fieldName, config] of Object.entries(files)) {
149
+ props[fieldName] = {
150
+ type: "string",
151
+ format: "binary",
152
+ ...config.description ? { description: config.description } : {},
153
+ ...config.maxSize ? { description: `Max size: ${config.maxSize} bytes` } : {}
154
+ };
155
+ }
156
+ finalSchema.properties = props;
157
+ }
158
+ return { required: true, content: { [contentType]: { schema: finalSchema } } };
159
+ }
160
+ extractResponses(route) {
161
+ const descriptions = route.schema?.responseDescriptions ?? {};
162
+ const defaultResponse = {
163
+ "200": {
164
+ description: descriptions["200"] ?? "Successful response",
165
+ content: { "application/json": { schema: { type: "object" } } }
166
+ }
167
+ };
168
+ if (!route.schema?.response) return defaultResponse;
169
+ const responseSchema = route.schema.response;
170
+ const responses = {};
171
+ if (isZodSchema(responseSchema)) {
172
+ responses["200"] = {
173
+ description: descriptions["200"] ?? "Successful response",
174
+ content: {
175
+ "application/json": { schema: zodSchemaToOpenApi(responseSchema) }
176
+ }
177
+ };
178
+ } else if (typeof responseSchema === "object" && responseSchema !== null) {
179
+ for (const [code, schema] of Object.entries(
180
+ responseSchema
181
+ )) {
182
+ responses[code] = {
183
+ description: descriptions[code] ?? `Response ${code}`,
184
+ content: {
185
+ "application/json": { schema: zodSchemaToOpenApi(schema) }
186
+ }
187
+ };
188
+ }
189
+ }
190
+ return Object.keys(responses).length > 0 ? responses : defaultResponse;
191
+ }
192
+ };
193
+ var DOCS_CSP = "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval' https://cdn.jsdelivr.net https://cdnjs.cloudflare.com https://unpkg.com; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com https://cdn.jsdelivr.net https://cdnjs.cloudflare.com; font-src 'self' https://fonts.gstatic.com data:; img-src 'self' data: https://validator.swagger.io; worker-src 'self' blob:;";
194
+ function escapeHtml(s) {
195
+ return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
196
+ }
197
+ function escapeJsString(s) {
198
+ return s.replace(/\\/g, "\\\\").replace(/'/g, "\\'");
199
+ }
200
+ function defineSecuritySchemes(schemes) {
201
+ return {
202
+ schemes,
203
+ require: (name, scopes = []) => [{ [name]: scopes }],
204
+ requireMultiple: (requirements) => {
205
+ const combined = {};
206
+ requirements.forEach((req) => {
207
+ combined[req] = [];
208
+ });
209
+ return [combined];
210
+ }
211
+ };
212
+ }
213
+ var Security = defineSecuritySchemes({
214
+ bearerAuth: { type: "http", scheme: "bearer" },
215
+ apiKey: { type: "apiKey", in: "header", name: "X-API-KEY" },
216
+ basicAuth: { type: "http", scheme: "basic" }
217
+ });
218
+ function inferSchemaFromPayload(data, depth = 0) {
219
+ if (depth > 32) return { type: "object" };
220
+ if (data === null) return { type: "null" };
221
+ if (Array.isArray(data)) {
222
+ return {
223
+ type: "array",
224
+ items: data.length > 0 ? inferSchemaFromPayload(data[0], depth + 1) : { type: "object" }
225
+ };
226
+ }
227
+ if (typeof data === "object") {
228
+ const properties = {};
229
+ for (const [key, value] of Object.entries(data)) {
230
+ properties[key] = inferSchemaFromPayload(value, depth + 1);
231
+ }
232
+ return { type: "object", properties };
233
+ }
234
+ return { type: typeof data };
235
+ }
236
+ function useOpenAPI(app, options) {
237
+ const rawPrefix = options.prefix ?? "/docs";
238
+ const normalizedPrefix = rawPrefix.startsWith("/") ? rawPrefix : `/${rawPrefix}`;
239
+ const prefix = normalizedPrefix === "/" ? "/" : normalizedPrefix.endsWith("/") ? normalizedPrefix.slice(0, -1) : normalizedPrefix;
240
+ const docsPaths = prefix === "/" ? ["/"] : [prefix, `${prefix}/`];
241
+ const docsPathSet = new Set(docsPaths);
242
+ const specPath = prefix === "/" ? "/openapi.json" : `${prefix}/openapi.json`;
243
+ const generator = new OpenApiGenerator(app, options);
244
+ let cachedSpec = null;
245
+ let cachedSpecJson = null;
246
+ let emittedPublicDocsWarning = false;
247
+ if (options.autoInferResponses) {
248
+ app.addHook("onPostHandler", (req, res, match) => {
249
+ if (!match?.route) return;
250
+ if (req.path === specPath || docsPathSet.has(req.path)) {
251
+ return;
252
+ }
253
+ const payload = res.payload;
254
+ if (payload === void 0) return;
255
+ if (!cachedSpec) cachedSpec = generator.generate();
256
+ const path = generator.formatPath(match.route.path);
257
+ const method = match.route.method.toLowerCase();
258
+ const statusCode = String(res.statusCode);
259
+ const existingResponse = cachedSpec.paths[path]?.[method]?.responses?.[statusCode];
260
+ const isDefault = existingResponse?.description === "Successful response" && existingResponse?.content?.["application/json"]?.schema?.type === "object";
261
+ if (existingResponse && !isDefault) return;
262
+ let parsedData = payload;
263
+ if (typeof payload === "string") {
264
+ try {
265
+ parsedData = JSON.parse(payload);
266
+ } catch {
267
+ }
268
+ }
269
+ if (cachedSpec.paths[path]?.[method]) {
270
+ cachedSpec.paths[path][method].responses[statusCode] = {
271
+ description: "Auto-inferred response",
272
+ content: {
273
+ "application/json": { schema: inferSchemaFromPayload(parsedData) }
274
+ }
275
+ };
276
+ cachedSpecJson = null;
277
+ }
278
+ });
279
+ }
280
+ const guard = async (req) => {
281
+ if (!options.protect) {
282
+ if (process.env.NODE_ENV === "production") {
283
+ if (!emittedPublicDocsWarning) {
284
+ emittedPublicDocsWarning = true;
285
+ console.warn(
286
+ "[axiomify/openapi] OpenAPI endpoints are not protected. Production access is denied by default. Provide a `protect` function or set `allowPublicInProduction: true` explicitly."
287
+ );
288
+ }
289
+ return options.allowPublicInProduction === true;
290
+ }
291
+ return true;
292
+ }
293
+ return Boolean(await options.protect(req));
294
+ };
295
+ app.route({
296
+ method: "GET",
297
+ path: specPath,
298
+ handler: async (req, res) => {
299
+ if (!await guard(req)) return res.status(403).send(null, "Forbidden");
300
+ if (!cachedSpec) cachedSpec = generator.generate();
301
+ if (!cachedSpecJson) cachedSpecJson = JSON.stringify(cachedSpec);
302
+ res.status(200).sendRaw(cachedSpecJson, "application/json");
303
+ }
304
+ });
305
+ const docsHandler = async (req, res) => {
306
+ if (!await guard(req)) return res.status(403).send(null, "Forbidden");
307
+ if (typeof res.setHeader === "function") {
308
+ res.setHeader("Content-Security-Policy", DOCS_CSP);
309
+ } else if (typeof res.header === "function") {
310
+ res.header("Content-Security-Policy", DOCS_CSP);
311
+ }
312
+ const isDev = process.env.NODE_ENV !== "production";
313
+ const specUrl = isDev ? `${specPath}?t=${Date.now()}` : specPath;
314
+ const html = `<!DOCTYPE html>
315
+ <html lang="en">
316
+ <head>
317
+ <meta charset="utf-8" />
318
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
319
+ <title>${escapeHtml(options.info.title)} - API Docs</title>
320
+ <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/swagger-ui/5.11.0/swagger-ui.min.css" integrity="sha384-bIuUyBV7i6P7z/kPAs1oeBIf8PMIqVkPVDzzaOL+QH7kWmvCT9HDTWwGVs0L4/9Q" crossorigin="anonymous" referrerpolicy="no-referrer" />
321
+ </head>
322
+ <body style="margin: 0; padding: 0;">
323
+ <div id="swagger-ui"></div>
324
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/swagger-ui/5.11.0/swagger-ui-bundle.min.js" integrity="sha384-XHDYRdiHvBq7oL4CtkiJKfdVVA5PydxYtssHVtRrvPlha1m+zz8kboiyx/MAsyl3" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
325
+ <script>
326
+ window.onload = () => {
327
+ window.ui = SwaggerUIBundle({
328
+ url: '${escapeJsString(specUrl)}',
329
+ dom_id: '#swagger-ui',
330
+ });
331
+ };
332
+ </script>
333
+ </body>
334
+ </html>`;
335
+ res.status(200).sendRaw(html, "text/html");
336
+ };
337
+ app.route({
338
+ method: "GET",
339
+ path: prefix,
340
+ handler: docsHandler
341
+ });
342
+ }
343
+
344
+ export { OpenApiGenerator, Security, defineSecuritySchemes, useOpenAPI };