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