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.
Files changed (96) hide show
  1. package/README.md +171 -54
  2. package/dist/rapidoc-flow-min.js +580 -0
  3. package/dist/rapidoc-flow-min.js.LICENSE.txt +171 -0
  4. package/dist/rapidoc-min.js +645 -202
  5. package/dist/rapidoc-min.js.LICENSE.txt +381 -0
  6. package/package.json +57 -42
  7. package/src/README.md +150 -0
  8. package/src/components/README.md +91 -0
  9. package/src/components/api-request.ts +912 -0
  10. package/src/components/api-response.ts +514 -0
  11. package/src/components/json-tree.ts +171 -0
  12. package/src/components/schema-base-element.ts +329 -0
  13. package/src/components/schema-table.ts +475 -0
  14. package/src/components/schema-tree.ts +393 -0
  15. package/src/components/tag-input.ts +180 -0
  16. package/src/flow.js +3 -0
  17. package/src/index.js +6 -0
  18. package/src/oauth-receiver.ts +29 -0
  19. package/src/rapi-flow.ts +1178 -0
  20. package/src/rapidoc-mini.ts +603 -0
  21. package/src/rapidoc.ts +1854 -0
  22. package/src/styles/README.md +96 -0
  23. package/src/styles/auth-styles.js +318 -0
  24. package/src/styles/border-styles.js +24 -0
  25. package/src/styles/css-bundle.js +129 -0
  26. package/src/styles/custom-styles.js +8 -0
  27. package/src/styles/dialog-box-styles.js +102 -0
  28. package/src/styles/endpoint-styles.js +564 -0
  29. package/src/styles/flex-styles.js +16 -0
  30. package/src/styles/flow-styles.js +248 -0
  31. package/src/styles/font-styles.js +432 -0
  32. package/src/styles/info-styles.js +36 -0
  33. package/src/styles/input-styles.js +555 -0
  34. package/src/styles/layout-styles.js +396 -0
  35. package/src/styles/microlighter-styles.js +99 -0
  36. package/src/styles/nav-styles.js +633 -0
  37. package/src/styles/param-list-styles.js +261 -0
  38. package/src/styles/schema-styles.js +296 -0
  39. package/src/styles/tab-styles.js +106 -0
  40. package/src/styles/theme-tokens.js +582 -0
  41. package/src/templates/README.md +89 -0
  42. package/src/templates/advanced-search-dialog-template.js +94 -0
  43. package/src/templates/api-response-template.js +223 -0
  44. package/src/templates/callback-template.js +86 -0
  45. package/src/templates/code-samples-template.js +52 -0
  46. package/src/templates/components-template.js +86 -0
  47. package/src/templates/downloads-template.js +70 -0
  48. package/src/templates/endpoint-template.js +377 -0
  49. package/src/templates/expanded-endpoint-template.js +270 -0
  50. package/src/templates/focused-endpoint-template.js +146 -0
  51. package/src/templates/header-template.js +92 -0
  52. package/src/templates/logo-template.js +21 -0
  53. package/src/templates/main-body-template.js +102 -0
  54. package/src/templates/navbar-template.js +498 -0
  55. package/src/templates/overview-template.js +104 -0
  56. package/src/templates/request-body-template.js +673 -0
  57. package/src/templates/request-params-template.js +485 -0
  58. package/src/templates/security-scheme-template.js +1272 -0
  59. package/src/templates/server-template.js +153 -0
  60. package/src/utils/README.md +146 -0
  61. package/src/utils/arazzo/README.md +119 -0
  62. package/src/utils/arazzo/components.js +15 -0
  63. package/src/utils/arazzo/graph.js +269 -0
  64. package/src/utils/arazzo/index.js +52 -0
  65. package/src/utils/arazzo/operations.js +106 -0
  66. package/src/utils/arazzo/parse.js +196 -0
  67. package/src/utils/arazzo/refs.js +40 -0
  68. package/src/utils/arazzo/svg.js +707 -0
  69. package/src/utils/auto-color-scheme.js +99 -0
  70. package/src/utils/bind-navbar.js +175 -0
  71. package/src/utils/color-utils.js +59 -0
  72. package/src/utils/common-utils.js +191 -0
  73. package/src/utils/curl-utils.js +126 -0
  74. package/src/utils/highlighter.js +419 -0
  75. package/src/utils/markdown-utils.js +110 -0
  76. package/src/utils/mcp/README.md +155 -0
  77. package/src/utils/mcp/common.js +166 -0
  78. package/src/utils/mcp/manifest.js +382 -0
  79. package/src/utils/mcp/project/server.ts +53 -0
  80. package/src/utils/mcp/project.js +88 -0
  81. package/src/utils/mcp/runtime.js +289 -0
  82. package/src/utils/mcp/schema.js +98 -0
  83. package/src/utils/mcp/workflow-runtime.js +374 -0
  84. package/src/utils/mcp/workflows.js +240 -0
  85. package/src/utils/mcp/zip.js +102 -0
  86. package/src/utils/mcp-generator.js +47 -0
  87. package/src/utils/mock-interceptor.js +557 -0
  88. package/src/utils/oauth-utils.js +87 -0
  89. package/src/utils/request-utils.js +264 -0
  90. package/src/utils/response-utils.js +203 -0
  91. package/src/utils/sanitize.js +57 -0
  92. package/src/utils/schema-utils.js +1954 -0
  93. package/src/utils/slugger.js +187 -0
  94. package/src/utils/spec-parser.js +941 -0
  95. package/src/utils/url-utils.js +246 -0
  96. package/src/utils/xml-utils.js +61 -0
