Theseus 0.3.95: Names & Certificates settings — UI shell for extra resolvers
New top-level settings page lets the user pre-configure which non-BNS name systems Theseus will resolve (ENS, Namecoin, Handshake, SNS, Unstoppable) and which extra TLS roots it will trust (Russian National CA, scoped by TLD). BNS is always on; everything else is off by default. Each resolver row has its own Advanced disclosure for RPC URL, gateway, endpoint and collision policy. The Russian CA row's Advanced holds the comma-separated TLD scope list that limits what the CA can sign for (default .ru/.рф/.gov.ru/.online.ru). State persists to %APPDATA%\Theseus\extra-resolvers.json via settings-only IPC (extra-resolvers-get / extra-resolvers-set / extra-resolvers-reset, origin-gated to the settings page). The adapters and setCertificateVerifyProc hook that read this file land in 0.3.96 (ENS) → 0.3.99 (Russian CA); each row's "Not wired yet — coming in 0.3.9X" caption goes away as its adapter ships. No home.html change in this release.
This commit is contained in:
parent
8eae448dde
commit
5d34fa18af
4 changed files with 298 additions and 2 deletions
73
main.js
73
main.js
|
|
@ -877,6 +877,59 @@ const homeCardsFile = () => path.join(app.getPath("userData"), "home-cards.json"
|
|||
const homeCardsRemoteCache = () => path.join(app.getPath("userData"), "home-cards-remote.json");
|
||||
const HOME_CARDS_URL = "https://dl.silentmode.st/home-cards.json";
|
||||
const HOME_CARDS_REFRESH_MS = 6 * 60 * 60 * 1000; // every 6h
|
||||
|
||||
// Extra resolvers + CA trust (Names & Certificates settings page, 0.3.95+).
|
||||
// UI shell lands first; the resolver adapters and setCertificateVerifyProc
|
||||
// hook read this file in 0.3.96–0.3.99. Everything off by default; BNS stays
|
||||
// on because it runs via Ariadne, not this file.
|
||||
const extraResolversFile = () => path.join(app.getPath("userData"), "extra-resolvers.json");
|
||||
const DEFAULT_EXTRA_RESOLVERS = {
|
||||
version: 1,
|
||||
resolvers: {
|
||||
ens: { enabled: false, rpc: "https://cloudflare-eth.com", gateway: "https://cloudflare-ipfs.com" },
|
||||
namecoin: { enabled: false, rpc: "" },
|
||||
handshake: { enabled: false, endpoint: "https://hnsrecursive.easyhandshake.com", collision_policy: "icann-wins" },
|
||||
sns: { enabled: false, rpc: "https://api.mainnet-beta.solana.com" },
|
||||
unstoppable: { enabled: false, endpoint: "https://resolve.unstoppabledomains.com" },
|
||||
},
|
||||
ca_bundles: {
|
||||
ru_national: { enabled: false, scope_tlds: ["ru", "рф", "gov.ru", "online.ru"] },
|
||||
},
|
||||
};
|
||||
function cloneExtraResolvers() { return JSON.parse(JSON.stringify(DEFAULT_EXTRA_RESOLVERS)); }
|
||||
function loadExtraResolvers() {
|
||||
try {
|
||||
if (fs.existsSync(extraResolversFile())) {
|
||||
const raw = JSON.parse(fs.readFileSync(extraResolversFile(), "utf8"));
|
||||
// Shallow-merge against defaults so a new field we add in a later version
|
||||
// shows up with its default value for existing users without a wipe.
|
||||
const d = cloneExtraResolvers();
|
||||
const out = { ...d, ...raw };
|
||||
out.resolvers = { ...d.resolvers };
|
||||
for (const k of Object.keys(d.resolvers)) out.resolvers[k] = { ...d.resolvers[k], ...(raw.resolvers?.[k] || {}) };
|
||||
out.ca_bundles = { ...d.ca_bundles };
|
||||
for (const k of Object.keys(d.ca_bundles)) out.ca_bundles[k] = { ...d.ca_bundles[k], ...(raw.ca_bundles?.[k] || {}) };
|
||||
return out;
|
||||
}
|
||||
} catch (e) { console.error("extra-resolvers load failed:", e.message); }
|
||||
return cloneExtraResolvers();
|
||||
}
|
||||
function saveExtraResolvers(state) {
|
||||
try { fs.writeFileSync(extraResolversFile(), JSON.stringify(state, null, 2)); }
|
||||
catch (e) { console.error("extra-resolvers save failed:", e.message); }
|
||||
}
|
||||
// Dotted-path write: setDotted(obj, "resolvers.ens.enabled", true) sets
|
||||
// obj.resolvers.ens.enabled = true, creating intermediate objects as needed.
|
||||
function setDotted(obj, path, value) {
|
||||
const parts = String(path || "").split(".").filter(Boolean);
|
||||
if (!parts.length) return;
|
||||
let o = obj;
|
||||
for (let i = 0; i < parts.length - 1; i++) {
|
||||
if (!o[parts[i]] || typeof o[parts[i]] !== "object") o[parts[i]] = {};
|
||||
o = o[parts[i]];
|
||||
}
|
||||
o[parts[parts.length - 1]] = value;
|
||||
}
|
||||
// Keep a stable v3-shaped clone of the defaults so a caller mutating what we
|
||||
// hand them (edit-mode runs do that) never pollutes the module-level seed.
|
||||
function cloneDefaults() { return JSON.parse(JSON.stringify(DEFAULT_HOME_STATE)); }
|
||||
|
|
@ -7644,6 +7697,26 @@ ipcMain.handle("home-cards-set", (e, state) => {
|
|||
saveHomeState(state); return true;
|
||||
});
|
||||
ipcMain.handle("home-cards-reset", (e) => { if (!isHomePageSender(e.sender)) return false; try { fs.unlinkSync(homeCardsFile()); } catch {} return true; });
|
||||
// Names & Certificates (0.3.95 UI shell). Settings-page-only; the adapters
|
||||
// in 0.3.96+ read loadExtraResolvers() internally — not through IPC — so the
|
||||
// renderer never gets more than its own choices back.
|
||||
ipcMain.handle("extra-resolvers-get", (e) => {
|
||||
if (!isSettingsSender(e)) throw new Error("extra-resolvers-get: settings only");
|
||||
return loadExtraResolvers();
|
||||
});
|
||||
ipcMain.handle("extra-resolvers-set", (e, key, value) => {
|
||||
if (!isSettingsSender(e)) throw new Error("extra-resolvers-set: settings only");
|
||||
if (typeof key !== "string" || !key) return false;
|
||||
const state = loadExtraResolvers();
|
||||
setDotted(state, key, value);
|
||||
saveExtraResolvers(state);
|
||||
return true;
|
||||
});
|
||||
ipcMain.handle("extra-resolvers-reset", (e) => {
|
||||
if (!isSettingsSender(e)) throw new Error("extra-resolvers-reset: settings only");
|
||||
try { fs.unlinkSync(extraResolversFile()); } catch {}
|
||||
return true;
|
||||
});
|
||||
ipcMain.handle("go-back", () => { const wc = activeTab()?.view.webContents; if (wc?.navigationHistory.canGoBack()) wc.navigationHistory.goBack(); });
|
||||
ipcMain.handle("go-forward", () => { const wc = activeTab()?.view.webContents; if (wc?.navigationHistory.canGoForward()) wc.navigationHistory.goForward(); });
|
||||
ipcMain.handle("reload", (_e, hard) => {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "theseus-navigator",
|
||||
"version": "0.3.94",
|
||||
"version": "0.3.95",
|
||||
"license": "MPL-2.0",
|
||||
"description": "Theseus Navigator — a browser that follows the thread. By Silent Mode, a Deviant project.",
|
||||
"author": "Silent Mode",
|
||||
|
|
|
|||
|
|
@ -114,4 +114,9 @@ contextBridge.exposeInMainWorld("cfg", {
|
|||
onWebappsChanged: (cb) => ipcRenderer.on("webapps-changed", () => cb()),
|
||||
appVersion: () => ipcRenderer.invoke("app-version"),
|
||||
restartApp: () => ipcRenderer.invoke("app-restart"),
|
||||
// Names & Certificates (0.3.95 — UI shell). State persists in
|
||||
// %APPDATA%\Theseus\extra-resolvers.json; adapters read it in 0.3.96+.
|
||||
resolversGet: () => ipcRenderer.invoke("extra-resolvers-get"),
|
||||
resolversSet: (key, value) => ipcRenderer.invoke("extra-resolvers-set", String(key || ""), value),
|
||||
resolversReset: () => ipcRenderer.invoke("extra-resolvers-reset"),
|
||||
});
|
||||
|
|
|
|||
220
settings.html
220
settings.html
|
|
@ -42,6 +42,18 @@
|
|||
.srow::before{content:"";width:9px;height:9px;border-radius:50%;background:var(--dim);flex:none}
|
||||
.srow.ok::before{background:#3ddc84}.srow.warn::before{background:#f5a524}
|
||||
.row.link{cursor:pointer}.row.link:hover{border-color:rgb(from var(--acid) r g b / .4)}.row .chev{color:var(--dim);font-size:20px;line-height:1}
|
||||
/* Advanced-disclosure block: hidden by default, click Advanced to reveal
|
||||
per-resolver RPC/endpoint/scope fields in Names & Certificates. */
|
||||
details.adv{margin-top:4px}
|
||||
details.adv>summary{cursor:pointer;color:var(--mut);font-size:12.5px;list-style:none;padding:2px 0;user-select:none}
|
||||
details.adv>summary::-webkit-details-marker{display:none}
|
||||
details.adv>summary::before{content:"▸ ";color:var(--dim);display:inline-block;width:1em;transition:transform .15s}
|
||||
details.adv[open]>summary::before{content:"▾ "}
|
||||
details.adv>summary:hover{color:var(--acid-text)}
|
||||
details.adv label{display:block;color:var(--mut);font-size:12px;margin:10px 0 4px;letter-spacing:.04em;text-transform:uppercase}
|
||||
details.adv input[type=text],details.adv select{width:100%;max-width:460px;background:#1b2330;color:var(--ink);border:1px solid var(--line);border-radius:8px;padding:7px 10px;font-size:13px;outline:none}
|
||||
details.adv input[type=text]:focus,details.adv select:focus{border-color:#4b7bec}
|
||||
.muted{color:var(--mut)}
|
||||
/* Plug-in main-page row: the .txt area is a click target that navigates to
|
||||
the plug-in's own settings sub-page (chevron on the title); .plug-ctl on
|
||||
the right hosts the update button + on/off toggle, and stops the click
|
||||
|
|
@ -308,6 +320,7 @@
|
|||
<a data-sec="theseusid">Theseus ID</a>
|
||||
<a data-sec="performance">Performance</a>
|
||||
<a data-sec="privacy">Privacy</a>
|
||||
<a data-sec="names">Names & Certificates</a>
|
||||
<a data-sec="plugins">Plug-ins</a>
|
||||
<a data-sec="addons">Extensions</a>
|
||||
<a data-sec="apps">Apps</a>
|
||||
|
|
@ -445,6 +458,127 @@
|
|||
the TITLE opens that plug-in's own settings sub-page (details +
|
||||
Uninstall). Toggle and buttons stopPropagation so they don't
|
||||
navigate. -->
|
||||
<!-- NAMES & CERTIFICATES — resolvers for other blockchain name systems,
|
||||
opt-in CA bundles scoped by TLD. Phase 1 (0.3.95): schema + UI only;
|
||||
toggles persist to extra-resolvers.json. Phase 2 (0.3.96–0.3.99): the
|
||||
resolver adapters and the setCertificateVerifyProc hook land under
|
||||
these toggles, no further UI churn. Each row's "Not wired yet" caption
|
||||
goes away as its adapter lights up. -->
|
||||
<section id="names" hidden>
|
||||
<h1>Names & Certificates</h1>
|
||||
<p class="lede">Decide which name systems Theseus can resolve, and which certificate roots it will trust. BNS (<i>.x</i>, <i>.bch</i>, <i>.sia</i>, …) is always on; everything else is off by default.</p>
|
||||
|
||||
<h2 class="sub" style="border-top:0;padding-top:0;margin-top:0">Name resolvers</h2>
|
||||
|
||||
<div class="row">
|
||||
<div class="txt">
|
||||
<div class="t">Bitcoin Cash Name System (BNS)</div>
|
||||
<div class="d"><i>.x</i>, <i>.bch</i>, <i>.sia</i>, <i>.coin</i>, <i>.dgb</i>, and the rest of the seeded TLDs. Powered by <a data-go="plugins/ariadne" style="color:var(--acid-text);cursor:pointer">Ariadne's Thread</a>.</div>
|
||||
</div>
|
||||
<label class="sw" title="BNS is built in and always on"><input type="checkbox" checked disabled><span class="track"><span class="knob"></span></span></label>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="txt">
|
||||
<div class="t">Ethereum Name Service (ENS)</div>
|
||||
<div class="d"><i>.eth</i> → content hash from the ENS registry on Ethereum, served via an IPFS/Arweave gateway. <span class="muted" id="rvNotWiredEns">Not wired yet — coming in 0.3.96.</span></div>
|
||||
<div class="acts"><details class="adv"><summary>Advanced</summary>
|
||||
<label>Ethereum RPC URL</label><input type="text" id="rvEnsRpc" placeholder="https://cloudflare-eth.com">
|
||||
<label>IPFS gateway</label><input type="text" id="rvEnsGateway" placeholder="https://cloudflare-ipfs.com">
|
||||
</details></div>
|
||||
</div>
|
||||
<label class="sw"><input type="checkbox" id="rvEnsOn"><span class="track"><span class="knob"></span></span></label>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="txt">
|
||||
<div class="t">Namecoin</div>
|
||||
<div class="d"><i>.bit</i> → DNS record from the Namecoin blockchain. <span class="muted" id="rvNotWiredNmc">Not wired yet — coming in 0.3.97.</span></div>
|
||||
<div class="acts"><details class="adv"><summary>Advanced</summary>
|
||||
<label>Namecoin JSON-RPC URL</label><input type="text" id="rvNmcRpc" placeholder="http://user:pass@127.0.0.1:8336">
|
||||
</details></div>
|
||||
</div>
|
||||
<label class="sw"><input type="checkbox" id="rvNmcOn"><span class="track"><span class="knob"></span></span></label>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="txt">
|
||||
<div class="t">Handshake (HNS)</div>
|
||||
<div class="d">User-registered TLDs on the Handshake chain. Handshake auctioned many 3-letter and ICANN-style TLDs on-chain, so collisions are common. <span class="muted" id="rvNotWiredHns">Not wired yet — coming in 0.3.97.</span></div>
|
||||
<div class="acts"><details class="adv"><summary>Advanced</summary>
|
||||
<label>Handshake resolver endpoint</label><input type="text" id="rvHnsEndpoint" placeholder="https://hnsrecursive.easyhandshake.com">
|
||||
<label>Collision policy</label>
|
||||
<select id="rvHnsCollisionPolicy">
|
||||
<option value="icann-wins">ICANN wins (safer; HNS only resolves TLDs the DNS root doesn't delegate)</option>
|
||||
<option value="hns-wins">HNS wins (prefer the blockchain's answer; may change where familiar TLDs point)</option>
|
||||
</select>
|
||||
</details></div>
|
||||
</div>
|
||||
<label class="sw"><input type="checkbox" id="rvHnsOn"><span class="track"><span class="knob"></span></span></label>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="txt">
|
||||
<div class="t">Solana Name Service (SNS)</div>
|
||||
<div class="d"><i>.sol</i> → owner record from the Solana name registry. <span class="muted" id="rvNotWiredSns">Not wired yet — coming in 0.3.98.</span></div>
|
||||
<div class="acts"><details class="adv"><summary>Advanced</summary>
|
||||
<label>Solana RPC URL</label><input type="text" id="rvSnsRpc" placeholder="https://api.mainnet-beta.solana.com">
|
||||
</details></div>
|
||||
</div>
|
||||
<label class="sw"><input type="checkbox" id="rvSnsOn"><span class="track"><span class="knob"></span></span></label>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="txt">
|
||||
<div class="t">Unstoppable Domains</div>
|
||||
<div class="d"><i>.crypto</i>, <i>.nft</i>, <i>.wallet</i>, <i>.bitcoin</i>, <i>.blockchain</i>, <i>.dao</i>, <i>.888</i>, <i>.polygon</i>, <i>.unstoppable</i>, and more.
|
||||
<br>⚠ <i>.x</i> also exists on Unstoppable — BNS wins on <i>.x</i>, so a Sirius registration always takes precedence. <span class="muted" id="rvNotWiredUd">Not wired yet — coming in 0.3.98.</span></div>
|
||||
<div class="acts"><details class="adv"><summary>Advanced</summary>
|
||||
<label>Resolver endpoint</label><input type="text" id="rvUdEndpoint" placeholder="https://resolve.unstoppabledomains.com">
|
||||
</details></div>
|
||||
</div>
|
||||
<label class="sw"><input type="checkbox" id="rvUdOn"><span class="track"><span class="knob"></span></span></label>
|
||||
</div>
|
||||
|
||||
<h2 class="sub">Certificate trust (TLS)</h2>
|
||||
|
||||
<div class="row">
|
||||
<div class="txt">
|
||||
<div class="t">Built-in Mozilla CA bundle</div>
|
||||
<div class="d">The standard trust anchors that ship with Theseus.</div>
|
||||
</div>
|
||||
<label class="sw" title="Always on"><input type="checkbox" checked disabled><span class="track"><span class="knob"></span></span></label>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="txt">
|
||||
<div class="t">Russian National CA</div>
|
||||
<div class="d">
|
||||
Lets Theseus open Russian government sites (gosuslugi.ru, nalog.gov.ru, …) that use the Russian national cert chain. <b>Does not</b> bypass network-level blocks — use Tor for that.
|
||||
<br>⚠ Trust is scoped to the TLDs below only. The CA cannot sign for other sites while this scope is in place. Mozilla and Chrome do not trust this CA by default. <span class="muted" id="rvNotWiredRuCa">Not wired yet — coming in 0.3.99.</span>
|
||||
</div>
|
||||
<div class="acts"><details class="adv"><summary>Advanced</summary>
|
||||
<label>Scope — this CA is trusted only for hosts under these TLDs (comma-separated)</label>
|
||||
<input type="text" id="caRuScope" placeholder="ru, рф, gov.ru, online.ru">
|
||||
</details></div>
|
||||
</div>
|
||||
<label class="sw"><input type="checkbox" id="caRuOn"><span class="track"><span class="knob"></span></span></label>
|
||||
</div>
|
||||
|
||||
<h2 class="sub">Preferences file</h2>
|
||||
<div class="row">
|
||||
<div class="txt">
|
||||
<div class="t">Reset to defaults</div>
|
||||
<div class="d">Clears every switch above and restores the defaults (BNS on, everything else off). Does not affect site history.</div>
|
||||
<div class="acts"><button class="btn small ghost" id="rvReset">Reset Names & Certificates</button></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p class="muted" style="margin-top:1.4rem;font-size:12.5px;line-height:1.5">
|
||||
Your choices are stored in <code>%APPDATA%\Theseus\extra-resolvers.json</code>. They land in the file now and will come alive as each resolver is wired — the plan is 0.3.96 ENS, 0.3.97 Namecoin + Handshake, 0.3.98 SNS + Unstoppable, 0.3.99 Russian CA.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section id="plugins" hidden>
|
||||
<h1>Plug-ins</h1>
|
||||
<p class="lede">Silent Mode components that live alongside Theseus — each with its own on/off, updates and settings page.</p>
|
||||
|
|
@ -816,6 +950,10 @@
|
|||
<div class="txt"><div class="t">Hide media devices</div><div class="d">Blank the labels and IDs of all cameras, microphones <b>and speakers</b> from <code>enumerateDevices()</code> — closes a WebRTC fingerprinting leak, like Firefox.</div></div>
|
||||
<label class="sw"><input type="checkbox" id="hideMediaDevices"><span class="track"><span class="knob"></span></span></label>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="txt"><div class="t">Block screen capture</div><div class="d">Refuse <code>getDisplayMedia()</code> outright, so no site or extension can even ask. Off by default because screen capture always opens a picker first — nothing is shared until you choose a screen or window and confirm.</div></div>
|
||||
<label class="sw"><input type="checkbox" id="blockScreenCapture"><span class="track"><span class="knob"></span></span></label>
|
||||
</div>
|
||||
|
||||
<h2 class="sub">Anti-fingerprinting</h2>
|
||||
<p class="subd">For each: <b>Show</b> (real), <b>Hide</b> (neutral value), <b>Spoof</b> (a decoy), or <b>Manual</b> (set your own).</p>
|
||||
|
|
@ -1272,7 +1410,7 @@
|
|||
// Running counts change as extensions start; refresh when the page is shown.
|
||||
document.addEventListener("visibilitychange", () => { if (!document.hidden) refresh(); });
|
||||
})();
|
||||
const TOGGLES = ["restoreSession", "backgroundThrottle", "freezeBackgroundTabs", "extensionsOnDemand", "walletAtLaunch", "preloadMenus", "blockCamera", "blockMicrophone", "hideMediaDevices", "gpc",
|
||||
const TOGGLES = ["restoreSession", "backgroundThrottle", "freezeBackgroundTabs", "extensionsOnDemand", "walletAtLaunch", "preloadMenus", "blockCamera", "blockMicrophone", "hideMediaDevices", "blockScreenCapture", "gpc",
|
||||
"clearCookiesOnQuit", "keepSignInsOnQuit", "clearCacheOnQuit", "clearStorageOnQuit", "clearHistoryOnQuit",
|
||||
"pwOfferSave", "pwOfferFill",
|
||||
"quickLinksShow", "translateAutoOffer",
|
||||
|
|
@ -2817,6 +2955,7 @@
|
|||
"open-tab": "Can open its own pages as full tabs.",
|
||||
"page-inject": "Runs code inside the web pages it declares — it can read and change what those pages show.",
|
||||
"capture-tab": "Can take screenshots of the current tab.",
|
||||
"screen-capture": "Can ask to record a screen or window — including other applications — and save it to Downloads. You pick the source each time.",
|
||||
"vault-derive": "Derives its own keys from your password vault. It never sees the vault itself.",
|
||||
"approval-modal": "Can ask you to approve an action in a dialog over the page.",
|
||||
"session-proxy": "Can route the browser's traffic through a proxy it controls.",
|
||||
|
|
@ -3102,6 +3241,85 @@
|
|||
});
|
||||
// Populate on first paint so the tab is ready when the user clicks in.
|
||||
(async () => { await loadAddonUpdates(); loadAddons(); })();
|
||||
|
||||
// Names & Certificates (0.3.95 — UI shell only; adapters land in 0.3.96–0.3.99).
|
||||
// The toggles + Advanced fields persist to %APPDATA%\Theseus\extra-resolvers.json
|
||||
// via main's extra-resolvers-get/set IPC. Nothing resolves yet; this is the
|
||||
// user's chance to pre-configure defaults before each adapter ships.
|
||||
(function () {
|
||||
if (!C || !C.resolversGet || !C.resolversSet) return; // older preload — skip
|
||||
const el = (i) => document.getElementById(i);
|
||||
const BINDS = [
|
||||
// id key type
|
||||
["rvEnsOn", "resolvers.ens.enabled", "bool"],
|
||||
["rvEnsRpc", "resolvers.ens.rpc", "text"],
|
||||
["rvEnsGateway", "resolvers.ens.gateway", "text"],
|
||||
["rvNmcOn", "resolvers.namecoin.enabled", "bool"],
|
||||
["rvNmcRpc", "resolvers.namecoin.rpc", "text"],
|
||||
["rvHnsOn", "resolvers.handshake.enabled", "bool"],
|
||||
["rvHnsEndpoint", "resolvers.handshake.endpoint", "text"],
|
||||
["rvHnsCollisionPolicy", "resolvers.handshake.collision_policy", "text"],
|
||||
["rvSnsOn", "resolvers.sns.enabled", "bool"],
|
||||
["rvSnsRpc", "resolvers.sns.rpc", "text"],
|
||||
["rvUdOn", "resolvers.unstoppable.enabled","bool"],
|
||||
["rvUdEndpoint", "resolvers.unstoppable.endpoint","text"],
|
||||
["caRuOn", "ca_bundles.ru_national.enabled","bool"],
|
||||
["caRuScope", "ca_bundles.ru_national.scope_tlds","csv"],
|
||||
];
|
||||
function dig(obj, path) {
|
||||
return path.split(".").reduce((o, k) => (o == null ? o : o[k]), obj);
|
||||
}
|
||||
function hydrate(state) {
|
||||
for (const [id, key, type] of BINDS) {
|
||||
const node = el(id); if (!node) continue;
|
||||
const v = dig(state, key);
|
||||
if (type === "bool") node.checked = !!v;
|
||||
else if (type === "csv") node.value = Array.isArray(v) ? v.join(", ") : (v || "");
|
||||
else node.value = v != null ? String(v) : "";
|
||||
}
|
||||
}
|
||||
function readValue(id, type) {
|
||||
const node = el(id); if (!node) return null;
|
||||
if (type === "bool") return !!node.checked;
|
||||
if (type === "csv") return node.value.split(/\s*,\s*/).map((s) => s.trim()).filter(Boolean);
|
||||
return node.value;
|
||||
}
|
||||
async function save(id, key, type) {
|
||||
const value = readValue(id, type);
|
||||
try { await C.resolversSet(key, value); } catch (e) { console.warn("resolversSet failed", key, e); }
|
||||
}
|
||||
// Fire-once wiring. Load current state when the section first shows,
|
||||
// persist each change through resolversSet.
|
||||
let wired = false;
|
||||
async function ensureWired() {
|
||||
if (wired) return; wired = true;
|
||||
try {
|
||||
const state = await C.resolversGet();
|
||||
hydrate(state || {});
|
||||
} catch (e) { console.warn("resolversGet failed", e); }
|
||||
for (const [id, key, type] of BINDS) {
|
||||
const node = el(id); if (!node) continue;
|
||||
const evt = type === "bool" ? "change" : "change"; // input events fire too often for text fields
|
||||
node.addEventListener(evt, () => save(id, key, type));
|
||||
}
|
||||
const resetBtn = el("rvReset");
|
||||
if (resetBtn) {
|
||||
resetBtn.addEventListener("click", async () => {
|
||||
if (!confirm("Reset every Names & Certificates toggle to defaults? BNS stays on; everything else goes off and Advanced fields are cleared.")) return;
|
||||
try {
|
||||
if (C.resolversReset) await C.resolversReset();
|
||||
const state = await C.resolversGet();
|
||||
hydrate(state || {});
|
||||
} catch (e) { console.warn("resolversReset failed", e); }
|
||||
});
|
||||
}
|
||||
}
|
||||
document.addEventListener("section", (e) => { if (e.detail === "names") ensureWired(); });
|
||||
// If the user lands on #names directly, the section event already fired
|
||||
// during showSection() before this IIFE registered the listener, so we
|
||||
// kick wiring manually when the page is already visible.
|
||||
try { if (!document.getElementById("names").hidden) ensureWired(); } catch {}
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue