2026-09-21 23:39:32 +02:00
<!doctype html>
< html lang = "en" >
< head >
< meta charset = "utf-8" >
< title > VPN< / title >
< style >
:root { color-scheme: light dark;
--bg:#0e131c; --panel:#141a24; --panel2:#191f2b; --line:rgba(255,255,255,.09);
--ink:#e7eaf1; --mut:#8b98a9; --dim:#5e6678; --acid:#0ac18e; --danger:#ff5b5b; }
@media (prefers-color-scheme: light) {
:root { --bg:#f8faff; --panel:#ffffff; --panel2:#eff3fb; --line:rgba(0,0,0,.10);
--ink:#1a1f2b; --mut:#5c6577; --dim:#8a93a5; }
}
* { box-sizing: border-box; }
html, body { margin: 0; height: 100%; }
body { background: var(--bg); color: var(--ink); font: 13px/1.5 system-ui, -apple-system, Segoe UI, Roboto, sans-serif; display: flex; flex-direction: column; }
header { display: flex; align-items: center; gap: 8px; padding: 12px 14px; border-bottom: 1px solid var(--line); background: var(--panel); }
header h1 { margin: 0; font-size: 14px; font-weight: 600; }
header .plat { margin-left: auto; color: var(--dim); font-size: 11px; }
.body { flex: 1; overflow-y: auto; padding: 14px; display: flex; flex-direction: column; gap: 12px; }
.big {
border: 1px solid var(--line); background: var(--panel); border-radius: 14px;
padding: 18px; display: flex; flex-direction: column; align-items: center; gap: 10px;
}
.pill { display: inline-flex; align-items: center; gap: 6px; padding: 3px 9px; border-radius: 999px;
font-size: 11px; font-weight: 600; letter-spacing: .04em; text-transform: uppercase;
background: rgba(139,152,169,.14); color: var(--mut); }
.pill.on { color: #0b0e14; background: var(--acid); }
.pill.err { color: #fff; background: var(--danger); }
.pill .dot { width: 6px; height: 6px; border-radius: 50%; background: currentColor; }
.toggle {
border: none; padding: 14px 28px; border-radius: 12px; font: inherit; font-weight: 700; font-size: 15px;
cursor: pointer; letter-spacing: .04em;
background: var(--acid); color: #0b0e14; min-width: 190px;
}
.toggle:disabled { opacity: .5; cursor: default; }
.toggle.off { background: var(--panel2); color: var(--ink); border: 1px solid var(--line); }
.progress { width: 100%; height: 4px; background: var(--panel2); border-radius: 999px; overflow: hidden; margin-top: 4px; }
.progress > span { display: block; height: 100%; background: var(--acid); width: 0%; transition: width .18s ease; }
.hint { color: var(--dim); font-size: 11.5px; text-align: center; }
.hint.err { color: var(--danger); }
.card { border: 1px solid var(--line); background: var(--panel); border-radius: 12px; padding: 14px; display: flex; flex-direction: column; gap: 10px; }
.card h2 { margin: 0 0 4px; font-size: 12.5px; font-weight: 600; letter-spacing: .06em; text-transform: uppercase; color: var(--dim); }
.card p { margin: 0; color: var(--mut); font-size: 12px; }
.row { display: flex; gap: 8px; align-items: center; }
.row textarea, .row input[type=text] {
flex: 1; border: 1px solid var(--line); background: var(--bg); color: var(--ink);
border-radius: 8px; padding: 8px 10px; font: 12px/1.5 ui-monospace, monospace; resize: vertical;
}
button.btn { border: 1px solid var(--line); background: var(--panel2); color: var(--ink);
border-radius: 8px; padding: 7px 12px; font: inherit; font-size: 12px; cursor: pointer; }
button.btn:hover { border-color: rgb(from var(--acid) r g b / .55); }
button.btn.small { padding: 4px 9px; font-size: 11.5px; }
.opt { display: flex; align-items: center; justify-content: space-between; gap: 8px; padding: 6px 0; }
.opt label { color: var(--ink); font-size: 12.5px; }
.opt .sub { color: var(--dim); font-size: 11.5px; }
.opt input[type=checkbox] { transform: scale(1.15); accent-color: var(--acid); }
.kv { display: grid; grid-template-columns: 100px 1fr; gap: 4px 12px; font-size: 11.5px; }
.kv b { font-weight: 600; color: var(--mut); }
.kv code { font: 11px ui-monospace, monospace; color: var(--ink); overflow-wrap: anywhere; }
.disclosure { border: 1px solid var(--line); background: var(--panel); border-radius: 12px; padding: 10px 12px; font-size: 12px; }
.disclosure summary { cursor: pointer; color: var(--mut); font-weight: 500; list-style: none; }
.disclosure summary::-webkit-details-marker { display: none; }
.disclosure[open] { padding-bottom: 14px; }
.disclosure[open] > *:not(summary) { margin-top: 10px; }
< / style >
< / head >
< body >
< header >
< h1 > VPN< / h1 >
< span class = "plat" id = "platform" > —< / span >
< / header >
< div class = "body" >
< div class = "big" >
< span class = "pill" id = "status" > < span class = "dot" > < / span > < span id = "statusText" > loading…< / span > < / span >
< button class = "toggle off" id = "toggle" disabled > Turn on< / button >
< div class = "progress" id = "progressBar" hidden > < span id = "progress" > < / span > < / div >
< div class = "hint" id = "hint" > < / div >
< / div >
< div class = "card" >
2026-09-22 19:56:01 +02:00
< h2 > Server< / h2 >
< p > Pick a Silent Mode endpoint, or point at your own. Server credentials stay on this device;
the panel only sees which one you picked.< / p >
2026-09-21 23:39:32 +02:00
< div class = "row" >
2026-09-22 19:56:01 +02:00
< select id = "serverSel" style = "flex:1;background:var(--bg);color:var(--ink);border:1px solid var(--line);border-radius:8px;padding:8px 10px;font:inherit" > < / select >
< button class = "btn small" id = "refreshServers" title = "Fetch the latest server list" > ↻< / button >
2026-09-21 23:39:32 +02:00
< / div >
2026-09-22 19:56:01 +02:00
< div class = "hint" id = "serverHint" style = "text-align:left" > < / div >
< div id = "customBlock" hidden >
< div class = "row" style = "margin-top:8px" >
2026-09-23 23:21:51 +02:00
< textarea id = "vless" rows = "3" placeholder = "vless://uuid@host:443?security=reality&pbk=…&sni=… …or paste an https:// subscription URL and it will be imported" > < / textarea >
2026-09-22 19:56:01 +02:00
< / div >
< div class = "row" style = "justify-content:flex-end" >
< button class = "btn small" id = "paste" > Paste< / button >
< button class = "btn small" id = "save" > Save< / button >
< / div >
2026-09-21 23:39:32 +02:00
< / div >
< / div >
vpn 0.1.2 → 0.1.3: subscription import + landing page mockup
Addon:
- Paste any https:// URL that returns a list of vless:// (either
newline-separated or base64) and the extension fetches, decodes,
parses, and adds every server to the dropdown. The full vless URL
never leaves the panel — the addon holds it in its own storage and
passes an opaque "sub-<hash>" id back for selection.
- Subscription CRUD on the addon side (listSubscriptions,
addSubscription, refreshSubscription, removeSubscription). A refresh
is a no-op inside the 6-hour TTL to avoid pounding the provider.
- Merges subscription servers with the baked-in three and gateway
overlay by id; the dropdown groups them under one banner.
Site:
- silentmode.st/vpn landing page: three-plan grid (Free, Pro at $1/mo
BCH, Max at $4/mo BCH), how-it-works four-step block, "the three
servers" strip with per-tier availability, why-this-VPN cards, FAQ.
Priced in USD, paid in BCH via the oracle at pay-time — same pattern
as the marketplace's USD-listing covenant, no reintroduction of
fiat/card processors.
2026-09-22 20:59:50 +02:00
< div class = "card" >
< h2 > Subscription< / h2 >
< p > Paste an HTTPS URL that returns a list of < code > vless://< / code > servers.
The URL and its servers stay on this device.< / p >
< div class = "row" >
< input id = "subUrl" type = "url" placeholder = "https://provider.example/subscribe/…" style = "flex:1;border:1px solid var(--line);background:var(--bg);color:var(--ink);border-radius:8px;padding:8px 10px;font:12px/1.5 ui-monospace,monospace" / >
< button class = "btn small" id = "subAdd" > Add< / button >
< / div >
< div id = "subList" class = "kv" style = "grid-template-columns:1fr auto auto;gap:6px 8px;font-size:12px" > < / div >
< div class = "hint" id = "subHint" style = "text-align:left" > < / div >
< / div >
2026-09-21 23:39:32 +02:00
< details class = "disclosure" >
< summary > Advanced< / summary >
< div class = "kv" id = "details" >
< b > Platform< / b > < code id = "d-platform" > —< / code >
< b > Engine< / b > < code id = "d-engine" > sing-box< / code >
< b > Binary< / b > < code id = "d-binary" > —< / code >
< b > SOCKS5< / b > < code id = "d-socks" > —< / code >
< / div >
< div class = "opt" style = "margin-top:10px" >
< div >
< label for = "autoOn" > Turn on at browser start< / label >
< div class = "sub" > Restore the VPN as soon as Theseus launches, if it was on when you last quit.< / div >
< / div >
< input type = "checkbox" id = "autoOn" >
< / div >
< div class = "row" style = "justify-content:flex-end;margin-top:6px" >
< button class = "btn small" id = "refreshBinary" > Re-download binary< / button >
< button class = "btn small" id = "clearCache" > Clear cache< / button >
< / div >
< / details >
< / div >
< script >
const SM = () => window.silentmode;
const $ = (id) => document.getElementById(id);
const toggle = $("toggle");
const statusEl = $("status");
const statusTx = $("statusText");
const hint = $("hint");
const progress = $("progress");
const progressBar = $("progressBar");
const platEl = $("platform");
const dPlat = $("d-platform");
const dBinary = $("d-binary");
const dSocks = $("d-socks");
let pending = false;
let currentVless = "";
2026-09-22 19:56:01 +02:00
let selectedServerId = "";
let serverCatalog = [];
2026-09-21 23:39:32 +02:00
2026-09-22 19:56:01 +02:00
const CUSTOM_ID = "__custom";
async function loadSelection() {
try { selectedServerId = await SM().storage.get("serverId", "") || ""; } catch { selectedServerId = ""; }
try { currentVless = await SM().storage.get("vless", "") || ""; } catch { currentVless = ""; }
2026-09-21 23:39:32 +02:00
$("vless").value = currentVless;
}
2026-09-22 19:56:01 +02:00
async function saveSelection() {
try { await SM().storage.set("serverId", selectedServerId); } catch {}
}
2026-09-21 23:39:32 +02:00
async function saveEndpoint() {
2026-09-23 23:21:51 +02:00
const raw = $("vless").value.trim();
// A very common mix-up: the provider handed over a subscription URL (plain
// https) rather than a single vless:// endpoint, and this box is the first
// text field people find. Route it to the subscription importer instead of
// rejecting it for not starting with vless://.
if (/^https?:\/\//i.test(raw)) {
const subHint = document.getElementById("subHint");
subHint.className = "hint";
subHint.textContent = "that looks like a subscription URL — importing…";
try {
const r = await SM().invoke("addSubscription", { url: raw });
$("vless").value = "";
subHint.textContent = `added ${r.label} — ${r.count} servers, pick one above`;
render(r.snapshot);
renderSubList();
} catch (e) {
subHint.className = "hint err";
subHint.textContent = `not a usable subscription: ${e?.message || e}`;
}
return;
}
currentVless = raw;
2026-09-21 23:39:32 +02:00
try { await SM().storage.set("vless", currentVless); } catch {}
render(await SM().invoke("status", {}));
}
2026-09-22 19:56:01 +02:00
function renderServerDropdown(servers) {
serverCatalog = Array.isArray(servers) ? servers : [];
const sel = $("serverSel");
const prev = sel.value || selectedServerId;
sel.innerHTML = "";
if (serverCatalog.length) {
const group = document.createElement("optgroup");
group.label = "Silent Mode servers";
for (const s of serverCatalog) {
const opt = document.createElement("option");
opt.value = s.id;
opt.textContent = `${s.flag || "🌐"} ${s.label || s.id}${s.status === "ready" ? "" : " · " + s.status}`;
opt.disabled = s.status !== "ready";
group.appendChild(opt);
}
sel.appendChild(group);
}
const custom = document.createElement("option");
custom.value = CUSTOM_ID;
2026-09-23 23:21:51 +02:00
custom.textContent = "⚙️ Custom — vless:// or subscription URL";
2026-09-22 19:56:01 +02:00
sel.appendChild(custom);
// Restore the previous selection if it survived the refresh.
if ([...sel.options].some((o) => o.value === prev)) sel.value = prev;
else if (currentVless) sel.value = CUSTOM_ID;
else if (serverCatalog.find((s) => s.status === "ready")) sel.value = serverCatalog.find((s) => s.status === "ready").id;
selectedServerId = sel.value;
onSelectionChanged();
}
function onSelectionChanged() {
const isCustom = selectedServerId === CUSTOM_ID;
$("customBlock").hidden = !isCustom;
const hit = serverCatalog.find((s) => s.id === selectedServerId);
const hint = $("serverHint");
if (isCustom) {
hint.className = "hint"; hint.textContent = currentVless ? "" : "Paste a vless:// URL below.";
} else if (hit & & hit.status !== "ready") {
hint.className = "hint err"; hint.textContent = `${hit.label || hit.id}: ${hit.status}`;
} else {
hint.className = "hint"; hint.textContent = "";
}
}
2026-09-21 23:39:32 +02:00
async function loadAuto() {
try { $("autoOn").checked = !!(await SM().storage.get("autoOn", false)); } catch {}
}
async function saveAuto() {
try { await SM().storage.set("autoOn", $("autoOn").checked); } catch {}
}
function render(s) {
if (!s) return;
platEl.textContent = s.platform;
dPlat.textContent = s.platform;
dBinary.textContent = s.binaryReady
? `sing-box ${s.binaryVersion || "?"} (cached)`
: (s.binaryError || "not downloaded");
dSocks.textContent = s.running ? `127.0.0.1:${s.socksPort}` : "off";
2026-09-22 19:56:01 +02:00
// Refresh the dropdown from the latest catalog snapshot. renderServerDropdown
// is a no-op when the id list hasn't changed since the last paint.
if (Array.isArray(s.servers)) renderServerDropdown(s.servers);
2026-09-21 23:39:32 +02:00
if (s.running) {
statusEl.className = "pill on"; statusTx.textContent = "connected";
toggle.className = "toggle"; toggle.textContent = "Turn off"; toggle.disabled = pending;
hint.className = "hint"; hint.textContent = "";
progressBar.hidden = true;
} else if (s.downloadPct > 0 & & s.downloadPct < 100 & & pending ) {
statusEl.className = "pill"; statusTx.textContent = "downloading";
toggle.textContent = "Preparing…"; toggle.disabled = true;
progressBar.hidden = false;
progress.style.width = `${s.downloadPct}%`;
hint.className = "hint"; hint.textContent = `sing-box · ${s.downloadPct}%`;
} else if (s.binaryError) {
statusEl.className = "pill err"; statusTx.textContent = "unavailable";
toggle.className = "toggle off"; toggle.textContent = "Turn on"; toggle.disabled = true;
hint.className = "hint err"; hint.textContent = s.binaryError;
progressBar.hidden = true;
} else {
statusEl.className = "pill"; statusTx.textContent = "off";
toggle.className = "toggle off"; toggle.textContent = "Turn on";
2026-09-22 19:56:01 +02:00
const readyServer = serverCatalog.find((sv) => sv.id === selectedServerId & & sv.status === "ready");
const canRun = (selectedServerId === CUSTOM_ID & & !!currentVless) || !!readyServer;
toggle.disabled = pending || !canRun;
2026-09-21 23:39:32 +02:00
hint.className = "hint";
2026-09-22 19:56:01 +02:00
if (!canRun) {
hint.textContent = selectedServerId === CUSTOM_ID
? "Paste a vless:// URL below to enable the toggle."
: "Pick a server (or Custom) to enable the toggle.";
} else {
hint.textContent = "";
}
2026-09-21 23:39:32 +02:00
progressBar.hidden = true;
}
if (s.lastError & & !s.running) {
hint.className = "hint err"; hint.textContent = s.lastError;
}
}
async function tick() {
try { render(await SM().invoke("status", {})); } catch {}
}
toggle.addEventListener("click", async () => {
if (pending) return;
pending = true;
const s = await SM().invoke("status", {});
try {
if (s.running) {
render(await SM().invoke("turnOff", {}));
2026-09-22 19:56:01 +02:00
} else if (selectedServerId === CUSTOM_ID) {
if (!currentVless) throw new Error("paste a vless:// URL first");
2026-09-21 23:39:32 +02:00
render(await SM().invoke("turnOn", { vless: currentVless }));
2026-09-22 19:56:01 +02:00
} else {
render(await SM().invoke("turnOn", { serverId: selectedServerId }));
2026-09-21 23:39:32 +02:00
}
} catch (e) {
hint.className = "hint err"; hint.textContent = e?.message || String(e);
statusEl.className = "pill err"; statusTx.textContent = "failed";
progressBar.hidden = true; toggle.disabled = false;
toggle.className = "toggle off"; toggle.textContent = "Turn on";
} finally {
pending = false;
}
});
2026-09-22 19:56:01 +02:00
$("serverSel").addEventListener("change", async () => {
selectedServerId = $("serverSel").value;
onSelectionChanged();
await saveSelection();
render(await SM().invoke("status", {}));
});
$("refreshServers").addEventListener("click", async () => {
hint.className = "hint"; hint.textContent = "refreshing catalog…";
try { render(await SM().invoke("refreshServers", {})); }
catch (e) { hint.className = "hint err"; hint.textContent = e?.message || String(e); }
});
vpn 0.1.2 → 0.1.3: subscription import + landing page mockup
Addon:
- Paste any https:// URL that returns a list of vless:// (either
newline-separated or base64) and the extension fetches, decodes,
parses, and adds every server to the dropdown. The full vless URL
never leaves the panel — the addon holds it in its own storage and
passes an opaque "sub-<hash>" id back for selection.
- Subscription CRUD on the addon side (listSubscriptions,
addSubscription, refreshSubscription, removeSubscription). A refresh
is a no-op inside the 6-hour TTL to avoid pounding the provider.
- Merges subscription servers with the baked-in three and gateway
overlay by id; the dropdown groups them under one banner.
Site:
- silentmode.st/vpn landing page: three-plan grid (Free, Pro at $1/mo
BCH, Max at $4/mo BCH), how-it-works four-step block, "the three
servers" strip with per-tier availability, why-this-VPN cards, FAQ.
Priced in USD, paid in BCH via the oracle at pay-time — same pattern
as the marketplace's USD-listing covenant, no reintroduction of
fiat/card processors.
2026-09-22 20:59:50 +02:00
// ---- subscription import ----
async function renderSubList() {
const subs = await SM().invoke("listSubscriptions", {});
const host = document.getElementById("subList");
if (!subs.length) { host.innerHTML = ""; return; }
host.innerHTML = subs.map((s) => {
const url = s.url.replace(/"/g, "" ");
return `< div style = "overflow-wrap:anywhere" > < b style = "color:var(--ink)" > ${s.label}< / b > < div style = "color:var(--dim);font-size:11px" > ${s.count} servers · ${s.url}< / div > < / div >
< button class = "btn small" data-sub-refresh = "${url}" > ↻< / button >
< button class = "btn small" data-sub-remove = "${url}" > ✕< / button > `;
}).join("");
host.querySelectorAll("[data-sub-refresh]").forEach((b) => b.addEventListener("click", async () => {
const subHint = document.getElementById("subHint");
subHint.className = "hint"; subHint.textContent = "refreshing…";
try {
const r = await SM().invoke("refreshSubscription", { url: b.dataset.subRefresh });
subHint.textContent = r.skipped ? "recent enough, skipped" : `${r.count} servers`;
render(r.snapshot);
renderSubList();
} catch (e) { subHint.className = "hint err"; subHint.textContent = e?.message || String(e); }
}));
host.querySelectorAll("[data-sub-remove]").forEach((b) => b.addEventListener("click", async () => {
const r = await SM().invoke("removeSubscription", { url: b.dataset.subRemove });
render(r.snapshot); renderSubList();
}));
}
document.getElementById("subAdd").addEventListener("click", async () => {
const url = document.getElementById("subUrl").value.trim();
const subHint = document.getElementById("subHint");
if (!url) { subHint.className = "hint err"; subHint.textContent = "paste a URL first"; return; }
subHint.className = "hint"; subHint.textContent = "fetching…";
try {
const r = await SM().invoke("addSubscription", { url });
document.getElementById("subUrl").value = "";
subHint.className = "hint"; subHint.textContent = `added ${r.label} — ${r.count} servers`;
render(r.snapshot);
renderSubList();
} catch (e) { subHint.className = "hint err"; subHint.textContent = e?.message || String(e); }
});
2026-09-21 23:39:32 +02:00
$("save").addEventListener("click", saveEndpoint);
$("paste").addEventListener("click", async () => {
try { $("vless").value = await navigator.clipboard.readText(); } catch {}
});
$("autoOn").addEventListener("change", saveAuto);
$("refreshBinary").addEventListener("click", async () => {
pending = true;
try {
hint.className = "hint"; hint.textContent = "downloading…";
await SM().invoke("clearCache", {});
await SM().invoke("prepareBinary", {});
render(await SM().invoke("status", {}));
} catch (e) {
hint.className = "hint err"; hint.textContent = e?.message || String(e);
} finally { pending = false; }
});
$("clearCache").addEventListener("click", async () => {
render(await SM().invoke("clearCache", {}));
});
// Live updates from index.js — download progress, spawn crashes, etc.
try {
window.silentmode.on?.("state", (s) => render(s));
} catch {}
(async () => {
2026-09-22 19:56:01 +02:00
await loadSelection();
2026-09-21 23:39:32 +02:00
await loadAuto();
render(await SM().invoke("status", {}));
vpn 0.1.2 → 0.1.3: subscription import + landing page mockup
Addon:
- Paste any https:// URL that returns a list of vless:// (either
newline-separated or base64) and the extension fetches, decodes,
parses, and adds every server to the dropdown. The full vless URL
never leaves the panel — the addon holds it in its own storage and
passes an opaque "sub-<hash>" id back for selection.
- Subscription CRUD on the addon side (listSubscriptions,
addSubscription, refreshSubscription, removeSubscription). A refresh
is a no-op inside the 6-hour TTL to avoid pounding the provider.
- Merges subscription servers with the baked-in three and gateway
overlay by id; the dropdown groups them under one banner.
Site:
- silentmode.st/vpn landing page: three-plan grid (Free, Pro at $1/mo
BCH, Max at $4/mo BCH), how-it-works four-step block, "the three
servers" strip with per-tier availability, why-this-VPN cards, FAQ.
Priced in USD, paid in BCH via the oracle at pay-time — same pattern
as the marketplace's USD-listing covenant, no reintroduction of
fiat/card processors.
2026-09-22 20:59:50 +02:00
renderSubList();
2026-09-22 19:56:01 +02:00
// Auto-on: if it was on before, try to bring it back with the last saved
// choice — server id if any, else custom vless. Silent failure is fine,
// the toggle stays in "off" and the hint carries the reason.
if ($("autoOn").checked) {
try {
if (selectedServerId === CUSTOM_ID & & currentVless) {
render(await SM().invoke("turnOn", { vless: currentVless }));
} else if (selectedServerId & & selectedServerId !== CUSTOM_ID) {
render(await SM().invoke("turnOn", { serverId: selectedServerId }));
}
} catch (e) { hint.className = "hint err"; hint.textContent = e?.message || String(e); }
2026-09-21 23:39:32 +02:00
}
// Periodic refresh for slow status changes (download bytes, unexpected
// sing-box exit). Cheap; runs only while the panel is visible.
setInterval(tick, 800);
})();
< / script >
< / body >
< / html >