rapidoc 11.0.0 → 12.0.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 +46 -31
- package/dist/rapidoc-min.js +1424 -213
- package/dist/rapidoc-static-min.js +13542 -0
- package/package.json +48 -34
- package/src/components/api-request.ts +869 -0
- package/src/components/api-response.ts +509 -0
- package/src/components/json-tree.ts +171 -0
- package/src/components/schema-base-element.ts +315 -0
- package/src/components/schema-table.ts +430 -0
- package/src/components/schema-tree.ts +386 -0
- package/src/components/tag-input.ts +184 -0
- package/src/index.js +6 -0
- package/src/oauth-receiver.ts +29 -0
- package/src/rapidoc-mini.ts +582 -0
- package/src/rapidoc.ts +1808 -0
- package/src/static/index.d.ts +149 -0
- package/src/static/index.js +62 -0
- package/src/static/navigation.js +497 -0
- package/src/static/render.js +490 -0
- package/src/static/serializer.js +216 -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 +559 -0
- package/src/styles/flex-styles.js +16 -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 +609 -0
- package/src/styles/param-list-styles.js +261 -0
- package/src/styles/schema-styles.js +288 -0
- package/src/styles/tab-styles.js +106 -0
- package/src/styles/theme-tokens.js +582 -0
- package/src/templates/advanced-search-dialog-template.js +94 -0
- package/src/templates/api-response-template.js +219 -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 +85 -0
- package/src/templates/endpoint-template.js +386 -0
- package/src/templates/expanded-endpoint-template.js +266 -0
- package/src/templates/focused-endpoint-template.js +145 -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 +489 -0
- package/src/templates/overview-template.js +104 -0
- package/src/templates/request-body-template.js +651 -0
- package/src/templates/request-params-template.js +474 -0
- package/src/templates/security-scheme-template.js +1263 -0
- package/src/templates/server-template.js +152 -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 +123 -0
- package/src/utils/highlighter.js +416 -0
- package/src/utils/markdown-utils.js +110 -0
- package/src/utils/mcp/common.js +467 -0
- package/src/utils/mcp/generators/csharp.js +242 -0
- package/src/utils/mcp/generators/go.js +298 -0
- package/src/utils/mcp/generators/java.js +330 -0
- package/src/utils/mcp/generators/python.js +248 -0
- package/src/utils/mcp/generators/rust.js +281 -0
- package/src/utils/mcp/generators/typescript.js +290 -0
- package/src/utils/mcp-generator.js +143 -0
- package/src/utils/mock-interceptor.js +508 -0
- package/src/utils/oauth-utils.js +87 -0
- package/src/utils/request-utils.js +203 -0
- package/src/utils/response-utils.js +203 -0
- package/src/utils/sanitize.js +57 -0
- package/src/utils/schema-utils.js +1927 -0
- package/src/utils/slugger.js +189 -0
- package/src/utils/spec-parser.js +915 -0
- package/src/utils/url-utils.js +211 -0
- package/src/utils/xml-utils.js +61 -0
|
@@ -0,0 +1,508 @@
|
|
|
1
|
+
import { generateExample } from "./schema-utils.js";
|
|
2
|
+
|
|
3
|
+
const STATUS_TEXTS = {
|
|
4
|
+
200: "OK",
|
|
5
|
+
201: "Created",
|
|
6
|
+
202: "Accepted",
|
|
7
|
+
204: "No Content",
|
|
8
|
+
301: "Moved Permanently",
|
|
9
|
+
302: "Found",
|
|
10
|
+
304: "Not Modified",
|
|
11
|
+
400: "Bad Request",
|
|
12
|
+
401: "Unauthorized",
|
|
13
|
+
403: "Forbidden",
|
|
14
|
+
404: "Not Found",
|
|
15
|
+
405: "Method Not Allowed",
|
|
16
|
+
409: "Conflict",
|
|
17
|
+
422: "Unprocessable Entity",
|
|
18
|
+
429: "Too Many Requests",
|
|
19
|
+
500: "Internal Server Error",
|
|
20
|
+
501: "Not Implemented",
|
|
21
|
+
502: "Bad Gateway",
|
|
22
|
+
503: "Service Unavailable",
|
|
23
|
+
504: "Gateway Timeout",
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
let isMockActive = false;
|
|
27
|
+
let originalFetch = null;
|
|
28
|
+
let originalXhrOpen = null;
|
|
29
|
+
let originalXhrSend = null;
|
|
30
|
+
let originalXhrSetRequestHeader = null;
|
|
31
|
+
|
|
32
|
+
let compiledRoutes = [];
|
|
33
|
+
let mockConfig = {
|
|
34
|
+
statusCode: "",
|
|
35
|
+
statusStrategy: "first",
|
|
36
|
+
delay: 0,
|
|
37
|
+
log: "true",
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
const routeCycleIndices = new Map();
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Normalizes and extracts base paths and hostnames from OpenAPI servers array.
|
|
44
|
+
*/
|
|
45
|
+
function extractServerBases(servers = []) {
|
|
46
|
+
const bases = new Set();
|
|
47
|
+
// Always include empty root base path
|
|
48
|
+
bases.add("");
|
|
49
|
+
|
|
50
|
+
servers.forEach((s) => {
|
|
51
|
+
const rawUrl = s.computedUrl || s.url || "";
|
|
52
|
+
if (!rawUrl) return;
|
|
53
|
+
|
|
54
|
+
try {
|
|
55
|
+
if (rawUrl.startsWith("http://") || rawUrl.startsWith("https://")) {
|
|
56
|
+
const u = new URL(rawUrl);
|
|
57
|
+
const p = u.pathname.replace(/\/+$/, "");
|
|
58
|
+
if (p) bases.add(p);
|
|
59
|
+
} else {
|
|
60
|
+
const p = rawUrl.replace(/\/+$/, "");
|
|
61
|
+
if (p) bases.add(p.startsWith("/") ? p : `/${p}`);
|
|
62
|
+
}
|
|
63
|
+
} catch {
|
|
64
|
+
// Ignore invalid URL formats
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
return Array.from(bases);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Converts an OpenAPI path template (e.g. /pets/{id}) into a matching RegExp.
|
|
73
|
+
*/
|
|
74
|
+
function compileRoutePattern(pathTemplate, basePath = "") {
|
|
75
|
+
const cleanBase = basePath.endsWith("/") ? basePath.slice(0, -1) : basePath;
|
|
76
|
+
const fullPath = cleanBase + (pathTemplate.startsWith("/") ? pathTemplate : `/${pathTemplate}`);
|
|
77
|
+
|
|
78
|
+
// Replace {param} with a placeholder token to avoid escaping issues
|
|
79
|
+
const tokenized = fullPath.replace(/\{([a-zA-Z0-9_-]+)\}/g, "__PARAM_$1__");
|
|
80
|
+
|
|
81
|
+
// Escape special regex characters
|
|
82
|
+
const escaped = tokenized.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, "\\$&");
|
|
83
|
+
|
|
84
|
+
// Replace placeholder token with regex capture group
|
|
85
|
+
const pattern = escaped.replace(/__PARAM_([a-zA-Z0-9_-]+)__/g, "(?<$1>[^/?#]+)");
|
|
86
|
+
|
|
87
|
+
return new RegExp(`^${pattern}(?:\\/)?$`);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Builds the route table from the resolved OpenAPI spec.
|
|
92
|
+
*/
|
|
93
|
+
export function compileRoutes(spec) {
|
|
94
|
+
const routes = [];
|
|
95
|
+
const rootBases = extractServerBases(spec?.servers);
|
|
96
|
+
|
|
97
|
+
if (!spec?.tags || !Array.isArray(spec.tags)) {
|
|
98
|
+
return routes;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const seenRoutes = new Set();
|
|
102
|
+
|
|
103
|
+
spec.tags.forEach((tag) => {
|
|
104
|
+
(tag.paths || []).forEach((pathItem) => {
|
|
105
|
+
const method = (pathItem.method || "GET").toUpperCase();
|
|
106
|
+
const pathTemplate = pathItem.path || "";
|
|
107
|
+
const routeKey = `${method} ${pathTemplate}`;
|
|
108
|
+
|
|
109
|
+
if (seenRoutes.has(routeKey)) {
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
seenRoutes.add(routeKey);
|
|
113
|
+
|
|
114
|
+
const endpointBases =
|
|
115
|
+
pathItem.servers?.length > 0 ? extractServerBases(pathItem.servers) : rootBases;
|
|
116
|
+
|
|
117
|
+
const patterns = endpointBases.map((base) => compileRoutePattern(pathTemplate, base));
|
|
118
|
+
|
|
119
|
+
routes.push({
|
|
120
|
+
method,
|
|
121
|
+
pathTemplate,
|
|
122
|
+
routeKey,
|
|
123
|
+
patterns,
|
|
124
|
+
responses: pathItem.responses || {},
|
|
125
|
+
parameters: pathItem.parameters || [],
|
|
126
|
+
});
|
|
127
|
+
});
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
return routes;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Matches a request (method + URL) against the compiled routes.
|
|
135
|
+
*/
|
|
136
|
+
function matchRoute(method, urlStr) {
|
|
137
|
+
if (!compiledRoutes.length || !urlStr) {
|
|
138
|
+
return null;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
let reqUrl;
|
|
142
|
+
try {
|
|
143
|
+
const origin =
|
|
144
|
+
typeof window !== "undefined" && window.location?.href
|
|
145
|
+
? window.location.href
|
|
146
|
+
: "http://localhost";
|
|
147
|
+
reqUrl = new URL(urlStr, origin);
|
|
148
|
+
} catch {
|
|
149
|
+
return null;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const upperMethod = (method || "GET").toUpperCase();
|
|
153
|
+
const pathname = decodeURI(reqUrl.pathname);
|
|
154
|
+
|
|
155
|
+
for (const route of compiledRoutes) {
|
|
156
|
+
if (route.method !== upperMethod) {
|
|
157
|
+
continue;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
for (const pattern of route.patterns) {
|
|
161
|
+
const match = pathname.match(pattern);
|
|
162
|
+
if (match) {
|
|
163
|
+
return {
|
|
164
|
+
route,
|
|
165
|
+
pathParams: match.groups || {},
|
|
166
|
+
url: reqUrl,
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
return null;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Chooses the status code based on config and strategy (first, cycle, random).
|
|
177
|
+
*/
|
|
178
|
+
function selectStatusCode(configuredCodesStr, strategy, endpointKey, responses = {}) {
|
|
179
|
+
const codes = (configuredCodesStr || "")
|
|
180
|
+
.split(",")
|
|
181
|
+
.map((s) => s.trim())
|
|
182
|
+
.filter(Boolean);
|
|
183
|
+
|
|
184
|
+
if (codes.length === 0) {
|
|
185
|
+
// Look for 2xx response defined in spec
|
|
186
|
+
const twoXx = Object.keys(responses).find((k) => k.startsWith("2"));
|
|
187
|
+
if (twoXx) return twoXx;
|
|
188
|
+
if ("default" in responses) return "default";
|
|
189
|
+
const firstCode = Object.keys(responses)[0];
|
|
190
|
+
return firstCode || "200";
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
if (strategy === "random") {
|
|
194
|
+
return codes[Math.floor(Math.random() * codes.length)];
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
if (strategy === "cycle") {
|
|
198
|
+
const currentIndex = routeCycleIndices.get(endpointKey) || 0;
|
|
199
|
+
const selected = codes[currentIndex % codes.length];
|
|
200
|
+
routeCycleIndices.set(endpointKey, (currentIndex + 1) % codes.length);
|
|
201
|
+
return selected;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// 'first'
|
|
205
|
+
return codes[0];
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Generates the mock response body and headers for a matched route.
|
|
210
|
+
*/
|
|
211
|
+
function buildMockResponsePayload(route, statusCode) {
|
|
212
|
+
const responseDef =
|
|
213
|
+
route.responses?.[statusCode] ||
|
|
214
|
+
route.responses?.[String(statusCode)] ||
|
|
215
|
+
route.responses?.["default"];
|
|
216
|
+
|
|
217
|
+
const headers = {
|
|
218
|
+
"X-Mock-Server": "RapiDoc",
|
|
219
|
+
"Access-Control-Allow-Origin": "*",
|
|
220
|
+
};
|
|
221
|
+
|
|
222
|
+
let body = "";
|
|
223
|
+
let contentType = "application/json";
|
|
224
|
+
|
|
225
|
+
if (responseDef?.content) {
|
|
226
|
+
const availableTypes = Object.keys(responseDef.content);
|
|
227
|
+
// Prefer json, then xml, then text, then first available
|
|
228
|
+
contentType =
|
|
229
|
+
availableTypes.find((t) => t.includes("json")) ||
|
|
230
|
+
availableTypes.find((t) => t.includes("xml")) ||
|
|
231
|
+
availableTypes.find((t) => t.includes("text")) ||
|
|
232
|
+
availableTypes[0] ||
|
|
233
|
+
"application/json";
|
|
234
|
+
|
|
235
|
+
const mediaObj = responseDef.content[contentType];
|
|
236
|
+
if (mediaObj) {
|
|
237
|
+
if (mediaObj.example !== undefined) {
|
|
238
|
+
body =
|
|
239
|
+
typeof mediaObj.example === "object"
|
|
240
|
+
? JSON.stringify(mediaObj.example, null, 2)
|
|
241
|
+
: String(mediaObj.example);
|
|
242
|
+
} else if (mediaObj.examples && Object.keys(mediaObj.examples).length > 0) {
|
|
243
|
+
const firstEg = Object.values(mediaObj.examples)[0]?.value;
|
|
244
|
+
if (firstEg !== undefined) {
|
|
245
|
+
body = typeof firstEg === "object" ? JSON.stringify(firstEg, null, 2) : String(firstEg);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
if (!body && mediaObj.schema) {
|
|
250
|
+
const generated = generateExample(
|
|
251
|
+
mediaObj.schema,
|
|
252
|
+
contentType,
|
|
253
|
+
null,
|
|
254
|
+
null,
|
|
255
|
+
true,
|
|
256
|
+
false,
|
|
257
|
+
"text",
|
|
258
|
+
true,
|
|
259
|
+
);
|
|
260
|
+
|
|
261
|
+
if (generated && generated.length > 0) {
|
|
262
|
+
const val = generated[0].exampleValue;
|
|
263
|
+
body = typeof val === "object" ? JSON.stringify(val, null, 2) : String(val);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// Handle empty or default body when spec has no content defined
|
|
270
|
+
if (!body) {
|
|
271
|
+
if (String(statusCode) === "204") {
|
|
272
|
+
body = "";
|
|
273
|
+
} else if (contentType.includes("json")) {
|
|
274
|
+
const statusNum = Number(statusCode) || 200;
|
|
275
|
+
body = JSON.stringify(
|
|
276
|
+
{
|
|
277
|
+
status: statusNum,
|
|
278
|
+
message: STATUS_TEXTS[statusNum] || "Mock Response",
|
|
279
|
+
},
|
|
280
|
+
null,
|
|
281
|
+
2,
|
|
282
|
+
);
|
|
283
|
+
} else {
|
|
284
|
+
body = STATUS_TEXTS[Number(statusCode)] || "OK";
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// Copy defined headers from spec if any
|
|
289
|
+
if (responseDef?.headers) {
|
|
290
|
+
Object.entries(responseDef.headers).forEach(([hdrKey, hdrDef]) => {
|
|
291
|
+
if (hdrDef.schema?.default !== undefined) {
|
|
292
|
+
headers[hdrKey] = String(hdrDef.schema.default);
|
|
293
|
+
} else if (hdrDef.example !== undefined) {
|
|
294
|
+
headers[hdrKey] = String(hdrDef.example);
|
|
295
|
+
}
|
|
296
|
+
});
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
if (String(statusCode) !== "204") {
|
|
300
|
+
headers["Content-Type"] = contentType;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
return {
|
|
304
|
+
statusCode: Number(statusCode) || 200,
|
|
305
|
+
statusText:
|
|
306
|
+
STATUS_TEXTS[Number(statusCode)] || (statusCode === "default" ? "OK" : "Mock Response"),
|
|
307
|
+
headers,
|
|
308
|
+
body,
|
|
309
|
+
};
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
/**
|
|
313
|
+
* Enables the in-browser mock server by patching window.fetch and XMLHttpRequest.
|
|
314
|
+
*/
|
|
315
|
+
export function enableMockServer(spec, options = {}) {
|
|
316
|
+
mockConfig = {
|
|
317
|
+
statusCode: options.statusCode || "",
|
|
318
|
+
statusStrategy: options.statusStrategy || "first",
|
|
319
|
+
delay: Number(options.delay) || 0,
|
|
320
|
+
log: options.log !== "false",
|
|
321
|
+
};
|
|
322
|
+
|
|
323
|
+
compiledRoutes = compileRoutes(spec);
|
|
324
|
+
routeCycleIndices.clear();
|
|
325
|
+
|
|
326
|
+
if (isMockActive) {
|
|
327
|
+
return;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
if (typeof window === "undefined") {
|
|
331
|
+
return;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
// --- Patch window.fetch ---
|
|
335
|
+
if (!originalFetch && window.fetch) {
|
|
336
|
+
originalFetch = window.fetch;
|
|
337
|
+
|
|
338
|
+
window.fetch = async function mockFetch(input, init = {}) {
|
|
339
|
+
if (!isMockActive) {
|
|
340
|
+
return originalFetch.apply(this, arguments);
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
let url = "";
|
|
344
|
+
let method = "GET";
|
|
345
|
+
|
|
346
|
+
if (typeof input === "string") {
|
|
347
|
+
url = input;
|
|
348
|
+
method = init?.method || "GET";
|
|
349
|
+
} else if (input instanceof URL) {
|
|
350
|
+
url = input.href;
|
|
351
|
+
method = init?.method || "GET";
|
|
352
|
+
} else if (input && typeof input === "object" && "url" in input) {
|
|
353
|
+
url = input.url;
|
|
354
|
+
method = init?.method || input.method || "GET";
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
const match = matchRoute(method, url);
|
|
358
|
+
if (!match) {
|
|
359
|
+
return originalFetch.apply(this, arguments);
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
const statusCode = selectStatusCode(
|
|
363
|
+
mockConfig.statusCode,
|
|
364
|
+
mockConfig.statusStrategy,
|
|
365
|
+
match.route.routeKey,
|
|
366
|
+
match.route.responses,
|
|
367
|
+
);
|
|
368
|
+
const payload = buildMockResponsePayload(match.route, statusCode);
|
|
369
|
+
|
|
370
|
+
if (mockConfig.delay > 0) {
|
|
371
|
+
await new Promise((resolve) => setTimeout(resolve, mockConfig.delay));
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
if (mockConfig.log) {
|
|
375
|
+
console.info(
|
|
376
|
+
`%c[RapiDoc Mock]%c ${method.toUpperCase()} ${match.url.pathname} ➔ ${payload.statusCode} ${payload.statusText}`,
|
|
377
|
+
"color:#10b981; font-weight:bold;",
|
|
378
|
+
"color:inherit;",
|
|
379
|
+
);
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
return new Response(payload.body, {
|
|
383
|
+
status: payload.statusCode,
|
|
384
|
+
statusText: payload.statusText,
|
|
385
|
+
headers: payload.headers,
|
|
386
|
+
});
|
|
387
|
+
};
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
// --- Patch XMLHttpRequest ---
|
|
391
|
+
if (!originalXhrOpen && window.XMLHttpRequest) {
|
|
392
|
+
originalXhrOpen = window.XMLHttpRequest.prototype.open;
|
|
393
|
+
originalXhrSend = window.XMLHttpRequest.prototype.send;
|
|
394
|
+
originalXhrSetRequestHeader = window.XMLHttpRequest.prototype.setRequestHeader;
|
|
395
|
+
|
|
396
|
+
window.XMLHttpRequest.prototype.open = function mockXhrOpen(method, url, ...rest) {
|
|
397
|
+
this._mockMethod = method;
|
|
398
|
+
this._mockUrl = url;
|
|
399
|
+
this._mockHeaders = {};
|
|
400
|
+
return originalXhrOpen.apply(this, [method, url, ...rest]);
|
|
401
|
+
};
|
|
402
|
+
|
|
403
|
+
window.XMLHttpRequest.prototype.setRequestHeader = function mockXhrSetHeader(name, value) {
|
|
404
|
+
if (this._mockHeaders) {
|
|
405
|
+
this._mockHeaders[name] = value;
|
|
406
|
+
}
|
|
407
|
+
return originalXhrSetRequestHeader.apply(this, arguments);
|
|
408
|
+
};
|
|
409
|
+
|
|
410
|
+
window.XMLHttpRequest.prototype.send = function mockXhrSend() {
|
|
411
|
+
if (!isMockActive) {
|
|
412
|
+
return originalXhrSend.apply(this, arguments);
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
const match = matchRoute(this._mockMethod, this._mockUrl);
|
|
416
|
+
if (!match) {
|
|
417
|
+
return originalXhrSend.apply(this, arguments);
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
const statusCode = selectStatusCode(
|
|
421
|
+
mockConfig.statusCode,
|
|
422
|
+
mockConfig.statusStrategy,
|
|
423
|
+
match.route.routeKey,
|
|
424
|
+
match.route.responses,
|
|
425
|
+
);
|
|
426
|
+
const payload = buildMockResponsePayload(match.route, statusCode);
|
|
427
|
+
|
|
428
|
+
const delayMs = mockConfig.delay > 0 ? mockConfig.delay : 10;
|
|
429
|
+
|
|
430
|
+
setTimeout(() => {
|
|
431
|
+
if (mockConfig.log) {
|
|
432
|
+
console.info(
|
|
433
|
+
`%c[RapiDoc Mock (XHR)]%c ${this._mockMethod.toUpperCase()} ${match.url.pathname} ➔ ${payload.statusCode} ${payload.statusText}`,
|
|
434
|
+
"color:#10b981; font-weight:bold;",
|
|
435
|
+
"color:inherit;",
|
|
436
|
+
);
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
Object.defineProperty(this, "readyState", { value: 4, writable: true });
|
|
440
|
+
Object.defineProperty(this, "status", { value: payload.statusCode, writable: true });
|
|
441
|
+
Object.defineProperty(this, "statusText", { value: payload.statusText, writable: true });
|
|
442
|
+
Object.defineProperty(this, "responseText", { value: payload.body, writable: true });
|
|
443
|
+
Object.defineProperty(this, "response", { value: payload.body, writable: true });
|
|
444
|
+
|
|
445
|
+
const headerStr = Object.entries(payload.headers)
|
|
446
|
+
.map(([k, v]) => `${k}: ${v}\r\n`)
|
|
447
|
+
.join("");
|
|
448
|
+
|
|
449
|
+
this.getAllResponseHeaders = () => headerStr;
|
|
450
|
+
this.getResponseHeader = (name) => {
|
|
451
|
+
const lower = name.toLowerCase();
|
|
452
|
+
const found = Object.keys(payload.headers).find((k) => k.toLowerCase() === lower);
|
|
453
|
+
return found ? payload.headers[found] : null;
|
|
454
|
+
};
|
|
455
|
+
|
|
456
|
+
this.dispatchEvent(new Event("readystatechange"));
|
|
457
|
+
this.dispatchEvent(new Event("load"));
|
|
458
|
+
this.dispatchEvent(new Event("loadend"));
|
|
459
|
+
if (typeof this.onreadystatechange === "function") {
|
|
460
|
+
this.onreadystatechange();
|
|
461
|
+
}
|
|
462
|
+
if (typeof this.onload === "function") {
|
|
463
|
+
this.onload();
|
|
464
|
+
}
|
|
465
|
+
}, delayMs);
|
|
466
|
+
};
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
isMockActive = true;
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
/**
|
|
473
|
+
* Disables the mock server and restores original network primitives.
|
|
474
|
+
*/
|
|
475
|
+
export function disableMockServer() {
|
|
476
|
+
isMockActive = false;
|
|
477
|
+
compiledRoutes = [];
|
|
478
|
+
routeCycleIndices.clear();
|
|
479
|
+
|
|
480
|
+
if (typeof window !== "undefined") {
|
|
481
|
+
if (originalFetch) {
|
|
482
|
+
window.fetch = originalFetch;
|
|
483
|
+
originalFetch = null;
|
|
484
|
+
}
|
|
485
|
+
if (originalXhrOpen) {
|
|
486
|
+
window.XMLHttpRequest.prototype.open = originalXhrOpen;
|
|
487
|
+
window.XMLHttpRequest.prototype.send = originalXhrSend;
|
|
488
|
+
window.XMLHttpRequest.prototype.setRequestHeader = originalXhrSetRequestHeader;
|
|
489
|
+
originalXhrOpen = null;
|
|
490
|
+
originalXhrSend = null;
|
|
491
|
+
originalXhrSetRequestHeader = null;
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
/**
|
|
497
|
+
* Updates mock options without re-registering or recompiling routes.
|
|
498
|
+
*/
|
|
499
|
+
export function updateMockConfig(options = {}) {
|
|
500
|
+
if (options.statusCode !== undefined) mockConfig.statusCode = options.statusCode;
|
|
501
|
+
if (options.statusStrategy !== undefined) mockConfig.statusStrategy = options.statusStrategy;
|
|
502
|
+
if (options.delay !== undefined) mockConfig.delay = Number(options.delay) || 0;
|
|
503
|
+
if (options.log !== undefined) mockConfig.log = options.log !== "false";
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
export function isMockServerActive() {
|
|
507
|
+
return isMockActive;
|
|
508
|
+
}
|
|
@@ -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
|
+
}
|