@zudojs/openapi 1.1.0 → 1.3.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.
Files changed (30) hide show
  1. package/README.md +62 -14
  2. package/dist/index.d.ts +1 -1
  3. package/dist/index.js +1 -1
  4. package/dist/openApiErrors/openApiError.base.d.ts +8 -15
  5. package/dist/openApiErrors/openApiError.base.js +8 -16
  6. package/dist/openApiHttp/openApiHttpAdapter.core.d.ts +2 -0
  7. package/dist/openApiHttp/openApiHttpAdapter.core.js +25 -1
  8. package/dist/openApiRegistry/openApiRegistry.core.d.ts +9 -0
  9. package/dist/openApiRegistry/openApiRegistry.core.js +13 -0
  10. package/dist/openApiSchema/schemaConverter.core.d.ts +13 -2
  11. package/dist/openApiSchema/schemaConverter.core.js +60 -12
  12. package/dist/openApiSerialization/openApiSerializer.core.js +25 -2
  13. package/dist/openApiUi/index.d.ts +2 -0
  14. package/dist/openApiUi/index.js +2 -0
  15. package/dist/openApiUi/openApiUi.assets.d.ts +41 -0
  16. package/dist/openApiUi/openApiUi.assets.js +48 -0
  17. package/dist/openApiUi/openApiUi.brand.d.ts +1 -1
  18. package/dist/openApiUi/openApiUi.brand.js +1 -1
  19. package/dist/openApiUi/openApiUi.core.d.ts +24 -5
  20. package/dist/openApiUi/openApiUi.core.js +35 -46
  21. package/dist/openApiUi/openApiUi.csp.d.ts +25 -0
  22. package/dist/openApiUi/openApiUi.csp.js +92 -0
  23. package/dist/openApiUi/openApiUiPage/index.d.ts +9 -0
  24. package/dist/openApiUi/openApiUiPage/index.js +9 -0
  25. package/dist/openApiUi/openApiUiPage/openApiUiPage.script.d.ts +14 -0
  26. package/dist/openApiUi/openApiUiPage/openApiUiPage.script.js +24 -0
  27. package/dist/openApiUi/openApiUiPage/openApiUiPage.theme.d.ts +6 -0
  28. package/dist/openApiUi/openApiUiPage/openApiUiPage.theme.js +25 -0
  29. package/dist/openApiValidation/openApiValidator.core.js +41 -20
  30. package/package.json +7 -2
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,22 +94,45 @@ 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" });
101
127
  ```
102
128
 
103
129
  Caller-supplied text is escaped, and input that would break out of the page is
104
- refused rather than mangled: a `javascript:` or `vbscript:` URL throws, an
105
- empty `specUrl` throws, and `customCss` containing `</style>` throws — that
106
- sequence ends the style block and lets the rest be parsed as HTML.
130
+ refused rather than mangled: a `javascript:` or `vbscript:` URL throws (the
131
+ scheme is read after stripping the control characters browsers ignore, so
132
+ `java\nscript:` is caught too), a `data:` URL that is not an image throws
133
+ (the assets base lands in `<script src>`), an empty `specUrl` throws, and
134
+ `customCss` containing `</style>` throws — that sequence ends the style block
135
+ and lets the rest be parsed as HTML.
107
136
 
108
137
  ## Branding
109
138
 
@@ -113,7 +142,7 @@ default, so a spec opened in one of them shows a logo rather than nothing.
113
142
 
114
143
  ```typescript
115
144
  new OpenAPIManager({ info }).generate().info["x-logo"];
