Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
187 changes: 154 additions & 33 deletions src/components/ScalarApiReference.astro
Original file line number Diff line number Diff line change
Expand Up @@ -7,21 +7,143 @@ interface Props {
const {specURL, proxyURL = import.meta.env.PUBLIC_KINDE_PROXY_URL} = Astro.props;
---

<div id="api-reference" data-spec-url={specURL} data-proxy-url={proxyURL || ''}></div>
<script
id="api-reference"
data-url={specURL}
data-proxy-url={proxyURL}
data-configuration='{"theme": "deepSpace", "hideDarkModeToggle": "true", "searchHotKey": "j"}'
></script>
<script
type="module"
src="https://cdn.jsdelivr.net/npm/@scalar/[email protected]/dist/browser/standalone.min.js"
></script>

<script>
import waitForElement from "@utils/waitForElement";
<script type="module">
// Inline waitForElement function
function waitForElement(selector, callback) {
const element = document.querySelector(selector);
if (element) {
callback(element);
} else {
setTimeout(() => waitForElement(selector, callback), 100);
}
}

// Get variables from data attributes
const container = document.getElementById("api-reference");
const specURL = container?.dataset.specUrl || '';
const proxyURL = container?.dataset.proxyUrl || '';

// Configuration for Scalar API Reference
const targetServers = [
{
url: "https://{your_kinde_subdomain}.kinde.com",
variables: {
your_kinde_subdomain: {
default: "your_kinde_subdomain"
}
}
}
];

// Initialize Scalar API Reference programmatically
async function initScalar() {
if (!container) {
console.error('API reference container not found');
return;
}

// Wait for Scalar to be available (check multiple ways)
let retries = 0;
const maxRetries = 30;
let Scalar = null;

while (retries < maxRetries && !Scalar) {
// Check different ways Scalar might be exposed
if (typeof window !== 'undefined') {
Scalar = window.Scalar || window.ScalarApiReference || (window.default && window.default.Scalar);
}
if (typeof globalThis !== 'undefined' && !Scalar) {
Scalar = globalThis.Scalar || globalThis.ScalarApiReference;
}

if (!Scalar) {
await new Promise(resolve => setTimeout(resolve, 100));
retries++;
}
}

// If Scalar is still not available, use data-attribute fallback
if (!Scalar) {
console.warn('Scalar API Reference not found, using data-attribute initialization');
container.setAttribute('data-url', specURL);
if (proxyURL) {
container.setAttribute('data-proxy-url', proxyURL);
}
container.setAttribute('data-configuration', JSON.stringify({
theme: "deepSpace",
hideDarkModeToggle: true,
searchHotKey: "j",
servers: targetServers
}));
container.id = 'api-reference';
return;
}

// Create API Reference instance with configuration
const config = {
target: container,
url: specURL,
configuration: {
theme: "deepSpace",
hideDarkModeToggle: true,
searchHotKey: "j",
servers: targetServers
}
};

// Add proxy if provided
if (proxyURL) {
config.proxy = proxyURL;
}
Comment on lines +89 to +103
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

Verify the Scalar configuration object structure.

The current structure nests theme, hideDarkModeToggle, searchHotKey, and servers under a configuration key. However, based on common Scalar API Reference patterns, these properties are typically placed at the top level of the config object, alongside spec or url.

If this structure is incorrect, Scalar initialization may fail silently and fall back to the data-attributes approach, which could result in unexpected behavior.

Please verify the correct structure by checking the Scalar API Reference documentation:

If the configuration properties should be at the top level, apply this diff:

-    const config = {
-      target: container,
-      url: specURL,
-      configuration: {
-        theme: "deepSpace",
-        hideDarkModeToggle: true,
-        searchHotKey: "j",
-        servers: targetServers
-      }
-    };
+    const config = {
+      target: container,
+      spec: {
+        url: specURL
+      },
+      theme: "deepSpace",
+      hideDarkModeToggle: true,
+      searchHotKey: "j",
+      servers: targetServers
+    };

🌐 Web query:

Scalar API Reference createApiReference configuration object structure

💡 Result:

Top-level configuration for Scalar.createApiReference is a universal object (or array of such objects). Key structure and common properties:

  • Root (object or array of objects). If array: multiple document/configurations; first is default unless one has default: true. [1]
  • OpenAPI source:
    • url: string (absolute/relative URL to JSON/YAML). [1]
    • content: string (inline OpenAPI JSON/YAML). [1]
    • sources: array of source objects (each with url/content/title/slug/default). [1]
  • Document metadata (per config/source):
    • title, slug, default (boolean). [1]
  • Appearance / behavior:
    • customCss: string (CSS to inject). [1]
    • proxyUrl (used in examples for cross-origin/proxying). [2]
  • Authentication (authentication object / AuthenticationConfiguration) — can prefill security schemes and preferredSecurityScheme, and has entries for apiKey/httpBearer/httpBasic/oauth2 flows. [1]
  • JS API helpers:
    • createApiReference(selectorOrElement, config) returns an app with methods like mount(), updateConfiguration(newConfig), destroy(). [2]

For full, exhaustive list and examples see the Scalar configuration docs. [1][2]


Fix the Scalar configuration object structure—all options should be at the top level.

Scalar configuration accepts properties like hideDarkModeToggle, searchHotKey, servers, theme, and proxyUrl as top-level configuration object properties. The current code incorrectly nests these under a configuration key, which doesn't exist in the Scalar API.

Pass configuration directly to Scalar.createApiReference, with url as a top-level property. Apply this fix:

-    const config = {
-      target: container,
-      url: specURL,
-      configuration: {
-        theme: "deepSpace",
-        hideDarkModeToggle: true,
-        searchHotKey: "j",
-        servers: targetServers
-      }
-    };
+    const config = {
+      target: container,
+      url: specURL,
+      theme: "deepSpace",
+      hideDarkModeToggle: true,
+      searchHotKey: "j",
+      servers: targetServers
+    };
🤖 Prompt for AI Agents
In src/components/ScalarApiReference.astro around lines 56 to 70, the Scalar
config incorrectly nests options under a configuration key and uses proxy
instead of proxyUrl; Scalar expects url and options like theme,
hideDarkModeToggle, searchHotKey, servers, and proxyUrl at the top level. Move
theme, hideDarkModeToggle, searchHotKey, and servers out of the nested
configuration object into the top-level config alongside target and url, and set
proxyUrl (not proxy) when proxyURL is provided, then pass that top-level config
into Scalar.createApiReference.


// Try different API methods based on Scalar version
let apiReference;
try {
if (Scalar.createApiReference) {
apiReference = Scalar.createApiReference(config);
} else if (Scalar.ApiReference) {
apiReference = new Scalar.ApiReference(config);
} else if (Scalar.default && Scalar.default.createApiReference) {
apiReference = Scalar.default.createApiReference(config);
} else {
throw new Error('No valid Scalar API method found');
}

// Store reference for potential updates later
if (typeof window !== 'undefined') {
window.__scalarApiReference = apiReference;
}
} catch (error) {
console.error('Error initializing Scalar API Reference:', error);
// Fallback to data-attribute initialization
container.setAttribute('data-url', specURL);
if (proxyURL) {
container.setAttribute('data-proxy-url', proxyURL);
}
container.setAttribute('data-configuration', JSON.stringify({
theme: "deepSpace",
hideDarkModeToggle: true,
searchHotKey: "j",
servers: targetServers
}));
}
}

// Initialize when DOM is ready
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', initScalar);
} else {
initScalar();
}

