feat(theseus/chrome): "Open link in new window" on the link context menu

A standalone page window on the same session (cookies, bns:// protocol,
session-wide bcnr preload) with Theseus's fingerprint + WebRTC policy and
no toolbar. Loads BCNR-first like a tab: a dotted host with a BCNR record
goes over bns://, otherwise clearnet; collision names follow the configured
policy without the "Open with…" interstitial. Cross-host navigations inside
the window stay BCNR-first; popups go to the main window's tabs. Its own
context menu offers open-in-tab / open-in-window / copy link and
back/forward/reload. Add-on page bridges (wallet inject) are tab-scoped and
don't run in these windows. openLinkWindow is exported for the test harness.

Verified in the dev app: coinspectrum.x opened as bns://coinspectrum.x with
the page title; navigate.st stayed https.
This commit is contained in:
Local Dev 2026-09-10 22:25:19 +02:00
parent 43d0021bf6
commit ed48646c71

88
main.js
View file

@ -2686,6 +2686,7 @@ function createTab(initial, opts = {}) {
items.push(
{ label: "Open link in new tab", click: () => createTab(p.linkURL) },
{ label: "Open link in new background tab", click: () => createTab(p.linkURL, { background: true }) },
{ label: "Open link in new window", click: () => openLinkWindow(p.linkURL) },
{ label: "Copy link address", click: () => clipboard.writeText(p.linkURL) },
{ type: "separator" },
);
@ -4760,6 +4761,91 @@ ipcMain.handle("hermes-send", async (_e, { to, text } = {}) => {
} catch (e) { return hErr(e); }
});
// ---- "Open link in new window" ----
// A standalone page window: same session (cookies, the bns:// protocol, the
// session-wide bcnr preload), Theseus's fingerprint + WebRTC policy, no
// toolbar. Loads BCNR-first like a tab does: a dotted host with a BCNR
// record goes over bns://, anything else over clearnet. Collision names
// follow the configured policy without the "Open with…" interstitial.
// Add-on page bridges (the wallet inject) are tab-scoped and don't run here.
const linkWindows = new Set();
async function targetUrlFor(input) {
let u;
try { u = new URL(String(input).includes("://") ? String(input) : "https://" + String(input)); } catch { return null; }
if (u.protocol !== "http:" && u.protocol !== "https:") return u.href;
const host = u.hostname.toLowerCase();
if (!isBnsHost(host)) return u.href;
let entry = null;
try { entry = await resolveHost(host); } catch {}
if (!entry) return u.href;
const policy = settings.collisionPolicy || "bcnr-first";
if (!isBcnrNativeTld(tldOf(host)) && policy === "icann-first") return u.href;
return `bns://${host}${u.pathname}${u.search}${u.hash}`;
}
async function openLinkWindow(input) {
const target = await targetUrlFor(input);
if (!target) return null;
const w = new BrowserWindow({
width: 1100, height: 760, title: "Theseus Navigator",
backgroundColor: nativeTheme.shouldUseDarkColors ? "#0b0e14" : "#ffffff",
icon: app.isPackaged ? path.join(process.resourcesPath, "icon.ico") : path.join(__dirname, "build", "icon.ico"),
webPreferences: { contextIsolation: true, nodeIntegration: false, sandbox: true },
});
w.setMenuBarVisibility(false);
const wc = w.webContents;
styleScrollbars(wc);
try { wc.setWebRTCIPHandlingPolicy(webrtcPolicy()); } catch {}
try { wc.setBackgroundThrottling(settings.backgroundThrottle); } catch {}
applyFingerprint(wc);
wc.on("page-title-updated", (_e, title) => { try { w.setTitle(title ? `${title} — Theseus` : "Theseus Navigator"); } catch {} });
// Cross-host navigations inside the window stay BCNR-first too. Same-host
// ones go through Chromium untouched (form POSTs must survive).
wc.on("will-navigate", (e, u) => {
try {
const p = new URL(u);
if (p.protocol !== "http:" && p.protocol !== "https:") return;
if (!isBnsHost(p.hostname)) return;
let cur = ""; try { cur = new URL(wc.getURL()).hostname; } catch {}
if (cur === p.hostname) return;
e.preventDefault();
targetUrlFor(u).then((t) => { if (t) wc.loadURL(t).catch(() => {}); });
} catch {}
});
// Popups from a page here go to the main window's tabs when it exists —
// one place for tabs — otherwise to another plain window.
wc.setWindowOpenHandler(({ url }) => {
if (url && url !== "about:blank") {
if (win && !win.isDestroyed()) { createTab(url); try { win.focus(); } catch {} }
else openLinkWindow(url);
}
return { action: "deny" };
});
wc.on("context-menu", (_e, p) => {
const items = [];
const haveMain = !!win && !win.isDestroyed();
if (p.linkURL) {
items.push(
{ label: "Open link in new tab", enabled: haveMain, click: () => { createTab(p.linkURL); try { win.focus(); } catch {} } },
{ label: "Open link in new window", click: () => openLinkWindow(p.linkURL) },
{ label: "Copy link address", click: () => clipboard.writeText(p.linkURL) },
{ type: "separator" },
);
}
if (p.isEditable) items.push({ role: "cut" }, { role: "copy" }, { role: "paste" }, { type: "separator" });
else if (p.selectionText) items.push({ role: "copy" }, { type: "separator" });
items.push(
{ label: "Back", enabled: wc.navigationHistory.canGoBack(), click: () => wc.navigationHistory.goBack() },
{ label: "Forward", enabled: wc.navigationHistory.canGoForward(), click: () => wc.navigationHistory.goForward() },
{ label: "Reload", click: () => wc.reload() },
);
Menu.buildFromTemplate(items).popup({ window: w });
});
linkWindows.add(w);
w.on("closed", () => linkWindows.delete(w));
wc.loadURL(target).catch(() => {});
return w;
}
function openHermesWindow() {
if (hermesWin && !hermesWin.isDestroyed()) {
hermesWin.focus(); return;
@ -5056,4 +5142,4 @@ if (!process.env.THESEUS_NO_AUTOSTART) {
app.on("window-all-closed", () => { stopTor(); if (process.platform !== "darwin") app.quit(); });
}
module.exports = { serveBns, resolveHost, isBnsHost, nativeTld, dualTld, registryOf };
module.exports = { serveBns, resolveHost, isBnsHost, nativeTld, dualTld, registryOf, openLinkWindow };