116
- // { url: "data:image/svg+xml;…", href: "https://zudo.dev", altText: "Zudo", … }
145
+ // { url: "data:image/svg+xml;…", href: "https://zudojs.oyinlola.site", altText: "Zudo", … }
117
146
  ```
118
147
 
119
148
  The `branding` option controls it, and the same value is used by
@@ -139,7 +168,8 @@ can show them without a network request: `ZUDO_MARK_SVG`, `ZUDO_MARK_DARK_SVG`,
139
168
  `ZUDO_WORDMARK_SVG`, `ZUDO_WORDMARK_DARK_SVG`, `ZUDO_FAVICON_SVG`, a
140
169
  `*_DATA_URI` counterpart for each, plus `ZUDO_SITE_URL`, `zudoLogo(overrides?)`
141
170
  and `svgToDataUri(svg)`. The types are `OpenAPIUIOptions`,
142
- `OpenAPIUIRenderer`, `OpenAPIUIResponse` and `OpenAPILogo`.
171
+ `OpenAPIUIRenderer`, `OpenAPIUIResponse`, `OpenAPIUIAssetIntegrity` and
172
+ `OpenAPILogo`.
143
173
 
144
174
  ## Schemas
145
175
 
@@ -170,9 +200,9 @@ produces
170
200
  {
171
201
  "type": "object",
172
202
  "properties": {
173
- "id": { "type": "string", "format": "uuid" },
203
+ "id": { "type": "string", "format": "uuid", "maxLength": 255 },
174
204
  "age": { "type": "integer", "minimum": 0 },
175
- "nickname": { "type": "string" }
205
+ "nickname": { "type": "string", "maxLength": 255 }
176
206
  },
177
207
  "required": ["id", "age"]
178
208
  }
@@ -180,9 +210,23 @@ produces
180
210
 
181
211
  Objects, arrays, enums, literals, unions, discriminated unions,
182
212
  intersections, records, tuples, sets, optionals, nullables, defaults,
183
- refinements, transforms, lazy schemas and the coercion wrappers are all
184
- converted, along with string and number constraints (`min`, `max`, `length`,
185
- `pattern`, `format`, `int`, `multipleOf`, `gt`, `lt`).
213
+ refinements, transforms, lazy schemas, bigints and the coercion wrappers are
214
+ all converted, along with string and number constraints (`min`, `max`,
215
+ `length`, `pattern`, `format`, `int`, `multipleOf`, `gt`, `lt`). Constraints
216
+ on a coercing schema (`coerce.number().int().min(1)`) are carried through.
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
+
225
+ A property is listed in `required` exactly when the object parser rejects
226
+ its absence: fields wrapped in `optional`, fields with a `default`, and
227
+ `any` / `unknown` fields are left out, and `.required()` forces every key
228
+ back on. A default supplied as a factory (`.default(() => new Date())`) is
229
+ invoked once and its value emitted.
186
230
 
187
231
  Anything that cannot be expressed exactly produces a **warning** rather than a
188
232
  silent `{}`:
@@ -259,7 +303,10 @@ The validator checks:
259
303
  - that path templates and `in: "path"` parameters agree in both directions —
260
304
  the classic "`{id}` is in the path but nowhere in `parameters`" mistake
261
305
  - that path parameters are marked required, and that no parameter is declared
262
- twice
306
+ twice in one list (an operation-level parameter may override a path-level
307
+ one with the same name and location)
308
+ - that no two paths are identical apart from their template parameter names
309
+ (`/users/{id}` next to `/users/{userId}`)
263
310
  - `operationId` uniqueness and length
264
311
  - that every `security` requirement names a scheme declared in
265
312
  `components.securitySchemes` — a typo there yields a document that _looks_
@@ -331,7 +378,8 @@ still produces a pointer that resolves.
331
378
 
332
379
  ## Errors
333
380
 
334
- 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
335
383
  default to status 500, not exposed — these are failures while a service builds
336
384
  or validates its own specification, not responses to a client request:
337
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);
@@ -57,6 +57,8 @@ export declare class OpenAPIManager {
57
57
  private readonly cacheTtlMs;
58
58
  private readonly now;
59
59
  private readonly logo;
60
+ /** True when `branding` was a caller-supplied logo rather than the default. */
61
+ private readonly customLogo;
60
62
  private cachedDocument?;
61
63
  private cachedAt;
62
64
  /** Whether the cached document was produced by a validating generate. */
@@ -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.
@@ -21,6 +22,8 @@ export class OpenAPIManager {
21
22
  cacheTtlMs;
22
23
  now;
23
24
  logo;
25
+ /** True when `branding` was a caller-supplied logo rather than the default. */
26
+ customLogo;
24
27
  cachedDocument;
25
28
  cachedAt = 0;
26
29
  /** Whether the cached document was produced by a validating generate. */
@@ -44,6 +47,8 @@ export class OpenAPIManager {
44
47
  : options.branding === true || options.branding === undefined
45
48
  ? zudoLogo()
46
49
  : options.branding;
50
+ this.customLogo =
51
+ typeof options.branding === "object" && options.branding !== null;
47
52
  if (options.info)
48
53
  this.registry.setInfo(options.info);
49
54
  for (const server of options.servers ?? [])
@@ -124,6 +129,11 @@ export class OpenAPIManager {
124
129
  * Safe to call repeatedly: routes are replaced rather than re-added.
125
130
  */
126
131
  generate(validate = false) {
132
+ // The scanner is the source of truth for routes. Re-setting on top of
133
+ // the previous route set kept routes that had since been removed or
134
+ // hidden, so `removeRoute()` had no effect once a document had been
135
+ // generated.
136
+ this.registry.clearRoutes();
127
137
  for (const route of this.scanner.scan()) {
128
138
  this.registry.setRoute(route);
129
139
  }
@@ -234,17 +244,31 @@ export class OpenAPIManager {
234
244
  ? `${info.title} · API reference`
235
245
  : undefined,
236
246
  ...options,
247
+ // Precedence: an explicit page logo, then the manager's `branding`
248
+ // (a custom logo is used as-is; `false` renders none), then the
249
+ // page's own default wordmark.
237
250
  logo: options.logo !== undefined
238
251
  ? options.logo
239
252
  : this.logo === undefined
240
253
  ? false
241
- : undefined,
254
+ : this.customLogo
255
+ ? this.logo
256
+ : undefined,
242
257
  });
258
+ const csp = options.contentSecurityPolicy === false
259
+ ? undefined
260
+ : (options.contentSecurityPolicy ??
261
+ buildOpenAPIUIContentSecurityPolicy(options, [
262
+ ...documentServerUrls(this.getDocument()),
263
+ ...(options.connectSources ?? []),
264
+ ]));
243
265
  return Object.freeze({
244
266
  status: 200,
245
267
  headers: Object.freeze({
246
268
  "content-type": "text/html; charset=utf-8",
247
269
  "cache-control": "public, max-age=300",
270
+ "x-content-type-options": "nosniff",
271
+ ...(csp === undefined ? {} : { "content-security-policy": csp }),
248
272
  }),
249
273
  body,
250
274
  });
@@ -35,6 +35,15 @@ export declare class OpenAPIRegistryImpl implements OpenAPIRegistry {
35
35
  * `generate()` twice failed.
36
36
  */
37
37
  setRoute(route: OpenAPIRoute): void;
38
+ /** Removes a route. Returns whether one was registered. */
39
+ removeRoute(method: string, path: string): boolean;
40
+ /**
41
+ * Drops every registered route while keeping components, servers, tags
42
+ * and security. `OpenAPIManager.generate()` rebuilds the route set from
43
+ * its scanner on each call; without this a route removed from the
44
+ * scanner lived on in the registry and in every later document.
45
+ */
46
+ clearRoutes(): void;
38
47
  private static register;
39
48
  registerSchema(name: string, schema: OpenAPISchema): void;
40
49
  registerResponse(name: string, response: OpenAPIResponse): void;
@@ -77,6 +77,19 @@ export class OpenAPIRegistryImpl {
77
77
  }),
78
78
  });
79
79
  }
80
+ /** Removes a route. Returns whether one was registered. */
81
+ removeRoute(method, path) {
82
+ return this.routes.delete(OpenAPIRegistryImpl.routeKey({ method, path }));
83
+ }
84
+ /**
85
+ * Drops every registered route while keeping components, servers, tags
86
+ * and security. `OpenAPIManager.generate()` rebuilds the route set from
87
+ * its scanner on each call; without this a route removed from the
88
+ * scanner lived on in the registry and in every later document.
89
+ */
90
+ clearRoutes() {
91
+ this.routes.clear();
92
+ }
80
93
  static register(map, section, name, value) {
81
94
  if (map.has(name)) {
82
95
  throw new OpenAPIComponentConflictError(section, name);
@@ -12,16 +12,27 @@ import type { OpenAPISchema } from "../openApiTypes/openApiTypes.core.js";
12
12
  * Field names as of `@zudojs/schema@0.1.0`:
13
13
  * object `_config.shape`, `_config.requiredKeys` (a Set), `_config.unknownKeys`
14
14
  * array `_config.itemSchema`, `_config.min`, `_config.max`, `_config.length`
15
+ * (no `max` means the parser's implicit ceiling, which is emitted)
15
16
  * string `_config.min|max|length|pattern|format`
16
17
  * number `_config.min|max|int|gt|lt|multipleOf`
18
+ * coerce.string / coerce.number
19
+ * `_constraints` — the wrapped StringSchema / NumberSchema that
20
+ * carries the `_config` above
17
21
  * union `_schemas` intersection `_left` / `_right`
18
22
  * enum `_values` literal `_expected`
19
23
  * optional `_inner` nullable `_inner`
20
- * default `_inner`, `_defaultValue` refine `_inner`
21
- * transform `_base` lazy `_factory` / `_inner`
24
+ * default `_inner`, `_defaultValue` (a value or a factory function)
25
+ * refine `_inner`
26
+ * transform `_inner` (`schema.transform(...)`, TransformModifierSchema)
27
+ * or `_base` (the standalone TransformSchema class)
28
+ * lazy `_factory` / `_inner`
22
29
  * record `_keySchema`, `_valueSchema` tuple `_schemas`
23
30
  * map `_keySchema`, `_valueSchema` set `_valueSchema`
24
31
  * metadata `_metadata` (description, example, title, deprecated)
32
+ *
33
+ * Object parsing accepts a missing key when the field schema is one of
34
+ * `optional`, `default`, `any` or `unknown` (`ACCEPTS_UNDEFINED` in
35
+ * schemaObject.core.ts), so exactly those are left out of `required`.
25
36
  */
26
37
  export interface SchemaConversionResult {
27
38
  readonly schema: OpenAPISchema;
@@ -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 = {
@@ -33,8 +34,50 @@ function isSchemaLike(value) {
33
34
  value !== null &&
34
35
  typeof value._type === "string");
35
36
  }
36
- function isOptionalSchema(value) {
37
- return isSchemaLike(value) && value._type === "optional";
37
+ /**
38
+ * Field schemas for which `ObjectSchema` accepts a missing key. Mirrors
39
+ * `ACCEPTS_UNDEFINED` in `@zudojs/schema`: a field with a default is filled
40
+ * in when absent, so documenting it as `required` publishes a contract
41
+ * stricter than the code that validates against it.
42
+ */
43
+ const ACCEPTS_MISSING_KEY = new Set([
44
+ "optional",
45
+ "default",
46
+ "any",
47
+ "unknown",
48
+ ]);
49
+ function acceptsMissingKey(value) {
50
+ return isSchemaLike(value) && ACCEPTS_MISSING_KEY.has(value._type);
51
+ }
52
+ /**
53
+ * The schema a `coerce.*` wrapper delegates its constraints to.
54
+ *
55
+ * `s.coerce.number().int().min(1)` keeps `int` and `min` on the wrapped
56
+ * `NumberSchema` under `_constraints`, not on the wrapper's own `_config`;
57
+ * reading the wrapper alone yields a bare `{ type: "number" }`.
58
+ */
59
+ function coercionTarget(schema) {
60
+ const constraints = schema["_constraints"];
61
+ return isSchemaLike(constraints) ? constraints : schema;
62
+ }
63
+ /**
64
+ * Resolves a `default` schema's value, which may be a factory function.
65
+ *
66
+ * A factory (`.default(() => new Date())`) is invoked once for the document.
67
+ * Emitting the function itself produces a `default` that `JSON.stringify`
68
+ * silently drops and the YAML serializer renders as source text.
69
+ */
70
+ function resolveDefaultValue(schema, state) {
71
+ const raw = schema["_defaultValue"];
72
+ if (typeof raw !== "function")
73
+ return raw;
74
+ try {
75
+ return raw();
76
+ }
77
+ catch (error) {
78
+ state.warnings.push(`A \`default\` factory threw and its value was omitted: ${error instanceof Error ? error.message : String(error)}`);
79
+ return undefined;
80
+ }
38
81
  }
