@zudojs/openapi 1.2.0 → 1.4.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
@@ -2,6 +2,12 @@
2
2
 
3
3
  OpenAPI 3.0 and 3.1 specification generation, validation, and serialization for Zudojs applications.
4
4
 
5
+ <!-- zudo-docs:start -->
6
+
7
+ **Documentation:** [zudojs.oyinlola.site/docs/packages-openapi](https://zudojs.oyinlola.site/docs/packages-openapi) · **For AI agents:** [Markdown version](https://zudojs.oyinlola.site/docs/packages-openapi.md), [llms.txt](https://zudojs.oyinlola.site/llms.txt)
8
+
9
+ <!-- zudo-docs:end -->
10
+
5
11
  ## Installation
6
12
 
7
13
  ```bash
@@ -88,13 +94,33 @@ manager. Both accept the same options:
88
94
  | `logo` | Header logo, or `false` for none | Zudo wordmark |
89
95
  | `favicon` | Favicon URL or data URI, or `false` | Zudo favicon |
90
96
  | `customCss` | CSS appended after the built-in theme | — |
91
- | `assetsBaseUrl` | Where the viewer's own JS and CSS load from | public CDN |
97
+ | `assetsBaseUrl` | Where the viewer's own JS and CSS load from | pinned jsDelivr |
98
+ | `assetIntegrity` | SRI hashes `{ script?, stylesheet? }`, or `false` | pinned hashes |
99
+ | `contentSecurityPolicy` | CSP header from `toUIResponse`, or `false` | restrictive policy |
100
+ | `connectSources` | Extra origins "Try it out" may call | — |
92
101
  | `swaggerOptions` | Forwarded to `SwaggerUIBundle`; ignored by ReDoc | — |
93
102
 
103
+ By default the viewer loads from exact, pinned versions on jsDelivr
104
+ (`swagger-ui-dist@5.33.0`, `redoc@2.5.4`; exported as `SWAGGER_UI_VERSION`
105
+ and `REDOC_VERSION`) with Subresource Integrity hashes, so a changed or
106
+ compromised CDN file is refused by the browser instead of running on your
107
+ API's origin.
108
+
109
+ `toUIResponse` also sends `x-content-type-options: nosniff` and a
110
+ `content-security-policy` built by `buildOpenAPIUIContentSecurityPolicy`:
111
+ scripts only from the asset origin plus the hash of the page's one inline
112
+ bootstrap script, `default-src 'none'`, no plugins, no `<base>`, framing
113
+ only by the same origin, and `connect-src` limited to the page's origin, the
114
+ spec URL and every absolute `servers[].url` in the document (the servers
115
+ "Try it out" calls). Add more with `connectSources`, replace the policy with
116
+ a string, or pass `contentSecurityPolicy: false` to send none.
117
+
94
118
  `assetsBaseUrl` points the viewer's assets at a self-hosted copy, which is what
95
119
  an air-gapped deployment needs — the default CDN renders a blank page with no
96
120
  egress. Swagger UI loads `swagger-ui.css` and `swagger-ui-bundle.js` from that
97
- base; ReDoc loads `redoc.standalone.js`.
121
+ base; ReDoc loads `redoc.standalone.js`. A self-hosted copy gets no
122
+ `integrity` attribute unless you pass `assetIntegrity`, since it may be a
123
+ different build.
98
124
 
99
125
  ```typescript
100
126
  manager.toUIResponse({ specUrl: "/openapi.json", assetsBaseUrl: "/vendor/swagger" });
@@ -116,7 +142,7 @@ default, so a spec opened in one of them shows a logo rather than nothing.
116
142
 
117
143
  ```typescript
118
144
  new OpenAPIManager({ info }).generate().info["x-logo"];
119
- // { url: "data:image/svg+xml;…", href: "https://zudo.dev", altText: "Zudo", … }
145
+ // { url: "data:image/svg+xml;…", href: "https://zudojs.oyinlola.site", altText: "Zudo", … }
120
146
  ```
121
147
 
122
148
  The `branding` option controls it, and the same value is used by
@@ -142,7 +168,8 @@ can show them without a network request: `ZUDO_MARK_SVG`, `ZUDO_MARK_DARK_SVG`,
142
168
  `ZUDO_WORDMARK_SVG`, `ZUDO_WORDMARK_DARK_SVG`, `ZUDO_FAVICON_SVG`, a
143
169
  `*_DATA_URI` counterpart for each, plus `ZUDO_SITE_URL`, `zudoLogo(overrides?)`
144
170
  and `svgToDataUri(svg)`. The types are `OpenAPIUIOptions`,
145
- `OpenAPIUIRenderer`, `OpenAPIUIResponse` and `OpenAPILogo`.
171
+ `OpenAPIUIRenderer`, `OpenAPIUIResponse`, `OpenAPIUIAssetIntegrity` and
172
+ `OpenAPILogo`.
146
173
 
147
174
  ## Schemas
148
175
 
@@ -173,9 +200,9 @@ produces
173
200
  {
174
201
  "type": "object",
175
202
  "properties": {
176
- "id": { "type": "string", "format": "uuid" },
203
+ "id": { "type": "string", "format": "uuid", "maxLength": 255 },
177
204
  "age": { "type": "integer", "minimum": 0 },
178
- "nickname": { "type": "string" }
205
+ "nickname": { "type": "string", "maxLength": 255 }
179
206
  },
180
207
  "required": ["id", "age"]
181
208
  }
@@ -188,6 +215,13 @@ all converted, along with string and number constraints (`min`, `max`,
188
215
  `length`, `pattern`, `format`, `int`, `multipleOf`, `gt`, `lt`). Constraints
189
216
  on a coercing schema (`coerce.number().int().min(1)`) are carried through.
190
217
 
218
+ `@zudojs/schema` caps every string at 255 characters and every array at 1000
219
+ items unless the schema sets its own `.max()`. The emitted `maxLength` /
220
+ `maxItems` carry that effective limit (read from `@zudojs/constants`'
221
+ `SCHEMA_DEFAULT_MAX_STRING_LENGTH` / `SCHEMA_DEFAULT_MAX_ARRAY_LENGTH`, the same
222
+ constants the parser uses), so a client generated from the document is never
223
+ told it may send a payload the server rejects.
224
+
191
225
  A property is listed in `required` exactly when the object parser rejects
192
226
  its absence: fields wrapped in `optional`, fields with a `default`, and
193
227
  `any` / `unknown` fields are left out, and `.required()` forces every key
@@ -344,7 +378,8 @@ still produces a pointer that resolves.
344
378
 
345
379
  ## Errors
346
380
 
347
- All errors extend `OpenAPIError` (a `BaseError` from `@zudojs/errors`) and
381
+ All errors extend `OpenAPIError`, which is the `@zudojs/errors` class
382
+ re-exported (a `BaseError`), and
348
383
  default to status 500, not exposed — these are failures while a service builds
349
384
  or validates its own specification, not responses to a client request:
350
385
 
package/dist/index.d.ts CHANGED
@@ -37,7 +37,7 @@
37
37
  * const yaml = manager.toYAML();
38
38
  * ```
39
39
  */
40
- export { renderOpenAPIUI, zudoLogo, svgToDataUri, ZUDO_MARK_SVG, ZUDO_MARK_DARK_SVG, ZUDO_WORDMARK_SVG, ZUDO_WORDMARK_DARK_SVG, ZUDO_FAVICON_SVG, ZUDO_MARK_DATA_URI, ZUDO_MARK_DARK_DATA_URI, ZUDO_WORDMARK_DATA_URI, ZUDO_WORDMARK_DARK_DATA_URI, ZUDO_FAVICON_DATA_URI, ZUDO_SITE_URL, type OpenAPIUIOptions, type OpenAPIUIRenderer, } from "./openApiUi/index.js";
40
+ export { renderOpenAPIUI, zudoLogo, svgToDataUri, ZUDO_MARK_SVG, ZUDO_MARK_DARK_SVG, ZUDO_WORDMARK_SVG, ZUDO_WORDMARK_DARK_SVG, ZUDO_FAVICON_SVG, ZUDO_MARK_DATA_URI, ZUDO_MARK_DARK_DATA_URI, ZUDO_WORDMARK_DATA_URI, ZUDO_WORDMARK_DARK_DATA_URI, ZUDO_FAVICON_DATA_URI, ZUDO_SITE_URL, SWAGGER_UI_VERSION, REDOC_VERSION, buildOpenAPIUIContentSecurityPolicy, type OpenAPIUIAssetIntegrity, type OpenAPIUIOptions, type OpenAPIUIRenderer, } from "./openApiUi/index.js";
41
41
  export { OpenAPIDocumentBuilder, createOpenAPIDocumentBuilder, type OpenAPIDocumentOptions, } from "./openApiDocument/index.js";
42
42
  export { OpenAPIRegistryImpl } from "./openApiRegistry/index.js";
43
43
  export type { OpenAPIRegistry, OpenAPIRoute, OpenAPIComponentRegistration, } from "./openApiRegistry/index.js";
package/dist/index.js CHANGED
@@ -38,7 +38,7 @@
38
38
  * ```
39
39
  */
40
40
  /* ─── Documentation UI & branding ──────────────────────────────────────── */
41
- export { renderOpenAPIUI, zudoLogo, svgToDataUri, ZUDO_MARK_SVG, ZUDO_MARK_DARK_SVG, ZUDO_WORDMARK_SVG, ZUDO_WORDMARK_DARK_SVG, ZUDO_FAVICON_SVG, ZUDO_MARK_DATA_URI, ZUDO_MARK_DARK_DATA_URI, ZUDO_WORDMARK_DATA_URI, ZUDO_WORDMARK_DARK_DATA_URI, ZUDO_FAVICON_DATA_URI, ZUDO_SITE_URL, } from "./openApiUi/index.js";
41
+ export { renderOpenAPIUI, zudoLogo, svgToDataUri, ZUDO_MARK_SVG, ZUDO_MARK_DARK_SVG, ZUDO_WORDMARK_SVG, ZUDO_WORDMARK_DARK_SVG, ZUDO_FAVICON_SVG, ZUDO_MARK_DATA_URI, ZUDO_MARK_DARK_DATA_URI, ZUDO_WORDMARK_DATA_URI, ZUDO_WORDMARK_DARK_DATA_URI, ZUDO_FAVICON_DATA_URI, ZUDO_SITE_URL, SWAGGER_UI_VERSION, REDOC_VERSION, buildOpenAPIUIContentSecurityPolicy, } from "./openApiUi/index.js";
42
42
  /* ─── Document builder ──────────────────────────────────────────────────── */
43
43
  export { OpenAPIDocumentBuilder, createOpenAPIDocumentBuilder, } from "./openApiDocument/index.js";
44
44
  /* ─── Registry ──────────────────────────────────────────────────────────── */
@@ -8,21 +8,14 @@
8
8
  * version mismatches into a client-visible body tells an attacker about the
9
9
  * shape of the API rather than telling the operator anything.
10
10
  */
11
- import { BaseError } from "@zudojs/errors";
12
- /** Options for creating an OpenAPI error. */
13
- export interface OpenAPIErrorOptions {
14
- readonly code?: string;
15
- /** Overrides the subclass's default status. */
16
- readonly statusCode?: number;
17
- /** Overrides the subclass's default exposure. */
18
- readonly expose?: boolean;
19
- readonly cause?: unknown;
20
- readonly metadata?: Readonly<Record<string, unknown>>;
21
- }
22
- /** Base error for all OpenAPI subsystem failures. */
23
- export declare class OpenAPIError extends BaseError {
24
- constructor(message: string, options?: OpenAPIErrorOptions);
25
- }
11
+ import { OpenAPIError, type OpenAPIErrorOptions } from "@zudojs/errors";
12
+ /**
13
+ * `OpenAPIError` (the base of every class in this package) and
14
+ * `OpenAPIErrorOptions` live in `@zudojs/errors` (code `OPENAPI_DOCUMENT`,
15
+ * category `openapi`, 500, not exposed); they are re-exported here so
16
+ * existing imports keep working.
17
+ */
18
+ export { OpenAPIError, type OpenAPIErrorOptions };
26
19
  /** Creates an OpenAPI error. */
27
20
  export declare function createOpenAPIError(message: string, options?: OpenAPIErrorOptions): OpenAPIError;
28
21
  /** Determines whether an unknown value is an OpenAPI error. */
@@ -8,22 +8,14 @@
8
8
  * version mismatches into a client-visible body tells an attacker about the
9
9
  * shape of the API rather than telling the operator anything.
10
10
  */
11
- import { BaseError, ErrorCode, ErrorCategory, ErrorSeverity, } from "@zudojs/errors";
12
- /** Base error for all OpenAPI subsystem failures. */
13
- export class OpenAPIError extends BaseError {
14
- constructor(message, options = {}) {
15
- super(message, {
16
- code: options.code ?? ErrorCode.OPENAPI_DOCUMENT,
17
- category: ErrorCategory.OPENAPI,
18
- severity: ErrorSeverity.ERROR,
19
- statusCode: options.statusCode ?? 500,
20
- expose: options.expose ?? false,
21
- cause: options.cause,
22
- metadata: options.metadata,
23
- });
24
- this.name = "OpenAPIError";
25
- }
26
- }
11
+ import { OpenAPIError } from "@zudojs/errors";
12
+ /**
13
+ * `OpenAPIError` (the base of every class in this package) and
14
+ * `OpenAPIErrorOptions` live in `@zudojs/errors` (code `OPENAPI_DOCUMENT`,
15
+ * category `openapi`, 500, not exposed); they are re-exported here so
16
+ * existing imports keep working.
17
+ */
18
+ export { OpenAPIError };
27
19
  /** Creates an OpenAPI error. */
28
20
  export function createOpenAPIError(message, options = {}) {
29
21
  return new OpenAPIError(message, options);
@@ -5,6 +5,7 @@ import { SchemaRegistryImpl } from "../openApiSchema/schemaRegistry.core.js";
5
5
  import { toOpenAPIJSON, toOpenAPIYAML, } from "../openApiSerialization/openApiSerializer.core.js";
6
6
  import { DEFAULT_MEDIA_TYPE, DEFAULT_OPENAPI_VERSION, DOCUMENT_CACHE_TTL_MS, } from "../openApiConstants/openApiConstants.core.js";
7
7
  import { renderOpenAPIUI, zudoLogo, } from "../openApiUi/openApiUi.core.js";
8
+ import { buildOpenAPIUIContentSecurityPolicy, documentServerUrls, } from "../openApiUi/openApiUi.csp.js";
8
9
  /**
9
10
  * High-level OpenAPI manager that coordinates generation, validation, and
10
11
  * serving.
@@ -254,11 +255,20 @@ export class OpenAPIManager {
254
255
  ? this.logo
255
256
  : undefined,
256
257
  });
258
+ const csp = options.contentSecurityPolicy === false
259
+ ? undefined
260
+ : (options.contentSecurityPolicy ??
261
+ buildOpenAPIUIContentSecurityPolicy(options, [
262
+ ...documentServerUrls(this.getDocument()),
263
+ ...(options.connectSources ?? []),
264
+ ]));
257
265
  return Object.freeze({
258
266
  status: 200,
259
267
  headers: Object.freeze({
260
268
  "content-type": "text/html; charset=utf-8",
261
269
  "cache-control": "public, max-age=300",
270
+ "x-content-type-options": "nosniff",
271
+ ...(csp === undefined ? {} : { "content-security-policy": csp }),
262
272
  }),
263
273
  body,
264
274
  });
@@ -1,5 +1,27 @@
1
- import { convertRouteToOpenAPI } from "./routeConverter.core.js";
1
+ import { convertRouteToOpenAPI, toOpenAPIPath } from "./routeConverter.core.js";
2
2
  import { OpenAPIRouteError } from "../openApiErrors/openApiError.types.js";
3
+ /**
4
+ * Identity of a route inside the generated document.
5
+ *
6
+ * Keyed on the OpenAPI path template rather than the source path, because
7
+ * that is what the document is keyed on: `/users/:id` and `/users/{id}` are
8
+ * one path item. Keying on the raw spelling let both register, and the
9
+ * second then replaced the first during generation — one operation vanished
10
+ * from the published spec with `validate()` reporting nothing.
11
+ *
12
+ * A path `toOpenAPIPath` cannot express keeps its raw spelling here so the
13
+ * conversion error still surfaces from `scan()`, where it always has.
14
+ */
15
+ function routeKey(method, path) {
16
+ let template;
17
+ try {
18
+ template = toOpenAPIPath(path);
19
+ }
20
+ catch {
21
+ template = path;
22
+ }
23
+ return `${method.toLowerCase()}:${template}`;
24
+ }
3
25
  /**
4
26
  * Collects routes and converts them into OpenAPI operations.
5
27
  *
@@ -12,23 +34,33 @@ export class OpenAPIRouteScannerImpl {
12
34
  routes = new Map();
13
35
  /** Registers a route. */
14
36
  addRoute(route) {
15
- const key = `${route.method.toLowerCase()}:${route.path}`;
16
- if (this.routes.has(key)) {
17
- throw new OpenAPIRouteError(`Route ${route.method.toUpperCase()} ${route.path} is already registered.`, { metadata: { method: route.method, path: route.path } });
37
+ const key = routeKey(route.method, route.path);
38
+ const existing = this.routes.get(key);
39
+ if (existing !== undefined) {
40
+ throw new OpenAPIRouteError(`Route ${route.method.toUpperCase()} ${route.path} is already registered` +
41
+ (existing.path === route.path
42
+ ? "."
43
+ : ` as ${existing.method.toUpperCase()} ${existing.path}; both describe the same OpenAPI path.`), {
44
+ metadata: {
45
+ method: route.method,
46
+ path: route.path,
47
+ existingPath: existing.path,
48
+ },
49
+ });
18
50
  }
19
51
  this.routes.set(key, route);
20
52
  }
21
53
  /** Registers a route, replacing any existing one for the same method+path. */
22
54
  setRoute(route) {
23
- this.routes.set(`${route.method.toLowerCase()}:${route.path}`, route);
55
+ this.routes.set(routeKey(route.method, route.path), route);
24
56
  }
25
57
  /** True when a route is registered for this method and path. */
26
58
  hasRoute(method, path) {
27
- return this.routes.has(`${method.toLowerCase()}:${path}`);
59
+ return this.routes.has(routeKey(method, path));
28
60
  }
29
61
  /** Removes a route. Returns whether one was removed. */
30
62
  removeRoute(method, path) {
31
- return this.routes.delete(`${method.toLowerCase()}:${path}`);
63
+ return this.routes.delete(routeKey(method, path));
32
64
  }
33
65
  /** Number of registered routes. */
34
66
  get size() {
@@ -11,7 +11,10 @@ import type { OpenAPISchema } from "../openApiTypes/openApiTypes.core.js";
11
11
  *
12
12
  * Field names as of `@zudojs/schema@0.1.0`:
13
13
  * object `_config.shape`, `_config.requiredKeys` (a Set), `_config.unknownKeys`
14
+ * (`"strip" | "strict" | "passthrough"`; absent means the parser's
15
+ * own `?? "strip"` default, so it is read with that same default)
14
16
  * array `_config.itemSchema`, `_config.min`, `_config.max`, `_config.length`
17
+ * (no `max` means the parser's implicit ceiling, which is emitted)
15
18
  * string `_config.min|max|length|pattern|format`
16
19
  * number `_config.min|max|int|gt|lt|multipleOf`
17
20
  * coerce.string / coerce.number
@@ -1,3 +1,4 @@
1
+ import { SCHEMA_DEFAULT_MAX_ARRAY_LENGTH, SCHEMA_DEFAULT_MAX_STRING_LENGTH, } from "@zudojs/constants";
1
2
  import { DEFAULT_OPENAPI_VERSION } from "../openApiConstants/openApiConstants.core.js";
2
3
  /** Maps `@zudojs/schema` string formats onto OpenAPI `format` values. */
3
4
  const STRING_FORMATS = {
@@ -155,7 +156,7 @@ function convertString(schema, state) {
155
156
  ? { minLength: exact, maxLength: exact }
156
157
  : {
157
158
  ...(num(c["min"]) !== undefined ? { minLength: num(c["min"]) } : {}),
158
- ...(num(c["max"]) !== undefined ? { maxLength: num(c["max"]) } : {}),
159
+ maxLength: num(c["max"]) ?? SCHEMA_DEFAULT_MAX_STRING_LENGTH,
159
160
  }),
160
161
  ...(pattern instanceof RegExp
161
162
  ? { pattern: pattern.source }
@@ -308,14 +309,19 @@ function convertSchemaNode(schema, state) {
308
309
  if (forced || !acceptsMissingKey(value))
309
310
  required.push(key);
310
311
  }
311
- const unknownKeys = c["unknownKeys"];
312
+ // `ObjectSchema` applies `?? "strip"` internally, so an absent
313
+ // `unknownKeys` is the same contract as an explicit `.strip()` and
314
+ // must document the same. Only `strict` emits
315
+ // `additionalProperties: false`: that is OpenAPI for "reject the
316
+ // payload", while strip accepts it and discards the extra key, so
317
+ // emitting it for strip made a generated client refuse what the
318
+ // service accepts.
319
+ const unknownKeys = c["unknownKeys"] ?? "strip";
312
320
  return {
313
321
  type: "object",
314
322
  ...(Object.keys(properties).length > 0 ? { properties } : {}),
315
323
  ...(required.length > 0 ? { required } : {}),
316
- ...(unknownKeys === "strip" || unknownKeys === "strict"
317
- ? { additionalProperties: false }
318
- : {}),
324
+ ...(unknownKeys === "strict" ? { additionalProperties: false } : {}),
319
325
  };
320
326
  }
321
327
  case "record": {
@@ -337,9 +343,7 @@ function convertSchemaNode(schema, state) {
337
343
  ...(num(c["min"]) !== undefined
338
344
  ? { minItems: num(c["min"]) }
339
345
  : {}),
340
- ...(num(c["max"]) !== undefined
341
- ? { maxItems: num(c["max"]) }
342
- : {}),
346
+ maxItems: num(c["max"]) ?? SCHEMA_DEFAULT_MAX_ARRAY_LENGTH,
343
347
  }),
344
348
  };
345
349
  }
@@ -5,4 +5,6 @@
5
5
  */
6
6
  export { renderOpenAPIUI, zudoLogo, escapeHtml, type OpenAPIUIOptions, type OpenAPIUIRenderer, } from "./openApiUi.core.js";
7
7
  export { ZUDO_MARK_SVG, ZUDO_MARK_DARK_SVG, ZUDO_WORDMARK_SVG, ZUDO_WORDMARK_DARK_SVG, ZUDO_FAVICON_SVG, ZUDO_MARK_DATA_URI, ZUDO_MARK_DARK_DATA_URI, ZUDO_WORDMARK_DATA_URI, ZUDO_WORDMARK_DARK_DATA_URI, ZUDO_FAVICON_DATA_URI, ZUDO_SITE_URL, svgToDataUri, } from "./openApiUi.brand.js";
8
+ export { SWAGGER_UI_VERSION, REDOC_VERSION, DEFAULT_SWAGGER_ASSETS, DEFAULT_REDOC_ASSETS, SWAGGER_UI_INTEGRITY, REDOC_INTEGRITY, type OpenAPIUIAssetIntegrity, } from "./openApiUi.assets.js";
9
+ export { buildOpenAPIUIContentSecurityPolicy } from "./openApiUi.csp.js";
8
10
  //# sourceMappingURL=index.d.ts.map
@@ -5,4 +5,6 @@
5
5
  */
6
6
  export { renderOpenAPIUI, zudoLogo, escapeHtml, } from "./openApiUi.core.js";
7
7
  export { ZUDO_MARK_SVG, ZUDO_MARK_DARK_SVG, ZUDO_WORDMARK_SVG, ZUDO_WORDMARK_DARK_SVG, ZUDO_FAVICON_SVG, ZUDO_MARK_DATA_URI, ZUDO_MARK_DARK_DATA_URI, ZUDO_WORDMARK_DATA_URI, ZUDO_WORDMARK_DARK_DATA_URI, ZUDO_FAVICON_DATA_URI, ZUDO_SITE_URL, svgToDataUri, } from "./openApiUi.brand.js";
8
+ export { SWAGGER_UI_VERSION, REDOC_VERSION, DEFAULT_SWAGGER_ASSETS, DEFAULT_REDOC_ASSETS, SWAGGER_UI_INTEGRITY, REDOC_INTEGRITY, } from "./openApiUi.assets.js";
9
+ export { buildOpenAPIUIContentSecurityPolicy } from "./openApiUi.csp.js";
8
10
  //# sourceMappingURL=index.js.map
@@ -0,0 +1,41 @@
1
+ /**
2
+ * Pinned viewer assets for the documentation page.
3
+ *
4
+ * Every default asset URL names an exact package version, and every file
5
+ * carries a Subresource Integrity hash. A floating tag such as
6
+ * `swagger-ui-dist@5` or `redoc/latest` runs whatever the CDN serves today on
7
+ * the API's own origin, where "Try it out" holds bearer tokens and cookies;
8
+ * a pinned, hashed file cannot change underneath the page.
9
+ *
10
+ * The hashes were computed from the files inside the npm tarballs
11
+ * (`swagger-ui-dist-5.33.0.tgz`, `redoc-2.5.4.tgz`) and checked against the
12
+ * copies served by cdn.jsdelivr.net and unpkg.com. When bumping a version,
13
+ * recompute them with
14
+ * `openssl dgst -sha384 -binary <file> | base64 -w0`.
15
+ */
16
+ /** Swagger UI version served by default. */
17
+ export declare const SWAGGER_UI_VERSION = "5.33.0";
18
+ /** ReDoc version served by default. */
19
+ export declare const REDOC_VERSION = "2.5.4";
20
+ /** Default base URL for Swagger UI assets. */
21
+ export declare const DEFAULT_SWAGGER_ASSETS = "https://cdn.jsdelivr.net/npm/swagger-ui-dist@5.33.0";
22
+ /** Default base URL for ReDoc assets. */
23
+ export declare const DEFAULT_REDOC_ASSETS = "https://cdn.jsdelivr.net/npm/redoc@2.5.4/bundles";
24
+ /** Integrity hashes for a viewer's script and (Swagger UI only) stylesheet. */
25
+ export interface OpenAPIUIAssetIntegrity {
26
+ /** SRI hash for the viewer script, e.g. `sha384-…`. */
27
+ readonly script?: string;
28
+ /** SRI hash for the viewer stylesheet (Swagger UI only). */
29
+ readonly stylesheet?: string;
30
+ }
31
+ /** SRI hashes for the default Swagger UI assets. */
32
+ export declare const SWAGGER_UI_INTEGRITY: OpenAPIUIAssetIntegrity;
33
+ /** SRI hashes for the default ReDoc assets. */
34
+ export declare const REDOC_INTEGRITY: OpenAPIUIAssetIntegrity;
35
+ /**
36
+ * Renders ` integrity="…" crossorigin="anonymous"` for an asset tag, or just
37
+ * ` crossorigin="anonymous"` when there is no hash. Rejects anything that is
38
+ * not a well-formed SRI token, since it is interpolated into an attribute.
39
+ */
40
+ export declare function integrityAttrs(hash: string | undefined): string;
41
+ //# sourceMappingURL=openApiUi.assets.d.ts.map
@@ -0,0 +1,48 @@
1
+ /**
2
+ * Pinned viewer assets for the documentation page.
3
+ *
4
+ * Every default asset URL names an exact package version, and every file
5
+ * carries a Subresource Integrity hash. A floating tag such as
6
+ * `swagger-ui-dist@5` or `redoc/latest` runs whatever the CDN serves today on
7
+ * the API's own origin, where "Try it out" holds bearer tokens and cookies;
8
+ * a pinned, hashed file cannot change underneath the page.
9
+ *
10
+ * The hashes were computed from the files inside the npm tarballs
11
+ * (`swagger-ui-dist-5.33.0.tgz`, `redoc-2.5.4.tgz`) and checked against the
12
+ * copies served by cdn.jsdelivr.net and unpkg.com. When bumping a version,
13
+ * recompute them with
14
+ * `openssl dgst -sha384 -binary <file> | base64 -w0`.
15
+ */
16
+ /** Swagger UI version served by default. */
17
+ export const SWAGGER_UI_VERSION = "5.33.0";
18
+ /** ReDoc version served by default. */
19
+ export const REDOC_VERSION = "2.5.4";
20
+ /** Default base URL for Swagger UI assets. */
21
+ export const DEFAULT_SWAGGER_ASSETS = `https://cdn.jsdelivr.net/npm/swagger-ui-dist@${SWAGGER_UI_VERSION}`;
22
+ /** Default base URL for ReDoc assets. */
23
+ export const DEFAULT_REDOC_ASSETS = `https://cdn.jsdelivr.net/npm/redoc@${REDOC_VERSION}/bundles`;
24
+ /** SRI hashes for the default Swagger UI assets. */
25
+ export const SWAGGER_UI_INTEGRITY = Object.freeze({
26
+ script: "sha384-YDALVcy8kj8yltLBVi1vBiBAUqdxvus673gM8XKwiy6aDUJFXivF/KCufekjYbVf",
27
+ stylesheet: "sha384-Ov4/wv3j2bmct8cDc5X4ngJZohVPzEmc6uDPH8WeljUxO5vtoykvMEfbu9Vh6RaW",
28
+ });
29
+ /** SRI hashes for the default ReDoc assets. */
30
+ export const REDOC_INTEGRITY = Object.freeze({
31
+ script: "sha384-w447zOpYfw/1Tv/5AK9NfHTlQIqE3RVR6KY62jCyy9zNDgO64cMwGGP1Fj0zJVf5",
32
+ });
33
+ const SRI = /^(sha256|sha384|sha512)-[A-Za-z0-9+/]+={0,2}$/;
34
+ /**
35
+ * Renders ` integrity="…" crossorigin="anonymous"` for an asset tag, or just
36
+ * ` crossorigin="anonymous"` when there is no hash. Rejects anything that is
37
+ * not a well-formed SRI token, since it is interpolated into an attribute.
38
+ */
39
+ export function integrityAttrs(hash) {
40
+ if (hash === undefined)
41
+ return ` crossorigin="anonymous"`;
42
+ const tokens = hash.trim().split(/\s+/);
43
+ if (tokens.length === 0 || !tokens.every((t) => SRI.test(t))) {
44
+ throw new TypeError(`Invalid Subresource Integrity value: "${hash}"`);
45
+ }
46
+ return ` integrity="${tokens.join(" ")}" crossorigin="anonymous"`;
47
+ }
48
+ //# sourceMappingURL=openApiUi.assets.js.map
@@ -22,5 +22,5 @@ export declare const ZUDO_WORDMARK_DATA_URI: string;
22
22
  export declare const ZUDO_WORDMARK_DARK_DATA_URI: string;
23
23
  export declare const ZUDO_FAVICON_DATA_URI: string;
24
24
  /** Where the logo links by default. */
25
- export declare const ZUDO_SITE_URL = "https://zudo.dev";
25
+ export declare const ZUDO_SITE_URL = "https://zudojs.oyinlola.site";
26
26
  //# sourceMappingURL=openApiUi.brand.d.ts.map
@@ -40,5 +40,5 @@ export const ZUDO_WORDMARK_DATA_URI = svgToDataUri(ZUDO_WORDMARK_SVG);
40
40
  export const ZUDO_WORDMARK_DARK_DATA_URI = svgToDataUri(ZUDO_WORDMARK_DARK_SVG);
41
41
  export const ZUDO_FAVICON_DATA_URI = svgToDataUri(ZUDO_FAVICON_SVG);
42
42
  /** Where the logo links by default. */
43
- export const ZUDO_SITE_URL = "https://zudo.dev";
43
+ export const ZUDO_SITE_URL = "https://zudojs.oyinlola.site";
44
44
  //# sourceMappingURL=openApiUi.brand.js.map
@@ -9,6 +9,7 @@
9
9
  * `content-type: text/html`. {@link OpenAPIManager.toUIResponse} does that.
10
10
  */
11
11
  import type { OpenAPILogo } from "../openApiTypes/openApiTypes.core.js";
12
+ import { type OpenAPIUIAssetIntegrity } from "./openApiUi.assets.js";
12
13
  /** Which viewer to render. */
13
14
  export type OpenAPIUIRenderer = "swagger" | "redoc";
14
15
  /** Options for {@link renderOpenAPIUI}. */
@@ -21,7 +22,7 @@ export interface OpenAPIUIOptions {
21
22
  readonly renderer?: OpenAPIUIRenderer;
22
23
  /**
23
24
  * Logo shown in the page header. Default: the Zudo wordmark linking to
24
- * zudo.dev. Pass `false` to render no logo at all.
25
+ * zudojs.oyinlola.site. Pass `false` to render no logo at all.
25
26
  */
26
27
  readonly logo?: OpenAPILogo | false;
27
28
  /** Favicon URL or data URI. Default: the Zudo favicon. */
@@ -29,12 +30,30 @@ export interface OpenAPIUIOptions {
29
30
  /** Extra CSS appended after the built-in theme. */
30
31
  readonly customCss?: string;
31
32
  /**
32
- * Base URL the viewer's own assets load from. Defaults to a public CDN:
33
- * `https://unpkg.com/swagger-ui-dist@5` for Swagger UI and
34
- * `https://cdn.redoc.ly/redoc/latest/bundles` for ReDoc. Point it at a
35
- * self-hosted copy for air-gapped deployments.
33
+ * Base URL the viewer's own assets load from. Defaults to exact, pinned
34
+ * versions on cdn.jsdelivr.net (`swagger-ui-dist@5.33.0`, `redoc@2.5.4`),
35
+ * loaded with Subresource Integrity. Point it at a self-hosted copy for
36
+ * air-gapped deployments; pass `assetIntegrity` to keep SRI on that copy.
36
37
  */
37
38
  readonly assetsBaseUrl?: string;
39
+ /**
40
+ * SRI hashes for the viewer assets. Default: the pinned hashes when
41
+ * `assetsBaseUrl` is not set, and none when it is (a self-hosted copy may
42
+ * be a different build). Pass `false` to omit `integrity` entirely.
43
+ */
44
+ readonly assetIntegrity?: OpenAPIUIAssetIntegrity | false;
45
+ /**
46
+ * Value for the `content-security-policy` header sent by
47
+ * `OpenAPIManager.toUIResponse`. Default: a restrictive policy built by
48
+ * `buildOpenAPIUIContentSecurityPolicy`. Pass `false` to send none.
49
+ * Ignored by `renderOpenAPIUI`, which returns only the HTML.
50
+ */
51
+ readonly contentSecurityPolicy?: string | false;
52
+ /**
53
+ * Extra origins "Try it out" may call, added to the default policy's
54
+ * `connect-src`. The document's absolute `servers` are added automatically.
55
+ */
56
+ readonly connectSources?: readonly string[];
38
57
  /** Options forwarded to `SwaggerUIBundle(...)`; ignored by ReDoc. */
39
58
  readonly swaggerOptions?: Readonly<Record<string, unknown>>;
40
59
  }
@@ -9,9 +9,9 @@
9
9
  * `content-type: text/html`. {@link OpenAPIManager.toUIResponse} does that.
10
10
  */
11
11
  import { ZUDO_FAVICON_DATA_URI, ZUDO_MARK_DATA_URI, ZUDO_SITE_URL, ZUDO_WORDMARK_DARK_DATA_URI, } from "./openApiUi.brand.js";
12
+ import { DEFAULT_REDOC_ASSETS, DEFAULT_SWAGGER_ASSETS, REDOC_INTEGRITY, SWAGGER_UI_INTEGRITY, integrityAttrs, } from "./openApiUi.assets.js";
13
+ import { swaggerInitScript, THEME_CSS } from "./openApiUiPage/index.js";
12
14
  const DEFAULT_TITLE = "API reference";
13
- const SWAGGER_ASSETS = "https://unpkg.com/swagger-ui-dist@5";
14
- const REDOC_ASSETS = "https://cdn.redoc.ly/redoc/latest/bundles";
15
15
  /** The default logo used by every branded page and by `info["x-logo"]`. */
16
16
  export function zudoLogo(overrides) {
17
17
  return Object.freeze({
@@ -31,13 +31,6 @@ export function escapeHtml(value) {
31
31
  .replace(/"/g, "&quot;")
32
32
  .replace(/'/g, "&#39;");
33
33
  }
34
- /** Serialises a value for an inline `<script>` without letting `</script>` through. */
35
- function jsLiteral(value) {
36
- return JSON.stringify(value)
37
- .replace(/</g, "\\u003c")
38
- .replace(/\u2028/g, "\\u2028")
39
- .replace(/\u2029/g, "\\u2029");
40
- }
41
34
  /**
42
35
  * Rejects URLs that could execute script when placed in `src`/`href`.
43
36
  *
@@ -83,26 +76,6 @@ function safeCss(css) {
83
76
  }
84
77
  return css;
85
78
  }
86
- const THEME_CSS = `
87
- :root{--zd-ink:#1A1A2E;--zd-red:#C0392B;--zd-bg:#FAFAF9;--zd-border:#E5E7EB}
88
- *{border-radius:0!important}
89
- html,body{margin:0;background:var(--zd-bg);font-family:Inter,system-ui,-apple-system,"Segoe UI",sans-serif}
90
- .zudo-bar{position:sticky;top:0;z-index:50;display:flex;align-items:center;gap:14px;height:56px;padding:0 20px;background:var(--zd-ink);color:var(--zd-bg);border-bottom:3px solid var(--zd-red)}
91
- .zudo-bar a{display:inline-flex;align-items:center;color:inherit;text-decoration:none}
92
- .zudo-bar img{height:22px;width:auto;display:block}
93
- .zudo-bar .zudo-sep{width:1px;height:22px;background:rgba(250,250,249,.25)}
94
- .zudo-bar .zudo-title{font-weight:800;font-size:14px;letter-spacing:.02em;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
95
- .zudo-bar .zudo-spec{margin-left:auto;font:700 11px/1 ui-monospace,SFMono-Regular,Menlo,monospace;letter-spacing:.06em;text-transform:uppercase;color:rgba(250,250,249,.75);border:2px solid rgba(250,250,249,.3);padding:6px 10px}
96
- .zudo-bar .zudo-spec:hover{border-color:var(--zd-bg);color:var(--zd-bg)}
97
- .zudo-foot{padding:16px 20px;border-top:1px solid var(--zd-border);color:#6B7280;font-size:12px;display:flex;gap:8px;align-items:center}
98
- .zudo-foot img{height:14px;width:auto}
99
- .swagger-ui .topbar{display:none}
100
- .swagger-ui .info .title{font-weight:900;letter-spacing:-.01em;color:var(--zd-ink)}
101
- .swagger-ui .opblock{border-width:2px;box-shadow:none}
102
- .swagger-ui .btn{border-width:2px;font-weight:700}
103
- .swagger-ui .btn.execute{background:var(--zd-red);border-color:var(--zd-red)}
104
- .swagger-ui .scheme-container{box-shadow:none;border-bottom:1px solid var(--zd-border);background:#fff}
105
- `;
106
79
  function header(options, title) {
107
80
  const logo = options.logo === undefined ? zudoLogo({ url: ZUDO_WORDMARK_DARK_DATA_URI }) : options.logo;
108
81
  const logoHtml = logo === false
@@ -131,27 +104,29 @@ export function renderOpenAPIUI(options) {
131
104
  }
132
105
  const title = options.title ?? DEFAULT_TITLE;
133
106
  const renderer = options.renderer ?? "swagger";
107
+ const integrity = resolveIntegrity(options, renderer);
134
108
  if (renderer === "redoc") {
135
- const base = (options.assetsBaseUrl ?? REDOC_ASSETS).replace(/(?<!\/)\/+$/, "");
109
+ const base = (options.assetsBaseUrl ?? DEFAULT_REDOC_ASSETS).replace(/(?<!\/)\/+$/, "");
136
110
  return (head(options, title, "") +
137
111
  `<body>${header(options, title)}` +
138
112
  `<redoc spec-url="${safeUrl(options.specUrl)}" hide-hostname></redoc>` +
139
- `<script src="${safeUrl(base)}/redoc.standalone.js"></script>` +
113
+ `<script src="${safeUrl(base)}/redoc.standalone.js"${integrityAttrs(integrity.script)}></script>` +
140
114
  `${footer()}</body></html>`);
141
115
  }
142
- const base = (options.assetsBaseUrl ?? SWAGGER_ASSETS).replace(/(?<!\/)\/+$/, "");
143
- const config = {
144
- url: options.specUrl,
145
- dom_id: "#zudo-openapi",
146
- deepLinking: true,
147
- displayRequestDuration: true,
148
- tryItOutEnabled: true,
149
- ...options.swaggerOptions,
150
- };
151
- return (head(options, title, `<link rel="stylesheet" href="${safeUrl(base)}/swagger-ui.css">`) +
116
+ const base = (options.assetsBaseUrl ?? DEFAULT_SWAGGER_ASSETS).replace(/(?<!\/)\/+$/, "");
117
+ return (head(options, title, `<link rel="stylesheet" href="${safeUrl(base)}/swagger-ui.css"${integrityAttrs(integrity.stylesheet)}>`) +
152
118
  `<body>${header(options, title)}<div id="zudo-openapi"></div>` +
153
- `<script src="${safeUrl(base)}/swagger-ui-bundle.js" crossorigin></script>` +
154
- `<script>window.addEventListener("load",function(){window.ui=SwaggerUIBundle(${jsLiteral(config)});});</script>` +
119
+ `<script src="${safeUrl(base)}/swagger-ui-bundle.js"${integrityAttrs(integrity.script)}></script>` +
120
+ `<script>${swaggerInitScript(options)}</script>` +
155
121
  `${footer()}</body></html>`);
156
122
  }
123
+ function resolveIntegrity(options, renderer) {
124
+ if (options.assetIntegrity === false)
125
+ return {};
126
+ if (options.assetIntegrity !== undefined)
127
+ return options.assetIntegrity;
128
+ if (options.assetsBaseUrl !== undefined)
129
+ return {};
130
+ return renderer === "redoc" ? REDOC_INTEGRITY : SWAGGER_UI_INTEGRITY;
131
+ }
157
132
  //# sourceMappingURL=openApiUi.core.js.map
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Content-Security-Policy for the documentation page.
3
+ *
4
+ * The page runs third-party viewer code on the API's own origin, so the
5
+ * policy limits scripts to the viewer's asset origin plus the hash of the
6
+ * page's single inline bootstrap script, forbids plugins, framing by other
7
+ * origins and `<base>` rewrites, and limits `fetch` to the page's origin, the
8
+ * spec URL and the servers the document declares ("Try it out" calls them).
9
+ */
10
+ import type { OpenAPIDocument } from "../openApiTypes/openApiTypes.core.js";
11
+ import type { OpenAPIUIOptions } from "./openApiUi.core.js";
12
+ /** Returns `'self'` for relative URLs, the origin for absolute http(s) ones. */
13
+ export declare function cspSourceFor(url: string): string | undefined;
14
+ /**
15
+ * Builds the `content-security-policy` header value for a page rendered by
16
+ * `renderOpenAPIUI(options)`.
17
+ *
18
+ * @param options - The same options the page was rendered with.
19
+ * @param connectSources - Extra URLs or origins "Try it out" may call,
20
+ * typically the document's `servers`.
21
+ */
22
+ export declare function buildOpenAPIUIContentSecurityPolicy(options: OpenAPIUIOptions, connectSources?: readonly string[]): string;
23
+ /** Every `servers[].url` in a document: top level, path items and operations. */
24
+ export declare function documentServerUrls(document: OpenAPIDocument): string[];
25
+ //# sourceMappingURL=openApiUi.csp.d.ts.map
@@ -0,0 +1,92 @@
1
+ /**
2
+ * Content-Security-Policy for the documentation page.
3
+ *
4
+ * The page runs third-party viewer code on the API's own origin, so the
5
+ * policy limits scripts to the viewer's asset origin plus the hash of the
6
+ * page's single inline bootstrap script, forbids plugins, framing by other
7
+ * origins and `<base>` rewrites, and limits `fetch` to the page's origin, the
8
+ * spec URL and the servers the document declares ("Try it out" calls them).
9
+ */
10
+ import { createHash } from "node:crypto";
11
+ import { DEFAULT_REDOC_ASSETS, DEFAULT_SWAGGER_ASSETS } from "./openApiUi.assets.js";
12
+ import { swaggerInitScript } from "./openApiUiPage/index.js";
13
+ /** Returns `'self'` for relative URLs, the origin for absolute http(s) ones. */
14
+ export function cspSourceFor(url) {
15
+ const trimmed = url.trim();
16
+ if (!/^[a-z][a-z0-9+.-]*:/i.test(trimmed) && !trimmed.startsWith("//")) {
17
+ return "'self'";
18
+ }
19
+ try {
20
+ const parsed = new URL(trimmed.startsWith("//") ? `https:${trimmed}` : trimmed);
21
+ if (parsed.protocol !== "https:" && parsed.protocol !== "http:")
22
+ return undefined;
23
+ return parsed.origin;
24
+ }
25
+ catch {
26
+ return undefined;
27
+ }
28
+ }
29
+ function unique(values) {
30
+ return [...new Set(values.filter((v) => v !== undefined))].join(" ");
31
+ }
32
+ /**
33
+ * Builds the `content-security-policy` header value for a page rendered by
34
+ * `renderOpenAPIUI(options)`.
35
+ *
36
+ * @param options - The same options the page was rendered with.
37
+ * @param connectSources - Extra URLs or origins "Try it out" may call,
38
+ * typically the document's `servers`.
39
+ */
40
+ export function buildOpenAPIUIContentSecurityPolicy(options, connectSources = []) {
41
+ const renderer = options.renderer ?? "swagger";
42
+ const assets = options.assetsBaseUrl ??
43
+ (renderer === "redoc" ? DEFAULT_REDOC_ASSETS : DEFAULT_SWAGGER_ASSETS);
44
+ const assetSource = cspSourceFor(assets) ?? "'self'";
45
+ const scriptSources = renderer === "redoc"
46
+ ? [assetSource]
47
+ : [
48
+ assetSource,
49
+ `'sha256-${createHash("sha256")
50
+ .update(swaggerInitScript(options))
51
+ .digest("base64")}'`,
52
+ ];
53
+ const connect = unique([
54
+ "'self'",
55
+ cspSourceFor(options.specUrl),
56
+ ...connectSources.map(cspSourceFor),
57
+ ]);
58
+ return [
59
+ "default-src 'none'",
60
+ `script-src ${unique(scriptSources)}`,
61
+ `style-src ${unique([assetSource, "'unsafe-inline'"])}`,
62
+ "img-src 'self' data: https:",
63
+ `font-src ${unique([assetSource, "data:"])}`,
64
+ `connect-src ${connect}`,
65
+ "worker-src 'self' blob:",
66
+ "object-src 'none'",
67
+ "base-uri 'none'",
68
+ "form-action 'self'",
69
+ "frame-ancestors 'self'",
70
+ ].join("; ");
71
+ }
72
+ /** Every `servers[].url` in a document: top level, path items and operations. */
73
+ export function documentServerUrls(document) {
74
+ const urls = (document.servers ?? []).map((s) => s.url);
75
+ for (const item of Object.values(document.paths ?? {})) {
76
+ if (item === undefined || item === null)
77
+ continue;
78
+ for (const s of item.servers ?? [])
79
+ urls.push(s.url);
80
+ for (const value of Object.values(item)) {
81
+ const servers = value?.servers;
82
+ if (!Array.isArray(servers))
83
+ continue;
84
+ for (const s of servers) {
85
+ if (typeof s?.url === "string")
86
+ urls.push(s.url);
87
+ }
88
+ }
89
+ }
90
+ return urls;
91
+ }
92
+ //# sourceMappingURL=openApiUi.csp.js.map
@@ -0,0 +1,9 @@
1
+ /**
2
+ * @zudojs/openapi/openApiUi/openApiUiPage
3
+ *
4
+ * Internal page parts shared by the renderer and its Content-Security-Policy:
5
+ * the Swagger UI bootstrap script and the Zudo theme CSS.
6
+ */
7
+ export { jsLiteral, swaggerInitScript, type SwaggerInitInput, } from "./openApiUiPage.script.js";
8
+ export { THEME_CSS } from "./openApiUiPage.theme.js";
9
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1,9 @@
1
+ /**
2
+ * @zudojs/openapi/openApiUi/openApiUiPage
3
+ *
4
+ * Internal page parts shared by the renderer and its Content-Security-Policy:
5
+ * the Swagger UI bootstrap script and the Zudo theme CSS.
6
+ */
7
+ export { jsLiteral, swaggerInitScript, } from "./openApiUiPage.script.js";
8
+ export { THEME_CSS } from "./openApiUiPage.theme.js";
9
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,14 @@
1
+ /**
2
+ * The one inline script the Swagger UI page runs, built in one place so the
3
+ * page and its Content-Security-Policy hash agree byte for byte.
4
+ */
5
+ /** Serialises a value for an inline `<script>` without letting `</script>` through. */
6
+ export declare function jsLiteral(value: unknown): string;
7
+ /** Inputs that shape the Swagger UI bootstrap script. */
8
+ export interface SwaggerInitInput {
9
+ readonly specUrl: string;
10
+ readonly swaggerOptions?: Readonly<Record<string, unknown>>;
11
+ }
12
+ /** Returns the body of the inline `<script>` that boots Swagger UI. */
13
+ export declare function swaggerInitScript(input: SwaggerInitInput): string;
14
+ //# sourceMappingURL=openApiUiPage.script.d.ts.map
@@ -0,0 +1,24 @@
1
+ /**
2
+ * The one inline script the Swagger UI page runs, built in one place so the
3
+ * page and its Content-Security-Policy hash agree byte for byte.
4
+ */
5
+ /** Serialises a value for an inline `<script>` without letting `</script>` through. */
6
+ export function jsLiteral(value) {
7
+ return JSON.stringify(value)
8
+ .replace(/</g, "\\u003c")
9
+ .replace(/\u2028/g, "\\u2028")
10
+ .replace(/\u2029/g, "\\u2029");
11
+ }
12
+ /** Returns the body of the inline `<script>` that boots Swagger UI. */
13
+ export function swaggerInitScript(input) {
14
+ const config = {
15
+ url: input.specUrl,
16
+ dom_id: "#zudo-openapi",
17
+ deepLinking: true,
18
+ displayRequestDuration: true,
19
+ tryItOutEnabled: true,
20
+ ...input.swaggerOptions,
21
+ };
22
+ return `window.addEventListener("load",function(){window.ui=SwaggerUIBundle(${jsLiteral(config)});});`;
23
+ }
24
+ //# sourceMappingURL=openApiUiPage.script.js.map
@@ -0,0 +1,6 @@
1
+ /**
2
+ * The Zudo theme applied on top of the viewer's own stylesheet.
3
+ */
4
+ /** Built-in page CSS; `customCss` is appended after it. */
5
+ export declare const THEME_CSS = "\n:root{--zd-ink:#1A1A2E;--zd-red:#C0392B;--zd-bg:#FAFAF9;--zd-border:#E5E7EB}\n*{border-radius:0!important}\nhtml,body{margin:0;background:var(--zd-bg);font-family:Inter,system-ui,-apple-system,\"Segoe UI\",sans-serif}\n.zudo-bar{position:sticky;top:0;z-index:50;display:flex;align-items:center;gap:14px;height:56px;padding:0 20px;background:var(--zd-ink);color:var(--zd-bg);border-bottom:3px solid var(--zd-red)}\n.zudo-bar a{display:inline-flex;align-items:center;color:inherit;text-decoration:none}\n.zudo-bar img{height:22px;width:auto;display:block}\n.zudo-bar .zudo-sep{width:1px;height:22px;background:rgba(250,250,249,.25)}\n.zudo-bar .zudo-title{font-weight:800;font-size:14px;letter-spacing:.02em;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}\n.zudo-bar .zudo-spec{margin-left:auto;font:700 11px/1 ui-monospace,SFMono-Regular,Menlo,monospace;letter-spacing:.06em;text-transform:uppercase;color:rgba(250,250,249,.75);border:2px solid rgba(250,250,249,.3);padding:6px 10px}\n.zudo-bar .zudo-spec:hover{border-color:var(--zd-bg);color:var(--zd-bg)}\n.zudo-foot{padding:16px 20px;border-top:1px solid var(--zd-border);color:#6B7280;font-size:12px;display:flex;gap:8px;align-items:center}\n.zudo-foot img{height:14px;width:auto}\n.swagger-ui .topbar{display:none}\n.swagger-ui .info .title{font-weight:900;letter-spacing:-.01em;color:var(--zd-ink)}\n.swagger-ui .opblock{border-width:2px;box-shadow:none}\n.swagger-ui .btn{border-width:2px;font-weight:700}\n.swagger-ui .btn.execute{background:var(--zd-red);border-color:var(--zd-red)}\n.swagger-ui .scheme-container{box-shadow:none;border-bottom:1px solid var(--zd-border);background:#fff}\n";
6
+ //# sourceMappingURL=openApiUiPage.theme.d.ts.map
@@ -0,0 +1,25 @@
1
+ /**
2
+ * The Zudo theme applied on top of the viewer's own stylesheet.
3
+ */
4
+ /** Built-in page CSS; `customCss` is appended after it. */
5
+ export const THEME_CSS = `
6
+ :root{--zd-ink:#1A1A2E;--zd-red:#C0392B;--zd-bg:#FAFAF9;--zd-border:#E5E7EB}
7
+ *{border-radius:0!important}
8
+ html,body{margin:0;background:var(--zd-bg);font-family:Inter,system-ui,-apple-system,"Segoe UI",sans-serif}
9
+ .zudo-bar{position:sticky;top:0;z-index:50;display:flex;align-items:center;gap:14px;height:56px;padding:0 20px;background:var(--zd-ink);color:var(--zd-bg);border-bottom:3px solid var(--zd-red)}
10
+ .zudo-bar a{display:inline-flex;align-items:center;color:inherit;text-decoration:none}
11
+ .zudo-bar img{height:22px;width:auto;display:block}
12
+ .zudo-bar .zudo-sep{width:1px;height:22px;background:rgba(250,250,249,.25)}
13
+ .zudo-bar .zudo-title{font-weight:800;font-size:14px;letter-spacing:.02em;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
14
+ .zudo-bar .zudo-spec{margin-left:auto;font:700 11px/1 ui-monospace,SFMono-Regular,Menlo,monospace;letter-spacing:.06em;text-transform:uppercase;color:rgba(250,250,249,.75);border:2px solid rgba(250,250,249,.3);padding:6px 10px}
15
+ .zudo-bar .zudo-spec:hover{border-color:var(--zd-bg);color:var(--zd-bg)}
16
+ .zudo-foot{padding:16px 20px;border-top:1px solid var(--zd-border);color:#6B7280;font-size:12px;display:flex;gap:8px;align-items:center}
17
+ .zudo-foot img{height:14px;width:auto}
18
+ .swagger-ui .topbar{display:none}
19
+ .swagger-ui .info .title{font-weight:900;letter-spacing:-.01em;color:var(--zd-ink)}
20
+ .swagger-ui .opblock{border-width:2px;box-shadow:none}
21
+ .swagger-ui .btn{border-width:2px;font-weight:700}
22
+ .swagger-ui .btn.execute{background:var(--zd-red);border-color:var(--zd-red)}
23
+ .swagger-ui .scheme-container{box-shadow:none;border-bottom:1px solid var(--zd-border);background:#fff}
24
+ `;
25
+ //# sourceMappingURL=openApiUiPage.theme.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zudojs/openapi",
3
- "version": "1.2.0",
3
+ "version": "1.4.0",
4
4
  "description": "OpenAPI 3.0 and 3.1 specification generation, validation, and serialization for Zudojs applications.",
5
5
  "license": "MIT",
6
6
  "author": {
@@ -26,7 +26,8 @@
26
26
  "!dist/.tsbuildinfo"
27
27
  ],
28
28
  "dependencies": {
29
- "@zudojs/errors": "1.0.1"
29
+ "@zudojs/constants": "1.1.1",
30
+ "@zudojs/errors": "1.2.0"
30
31
  },
31
32
  "devDependencies": {
32
33
  "@types/node": "^26.4.1",