@axiomify/cli 5.0.0 → 6.0.0-rc.2

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