@axiomify/cli 4.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,85 +1,152 @@
1
1
  # @axiomify/cli
2
2
 
3
- The official Command Line Interface for the Axiomify framework.
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
- `@axiomify/cli` provides a lightning-fast development experience, production-ready build steps, and powerful inspection tools for your Axiomify applications.
7
+ ## Install
6
8
 
7
- ## 📦 Installation
9
+ ```bash
10
+ npm install -D @axiomify/cli
11
+ # or invoke without installing:
12
+ npx @axiomify/cli init my-api
13
+ ```
8
14
 
9
- We recommend installing the CLI locally as a development dependency in your project so your CI/CD pipelines can utilize it:
15
+ Per-project install is recommended so the CLI version stays pinned to
16
+ the same major as your `@axiomify/*` runtime packages.
10
17
 
11
- ```bash
12
- npm install @axiomify/cli -D
13
- ````
18
+ ## Commands at a glance
14
19
 
15
- You can also install it globally if you want to use the `init` command anywhere on your machine:
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 |
16
29
 
17
- ```bash
18
- npm install -g @axiomify/cli
19
- ```
30
+ `[entry]` defaults to `src/index.ts` everywhere it's accepted.
20
31
 
21
- ## 🛠️ Commands
32
+ For the full reference (flags, exit codes, CI examples), see
33
+ [`docs/packages/cli.md`](../../docs/packages/cli.md).
22
34
 
23
- | Command | Description |
24
- | :--- | :--- |
25
- | `axiomify init` | Scaffolds a new, production-ready Axiomify project. |
26
- | `axiomify dev <entry>` | Starts the development server with hot-module reloading (HMR). |
27
- | `axiomify build <entry>` | Compiles your TypeScript application for production. |
28
- | `axiomify routes <entry>`| Inspects your app and prints a visual table of all registered routes. |
35
+ ## `axiomify init`
29
36
 
30
- ## 🚀 Usage Guide
37
+ ```bash
38
+ axiomify init my-api
39
+ ```
40
+
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.
31
45
 
32
- ### 1\. Project Scaffolding
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.
33
49
 
34
- Quickly generate a new project with all the necessary TypeScript configurations and adapter boilerplates:
50
+ ## `axiomify dev` / `axiomify build`
35
51
 
36
52
  ```bash
37
- npx @axiomify/cli init my-new-app
53
+ axiomify dev # watches src/, restarts on change
54
+ axiomify build # bundles to dist/index.js
38
55
  ```
39
56
 
40
- ### 2\. Development Mode
57
+ Both use esbuild. `dev` sends SIGTERM first so your `gracefulShutdown`
58
+ hooks can drain, with a SIGKILL fallback after 3 seconds.
41
59
 
42
- Run your application locally. The CLI automatically watches your file system and restarts the server when it detects changes.
60
+ ## `axiomify routes`
61
+
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.
43
66
 
44
- ```bash
45
- npx axiomify dev src/index.ts
46
67
  ```
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
82
+ ```
83
+
84
+ Flags: `--json`, `--method GET,POST,WS`, `--filter "/api/v1/*"`,
85
+ `--sort path|method`.
47
86
 
48
- ### 3\. Route Inspector
87
+ WebSocket routes (`app.ws(...)`) appear under the `WS` pseudo-method
88
+ alongside HTTP routes — earlier CLI versions silently omitted them.
49
89
 
50
- Having trouble debugging an endpoint? The route inspector parses your Radix tree and prints a clean, color-coded table of every available method, path, and attached schema directly to your terminal.
90
+ ## `axiomify openapi`
51
91
 
52
92
  ```bash
53
- npx axiomify routes src/index.ts
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)"
54
98
  ```
55
99
 
56
- ### 4\. Production 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.
57
106
 
58
- Compiles your application into highly optimized JavaScript ready for edge or serverless deployment.
107
+ ## `axiomify check`
59
108
 
60
109
  ```bash
61
- npx axiomify build src/index.ts
110
+ axiomify check
62
111
  ```
63
112
 
64
- ## 📖 Standard `package.json` Setup
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.
65
124
 
66
- For the best developer experience, map the CLI commands to your project's npm scripts:
125
+ Exit code 1 on any fail wire into CI to gate deploys.
67
126
 
68
- ```json
69
- {
70
- "scripts": {
71
- "dev": "axiomify dev src/index.ts",
72
- "build": "axiomify build src/index.ts",
73
- "start": "node dist/index.js",
74
- "routes": "axiomify routes src/index.ts"
75
- }
76
- }
127
+ ## `axiomify doctor`
128
+
129
+ ```bash
130
+ axiomify doctor
77
131
  ```
78
132
 
79
- ## 📚 Documentation
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.
80
137
 
81
- For complete documentation, guides, and ecosystem packages, please visit the [Axiomify Master Repository](https://github.com/OTopman/axiomify).
138
+ Run on a fresh clone or new CI runner before chasing test failures that
139
+ turn out to be Node-version mismatches.
82
140
 
83
- ## 📄 License
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
+ ```
84
150
 
85
- MIT
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 };