39
82
  function extractMeta(schema) {
40
83
  const meta = schema._metadata;
@@ -113,7 +156,7 @@ function convertString(schema, state) {
113
156
  ? { minLength: exact, maxLength: exact }
114
157
  : {
115
158
  ...(num(c["min"]) !== undefined ? { minLength: num(c["min"]) } : {}),
116
- ...(num(c["max"]) !== undefined ? { maxLength: num(c["max"]) } : {}),
159
+ maxLength: num(c["max"]) ?? SCHEMA_DEFAULT_MAX_STRING_LENGTH,
117
160
  }),
118
161
  ...(pattern instanceof RegExp
119
162
  ? { pattern: pattern.source }
@@ -225,14 +268,17 @@ function convertSchemaNode(schema, state) {
225
268
  const c = config(schema);
226
269
  switch (schema._type) {
227
270
  case "string":
228
- case "coerce.string":
229
271
  return convertString(schema, state);
272
+ case "coerce.string":
273
+ return convertString(coercionTarget(schema), state);
230
274
  case "number":
231
- case "coerce.number":
232
275
  return convertNumber(schema, state);
276
+ case "coerce.number":
277
+ return convertNumber(coercionTarget(schema), state);
233
278
  case "boolean":
234
279
  case "coerce.boolean":
235
280
  return { type: "boolean" };
281
+ case "bigint":
236
282
  case "coerce.bigint":
237
283
  return { type: "string", format: "int64" };
238
284
  case "null":
@@ -256,10 +302,11 @@ function convertSchemaNode(schema, state) {
256
302
  const required = [];
257
303
  for (const [key, value] of Object.entries(shape)) {
258
304
  defineProperty(properties, key, convertNode(value, state));
259
- // A field is required unless it is wrapped in `optional`. An explicit
305
+ // A field is required unless the object parser accepts its absence
306
+ // (`optional`, `default`, `any`, `unknown`). An explicit
260
307
  // `requiredKeys` set (from `.required()`) forces it back on.
261
308
  const forced = requiredKeys instanceof Set && requiredKeys.has(key);
262
- if (forced || !isOptionalSchema(value))
309
+ if (forced || !acceptsMissingKey(value))
263
310
  required.push(key);
264
311
  }
265
312
  const unknownKeys = c["unknownKeys"];
@@ -291,9 +338,7 @@ function convertSchemaNode(schema, state) {
291
338
  ...(num(c["min"]) !== undefined
292
339
  ? { minItems: num(c["min"]) }
293
340
  : {}),
294
- ...(num(c["max"]) !== undefined
295
- ? { maxItems: num(c["max"]) }
296
- : {}),
341
+ maxItems: num(c["max"]) ?? SCHEMA_DEFAULT_MAX_ARRAY_LENGTH,
297
342
  }),
298
343
  };
299
344
  }
@@ -403,7 +448,7 @@ function convertSchemaNode(schema, state) {
403
448
  return applyNullable(convertInner(schema["_inner"], state, "nullable"), state.version);
404
449
  case "default": {
405
450
  const inner = convertInner(schema["_inner"], state, "default");
406
- const defaultValue = schema["_defaultValue"];
451
+ const defaultValue = resolveDefaultValue(schema, state);
407
452
  return defaultValue === undefined
408
453
  ? inner
409
454
  : { ...inner, default: defaultValue };
@@ -413,7 +458,10 @@ function convertSchemaNode(schema, state) {
413
458
  // inner shape is still the right description of the data.
414
459
  return convertInner(schema["_inner"], state, "refine");
415
460
  case "transform":
416
- return convertInner(schema["_base"], state, "transform");
461
+ // `schema.transform(fn)` / `s.transform(schema, fn)` build a
462
+ // TransformModifierSchema, whose source is `_inner`; the standalone
463
+ // TransformSchema class names it `_base`.
464
+ return convertInner(schema["_inner"] ?? schema["_base"], state, "transform");
417
465
  case "lazy": {
418
466
  const resolved = schema["_inner"] ?? resolveLazy(schema, state);
419
467
  if (resolved === undefined)
@@ -31,8 +31,30 @@ const YAML_RESERVED = new Set([
31
31
  ]);
32
32
  /** A bare (unquoted) YAML scalar may not start with these. */
33
33
  const YAML_UNSAFE_START = /^[-?:,[\]{}#&*!|>'"%@`\s]/;
34
- const YAML_UNSAFE_ANYWHERE = /[:#\n\r\t]|: |\s#/;
35
- const YAML_NUMERIC = /^[-+]?(\d+\.?\d*|\.\d+)([eE][-+]?\d+)?$/;
34
+ // Control characters have no plain-scalar form; `JSON.stringify` escapes them.
35
+ const YAML_UNSAFE_ANYWHERE = /[:#\u0000-\u001f\u007f]|: |\s#/;
36
+ const YAML_NUMERIC = /^[-+]?(?:\d+(?:\.\d*)?|\.\d+)(?:[eE][-+]?\d+)?$/;
37
+ /**
38
+ * Other strings a YAML parser resolves to a non-string: the infinity and
39
+ * not-a-number forms, hexadecimal, octal and binary integers (1.1 and 1.2
40
+ * core schema), digit groups with `_`, sexagesimal numbers, timestamps
41
+ * and dates (1.1), the `=` value key and the `<<` merge key. A `version:
42
+ * 2024-01-01` or an enum value of `0x1F` came back from the parser as a
43
+ * date or the number 31.
44
+ */
45
+ const YAML_SPECIAL_SCALARS = [
46
+ /^[-+]?\.(?:inf|nan)$/i,
47
+ /^[-+]?0x[0-9a-f_]+$/i,
48
+ /^[-+]?0o?[0-7_]+$/i,
49
+ /^[-+]?0b[01_]+$/i,
50
+ /^[-+]?[0-9][0-9_]*(?:\.[0-9_]*)?(?:[eE][-+]?[0-9]+)?$/,
51
+ /^[-+]?[0-9][0-9_]*(?::[0-5]?[0-9])+(?:\.[0-9_]*)?$/,
52
+ /^\d{4}-\d{1,2}-\d{1,2}(?:[Tt ]|$)/,
53
+ /^(?:=|<<)$/,
54
+ ];
55
+ function isSpecialScalar(value) {
56
+ return YAML_SPECIAL_SCALARS.some((pattern) => pattern.test(value));
57
+ }
36
58
  function quoteYamlString(value) {
37
59
  // Double quotes with JSON escaping is always valid YAML and needs no
38
60
  // decision about block scalars or line folding.
@@ -43,6 +65,7 @@ function yamlScalar(value) {
43
65
  YAML_UNSAFE_START.test(value) ||
44
66
  YAML_UNSAFE_ANYWHERE.test(value) ||
45
67
  YAML_NUMERIC.test(value) ||
68
+ isSpecialScalar(value) ||
46
69
  value !== value.trim()) {
47
70
  return quoteYamlString(value);
48
71
  }
@@ -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,18 +31,25 @@ 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
- /** Rejects URLs that could execute script when placed in `src`/`href`. */
34
+ /**
35
+ * Rejects URLs that could execute script when placed in `src`/`href`.
36
+ *
37
+ * The scheme is read after stripping ASCII control characters, because the
38
+ * URL parser browsers apply does the same: `java\nscript:alert(1)` is a
39
+ * `javascript:` URL to every browser and nothing to a regex that only
40
+ * looks at the string as written. `data:` is accepted for images only —
41
+ * the assets base is interpolated into `<script src>`, where a
42
+ * `data:text/javascript,` base would run inline.
43
+ */
42
44
  function safeUrl(value) {
43
45
  const trimmed = value.trim();
44
- if (/^\s*(javascript|vbscript):/i.test(trimmed)) {
45
- throw new TypeError(`Refusing to render a "${trimmed.split(":")[0]}:" URL`);
46
+ const normalized = trimmed.replace(/[\u0000-\u0020\u007f]/g, "");
47
+ const scheme = /^([a-z][a-z0-9+.-]*):/i.exec(normalized)?.[1]?.toLowerCase();
48
+ if (scheme === "javascript" || scheme === "vbscript") {
49
+ throw new TypeError(`Refusing to render a "${scheme}:" URL`);
50
+ }
51
+ if (scheme === "data" && !/^data:image\//i.test(normalized)) {
52
+ throw new TypeError('Refusing to render a non-image "data:" URL; only data:image/* is allowed.');
46
53
  }
47
54
  // Attributes are always double-quoted here, so a single quote (common in
48
55
  // data URIs) can stay as-is.
@@ -69,26 +76,6 @@ function safeCss(css) {
69
76
  }
70
77
  return css;
71
78
  }
72
- const THEME_CSS = `
73
- :root{--zd-ink:#1A1A2E;--zd-red:#C0392B;--zd-bg:#FAFAF9;--zd-border:#E5E7EB}
74
- *{border-radius:0!important}
75
- html,body{margin:0;background:var(--zd-bg);font-family:Inter,system-ui,-apple-system,"Segoe UI",sans-serif}
76
- .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)}
77
- .zudo-bar a{display:inline-flex;align-items:center;color:inherit;text-decoration:none}
78
- .zudo-bar img{height:22px;width:auto;display:block}
79
- .zudo-bar .zudo-sep{width:1px;height:22px;background:rgba(250,250,249,.25)}
80
- .zudo-bar .zudo-title{font-weight:800;font-size:14px;letter-spacing:.02em;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
81
- .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}
82
- .zudo-bar .zudo-spec:hover{border-color:var(--zd-bg);color:var(--zd-bg)}
83
- .zudo-foot{padding:16px 20px;border-top:1px solid var(--zd-border);color:#6B7280;font-size:12px;display:flex;gap:8px;align-items:center}
84
- .zudo-foot img{height:14px;width:auto}
85
- .swagger-ui .topbar{display:none}
86
- .swagger-ui .info .title{font-weight:900;letter-spacing:-.01em;color:var(--zd-ink)}
87
- .swagger-ui .opblock{border-width:2px;box-shadow:none}
88
- .swagger-ui .btn{border-width:2px;font-weight:700}
89
- .swagger-ui .btn.execute{background:var(--zd-red);border-color:var(--zd-red)}
90
- .swagger-ui .scheme-container{box-shadow:none;border-bottom:1px solid var(--zd-border);background:#fff}
91
- `;
92
79
  function header(options, title) {
93
80
  const logo = options.logo === undefined ? zudoLogo({ url: ZUDO_WORDMARK_DARK_DATA_URI }) : options.logo;
94
81
  const logoHtml = logo === false
@@ -117,27 +104,29 @@ export function renderOpenAPIUI(options) {
117
104
  }
118
105
  const title = options.title ?? DEFAULT_TITLE;
119
106
  const renderer = options.renderer ?? "swagger";
107
+ const integrity = resolveIntegrity(options, renderer);
120
108
  if (renderer === "redoc") {
121
- const base = (options.assetsBaseUrl ?? REDOC_ASSETS).replace(/\/+$/, "");
109
+ const base = (options.assetsBaseUrl ?? DEFAULT_REDOC_ASSETS).replace(/(?<!\/)\/+$/, "");
122
110
  return (head(options, title, "") +
123
111
  `<body>${header(options, title)}` +
124
112
  `<redoc spec-url="${safeUrl(options.specUrl)}" hide-hostname></redoc>` +
125
- `<script src="${safeUrl(base)}/redoc.standalone.js"></script>` +
113
+ `<script src="${safeUrl(base)}/redoc.standalone.js"${integrityAttrs(integrity.script)}></script>` +
126
114
  `${footer()}</body></html>`);
127
115
  }
128
- const base = (options.assetsBaseUrl ?? SWAGGER_ASSETS).replace(/\/+$/, "");
129
- const config = {
130
- url: options.specUrl,
131
- dom_id: "#zudo-openapi",
132
- deepLinking: true,
133
- displayRequestDuration: true,
134
- tryItOutEnabled: true,
135
- ...options.swaggerOptions,
136
- };
137
- 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)}>`) +
138
118
  `<body>${header(options, title)}<div id="zudo-openapi"></div>` +
139
- `<script src="${safeUrl(base)}/swagger-ui-bundle.js" crossorigin></script>` +
140
- `<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>` +
141
121
  `${footer()}</body></html>`);
142
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
+ }
143
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
@@ -1,5 +1,5 @@
1
1
  import { OpenAPIValidationError } from "../openApiErrors/openApiError.types.js";
2
- import { MAX_OPERATION_ID_LENGTH, RESPONSE_KEY_PATTERN, SUPPORTED_OPENAPI_VERSIONS, } from "../openApiConstants/openApiConstants.core.js";
2
+ import { MAX_OPERATION_ID_LENGTH, PATH_TEMPLATE_PARAMETER, RESPONSE_KEY_PATTERN, SUPPORTED_OPENAPI_VERSIONS, } from "../openApiConstants/openApiConstants.core.js";
3
3
  import { extractPathParameters } from "../openApiRouting/routeConverter.core.js";
4
4
  import { unescapeJsonPointerSegment } from "../openApiSchema/references.core.js";
5
5
  const OPERATIONS = [
@@ -77,9 +77,22 @@ export class OpenAPIValidatorImpl {
77
77
  }
78
78
  }
79
79
  validatePaths(document, collector) {
80
+ /** Paths seen so far, keyed by their template with parameter names erased. */
81
+ const shapes = new Map();
80
82
  for (const [path, pathItem] of Object.entries(document.paths ?? {})) {
81
83
  if (!pathItem)
82
84
  continue;
85
+ // "/users/{id}" and "/users/{userId}" have the same hierarchy and
86
+ // differ only in template names; the specification forbids both
87
+ // existing, since a router cannot tell them apart.
88
+ const shape = path.replace(PATH_TEMPLATE_PARAMETER, "{}");
89
+ const twin = shapes.get(shape);
90
+ if (twin !== undefined && twin !== path) {
91
+ error(collector, ["paths", path], `Path "${path}" is identical to "${twin}" apart from its template parameter names; such paths must not both exist.`);
92
+ }
93
+ else {
94
+ shapes.set(shape, path);
95
+ }
83
96
  if (!path.startsWith("/")) {
84
97
  error(collector, ["paths", path], `Path "${path}" must start with "/".`);
85
98
  }
@@ -121,28 +134,33 @@ export class OpenAPIValidatorImpl {
121
134
  operation.operationId.length > MAX_OPERATION_ID_LENGTH) {
122
135
  error(collector, [...base, "operationId"], `operationId "${operation.operationId}" exceeds the maximum length of ${MAX_OPERATION_ID_LENGTH}.`);
123
136
  }
124
- const parameters = [
125
- ...(pathItem.parameters ?? []),
126
- ...(operation.parameters ?? []),
127
- ];
128
- const seen = new Set();
129
- for (const parameter of parameters) {
130
- if (!parameter.name) {
131
- error(collector, [...base, "parameters"], "Every parameter must have a name.");
132
- continue;
133
- }
134
- const key = `${parameter.in}:${parameter.name}`;
135
- if (seen.has(key)) {
136
- error(collector, [...base, "parameters"], `Duplicate parameter "${parameter.name}" in "${parameter.in}".`);
137
- }
138
- seen.add(key);
139
- if (parameter.in === "path" && parameter.required !== true) {
140
- error(collector, [...base, "parameters"], `Path parameter "${parameter.name}" must be required.`);
137
+ // Uniqueness is per list: the specification forbids duplicates within
138
+ // the path item's list and within the operation's list, while an
139
+ // operation-level parameter *overrides* a path-level one with the same
140
+ // name and location. Treating that override as a duplicate rejected
141
+ // legal documents.
142
+ const parameters = new Map();
143
+ for (const list of [pathItem.parameters, operation.parameters]) {
144
+ const seen = new Set();
145
+ for (const parameter of list ?? []) {
146
+ if (!parameter.name) {
147
+ error(collector, [...base, "parameters"], "Every parameter must have a name.");
148
+ continue;
149
+ }
150
+ const key = `${parameter.in}:${parameter.name}`;
151
+ if (seen.has(key)) {
152
+ error(collector, [...base, "parameters"], `Duplicate parameter "${parameter.name}" in "${parameter.in}".`);
153
+ }
154
+ seen.add(key);
155
+ parameters.set(key, parameter);
156
+ if (parameter.in === "path" && parameter.required !== true) {
157
+ error(collector, [...base, "parameters"], `Path parameter "${parameter.name}" must be required.`);
158
+ }
141
159
  }
142
160
  }
143
161
  // The classic OpenAPI mistake: a templated path with no matching
144
162
  // parameter, or a path parameter that no template slot refers to.
145
- const declaredPathParameters = new Set(parameters
163
+ const declaredPathParameters = new Set([...parameters.values()]
146
164
  .filter((parameter) => parameter.in === "path")
147
165
  .map((parameter) => parameter.name));
148
166
  for (const name of templateParameters) {
@@ -309,7 +327,10 @@ function resolvePointer(document, ref) {
309
327
  current = current[index];
310
328
  continue;
311
329
  }
312
- if (!(segment in current))
330
+ // Own properties only: `in` walks the prototype chain, so a reference
331
+ // to `#/components/schemas/constructor` resolved to
332
+ // `Object.prototype.constructor` and validated as present.
333
+ if (!Object.prototype.hasOwnProperty.call(current, segment))
313
334
  return false;
314
335
  current = current[segment];
315
336
  }
package/package.json CHANGED
@@ -1,8 +1,12 @@
1
1
  {
2
2
  "name": "@zudojs/openapi",
3
- "version": "1.1.0",
3
+ "version": "1.3.0",
4
4
  "description": "OpenAPI 3.0 and 3.1 specification generation, validation, and serialization for Zudojs applications.",
5
5
  "license": "MIT",
6
+ "author": {
7
+ "name": "Oluwayemi Oyinlola",
8
+ "url": "https://github.com/oyinlola-tech"
9
+ },
6
10
  "type": "module",
7
11
  "main": "./dist/index.js",
8
12
  "module": "./dist/index.js",
@@ -22,7 +26,8 @@
22
26
  "!dist/.tsbuildinfo"
23
27
  ],
24
28
  "dependencies": {
25
- "@zudojs/errors": "1.0.0"
29
+ "@zudojs/constants": "1.1.0",
30
+ "@zudojs/errors": "1.1.0"
26
31
  },
27
32
  "devDependencies": {
28
33
  "@types/node": "^26.4.1",