feat(theseus/settings): Ariadne's Thread on/off toggle for system-wide BCDN resolution

New card under Settings > Registries: shows whether the system-wide
resolver daemon is running, stopped, or not installed on this machine,
and lets the user turn it on/off without opening the installer.

Ariadne runs as two elevated Windows Scheduled Tasks ("BNS Resolver
Daemon" + "BNS Sia Bridge"). Toggling requires admin — main spawns an
elevated PowerShell (Start-Process -Verb RunAs) that UAC-prompts once
per action, then re-queries state. Query is unelevated
Get-ScheduledTask so status checks are silent.

Three surfaced states:
  running       - "every browser on this machine resolves BCDN names"
  stopped       - "only Theseus resolves BCDN names; other browsers won't"
  not-installed - link to silentmode.st/tools to grab the standalone installer

Theseus's own resolver is unaffected either way — it lives in-process
and doesn't depend on Ariadne. This toggle only controls what non-
Theseus browsers on the same box can resolve.
This commit is contained in:
Local Dev 2026-09-07 00:31:16 +02:00
parent 5642959eca
commit 10f01644c1
3 changed files with 102 additions and 0 deletions

46
main.js
View file

@ -2647,6 +2647,52 @@ ipcMain.handle("collision-set-policy", (_e, p) => {
return settings.collisionPolicy;
});
ipcMain.handle("collision-reset", () => { collisions = { byName: {}, byTld: {} }; saveCollisions(); return true; });
// Ariadne's Thread system-wide resolver — installed by AriadneResolver-Setup.exe
// as two Windows Scheduled Tasks ("BNS Resolver Daemon" + "BNS Sia Bridge").
// Turning them off means non-Theseus browsers stop resolving BCDN names on
// this machine; Theseus itself uses its own in-process resolver so it's
// unaffected. Toggling requires admin (tasks run as SYSTEM) — start/stop go
// through an elevated powershell that UAC-prompts once per action.
const ARIADNE_TASKS = ["BNS Resolver Daemon", "BNS Sia Bridge"];
function ariadneQueryState() {
return new Promise((resolve) => {
const { spawn } = require("child_process");
// Get-ScheduledTask returns Ready | Running | Disabled | (missing → error).
// Print state or "MISSING" for each task; MISSING on the primary task
// means Ariadne isn't installed at all.
const ps = spawn("powershell.exe", ["-NoProfile", "-NonInteractive", "-Command",
"$names=@('BNS Resolver Daemon','BNS Sia Bridge');" +
"$names | ForEach-Object { $t = Get-ScheduledTask -TaskName $_ -ErrorAction SilentlyContinue;" +
" if ($t) { \"$_=$($t.State)\" } else { \"$_=MISSING\" } }"], { windowsHide: true });
let out = "";
ps.stdout.on("data", (d) => { out += d; });
ps.on("close", () => {
const lines = out.trim().split(/\r?\n/).filter(Boolean);
const map = Object.fromEntries(lines.map((l) => { const i = l.lastIndexOf("="); return [l.slice(0, i), l.slice(i + 1)]; }));
const primary = map["BNS Resolver Daemon"];
if (!primary || primary === "MISSING") return resolve({ state: "not-installed" });
if (primary === "Running") return resolve({ state: "running" });
return resolve({ state: "stopped" });
});
ps.on("error", () => resolve({ state: "not-installed" }));
});
}
function ariadneSetState(on) {
return new Promise((resolve, reject) => {
const { spawn } = require("child_process");
// Elevated PowerShell — triggers UAC prompt. -Verb RunAs starts a separate
// elevated process; we can't easily capture its stdout, but we don't need
// to (re-query state afterwards).
const verb = on ? "Start-ScheduledTask" : "Stop-ScheduledTask";
const cmd = `foreach ($t in 'BNS Resolver Daemon','BNS Sia Bridge') { try { ${verb} -TaskName $t -ErrorAction Stop } catch {} }`;
const ps = spawn("powershell.exe", ["-NoProfile", "-Command",
"Start-Process powershell -Verb RunAs -Wait -WindowStyle Hidden -ArgumentList '-NoProfile','-Command',\"" + cmd.replace(/"/g, "`\"") + "\""], { windowsHide: true });
ps.on("close", (code) => { if (code === 0) resolve(true); else reject(new Error("UAC declined or task failed (exit " + code + ")")); });
ps.on("error", (e) => reject(e));
});
}
ipcMain.handle("ariadne-state", () => ariadneQueryState().catch(() => ({ state: "not-installed" })));
ipcMain.handle("ariadne-toggle", (_e, on) => ariadneSetState(!!on).catch((e) => ({ ok: false, error: e?.message || String(e) })));
// Storage: clear right now (any subset). "history" also drops the saved-session file.
// ---- Password vault -------------------------------------------------------
// The vault lives at userData/passwords.vault (encrypted). Unlock state is

View file

@ -31,6 +31,8 @@ contextBridge.exposeInMainWorld("cfg", {
collisionState: () => ipcRenderer.invoke("collision-state"),
setCollisionPolicy: (p) => ipcRenderer.invoke("collision-set-policy", p),
resetCollisions: () => ipcRenderer.invoke("collision-reset"),
ariadneState: () => ipcRenderer.invoke("ariadne-state"),
ariadneToggle: (on) => ipcRenderer.invoke("ariadne-toggle", !!on),
// Add-ons management (Settings > Add-ons tab).
listAddons: () => ipcRenderer.invoke("addons-list"),
setAddonEnabled: (id, enabled) => ipcRenderer.invoke("addons-set-enabled", id, !!enabled),

View file

@ -314,6 +314,29 @@
<div id="colSummary" class="pmuted" style="font-size:12.5px"></div>
<div><button id="resetCollisions" class="btn">Reset remembered choices</button></div>
</div>
<div class="row" style="flex-direction:column;align-items:stretch;gap:8px">
<div class="txt">
<div class="t">System-wide resolver — Ariadne's Thread</div>
<div class="d">
A local daemon that resolves BCDN names for <b>every browser on this machine</b>
(Chrome, Edge, Firefox, etc.), not just Theseus. Turning it off means non-Theseus
browsers stop resolving <code>.bch</code> / <code>.x</code> / other BCNR TLDs;
Theseus keeps working either way, since it has its own built-in resolver.
</div>
</div>
<div id="ariadneStatus" class="pmuted" style="font-size:12.5px">checking…</div>
<div style="display:flex;gap:8px">
<button id="ariadneStart" class="btn" hidden>Turn on</button>
<button id="ariadneStop" class="btn" hidden>Turn off</button>
<button id="ariadneRefresh" class="btn">Refresh</button>
</div>
<div id="ariadneMissing" class="pmuted" style="font-size:12px" hidden>
Ariadne's Thread isn't installed on this machine. If you skipped it during
Theseus setup, you can install it separately from
<a href="https://silentmode.st/tools/">silentmode.st/tools</a> — grab
<code>AriadneResolver-Setup</code>.
</div>
</div>
</section>
<!-- PERFORMANCE -->
<section id="performance" hidden>
@ -892,6 +915,37 @@
C.resetCollisions().then(refreshCollisions);
};
// ---- Ariadne's Thread system-wide resolver toggle -----------------------
// Ariadne runs as two elevated Scheduled Tasks ("BNS Resolver Daemon" +
// "BNS Sia Bridge"). Toggling requires admin — main spawns an elevated
// powershell for start/stop, which prompts UAC once per action.
const arStatus = document.getElementById("ariadneStatus");
const arStart = document.getElementById("ariadneStart");
const arStop = document.getElementById("ariadneStop");
const arRefresh = document.getElementById("ariadneRefresh");
const arMissing = document.getElementById("ariadneMissing");
async function refreshAriadne() {
arStatus.textContent = "checking…";
arStart.hidden = true; arStop.hidden = true; arMissing.hidden = true;
try {
const r = await C.ariadneState();
if (r.state === "running") {
arStatus.innerHTML = 'Status: <b style="color:var(--acid)">running</b> — every browser on this machine resolves BCDN names.';
arStop.hidden = false;
} else if (r.state === "stopped") {
arStatus.innerHTML = 'Status: <b>stopped</b> — only Theseus resolves BCDN names; other browsers won\'t.';
arStart.hidden = false;
} else {
arStatus.textContent = "Status: not installed on this machine.";
arMissing.hidden = false;
}
} catch (e) { arStatus.textContent = "Status check failed: " + (e?.message || e); }
}
arStart.onclick = async () => { arStart.disabled = true; try { await C.ariadneToggle(true); } finally { arStart.disabled = false; refreshAriadne(); } };
arStop.onclick = async () => { arStop.disabled = true; try { await C.ariadneToggle(false); } finally { arStop.disabled = false; refreshAriadne(); } };
arRefresh.onclick = refreshAriadne;
refreshAriadne();
// ---- Passwords section: three states (setup / locked / unlocked) ---------
// The vault lives in main.js — this UI just calls IPC. No plaintext ever
// sits in this DOM except the value produced by a specific Show/Copy click.