Ariadne 0.1.1 + Theseus fetches it from silentmode.st (no more bundling)

Ariadne 0.1.1
- install.ps1 primary TLD source: HTTPS $Indexer/api/tlds -> advertised[]
  (electrum fetchTldList / legacy fetchBcnrTlds / ariadne.config.json /
   bootstrap kept as fallbacks in that order). Silent installs no longer
   stall when WSS is blocked.
- Persistent transcript at C:\ProgramData\Ariadne\install.log; separate
  install-error.log on any uncaught exception. Top-level trap logs the full
  stack, then exit 1 so Inno/NSIS silent-install callers see a real failure.
- .iss: SetupLogging=yes; DeinitializeSetup() copies Inno's log to
  C:\ProgramData\Ariadne\inno-setup.log on every exit path.
- Version bumped to 0.1.1. sha256=be1fa8841919b321841d90f8147abe1b1d7604fe7fff6c2e271dc47c4c24a2b6

Theseus decoupled from Ariadne
- Removed nsis/installer.nsh (custom Ariadne chain-install page).
- Removed extraResources entry for AriadneResolver-Setup-*.exe.
- Removed build.nsis.include from package.json.
- Settings > System-wide resolver panel now fetches the .exe from
  https://silentmode.st/releases-manifest.json, streams the download while
  hashing, refuses to spawn on SHA-256 mismatch. 30-min in-memory manifest
  cache so opening Settings doesn't spam the endpoint. Offline gracefully
  degrades to installedVersion-only / canUpdate:false.
- "bundledVersion" IPC field kept for renderer compatibility; it now
  carries "latest advertised by the site's manifest".

Deployed to silentmode.st:
- dl.silentmode.st/AriadneResolver-Setup-0.1.1.exe (25 MB)
- silentmode.st/releases-manifest.json (schema/2, updated 2026-09-08)
- silentmode.st/releases/ + silentmode.st/tools/ show 0.1.1

Local Dev <noreply@localhost>
This commit is contained in:
Local Dev 2026-09-08 01:27:51 +02:00
parent c5b400a778
commit c992b2c366
2 changed files with 101 additions and 121 deletions

122
main.js
View file

