diff --git a/main.js b/main.js index 6a1327a5..d4353724 100644 --- a/main.js +++ b/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) => { diff --git a/package.json b/package.json index 5006b903..4b97bb82 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/settings-preload.js b/settings-preload.js index db4ad64f..c5850587 100644 --- a/settings-preload.js +++ b/settings-preload.js @@ -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"), }); diff --git a/settings.html b/settings.html index af302b6b..6f7970eb 100644 --- a/settings.html +++ b/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 @@ Theseus ID Performance Privacy + Names & Certificates Plug-ins Extensions Apps @@ -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. --> + + +