@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.
- package/README.md +62 -14
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/openApiErrors/openApiError.base.d.ts +8 -15
- package/dist/openApiErrors/openApiError.base.js +8 -16
- package/dist/openApiHttp/openApiHttpAdapter.core.d.ts +2 -0
- package/dist/openApiHttp/openApiHttpAdapter.core.js +25 -1
- package/dist/openApiRegistry/openApiRegistry.core.d.ts +9 -0
- package/dist/openApiRegistry/openApiRegistry.core.js +13 -0
- package/dist/openApiSchema/schemaConverter.core.d.ts +13 -2
- package/dist/openApiSchema/schemaConverter.core.js +60 -12
- package/dist/openApiSerialization/openApiSerializer.core.js +25 -2
- package/dist/openApiUi/index.d.ts +2 -0
- package/dist/openApiUi/index.js +2 -0
- package/dist/openApiUi/openApiUi.assets.d.ts +41 -0
- package/dist/openApiUi/openApiUi.assets.js +48 -0
- package/dist/openApiUi/openApiUi.brand.d.ts +1 -1
- package/dist/openApiUi/openApiUi.brand.js +1 -1
- package/dist/openApiUi/openApiUi.core.d.ts +24 -5
- package/dist/openApiUi/openApiUi.core.js +35 -46
- package/dist/openApiUi/openApiUi.csp.d.ts +25 -0
- package/dist/openApiUi/openApiUi.csp.js +92 -0
- package/dist/openApiUi/openApiUiPage/index.d.ts +9 -0
- package/dist/openApiUi/openApiUiPage/index.js +9 -0
- package/dist/openApiUi/openApiUiPage/openApiUiPage.script.d.ts +14 -0
- package/dist/openApiUi/openApiUiPage/openApiUiPage.script.js +24 -0
- package/dist/openApiUi/openApiUiPage/openApiUiPage.theme.d.ts +6 -0
- package/dist/openApiUi/openApiUiPage/openApiUiPage.theme.js +25 -0
- package/dist/openApiValidation/openApiValidator.core.js +41 -20
- 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 |
|
|
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
|
|
105
|
-
|
|
106
|
-
|
|
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://
|
|
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
|
|
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
|
|
184
|
-
converted, along with string and number constraints (`min`, `max`,
|
|
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
|
|
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 {
|
|
12
|
-
/**
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
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 {
|
|
12
|
-
/**
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
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
|
-
:
|
|
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`
|
|
21
|
-
*
|
|
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
|
-
|
|
37
|
-
|
|
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
|
-
|
|
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
|
|
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 || !
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
35
|
-
const
|
|
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
|
package/dist/openApiUi/index.js
CHANGED
|
@@ -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://
|
|
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://
|
|
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
|
-
*
|
|
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
|
|
33
|
-
* `
|
|
34
|
-
*
|
|
35
|
-
*
|
|
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, """)
|
|
32
32
|
.replace(/'/g, "'");
|
|
33
33
|
}
|
|
34
|
-
/**
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
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
|
-
|
|
45
|
-
|
|
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 ??
|
|
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 ??
|
|
129
|
-
|
|
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"
|
|
140
|
-
`<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
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
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
|
-
|
|
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.
|
|
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/
|
|
29
|
+
"@zudojs/constants": "1.1.0",
|
|
30
|
+
"@zudojs/errors": "1.1.0"
|
|
26
31
|
},
|
|
27
32
|
"devDependencies": {
|
|
28
33
|
"@types/node": "^26.4.1",
|