// Wait for subdomain input to be ready, then focus and select text on focus, making it easier for users to enter their Kinde subdomain
waitForElement("#variable-subdomain", (inputElement: HTMLInputElement) => {
waitForElement("#variable-subdomain", (inputElement) => {
inputElement.addEventListener("focus", () => {
inputElement.select();
});
Expand All @@ -31,7 +153,7 @@ const {specURL, proxyURL = import.meta.env.PUBLIC_KINDE_PROXY_URL} = Astro.props
});
});

waitForElement('.authentication-content label', (element: HTMLLabelElement) => {
waitForElement('.authentication-content label', (element) => {
if (window.location.pathname.includes('management')) {
element.textContent = "M2M Token"
}
Expand All @@ -40,20 +162,20 @@ const {specURL, proxyURL = import.meta.env.PUBLIC_KINDE_PROXY_URL} = Astro.props
element.textContent = "User access token"
}

const authenticationLabel = document.querySelector('.security-scheme-label') as HTMLDivElement;
const authenticationLabel = document.querySelector('.security-scheme-label');

if (authenticationLabel) {
authenticationLabel.textContent = "Authentication";
}
})

function updateURL(urlParams: URLSearchParams) {
function updateURL(urlParams) {
// update url params
const newUrl = urlParams.toString() ? window.location.pathname + '?' + urlParams.toString() : window.location.pathname;
history.replaceState(null, '', newUrl);

// remove params from links
const links: NodeListOf<HTMLAnchorElement> = document.querySelectorAll('.sidebar-group a');
const links = document.querySelectorAll('.sidebar-group a');
links.forEach((link) => {
const url = new URL(link.href, window.location.origin);
url.searchParams.delete('token');
Expand All @@ -62,9 +184,8 @@ const {specURL, proxyURL = import.meta.env.PUBLIC_KINDE_PROXY_URL} = Astro.props
});
}

function checkSubdomain(subdomainElement: HTMLInputElement) {

const parentEl = subdomainElement.parentElement as HTMLElement;
function checkSubdomain(subdomainElement) {
const parentEl = subdomainElement.parentElement;

if (subdomainElement.value === "your_kinde_subdomain" || subdomainElement.value.trim() === "") {
parentEl.style.animation = `pulse 1s linear infinite`;
Expand All @@ -73,29 +194,25 @@ const {specURL, proxyURL = import.meta.env.PUBLIC_KINDE_PROXY_URL} = Astro.props
}
}

waitForElement(".introduction-card", (parentEl: HTMLDivElement) => {
const subdomainInputElement = parentEl.querySelector(
"input#variable-subdomain"
) as HTMLInputElement;
const tokenInputElement = parentEl.querySelector(
".authentication-content input"
) as HTMLInputElement;
waitForElement(".introduction-card", (parentEl) => {
const subdomainInputElement = parentEl.querySelector("input#variable-subdomain");
const tokenInputElement = parentEl.querySelector(".authentication-content input");
const urlParams = new URLSearchParams(window.location.search);


// Check for "token" in URL params
if (urlParams.has('token')) {
const token = urlParams.get('token');
tokenInputElement.value = token as string;
tokenInputElement.value = token;
tokenInputElement.dispatchEvent(new Event("input"));
urlParams.delete('token');
}
Comment on lines +197 to 209
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Add null check for tokenInputElement.

Line 161 uses querySelector which can return null if the authentication input doesn't exist. Lines 168-169 then access properties on tokenInputElement without checking, which will cause a null pointer exception if the element isn't found.

Apply this diff:

   waitForElement(".introduction-card", (parentEl) => {
     const subdomainInputElement = parentEl.querySelector("input#variable-subdomain");
     const tokenInputElement = parentEl.querySelector(".authentication-content input");
     const urlParams = new URLSearchParams(window.location.search);
     
 
     // Check for "token" in URL params
-    if (urlParams.has('token')) {
+    if (urlParams.has('token') && tokenInputElement) {
         const token = urlParams.get('token');
         tokenInputElement.value = token;
         tokenInputElement.dispatchEvent(new Event("input"));
         urlParams.delete('token');
     }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
waitForElement(".introduction-card", (parentEl) => {
const subdomainInputElement = parentEl.querySelector("input#variable-subdomain");
const tokenInputElement = parentEl.querySelector(".authentication-content input");
const urlParams = new URLSearchParams(window.location.search);
// Check for "token" in URL params
if (urlParams.has('token')) {
const token = urlParams.get('token');
tokenInputElement.value = token as string;
tokenInputElement.value = token;
tokenInputElement.dispatchEvent(new Event("input"));
urlParams.delete('token');
}
waitForElement(".introduction-card", (parentEl) => {
const subdomainInputElement = parentEl.querySelector("input#variable-subdomain");
const tokenInputElement = parentEl.querySelector(".authentication-content input");
const urlParams = new URLSearchParams(window.location.search);
// Check for "token" in URL params
if (urlParams.has('token') && tokenInputElement) {
const token = urlParams.get('token');
tokenInputElement.value = token;
tokenInputElement.dispatchEvent(new Event("input"));
urlParams.delete('token');
}
🤖 Prompt for AI Agents
In src/components/ScalarApiReference.astro around lines 159 to 171,
tokenInputElement is queried with querySelector() and may be null; add a null
check before accessing its .value or dispatching events. Update the code to
verify tokenInputElement exists (e.g., if (tokenInputElement) { ... }) and only
then assign tokenInputElement.value, dispatch the input event, and delete the
token param; keep behavior unchanged when the element is present and do nothing
when it's absent.


// Check for "subdomain" in URL params
if (urlParams.has('subdomain')) {
const subdomain = urlParams.get('subdomain');
sessionStorage.setItem('subdomain', subdomain as string);
subdomainInputElement.value = subdomain as string;
sessionStorage.setItem('subdomain', subdomain);
subdomainInputElement.value = subdomain;
subdomainInputElement.dispatchEvent(new Event("input"));
urlParams.delete('subdomain');
} else {
Expand All @@ -112,7 +229,7 @@ const {specURL, proxyURL = import.meta.env.PUBLIC_KINDE_PROXY_URL} = Astro.props

// Update sessionStorage on subdomain input change
subdomainInputElement.addEventListener('input', (event) => {
const subdomain = (event.target as HTMLInputElement).value;
const subdomain = event.target.value;
sessionStorage.setItem('subdomain', subdomain);
checkSubdomain(subdomainInputElement)
});
Expand All @@ -121,15 +238,19 @@ const {specURL, proxyURL = import.meta.env.PUBLIC_KINDE_PROXY_URL} = Astro.props
// Detect when api client overlay is up, so we can add a class to the body and then control body overflow, header z-index and position (from fixed to static)
const observer = new MutationObserver((mutationsList) => {
mutationsList.forEach((mutation) => {
mutation.addedNodes.forEach((node: any) => {
if (node.nodeType === 1 && node.classList.contains("api-client-drawer")) {
document.body?.classList.add("api-client-drawer-open");
mutation.addedNodes.forEach((node) => {
if (node.nodeType === 1 && node.classList && node.classList.contains("api-client-drawer")) {
if (document.body) {
document.body.classList.add("api-client-drawer-open");
}
}
});

mutation.removedNodes.forEach((node: any) => {
if (node.nodeType === 1 && node.classList.contains("api-client-drawer")) {
document.body?.classList.remove("api-client-drawer-open");
mutation.removedNodes.forEach((node) => {
if (node.nodeType === 1 && node.classList && node.classList.contains("api-client-drawer")) {
if (document.body) {
document.body.classList.remove("api-client-drawer-open");
}
}
});
});
Expand Down