@@ -0,0 +1,1272 @@
1
+ /**
2
+ * Renders the authentication modal dialog supporting API Keys, HTTP Basic/Bearer, OAuth2 flows, and OpenID Connect.
3
+ */
4
+ import { html } from "lit";
5
+ import { unsafeHTML } from "lit/directives/unsafe-html.js";
6
+ import { sanitizeHTML } from "../utils/sanitize.js";
7
+ import { oauthReceiverUrl } from "../utils/url-utils.js";
8
+ import {
9
+ OAUTH_CHANNEL,
10
+ isExpectedOAuthReply,
11
+ pkceChallenge,
12
+ randomUrlSafe,
13
+ } from "../utils/oauth-utils.js";
14
+ import { marked } from "marked";
15
+
16
+ const localStorageKey = "rapidoc";
17
+
18
+ function toBase64(str) {
19
+ try {
20
+ const bytes = new TextEncoder().encode(str);
21
+ let binary = "";
22
+ for (let i = 0; i < bytes.byteLength; i++) {
23
+ binary += String.fromCharCode(bytes[i]);
24
+ }
25
+ return btoa(binary);
26
+ } catch {
27
+ return btoa(str);
28
+ }
29
+ }
30
+
31
+ function normalizeParams(params) {
32
+ const result = new URLSearchParams();
33
+ if (!params) {
34
+ return result;
35
+ }
36
+ if (params instanceof URLSearchParams) {
37
+ for (const [k, v] of params.entries()) {
38
+ result.set(k, v);
39
+ }
40
+ return result;
41
+ }
42
+ if (typeof params === "object") {
43
+ Object.entries(params).forEach(([k, v]) => {
44
+ if (v !== undefined && v !== null) {
45
+ result.set(k, String(v));
46
+ }
47
+ });
48
+ return result;
49
+ }
50
+ if (typeof params === "string") {
51
+ const trimmed = params.trim();
52
+ if (trimmed.startsWith("{") && trimmed.endsWith("}")) {
53
+ try {
54
+ const obj = JSON.parse(trimmed);
55
+ if (typeof obj === "object" && obj !== null) {
56
+ Object.entries(obj).forEach(([k, v]) => {
57
+ if (v !== undefined && v !== null) {
58
+ result.set(k, String(v));
59
+ }
60
+ });
61
+ return result;
62
+ }
63
+ } catch {
64
+ // Not valid JSON, fall back to URLSearchParams
65
+ }
66
+ }
67
+ const cleanStr = trimmed.startsWith("?") ? trimmed.slice(1) : trimmed;
68
+ const parsed = new URLSearchParams(cleanStr);
69
+ for (const [k, v] of parsed.entries()) {
70
+ result.set(k, v);
71
+ }
72
+ return result;
73
+ }
74
+ return result;
75
+ }
76
+
77
+ /**
78
+ * Applies a key to a security scheme so Try sends it. `note` (from the page hosting RapiDoc, through setApiKey) says
79
+ * where the key came from, e.g. the reader's sign-in: the scheme then shows the note instead of its own form until
80
+ * the reader asks for the form. Any later change of the key (form, GET TOKEN, REMOVE, CLEAR ALL) drops the note.
81
+ */
82
+ export function applyApiKey(
83
+ securitySchemeId,
84
+ username = "",
85
+ password = "",
86
+ providedApikeyVal = "",
87
+ note = "",
88
+ ) {
89
+ const securityObj = this.resolvedSpec.securitySchemes?.find(
90
+ (v) => v.securitySchemeId === securitySchemeId,
91
+ );
92
+ if (!securityObj) {
93
+ return false;
94
+ }
95
+ let finalApiKeyValue = "";
96
+ if (securityObj.scheme?.toLowerCase() === "basic") {
97
+ if (username) {
98
+ finalApiKeyValue = `Basic ${toBase64(`${username}:${password}`)}`;
99
+ }
100
+ } else if (providedApikeyVal) {
101
+ securityObj.value = providedApikeyVal;
102
+ finalApiKeyValue = `${securityObj.scheme?.toLowerCase() === "bearer" ? "Bearer " : ""}${providedApikeyVal}`;
103
+ }
104
+ if (finalApiKeyValue) {
105
+ securityObj.finalKeyValue = finalApiKeyValue;
106
+ securityObj.hostNote = note ? String(note) : "";
107
+ securityObj.hostFormOpen = false;
108
+ this.requestUpdate();
109
+ return true;
110
+ }
111
+ return false;
112
+ }
113
+
114
+ export function onClearAllApiKeys() {
115
+ this.resolvedSpec.securitySchemes?.forEach((v) => {
116
+ v.user = "";
117
+ v.password = "";
118
+ v.value = "";
119
+ v.finalKeyValue = "";
120
+ v.hostNote = "";
121
+ });
122
+ if (this.persistAuth === "true") {
123
+ setPersistedApiKeys.call(this, {});
124
+ }
125
+ this.requestUpdate();
126
+ }
127
+
128
+ function getPersistedApiKeys() {
129
+ return JSON.parse(localStorage.getItem(localStorageKey)) || {};
130
+ }
131
+
132
+ function setPersistedApiKeys(obj) {
133
+ localStorage.setItem(localStorageKey, JSON.stringify(obj));
134
+ }
135
+
136
+ export function recoverPersistedApiKeys() {
137
+ const rapidocLs = getPersistedApiKeys.call(this);
138
+ Object.values(rapidocLs).forEach((p) => {
139
+ applyApiKey.call(this, p.securitySchemeId, "", "", p.value);
140
+ });
141
+ }
142
+
143
+ function onApiKeyChange(securitySchemeId) {
144
+ let apiKeyValue = "";
145
+ const securityObj = this.resolvedSpec.securitySchemes.find(
146
+ (v) => v.securitySchemeId === securitySchemeId,
147
+ );
148
+ if (securityObj) {
149
+ const schemeEl = this.shadowRoot.getElementById(`security-scheme-${securitySchemeId}`);
150
+ if (schemeEl) {
151
+ if (
152
+ securityObj.type &&
153
+ securityObj.scheme &&
154
+ securityObj.type === "http" &&
155
+ securityObj.scheme.toLowerCase() === "basic"
156
+ ) {
157
+ const userVal = schemeEl.querySelector(".api-key-user").value.trim();
158
+ const passwordVal = schemeEl.querySelector(".api-key-password").value.trim();
159
+ applyApiKey.call(this, securitySchemeId, userVal, passwordVal);
160
+ } else {
161
+ apiKeyValue = schemeEl.querySelector(".api-key-input").value.trim();
162
+ applyApiKey.call(this, securitySchemeId, "", "", apiKeyValue);
163
+ }
164
+ if (this.persistAuth === "true") {
165
+ // Only what recoverPersistedApiKeys restores: the apiKey / bearer value. Basic credentials
166
+ // (and the derived Authorization header) are never written to localStorage.
167
+ const rapidocLs = getPersistedApiKeys.call(this);
168
+ if (securityObj.value) {
169
+ rapidocLs[securitySchemeId] = { securitySchemeId, value: securityObj.value };
170
+ } else {
171
+ delete rapidocLs[securitySchemeId];
172
+ }
173
+ setPersistedApiKeys.call(this, rapidocLs);
174
+ }
175
+ }
176
+ }
177
+ }
178
+
179
+ /** Shows a status line under an OAuth flow. Always as text: provider error messages are never HTML. */
180
+ function showOAuthStatus(el, text, colorVar) {
181
+ if (!el) {
182
+ return;
183
+ }
184
+ const span = document.createElement("span");
185
+ span.style.color = `var(${colorVar})`;
186
+ span.textContent = text;
187
+ el.replaceChildren(span);
188
+ }
189
+
190
+ // Updates the OAuth Access Token (API key), so it reflects in UI and gets used in TRY calls
191
+ function updateOAuthKey(securitySchemeId, accessToken, tokenType = "Bearer") {
192
+ const securityObj = this.resolvedSpec.securitySchemes.find(
193
+ (v) => v.securitySchemeId === securitySchemeId,
194
+ );
195
+ securityObj.finalKeyValue = `${tokenType.toLowerCase() === "bearer" ? "Bearer" : tokenType.toLowerCase() === "mac" ? "MAC" : tokenType} ${accessToken}`;
196
+ securityObj.hostNote = "";
197
+ this.requestUpdate();
198
+ }
199
+
200
+ // Gets Access-Token in exchange of Authorization Code
201
+ async function fetchAccessToken(
202
+ tokenUrl,
203
+ clientId,
204
+ clientSecret,
205
+ redirectUrl,
206
+ grantType,
207
+ authCode,
208
+ securitySchemeId,
209
+ authFlowDivEl,
210
+ sendClientSecretIn = "request-body",
211
+ scopes = null,
212
+ username = null,
213
+ password = null,
214
+ tokenParamsSpec = null,
215
+ codeVerifier = null,
216
+ ) {
217
+ const respDisplayEl = authFlowDivEl
218
+ ? authFlowDivEl.querySelector(".oauth-resp-display")
219
+ : undefined;
220
+ const urlFormParams = new URLSearchParams();
221
+ const headers = new Headers();
222
+
223
+ // Merge vendor extension x-token-params (if any)
224
+ const specTokenParams = normalizeParams(tokenParamsSpec);
225
+ for (const [k, v] of specTokenParams.entries()) {
226
+ urlFormParams.set(k, v);
227
+ }
228
+
229
+ // Merge additional-token-params property/attribute on <rapi-doc> (if any)
230
+ const attrTokenParams = normalizeParams(this.additionalTokenParams);
231
+ for (const [k, v] of attrTokenParams.entries()) {
232
+ urlFormParams.set(k, v);
233
+ }
234
+
235
+ urlFormParams.set("grant_type", grantType);
236
+ if (grantType !== "client_credentials" && grantType !== "password") {
237
+ urlFormParams.set("redirect_uri", redirectUrl);
238
+ }
239
+ if (authCode) {
240
+ urlFormParams.set("code", authCode);
241
+ if (codeVerifier) {
242
+ urlFormParams.set("code_verifier", codeVerifier); // PKCE: proves this page started the sign-in
243
+ }
244
+ }
245
+ if (sendClientSecretIn === "header" && clientSecret) {
246
+ headers.set("Authorization", `Basic ${toBase64(`${clientId}:${clientSecret}`)}`);
247
+ } else {
248
+ if (clientId) {
249
+ urlFormParams.set("client_id", clientId);
250
+ }
251
+ if (clientSecret) {
252
+ urlFormParams.set("client_secret", clientSecret);
253
+ }
254
+ }
255
+ if (grantType === "password") {
256
+ urlFormParams.set("username", username);
257
+ urlFormParams.set("password", password);
258
+ }
259
+ if (scopes) {
260
+ urlFormParams.set("scope", scopes);
261
+ }
262
+
263
+ const requestDetail = {
264
+ method: "POST",
265
+ headers,
266
+ body: urlFormParams,
267
+ };
268
+ const beforeTokenEvent = new CustomEvent("before-token-request", {
269
+ bubbles: true,
270
+ composed: true,
271
+ cancelable: true,
272
+ detail: {
273
+ securitySchemeId,
274
+ grantType,
275
+ tokenUrl,
276
+ params: urlFormParams,
277
+ headers,
278
+ request: requestDetail,
279
+ },
280
+ });
281
+ const notCancelled = this.dispatchEvent(beforeTokenEvent);
282
+ if (!notCancelled) {
283
+ return false;
284
+ }
285
+
286
+ try {
287
+ const resp = await fetch(tokenUrl, {
288
+ method: requestDetail.method || "POST",
289
+ headers: requestDetail.headers || headers,
290
+ body: requestDetail.body || urlFormParams,
291
+ });
292
+ const tokenResp = await resp.json();
293
+ if (resp.ok) {
294
+ if (tokenResp.token_type && tokenResp.access_token) {
295
+ updateOAuthKey.call(this, securitySchemeId, tokenResp.access_token, tokenResp.token_type);
296
+ showOAuthStatus(respDisplayEl, "Access Token Received", "--green");
297
+ return true;
298
+ }
299
+ } else {
300
+ showOAuthStatus(
301
+ respDisplayEl,
302
+ tokenResp.error_description || tokenResp.error || "Unable to get access token",
303
+ "--destructive",
304
+ );
305
+ return false;
306
+ }
307
+ } catch {
308
+ showOAuthStatus(respDisplayEl, "Failed to get access token", "--destructive");
309
+ return false;
310
+ }
311
+ }
312
+
313
+ /**
314
+ * Handles the provider's reply to this sign-in (already checked by waitForOAuthReply): exchanges a code for a
315
+ * token, or applies a token (implicit flow). Accepts the older receiver's key names too.
316
+ */
317
+ function onOAuthReply(reply, flow) {
318
+ const respDisplayEl = flow.authFlowDivEl
319
+ ? flow.authFlowDivEl.querySelector(".oauth-resp-display")
320
+ : undefined;
321
+ if (reply.error) {
322
+ showOAuthStatus(respDisplayEl, reply.errorDescription || reply.error, "--destructive");
323
+ return;
324
+ }
325
+ if (reply.responseType === "code") {
326
+ fetchAccessToken.call(
327
+ this,
328
+ flow.tokenUrl,
329
+ flow.clientId,
330
+ flow.clientSecret,
331
+ flow.redirectUrl,
332
+ flow.grantType,
333
+ reply.code,
334
+ flow.securitySchemeId,
335
+ flow.authFlowDivEl,
336
+ flow.sendClientSecretIn,
337
+ null,
338
+ null,
339
+ null,
340
+ flow.tokenParamsSpec,
341
+ flow.codeVerifier,
342
+ );
343
+ } else {
344
+ const accessToken = reply.accessToken || reply.access_token;
345
+ if (accessToken) {
346
+ updateOAuthKey.call(
347
+ this,
348
+ flow.securitySchemeId,
349
+ accessToken,
350
+ reply.tokenType || reply.token_type || "Bearer",
351
+ );
352
+ }
353
+ }
354
+ }
355
+
356
+ /**
357
+ * Waits for the return page's reply to this sign-in: a window message from the popup, or a BroadcastChannel
358
+ * message when the provider's page cut the popup's link to this window (Cross-Origin-Opener-Policy). Anything
359
+ * that isn't the reply to this sign-in (other origin, other window, other state) is ignored, so it can neither
360
+ * inject a code or token nor end the wait. Starting another sign-in stops the previous wait.
361
+ */
362
+ function waitForOAuthReply(popup, state, onReply) {
363
+ if (typeof this.stopOAuthWait === "function") {
364
+ this.stopOAuthWait();
365
+ }
366
+ const channel =
367
+ typeof BroadcastChannel === "function" ? new BroadcastChannel(OAUTH_CHANNEL) : null;
368
+ let onMessage;
369
+ const stop = () => {
370
+ window.removeEventListener("message", onMessage);
371
+ if (channel) {
372
+ channel.close();
373
+ }
374
+ if (this.stopOAuthWait === stop) {
375
+ this.stopOAuthWait = null;
376
+ }
377
+ };
378
+ const finish = (reply) => {
379
+ stop();
380
+ try {
381
+ popup.close();
382
+ } catch {
383
+ // already closed
384
+ }
385
+ onReply(reply);
386
+ };
387
+ onMessage = (msgEvent) => {
388
+ if (
389
+ isExpectedOAuthReply(
390
+ msgEvent.data,
391
+ { state, origin: window.location.origin },
392
+ msgEvent.origin,
393
+ msgEvent.source,
394
+ popup,
395
+ )
396
+ ) {
397
+ finish(msgEvent.data);
398
+ }
399
+ };
400
+ window.addEventListener("message", onMessage);
401
+ if (channel) {
402
+ channel.onmessage = (msgEvent) => {
403
+ if (isExpectedOAuthReply(msgEvent.data, { state })) {
404
+ finish(msgEvent.data);
405
+ }
406
+ };
407
+ }
408
+ this.stopOAuthWait = stop;
409
+ }
410
+
411
+ async function onInvokeOAuthFlow(
412
+ securitySchemeId,
413
+ flowType,
414
+ authUrl,
415
+ tokenUrl,
416
+ e,
417
+ authorizeParamsSpec = null,
418
+ tokenParamsSpec = null,
419
+ ) {
420
+ const authFlowDivEl = e.target.closest(".oauth-flow");
421
+ const clientId = authFlowDivEl.querySelector(".oauth-client-id")
422
+ ? authFlowDivEl.querySelector(".oauth-client-id").value.trim()
423
+ : "";
424
+ const clientSecret = authFlowDivEl.querySelector(".oauth-client-secret")
425
+ ? authFlowDivEl.querySelector(".oauth-client-secret").value.trim()
426
+ : "";
427
+ const username = authFlowDivEl.querySelector(".api-key-user")
428
+ ? authFlowDivEl.querySelector(".api-key-user").value.trim()
429
+ : "";
430
+ const password = authFlowDivEl.querySelector(".api-key-password")
431
+ ? authFlowDivEl.querySelector(".api-key-password").value.trim()
432
+ : "";
433
+ const sendClientSecretIn = authFlowDivEl.querySelector(".oauth-send-client-secret-in")
434
+ ? authFlowDivEl.querySelector(".oauth-send-client-secret-in").value.trim()
435
+ : "request-body";
436
+ const checkedScopeEls = [...authFlowDivEl.querySelectorAll(".scope-checkbox:checked")];
437
+ const pkceCheckboxEl = authFlowDivEl.querySelector(`#${securitySchemeId}-pkce`);
438
+ // Unguessable per sign-in: state ties the reply to this sign-in, the PKCE verifier proves it at the token endpoint.
439
+ const state = randomUrlSafe(16);
440
+ const nonce = randomUrlSafe(16);
441
+ const usePkce = Boolean(pkceCheckboxEl && pkceCheckboxEl.checked);
442
+ const codeVerifier = usePkce ? randomUrlSafe(32) : null;
443
+ const redirectUrlObj = oauthReceiverUrl(window.location, this.oauthReceiver);
444
+ let grantType = "";
445
+ let responseType = "";
446
+ let newWindow;
447
+
448
+ // clear previous error messages
449
+ const errEls = [...authFlowDivEl.parentNode.querySelectorAll(".oauth-resp-display")];
450
+ errEls.forEach((v) => {
451
+ v.replaceChildren();
452
+ });
453
+
454
+ if (flowType === "authorizationCode" || flowType === "implicit") {
455
+ const authUrlObj = new URL(authUrl);
456
+ if (flowType === "authorizationCode") {
457
+ grantType = "authorization_code";
458
+ responseType = "code";
459
+ } else if (flowType === "implicit") {
460
+ responseType = "token";
461
+ }
462
+ const authCodeParams = new URLSearchParams(authUrlObj.search);
463
+
464
+ // Merge vendor extension x-authorize-params (if any)
465
+ const specAuthParams = normalizeParams(authorizeParamsSpec);
466
+ for (const [k, v] of specAuthParams.entries()) {
467
+ authCodeParams.set(k, v);
468
+ }
469
+
470
+ // Merge additional-authorize-params property/attribute on <rapi-doc> (if any)
471
+ const attrAuthParams = normalizeParams(this.additionalAuthorizeParams);
472
+ for (const [k, v] of attrAuthParams.entries()) {
473
+ authCodeParams.set(k, v);
474
+ }
475
+
476
+ const selectedScopes = checkedScopeEls.map((v) => v.value).join(" ");
477
+ if (selectedScopes) {
478
+ authCodeParams.set("scope", selectedScopes);
479
+ }
480
+ authCodeParams.set("client_id", clientId);
481
+ authCodeParams.set("redirect_uri", redirectUrlObj.toString());
482
+ authCodeParams.set("response_type", responseType);
483
+ authCodeParams.set("state", state);
484
+ authCodeParams.set("nonce", nonce);
485
+ if (codeVerifier) {
486
+ authCodeParams.set("code_challenge", await pkceChallenge(codeVerifier));
487
+ authCodeParams.set("code_challenge_method", "S256");
488
+ }
489
+
490
+ const beforeAuthorizeEvent = new CustomEvent("before-authorize", {
491
+ bubbles: true,
492
+ composed: true,
493
+ cancelable: true,
494
+ detail: {
495
+ securitySchemeId,
496
+ flowType,
497
+ authUrl,
498
+ authParams: authCodeParams,
499
+ },
500
+ });
501
+ const notCancelled = this.dispatchEvent(beforeAuthorizeEvent);
502
+ if (!notCancelled) {
503
+ return;
504
+ }
505
+
506
+ authUrlObj.search = authCodeParams.toString();
507
+ newWindow = window.open(authUrlObj.toString());
508
+ if (!newWindow) {
509
+ console.error(`RapiDoc: Unable to open ${authUrlObj.toString()} in a new window`);
510
+ showOAuthStatus(
511
+ authFlowDivEl.querySelector(".oauth-resp-display"),
512
+ "The sign-in window was blocked",
513
+ "--destructive",
514
+ );
515
+ return;
516
+ }
517
+ waitForOAuthReply.call(this, newWindow, state, (reply) =>
518
+ onOAuthReply.call(this, reply, {
519
+ tokenUrl,
520
+ clientId,
521
+ clientSecret,
522
+ redirectUrl: redirectUrlObj.toString(),
523
+ grantType,
524
+ sendClientSecretIn,
525
+ securitySchemeId,
526
+ authFlowDivEl,
527
+ tokenParamsSpec,
528
+ codeVerifier,
529
+ }),
530
+ );
531
+ } else if (flowType === "clientCredentials") {
532
+ grantType = "client_credentials";
533
+ const selectedScopes = checkedScopeEls.map((v) => v.value).join(" ");
534
+ fetchAccessToken.call(
535
+ this,
536
+ tokenUrl,
537
+ clientId,
538
+ clientSecret,
539
+ redirectUrlObj.toString(),
540
+ grantType,
541
+ "",
542
+ securitySchemeId,
543
+ authFlowDivEl,
544
+ sendClientSecretIn,
545
+ selectedScopes,
546
+ null,
547
+ null,
548
+ tokenParamsSpec,
549
+ );
550
+ } else if (flowType === "password") {
551
+ grantType = "password";
552
+ const selectedScopes = checkedScopeEls.map((v) => v.value).join(" ");
553
+ fetchAccessToken.call(
554
+ this,
555
+ tokenUrl,
556
+ clientId,
557
+ clientSecret,
558
+ redirectUrlObj.toString(),
559
+ grantType,
560
+ "",
561
+ securitySchemeId,
562
+ authFlowDivEl,
563
+ sendClientSecretIn,
564
+ selectedScopes,
565
+ username,
566
+ password,
567
+ tokenParamsSpec,
568
+ );
569
+ }
570
+ }
571
+
572
+ function oAuthFlowTemplate(
573
+ flowName,
574
+ clientId,
575
+ clientSecret,
576
+ securitySchemeId,
577
+ authFlow,
578
+ defaultScopes = [],
579
+ receiveTokenIn = "request-body",
580
+ receiveTokenInOptions = undefined,
581
+ allowTry = "true",
582
+ authorizeParams = null,
583
+ tokenParams = null,
584
+ ) {
585
+ let { authorizationUrl, tokenUrl, refreshUrl } = authFlow;
586
+ const pkceOnly = authFlow["x-pkce-only"] || false;
587
+ const isUrlAbsolute = (url) => url.indexOf("://") > 0 || url.indexOf("//") === 0;
588
+ /*
589
+ Calculcate Relative URL based on the following logic
590
+ IF this.selectedServer?.computedUrl ends with slash and the refreshUrl / tokenUrl / authorizationUrl do not start with slash
591
+ THEN Relative URL is concatenate else use this.selectedServer?.computedUrl + refreshUrl / tokenUrl / authorizationUrl
592
+ ELSE Relative URL is concatenate else use (origin of this.selectedServer?.computedUrl) + refreshUrl / tokenUrl / authorizationUrl
593
+ */
594
+ const url = new URL(this.selectedServer?.computedUrl);
595
+ const originUrl = url.origin;
596
+ if (refreshUrl && !isUrlAbsolute(refreshUrl)) {
597
+ if (
598
+ this.selectedServer?.computedUrl.trim().endsWith("/") &&
599
+ !refreshUrl.trim().startsWith("/")
600
+ ) {
601
+ refreshUrl = `${this.selectedServer?.computedUrl.trim()}${refreshUrl.trim()}`;
602
+ } else {
603
+ refreshUrl = `${originUrl}/${refreshUrl.replace(/^\//, "")}`;
604
+ }
605
+ }
606
+ if (tokenUrl && !isUrlAbsolute(tokenUrl)) {
607
+ if (this.selectedServer?.computedUrl.trim().endsWith("/") && !tokenUrl.trim().startsWith("/")) {
608
+ tokenUrl = `${this.selectedServer?.computedUrl.trim()}${tokenUrl.trim()}`;
609
+ } else {
610
+ tokenUrl = `${originUrl}/${tokenUrl.replace(/^\//, "")}`;
611
+ }
612
+ }
613
+ if (authorizationUrl && !isUrlAbsolute(authorizationUrl)) {
614
+ if (
615
+ this.selectedServer?.computedUrl.trim().endsWith("/") &&
616
+ !authorizationUrl.trim().startsWith("/")
617
+ ) {
618
+ authorizationUrl = `${this.selectedServer?.computedUrl.trim()}${authorizationUrl.trim()}`;
619
+ } else {
620
+ authorizationUrl = `${originUrl}/${authorizationUrl.replace(/^\//, "")}`;
621
+ }
622
+ }
623
+ let flowNameDisplay;
624
+ if (flowName === "authorizationCode") {
625
+ flowNameDisplay = "Authorization Code Flow";
626
+ } else if (flowName === "clientCredentials") {
627
+ flowNameDisplay = "Client Credentials Flow";
628
+ } else if (flowName === "implicit") {
629
+ flowNameDisplay = "Implicit Flow";
630
+ } else if (flowName === "password") {
631
+ flowNameDisplay = "Password Flow";
632
+ } else {
633
+ flowNameDisplay = flowName;
634
+ }
635
+ const showCredentials =
636
+ flowName === "authorizationCode" || flowName === "clientCredentials" || flowName === "password";
637
+ const isKnownFlow = showCredentials || flowName === "implicit";
638
+ return html`
639
+ <div class="oauth-flow ${flowName}">
640
+ <h3 class="rapidoc-heading label-heading tiny-title upper" part="heading heading-3">${flowNameDisplay}</h3>
641
+ ${
642
+ authorizationUrl || tokenUrl || refreshUrl
643
+ ? html`<dl class="oauth-urls">
644
+ ${
645
+ authorizationUrl
646
+ ? html`<dt>Auth URL</dt>
647
+ <dd class="mono-font">${authorizationUrl}</dd>`
648
+ : ""
649
+ }
650
+ ${
651
+ tokenUrl
652
+ ? html`<dt>Token URL</dt>
653
+ <dd class="mono-font">${tokenUrl}</dd>`
654
+ : ""
655
+ }
656
+ ${
657
+ refreshUrl
658
+ ? html`<dt>Refresh URL</dt>
659
+ <dd class="mono-font">${refreshUrl}</dd>`
660
+ : ""
661
+ }
662
+ </dl>`
663
+ : ""
664
+ }
665
+ ${
666
+ isKnownFlow && authFlow.scopes
667
+ ? html`<div class="oauth-scopes-label">Scopes</div>
668
+ <div class="oauth-scopes" part="section-auth-scopes">
669
+ ${Object.entries(authFlow.scopes).map(
670
+ ([scope, scopeDescr], index) =>
671
+ html`<label class="auth-toggle" for="${securitySchemeId}${flowName}${index}">
672
+ <input
673
+ type="checkbox"
674
+ part="checkbox checkbox-auth-scope"
675
+ class="scope-checkbox"
676
+ id="${securitySchemeId}${flowName}${index}"
677
+ ?checked="${defaultScopes.includes(scope)}"
678
+ value="${scope}"
679
+ />
680
+ <span
681
+ ><span class="oauth-scope-name mono-font">${scope}</span>${
682
+ scope !== scopeDescr && scopeDescr
683
+ ? html` <span class="oauth-scope-descr">${scopeDescr}</span>`
684
+ : ""
685
+ }</span
686
+ >
687
+ </label>`,
688
+ )}
689
+ </div>`
690
+ : ""
691
+ }
692
+ ${
693
+ isKnownFlow && allowTry === "true"
694
+ ? html`${
695
+ flowName === "authorizationCode"
696
+ ? html`<label class="auth-toggle oauth-pkce" for="${securitySchemeId}-pkce">
697
+ <input
698
+ type="checkbox"
699
+ part="checkbox checkbox-auth-scope"
700
+ id="${securitySchemeId}-pkce"
701
+ checked
702
+ ?disabled=${pkceOnly}
703
+ />
704
+ <span>Send Proof Key for Code Exchange (PKCE)</span>
705
+ </label>`
706
+ : ""
707
+ }
708
+ ${
709
+ flowName === "password"
710
+ ? html`<div class="auth-controls">
711
+ <input
712
+ type="text"
713
+ value=""
714
+ placeholder="username"
715
+ spellcheck="false"
716
+ class="oauth2 ${flowName} ${securitySchemeId} api-key-user"
717
+ part="textbox textbox-username"
718
+ id="input-${securitySchemeId}-${flowName}-api-key-user"
719
+ />
720
+ <input
721
+ type="password"
722
+ value=""
723
+ placeholder="password"
724
+ spellcheck="false"
725
+ class="oauth2 ${flowName} ${securitySchemeId} api-key-password"
726
+ part="textbox textbox-password"
727
+ id="input-${securitySchemeId}-${flowName}-api-key-password"
728
+ />
729
+ </div>`
730
+ : ""
731
+ }
732
+ <div class="auth-controls">
733
+ <input
734
+ type="text"
735
+ part="textbox textbox-auth-client-id"
736
+ value="${clientId || ""}"
737
+ placeholder="client-id"
738
+ spellcheck="false"
739
+ class="oauth2 ${flowName} ${securitySchemeId} oauth-client-id"
740
+ />
741
+ ${
742
+ showCredentials
743
+ ? html`<input
744
+ id="${securitySchemeId}-${flowName}-oauth-client-secret"
745
+ type="password"
746
+ part="textbox textbox-auth-client-secret"
747
+ value="${clientSecret || ""}"
748
+ placeholder="client-secret"
749
+ spellcheck="false"
750
+ class="oauth2 ${flowName} ${securitySchemeId} oauth-client-secret"
751
+ ?hidden=${pkceOnly}
752
+ />
753
+ <select
754
+ class="${flowName} ${securitySchemeId} oauth-send-client-secret-in"
755
+ aria-label="Send client secret in"
756
+ ?hidden=${pkceOnly}
757
+ >
758
+ ${
759
+ !receiveTokenInOptions || receiveTokenInOptions.includes("request-body")
760
+ ? html`<option value="request-body" .selected=${receiveTokenIn === "request-body"}>Request Body</option>`
761
+ : ""
762
+ }
763
+ ${
764
+ !receiveTokenInOptions || receiveTokenInOptions.includes("header")
765
+ ? html`<option value="header" .selected=${receiveTokenIn === "header"}>Authorization Header</option>`
766
+ : ""
767
+ }
768
+ </select>`
769
+ : ""
770
+ }
771
+ <button
772
+ class="btn btn-outline"
773
+ part="btn btn-outline"
774
+ @click="${(e) => {
775
+ onInvokeOAuthFlow.call(
776
+ this,
777
+ securitySchemeId,
778
+ flowName,
779
+ authorizationUrl,
780
+ tokenUrl,
781
+ e,
782
+ authorizeParams,
783
+ tokenParams,
784
+ );
785
+ }}"
786
+ >
787
+ GET TOKEN
788
+ </button>
789
+ </div>
790
+ <div class="oauth-resp-display red-text small-font-size"></div>`
791
+ : ""
792
+ }
793
+ </div>
794
+ `;
795
+ }
796
+
797
+ function removeApiKey(securitySchemeId) {
798
+ const securityObj = this.resolvedSpec.securitySchemes?.find(
799
+ (v) => v.securitySchemeId === securitySchemeId,
800
+ );
801
+ securityObj.user = "";
802
+ securityObj.password = "";
803
+ securityObj.value = "";
804
+ securityObj.finalKeyValue = "";
805
+ securityObj.hostNote = "";
806
+ if (this.persistAuth === "true") {
807
+ const rapidocLs = getPersistedApiKeys.call(this);
808
+ delete rapidocLs[securityObj.securitySchemeId];
809
+ setPersistedApiKeys.call(this, rapidocLs);
810
+ }
811
+ this.requestUpdate();
812
+ }
813
+
814
+ export default function securitySchemeTemplate(allowTry = "true") {
815
+ if (!this.resolvedSpec) {
816
+ return "";
817
+ }
818
+
819
+ const providedApiKeys = this.resolvedSpec.securitySchemes?.filter((v) => v.finalKeyValue);
820
+ if (!providedApiKeys) {
821
+ return;
822
+ }
823
+ return html`
824
+ <section
825
+ id="auth"
826
+ part="section-auth"
827
+ style="text-align:left; direction:ltr; margin-top:24px; margin-bottom:24px;"
828
+ class="observe-me ${"read focused".includes(this.renderStyle) ? "section-gap--read-mode" : "section-gap "}"
829
+ >
830
+ <h2 class="rapidoc-heading sub-title regular-font" part="section-auth-title heading heading-2">Authentication</h2>
831
+ ${
832
+ allowTry === "true"
833
+ ? html`<div class="auth-status small-font-size">
834
+ ${
835
+ providedApiKeys.length > 0
836
+ ? html`<div class="blue-text">${providedApiKeys.length} API key applied</div>
837
+ <button
838
+ class="btn btn-outline"
839
+ part="btn btn-outline"
840
+ @click=${() => {
841
+ onClearAllApiKeys.call(this);
842
+ }}
843
+ >
844
+ CLEAR ALL API KEYS
845
+ </button>`
846
+ : html`<div class="red-text">No API key applied</div>`
847
+ }
848
+ </div>`
849
+ : ""
850
+ }
851
+ ${
852
+ this.resolvedSpec.securitySchemes && this.resolvedSpec.securitySchemes.length > 0
853
+ ? html` <ul class="auth-list" id="auth-list" part="auth-list" role="list">
854
+ ${this.resolvedSpec.securitySchemes
855
+ .filter((v) => v.type)
856
+ .map((v) => {
857
+ // A key the host page applied with a note (e.g. the reader's sign-in): the note replaces the
858
+ // scheme's form, which stays one click away
859
+ const hostNote = allowTry === "true" && v.finalKeyValue ? v.hostNote : "";
860
+ const folded = hostNote && !v.hostFormOpen;
861
+ return html`
862
+ <li id="security-scheme-${v.securitySchemeId}" class="auth-scheme ${v.type.toLowerCase()}" part="auth-scheme">
863
+ <div class="auth-scheme-body">
864
+ <div class="auth-scheme-header">
865
+ <span class="auth-scheme-title">${v.typeDisplay}</span>
866
+ ${
867
+ v.finalKeyValue
868
+ ? html`<span class="blue-text">Key Applied</span>
869
+ <button
870
+ class="btn btn-outline btn-sm"
871
+ part="btn btn-outline"
872
+ @click=${() => {
873
+ removeApiKey.call(this, v.securitySchemeId);
874
+ }}
875
+ >
876
+ REMOVE
877
+ </button>`
878
+ : ""
879
+ }
880
+ </div>
881
+ ${v.description ? html`<div class="m-markdown">${unsafeHTML(sanitizeHTML(marked(v.description || "")))}</div>` : ""}
882
+ ${
883
+ hostNote
884
+ ? html`<div class="auth-host-note" part="auth-host-note">${hostNote}</div>
885
+ <button
886
+ class="btn btn-link auth-host-toggle"
887
+ part="btn btn-link"
888
+ aria-expanded="${folded ? "false" : "true"}"
889
+ @click=${() => {
890
+ v.hostFormOpen = !v.hostFormOpen;
891
+ this.requestUpdate();
892
+ }}
893
+ >
894
+ ${folded ? "Use a different token" : "Hide token form"}
895
+ </button>`
896
+ : ""
897
+ }
898
+ ${
899
+ !folded && v.type.toLowerCase() === "apikey"
900
+ ? html`<div class="auth-hint">Send <code>${v.name}</code> in <code>${v.in}</code></div>
901
+ ${
902
+ allowTry === "true"
903
+ ? v.in !== "cookie"
904
+ ? html`<div class="auth-controls">
905
+ <input
906
+ type="text"
907
+ value="${v.value}"
908
+ class="${v.type} ${v.securitySchemeId} api-key-input"
909
+ placeholder="api-token"
910
+ spellcheck="false"
911
+ id="${v.type}-${v.securitySchemeId}-api-key-input"
912
+ />
913
+ <button
914
+ class="btn btn-outline"
915
+ part="btn btn-outline"
916
+ @click="${() => {
917
+ onApiKeyChange.call(this, v.securitySchemeId);
918
+ }}"
919
+ >
920
+ ${v.finalKeyValue ? "UPDATE" : "SET"}
921
+ </button>
922
+ </div>`
923
+ : html`<div class="auth-hint gray-text">Cookies cannot be set from here</div>`
924
+ : ""
925
+ }`
926
+ : ""
927
+ }
928
+ ${
929
+ !folded &&
930
+ v.type.toLowerCase() === "http" &&
931
+ v.scheme?.toLowerCase() === "basic"
932
+ ? html`<div class="auth-hint">
933
+ Send <code>Authorization</code> in <code>header</code> containing the word <code>Basic</code> followed by
934
+ a space and a base64 encoded string of <code>username:password</code>.
935
+ </div>
936
+ ${
937
+ allowTry === "true"
938
+ ? html`<div class="auth-controls">
939
+ <input
940
+ type="text"
941
+ value="${v.user}"
942
+ placeholder="username"
943
+ spellcheck="false"
944
+ class="${v.type} ${v.securitySchemeId} api-key-user"
945
+ id="input-${v.type}-${v.securitySchemeId}-api-key-user"
946
+ />
947
+ <input
948
+ type="password"
949
+ value="${v.password}"
950
+ placeholder="password"
951
+ spellcheck="false"
952
+ class="${v.type} ${v.securitySchemeId} api-key-password"
953
+ id="input-${v.type}-${v.securitySchemeId}-api-key-password"
954
+ />
955
+ <button
956
+ class="btn btn-outline"
957
+ @click="${() => {
958
+ onApiKeyChange.call(this, v.securitySchemeId);
959
+ }}"
960
+ part="btn btn-outline"
961
+ >
962
+ ${v.finalKeyValue ? "UPDATE" : "SET"}
963
+ </button>
964
+ </div>`
965
+ : ""
966
+ }`
967
+ : ""
968
+ }
969
+ ${
970
+ !folded &&
971
+ v.type.toLowerCase() === "http" &&
972
+ v.scheme?.toLowerCase() === "bearer"
973
+ ? html`<div class="auth-hint">
974
+ Send <code>Authorization</code> in <code>header</code> containing the word <code>Bearer</code> followed by
975
+ a space and token value
976
+ </div>
977
+ ${
978
+ allowTry === "true"
979
+ ? html`<div class="auth-controls">
980
+ <input
981
+ type="text"
982
+ value="${v.value}"
983
+ class="${v.type} ${v.securitySchemeId} api-key-input"
984
+ placeholder="api-token"
985
+ spellcheck="false"
986
+ id="${v.type}-${v.securitySchemeId}-api-key-input"
987
+ />
988
+ <button
989
+ class="btn btn-outline"
990
+ part="btn btn-outline"
991
+ @click="${() => {
992
+ onApiKeyChange.call(this, v.securitySchemeId);
993
+ }}"
994
+ >
995
+ ${v.finalKeyValue ? "UPDATE" : "SET"}
996
+ </button>
997
+ </div>`
998
+ : ""
999
+ }`
1000
+ : ""
1001
+ }
1002
+ </div>
1003
+ ${
1004
+ !folded && v.type.toLowerCase() === "oauth2"
1005
+ ? html`<div class="auth-flows" part="auth-flows">
1006
+ ${Object.keys(v.flows).map((f) =>
1007
+ oAuthFlowTemplate.call(
1008
+ this,
1009
+ f,
1010
+ v.flows[f]["x-client-id"] || v["x-client-id"] || "",
1011
+ v.flows[f]["x-client-secret"] || v["x-client-secret"] || "",
1012
+ v.securitySchemeId,
1013
+ v.flows[f],
1014
+ v.flows[f]["x-default-scopes"] || v["x-default-scopes"],
1015
+ v.flows[f]["x-receive-token-in"] || v["x-receive-token-in"],
1016
+ v.flows[f]["x-receive-token-in-options"] ||
1017
+ v["x-receive-token-in-options"],
1018
+ allowTry,
1019
+ v.flows[f]["x-authorize-params"] ||
1020
+ v["x-authorize-params"] ||
1021
+ null,
1022
+ v.flows[f]["x-token-params"] || v["x-token-params"] || null,
1023
+ ),
1024
+ )}
1025
+ </div>`
1026
+ : ""
1027
+ }
1028
+ </li>
1029
+ `;
1030
+ })}
1031
+ </ul>`
1032
+ : ""
1033
+ }
1034
+ <slot name="auth"></slot>
1035
+ </section>
1036
+ `;
1037
+ }
1038
+
1039
+ function renderSecuritySchemeDetail(andSecurityItem, isMultiple, j) {
1040
+ const scopeHtml = andSecurityItem.scopes
1041
+ ? html` <div>
1042
+ <b>Required scopes:</b><br />
1043
+ <div style="margin-left:8px">
1044
+ ${andSecurityItem.scopes.split(",").map((scope, cnt) => html`${cnt === 0 ? "" : "┃"}<span>${scope.trim()}</span>`)}
1045
+ </div>
1046
+ </div>`
1047
+ : "";
1048
+
1049
+ if (andSecurityItem.type === "oauth2") {
1050
+ return html` <div>
1051
+ ${isMultiple ? html`<b>${j + 1}.</b> &nbsp;` : "Needs "}OAuth Token
1052
+ <span style="font-family:var(--font-mono); color:var(--primary);">${andSecurityItem.securitySchemeId}</span>
1053
+ in <b>Authorization header</b>
1054
+ ${scopeHtml}
1055
+ </div>`;
1056
+ }
1057
+
1058
+ if (andSecurityItem.type === "http") {
1059
+ return html` <div>
1060
+ ${isMultiple ? html`<b>${j + 1}.</b> &nbsp;` : "Requires "}
1061
+ ${andSecurityItem.scheme === "basic" ? "Base 64 encoded username:password" : html`Bearer Token <b>${andSecurityItem.nameId}</b>`} in
1062
+ <b>Authorization header</b>
1063
+ ${scopeHtml}
1064
+ </div>`;
1065
+ }
1066
+
1067
+ return html` <div>
1068
+ ${isMultiple ? html`<b>${j + 1}.</b> &nbsp;` : "Requires "} Token in <b>${andSecurityItem.name} ${andSecurityItem.in}</b>
1069
+ ${scopeHtml}
1070
+ </div>`;
1071
+ }
1072
+
1073
+ /*
1074
+ * Operation security chips: each chip shows its scheme details in a popover="hint" panel (a manual popover
1075
+ * where "hint" is unsupported) on hover or keyboard focus. The panel renders in the top layer, so it is never
1076
+ * clipped by scroll containers, and is anchored to its chip with CSS anchor positioning (JS fallback below).
1077
+ */
1078
+ const supportsAnchorPositioning = typeof CSS !== "undefined" && CSS.supports?.("anchor-name: --a");
1079
+ const securityHideTimers = new WeakMap();
1080
+ let openSecurityPopover = null;
1081
+
1082
+ function onSecurityPopoverKeydown(e) {
1083
+ if (e.key === "Escape" && openSecurityPopover) {
1084
+ hideSecurityPopover(openSecurityPopover);
1085
+ }
1086
+ }
1087
+
1088
+ function hideSecurityPopover(popover) {
1089
+ if (popover.matches(":popover-open")) {
1090
+ popover.hidePopover();
1091
+ }
1092
+ if (openSecurityPopover === popover) {
1093
+ openSecurityPopover = null;
1094
+ document.removeEventListener("keydown", onSecurityPopoverKeydown);
1095
+ }
1096
+ }
1097
+
1098
+ // Fallback without CSS anchor positioning: places the panel below the chip (above when there is no room),
1099
+ // left-aligned with it and kept inside the viewport
1100
+ function placeSecurityPopover(chip, popover) {
1101
+ const gap = 6;
1102
+ const edge = 8;
1103
+ const r = chip.getBoundingClientRect();
1104
+ const { offsetWidth: w, offsetHeight: h } = popover;
1105
+ const left = Math.max(edge, Math.min(r.left, window.innerWidth - w - edge));
1106
+ const top =
1107
+ r.bottom + gap + h > window.innerHeight && r.top - gap - h >= 0
1108
+ ? r.top - gap - h
1109
+ : r.bottom + gap;
1110
+ Object.assign(popover.style, { inset: "auto", margin: "0", left: `${left}px`, top: `${top}px` });
1111
+ }
1112
+
1113
+ function showSecurityPopover(e) {
1114
+ const chip = e.currentTarget.closest(".security-chip");
1115
+ const popover = chip?.querySelector(".security-popover");
1116
+ if (!popover || typeof popover.showPopover !== "function") {
1117
+ return;
1118
+ }
1119
+ clearTimeout(securityHideTimers.get(chip));
1120
+ if (openSecurityPopover && openSecurityPopover !== popover) {
1121
+ hideSecurityPopover(openSecurityPopover);
1122
+ }
1123
+ if (!popover.matches(":popover-open")) {
1124
+ popover.showPopover();
1125
+ }
1126
+ if (!supportsAnchorPositioning) {
1127
+ placeSecurityPopover(chip, popover);
1128
+ }
1129
+ if (openSecurityPopover !== popover) {
1130
+ openSecurityPopover = popover;
1131
+ document.addEventListener("keydown", onSecurityPopoverKeydown);
1132
+ }
1133
+ }
1134
+
1135
+ // Delayed so the pointer can cross the gap into the panel; the panel stays open while the chip is hovered or focused
1136
+ function scheduleHideSecurityPopover(e) {
1137
+ const chip = e.currentTarget;
1138
+ clearTimeout(securityHideTimers.get(chip));
1139
+ securityHideTimers.set(
1140
+ chip,
1141
+ setTimeout(() => {
1142
+ const popover = chip.querySelector(".security-popover");
1143
+ if (popover && !chip.matches(":hover, :focus-within")) {
1144
+ hideSecurityPopover(popover);
1145
+ }
1146
+ }, 150),
1147
+ );
1148
+ }
1149
+
1150
+ const lockIcon = html`<svg class="security-lock" aria-hidden="true" viewBox="0 0 24 24" fill="none" stroke-width="1.75">
1151
+ <rect x="5" y="11" width="14" height="10" rx="2" />
1152
+ <path d="M8 11V7a4 4 0 0 1 8 0v4" />
1153
+ </svg>`;
1154
+
1155
+ export function pathSecurityTemplate(pathSecurity, elementId = "") {
1156
+ if (
1157
+ !this.resolvedSpec.securitySchemes ||
1158
+ !Array.isArray(pathSecurity) ||
1159
+ pathSecurity.length === 0
1160
+ ) {
1161
+ return "";
1162
+ }
1163
+
1164
+ const isOptional = pathSecurity.some((p) => !p || Object.keys(p).length === 0);
1165
+ const activeSecurity = pathSecurity.filter((p) => p && Object.keys(p).length > 0);
1166
+
1167
+ if (activeSecurity.length === 0) {
1168
+ return "";
1169
+ }
1170
+
1171
+ const orSecurityKeys = [];
1172
+ activeSecurity.forEach((pSecurity) => {
1173
+ const andSecurityKeys = [];
1174
+ const andKeyTypes = [];
1175
+ Object.keys(pSecurity).forEach((pathSecurityKey) => {
1176
+ let pathScopes = "";
1177
+ const s = this.resolvedSpec.securitySchemes.find(
1178
+ (ss) => ss.securitySchemeId === pathSecurityKey,
1179
+ );
1180
+ if (pSecurity[pathSecurityKey] && Array.isArray(pSecurity[pathSecurityKey])) {
1181
+ pathScopes = pSecurity[pathSecurityKey].join(", ");
1182
+ }
1183
+ if (s) {
1184
+ andKeyTypes.push(s.typeDisplay);
1185
+ andSecurityKeys.push({ ...s, scopes: pathScopes });
1186
+ }
1187
+ });
1188
+
1189
+ const displayType =
1190
+ andKeyTypes.length > 1
1191
+ ? `${andKeyTypes[0]} + ${andKeyTypes.length - 1} more`
1192
+ : andKeyTypes[0];
1193
+ orSecurityKeys.push({
1194
+ securityTypes: isOptional ? `${displayType} (Optional)` : displayType,
1195
+ securityDefs: andSecurityKeys,
1196
+ });
1197
+ });
1198
+
1199
+ const idPrefix = `${elementId || "operation"}--security`;
1200
+ const anchorPrefix = `--${idPrefix.replace(/[^\w-]/g, "_")}`;
1201
+ const isLink = this.updateRoute === "true" && this.allowAuthentication === "true";
1202
+
1203
+ return html`
1204
+ <div class="operation-security" part="section-operation-security" role="group" aria-labelledby="${idPrefix}-label">
1205
+ <span id="${idPrefix}-label" class="security-label visually-hidden" part="label-operation-security">Authorization</span>
1206
+ ${
1207
+ isLink
1208
+ ? html`<a
1209
+ class="security-lock-btn"
1210
+ part="anchor anchor-operation-security"
1211
+ href="#auth"
1212
+ title="Go to Authentication"
1213
+ aria-label="Authorization: go to Authentication section"
1214
+ >${lockIcon}</a
1215
+ >`
1216
+ : html`<span class="security-lock-btn">${lockIcon}</span>`
1217
+ }
1218
+ <ul class="value-list-items security-chips" role="list">
1219
+ ${orSecurityKeys.map((orItem, i) => {
1220
+ if (!orItem.securityTypes) {
1221
+ return "";
1222
+ }
1223
+ const isMultiple = orItem.securityDefs.length > 1;
1224
+ const hasDetails = orItem.securityDefs.length > 0;
1225
+ const popoverId = `${idPrefix}-${i}`;
1226
+ const anchorName = `${anchorPrefix}-${i}`;
1227
+ // Chips only open their details popover; the lock before them links to the Authentication section
1228
+ const labelHtml = hasDetails
1229
+ ? html`<button type="button" class="security-chip-label" aria-describedby="${popoverId}" @click="${showSecurityPopover}">
1230
+ ${orItem.securityTypes}
1231
+ </button>`
1232
+ : html`<span class="security-chip-label">${orItem.securityTypes}</span>`;
1233
+
1234
+ return html`
1235
+ <li>
1236
+ <span
1237
+ class="security-chip ${hasDetails ? "has-details" : ""}"
1238
+ part="operation-security-chip"
1239
+ style="anchor-name:${anchorName}"
1240
+ @mouseenter="${showSecurityPopover}"
1241
+ @mouseleave="${scheduleHideSecurityPopover}"
1242
+ @focusin="${showSecurityPopover}"
1243
+ @focusout="${scheduleHideSecurityPopover}"
1244
+ >
1245
+ ${labelHtml}
1246
+ ${
1247
+ hasDetails
1248
+ ? html`
1249
+ <div
1250
+ id="${popoverId}"
1251
+ class="security-popover"
1252
+ part="operation-security-details"
1253
+ popover="hint"
1254
+ role="tooltip"
1255
+ style="position-anchor:${anchorName}"
1256
+ >
1257
+ ${isMultiple ? html`<div>Requires <b>all</b> of the following:</div>` : ""}
1258
+ <div style="padding-left:${isMultiple ? "8px" : "0"};">
1259
+ ${orItem.securityDefs.map((secDef, j) => renderSecuritySchemeDetail(secDef, isMultiple, j))}
1260
+ </div>
1261
+ </div>
1262
+ `
1263
+ : ""
1264
+ }
1265
+ </span>
1266
+ </li>
1267
+ `;
1268
+ })}
1269
+ </ul>
1270
+ </div>
1271
+ `;
1272
+ }