"use strict"; (() => { const element = id => document.getElementById(id); let session = null, spec = null, ui = null, translations = {}, fallback = {}, epoch = 0; let documents = [], docsByPath = new Map(), currentDocument = null, activeTab = "reader"; let channel = null; try { channel = new BroadcastChannel("rustproxy-admin-session"); } catch { } const t = key => translations[key] ?? fallback[key] ?? key; function docsConfirm(message) { return new Promise(resolve => { const dialog = document.createElement("dialog"); dialog.className = "docs-confirm"; const title = document.createElement("h2"); title.textContent = t("confirmTitle"); const body = document.createElement("p"); body.textContent = message; const actions = document.createElement("div"); actions.className = "docs-confirm-actions"; const cancel = document.createElement("button"); cancel.type = "button"; cancel.textContent = t("cancelAction"); const accept = document.createElement("button"); accept.type = "button"; accept.className = "docs-confirm-primary"; accept.textContent = t("continueAction"); actions.append(cancel, accept); dialog.append(title, body, actions); document.body.append(dialog); let settled = false; const finish = value => { if (settled) return; settled = true; if (dialog.open) dialog.close(); dialog.remove(); resolve(value); }; cancel.addEventListener("click", () => finish(false)); accept.addEventListener("click", () => finish(true)); dialog.addEventListener("cancel", event => { event.preventDefault(); finish(false); }); dialog.addEventListener("close", () => finish(false)); dialog.showModal(); cancel.focus(); }); } function clear() { epoch++; session = null; spec = null; ui = null; documents = []; docsByPath = new Map(); currentDocument = null; element("docs-writes").checked = false; element("swagger-ui").replaceChildren(); element("docs-reader-list").replaceChildren(); element("docs-reader-content").replaceChildren(); element("docs-session").textContent = ""; } function expired() { clear(); element("docs-error").replaceChildren(); const link = document.createElement("a"); link.href = "/?next=docs"; link.textContent = t("docsExpired"); element("docs-error").append(link); } channel?.addEventListener("message", event => { if (event.data === "locked") expired(); }); async function get(path) { const response = await fetch(path, { credentials: "same-origin", cache: "no-store", redirect: "error" }); if (!response.ok) { if (response.status === 401) expired(); throw new Error(t("docsError")); } return response.json(); } async function checkSession() { const mine = epoch; const info = await get("/api/auth/session"); if (mine !== epoch || !session || info.csrf_token !== session.csrf_token || info.username !== session.username) { expired(); throw new Error(t("docsExpired")); } return info; } async function requestInterceptor(request) { if (!session || !spec) throw new Error(t("docsExpired")); const url = new URL(request.url, location.origin), method = (request.method || "GET").toLowerCase(); if (url.origin !== location.origin || url.username || url.password || !Object.hasOwn(spec.paths, url.pathname) || !Object.hasOwn(spec.paths[url.pathname], method)) throw new Error("Only documented same-origin operations are allowed."); if (["/api/auth/login", "/api/auth/setup", "/api/events"].includes(url.pathname)) throw new Error(t("docsReadOnly")); await checkSession(); request.credentials = "same-origin"; request.headers = { ...request.headers, "X-RustProxy-Request": "1" }; request.cache = "no-store"; request.redirect = "error"; if (!["get", "head"].includes(method)) { if (!element("docs-writes").checked || !(await docsConfirm(t("docsWriteConfirm")))) throw new Error(t("docsReadOnly")); request.headers["X-CSRF-Token"] = session.csrf_token; } return request; } function renderApi() { if (!session || !spec || typeof SwaggerUIBundle !== "function") return; const displaySpec = structuredClone(spec); for (const path of Object.values(displaySpec.paths)) for (const [method, operation] of Object.entries(path)) { if (["get", "post", "put", "delete", "patch", "head", "options"].includes(method) && operation.parameters) operation.parameters = operation.parameters.filter(p => !(p.in === "header" && ["origin", "x-rustproxy-request"].includes(p.name.toLowerCase()))); } ui = SwaggerUIBundle({ spec: displaySpec, dom_id: "#swagger-ui", layout: "BaseLayout", presets: [SwaggerUIBundle.presets.apis], validatorUrl: null, queryConfigEnabled: false, persistAuthorization: false, withCredentials: true, supportedSubmitMethods: element("docs-writes").checked ? ["get", "post", "put"] : ["get"], docExpansion: "none", filter: true, displayRequestDuration: true, tryItOutEnabled: false, showMutatedRequest: false, requestInterceptor, responseInterceptor: response => { if (response.status === 401) expired(); return response; } }); } function normalizeDocPath(base, target) { const clean = target.split("#", 1)[0].split("?", 1)[0]; if (!clean) return base; const stack = clean.startsWith("/") ? [] : base.split("/").slice(0, -1); for (const part of clean.replace(/^\//, "").split("/")) { if (!part || part === ".") continue; if (part === "..") { if (stack.length) stack.pop(); continue; } stack.push(part); } return stack.join("/"); } function appendInline(parent, text, basePath) { const pattern = /(`[^`]+`|\[[^\]]+\]\([^)]+\))/g; let offset = 0; for (const match of text.matchAll(pattern)) { if (match.index > offset) parent.append(document.createTextNode(text.slice(offset, match.index))); const token = match[0]; if (token.startsWith("`")) { const code = document.createElement("code"); code.textContent = token.slice(1, -1); parent.append(code); } else { const parsed = /^\[([^\]]+)\]\(([^)]+)\)$/.exec(token); const anchor = document.createElement("a"); anchor.textContent = parsed[1]; const target = parsed[2].trim(); if (/^https?:\/\//i.test(target)) { anchor.href = target; anchor.target = "_blank"; anchor.rel = "noreferrer"; } else if (target.startsWith("#")) { anchor.href = target; } else { const resolved = normalizeDocPath(basePath, target); if (docsByPath.has(resolved)) { anchor.href = `#doc=${encodeURIComponent(resolved)}`; anchor.addEventListener("click", event => { event.preventDefault(); selectDocument(resolved, true); }); } else { anchor.href = target; } } parent.append(anchor); } offset = match.index + token.length; } if (offset < text.length) parent.append(document.createTextNode(text.slice(offset))); } function renderMarkdown(documentInfo) { const target = element("docs-reader-content"); target.replaceChildren(); const lines = documentInfo.content.replaceAll("\r\n", "\n").split("\n"); let code = null, list = null, paragraph = []; const flushParagraph = () => { if (!paragraph.length) return; const p = document.createElement("p"); appendInline(p, paragraph.join(" ").trim(), documentInfo.path); target.append(p); paragraph = []; }; const closeList = () => { list = null; }; for (const line of lines) { if (line.startsWith("```")) { flushParagraph(); closeList(); if (code) { target.append(code.pre); code = null; } else { const pre = document.createElement("pre"), node = document.createElement("code"); pre.append(node); code = { pre, node, lines: [] }; } continue; } if (code) { code.lines.push(line); code.node.textContent = code.lines.join("\n"); continue; } const heading = /^(#{1,4})\s+(.+)$/.exec(line); if (heading) { flushParagraph(); closeList(); const h = document.createElement(`h${heading[1].length}`); appendInline(h, heading[2], documentInfo.path); target.append(h); continue; } const bullet = /^\s*[-*]\s+(.+)$/.exec(line), ordered = /^\s*\d+\.\s+(.+)$/.exec(line); if (bullet || ordered) { flushParagraph(); const tag = ordered ? "ol" : "ul"; if (!list || list.tagName.toLowerCase() !== tag) { list = document.createElement(tag); target.append(list); } const item = document.createElement("li"); appendInline(item, (bullet || ordered)[1], documentInfo.path); list.append(item); continue; } if (/^\s*>\s?/.test(line)) { flushParagraph(); closeList(); const q = document.createElement("blockquote"); appendInline(q, line.replace(/^\s*>\s?/, ""), documentInfo.path); target.append(q); continue; } if (/^\s*---+\s*$/.test(line)) { flushParagraph(); closeList(); target.append(document.createElement("hr")); continue; } if (!line.trim()) { flushParagraph(); closeList(); continue; } closeList(); paragraph.push(line.trim()); } flushParagraph(); if (code) target.append(code.pre); } const categoryLabel = category => t({ project: "docsCategoryProject", guides: "docsCategoryGuides", technical: "docsCategoryTechnical", audits: "docsCategoryAudits", historical: "docsCategoryHistorical" }[category] || category); function renderReaderList() { const query = (element("docs-reader-search").value || "").trim().toLowerCase(); const root = element("docs-reader-list"); root.replaceChildren(); const matches = documents.filter(item => !query || `${item.title} ${item.path}`.toLowerCase().includes(query)); for (const category of ["project", "guides", "technical", "audits", "historical"]) { const group = matches.filter(item => item.category === category); if (!group.length) continue; const heading = document.createElement("div"); heading.className = "reader-group"; heading.textContent = categoryLabel(category); root.append(heading); for (const item of group) { const button = document.createElement("button"); button.type = "button"; button.classList.toggle("active", item.path === currentDocument?.path); const title = document.createElement("span"); title.textContent = item.title; const path = document.createElement("small"); path.textContent = item.path; button.append(title, path); button.addEventListener("click", () => selectDocument(item.path, true)); root.append(button); } } if (!root.children.length) { const empty = document.createElement("div"); empty.className = "reader-empty"; empty.textContent = t("docsReaderEmpty"); root.append(empty); } } function selectDocument(path, updateHash = false) { const item = docsByPath.get(path) || docsByPath.get("docs/README.md") || documents[0]; if (!item) return; currentDocument = item; renderReaderList(); renderMarkdown(item); if (updateHash) history.replaceState(null, "", `#doc=${encodeURIComponent(item.path)}`); } function selectTab(tab) { activeTab = tab === "api" ? "api" : "reader"; element("docs-reader-pane").hidden = activeTab !== "reader"; element("docs-api-pane").hidden = activeTab !== "api"; element("docs-tab-reader").classList.toggle("active", activeTab === "reader"); element("docs-tab-api").classList.toggle("active", activeTab === "api"); if (activeTab === "api") renderApi(); } element("docs-writes").addEventListener("change", renderApi); element("docs-reader-search").addEventListener("input", renderReaderList); element("docs-tab-reader").addEventListener("click", () => selectTab("reader")); element("docs-tab-api").addEventListener("click", () => selectTab("api")); element("docs-logout").addEventListener("click", async () => { if (!session) return; try { await checkSession(); const response = await fetch("/api/auth/logout", { method: "POST", credentials: "same-origin", cache: "no-store", redirect: "error", headers: { "X-RustProxy-Request": "1", "X-CSRF-Token": session.csrf_token } }); if (!response.ok && response.status !== 401) throw new Error(t("docsError")); channel?.postMessage("locked"); expired(); } catch (error) { element("docs-error").textContent = error.message; } }); async function boot() { const mine = ++epoch; try { const info = await get("/api/auth/session"); if (mine !== epoch) return; const [documentSpec, bundle, docBundle, preferences] = await Promise.all([get("/docs/openapi.json"), get("/assets/langs.json"), get("/docs/content.json"), get("/api/preferences")]); if (mine !== epoch) return; const packs = bundle.languages || []; fallback = packs.find(p => p.id === bundle.default)?.translations || {}; const selected = preferences?.language || "auto", candidates = selected === "auto" ? (navigator.languages || [navigator.language]) : [selected]; const normalized = candidates.filter(Boolean).map(value => value.replaceAll("_", "-").toLowerCase()); const pack = normalized.map(value => packs.find(p => p.id.replaceAll("_", "-").toLowerCase() === value || p.meta.locale.toLowerCase() === value) || packs.find(p => p.meta.locale.toLowerCase().split("-")[0] === value.split("-")[0])).find(Boolean) || packs.find(p => p.id === bundle.default); translations = pack?.translations || {}; window.document.documentElement.lang = pack?.meta.locale || "und"; window.document.documentElement.dir = pack?.meta.direction || "ltr"; window.document.querySelectorAll("[data-i18n]").forEach(n => n.textContent = t(n.dataset.i18n)); window.document.querySelectorAll("[data-i18n-placeholder]").forEach(n => n.placeholder = t(n.dataset.i18nPlaceholder)); session = info; spec = documentSpec; documents = Array.isArray(docBundle.documents) ? docBundle.documents : []; docsByPath = new Map(documents.map(item => [item.path, item])); element("docs-session").textContent = `${t("docsAccount")}: ${session.username}`; element("docs-error").textContent = ""; const hash = new URLSearchParams(location.hash.replace(/^#/, "")).get("doc"); selectDocument(hash && docsByPath.has(hash) ? hash : "docs/README.md"); selectTab("reader"); } catch (error) { if (mine === epoch) { clear(); element("docs-error").textContent = error.message; } } } document.addEventListener("visibilitychange", () => { if (!document.hidden && session) void checkSession().catch(() => expired()); }); window.addEventListener("pageshow", event => { if (event.persisted) { clear(); void boot(); } }); setInterval(() => { if (!document.hidden && session) void checkSession().catch(() => expired()); }, 30000); void boot(); })();