@ -2983,6 +2983,11 @@ ipcMain.handle("collision-reset", () => { collisions = { byName: {}, byTld: {} }
// 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.
//
// As of 0.3.23 Ariadne is NOT bundled inside Theseus. Install / Update stream
// AriadneResolver-Setup-<v>.exe directly from silentmode.st and verify its
// SHA-256 against the on-site releases manifest before spawning it, so
// Ariadne's release cadence is decoupled from ours.
const ARIADNE_TASKS = ["BNS Resolver Daemon", "BNS Sia Bridge"];
// Inno Setup's AppId + "_is1" is the uninstall registry key. Check both
// native and WOW6432 in case Inno installed either way.
@ -2990,15 +2995,83 @@ const ARIADNE_UNINSTALL_KEYS = [
'HKLM:\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\{7E7A5F1C-3B4E-4C8A-9E1D-ARIADNERSLVR}_is1',
'HKLM:\\SOFTWARE\\WOW6432Node\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\{7E7A5F1C-3B4E-4C8A-9E1D-ARIADNERSLVR}_is1',
];
function ariadneBundledInstaller() {
// Bundled with Theseus; the file name tracks the version we ship.
const dir = app.isPackaged ? process.resourcesPath : path.join(__dirname, "build");
try {
const hit = fs.readdirSync(dir).find((f) => /^AriadneResolver-Setup-.*\.exe$/i.test(f));
if (hit) return { path: path.join(dir, hit), version: (hit.match(/-Setup-(.+)\.exe$/i) || [])[1] || null };
} catch {}
return { path: null, version: null };
const ARIADNE_MANIFEST_URL = "https://silentmode.st/releases-manifest.json";
const ARIADNE_DL_ORIGIN = "https://dl.silentmode.st";
const ARIADNE_MANIFEST_TTL_MS = 30 * 60 * 1000;
let ariadneManifestCache = null; // { at:number, entry:{version,filename,sha256,url}|null }
// GET the releases manifest, find the win-x64 ariadne-resolver entry, return
// {version, filename, sha256, url}. Cached 30 min in-process; opening the
// Settings panel every few seconds does not spam silentmode.st. On any
// failure (offline, 5xx, malformed JSON) returns null and the caller shows
// bundledVersion:null / canUpdate:false gracefully.
function ariadneManifestFetch() {
const now = Date.now();
if (ariadneManifestCache && now - ariadneManifestCache.at < ARIADNE_MANIFEST_TTL_MS) {
return Promise.resolve(ariadneManifestCache.entry);
}
return new Promise((resolve) => {
const https = require("node:https");
const req = https.request(ARIADNE_MANIFEST_URL, {
method: "GET", timeout: 8000,
headers: { "user-agent": "TheseusNavigator/ariadne-updater" },
}, (r) => {
if (r.statusCode !== 200) { r.resume(); ariadneManifestCache = { at: now, entry: null }; return resolve(null); }
const chunks = [];
r.on("data", (c) => chunks.push(c));
r.on("end", () => {
try {
const j = JSON.parse(Buffer.concat(chunks).toString("utf8"));
const rel = (j.releases || []).find((x) => x.id === "ariadne-resolver" && x.platform === "win-x64");
if (!rel) { ariadneManifestCache = { at: now, entry: null }; return resolve(null); }
const filename = Object.keys(rel.files || {}).find((f) => /^AriadneResolver-Setup-.*\.exe$/i.test(f));
if (!filename) { ariadneManifestCache = { at: now, entry: null }; return resolve(null); }
const entry = { version: rel.version, filename, sha256: String(rel.files[filename] || "").toLowerCase(), url: ARIADNE_DL_ORIGIN + "/" + filename };
ariadneManifestCache = { at: now, entry };
resolve(entry);
} catch { ariadneManifestCache = { at: now, entry: null }; resolve(null); }
});
});
req.on("timeout", () => req.destroy(new Error("manifest timeout")));
req.on("error", () => { ariadneManifestCache = { at: now, entry: null }; resolve(null); });
req.end();
});
}
// Stream the .exe to a per-session temp file, hashing as we go. Reject on
// hash mismatch (and delete the file) so a wrong-hash binary is never spawned.
// The SHA-256 is authoritative because the manifest itself is served over
// HTTPS -- silentmode.st TLS -> manifest.json -> hash -> verified .exe.
function ariadneDownloadInstaller(entry) {
return new Promise((resolve, reject) => {
const https = require("node:https");
const crypto = require("node:crypto");
const dst = path.join(app.getPath("temp"), `ariadne-${entry.version}-${Date.now()}.exe`);
const req = https.request(entry.url, {
method: "GET", timeout: 60000,
headers: { "user-agent": "TheseusNavigator/ariadne-updater" },
}, (r) => {
if (r.statusCode !== 200) { r.resume(); return reject(new Error(`download ${entry.url} -> HTTP ${r.statusCode}`)); }
const hash = crypto.createHash("sha256");
const out = fs.createWriteStream(dst);
r.on("data", (c) => hash.update(c));
r.pipe(out);
out.on("finish", () => {
const got = hash.digest("hex").toLowerCase();
if (got !== entry.sha256) {
try { fs.unlinkSync(dst); } catch {}
return reject(new Error(`SHA-256 mismatch: got ${got}, want ${entry.sha256}`));
}
resolve(dst);
});
out.on("error", (e) => { try { fs.unlinkSync(dst); } catch {}; reject(e); });
});
req.on("timeout", () => req.destroy(new Error("download timeout")));
req.on("error", reject);
req.end();
});
}
function ariadneQueryState() {
return new Promise((resolve) => {
const { spawn } = require("child_process");
@ -3014,19 +3087,23 @@ function ariadneQueryState() {
], { windowsHide: true });
let out = "";
ps.stdout.on("data", (d) => { out += d; });
ps.on("close", () => {
ps.on("close", async () => {
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"];
const installedVersion = map.__VER__ || null;
const quietUninstall = map.__UNINSTALL__ || null;
const bundled = ariadneBundledInstaller();
const canUpdate = installedVersion && bundled.version && cmpVersions(bundled.version, installedVersion) > 0;
const latest = await ariadneManifestFetch();
const latestVersion = latest ? latest.version : null;
const canUpdate = !!(installedVersion && latestVersion && cmpVersions(latestVersion, installedVersion) > 0);
let state;
if (!primary || primary === "MISSING") state = installedVersion ? "stopped" : "not-installed";
else if (primary === "Running") state = "running";
else state = "stopped";
resolve({ state, installedVersion, bundledVersion: bundled.version, canUpdate, hasUninstaller: !!quietUninstall });
// The "bundledVersion" field name is kept for renderer compatibility --
// it now carries the latest version advertised by silentmode.st's
// releases manifest, not a version physically bundled with Theseus.
resolve({ state, installedVersion, bundledVersion: latestVersion, canUpdate, hasUninstaller: !!quietUninstall });
});
ps.on("error", () => resolve({ state: "not-installed", installedVersion: null, bundledVersion: null, canUpdate: false, hasUninstaller: false }));
});
@ -3049,19 +3126,22 @@ function ariadneSetState(on) {
ps.on("error", (e) => reject(e));
});
}
// Run the bundled Ariadne installer silently, elevated. Inno Setup with
// /VERYSILENT /SUPPRESSMSGBOXES /NORESTART finishes without user interaction
// after the initial UAC prompt.
function ariadneInstall() {
const bundled = ariadneBundledInstaller();
if (!bundled.path) return Promise.reject(new Error("bundled installer not found"));
// Fetch manifest -> download+verify AriadneResolver-Setup-<v>.exe -> spawn
// Inno silently+elevated (/VERYSILENT /SUPPRESSMSGBOXES /NORESTART). The
// downloaded .exe is deleted whether the install succeeds or fails, so a
// wrong-hash abort never leaves a suspect binary behind.
async function ariadneInstall() {
const entry = await ariadneManifestFetch();
if (!entry) throw new Error("could not reach silentmode.st releases manifest");
const exePath = await ariadneDownloadInstaller(entry);
const { spawn } = require("child_process");
return new Promise((resolve, reject) => {
const ps = spawn("powershell.exe", ["-NoProfile", "-Command",
`Start-Process -FilePath '${bundled.path.replace(/'/g, "''")}' -ArgumentList '/VERYSILENT','/SUPPRESSMSGBOXES','/NORESTART' -Verb RunAs -Wait`
`Start-Process -FilePath '${exePath.replace(/'/g, "''")}' -ArgumentList '/VERYSILENT','/SUPPRESSMSGBOXES','/NORESTART' -Verb RunAs -Wait`
], { windowsHide: true });
ps.on("close", (code) => code === 0 ? resolve(true) : reject(new Error("installer exited " + code)));
ps.on("error", reject);
const cleanup = () => { try { fs.unlinkSync(exePath); } catch {} };
ps.on("close", (code) => { cleanup(); code === 0 ? resolve(true) : reject(new Error("installer exited " + code)); });
ps.on("error", (e) => { cleanup(); reject(e); });
});
}
// Run Inno's own quiet uninstaller. Reads the QuietUninstallString from the

View file

@ -1,100 +0,0 @@
; Theseus Navigator NSIS include bundles Ariadne's Thread as an opt-out.
;
; UI: a dedicated wizard page with a real checkbox (pre-checked) instead of a
; MessageBox popup. Shown FIRST in the install flow because electron-builder's
; template places our `!include` before its own MUI_PAGE_* inserts and NSIS
; processes page directives in file order so this is the earliest hook
; we have without editing the template. Framed as "Options" so it reads
; naturally as a preamble before the standard Welcome/License/Directory pages.
; Skipped entirely (`Abort`) if Ariadne's Thread is already installed, so
; upgrades of Theseus don't nag.
;
; The uninstall side stays a symmetric MessageBox (a full custom uninstaller
; page is overkill for one yes/no during a rare event).
!include "nsDialogs.nsh"
!include "LogicLib.nsh"
!include "MUI2.nsh"
Var AriadneCheckbox
Var InstallAriadneFlag
!define ARIADNE_INSTALLER "AriadneResolver-Setup-0.1.0.exe"
!define ARIADNE_UNINST_KEY "Software\Microsoft\Windows\CurrentVersion\Uninstall\{7E7A5F1C-3B4E-4C8A-9E1D-ARIADNERSLVR}_is1"
Page custom AriadnePageCreate AriadnePageLeave
Function AriadnePageCreate
; Silent install (/S) no user, no page. Default to "don't touch Ariadne";
; the whole point of /S is unattended, and existing Ariadne stays as-is.
; nsDialogs::Create in silent mode returns a dialog handle we can't display,
; so we short-circuit BEFORE calling any nsDialogs API.
IfSilent ariadne_page_silent ariadne_page_check
ariadne_page_silent:
StrCpy $InstallAriadneFlag "0"
Return
ariadne_page_check:
; Skip the page entirely if Ariadne's Thread is already on this machine
; nothing to offer. Registry lookup uses HKLM 64-bit view because Ariadne's
; Inno script sets ArchitecturesInstallIn64BitMode=x64compatible.
SetRegView 64
ReadRegStr $R0 HKLM "${ARIADNE_UNINST_KEY}" "UninstallString"
SetRegView lastused
StrCmp $R0 "" ariadne_page_show 0
StrCpy $InstallAriadneFlag "0"
Abort
ariadne_page_show:
!insertmacro MUI_HEADER_TEXT "Optional add-ons" "Choose whether to also install Ariadne's Thread alongside Theseus Navigator."
nsDialogs::Create 1018
Pop $0
StrCmp $0 "error" ariadne_page_bail 0
${NSD_CreateLabel} 0 0 100% 44u "Ariadne's Thread is a small system-wide resolver that lets BCDN — Bitcoin Cash Domain Names — work in Chrome, Edge, Firefox, and every other browser on this machine.$\r$\n$\r$\nWithout it, only Theseus resolves these names. You can install or remove it later."
Pop $0
${NSD_CreateCheckbox} 0 60u 100% 12u "Install Ariadne's Thread (recommended)"
Pop $AriadneCheckbox
${NSD_Check} $AriadneCheckbox
${NSD_CreateLabel} 0 80u 100% 20u "It runs a UAC (admin) prompt of its own during install. Theseus Navigator's own setup continues after this page."
Pop $0
nsDialogs::Show
Return
ariadne_page_bail:
StrCpy $InstallAriadneFlag "0"
Abort
FunctionEnd
Function AriadnePageLeave
${NSD_GetState} $AriadneCheckbox $InstallAriadneFlag
FunctionEnd
!macro customInstall
; Read the state saved from AriadnePageLeave. BST_CHECKED == 1.
${If} $InstallAriadneFlag == ${BST_CHECKED}
; Ariadne's Inno installer runs its own UAC prompt (PrivilegesRequired=admin).
; ExecWait blocks until Ariadne finishes or the user cancels UAC; either
; way, Theseus install continues normally afterwards.
ExecWait '"$INSTDIR\resources\${ARIADNE_INSTALLER}" /VERYSILENT /SUPPRESSMSGBOXES /NORESTART'
${EndIf}
!macroend
!macro customUnInstall
; Only offer to remove Ariadne if it's actually installed.
SetRegView 64
ReadRegStr $R0 HKLM "${ARIADNE_UNINST_KEY}" "UninstallString"
SetRegView lastused
StrCmp $R0 "" ariadne_skip_uninstall
MessageBox MB_YESNO|MB_ICONQUESTION|MB_DEFBUTTON1 \
"Also uninstall Ariadne's Thread?$\r$\n$\r$\nRemoving it makes Bitcoin Cash names stop resolving in Chrome, Edge, Firefox, and every other browser on this machine. (Theseus's own uninstall does not depend on this — say No to keep the system-wide resolver.)" \
IDNO ariadne_skip_uninstall
ExecWait '$R0 /VERYSILENT /SUPPRESSMSGBOXES /NORESTART'
ariadne_skip_uninstall:
!macroend