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.
Files changed (80) hide show
  1. package/README.md +46 -31
  2. package/dist/rapidoc-min.js +1424 -213
  3. package/dist/rapidoc-static-min.js +13542 -0
  4. package/package.json +48 -34
  5. package/src/components/api-request.ts +869 -0
  6. package/src/components/api-response.ts +509 -0
  7. package/src/components/json-tree.ts +171 -0
  8. package/src/components/schema-base-element.ts +315 -0
  9. package/src/components/schema-table.ts +430 -0
  10. package/src/components/schema-tree.ts +386 -0
  11. package/src/components/tag-input.ts +184 -0
  12. package/src/index.js +6 -0
  13. package/src/oauth-receiver.ts +29 -0
  14. package/src/rapidoc-mini.ts +582 -0
  15. package/src/rapidoc.ts +1808 -0
  16. package/src/static/index.d.ts +149 -0
  17. package/src/static/index.js +62 -0
  18. package/src/static/navigation.js +497 -0
  19. package/src/static/render.js +490 -0
  20. package/src/static/serializer.js +216 -0
  21. package/src/styles/auth-styles.js +318 -0
  22. package/src/styles/border-styles.js +24 -0
  23. package/src/styles/css-bundle.js +129 -0
  24. package/src/styles/custom-styles.js +8 -0
  25. package/src/styles/dialog-box-styles.js +102 -0
  26. package/src/styles/endpoint-styles.js +559 -0
  27. package/src/styles/flex-styles.js +16 -0
  28. package/src/styles/font-styles.js +432 -0
  29. package/src/styles/info-styles.js +36 -0
  30. package/src/styles/input-styles.js +555 -0
  31. package/src/styles/layout-styles.js +396 -0
  32. package/src/styles/microlighter-styles.js +99 -0
  33. package/src/styles/nav-styles.js +609 -0
  34. package/src/styles/param-list-styles.js +261 -0
  35. package/src/styles/schema-styles.js +288 -0
  36. package/src/styles/tab-styles.js +106 -0
  37. package/src/styles/theme-tokens.js +582 -0
  38. package/src/templates/advanced-search-dialog-template.js +94 -0
  39. package/src/templates/api-response-template.js +219 -0
  40. package/src/templates/callback-template.js +86 -0
  41. package/src/templates/code-samples-template.js +52 -0
  42. package/src/templates/components-template.js +86 -0
  43. package/src/templates/downloads-template.js +85 -0
  44. package/src/templates/endpoint-template.js +386 -0
  45. package/src/templates/expanded-endpoint-template.js +266 -0
  46. package/src/templates/focused-endpoint-template.js +145 -0
  47. package/src/templates/header-template.js +92 -0
  48. package/src/templates/logo-template.js +21 -0
  49. package/src/templates/main-body-template.js +102 -0
  50. package/src/templates/navbar-template.js +489 -0
  51. package/src/templates/overview-template.js +104 -0
  52. package/src/templates/request-body-template.js +651 -0
  53. package/src/templates/request-params-template.js +474 -0
  54. package/src/templates/security-scheme-template.js +1263 -0
  55. package/src/templates/server-template.js +152 -0
  56. package/src/utils/auto-color-scheme.js +99 -0
  57. package/src/utils/bind-navbar.js +175 -0
  58. package/src/utils/color-utils.js +59 -0
  59. package/src/utils/common-utils.js +191 -0
  60. package/src/utils/curl-utils.js +123 -0
  61. package/src/utils/highlighter.js +416 -0
  62. package/src/utils/markdown-utils.js +110 -0
  63. package/src/utils/mcp/common.js +467 -0
  64. package/src/utils/mcp/generators/csharp.js +242 -0
  65. package/src/utils/mcp/generators/go.js +298 -0
  66. package/src/utils/mcp/generators/java.js +330 -0
  67. package/src/utils/mcp/generators/python.js +248 -0
  68. package/src/utils/mcp/generators/rust.js +281 -0
  69. package/src/utils/mcp/generators/typescript.js +290 -0
  70. package/src/utils/mcp-generator.js +143 -0
  71. package/src/utils/mock-interceptor.js +508 -0
  72. package/src/utils/oauth-utils.js +87 -0
  73. package/src/utils/request-utils.js +203 -0
  74. package/src/utils/response-utils.js +203 -0
  75. package/src/utils/sanitize.js +57 -0
  76. package/src/utils/schema-utils.js +1927 -0
  77. package/src/utils/slugger.js +189 -0
  78. package/src/utils/spec-parser.js +915 -0
  79. package/src/utils/url-utils.js +211 -0
  80. 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
+ }