diff --git a/main.js b/main.js index d374b82..eb0839c 100644 --- a/main.js +++ b/main.js @@ -237,6 +237,22 @@ function loadSettings() { // uses whatever the user left it at last time. const w = Number(settings.sidebarWidth) || SIDEBAR_W_DEFAULT; sidebarW = Math.max(SIDEBAR_W_MIN, Math.min(SIDEBAR_W_MAX, w)); + // Normalize the search-engine state so the toolbar picker and Settings tab + // can never disagree. Two invariants: + // 1. Every enabledEngines id must also be in installedEngines. If a user + // manually edited settings.json (or an upgrade left the two out of + // sync), we add the missing installed rows now. + // 2. settings.searchEngine must be an enabled engine. If the default from + // SETTINGS_DEFAULTS points at an id the user has disabled, fall back + // to the first currently-enabled engine. + try { + const enabled = Array.isArray(settings.enabledEngines) ? settings.enabledEngines : DEFAULT_ENABLED.slice(); + const installed = Array.isArray(settings.installedEngines) ? settings.installedEngines : DEFAULT_ENABLED.slice(); + settings.installedEngines = [...new Set([...installed, ...enabled])]; + if (!enabled.includes(settings.searchEngine)) { + settings.searchEngine = enabled[0] || DEFAULT_ENABLED[0]; + } + } catch (e) { console.warn("engine normalize:", e?.message); } } function saveSettings() { try { fs.writeFileSync(settingsFile(), JSON.stringify(settings, null, 2)); } catch (e) { console.error("settings save failed:", e.message); } @@ -310,14 +326,15 @@ function emitUpdateAvailable() { // can add/edit/remove via the page's edit mode. First-run seed = the classic // Silent Mode showcase (hello.bch, theseus.bch, silentmode.bch, etc.). const DEFAULT_HOME_CARDS = [ - { title: "hello.bch", url: "https://hello.bch/", sub: "On the blockchain itself.", badge: "on-chain" }, - { title: "theseus.bch", url: "https://theseus.bch/", sub: "This browser's own name.", badge: "on-chain" }, - { title: "silentmode.bch", url: "https://silentmode.bch/", sub: "The division behind this stack.", badge: "on-chain" }, - { title: "argo.bch", url: "https://argo.bch/", sub: "Host network, crewed by Argonauts.", badge: "on-chain" }, - { title: "coinspectrum.deviant.bch", url: "https://coinspectrum.deviant.bch/", sub: "A full site from Sia.", badge: "Sia" }, - { title: "coinspectrum.bch", url: "https://coinspectrum.bch/", sub: "Same site, direct server.", badge: "server" }, - { title: "faucet.deviant.bch", url: "https://faucet.deviant.bch/", sub: "Testnet faucet hub, on Sia.", badge: "Sia" }, - { title: "siatest.bch", url: "https://siatest.bch/", sub: "A page with no server.", badge: "Sia" }, + { title: "hello.bch", url: "https://hello.bch/", sub: "A small page on the blockchain itself.", badge: "on-chain" }, + { title: "siatest.bch", url: "https://siatest.bch/", sub: "A page with no server, backed by Sia.", badge: "Sia" }, + { title: "silentmode.x", url: "https://silentmode.x/", sub: "The project's .x address.", badge: ".x" }, + { title: "theseus.x", url: "https://theseus.x/", sub: "This browser's own address.", badge: ".x" }, + { title: "sirius.x", url: "https://sirius.x/", sub: "Register and manage BCDN names.", badge: ".x" }, + { title: "hephaestus.x", url: "https://hephaestus.x/", sub: "Code host — Silent Mode's Forgejo.", badge: ".x" }, + { title: "prometheus.x", url: "https://prometheus.x/", sub: "Storage-as-a-product.", badge: ".x" }, + { title: "helios.x", url: "https://helios.x/", sub: "Discovery + navigation.", badge: ".x" }, + { title: "hermes.x", url: "https://hermes.x/", sub: "Messaging — NIP-17 over Nostr.", badge: ".x" }, ]; const homeCardsFile = () => path.join(app.getPath("userData"), "home-cards.json"); function loadHomeCards() { @@ -1156,6 +1173,11 @@ let sidebarW = SIDEBAR_W_DEFAULT; // The add-on host is the single point of truth for what's installed and // active. Populated by initAddons() at app-ready time. let addonHost = null; +// One-shot proxy-login handler installed by setSessionProxy when the +// extension provided credentials. Removed and re-installed on every +// setSessionProxy call so the current credentials always match the +// current proxy. +let proxyLoginHandler = null; const { AddonHost } = require("./addons-host.js"); function addonsUserDir() { return path.join(app.getPath("userData"), "addons"); } function addonsDataDir() { return path.join(app.getPath("userData"), "addons-data"); } @@ -1189,15 +1211,46 @@ function initAddons() { // Session-proxy capability. Add-ons that declare "session-proxy" in // their manifest can call api.setSessionProxy(rules) to swap // Chromium's outbound network path. Same primitive Tor uses. + // + // Authentication: Chromium's setProxy does NOT parse credentials from + // `socks5://user:pass@host:port` — it rejects it as ERR_NO_SUPPORTED_ + // PROXIES. Add-ons pass auth separately either as an object: + // api.setSessionProxy({ proxyRules, auth: { username, password } }) + // or inline URL — this hook strips the user:pass@ and installs a + // one-shot login handler on the default session that answers with + // the extracted credentials next time Chromium asks the proxy for auth. setSessionProxy: async (rules, addonId) => { const ses = session.defaultSession; + // Always clear any prior proxy-login handler before swapping. + if (proxyLoginHandler) { ses.off("login", proxyLoginHandler); proxyLoginHandler = null; } if (rules == null || rules === "") { console.log(`[addons] [${addonId}] clearing session proxy`); try { await ses.setProxy({ proxyRules: "" }); } catch (e) { console.warn("proxy clear failed:", e?.message); } return; } - const opts = typeof rules === "string" ? { proxyRules: rules } : rules; - console.log(`[addons] [${addonId}] setting session proxy:`, opts.proxyRules || JSON.stringify(opts)); + let opts; + let auth = null; + if (typeof rules === "string") { + // Parse inline creds: "scheme://user:pass@host:port". + const m = rules.match(/^([a-z0-9+.-]+:\/\/)([^:@\/]+):([^@\/]+)@(.+)$/i); + if (m) { opts = { proxyRules: m[1] + m[4] }; auth = { username: m[2], password: decodeURIComponent(m[3]) }; } + else opts = { proxyRules: rules }; + } else { + opts = { proxyRules: rules.proxyRules }; + if (rules.auth && rules.auth.username != null) auth = { username: String(rules.auth.username), password: String(rules.auth.password || "") }; + } + const publicRules = opts.proxyRules; // never log the password + console.log(`[addons] [${addonId}] setting session proxy:`, publicRules, auth ? "(auth pending)" : ""); + if (auth) { + // Chromium fires session#login with `authenticationResponseDetails.isProxy === true` + // when the proxy asks for creds. Answer once per session. + proxyLoginHandler = (event, _details, authInfo, callback) => { + if (!authInfo || !authInfo.isProxy) return; + event.preventDefault(); + callback(auth.username, auth.password); + }; + ses.on("login", proxyLoginHandler); + } try { await ses.setProxy(opts); } catch (e) { console.warn("proxy set failed:", e?.message); } }, }); diff --git a/package.json b/package.json index 4348e8e..01dd74e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "theseus-navigator", - "version": "0.2.3", + "version": "0.2.4", "description": "Theseus Navigator — a browser that follows the thread. By Silent Mode, a Deviant project.", "author": "Silent Mode", "main": "main.js",