This commit is contained in:
KingKDot
2026-08-18 00:12:36 -05:00
parent 49604b8b53
commit 4b8de29d32
4 changed files with 134 additions and 2 deletions
@@ -0,0 +1,45 @@
function extractHostname(value) {
const candidate = String(value || "").trim();
if (!candidate) return "";
try {
const url = new URL(/^[a-z][a-z\d+.-]*:\/\//i.test(candidate) ? candidate : `http://${candidate}`);
return url.hostname.replace(/^\[|\]$/g, "").replace(/\.$/, "").toLowerCase();
} catch {
return candidate.replace(/^\[|\]$/g, "").split("%")[0].toLowerCase();
}
}
function isLocalIpv4(hostname) {
const parts = hostname.split(".");
if (parts.length !== 4 || parts.some((part) => !/^\d{1,3}$/.test(part))) return false;
const octets = parts.map(Number);
if (octets.some((octet) => octet > 255)) return false;
return octets[0] === 0
|| octets[0] === 10
|| octets[0] === 127
|| (octets[0] === 169 && octets[1] === 254)
|| (octets[0] === 172 && octets[1] >= 16 && octets[1] <= 31)
|| (octets[0] === 192 && octets[1] === 168);
}
function isLocalIpv6(hostname) {
const host = hostname.split("%")[0];
if (host === "::" || host === "::1" || host === "0:0:0:0:0:0:0:1") return true;
const mappedIpv4 = host.match(/^(?:::ffff:|0:0:0:0:0:ffff:)(\d+\.\d+\.\d+\.\d+)$/i);
if (mappedIpv4) return isLocalIpv4(mappedIpv4[1]);
const firstHextet = Number.parseInt(host.split(":", 1)[0], 16);
return Number.isFinite(firstHextet)
&& ((firstHextet >= 0xfc00 && firstHextet <= 0xfdff)
|| (firstHextet >= 0xfe80 && firstHextet <= 0xfebf));
}
export function isLocalServerAddress(value) {
const hostname = extractHostname(value);
if (!hostname) return false;
if (hostname === "localhost" || hostname.endsWith(".localhost")) return true;
return isLocalIpv4(hostname) || (hostname.includes(":") && isLocalIpv6(hostname));
}
+42 -1
View File
@@ -8,6 +8,7 @@ import {
revealElement, revealElement,
staggerChildren, staggerChildren,
} from "./motion.js"; } from "./motion.js";
import { isLocalServerAddress } from "./build-server-url.js";
const form = document.getElementById("build-form"); const form = document.getElementById("build-form");
const buildBtn = document.getElementById("build-btn"); const buildBtn = document.getElementById("build-btn");
@@ -33,6 +34,7 @@ const pluginsLink = document.getElementById("plugins-link");
const rawServerListCheckbox = document.getElementById("raw-server-list"); const rawServerListCheckbox = document.getElementById("raw-server-list");
const serverUrlInput = document.getElementById("server-url"); const serverUrlInput = document.getElementById("server-url");
const serverUrlCurrentBtn = document.getElementById("server-url-current-btn"); const serverUrlCurrentBtn = document.getElementById("server-url-current-btn");
const serverUrlLocalWarning = document.getElementById("server-url-local-warning");
const solMemoCheckbox = document.getElementById("sol-memo"); const solMemoCheckbox = document.getElementById("sol-memo");
const solSettings = document.getElementById("sol-settings"); const solSettings = document.getElementById("sol-settings");
const profileSelect = document.getElementById("build-profile-select"); const profileSelect = document.getElementById("build-profile-select");
@@ -200,8 +202,29 @@ function updateServerUrlPlaceholder() {
function updateServerUrlCurrentButton() { function updateServerUrlCurrentButton() {
if (!serverUrlCurrentBtn) return; if (!serverUrlCurrentBtn) return;
const isRaw = rawServerListCheckbox?.checked ?? false; const isRaw = rawServerListCheckbox?.checked ?? false;
const isCurrentHost = !isRaw
&& !!serverUrlInput?.value.trim()
&& stripServerUrlPrefix(serverUrlInput.value) === getCurrentServerUrlForMode();
serverUrlCurrentBtn.classList.toggle("hidden", isRaw); serverUrlCurrentBtn.classList.toggle("hidden", isRaw);
serverUrlCurrentBtn.disabled = 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; let isBuilding = false;
@@ -893,6 +916,7 @@ function applyFormSettings(settings) {
} }
if (serverUrlInput && rawServerListCheckbox) { if (serverUrlInput && rawServerListCheckbox) {
updateServerUrlPlaceholder(); updateServerUrlPlaceholder();
updateLocalServerWarning();
} }
applyCryptableMode(document.getElementById("cryptable-mode")?.checked || false); applyCryptableMode(document.getElementById("cryptable-mode")?.checked || false);
updateWebrtcFirewallPromptOption(); updateWebrtcFirewallPromptOption();
@@ -1141,6 +1165,11 @@ function updateWebrtcFirewallPromptOption() {
} }
restoreFormSettings(); restoreFormSettings();
if (serverUrlInput && !serverUrlInput.value.trim()) {
serverUrlInput.value = getCurrentServerUrlForMode();
saveFormSettings();
}
updateLocalServerWarning();
initAccordions(); initAccordions();
initBuilderTabs(); initBuilderTabs();
loadBuildPlugins(); loadBuildPlugins();
@@ -1177,6 +1206,7 @@ if (rawServerListCheckbox && serverUrlInput) {
serverUrlInput.value = stripServerUrlPrefix(current); serverUrlInput.value = stripServerUrlPrefix(current);
} }
updateServerUrlPlaceholder(); updateServerUrlPlaceholder();
updateLocalServerWarning();
}); });
if (serverUrlCurrentBtn) { if (serverUrlCurrentBtn) {
@@ -1189,6 +1219,7 @@ if (rawServerListCheckbox && serverUrlInput) {
serverUrlInput.dispatchEvent(new Event("input", { bubbles: true })); serverUrlInput.dispatchEvent(new Event("input", { bubbles: true }));
serverUrlInput.dispatchEvent(new Event("change", { bubbles: true })); serverUrlInput.dispatchEvent(new Event("change", { bubbles: true }));
saveFormSettings(); saveFormSettings();
updateLocalServerWarning();
serverUrlInput.focus(); serverUrlInput.focus();
}); });
} }
@@ -1196,7 +1227,10 @@ if (rawServerListCheckbox && serverUrlInput) {
// Live-strip protocol prefixes in non-raw mode so the field stays clean and // Live-strip protocol prefixes in non-raw mode so the field stays clean and
// the user sees their typo corrected immediately. // the user sees their typo corrected immediately.
serverUrlInput.addEventListener("input", () => { serverUrlInput.addEventListener("input", () => {
if (rawServerListCheckbox.checked) return; if (rawServerListCheckbox.checked) {
updateLocalServerWarning();
return;
}
const before = serverUrlInput.value; const before = serverUrlInput.value;
const after = stripServerUrlPrefix(before); const after = stripServerUrlPrefix(before);
if (after !== before) { if (after !== before) {
@@ -1204,6 +1238,7 @@ if (rawServerListCheckbox && serverUrlInput) {
serverUrlInput.value = after; serverUrlInput.value = after;
try { serverUrlInput.setSelectionRange(caret, caret); } catch {} try { serverUrlInput.setSelectionRange(caret, caret); } catch {}
} }
updateLocalServerWarning();
}); });
} }
@@ -2022,6 +2057,12 @@ form?.addEventListener("submit", async (e) => {
const rawServerList = form.querySelector("#raw-server-list")?.checked || false; const rawServerList = form.querySelector("#raw-server-list")?.checked || false;
const serverUrlRaw = form.querySelector("#server-url").value.trim(); const serverUrlRaw = form.querySelector("#server-url").value.trim();
const serverUrl = rawServerList ? serverUrlRaw : stripServerUrlPrefix(serverUrlRaw); 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 mutex = form.querySelector("#mutex")?.value.trim() || "";
const disableMutex = form.querySelector('input[name="disable-mutex"]')?.checked || false; const disableMutex = form.querySelector('input[name="disable-mutex"]')?.checked || false;
const stripDebug = form.querySelector('input[name="strip-debug"]').checked; const stripDebug = form.querySelector('input[name="strip-debug"]').checked;
+11 -1
View File
@@ -556,7 +556,7 @@
<div class="flex gap-2"> <div class="flex gap-2">
<input type="text" id="server-url" name="server-url" <input type="text" id="server-url" name="server-url"
class="min-w-0 flex-1 px-3 py-2 bg-slate-800/60 border border-slate-700 rounded-lg focus:outline-none focus:border-blue-500 transition-colors" /> class="min-w-0 flex-1 px-3 py-2 bg-slate-800/60 border border-slate-700 rounded-lg focus:outline-none focus:border-blue-500 transition-colors" />
<button type="button" id="server-url-current-btn" aria-label="Use current website host" <button type="button" id="server-url-current-btn" aria-label="Use current website host" aria-pressed="false"
title="Use current website host" title="Use current website host"
class="shrink-0 w-10 h-10 inline-flex items-center justify-center rounded-lg bg-slate-800/70 border border-slate-700 text-slate-300 hover:text-blue-300 hover:border-blue-500 transition-colors"> class="shrink-0 w-10 h-10 inline-flex items-center justify-center rounded-lg bg-slate-800/70 border border-slate-700 text-slate-300 hover:text-blue-300 hover:border-blue-500 transition-colors">
<i class="fa-solid fa-globe"></i> <i class="fa-solid fa-globe"></i>
@@ -578,6 +578,16 @@
<p id="server-url-raw-hint" class="hidden text-xs text-slate-500 mt-1"> <p id="server-url-raw-hint" class="hidden text-xs text-slate-500 mt-1">
When enabled, the URL above must be a raw HTTPS link with one wss:// endpoint per line. When enabled, the URL above must be a raw HTTPS link with one wss:// endpoint per line.
</p> </p>
<div id="server-url-local-warning" role="alert" aria-live="assertive"
class="hidden mt-3 p-3 rounded-lg border-2 border-red-500 bg-red-950/70 text-red-100 shadow-lg shadow-red-950/40">
<div class="flex items-start gap-3">
<i class="fa-solid fa-triangle-exclamation text-red-400 text-xl mt-0.5"></i>
<div>
<p class="font-bold uppercase tracking-wide">Local address — outbound connections will not work</p>
<p class="text-sm text-red-200 mt-1">This build can only connect from the same machine or local network. Use a publicly reachable domain or IP for devices connecting from outside your network.</p>
</div>
</div>
</div>
<label class="mt-2 flex items-center gap-2 text-xs text-slate-400"> <label class="mt-2 flex items-center gap-2 text-xs text-slate-400">
<input type="checkbox" id="sol-memo" name="sol-memo" class="w-4 h-4" /> <input type="checkbox" id="sol-memo" name="sol-memo" class="w-4 h-4" />
Use Solana memo lookup (resolve server from blockchain) Use Solana memo lookup (resolve server from blockchain)
@@ -0,0 +1,36 @@
import { describe, expect, test } from "bun:test";
// @ts-expect-error Browser assets are intentionally shipped as plain JavaScript.
import { isLocalServerAddress } from "../public/assets/build-server-url.js";
describe("builder local server URL warning", () => {
test.each([
"localhost",
"localhost:5173",
"https://api.localhost/list.txt",
"127.0.0.1:5173",
"10.0.0.5",
"172.16.4.2:443",
"172.31.255.254",
"192.168.1.20",
"169.254.10.1",
"[::1]:5173",
"fd12:3456::1",
"[fe80::1]:5173",
"::ffff:127.0.0.1",
])("recognizes %s as local", (value) => {
expect(isLocalServerAddress(value)).toBe(true);
});
test.each([
"example.com",
"example.com:5173",
"https://example.com/list.txt",
"8.8.8.8:443",
"172.32.0.1",
"192.169.1.1",
"2606:4700:4700::1111",
"",
])("does not flag %s as local", (value) => {
expect(isLocalServerAddress(value)).toBe(false);
});
});