theseus/addon-inject-preload.js
Local Dev ed441b4e9d Passwords: save and fill logins inside iframes too
Sign-in widgets and embedded checkouts often put the login form in an
iframe, and the password hooks only ran in a tab's top frame, so those
logins were never offered for saving or filling.

- Web tabs now run preloads in iframes (nodeIntegrationInSubFrames; pages
  still get no Node). Only the password hooks act there: the home-page
  bridge, window.bcnr, add-on page scripts and window.theseusId return early
  outside the top frame, exactly as before.
- A login in a frame belongs to the frame's own site, taken from that
  frame's committed URL. The save prompt says "login.example (in a frame on
  shop.example)", the fill offer names both, and the fill goes into that
  exact frame only while it is still that tab's and still on that site.
- The "did the login go through" check runs against the frame.

Verified with a shop page embedding a cross-site login frame: save offer,
fill offer and fill all target the frame; the outer page gets nothing;
top-frame logins behave as before.
2026-10-05 20:34:17 +02:00

45 lines
2.2 KiB
JavaScript

// Session-wide preload that runs every page-inject add-on's bridge script in
// the isolated world of tabs whose URL matches the add-on's declared origin
// patterns. Registered via session.defaultSession.setPreloads in main.js
// alongside bcnr-preload.js.
//
// The decision of WHICH scripts apply is made in main against the sender's
// committed URL, not against anything the page can influence. Each script
// gets a `theseus` object scoped to its add-on id:
// theseus.contextBridge — expose an API into the page's main world
// theseus.invoke(msg, payload) — call the add-on's onMessage(msg) handler
// theseus.origin — the page origin main will show the user
// theseus.evalInPage(code) — run code in the page's main world, resolve
// its value (cookie-consent rules need it)
// plus a `require` that only resolves "electron" so scripts written in the
// ordinary preload idiom keep working.
const { contextBridge, ipcRenderer, webFrame } = require("electron");
// Web tabs run preloads in iframes too (nodeIntegrationInSubFrames, for the
// password hooks). Add-on page scripts (wallet providers, consent rules)
// stay top-frame only, as they always were: main decides them from the
// tab's URL, not the frame's, and must never put them into third-party frames.
if (window.top !== window) return;
let injections = [];
try { injections = ipcRenderer.sendSync("addon-inject-scripts", location.href) || []; }
catch (e) { console.warn("[theseus] add-on inject query failed:", e?.message || e); }
for (const inj of injections) {
const id = String(inj.id);
const theseus = Object.freeze({
id,
origin: inj.origin,
contextBridge,
invoke: (msg, payload) => ipcRenderer.invoke("addon-page-msg", id, String(msg), payload),
evalInPage: (code) => webFrame.executeJavaScript(String(code)),
});
const scopedRequire = (name) => {
if (name === "electron") return { contextBridge };
throw new Error(`addon inject scripts may only require("electron") — got ${name}`);
};
try {
new Function("theseus", "require", inj.source)(theseus, scopedRequire);
} catch (e) {
console.warn(`[theseus] add-on "${id}" page-inject failed:`, e?.message || e);
}
}