From de46b72764c587cef72d5f002357444290dae7d6 Mon Sep 17 00:00:00 2001 From: Local Dev Date: Wed, 7 Oct 2026 20:27:03 +0200 Subject: [PATCH] Password manager and Theseus ID in the left sidebar's web apps MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Quick links in the left sidebar (X, Telegram, WhatsApp, …) open in their own panel, which is not a tab: it had no page preload, and the password manager and Theseus ID accepted only tabs as senders. So a login typed there was never offered for saving, a saved one was never offered for filling, and a site that accepts Theseus ID answered "this page cannot use Theseus ID". The panel now gets the same preload as a web tab (iframes included), and a stand-in tab for it lets the existing offer, save, fill and sign-in code treat it as one more tab-like place: offers show while the panel is open, fill goes into the panel rather than the active tab, and the prompts it raises are queued as no tab's, so they show over whichever tab is active. --- main.js | 56 +++++++++++++++++++++++++++++++++++++++----------------- 1 file changed, 39 insertions(+), 17 deletions(-) diff --git a/main.js b/main.js index 8cfc14a0..2a85c416 100644 --- a/main.js +++ b/main.js @@ -3914,8 +3914,9 @@ function emitPwAvailability() { // Kept intentionally small — the whole autofill affordance is opt-in // (user clicks the chip; nothing runs on page load). // frameRef: the iframe the login was offered in (null = the tab's own page). -async function pwFillIntoActiveTab(entry, frameRef = null) { - const t = activeTab(); if (!t) return false; +// into: the tab (or the quick panel's stand-in) to fill; the active tab if not given. +async function pwFillIntoActiveTab(entry, frameRef = null, into = null) { + const t = into || activeTab(); if (!t) return false; // Re-check at fill time: the page (or frame) may have navigated since the // picker opened. A frame must still be that tab's, still on that site. let target = t.view.webContents; @@ -5061,7 +5062,9 @@ function createWindow() { // Third-party sites only — no preload (home-preload's navigate/cards API // has no business here), and its popups become ordinary tabs instead of // bare Electron windows outside every tab protection. - quickPanel = new WebContentsView(); + // Same preload as a web tab, iframes included, so a login typed in a quick + // link (Twitter, …) gets the password manager's offers like any tab. + quickPanel = new WebContentsView({ webPreferences: { preload: path.join(__dirname, "home-preload.js"), nodeIntegrationInSubFrames: true } }); quickPanel.webContents.setWindowOpenHandler(({ url }) => { if (url && /^(?:https?|bns):/i.test(url)) createTab(url); return { action: "deny" }; @@ -6483,6 +6486,22 @@ ipcMain.handle("addon-msg", async (e, msg, payload) => { // page-inject origins may talk to it, and only through messages the add-on // registered. Origin is "://" (bns:// shown as https://). function tabForSender(sender) { return tabs.find((t) => t.view.webContents === sender) || null; } +// The left sidebar's quick-link panel (Twitter, Telegram, …) is not a tab, +// but logins happen in it, so the password manager and Theseus ID treat it as +// one more tab-like place: this stand-in has the view and an id the tab code +// can carry around. It is never in `tabs`, so the tab strip never sees it. +const QUICK_TAB_ID = "quick-panel"; +const quickTab = { id: QUICK_TAB_ID, quick: true, get view() { return quickPanel; } }; +function pwTabById(id) { + if (id === QUICK_TAB_ID) return quickPanel && activeQuickLinkId ? quickTab : null; + return tabs.find((t) => t.id === id) || null; +} +function pwTabForSender(sender) { + return tabForSender(sender) || (quickPanel && sender === quickPanel.webContents ? quickTab : null); +} +// Approvals are queued per tab and shown only over the active one; one raised +// for the quick panel belongs to no tab, so it shows whatever tab is active. +const approvalTabOf = (tabId) => (tabId === QUICK_TAB_ID ? null : tabId); function pageOriginOf(url) { try { const u = new URL(String(url).replace(/^bns:\/\//i, "https://")); @@ -8164,9 +8183,10 @@ ipcMain.handle("pw-fill-resize", (_e, h) => { ipcMain.handle("pw-fill-pick", async (e, id) => { if (!pwFillPop || e.sender !== pwFillPop.webContents) return { ok: false, err: "picker only" }; showPwFill(false); - const t = activeTab(); - // The offer came from a login field (possibly in an iframe), or from the - // toolbar chip (the tab's own page). + // The offer came from a login field (possibly in an iframe, possibly in the + // quick panel), or from the toolbar chip (the active tab's own page). + const quickT = pwfOffer && pwfOffer.tabId === QUICK_TAB_ID ? pwTabById(QUICK_TAB_ID) : null; + const t = quickT || activeTab(); const offer = pwfOffer && t && pwfOffer.tabId === t.id ? pwfOffer : null; const host = offer ? offer.host : t ? liveHost(t) : ""; let justUnlocked = false; // a PIN / password typed for this very fill also answers the per-login check @@ -8191,7 +8211,7 @@ ipcMain.handle("pw-fill-pick", async (e, id) => { if (!c.ok) return { ok: false, err: "cancelled" }; } const password = await v.resolvePassword(vaultState, id); - return await pwFillIntoActiveTab({ domain: entry.domain, username: entry.username, password }, offer && offer.ref); + return await pwFillIntoActiveTab({ domain: entry.domain, username: entry.username, password }, offer && offer.ref, t); } catch (e) { return { ok: false, err: e?.message || String(e) }; } }); @@ -8210,7 +8230,7 @@ function frameHost(frame) { } catch { return ""; } } function pwSource(e) { - const t = tabForSender(e.sender); + const t = pwTabForSender(e.sender); if (!t || t.settings || t.addonId) return null; const frame = e.senderFrame; if (!frame || frame.detached) return null; @@ -8233,7 +8253,8 @@ ipcMain.on("pw-form", (e, rect) => { try { if (!settings.pwOfferFill) return; const src = pwSource(e); - if (!src || src.t.id !== activeId) return; + // The active tab, or the quick panel while it is open beside it. + if (!src || (src.t.quick ? !activeQuickLinkId : src.t.id !== activeId)) return; const { t, host } = src; if (!fs.existsSync(vaultFile())) return; let matches = [], locked = false; @@ -8305,7 +8326,7 @@ ipcMain.on("pw-capture", (e, data) => { async function pwLoginSettled(tabId, host, ref) { const CHECK = "[...document.querySelectorAll('input[type=password]')].some((e) => e.offsetWidth > 0 && e.offsetHeight > 0)"; for (let i = 0; i < 8; i++) { - const t = tabs.find((x) => x.id === tabId); + const t = pwTabById(tabId); if (!t) return false; let pwField = false; if (ref) { @@ -8324,8 +8345,9 @@ async function pwLoginSettled(tabId, host, ref) { return false; } async function offerSavePassword(tabId, host, username, password, { ref = null, topHost = "" } = {}) { - if (!tabs.some((x) => x.id === tabId)) return; + if (!pwTabById(tabId)) return; if (!(await pwLoginSettled(tabId, host, ref))) return; + const apTab = approvalTabOf(tabId); // A login in an iframe is saved for the iframe's site; say so. const where = ref && topHost && topHost !== host ? `${host} (in a frame on ${topHost})` : host; if (!fs.existsSync(vaultFile())) { @@ -8335,7 +8357,7 @@ async function offerSavePassword(tabId, host, username, password, { ref = null, body: "Theseus can remember this login and fill it next time. Set up the Theseus Vault first: it is encrypted with a master password only you know, and nothing leaves this computer.", origin: where, actions: [{ id: "setup", label: "Set up the vault", primary: true }, { id: "never", label: "Never for this site" }, { id: "cancel", label: "Not now" }], - }, null, tabId); + }, null, apTab); if (pick === "setup") openSettingsTab("passwords"); else if (pick === "never") pwNeverFor(host); return; @@ -8362,7 +8384,7 @@ async function offerSavePassword(tabId, host, username, password, { ref = null, actions: update ? [{ id: "save", label: "Update", primary: true }, { id: "cancel", label: "Not now" }] : [{ id: "save", label: "Save", primary: true }, { id: "never", label: "Never for this site" }, { id: "cancel", label: "Not now" }], - }, null, tabId); + }, null, apTab); if (pick === "never") return pwNeverFor(host); if (!pick.startsWith("save")) return; const confirm = pick.split("+").includes("confirm"); @@ -8489,10 +8511,10 @@ function tidErr(err) { if (code === "error") console.warn("[theseus-id]", err && err.message); return { ok: false, error: { code, message: code === "error" ? "Theseus ID failed" : String(err.message || code) } }; } -// The caller must be the top frame of a web tab; the origin and URL come -// from what that frame has committed, never from the payload. +// The caller must be the top frame of a web tab or of the quick panel; the +// origin and URL come from what that frame has committed, never from the payload. function tidCaller(e) { - const t = tabForSender(e.sender); + const t = pwTabForSender(e.sender); if (!t || t.settings || t.addonId) return { error: { code: "origin-not-listed", message: "this page cannot use Theseus ID" } }; if (e.senderFrame !== e.sender.mainFrame) return { error: { code: "not-top-frame", message: "Theseus ID works only in the top frame" } }; const url = e.sender.getURL(); @@ -8507,7 +8529,7 @@ ipcMain.handle("theseus-id:signIn", async (e, payload) => { const result = await theseusId().signIn({ projectId: p.projectId, nonce: p.nonce, statement: p.statement, requestId: p.requestId, resources: p.resources, expiresIn: p.expiresIn, gesture: !!p.gesture, - origin: c.origin, uri: c.uri, ctx: { tabId: c.t.id }, + origin: c.origin, uri: c.uri, ctx: { tabId: approvalTabOf(c.t.id) }, }); return { ok: true, result }; } catch (err) { return tidErr(err); }