"use strict"; (() => { const form = $("certificate-form"), file = $("certificate-file"), source = $("certificate-file-source"); const names = $("certificate-hosts"), button = $("certificate-inspect"), status = $("certificate-inspect-status"); let timer = null, sequence = 0, namesRevision = 0, resetGeneration = 0, applying = false; let manuallyEdited = !!names.value.trim(), pending = null, lastSuccess = ""; const identity = () => JSON.stringify([source.value, file.value.trim(), authEpoch]); function validFile() { const value = file.value.trim(); return source.value === "external" ? value.startsWith("/") && value.length <= 4096 && !/[\x00-\x1f\x7f]/.test(value) : value.length <= 96 && value !== "." && value !== ".." && /^[A-Za-z0-9_.-]+$/.test(value); } function message(key = "", detail = "") { if (key && !detail) status.dataset.i18n = key; else delete status.dataset.i18n; status.textContent = key ? `${t(key)}${detail ? ` ${detail}` : ""}` : ""; status.classList.toggle("status-error", !!detail); } function refresh() { button.disabled = !validFile() || !!pending; } function invalidate() { clearTimeout(timer); timer = null; sequence++; pending?.controller.abort(); pending = null; lastSuccess = ""; refresh(); } function writeNames(value) { names.value = value; applying = true; try { names.dispatchEvent(new Event("input", { bubbles: true })); } finally { applying = false; } } function reset({ preserveNames = false } = {}) { invalidate(); manuallyEdited = preserveNames && !!names.value.trim(); namesRevision++; resetGeneration++; message(); refresh(); } async function read(force = false) { clearTimeout(timer); timer = null; if (!authenticated || !validFile()) { refresh(); return; } const id = identity(); if (!force && pending?.id === id) return pending.promise; if (!force && lastSuccess === id) return; invalidate(); const current = sequence, epoch = authEpoch, revision = namesRevision, previousNames = names.value; const certFile = file.value.trim(), controller = new AbortController(); message("certificateInspectReading"); const active = { id, controller, promise: null }; pending = active; refresh(); active.promise = (async () => { try { const result = await api("/api/certificates/inspect", { method: "POST", body: JSON.stringify({ cert_file: certFile }), signal: controller.signal, }); if (current !== sequence || epoch !== authEpoch || !authenticated || identity() !== id) return; const hosts = Array.isArray(result.covered_hosts) ? result.covered_hosts.filter(host => typeof host === "string") : []; lastSuccess = id; const update = revision === namesRevision && names.value === previousNames && (force || !manuallyEdited); if (update) { writeNames(hosts.join(", ")); manuallyEdited = false; } message(!hosts.length ? "certificateInspectNoNames" : update ? "certificateInspectReady" : "certificateInspectKept"); } catch (error) { if (current === sequence && epoch === authEpoch && authenticated && identity() === id && error.name !== "AbortError") { message("certificateInspectFailed", error.message); } } finally { if (pending === active) { pending = null; refresh(); } } })(); return active.promise; } function schedule() { invalidate(); if (!manuallyEdited && names.value) writeNames(""); message(); if (validFile()) timer = setTimeout(() => { void read(); }, 400); } async function beforeSave() { const generation = resetGeneration; let id; do { id = identity(); await read(); } while (authenticated && generation === resetGeneration && id !== identity()); return generation === resetGeneration; } names.addEventListener("input", () => { if (applying) return; manuallyEdited = true; namesRevision++; }); file.addEventListener("input", schedule); file.addEventListener("change", () => { void read(); }); file.addEventListener("blur", () => { void read(); }); source.addEventListener("change", schedule); form.addEventListener("reset", () => reset()); button.addEventListener("click", () => { void read(true); }); window.rustProxyCertificateInspection = { reset, schedule, read, beforeSave, refresh }; refresh(); })();