// Runs ONE community extension outside the browser process. // // An extension used to be require()d into Electron's main process, where it // could load `electron`, hook the unlock prompt, read the vault files and // the wallet's store, or shell out to the OS keystore — its declared // capabilities only bound an honest extension. Extensions that do not ship // with Theseus now run here instead: a separate process started in Node mode // under Node's permission model, allowed to read nothing but its own folder // (and write nothing but its own scratch folder), with no child processes, // no workers, no native add-ons and no Electron. Everything it can do to the // browser goes through the message channel below, and the host answers only // the calls on its allow-list, with the same capability checks as before. // // The `api` object keeps the in-process shape where it can. Differences an // extension author will meet: // - host calls return promises (tabs.active() included); // - api.require / api.import are gone — bundle your dependencies; // - registerRequestFilter and registerSiteRoute are not offered (both hand // the host a function it would have to call synchronously); // - vault.imports, vault.pin and vault.lifecycle.unlock/setup/lock are // built-in only, as they already were. "use strict"; const { AsyncLocalStorage } = require("node:async_hooks"); const callScope = new AsyncLocalStorage(); // which page call a host request belongs to let seq = 0; const pending = new Map(); // id -> { resolve, reject } const handlers = new Map(); // message name -> fn(payload, ctx) const tabListeners = new Set(); let mod = null; // Messages cross the process boundary as JSON (the binary 'advanced' channel // format is tied to the exact V8 build on both ends). Bytes and BigInts are // tagged so they arrive as what they were. function wireEnc(v, depth = 0) { if (depth > 40) throw new Error("value too deeply nested"); if (v === null || v === undefined) return v === undefined ? undefined : null; if (typeof v === "bigint") return { __wire: "big", v: v.toString() }; if (typeof v === "function" || typeof v === "symbol") return undefined; if (typeof v !== "object") return v; if (v instanceof Uint8Array) return { __wire: "u8", v: Buffer.from(v.buffer, v.byteOffset, v.byteLength).toString("base64") }; if (v instanceof ArrayBuffer) return { __wire: "u8", v: Buffer.from(v).toString("base64") }; if (Array.isArray(v)) return v.map((x) => { const e = wireEnc(x, depth + 1); return e === undefined ? null : e; }); if (v instanceof Date) return v.toISOString(); const out = {}; for (const k of Object.keys(v)) { const e = wireEnc(v[k], depth + 1); if (e !== undefined) out[k] = e; } return out; } function wireDec(v) { if (v === null || typeof v !== "object") return v; if (Array.isArray(v)) return v.map(wireDec); if (v.__wire === "u8" && typeof v.v === "string") return new Uint8Array(Buffer.from(v.v, "base64")); if (v.__wire === "big" && typeof v.v === "string") return BigInt(v.v); const out = {}; for (const k of Object.keys(v)) out[k] = wireDec(v[k]); return out; } const send = (m) => { try { process.send(wireEnc(m)); } catch { /* host is gone */ } }; const errOut = (e) => ({ message: String((e && e.message) || e), code: e && e.code != null ? e.code : undefined }); const plain = (v) => (v === undefined ? undefined : JSON.parse(JSON.stringify(v))); function hostCall(path, args) { return new Promise((resolve, reject) => { const id = ++seq; pending.set(id, { resolve, reject }); const scope = callScope.getStore(); send({ t: "api", id, path, args, callId: scope ? scope.callId : null }); }); } const builtInOnly = (what) => () => Promise.reject(new Error(`${what} is reserved for built-in add-ons`)); const notOffered = (what, why) => () => { throw new Error(`${what} is not available to sandboxed extensions — ${why}`); }; function makeApi(init) { const store = init.store && typeof init.store === "object" ? init.store : {}; const call = (path) => (...args) => hostCall(path, args); return { id: init.manifest.id, folder: init.folder, dataDir: init.scratchDir, features: Object.freeze({ ...(init.features || {}), sandboxed: true }), log: (...a) => send({ t: "log", args: a.map((x) => (typeof x === "string" ? x : (() => { try { return JSON.stringify(x); } catch { return String(x); } })())) }), // The store is this extension's own key/value file. It is handed over // whole at start and written through, so get() stays synchronous. storage: { get: (key, fallback = null) => (Object.prototype.hasOwnProperty.call(store, key) && store[key] != null ? plain(store[key]) : fallback), set: (key, value) => { const k = String(key); if (value === null || value === undefined) delete store[k]; else store[k] = plain(value); send({ t: "storage.set", key: k, value: value === undefined ? null : plain(value) }); }, all: () => plain(store), }, onMessage: (name, fn) => { if (typeof name !== "string" || !name || typeof fn !== "function") throw new Error("onMessage needs (name, fn)"); handlers.set(name, fn); send({ t: "handler", name }); }, emit: (msg, payload) => { hostCall("emit", [String(msg), payload]).catch(() => {}); }, registerSidebarPanel: (spec) => { hostCall("registerSidebarPanel", [spec]).catch((e) => send({ t: "log", args: ["registerSidebarPanel: " + e.message] })); }, revealSidebar: (panelId) => { hostCall("revealSidebar", [panelId]).catch(() => {}); }, openTab: call("openTab"), openSettings: call("openSettings"), setSessionProxy: call("setSessionProxy"), captureTab: call("captureTab"), saveCapture: call("saveCapture"), scanActiveTabForUris: call("scanActiveTabForUris"), approvalModal: call("approvalModal"), checkAndStageSelfUpdate: call("checkAndStageSelfUpdate"), applySelfUpdate: call("applySelfUpdate"), restartApp: call("restartApp"), startAtLaunch: (on) => { hostCall("startAtLaunch", [!!on]).catch(() => {}); }, whenUiReady: call("whenUiReady"), tabs: { active: call("tabs.active"), onChange: (cb) => { if (typeof cb !== "function") return () => {}; tabListeners.add(cb); send({ t: "tabs.watch" }); return () => tabListeners.delete(cb); }, }, vault: { derive: call("vault.derive"), requestUnlock: call("vault.requestUnlock"), lifecycle: { status: call("vault.lifecycle.status"), unlock: builtInOnly("vault.lifecycle.unlock"), setup: builtInOnly("vault.lifecycle.setup"), lock: builtInOnly("vault.lifecycle.lock"), }, imports: { list: builtInOnly("vault.imports"), add: builtInOnly("vault.imports"), remove: builtInOnly("vault.imports"), signer: builtInOnly("vault.imports") }, pin: { status: builtInOnly("vault.pin"), unlock: builtInOnly("vault.pin"), set: builtInOnly("vault.pin"), clear: builtInOnly("vault.pin") }, }, registerRequestFilter: notOffered("registerRequestFilter", "the host would have to call into the extension synchronously for every request"), registerSiteRoute: notOffered("registerSiteRoute", "site routes are for built-in add-ons"), require: notOffered("api.require", "bundle the modules you need inside the extension folder"), import: notOffered("api.import", "bundle the modules you need inside the extension folder"), }; } process.on("message", async (raw) => { if (!raw || typeof raw !== "object") return; const m = wireDec(raw); if (m.t === "res") { const p = pending.get(m.id); if (!p) return; pending.delete(m.id); if (m.ok) p.resolve(m.value); else { const e = new Error(m.error && m.error.message || "host call failed"); if (m.error && m.error.code != null) e.code = m.error.code; p.reject(e); } return; } if (m.t === "activate") { try { mod = require(m.mainPath); if (!mod || typeof mod.activate !== "function") throw new Error("main file must export an activate(api) function"); await mod.activate(makeApi(m)); send({ t: "activated", ok: true }); } catch (e) { send({ t: "activated", ok: false, error: errOut(e) }); } return; } if (m.t === "call") { const fn = handlers.get(m.name); if (!fn) { send({ t: "res", id: m.id, ok: false, error: { message: `no handler for "${m.name}"` } }); return; } try { const value = await callScope.run({ callId: m.id }, () => fn(m.payload, m.ctx || {})); send({ t: "res", id: m.id, ok: true, value: value === undefined ? null : value }); } catch (e) { send({ t: "res", id: m.id, ok: false, error: errOut(e) }); } return; } if (m.t === "tabs") { for (const cb of tabListeners) { try { cb(m.data); } catch {} } return; } if (m.t === "deactivate") { try { if (mod && typeof mod.deactivate === "function") await mod.deactivate(); } catch {} process.exit(0); } }); // An extension's stray rejection or exception must not take its process down // silently — say so in the host log and keep serving. process.on("uncaughtException", (e) => send({ t: "log", args: ["uncaught exception: " + ((e && e.stack) || e)] })); process.on("unhandledRejection", (e) => send({ t: "log", args: ["unhandled rejection: " + ((e && e.message) || e)] })); process.on("disconnect", () => process.exit(0)); send({ t: "ready" });