rapidoc 11.0.1 → 12.1.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 +171 -54
- package/dist/rapidoc-flow-min.js +580 -0
- package/dist/rapidoc-flow-min.js.LICENSE.txt +171 -0
- package/dist/rapidoc-min.js +645 -202
- package/dist/rapidoc-min.js.LICENSE.txt +381 -0
- package/package.json +57 -42
- package/src/README.md +150 -0
- package/src/components/README.md +91 -0
- package/src/components/api-request.ts +912 -0
- package/src/components/api-response.ts +514 -0
- package/src/components/json-tree.ts +171 -0
- package/src/components/schema-base-element.ts +329 -0
- package/src/components/schema-table.ts +475 -0
- package/src/components/schema-tree.ts +393 -0
- package/src/components/tag-input.ts +180 -0
- package/src/flow.js +3 -0
- package/src/index.js +6 -0
- package/src/oauth-receiver.ts +29 -0
- package/src/rapi-flow.ts +1178 -0
- package/src/rapidoc-mini.ts +603 -0
- package/src/rapidoc.ts +1854 -0
- package/src/styles/README.md +96 -0
- package/src/styles/auth-styles.js +318 -0
- package/src/styles/border-styles.js +24 -0
- package/src/styles/css-bundle.js +129 -0
- package/src/styles/custom-styles.js +8 -0
- package/src/styles/dialog-box-styles.js +102 -0
- package/src/styles/endpoint-styles.js +564 -0
- package/src/styles/flex-styles.js +16 -0
- package/src/styles/flow-styles.js +248 -0
- package/src/styles/font-styles.js +432 -0
- package/src/styles/info-styles.js +36 -0
- package/src/styles/input-styles.js +555 -0
- package/src/styles/layout-styles.js +396 -0
- package/src/styles/microlighter-styles.js +99 -0
- package/src/styles/nav-styles.js +633 -0
- package/src/styles/param-list-styles.js +261 -0
- package/src/styles/schema-styles.js +296 -0
- package/src/styles/tab-styles.js +106 -0
- package/src/styles/theme-tokens.js +582 -0
- package/src/templates/README.md +89 -0
- package/src/templates/advanced-search-dialog-template.js +94 -0
- package/src/templates/api-response-template.js +223 -0
- package/src/templates/callback-template.js +86 -0
- package/src/templates/code-samples-template.js +52 -0
- package/src/templates/components-template.js +86 -0
- package/src/templates/downloads-template.js +70 -0
- package/src/templates/endpoint-template.js +377 -0
- package/src/templates/expanded-endpoint-template.js +270 -0
- package/src/templates/focused-endpoint-template.js +146 -0
- package/src/templates/header-template.js +92 -0
- package/src/templates/logo-template.js +21 -0
- package/src/templates/main-body-template.js +102 -0
- package/src/templates/navbar-template.js +498 -0
- package/src/templates/overview-template.js +104 -0
- package/src/templates/request-body-template.js +673 -0
- package/src/templates/request-params-template.js +485 -0
- package/src/templates/security-scheme-template.js +1272 -0
- package/src/templates/server-template.js +153 -0
- package/src/utils/README.md +146 -0
- package/src/utils/arazzo/README.md +119 -0
- package/src/utils/arazzo/components.js +15 -0
- package/src/utils/arazzo/graph.js +269 -0
- package/src/utils/arazzo/index.js +52 -0
- package/src/utils/arazzo/operations.js +106 -0
- package/src/utils/arazzo/parse.js +196 -0
- package/src/utils/arazzo/refs.js +40 -0
- package/src/utils/arazzo/svg.js +707 -0
- package/src/utils/auto-color-scheme.js +99 -0
- package/src/utils/bind-navbar.js +175 -0
- package/src/utils/color-utils.js +59 -0
- package/src/utils/common-utils.js +191 -0
- package/src/utils/curl-utils.js +126 -0
- package/src/utils/highlighter.js +419 -0
- package/src/utils/markdown-utils.js +110 -0
- package/src/utils/mcp/README.md +155 -0
- package/src/utils/mcp/common.js +166 -0
- package/src/utils/mcp/manifest.js +382 -0
- package/src/utils/mcp/project/server.ts +53 -0
- package/src/utils/mcp/project.js +88 -0
- package/src/utils/mcp/runtime.js +289 -0
- package/src/utils/mcp/schema.js +98 -0
- package/src/utils/mcp/workflow-runtime.js +374 -0
- package/src/utils/mcp/workflows.js +240 -0
- package/src/utils/mcp/zip.js +102 -0
- package/src/utils/mcp-generator.js +47 -0
- package/src/utils/mock-interceptor.js +557 -0
- package/src/utils/oauth-utils.js +87 -0
- package/src/utils/request-utils.js +264 -0
- package/src/utils/response-utils.js +203 -0
- package/src/utils/sanitize.js +57 -0
- package/src/utils/schema-utils.js +1954 -0
- package/src/utils/slugger.js +187 -0
- package/src/utils/spec-parser.js +941 -0
- package/src/utils/url-utils.js +246 -0
- package/src/utils/xml-utils.js +61 -0
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Helpers for the OAuth popup flow (Authorize button): unguessable values, PKCE, reading the reply the
|
|
3
|
+
* provider sends to the return page (`<oauth-receiver>`), and checking that reply before it is trusted.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/** BroadcastChannel the return page also replies on: still works when the provider's page cuts `window.opener`. */
|
|
7
|
+
export const OAUTH_CHANNEL = "rapidoc-oauth";
|
|
8
|
+
|
|
9
|
+
function base64Url(bytes) {
|
|
10
|
+
let binary = "";
|
|
11
|
+
for (const byte of bytes) binary += String.fromCharCode(byte);
|
|
12
|
+
return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* A random URL-safe string from the platform's cryptographic random generator.
|
|
17
|
+
* 32 bytes give a 43-character value, the shortest a PKCE code_verifier may be.
|
|
18
|
+
*
|
|
19
|
+
* @param {number} [bytes]
|
|
20
|
+
*/
|
|
21
|
+
export function randomUrlSafe(bytes = 32) {
|
|
22
|
+
return base64Url(crypto.getRandomValues(new Uint8Array(bytes)));
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* PKCE S256 code_challenge for a code_verifier (RFC 7636): base64url(SHA-256(verifier)).
|
|
27
|
+
*
|
|
28
|
+
* @param {string} verifier
|
|
29
|
+
* @returns {Promise<string>}
|
|
30
|
+
*/
|
|
31
|
+
export async function pkceChallenge(verifier) {
|
|
32
|
+
const digest = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(verifier));
|
|
33
|
+
return base64Url(new Uint8Array(digest));
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* The provider's reply as the return page sees it: authorization code flow in the query, implicit flow in the
|
|
38
|
+
* fragment. `state` is read in both, so the opener can check it.
|
|
39
|
+
*
|
|
40
|
+
* @param {{search: string, hash: string}} location
|
|
41
|
+
*/
|
|
42
|
+
export function readOAuthReply(location) {
|
|
43
|
+
if (location.search && location.search.length > 1) {
|
|
44
|
+
const params = new URLSearchParams(location.search);
|
|
45
|
+
return {
|
|
46
|
+
responseType: "code",
|
|
47
|
+
code: params.get("code"),
|
|
48
|
+
state: params.get("state"),
|
|
49
|
+
error: params.get("error"),
|
|
50
|
+
errorDescription: params.get("error_description"),
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
if (location.hash && location.hash.length > 1) {
|
|
54
|
+
const params = new URLSearchParams(location.hash.slice(1));
|
|
55
|
+
return {
|
|
56
|
+
responseType: "token",
|
|
57
|
+
accessToken: params.get("access_token"),
|
|
58
|
+
tokenType: params.get("token_type"),
|
|
59
|
+
state: params.get("state"),
|
|
60
|
+
error: params.get("error"),
|
|
61
|
+
errorDescription: params.get("error_description"),
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Is this the reply to the sign-in we started? It must look like a reply, carry the `state` we sent, and, when it
|
|
69
|
+
* came as a window message, come from this site and from the popup we opened. Anything else is ignored, so a
|
|
70
|
+
* message from another frame or window can neither inject a code or token nor end the wait.
|
|
71
|
+
*
|
|
72
|
+
* @param {object} reply - message data
|
|
73
|
+
* @param {object} expected
|
|
74
|
+
* @param {string} expected.state
|
|
75
|
+
* @param {string} [expected.origin] - for window messages: this page's origin
|
|
76
|
+
* @param {string} [origin] - for window messages: MessageEvent.origin
|
|
77
|
+
* @param {*} [source] - for window messages: MessageEvent.source
|
|
78
|
+
* @param {*} [expectedSource] - the popup window
|
|
79
|
+
*/
|
|
80
|
+
export function isExpectedOAuthReply(reply, expected, origin, source, expectedSource) {
|
|
81
|
+
if (!reply || typeof reply !== "object") return false;
|
|
82
|
+
if (reply.responseType !== "code" && reply.responseType !== "token") return false;
|
|
83
|
+
if (!expected.state || reply.state !== expected.state) return false;
|
|
84
|
+
if (expected.origin !== undefined && origin !== expected.origin) return false;
|
|
85
|
+
if (expectedSource !== undefined && source !== expectedSource) return false;
|
|
86
|
+
return true;
|
|
87
|
+
}
|
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Serializes a header parameter value according to OpenAPI 3 rules.
|
|
3
|
+
* Objects can be serialized exploded (key=value,key2=value2) or unexploded (key,value,key2,value2).
|
|
4
|
+
*
|
|
5
|
+
* @param {any} rawValue - Raw header parameter value
|
|
6
|
+
* @param {object} [options]
|
|
7
|
+
* @param {boolean} [options.explode=false] - Whether to explode object properties with '='
|
|
8
|
+
* @param {boolean} [options.isObject=false] - Whether value is an object
|
|
9
|
+
* @returns {string} Serialized header string
|
|
10
|
+
*/
|
|
11
|
+
export function serializeHeaderValue(rawValue, options = {}) {
|
|
12
|
+
const { explode = false, isObject = false } = options;
|
|
13
|
+
|
|
14
|
+
if (rawValue === undefined || rawValue === null || rawValue === "") {
|
|
15
|
+
return "";
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
if (isObject || (typeof rawValue === "object" && !Array.isArray(rawValue))) {
|
|
19
|
+
let obj = rawValue;
|
|
20
|
+
if (typeof rawValue === "string") {
|
|
21
|
+
try {
|
|
22
|
+
obj = JSON.parse(rawValue.replace(/\n/g, "").trim());
|
|
23
|
+
} catch {
|
|
24
|
+
return "";
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if (!obj || typeof obj !== "object") {
|
|
29
|
+
return "";
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const separator = explode ? "=" : ",";
|
|
33
|
+
return Object.keys(obj)
|
|
34
|
+
.map((key) => {
|
|
35
|
+
const val = obj[key];
|
|
36
|
+
const valStr = typeof val === "object" ? JSON.stringify(val) : String(val);
|
|
37
|
+
return `${key}${separator}${valStr}`;
|
|
38
|
+
})
|
|
39
|
+
.join(",");
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (Array.isArray(rawValue)) {
|
|
43
|
+
const filtered = rawValue.filter((v) => v !== "" && v !== undefined && v !== null);
|
|
44
|
+
return filtered.join(",");
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return String(rawValue);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Constructs a Headers instance for an API request from structured inputs.
|
|
52
|
+
*
|
|
53
|
+
* @param {object} options
|
|
54
|
+
* @param {string} [options.accept] - Accept header
|
|
55
|
+
* @param {Array<object>} [options.apiKeys] - Applied API keys
|
|
56
|
+
* @param {Array<{ name: string, value: any, explode?: boolean, isObject?: boolean }>} [options.headerParams]
|
|
57
|
+
* @param {string} [options.contentType] - Content-Type to apply (omitted for multipart/form-data)
|
|
58
|
+
* @returns {Headers}
|
|
59
|
+
*/
|
|
60
|
+
export function constructFetchHeaders({
|
|
61
|
+
accept = "",
|
|
62
|
+
apiKeys = [],
|
|
63
|
+
headerParams = [],
|
|
64
|
+
contentType = "",
|
|
65
|
+
} = {}) {
|
|
66
|
+
const reqHeaders = new Headers();
|
|
67
|
+
|
|
68
|
+
if (accept) {
|
|
69
|
+
reqHeaders.append("Accept", accept);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (Array.isArray(apiKeys)) {
|
|
73
|
+
apiKeys
|
|
74
|
+
.filter((k) => k && k.in === "header" && k.finalKeyValue)
|
|
75
|
+
.forEach((k) => {
|
|
76
|
+
reqHeaders.append(k.name, k.finalKeyValue);
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (Array.isArray(headerParams)) {
|
|
81
|
+
headerParams.forEach((param) => {
|
|
82
|
+
if (
|
|
83
|
+
param &&
|
|
84
|
+
param.name &&
|
|
85
|
+
param.value !== undefined &&
|
|
86
|
+
param.value !== null &&
|
|
87
|
+
param.value !== ""
|
|
88
|
+
) {
|
|
89
|
+
const headerValue = serializeHeaderValue(param.value, {
|
|
90
|
+
explode: param.explode === true || param.explode === "true",
|
|
91
|
+
isObject: param.isObject === true,
|
|
92
|
+
});
|
|
93
|
+
if (headerValue) {
|
|
94
|
+
reqHeaders.append(param.name, headerValue);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (contentType && !contentType.includes("form-data")) {
|
|
101
|
+
reqHeaders.append("Content-Type", contentType);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return reqHeaders;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Builds a URLSearchParams body from field entries or a dynamic JSON template.
|
|
109
|
+
*
|
|
110
|
+
* @param {Array<{ name: string, value: any, isArray?: boolean }>} fields
|
|
111
|
+
* @param {string} [dynamicJson] - Raw JSON string for dynamic forms
|
|
112
|
+
* @returns {URLSearchParams}
|
|
113
|
+
*/
|
|
114
|
+
export function buildUrlEncodedBody(fields = [], dynamicJson = "") {
|
|
115
|
+
const params = new URLSearchParams();
|
|
116
|
+
|
|
117
|
+
if (dynamicJson) {
|
|
118
|
+
try {
|
|
119
|
+
const obj = JSON.parse(dynamicJson);
|
|
120
|
+
if (obj && typeof obj === "object") {
|
|
121
|
+
for (const prop of Object.keys(obj)) {
|
|
122
|
+
params.append(
|
|
123
|
+
prop,
|
|
124
|
+
typeof obj[prop] === "object" ? JSON.stringify(obj[prop]) : String(obj[prop]),
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
return params;
|
|
128
|
+
}
|
|
129
|
+
} catch {
|
|
130
|
+
// Fall through to fields
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
if (Array.isArray(fields)) {
|
|
135
|
+
fields.forEach((field) => {
|
|
136
|
+
if (
|
|
137
|
+
!field ||
|
|
138
|
+
!field.name ||
|
|
139
|
+
field.value === undefined ||
|
|
140
|
+
field.value === null ||
|
|
141
|
+
field.value === ""
|
|
142
|
+
) {
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
if (field.isArray) {
|
|
146
|
+
const arr = Array.isArray(field.value) ? field.value : [field.value];
|
|
147
|
+
const filtered = arr.filter((v) => v !== "" && v !== undefined && v !== null);
|
|
148
|
+
if (filtered.length > 0) {
|
|
149
|
+
params.append(field.name, filtered.join(","));
|
|
150
|
+
}
|
|
151
|
+
} else {
|
|
152
|
+
params.append(field.name, String(field.value));
|
|
153
|
+
}
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
return params;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const isComplexSchema = (schema) =>
|
|
161
|
+
!!schema &&
|
|
162
|
+
(schema.type === "object" ||
|
|
163
|
+
(Array.isArray(schema.type) && schema.type.includes("object")) ||
|
|
164
|
+
(!schema.type && !!schema.properties));
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Content-Type of a multipart/form-data part, following OpenAPI's "Special Considerations for multipart Content":
|
|
168
|
+
* the encoding object's contentType when the spec gives one (the first of a comma-separated list), application/json
|
|
169
|
+
* for an object or an array of objects, and "" otherwise (text/plain, or a file's own type).
|
|
170
|
+
*
|
|
171
|
+
* @param {object} schema - Schema of the form field
|
|
172
|
+
* @param {object} [encoding] - The media type's encoding entry for that field
|
|
173
|
+
* @returns {string}
|
|
174
|
+
*/
|
|
175
|
+
export function multipartPartContentType(schema, encoding) {
|
|
176
|
+
const declared =
|
|
177
|
+
typeof encoding?.contentType === "string" ? encoding.contentType.split(",")[0].trim() : "";
|
|
178
|
+
if (declared) {
|
|
179
|
+
return declared;
|
|
180
|
+
}
|
|
181
|
+
if (isComplexSchema(schema) || (schema?.type === "array" && isComplexSchema(schema.items))) {
|
|
182
|
+
return "application/json";
|
|
183
|
+
}
|
|
184
|
+
return "";
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// Quotes and line breaks in part names and file names are percent-encoded, as browsers do for FormData
|
|
188
|
+
const escapePartName = (name) =>
|
|
189
|
+
String(name).replace(/"/g, "%22").replace(/\r/g, "%0D").replace(/\n/g, "%0A");
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Serializes FormData into a multipart/form-data body in which parts carry a Content-Type.
|
|
193
|
+
* A browser only sends a Content-Type for a part that is a file, and a Blob part always gets filename="blob",
|
|
194
|
+
* which servers read as an upload; so parts with a type in partContentTypes are written by hand.
|
|
195
|
+
*
|
|
196
|
+
* @param {FormData} formData
|
|
197
|
+
* @param {Object<string, string>} partContentTypes - Content-Type per field name, for fields that are not files
|
|
198
|
+
* @param {string} [boundary]
|
|
199
|
+
* @returns {{ body: Blob, contentType: string }}
|
|
200
|
+
*/
|
|
201
|
+
export function serializeMultipartBody(
|
|
202
|
+
formData,
|
|
203
|
+
partContentTypes = {},
|
|
204
|
+
boundary = `----RapiDocFormBoundary${Math.random().toString(36).slice(2)}${Date.now().toString(36)}`,
|
|
205
|
+
) {
|
|
206
|
+
const chunks = [];
|
|
207
|
+
for (const [name, value] of formData.entries()) {
|
|
208
|
+
const isFile = typeof value !== "string";
|
|
209
|
+
let head = `--${boundary}\r\nContent-Disposition: form-data; name="${escapePartName(name)}"`;
|
|
210
|
+
if (isFile) {
|
|
211
|
+
head += `; filename="${escapePartName(value.name ?? "blob")}"\r\nContent-Type: ${value.type || partContentTypes[name] || "application/octet-stream"}`;
|
|
212
|
+
} else if (partContentTypes[name]) {
|
|
213
|
+
head += `\r\nContent-Type: ${partContentTypes[name]}`;
|
|
214
|
+
}
|
|
215
|
+
chunks.push(`${head}\r\n\r\n`, isFile ? value : value.replace(/\r?\n/g, "\r\n"), "\r\n");
|
|
216
|
+
}
|
|
217
|
+
chunks.push(`--${boundary}--\r\n`);
|
|
218
|
+
return { body: new Blob(chunks), contentType: `multipart/form-data; boundary=${boundary}` };
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Builds a FormData instance from structured field entries.
|
|
223
|
+
*
|
|
224
|
+
* @param {Array<{ name: string, value: any, file?: File, isArray?: boolean }>} fields
|
|
225
|
+
* @returns {FormData}
|
|
226
|
+
*/
|
|
227
|
+
export function buildMultipartFormData(fields = []) {
|
|
228
|
+
const formData = new FormData();
|
|
229
|
+
|
|
230
|
+
if (!Array.isArray(fields)) {
|
|
231
|
+
return formData;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
fields.forEach((field) => {
|
|
235
|
+
if (!field || !field.name) {
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
if (typeof File !== "undefined" && field.file instanceof File) {
|
|
240
|
+
formData.append(field.name, field.file, field.file.name);
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
if (field.value === undefined || field.value === null || field.value === "") {
|
|
245
|
+
return;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
if (field.isArray) {
|
|
249
|
+
const arr = Array.isArray(field.value) ? field.value : [field.value];
|
|
250
|
+
const filtered = arr.filter((v) => v !== "" && v !== undefined && v !== null);
|
|
251
|
+
filtered.forEach((item) => {
|
|
252
|
+
if (typeof File !== "undefined" && item instanceof File) {
|
|
253
|
+
formData.append(field.name, item, item.name);
|
|
254
|
+
} else {
|
|
255
|
+
formData.append(field.name, String(item));
|
|
256
|
+
}
|
|
257
|
+
});
|
|
258
|
+
} else {
|
|
259
|
+
formData.append(field.name, String(field.value));
|
|
260
|
+
}
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
return formData;
|
|
264
|
+
}
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
import { formatXml } from "./xml-utils.js";
|
|
2
|
+
import { schemaToAST, generateExample, standardizeExample } from "./schema-utils.js";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Parses and processes a standard Fetch API Response into RapiDoc formatted response state.
|
|
6
|
+
* Handles JSON parsing, XML formatting, NDJSON streaming format, charset decoding,
|
|
7
|
+
* and binary MIME sniffing with RFC 5987 Content-Disposition decoding.
|
|
8
|
+
*
|
|
9
|
+
* @param {Response} fetchResponse - Standard Fetch Response object
|
|
10
|
+
* @returns {Promise<{
|
|
11
|
+
* responseText: string,
|
|
12
|
+
* respJson?: any,
|
|
13
|
+
* respBlob?: Blob,
|
|
14
|
+
* respText?: string,
|
|
15
|
+
* responseIsBlob: boolean,
|
|
16
|
+
* responseBlobType: string,
|
|
17
|
+
* responseBlobUrl: string,
|
|
18
|
+
* respContentDisposition: string
|
|
19
|
+
* }>}
|
|
20
|
+
*/
|
|
21
|
+
export async function processFetchResponse(fetchResponse) {
|
|
22
|
+
let respBlob;
|
|
23
|
+
let respJson;
|
|
24
|
+
let respText;
|
|
25
|
+
let responseIsBlob = false;
|
|
26
|
+
let responseBlobType = "";
|
|
27
|
+
let responseBlobUrl = "";
|
|
28
|
+
let respContentDisposition = "";
|
|
29
|
+
let responseText = "";
|
|
30
|
+
|
|
31
|
+
let contentType = fetchResponse.headers.get("content-type");
|
|
32
|
+
const respEmpty = (await fetchResponse.clone().text()).length === 0;
|
|
33
|
+
if (respEmpty) {
|
|
34
|
+
responseText = "";
|
|
35
|
+
} else if (contentType) {
|
|
36
|
+
contentType = contentType.split(";")[0].trim();
|
|
37
|
+
if (contentType === "application/x-ndjson") {
|
|
38
|
+
responseText = await fetchResponse.text();
|
|
39
|
+
} else if (contentType.includes("json")) {
|
|
40
|
+
if (/charset=[^"']+/.test(contentType)) {
|
|
41
|
+
const encoding = contentType.split("charset=")[1];
|
|
42
|
+
const buffer = await fetchResponse.arrayBuffer();
|
|
43
|
+
try {
|
|
44
|
+
respText = new TextDecoder(encoding).decode(buffer);
|
|
45
|
+
} catch {
|
|
46
|
+
respText = new TextDecoder("utf-8").decode(buffer);
|
|
47
|
+
}
|
|
48
|
+
try {
|
|
49
|
+
respJson = JSON.parse(respText);
|
|
50
|
+
responseText = JSON.stringify(respJson, null, 2);
|
|
51
|
+
} catch {
|
|
52
|
+
responseText = respText;
|
|
53
|
+
}
|
|
54
|
+
} else {
|
|
55
|
+
respJson = await fetchResponse.json();
|
|
56
|
+
responseText = JSON.stringify(respJson, null, 2);
|
|
57
|
+
}
|
|
58
|
+
} else if (
|
|
59
|
+
/^font\/|tar$|zip$|7z$|rtf$|msword$|excel$|\/pdf$|\/octet-stream$|^application\/vnd\./.test(
|
|
60
|
+
contentType,
|
|
61
|
+
)
|
|
62
|
+
) {
|
|
63
|
+
responseIsBlob = true;
|
|
64
|
+
responseBlobType = "download";
|
|
65
|
+
} else if (contentType.startsWith("image")) {
|
|
66
|
+
responseIsBlob = true;
|
|
67
|
+
responseBlobType = "image";
|
|
68
|
+
} else if (/^audio|^image|^video/.test(contentType)) {
|
|
69
|
+
responseIsBlob = true;
|
|
70
|
+
responseBlobType = "view";
|
|
71
|
+
} else {
|
|
72
|
+
respText = await fetchResponse.text();
|
|
73
|
+
if (contentType.includes("xml")) {
|
|
74
|
+
responseText = formatXml(respText, { textNodesOnSameLine: true, indentor: " " });
|
|
75
|
+
} else {
|
|
76
|
+
responseText = respText;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
if (responseIsBlob) {
|
|
80
|
+
const contentDisposition = fetchResponse.headers.get("content-disposition") || "";
|
|
81
|
+
let filenameFromContentDisposition = "filename";
|
|
82
|
+
if (contentDisposition) {
|
|
83
|
+
const filenameStarRegexMatch = contentDisposition.match(/filename\*=\s*UTF-8''([^;]+)/);
|
|
84
|
+
if (filenameStarRegexMatch) {
|
|
85
|
+
filenameFromContentDisposition = decodeURIComponent(filenameStarRegexMatch[1]);
|
|
86
|
+
} else {
|
|
87
|
+
const filenameMatch = contentDisposition.match(/filename="?([^"]+)"?/);
|
|
88
|
+
if (filenameMatch) {
|
|
89
|
+
filenameFromContentDisposition = filenameMatch[1];
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
respContentDisposition = filenameFromContentDisposition;
|
|
94
|
+
respBlob = await fetchResponse.blob();
|
|
95
|
+
if (typeof URL !== "undefined" && URL.createObjectURL) {
|
|
96
|
+
responseBlobUrl = URL.createObjectURL(respBlob);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
} else {
|
|
100
|
+
respText = await fetchResponse.text();
|
|
101
|
+
responseText = respText;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return {
|
|
105
|
+
responseText,
|
|
106
|
+
respJson,
|
|
107
|
+
respBlob,
|
|
108
|
+
respText,
|
|
109
|
+
responseIsBlob,
|
|
110
|
+
responseBlobType,
|
|
111
|
+
responseBlobUrl,
|
|
112
|
+
respContentDisposition,
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Pre-computes schema ASTs, examples, and headers for all response codes and MIME types.
|
|
118
|
+
* Pure function extracted from api-response component to enable caching and unit testing.
|
|
119
|
+
*
|
|
120
|
+
* @param {object} responses - OpenAPI responses map
|
|
121
|
+
* @param {object} [options]
|
|
122
|
+
* @param {boolean|string} [options.callback=false]
|
|
123
|
+
* @param {boolean|string} [options.webhook=false]
|
|
124
|
+
* @returns {{
|
|
125
|
+
* headersForEachRespStatus: object,
|
|
126
|
+
* mimeResponsesForEachStatus: object,
|
|
127
|
+
* defaultStatus: string,
|
|
128
|
+
* defaultMimeType: string
|
|
129
|
+
* }}
|
|
130
|
+
*/
|
|
131
|
+
export function prepareResponseDefinitions(responses, options = {}) {
|
|
132
|
+
const { callback = false, webhook = false } = options;
|
|
133
|
+
const isCallbackOrWebhook =
|
|
134
|
+
callback === true || callback === "true" || webhook === true || webhook === "true";
|
|
135
|
+
|
|
136
|
+
const headersForEachRespStatus = {};
|
|
137
|
+
const mimeResponsesForEachStatus = {};
|
|
138
|
+
let defaultStatus = "";
|
|
139
|
+
let defaultMimeType = "";
|
|
140
|
+
|
|
141
|
+
if (!responses || typeof responses !== "object") {
|
|
142
|
+
return {
|
|
143
|
+
headersForEachRespStatus,
|
|
144
|
+
mimeResponsesForEachStatus,
|
|
145
|
+
defaultStatus,
|
|
146
|
+
defaultMimeType,
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
for (const statusCode of Object.keys(responses)) {
|
|
151
|
+
if (statusCode === "$$ref") {
|
|
152
|
+
continue;
|
|
153
|
+
}
|
|
154
|
+
if (!defaultStatus) {
|
|
155
|
+
defaultStatus = statusCode;
|
|
156
|
+
}
|
|
157
|
+
const allMimeResp = {};
|
|
158
|
+
const content = responses[statusCode]?.content;
|
|
159
|
+
|
|
160
|
+
if (content && typeof content === "object") {
|
|
161
|
+
for (const mimeResp of Object.keys(content)) {
|
|
162
|
+
const mimeRespObj = content[mimeResp];
|
|
163
|
+
if (!defaultMimeType) {
|
|
164
|
+
defaultMimeType = mimeResp;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const schemaTree = mimeRespObj?.schema ? schemaToAST(mimeRespObj.schema) : null;
|
|
168
|
+
const respExamples = generateExample(
|
|
169
|
+
mimeRespObj?.schema,
|
|
170
|
+
mimeResp,
|
|
171
|
+
standardizeExample(mimeRespObj?.examples),
|
|
172
|
+
standardizeExample(mimeRespObj?.example),
|
|
173
|
+
!isCallbackOrWebhook,
|
|
174
|
+
isCallbackOrWebhook,
|
|
175
|
+
mimeResp.includes("json") ? "json" : "text",
|
|
176
|
+
);
|
|
177
|
+
|
|
178
|
+
allMimeResp[mimeResp] = {
|
|
179
|
+
description: responses[statusCode].description,
|
|
180
|
+
examples: respExamples,
|
|
181
|
+
selectedExample: respExamples[0]?.exampleId || "",
|
|
182
|
+
schemaTree,
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const tempHeaders = [];
|
|
188
|
+
if (responses[statusCode]?.headers) {
|
|
189
|
+
for (const key of Object.keys(responses[statusCode].headers)) {
|
|
190
|
+
tempHeaders.push({ name: key, ...responses[statusCode].headers[key] });
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
headersForEachRespStatus[statusCode] = tempHeaders;
|
|
194
|
+
mimeResponsesForEachStatus[statusCode] = allMimeResp;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
return {
|
|
198
|
+
headersForEachRespStatus,
|
|
199
|
+
mimeResponsesForEachStatus,
|
|
200
|
+
defaultStatus,
|
|
201
|
+
defaultMimeType,
|
|
202
|
+
};
|
|
203
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
// One instance for every call: constructing it per description is measurable on large schemas
|
|
2
|
+
let nativeSanitizer;
|
|
3
|
+
|
|
4
|
+
export function sanitizeHTML(htmlString) {
|
|
5
|
+
if (typeof htmlString !== "string") {
|
|
6
|
+
return "";
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
// Use native Sanitizer API if supported
|
|
10
|
+
if (typeof window !== "undefined" && window.Sanitizer && Element.prototype.setHTML) {
|
|
11
|
+
try {
|
|
12
|
+
const el = document.createElement("div");
|
|
13
|
+
nativeSanitizer ??= new window.Sanitizer({ allowAttributes: { "*": ["*"] } });
|
|
14
|
+
el.setHTML(htmlString, { sanitizer: nativeSanitizer });
|
|
15
|
+
return el.innerHTML;
|
|
16
|
+
} catch {
|
|
17
|
+
// If Sanitizer with options throws, fall through to fallback shim
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// Minimal shim for unsupported browsers to prevent basic XSS
|
|
22
|
+
let doc;
|
|
23
|
+
if (typeof DOMParser !== "undefined") {
|
|
24
|
+
doc = new DOMParser().parseFromString(htmlString, "text/html");
|
|
25
|
+
} else {
|
|
26
|
+
// Fallback for SSR/Node if needed, though this is primarily browser-side
|
|
27
|
+
return htmlString;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Remove script tags
|
|
31
|
+
const scripts = doc.querySelectorAll("script");
|
|
32
|
+
for (let i = scripts.length - 1; i >= 0; i--) {
|
|
33
|
+
scripts[i].parentNode.removeChild(scripts[i]);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Remove on* attributes and javascript: URIs
|
|
37
|
+
const allElements = doc.querySelectorAll("*");
|
|
38
|
+
for (let i = 0; i < allElements.length; i++) {
|
|
39
|
+
const el = allElements[i];
|
|
40
|
+
for (let j = el.attributes.length - 1; j >= 0; j--) {
|
|
41
|
+
const attr = el.attributes[j];
|
|
42
|
+
const attrName = attr.name.toLowerCase();
|
|
43
|
+
const attrValue = attr.value.toLowerCase();
|
|
44
|
+
|
|
45
|
+
if (attrName.startsWith("on")) {
|
|
46
|
+
el.removeAttribute(attr.name);
|
|
47
|
+
} else if (
|
|
48
|
+
(attrName === "href" || attrName === "src") &&
|
|
49
|
+
(attrValue.includes("javascript:") || attrValue.includes("data:text/html"))
|
|
50
|
+
) {
|
|
51
|
+
el.removeAttribute(attr.name);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return doc.body.innerHTML;
|
|
57
|
+
}
|