Ship Theseus 0.2.4 7fd323a8 (home cards refresh + engine-picker sync + proxy auth support)

Setup    7fd323a87bd32b780e147de18e16ecd82f89960bbd8e9a619c5d25d374597cd2
Portable 3166e64cf56badd7b26c4c793dc79bbed6f9d6c48fbd467d97f845385b91b1dd

Home cards: DEFAULT_HOME_CARDS replaced with the .x sibling grid the
user asked for -- hello.bch, siatest.bch (the "types of BCDN" pair),
then silentmode.x / theseus.x / sirius.x / hephaestus.x /
prometheus.x / helios.x / hermes.x. Existing installs with a saved
home-cards.json keep their edits (defaults only seed fresh profiles).

Search engine picker sync: user reported the toolbar dropdown listed
engines as active that Settings > Search showed differently. Root
cause: settings.searchEngine could be pointing at an id not in the
currently-enabled set (stale settings.json after DEFAULT_ENABLED
changes across versions). loadSettings now normalizes on boot -- if
searchEngine isn't enabled, fall back to enabled[0]; and
installedEngines gets unioned with enabledEngines so the two lists
can't disagree in ways that make toolbar and Settings render
different rows.

Proxy auth support in the framework: setSessionProxy accepts
`{ proxyRules, auth: { username, password } }` or an inline
`socks5://user:pass@host:port` URL. When creds are present, the
handler strips them from the URL, installs a session#login listener
on the default session that answers with them, then calls setProxy.
Chromium's SOCKS5 client doesn't consume proxy auth (known Chromium
limitation), but HTTP proxies work; SOCKS-based extensions need to
gate by IP allowlist at their server. Log line masks the password.

Update chip note: the "download opens in a different browser" was
0.2.0-era behavior. 0.2.1 rewired it to session.downloadURL. Anyone
still seeing it needs to install 0.2.1+ once.

Deployed: scp + sia-upload both trees, verified HEAD 200 + manifest
0.2.4 live.
This commit is contained in:
Local Dev 2026-08-31 17:24:39 +02:00
parent 49dde73c6b
commit 454255e963
2 changed files with 64 additions and 11 deletions

73
main.js
View file

@ -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); }
},
});

View file

@ -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",