feat(theseus/addons): vault-derive, page-inject and approval-modal capabilities
Three opt-in capabilities for add-ons, plus the plumbing they need:
- vault-derive: api.vault.derive("<id>/<path>") resolves once the password
vault is unlocked with a 32-byte HKDF child of the vault root under
"silentmode/addons/<path>". Path must start with the add-on id.
- page-inject: manifest "page-inject" {preload, origins}; a session-wide
preload asks main (sync, against the committed URL) which add-on bridges
apply and runs them in the isolated world with a scoped `theseus` object.
- approval-modal: api.approvalModal({title, body, origin, rows, actions,
checkbox}) shows a consent overlay over the tab area (approval.html);
resolves to the picked action id, "cancel", or "<id>+<checkbox>".
- api.onMessage/emit + window.silentmode.invoke/on for panel <-> activate()
messaging; page bridges use addon-page-msg, gated by tab + origin match.
- api.require so add-ons can share Theseus's dependency tree.
2026-09-06 02:33:26 +02:00
|
|
|
// 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
|
|
|
|
|
// plus a `require` that only resolves "electron" so scripts written in the
|
|
|
|
|
// ordinary preload idiom keep working.
|
|
|
|
|
const { contextBridge, ipcRenderer } = require("electron");
|
|
|
|
|
|
|
|
|
|
let injections = [];
|
2026-09-06 02:56:34 +02:00
|
|
|
try { injections = ipcRenderer.sendSync("addon-inject-scripts", location.href) || []; }
|
|
|
|
|
catch (e) { console.warn("[theseus] add-on inject query failed:", e?.message || e); }
|
feat(theseus/addons): vault-derive, page-inject and approval-modal capabilities
Three opt-in capabilities for add-ons, plus the plumbing they need:
- vault-derive: api.vault.derive("<id>/<path>") resolves once the password
vault is unlocked with a 32-byte HKDF child of the vault root under
"silentmode/addons/<path>". Path must start with the add-on id.
- page-inject: manifest "page-inject" {preload, origins}; a session-wide
preload asks main (sync, against the committed URL) which add-on bridges
apply and runs them in the isolated world with a scoped `theseus` object.
- approval-modal: api.approvalModal({title, body, origin, rows, actions,
checkbox}) shows a consent overlay over the tab area (approval.html);
resolves to the picked action id, "cancel", or "<id>+<checkbox>".
- api.onMessage/emit + window.silentmode.invoke/on for panel <-> activate()
messaging; page bridges use addon-page-msg, gated by tab + origin match.
- api.require so add-ons can share Theseus's dependency tree.
2026-09-06 02:33:26 +02:00
|
|
|
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),
|
|
|
|
|
});
|
|
|
|
|
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);
|
|
|
|
|
}
|
|
|
|
|
}
|