QOL shit
This commit is contained in:
@@ -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));
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
|||||||
@@ -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);
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user