(function () {
"use strict";
const SPEC_URL = window.__DOXA_API_SPEC_URL__ || "doxaApi.json";
let apis = [];
let activeApiIndex = 0;
let spec = null;
let activeEndpoint = null;
let activeTryTab = "body";
let activeCodeLang = "curl";
let mockMode = localStorage.getItem("DoxaApi.mockMode") === "1";
let collapsedGroups = new Set(JSON.parse(localStorage.getItem("DoxaApi.collapsed") || "[]"));
// schemeId -> { value } | { username, password } | { token }
let authCredentials = JSON.parse(localStorage.getItem("DoxaApi.auth") || "{}");
// Array of { id, method, path, status, elapsedMs, time, request, response }
let requestHistory = JSON.parse(localStorage.getItem("DoxaApi.history") || "[]");
const el = {
nav: document.getElementById("navContent"),
detail: document.getElementById("detailContent"),
try: document.getElementById("tryContent"),
search: document.getElementById("searchInput"),
themeToggle: document.getElementById("themeToggle"),
brandTitle: document.getElementById("brandTitle"),
brandVersion: document.getElementById("brandVersion"),
apiSelector: document.getElementById("apiSelector"),
modalRoot: document.getElementById("modalRoot"),
};
function persistAuth() {
localStorage.setItem("DoxaApi.auth", JSON.stringify(authCredentials));
updateAuthBadge();
}
function persistHistory() {
if (requestHistory.length > 30) requestHistory = requestHistory.slice(0, 30);
localStorage.setItem("DoxaApi.history", JSON.stringify(requestHistory));
}
function updateAuthBadge() {
const dot = document.getElementById("authBadgeDot");
if (!dot) return;
const hasAny = Object.keys(authCredentials).some((k) => credentialIsSet(authCredentials[k]));
dot.style.display = hasAny ? "block" : "none";
}
function credentialIsSet(cred) {
if (!cred) return false;
return !!(cred.value || cred.token || (cred.username && cred.password));
}
function refreshApiSelector() {
if (!el.apiSelector) return;
el.apiSelector.innerHTML = apis.map((a, i) => `${(a.info && a.info.title) || ('API ' + (i + 1))} `).join('');
el.apiSelector.value = String(activeApiIndex);
}
async function init() {
renderSkeleton();
try {
const res = await fetch(SPEC_URL);
if (!res.ok) throw new Error("HTTP " + res.status);
const importedSpec = await res.json();
const extend = apis.length > 0 && window.confirm("Extend the currently selected API?\n\nOK = Extend Current API\nCancel = Import As New API (default)");
if (extend) {
spec.groups = [...(spec.groups || []), ...(importedSpec.groups || [])];
spec.schemas = Object.assign(spec.schemas || {}, importedSpec.schemas || {});
spec.securitySchemes = Object.assign(spec.securitySchemes || {}, importedSpec.securitySchemes || {});
} else {
apis.push(importedSpec);
activeApiIndex = apis.length - 1;
spec = importedSpec;
}
refreshApiSelector();
if (spec.info && spec.info.title) el.brandTitle.textContent = spec.info.title;
if (spec.info && spec.info.version) el.brandVersion.textContent = spec.info.version;
} catch (err) {
el.nav.innerHTML = `
Couldn't load doxaApi.json${escapeHtml(String(err))}
`;
return;
}
if (spec.info && spec.info.title) el.brandTitle.textContent = spec.info.title;
if (spec.info && spec.info.version) el.brandVersion.textContent = spec.info.version;
document.title = (spec.info && spec.info.title) || "API Documentation";
renderNav("");
renderOverview();
renderTryEmpty();
bindGlobalEvents();
updateAuthBadge();
}
function renderSkeleton() {
el.nav.innerHTML = Array.from({ length: 6 })
.map(() => `
`)
.join("");
}
function totalEndpointCount() {
return (spec.groups || []).reduce((n, g) => n + g.endpoints.length, 0);
}
function totalSchemaCount() {
return Object.keys(spec.schemas || {}).length;
}
function mockServerAvailable() {
return !!(spec.features && spec.features.mockServer);
}
function consoleAvailable() {
return !!(spec.features && spec.features.console);
}
// Nav rendering
function renderNav(filterText) {
const term = filterText.trim().toLowerCase();
const groups = spec.groups || [];
let html = `
Overview
`;
if (groups.length === 0) {
html += `No endpoints found.
`;
el.nav.innerHTML = html;
return;
}
let totalMatches = 0;
let groupHtml = "";
for (const group of groups) {
const endpoints = group.endpoints.filter((e) => matchesFilter(e, term));
if (term && endpoints.length === 0) continue;
totalMatches += endpoints.length;
const isCollapsed = collapsedGroups.has(group.name) && !term;
groupHtml += `
${endpoints.map((e) => navEndpointHtml(group, e)).join("")}
`;
}
if (term && totalMatches === 0) {
html += `No endpoints match "${escapeHtml(filterText)}"
`;
} else {
html += groupHtml;
}
el.nav.innerHTML = html;
}
function navEndpointHtml(group, endpoint) {
const isActive =
activeEndpoint &&
activeEndpoint.endpoint.operationId === endpoint.operationId;
return `
${endpoint.method}
${escapeHtml(endpoint.path)}
`;
}
function matchesFilter(endpoint, term) {
if (!term) return true;
return (
endpoint.path.toLowerCase().includes(term) ||
(endpoint.summary || "").toLowerCase().includes(term) ||
endpoint.method.toLowerCase().includes(term)
);
}
// Overview / welcome screen
function renderOverview() {
const info = spec.info || {};
const groups = spec.groups || [];
let html = ``;
html += `
`;
html += `
`;
html += `
${escapeHtml(info.title || "API Documentation")} `;
if (info.description) {
html += `
${escapeHtml(info.description)}
`;
}
html += `
`;
html += `
`; // hero
html += `
`;
html += `
`;
html += `
${totalEndpointCount()}
Endpoints
`;
html += `
${totalSchemaCount()}
Schemas
`;
html += `
${Object.keys(spec.securitySchemes || {}).length}
Auth schemes
`;
html += `
`;
if (spec.securitySchemes && Object.keys(spec.securitySchemes).length > 0) {
html += `
Authentication `;
html += `
`;
for (const [id, scheme] of Object.entries(spec.securitySchemes)) {
html += `
${escapeHtml(authSchemeLabel(scheme))}
`;
}
html += `
`;
}
html += `
Browse by group `;
for (const group of groups) {
html += `
`;
for (const ep of group.endpoints) {
html += `
${ep.method}
${escapeHtml(ep.path)}
${escapeHtml(ep.summary || "")}
`;
}
html += `
`;
}
html += `
`;
el.detail.innerHTML = html;
el.detail.scrollTop = 0;
}
function renderTryEmpty() {
el.try.innerHTML = `
Pick an endpoint to send a live request.
`;
}
function selectEndpoint(group, endpoint) {
activeEndpoint = { group, endpoint };
activeTryTab = "body";
document.querySelectorAll(".nav-endpoint").forEach((b) => {
b.classList.toggle("active", b.dataset.op === endpoint.operationId);
});
const overviewLink = document.querySelector(".nav-overview-link");
if (overviewLink) overviewLink.classList.remove("active");
renderDetail(group, endpoint);
renderTry(group, endpoint);
}
function renderDetail(group, endpoint) {
const methodColorVar = `var(--m-${endpoint.method.toLowerCase()}, var(--m-default))`;
let html = ``;
html += ``;
if (endpoint.parameters && endpoint.parameters.length > 0) {
html += `
Parameters ${endpoint.parameters.length} `;
html += `
Name Located in Type Description `;
for (const p of endpoint.parameters) {
html += `
${escapeHtml(p.name)}${p.required ? '* ' : ""}
${p.in}
${schemaTypeLabel(p.schema)}
${escapeHtml(p.description || "-")}
`;
}
html += `
`;
}
if (endpoint.requestBody) {
html += `
Request body `;
html += `
${renderSchemaTree(endpoint.requestBody.schema, 0)}
`;
}
if (endpoint.responses && endpoint.responses.length > 0) {
html += `
Responses ${endpoint.responses.length} `;
for (const r of endpoint.responses) {
const cls = r.statusCode[0] === "2" ? "status-2xx" : r.statusCode[0] === "4" ? "status-4xx" : "status-5xx";
html += `
`;
if (r.schema) {
html += `
${renderSchemaTree(r.schema, 0)}
`;
}
html += `
`;
}
html += `
`;
}
html += `
`;
el.detail.innerHTML = html;
el.detail.scrollTop = 0;
const copyBtn = document.getElementById("copyRouteBtn");
if (copyBtn) {
copyBtn.addEventListener("click", () => {
navigator.clipboard.writeText(endpoint.path).then(() => flashIcon(copyBtn));
});
}
}
function flashIcon(btn) {
const original = btn.innerHTML;
btn.innerHTML = ` `;
setTimeout(() => (btn.innerHTML = original), 1100);
}
function authSchemeLabel(scheme) {
if (!scheme) return "Auth";
if (scheme.type === "apiKey") return `API key (${scheme.name || "key"})`;
if (scheme.type === "http" && scheme.scheme === "bearer") return "Bearer token";
if (scheme.type === "http" && scheme.scheme === "basic") return "Basic auth";
if (scheme.type === "oauth2") return "OAuth 2.0";
return scheme.id || "Auth";
}
function schemaTypeLabel(schema) {
if (!schema) return "any";
if (schema.refName) return schema.refName;
if (schema.type === "array") return schemaTypeLabel(schema.items) + "[]";
if (schema.type === "enum") return "enum";
return schema.format ? `${schema.type} (${schema.format})` : schema.type;
}
function renderSchemaTree(schema, depth) {
if (!schema) return ``;
const indent = " ".repeat(depth);
if (schema.refName && spec.schemas && spec.schemas[schema.refName] && depth < 6) {
const resolved = spec.schemas[schema.refName];
return renderSchemaTree({ ...resolved, refName: undefined }, depth);
}
if (schema.type === "object" && schema.properties) {
const required = new Set(schema.required || []);
let lines = [`{ `];
const entries = Object.entries(schema.properties);
entries.forEach(([key, propSchema], i) => {
const isReq = required.has(key);
const comma = i < entries.length - 1 ? "," : "";
const nullableMark = propSchema && propSchema.nullable ? '? ' : "";
lines.push(
`${indent} ${escapeHtml(key)} ${isReq ? '* ' : ""}${nullableMark}: ${renderInlineType(propSchema, depth + 1)}${comma} `
);
});
lines.push(`${indent}} `);
return lines.join("\n");
}
if (schema.type === "array") {
return `[ \n${indent} ${renderInlineType(schema.items, depth + 1)}\n${indent}] `;
}
if (schema.type === "enum") {
return `enum `;
}
return `${schema.type}${schema.format ? " (" + schema.format + ")" : ""} `;
}
function renderInlineType(schema, depth) {
if (!schema) return ``;
if (schema.refName) {
if (spec.schemas && spec.schemas[schema.refName] && depth < 6) {
return renderSchemaTree({ ...spec.schemas[schema.refName] }, depth);
}
return `${escapeHtml(schema.refName)} `;
}
if (schema.type === "object" && schema.properties) return renderSchemaTree(schema, depth);
if (schema.type === "array") {
return `Array< ${renderInlineType(schema.items, depth)}> `;
}
if (schema.type === "enum") {
return ``;
}
return `${schema.type}${schema.format ? " (" + schema.format + ")" : ""} `;
}
// Try-it-out pane
function renderTry(group, endpoint) {
const methodColorVar = `var(--m-${endpoint.method.toLowerCase()}, var(--m-default))`;
let html = ``;
html += ``;
html += `
Request
Code
History
`;
html += `
`;
if (endpoint.security && endpoint.security.length > 0) {
const satisfied = endpoint.security.some((id) => credentialIsSet(authCredentials[id]));
html += `
${satisfied ? "Credentials will be sent with this request" : "This endpoint requires authorization"}
${satisfied ? "Edit" : "Set up"}
`;
}
if (mockServerAvailable()) {
html += `
Send to mock server (fake response, no backend needed)
`;
}
const pathParams = endpoint.parameters.filter((p) => p.in === "path");
const queryParams = endpoint.parameters.filter((p) => p.in === "query");
const headerParams = endpoint.parameters.filter((p) => p.in === "header");
if (pathParams.length) {
html += `
Path parameters
`;
for (const p of pathParams) {
html += `
`;
}
html += `
`;
}
if (queryParams.length) {
html += `
Query parameters
`;
for (const p of queryParams) {
html += `
`;
}
html += `
`;
}
if (headerParams.length) {
html += `
Headers
`;
for (const p of headerParams) {
html += `
`;
}
html += `
`;
}
if (endpoint.requestBody) {
const example = endpoint.requestBody.example || generateExampleJson(endpoint.requestBody.schema, 0);
html += `
`;
}
html += `
Send request
`;
html += `
`;
html += `
`; // tryTabBody
html += `
`;
html += renderCodeTab(endpoint);
html += `
`;
html += `
`;
html += renderHistoryTab(endpoint);
html += `
`;
html += `
`;
el.try.innerHTML = html;
document.getElementById("sendBtn").addEventListener("click", () => sendTryRequest(endpoint));
const mockToggle = document.getElementById("mockModeToggle");
if (mockToggle) {
mockToggle.addEventListener("change", () => {
mockMode = mockToggle.checked;
localStorage.setItem("DoxaApi.mockMode", mockMode ? "1" : "0");
});
}
const openAuthBtn = document.getElementById("openAuthFromTry");
if (openAuthBtn) {
openAuthBtn.addEventListener("click", () => openAuthModal());
}
el.try.querySelectorAll("[data-try-tab]").forEach((btn) => {
btn.addEventListener("click", () => {
activeTryTab = btn.dataset.tryTab;
renderTry(group, endpoint);
});
});
bindCodeTabEvents(endpoint);
bindHistoryTabEvents(endpoint);
}
function renderCodeTab(endpoint) {
const langs = [
{ id: "curl", label: "cURL" },
{ id: "js", label: "JavaScript" },
{ id: "python", label: "Python" },
{ id: "csharp", label: "C#" },
{ id: "go", label: "Go" },
];
let html = ``;
for (const lang of langs) {
html += `${lang.label} `;
}
html += `
`;
html += ``;
html += `${buildCodeSnippet(endpoint, activeCodeLang)}
`;
return html;
}
function bindCodeTabEvents(endpoint) {
const root = document.getElementById("tryTabCode");
if (!root) return;
root.querySelectorAll("[data-code-lang]").forEach((btn) => {
btn.addEventListener("click", () => {
activeCodeLang = btn.dataset.codeLang;
document.getElementById("tryTabCode").innerHTML = renderCodeTab(endpoint);
bindCodeTabEvents(endpoint);
});
});
const copyBtn = document.getElementById("copyCodeBtn");
if (copyBtn) {
copyBtn.addEventListener("click", () => {
const text = document.getElementById("codeSnippet").textContent;
navigator.clipboard.writeText(text).then(() => {
const original = copyBtn.innerHTML;
copyBtn.textContent = "Copied";
setTimeout(() => (copyBtn.innerHTML = original), 1100);
});
});
}
}
function renderHistoryTab(endpoint) {
const items = requestHistory.filter((h) => h.operationId === endpoint.operationId);
if (items.length === 0) {
return `No requests sent yet for this endpoint. Responses you send will appear here.
`;
}
let html = "";
for (const item of items) {
const cls = item.status && item.status < 300 ? "status-2xx" : item.status && item.status < 500 ? "status-4xx" : "status-5xx";
html += `
${item.status || "ERR"}
${escapeHtml(item.time)}
${escapeHtml(item.method)} ${escapeHtml(item.url)}
`;
}
return html;
}
function bindHistoryTabEvents(endpoint) {
const root = document.getElementById("tryTabHistory");
if (!root) return;
root.querySelectorAll("[data-history-id]").forEach((node) => {
node.addEventListener("click", () => {
const item = requestHistory.find((h) => h.id === node.dataset.historyId);
if (!item) return;
activeTryTab = "body";
renderTry({ name: "" }, endpoint);
showResponsePanelFromHistory(item);
});
});
}
function showResponsePanelFromHistory(item) {
const panel = document.getElementById("responsePanel");
if (!panel || !item.response) return;
renderResponsePanel(panel, item.response);
}
// Builds a language-agnostic description of the request: url, headers, body.
// Shared by every code-snippet generator and by the auth-aware "Send" flow.
function buildRequestPlan(endpoint) {
const base = (spec.servers && spec.servers[0]) || "";
const path = endpoint.path;
const headers = { Accept: "application/json" };
if (endpoint.requestBody) {
headers["Content-Type"] = endpoint.requestBody.contentType || "application/json";
}
applyAuthHeaders(endpoint, headers);
const body = endpoint.requestBody
? (endpoint.requestBody.example || generateExampleJson(endpoint.requestBody.schema, 0))
: null;
return { url: base + path, method: endpoint.method, headers, body };
}
// Mutates `headers` (and returns query/string additions where relevant) based on
// whichever security schemes apply to the endpoint and have stored credentials.
function applyAuthHeaders(endpoint, headers, queryParamsOut) {
if (!endpoint.security || endpoint.security.length === 0) return;
for (const schemeId of endpoint.security) {
const scheme = (spec.securitySchemes || {})[schemeId];
const cred = authCredentials[schemeId];
if (!scheme || !credentialIsSet(cred)) continue;
if (scheme.type === "apiKey") {
const headerName = scheme.name || "X-API-Key";
if ((scheme.in || "header") === "header") {
headers[headerName] = cred.value;
} else if (scheme.in === "query" && queryParamsOut) {
queryParamsOut[headerName] = cred.value;
}
} else if (scheme.type === "http" && scheme.scheme === "bearer") {
headers["Authorization"] = `Bearer ${cred.token}`;
} else if (scheme.type === "http" && scheme.scheme === "basic") {
const encoded = typeof btoa === "function" ? btoa(`${cred.username}:${cred.password}`) : "";
headers["Authorization"] = `Basic ${encoded}`;
} else if (scheme.type === "oauth2") {
headers["Authorization"] = `Bearer ${cred.token}`;
}
// First matching satisfied scheme wins (schemes are an OR requirement).
break;
}
}
function buildCodeSnippet(endpoint, lang) {
const plan = buildRequestPlan(endpoint);
switch (lang) {
case "js": return buildJsSnippet(plan);
case "python": return buildPythonSnippet(plan);
case "csharp": return buildCSharpSnippet(plan);
case "go": return buildGoSnippet(plan);
default: return buildCurlSnippet(plan);
}
}
function buildCurlSnippet(plan) {
const lines = [];
lines.push(`curl -X ${plan.method} \\`);
lines.push(` "${escapeHtml(plan.url)}" \\`);
const headerEntries = Object.entries(plan.headers);
headerEntries.forEach(([k, v], i) => {
const isLast = i === headerEntries.length - 1 && !plan.body;
lines.push(` -H "${escapeHtml(k)}: ${escapeHtml(v)}"${isLast ? "" : " \\"}`);
});
if (plan.body) {
lines.push(` -d '${escapeHtml(plan.body)}' `);
}
return lines.join("\n");
}
function buildJsSnippet(plan) {
const headersObj = JSON.stringify(plan.headers, null, 2).replace(/\n/g, "\n ");
const lines = [
`const response = await fetch("${plan.url}", {`,
` method: "${plan.method}",`,
` headers: ${headersObj},`,
];
if (plan.body) {
lines.push(` body: ${JSON.stringify(plan.body)},`);
}
lines.push(`});`);
lines.push(`const data = await response.json();`);
lines.push(`console.log(data);`);
return escapeHtml(lines.join("\n"));
}
function buildPythonSnippet(plan) {
const lines = [`import requests`, ``, `response = requests.request(`, ` "${plan.method}",`, ` "${plan.url}",`];
const headerLines = Object.entries(plan.headers).map(([k, v]) => ` "${k}": "${v}",`);
lines.push(` headers={`);
lines.push(...headerLines);
lines.push(` },`);
if (plan.body) {
lines.push(` data="""${plan.body}""",`);
}
lines.push(`)`);
lines.push(``);
lines.push(`print(response.status_code, response.json())`);
return escapeHtml(lines.join("\n"));
}
function buildCSharpSnippet(plan) {
const lines = [
`using var client = new HttpClient();`,
`using var request = new HttpRequestMessage(HttpMethod.${capitalize(plan.method.toLowerCase())}, "${plan.url}");`,
];
for (const [k, v] of Object.entries(plan.headers)) {
if (k.toLowerCase() === "content-type") continue;
lines.push(`request.Headers.TryAddWithoutValidation("${k}", "${v}");`);
}
if (plan.body) {
const contentType = plan.headers["Content-Type"] || "application/json";
lines.push(`request.Content = new StringContent(${JSON.stringify(plan.body)}, System.Text.Encoding.UTF8, "${contentType}");`);
}
lines.push(`using var response = await client.SendAsync(request);`);
lines.push(`var body = await response.Content.ReadAsStringAsync();`);
lines.push(`Console.WriteLine($"{(int)response.StatusCode}: {body}");`);
return escapeHtml(lines.join("\n"));
}
function buildGoSnippet(plan) {
const lines = [
`package main`,
``,
`import (`,
`\t"fmt"`,
`\t"io"`,
`\t"net/http"`,
`\t"strings"`,
`)`,
``,
`func main() {`,
];
if (plan.body) {
lines.push(`\tbody := strings.NewReader(\`${plan.body.replace(/`/g, "'")}\`)`);
lines.push(`\treq, _ := http.NewRequest("${plan.method}", "${plan.url}", body)`);
} else {
lines.push(`\treq, _ := http.NewRequest("${plan.method}", "${plan.url}", nil)`);
}
for (const [k, v] of Object.entries(plan.headers)) {
lines.push(`\treq.Header.Set("${k}", "${v}")`);
}
lines.push(`\tresp, err := http.DefaultClient.Do(req)`);
lines.push(`\tif err != nil {`);
lines.push(`\t\tpanic(err)`);
lines.push(`\t}`);
lines.push(`\tdefer resp.Body.Close()`);
lines.push(`\tdata, _ := io.ReadAll(resp.Body)`);
lines.push(`\tfmt.Println(resp.StatusCode, string(data))`);
lines.push(`}`);
return escapeHtml(lines.join("\n"));
}
function capitalize(s) {
return s.charAt(0).toUpperCase() + s.slice(1);
}
function generateExampleJson(schema, depth) {
const value = generateExampleValue(schema, depth, new Set());
return JSON.stringify(value, null, 2);
}
function generateExampleValue(schema, depth, seen) {
if (!schema || depth > 6) return null;
if (schema.refName) {
if (seen.has(schema.refName)) return {};
const resolved = spec.schemas && spec.schemas[schema.refName];
if (!resolved) return {};
const nextSeen = new Set(seen);
nextSeen.add(schema.refName);
return generateExampleValue(resolved, depth + 1, nextSeen);
}
switch (schema.type) {
case "string":
if (schema.format === "date-time") return new Date().toISOString();
if (schema.format === "uuid") return "00000000-0000-0000-0000-000000000000";
return "string";
case "integer":
return 0;
case "number":
return 0;
case "boolean":
return true;
case "enum":
return (schema.enumValues && schema.enumValues[0]) || "string";
case "array":
return [generateExampleValue(schema.items, depth + 1, seen)];
case "object": {
if (!schema.properties) return {};
const obj = {};
for (const [key, propSchema] of Object.entries(schema.properties)) {
obj[key] = generateExampleValue(propSchema, depth + 1, seen);
}
return obj;
}
default:
return null;
}
}
async function sendTryRequest(endpoint) {
const btn = document.getElementById("sendBtn");
const label = document.getElementById("sendBtnLabel");
const panel = document.getElementById("responsePanel");
let path = endpoint.path;
document.querySelectorAll('[data-param-in="path"]').forEach((input) => {
const name = input.dataset.paramName;
path = path.replace(`{${name}}`, encodeURIComponent(input.value || ""));
});
const isMock = mockMode && mockServerAvailable();
const url = isMock
? new URL("mock", window.location.href)
: new URL(path, window.location.origin);
if (isMock) {
url.searchParams.set("operationId", endpoint.operationId);
}
document.querySelectorAll('[data-param-in="query"]').forEach((input) => {
if (input.value) url.searchParams.set(input.dataset.paramName, input.value);
});
const headers = { Accept: "application/json" };
document.querySelectorAll('[data-param-in="header"]').forEach((input) => {
if (input.value) headers[input.dataset.paramName] = input.value;
});
let body = undefined;
if (endpoint.requestBody) {
headers["Content-Type"] = endpoint.requestBody.contentType || "application/json";
const bodyEl = document.getElementById("tryBody");
body = bodyEl ? bodyEl.value : undefined;
}
const authQueryParams = {};
if (!isMock) {
applyAuthHeaders(endpoint, headers, authQueryParams);
for (const [k, v] of Object.entries(authQueryParams)) {
url.searchParams.set(k, v);
}
}
btn.disabled = true;
label.textContent = "Sending…";
btn.querySelector("svg").style.display = "none";
btn.insertBefore(spinnerEl(), btn.firstChild);
const startTime = performance.now();
let resultForHistory = null;
try {
const res = await fetch(url.toString(), {
method: isMock ? "GET" : endpoint.method,
headers: isMock ? { Accept: "application/json" } : headers,
body: isMock || endpoint.method === "GET" || endpoint.method === "HEAD" ? undefined : body,
});
const elapsedMs = Math.round(performance.now() - startTime);
const contentType = res.headers.get("content-type") || "";
let bodyText;
let isJson = false;
if (contentType.includes("application/json")) {
try {
const json = await res.json();
bodyText = JSON.stringify(json, null, 2);
isJson = true;
} catch {
bodyText = await res.text();
}
} else {
bodyText = await res.text();
}
resultForHistory = {
status: res.status,
ok: res.ok,
elapsedMs,
body: bodyText,
isJson,
isMock,
};
renderResponsePanel(panel, resultForHistory);
} catch (err) {
const elapsedMs = Math.round(performance.now() - startTime);
resultForHistory = {
status: null,
ok: false,
elapsedMs,
body: String(err && err.message ? err.message : err),
isJson: false,
networkError: true,
};
renderResponsePanel(panel, resultForHistory);
} finally {
btn.disabled = false;
label.textContent = "Send request";
const spinner = btn.querySelector(".spinner");
if (spinner) spinner.remove();
btn.querySelector("svg").style.display = "";
requestHistory.unshift({
id: "h" + Date.now() + Math.random().toString(36).slice(2, 7),
operationId: endpoint.operationId,
method: isMock ? "GET (mock)" : endpoint.method,
url: url.toString(),
status: resultForHistory ? resultForHistory.status : null,
time: new Date().toLocaleTimeString(),
response: resultForHistory,
});
persistHistory();
}
}
function spinnerEl() {
const s = document.createElement("span");
s.className = "spinner";
return s;
}
function renderResponsePanel(panel, result) {
const statusClass = result.networkError
? "status-5xx"
: result.status < 300
? "status-2xx"
: result.status < 500
? "status-4xx"
: "status-5xx";
const statusLabel = result.networkError ? "Network error" : result.status;
panel.innerHTML = `
${result.isJson ? syntaxHighlightJson(result.body) : escapeHtml(result.body)
}
`;
document.getElementById("copyResponseBtn").addEventListener("click", () => {
navigator.clipboard.writeText(result.body).then(() => {
const btn = document.getElementById("copyResponseBtn");
btn.lastChild.textContent = "Copied";
setTimeout(() => (btn.lastChild.textContent = "Copy"), 1200);
});
});
}
function syntaxHighlightJson(json) {
const escaped = escapeHtml(json);
return escaped.replace(
/("(\\u[a-zA-Z0-9]{4}|\\[^u]|[^\\"])*"(\s*:)?|\b(true|false)\b|\bnull\b|-?\d+(\.\d+)?([eE][+-]?\d+)?)/g,
(match) => {
let cls = "json-number";
if (/^"/.test(match)) {
cls = /:$/.test(match) ? "json-key" : "json-string";
} else if (/true|false/.test(match)) {
cls = "json-boolean";
} else if (/null/.test(match)) {
cls = "json-null";
}
return `${match} `;
}
);
}
// ---------------------------------------------------------------
// Modal system - lightweight, dependency-free, used for Auth,
// Console, and Diff. Only one modal is shown at a time.
// ---------------------------------------------------------------
function closeModal() {
if (!el.modalRoot) return;
el.modalRoot.innerHTML = "";
}
function openModal(title, bodyHtml, { wide } = {}) {
if (!el.modalRoot) return null;
el.modalRoot.innerHTML = `
`;
const backdrop = document.getElementById("modalBackdrop");
document.getElementById("modalCloseBtn").addEventListener("click", closeModal);
backdrop.addEventListener("click", (e) => {
if (e.target === backdrop) closeModal();
});
const escHandler = (e) => {
if (e.key === "Escape") {
closeModal();
document.removeEventListener("keydown", escHandler);
}
};
document.addEventListener("keydown", escHandler);
return document.getElementById("modalBody");
}
// ---------------------------------------------------------------
// Authorization modal - one card per security scheme declared by
// the API, with scheme-appropriate inputs (API key / bearer token /
// basic credentials / OAuth2 client credentials). Values are kept
// in localStorage so they survive reloads, and are applied to every
// "Try it" request, cURL snippet, and generated code sample whose
// endpoint references that scheme.
// ---------------------------------------------------------------
function openAuthModal() {
const schemes = (spec && spec.securitySchemes) || {};
const ids = Object.keys(schemes);
let body;
if (ids.length === 0) {
body = `This API doesn't declare any security schemes. Configure them via DoxaApiOptions.AddBearerAuth() and similar helpers.
`;
} else {
body = ids.map((id) => authSchemeCardHtml(id, schemes[id])).join("");
}
const modalBody = openModal("Authorization", body, {});
if (!modalBody || ids.length === 0) return;
for (const id of ids) {
bindAuthSchemeCard(modalBody, id, schemes[id]);
}
}
function authSchemeCardHtml(id, scheme) {
const cred = authCredentials[id] || {};
const isSet = credentialIsSet(cred);
let fieldsHtml = "";
if (scheme.type === "apiKey") {
fieldsHtml = ``;
} else if (scheme.type === "http" && scheme.scheme === "basic") {
fieldsHtml = `
`;
} else if (scheme.type === "oauth2") {
const flow = scheme.flows && (scheme.flows.clientCredentials || scheme.flows.authorizationCode);
fieldsHtml = `
${flow && flow.tokenUrl ? `Token URL: ${escapeHtml(flow.tokenUrl)}
` : ""}`;
} else {
// http bearer (default) and any unrecognized http-style scheme
fieldsHtml = ``;
}
return `
${escapeHtml(authSchemeLabel(scheme))}
${escapeHtml(scheme.type)}
${scheme.description ? `
${escapeHtml(scheme.description)}
` : ""}
${fieldsHtml}
Clear credentials
`;
}
function bindAuthSchemeCard(root, id, scheme) {
const card = root.querySelector(`[data-scheme-id="${cssEscape(id)}"]`);
if (!card) return;
const persistFromInputs = () => {
const cred = {};
card.querySelectorAll("[data-auth-field]").forEach((input) => {
cred[input.dataset.authField] = input.value;
});
authCredentials[id] = cred;
persistAuth();
const statusDot = card.querySelector(".auth-scheme-status");
if (statusDot) statusDot.classList.toggle("set", credentialIsSet(cred));
};
card.querySelectorAll("[data-auth-field]").forEach((input) => {
input.addEventListener("input", persistFromInputs);
});
const clearBtn = card.querySelector("[data-auth-clear]");
if (clearBtn) {
clearBtn.addEventListener("click", () => {
delete authCredentials[id];
persistAuth();
card.querySelectorAll("[data-auth-field]").forEach((input) => (input.value = ""));
const statusDot = card.querySelector(".auth-scheme-status");
if (statusDot) statusDot.classList.remove("set");
});
}
}
function cssEscape(s) {
return String(s).replace(/["\\]/g, "\\$&");
}
// ---------------------------------------------------------------
// Console - a free-form HTTP request runner that isn't tied to any
// documented operation. Requests are sent server-side via the
// /{prefix}/console endpoint, which avoids CORS restrictions when
// pointing at a different host than the one serving the docs.
// ---------------------------------------------------------------
function openConsoleModal() {
const base = (spec.servers && spec.servers[0]) || "";
const body = `
${["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"].map((m) => `${m} `).join("")}
Headers (one per line, "Name: value")
Send request
`;
const modalBody = openModal("API Console", body, { wide: true });
if (!modalBody) return;
document.getElementById("consoleSendBtn").addEventListener("click", sendConsoleRequest);
}
function parseHeaderLines(text) {
const headers = {};
(text || "").split("\n").forEach((line) => {
const idx = line.indexOf(":");
if (idx === -1) return;
const key = line.slice(0, idx).trim();
const value = line.slice(idx + 1).trim();
if (key) headers[key] = value;
});
return headers;
}
async function sendConsoleRequest() {
const btn = document.getElementById("consoleSendBtn");
const label = document.getElementById("consoleSendLabel");
const panel = document.getElementById("consoleResponsePanel");
const method = document.getElementById("consoleMethod").value;
const url = document.getElementById("consoleUrl").value.trim();
const headers = parseHeaderLines(document.getElementById("consoleHeaders").value);
const body = document.getElementById("consoleBody").value;
if (!url) {
panel.innerHTML = `Enter a URL to send a request.
`;
return;
}
btn.disabled = true;
label.textContent = "Sending…";
const startTime = performance.now();
try {
const res = await fetch("console", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ method, url, headers, body: body || undefined }),
});
const payload = await res.json();
const elapsedMs = payload.elapsedMs || Math.round(performance.now() - startTime);
let isJson = false;
let bodyText = payload.body || "";
try {
JSON.parse(bodyText);
isJson = true;
} catch {
isJson = false;
}
renderResponsePanel(panel, {
status: payload.statusCode,
ok: payload.statusCode >= 200 && payload.statusCode < 300,
elapsedMs,
body: isJson ? JSON.stringify(JSON.parse(bodyText), null, 2) : bodyText,
isJson,
networkError: !!payload.networkError,
});
} catch (err) {
renderResponsePanel(panel, {
status: null,
ok: false,
elapsedMs: Math.round(performance.now() - startTime),
body: String(err && err.message ? err.message : err),
isJson: false,
networkError: true,
});
} finally {
btn.disabled = false;
label.textContent = "Send request";
}
}
// ---------------------------------------------------------------
// Diff - compares the currently loaded spec against an uploaded
// OpenAPI / Swagger / DoxaApi JSON file and reports added, removed,
// and changed operations - useful for spotting breaking changes
// before shipping a new API version.
// ---------------------------------------------------------------
function computeSpecDiff(oldSpec, newSpec) {
const key = (ep) => `${ep.method} ${ep.path}`;
const oldOps = new Map();
const newOps = new Map();
for (const g of oldSpec.groups || []) for (const e of g.endpoints) oldOps.set(key(e), e);
for (const g of newSpec.groups || []) for (const e of g.endpoints) newOps.set(key(e), e);
const rows = [];
for (const [k, ep] of newOps) {
if (!oldOps.has(k)) rows.push({ tag: "added", key: k, summary: ep.summary });
}
for (const [k, ep] of oldOps) {
if (!newOps.has(k)) rows.push({ tag: "removed", key: k, summary: ep.summary });
}
for (const [k, oldEp] of oldOps) {
const newEp = newOps.get(k);
if (!newEp) continue;
const changes = [];
if ((oldEp.requestBody == null) !== (newEp.requestBody == null)) changes.push("request body");
if (JSON.stringify((oldEp.parameters || []).map((p) => p.name).sort()) !==
JSON.stringify((newEp.parameters || []).map((p) => p.name).sort())) changes.push("parameters");
if (JSON.stringify((oldEp.security || []).slice().sort()) !==
JSON.stringify((newEp.security || []).slice().sort())) changes.push("security");
if (!!oldEp.deprecated !== !!newEp.deprecated) changes.push("deprecation status");
if (changes.length > 0) {
rows.push({ tag: "changed", key: k, summary: changes.join(", ") });
}
}
return rows.sort((a, b) => a.key.localeCompare(b.key));
}
function openDiffModal(otherSpec) {
const rows = computeSpecDiff(spec, otherSpec);
const added = rows.filter((r) => r.tag === "added").length;
const removed = rows.filter((r) => r.tag === "removed").length;
const changed = rows.filter((r) => r.tag === "changed").length;
let body = `
+${added} endpoints added
-${removed} endpoints removed
~${changed} endpoints changed
`;
if (rows.length === 0) {
body += `No differences found between the current API and the uploaded spec.
`;
} else {
body += rows.map((r) => `
${r.tag}
${escapeHtml(r.key)} ${r.summary ? ` - ${escapeHtml(r.summary)}` : ""}
`).join("");
}
openModal("Compare specs", body, { wide: true });
}
// Global events
function bindGlobalEvents() {
const importBtn = document.getElementById("importBtn");
const importFile = document.getElementById("importFile");
const exportDoxaApiBtn = document.getElementById("exportDoxaApiBtn");
const exportOpenApiBtn = document.getElementById("exportOpenApiBtn");
const exportSwaggerBtn = document.getElementById("exportSwaggerBtn");
const dashboardBtn = document.getElementById("dashboardBtn");
const closeDashboard = document.getElementById("closeDashboard");
const dashboardPanel = document.getElementById("dashboardPanel");
importBtn?.addEventListener("click", () => importFile.click());
importFile?.addEventListener("change", async (e) => {
const file = e.target.files?.[0];
if (!file) return;
const text = await file.text();
const res = await fetch("import", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: text
});
if (!res.ok) {
alert(await res.text());
return;
}
const importedSpec = await res.json();
const extend = apis.length > 0 && window.confirm("Extend the currently selected API?\n\nOK = Extend Current API\nCancel = Import As New API (default)");
if (extend) {
spec.groups = [...(spec.groups || []), ...(importedSpec.groups || [])];
spec.schemas = Object.assign(spec.schemas || {}, importedSpec.schemas || {});
spec.securitySchemes = Object.assign(spec.securitySchemes || {}, importedSpec.securitySchemes || {});
} else {
apis.push(importedSpec);
activeApiIndex = apis.length - 1;
spec = importedSpec;
}
refreshApiSelector();
if (spec.info && spec.info.title) el.brandTitle.textContent = spec.info.title;
if (spec.info && spec.info.version) el.brandVersion.textContent = spec.info.version;
activeEndpoint = null;
renderNav("");
renderOverview();
renderTryEmpty();
});
function download(url, filename) {
const a = document.createElement("a");
a.href = url;
a.download = filename;
document.body.appendChild(a);
a.click();
a.remove();
}
exportDoxaApiBtn?.addEventListener("click", () => download("doxaApi.json", "DoxaApi.json"));
exportOpenApiBtn?.addEventListener("click", () => download("openapi.json", "openapi.json"));
exportSwaggerBtn?.addEventListener("click", () => download("swagger.json", "swagger.json"));
// Dashboard functionality
dashboardBtn?.addEventListener("click", () => openDashboard());
closeDashboard?.addEventListener("click", () => closeDashboardPanel());
const authBtn = document.getElementById("authBtn");
authBtn?.addEventListener("click", () => openAuthModal());
const consoleBtn = document.getElementById("consoleBtn");
if (consoleBtn) {
if (!consoleAvailable()) {
consoleBtn.style.display = "none";
} else {
consoleBtn.addEventListener("click", () => openConsoleModal());
}
}
const diffFile = document.getElementById("diffFile");
const compareBtn = document.getElementById("compareBtn");
compareBtn?.addEventListener("click", () => diffFile?.click());
diffFile?.addEventListener("change", async (e) => {
const file = e.target.files?.[0];
if (!file) return;
try {
const text = await file.text();
const parsed = JSON.parse(text);
const normalized = parsed.groups
? parsed
: await (async () => {
const res = await fetch("import", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: text
});
if (!res.ok) throw new Error(await res.text());
return res.json();
})();
openDiffModal(normalized);
} catch (err) {
alert("Couldn't compare spec: " + (err && err.message ? err.message : err));
} finally {
diffFile.value = "";
}
});
el.nav.addEventListener("click", (e) => {
const overviewBtn = e.target.closest("[data-overview]");
if (overviewBtn) {
activeEndpoint = null;
document.querySelectorAll(".nav-endpoint").forEach((b) => b.classList.remove("active"));
overviewBtn.classList.add("active");
renderOverview();
renderTryEmpty();
return;
}
const groupToggle = e.target.closest("[data-toggle-group]");
if (groupToggle) {
const name = groupToggle.dataset.toggleGroup;
if (collapsedGroups.has(name)) collapsedGroups.delete(name);
else collapsedGroups.add(name);
localStorage.setItem("DoxaApi.collapsed", JSON.stringify([...collapsedGroups]));
renderNav(el.search.value);
return;
}
const endpointBtn = e.target.closest("[data-op]");
if (endpointBtn) {
const opId = endpointBtn.dataset.op;
for (const group of spec.groups) {
const endpoint = group.endpoints.find((ep) => ep.operationId === opId);
if (endpoint) {
selectEndpoint(group, endpoint);
break;
}
}
}
});
el.detail.addEventListener("click", (e) => {
const authBadge = e.target.closest("[data-overview-auth]");
if (authBadge) {
openAuthModal();
return;
}
const row = e.target.closest("[data-op]");
if (!row) return;
const opId = row.dataset.op;
for (const group of spec.groups) {
const endpoint = group.endpoints.find((ep) => ep.operationId === opId);
if (endpoint) {
selectEndpoint(group, endpoint);
break;
}
}
});
el.search.addEventListener("input", () => renderNav(el.search.value));
document.addEventListener("keydown", (e) => {
if (e.key === "/" && document.activeElement !== el.search) {
e.preventDefault();
el.search.focus();
}
if (e.key === "Escape" && document.activeElement === el.search) {
el.search.blur();
}
});
el.themeToggle.addEventListener("click", () => {
const root = document.documentElement;
const current = root.getAttribute("data-theme");
const isDark =
current === "dark" || (current === "auto" && window.matchMedia("(prefers-color-scheme: dark)").matches);
const next = isDark ? "light" : "dark";
root.setAttribute("data-theme", next);
localStorage.setItem("DoxaApi.theme", next);
});
const savedTheme = localStorage.getItem("DoxaApi.theme");
if (savedTheme) document.documentElement.setAttribute("data-theme", savedTheme);
}
// Helpers
function escapeHtml(str) {
return String(str ?? "").replace(/[&<>"']/g, (c) => ({
"&": "&",
"<": "<",
">": ">",
'"': """,
"'": "'",
}[c]));
}
function escapeAttr(str) {
return escapeHtml(str);
}
// Dashboard functions
async function openDashboard() {
const dashboardPanel = document.getElementById("dashboardPanel");
const dashboard = document.querySelector(".dashboard-panel");
if (!dashboardPanel) return;
dashboardPanel.style.display = "block";
try {
const res = await fetch("analytics");
if (!res.ok) throw new Error("Failed to load analytics");
const analyticsData = await res.json();
updateDashboard(analyticsData);
// Refresh every 5 seconds
if (window.dashboardInterval) clearInterval(window.dashboardInterval);
window.dashboardInterval = setInterval(async () => {
const res = await fetch("analytics");
if (res.ok) {
const data = await res.json();
updateDashboard(data);
}
}, 5000);
} catch (err) {
console.error("Dashboard load error:", err);
}
}
function closeDashboardPanel() {
const dashboardPanel = document.getElementById("dashboardPanel");
if (dashboardPanel) dashboardPanel.style.display = "none";
if (window.dashboardInterval) {
clearInterval(window.dashboardInterval);
window.dashboardInterval = null;
}
}
function updateDashboard(data) {
document.getElementById("totalEndpoints").textContent = data.totalEndpoints || "0";
document.getElementById("totalRequests").textContent = data.totalRequests || "0";
document.getElementById("totalErrors").textContent = data.totalErrors || "0";
document.getElementById("avgResponse").textContent = Math.round((data.averageResponseTime || 0)) + "ms";
// Update top endpoints
const topEndpointsList = (data.topEndpoints || []).map(ep => `
${ep.method}
${ep.path}
${ep.requestCount} req
`).join("") || "No data yet
";
document.getElementById("topEndpointsChart").innerHTML = `
Top Endpoints
${topEndpointsList}
`;
// Update recent requests
const recentRequestsList = (data.recentRequests || []).map(req => {
const statusClass = req.statusCode >= 400 ? "request-status-error" : "request-status-success";
return `
${req.method}
${req.path}
${req.statusCode}
${req.responseTimeMs}ms
`;
}).join("") || "No requests yet
";
document.getElementById("recentRequestsTable").innerHTML = `
Recent Requests
${recentRequestsList}
`;
}
init();
})();