diff --git a/main.js b/main.js index 71022fff..a7ddce83 100644 --- a/main.js +++ b/main.js @@ -5623,6 +5623,66 @@ ipcMain.handle("ariadne-toggle", (_e, on) => ariadneSetState(!!on).catch((e) => ipcMain.handle("ariadne-install", () => ariadneInstall().then(() => ({ ok: true })).catch((e) => ({ ok: false, error: e?.message || String(e) }))); ipcMain.handle("ariadne-update", () => ariadneUpdate().then(() => ({ ok: true })).catch((e) => ({ ok: false, error: e?.message || String(e) }))); ipcMain.handle("ariadne-uninstall", () => ariadneUninstall().then(() => ({ ok: true })).catch((e) => ({ ok: false, error: e?.message || String(e) }))); + +// ---- Ariadne 0.1.13+ settings + status wiring -------------------------- +// The daemon's own read/write surface lives at http://127.0.0.1/api/status +// and C:\ProgramData\Ariadne\policy.json. The policy file is ACL'd user- +// writable by install.ps1, so all four of these run WITHOUT UAC. +const ARIADNE_POLICY_FILE = (() => { + const dir = process.env.PROGRAMDATA || "C:\\ProgramData"; + return path.join(dir, "Ariadne", "policy.json"); +})(); +function ariadneReadPolicyFile() { + try { + if (!fs.existsSync(ARIADNE_POLICY_FILE)) return {}; + const raw = fs.readFileSync(ARIADNE_POLICY_FILE, "utf8").replace(/^\uFEFF/, ""); + return JSON.parse(raw) || {}; + } catch { return {}; } +} +function ariadneWritePolicyFile(obj) { + try { + fs.mkdirSync(path.dirname(ARIADNE_POLICY_FILE), { recursive: true }); + fs.writeFileSync(ARIADNE_POLICY_FILE, JSON.stringify(obj, null, 2), "utf8"); + return { ok: true }; + } catch (e) { return { ok: false, error: e?.message || String(e) }; } +} +// GET http://127.0.0.1/api/status against the local daemon. Returns the full +// status document, or {ok:false, error} on any failure (daemon down, port +// blocked, localApi turned off in policy.json -> the daemon returns 503). +function ariadneFetchStatus() { + return new Promise((resolve) => { + const req = http.request({ host: "127.0.0.1", port: 80, path: "/api/status", method: "GET", headers: { "user-agent": "TheseusNavigator/ariadne-panel" } }, (r) => { + const chunks = []; + r.on("data", (c) => chunks.push(c)); + r.on("end", () => { + if (r.statusCode !== 200) return resolve({ ok: false, error: `daemon HTTP ${r.statusCode}`, body: Buffer.concat(chunks).toString("utf8").slice(0, 400) }); + try { resolve({ ok: true, status: JSON.parse(Buffer.concat(chunks).toString("utf8")) }); } + catch (e) { resolve({ ok: false, error: "malformed JSON from daemon: " + e.message }); } + }); + }); + req.setTimeout(4000, () => req.destroy(new Error("daemon timeout on 127.0.0.1/api/status"))); + req.on("error", (e) => resolve({ ok: false, error: e.message })); + req.end(); + }); +} +ipcMain.handle("ariadne-get-status", () => ariadneFetchStatus()); +ipcMain.handle("ariadne-get-policy", () => ({ ok: true, policy: ariadneReadPolicyFile() })); +ipcMain.handle("ariadne-set-policy", (_e, value) => { + const v = String(value || "").toLowerCase(); + if (!["bcnr-first", "icann-first"].includes(v)) return { ok: false, error: `invalid policy '${value}' (expected bcnr-first or icann-first)` }; + const cur = ariadneReadPolicyFile(); + cur.policy = v; + return ariadneWritePolicyFile(cur); +}); +ipcMain.handle("ariadne-set-source", (_e, name, enabled) => { + const known = ["snapshotHttps", "electrumWss", "perQueryLookup", "diskCache", "localApi"]; + if (!known.includes(String(name || ""))) return { ok: false, error: `unknown source '${name}' (known: ${known.join(", ")})` }; + const cur = ariadneReadPolicyFile(); + if (!cur.sources || typeof cur.sources !== "object") cur.sources = {}; + if (!cur.sources[name] || typeof cur.sources[name] !== "object") cur.sources[name] = {}; + cur.sources[name].enabled = !!enabled; + return ariadneWritePolicyFile(cur); +}); // 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 diff --git a/settings-preload.js b/settings-preload.js index ff7bb4a7..84bed9da 100644 --- a/settings-preload.js +++ b/settings-preload.js @@ -72,6 +72,13 @@ contextBridge.exposeInMainWorld("cfg", { ariadneInstall: () => ipcRenderer.invoke("ariadne-install"), ariadneUpdate: () => ipcRenderer.invoke("ariadne-update"), ariadneUninstall: () => ipcRenderer.invoke("ariadne-uninstall"), + // Local BNS daemon settings + status (0.1.13+). All read/write against + // C:\ProgramData\Ariadne\policy.json (user-writable ACL) and the daemon's + // own /api/status endpoint on 127.0.0.1. No UAC required for any of these. + ariadneGetStatus: () => ipcRenderer.invoke("ariadne-get-status"), + ariadneGetPolicy: () => ipcRenderer.invoke("ariadne-get-policy"), + ariadneSetPolicy: (policy) => ipcRenderer.invoke("ariadne-set-policy", String(policy || "")), + ariadneSetSource: (name, enabled) => ipcRenderer.invoke("ariadne-set-source", String(name || ""), !!enabled), // Manual "Check for updates" — un-dismisses any existing chip and re- // fetches the release manifest. Returns { updateAvailable, currentVersion }. recheckUpdate: () => ipcRenderer.invoke("recheck-update"), diff --git a/settings.html b/settings.html index 0e19ec4c..e3cac5db 100644 --- a/settings.html +++ b/settings.html @@ -105,6 +105,16 @@ .polrow{display:flex;align-items:center;gap:10px;background:#10151f;border:1px solid var(--line);border-radius:8px;padding:8px 12px;font-size:13px;cursor:pointer} .polrow:hover{background:#141c28} .polrow input{accent-color:var(--acid)} + /* Ariadne sources table -- 3-col grid instead of so it inherits our row styling and reflows nicely. */ + .ariadne-sources{display:flex;flex-direction:column;gap:1px;background:var(--line);border:1px solid var(--line);border-radius:8px;overflow:hidden;font-size:13px} + .ariadne-sources .asrc-head, .ariadne-sources .asrc-row{display:grid;grid-template-columns:1fr 70px minmax(180px,2fr);align-items:center;gap:12px;padding:8px 12px;background:#10151f} + .ariadne-sources .asrc-head{font-weight:600;color:var(--muted);background:#0c1119;font-size:12px;text-transform:uppercase;letter-spacing:.4px} + .ariadne-sources .asrc-row .aname{color:var(--ink);font-weight:500} + .ariadne-sources .asrc-row .aname small{display:block;color:var(--muted);font-weight:400;margin-top:2px;font-size:11.5px} + .ariadne-sources .asrc-row .astate{color:var(--muted);font-size:11.5px;font-family:ui-monospace,'SF Mono',Menlo,Consolas,monospace} + .ariadne-sources .asrc-row .astate.ok{color:var(--acid-text)} + .ariadne-sources .asrc-row .astate.err{color:#f6768a} + .ariadne-sources input[type=checkbox]{accent-color:var(--acid);width:18px;height:18px;cursor:pointer} .pmuted{color:var(--mut)} .ceng{display:flex;align-items:center;gap:8px;background:#10151f;border:1px solid var(--line);border-radius:8px;padding:6px 10px;margin-bottom:6px;font-size:13px} .ceng .cs{font-size:14px;flex:none} @@ -217,6 +227,12 @@ hover tints keyed to BCH dark for consistent depth. */ .polrow{ background:#f4f8fb; color:#253A49; } .polrow:hover{ background:#eaf0f5; } + .ariadne-sources{background:#e6ecf1;border-color:#e6ecf1} + .ariadne-sources .asrc-head, .ariadne-sources .asrc-row{background:#f4f8fb;color:#253A49} + .ariadne-sources .asrc-head{background:#eaf0f5;color:#5a7080} + .ariadne-sources .asrc-row .aname{color:#253A49} + .ariadne-sources .asrc-row .aname small{color:#5a7080} + #ariadneStatusJson{background:#f4f8fb !important;color:#253A49} .polrow input:checked ~ span, .polrow:has(input:checked){ background:rgb(from var(--acid) r g b / .10); border-color:color-mix(in srgb, var(--acid) 35%, transparent); } .segseg{ background:#eef1f6; } @@ -398,6 +414,28 @@ +

Collision policy

+

When a name exists on both BCNR and ICANN, which one wins? Applies machine-wide — every browser sees the same answer. Change is picked up within 5 seconds; no daemon restart, no UAC.

+
+ + +
+ +

Sources

+

Each index source can be toggled independently to isolate a path or measure without it. Changes hot-reload every 5 s via C:\ProgramData\Ariadne\policy.json — no restart. State column shows the last attempt result for each.

+
+
SourceEnabledState
+
+
+ +

Status report

+

Full snapshot of what the daemon currently sees — daemon version, permitted TLDs, index size, last fetch per source. Paste into a support conversation.

+
+ + +
+
Loading…
+

Remove

Uninstall the resolver from this machine. Theseus itself keeps working — its built-in resolver isn't affected.

@@ -1547,6 +1585,96 @@ arRefreshSub.onclick = refreshAriadne; refreshAriadne(); + // ---- Ariadne 0.1.13+ sub-page: policy + sources + status report ----- + // All read/write against the daemon's /api/status (over 127.0.0.1) and + // C:\ProgramData\Ariadne\policy.json (user-writable ACL) -- no UAC. + const arPolicyRadios = document.querySelectorAll('input[name="ariadnePolicy"]'); + const arSourcesBody = q("ariadneSourcesBody"); + const arStatusJson = q("ariadneStatusJson"); + const arStatusCopy = q("ariadneStatusCopy"); + const arStatusRefr = q("ariadneStatusRefresh"); + const SOURCE_DESCRIPTIONS = { + snapshotHttps: { label: "HTTPS snapshot (VPS + Sia)", hint: "dl.silentmode.st + s3.silentmode.st, polled every 30 s with ETag" }, + electrumWss: { label: "Electrum WSS (chipnet)", hint: "direct chain read via WebSocket; secondary refresh" }, + perQueryLookup: { label: "Per-query indexer fallback", hint: "navigate.st/api/name/<host> on index miss, 30-s LRU" }, + diskCache: { label: "Disk cache", hint: "C:\\ProgramData\\Ariadne\\bns-name-snapshot.json — read at boot, written on any HTTPS win" }, + localApi: { label: "Local BNS indexer API", hint: "http://127.0.0.1/api/tlds, /api/name/:n, /api/registry, /api/status" }, + }; + function fmtTime(iso) { + if (!iso) return "never"; + try { const d = new Date(iso); const s = Math.round((Date.now() - d.getTime()) / 1000); return s < 60 ? `${s} s ago` : s < 3600 ? `${Math.round(s/60)} m ago` : d.toLocaleString(); } catch { return String(iso); } + } + function sourceState(name, st) { + if (!st) return { text: "no state yet", ok: false, err: false }; + if (name === "diskCache") return { text: st.exists ? `${(st.size||0).toLocaleString()} B · ${fmtTime(st.mtime)}` : "no file yet", ok: !!st.exists, err: false }; + if (name === "localApi") return { text: st.enabled ? "listening" : "disabled", ok: !!st.enabled, err: false }; + if (name === "perQueryLookup") return { text: `${st.hits||0} hit · ${st.misses||0} miss · ${st.errors||0} err · last ${fmtTime(st.lastAttempt)}`, ok: (st.hits||0) > 0 || !st.lastAttempt, err: (st.errors||0) > 0 }; + if (st.lastError) return { text: `error: ${st.lastError.slice(0, 80)} · last ${fmtTime(st.lastAttempt)}`, ok: false, err: true }; + if (st.lastSuccess) return { text: `ok · last success ${fmtTime(st.lastSuccess)}` + (st.refreshes ? ` · ${st.refreshes} refresh${st.refreshes>1?"es":""}` : ""), ok: true, err: false }; + return { text: `waiting · last try ${fmtTime(st.lastAttempt)}`, ok: false, err: false }; + } + let arLastStatus = null; + async function refreshAriadneStatus() { + arStatusJson.textContent = "Loading…"; + arSourcesBody.innerHTML = ""; + const r = await C.ariadneGetStatus(); + if (!r || !r.ok) { + arStatusJson.textContent = `Cannot reach the daemon: ${r?.error || "unknown error"}\n\nIs Ariadne's Thread running? Turn it on from the Status section above.`; + arSourcesBody.innerHTML = '
Daemon unreachable — start it from Status above.
'; + return; + } + arLastStatus = r.status; + arStatusJson.textContent = JSON.stringify(r.status, null, 2); + const sources = r.status.sources || {}; + arSourcesBody.innerHTML = ""; + for (const name of Object.keys(SOURCE_DESCRIPTIONS)) { + const d = SOURCE_DESCRIPTIONS[name]; + const st = sources[name] || {}; + const s = sourceState(name, st); + const row = document.createElement("div"); + row.className = "asrc-row"; + row.innerHTML = `${d.label}${d.hint}` + + `` + + `${s.text}`; + arSourcesBody.appendChild(row); + } + // Wire the enable checkboxes AFTER they're in the DOM. + arSourcesBody.querySelectorAll('input[type="checkbox"][data-src]').forEach((cb) => { + cb.addEventListener("change", async () => { + cb.disabled = true; + const w = await C.ariadneSetSource(cb.dataset.src, cb.checked); + cb.disabled = false; + if (!w?.ok) { alert(`Could not save: ${w?.error || "unknown error"}`); cb.checked = !cb.checked; return; } + // Poll again after the daemon's 5-s hot-reload tick so state text catches up. + setTimeout(refreshAriadneStatus, 6000); + }); + }); + } + async function loadAriadnePolicy() { + const r = await C.ariadneGetPolicy(); + const p = (r?.policy?.policy || "bcnr-first").toLowerCase(); + arPolicyRadios.forEach((rb) => { rb.checked = (rb.value === p); }); + } + arPolicyRadios.forEach((rb) => { + rb.addEventListener("change", async () => { + if (!rb.checked) return; + const w = await C.ariadneSetPolicy(rb.value); + if (!w?.ok) alert(`Could not save policy: ${w?.error || "unknown error"}`); + }); + }); + arStatusRefr.onclick = refreshAriadneStatus; + arStatusCopy.onclick = async () => { + if (!arLastStatus) return; + try { await navigator.clipboard.writeText(JSON.stringify(arLastStatus, null, 2)); arStatusCopy.textContent = "Copied"; setTimeout(() => (arStatusCopy.textContent = "Copy report"), 1500); } + catch { arStatusCopy.textContent = "Copy failed"; setTimeout(() => (arStatusCopy.textContent = "Copy report"), 1500); } + }; + // Load once on script boot; also every time the user navigates INTO the + // Ariadne sub-page (fresh state, no cached-stale JSON on re-entry). + loadAriadnePolicy(); refreshAriadneStatus(); + document.addEventListener("section", (e) => { + if (e.detail === "plugins/ariadne") { loadAriadnePolicy(); refreshAriadneStatus(); } + }); + // AEGIS —————————————————————————————————————————————————————————— // Bundled add-on shipped OTA. Same addons-check-updates IPC the // Extensions page uses, filtered for the aegis id. Legacy id