// bcnr-preload.js — session-wide preload that installs `window.bcnr` on every // page (regular tabs, popups, chrome/settings/etc). Reads only — no signing, // no wallet unlock, no permission prompts. These four methods query the same // resolver Theseus already runs for its address bar; nothing about the local // user leaks, so no origin gate is needed for this surface. // // Sequencing: registered via `session.defaultSession.setPreloads([...])` in // main.js at whenReady, which runs BEFORE per-WebContentsView preloads (home, // settings, popover, etc.), so those preloads still install their own bridges // on top of `window.bcnr`. See DESIGN-integrated-wallet.md §3 for the full // API surface. const { contextBridge, ipcRenderer } = require("electron"); // Web tabs run preloads in iframes too (nodeIntegrationInSubFrames, for the // password hooks in home-preload.js). This one stays top-frame only, as // it always was. if (window.top !== window) return; // Every method returns a Promise; a name that fails to resolve or isn't // registered comes back as `null` (not an error) so page code can treat // "no such name" as data, not an exception. `getBcnrTlds` always returns // an array — even the seed ["bch"] before the on-chain list has landed. contextBridge.exposeInMainWorld("bcnr", { resolveName: (name) => ipcRenderer.invoke("bcnr:resolveName", name), isRegistered: (name) => ipcRenderer.invoke("bcnr:isRegistered", name), getBcnrTlds: () => ipcRenderer.invoke("bcnr:getBcnrTlds"), getRecordVersion: (name) => ipcRenderer.invoke("bcnr:getRecordVersion", name), // Owner-signed DNS records (A/AAAA/MX/TXT/CNAME/NS) published beside the // name's Sia content and verified by the gateway against the current NFT // holder. `{ name, dns, seq, updatedAt, owner }`, or null when the name is // unregistered or has published no manifest. Waits ≤ 3 s for a fetch. dnsRecords: (name) => ipcRenderer.invoke("bcnr:dnsRecords", name), // Diagnostic — the eTLD+1 permission origin Theseus computes for THIS page. // dApp devs use this to see how their subdomains bucket under one grant. // Returns null for opaque origins (data:, blob:) which never hold grants. getOrigin: () => ipcRenderer.invoke("bcnr:getOrigin"), // One-click install of a community extension by catalog id (what // theseus.x/extensions' Install button calls). The page names an id only; // Theseus fetches the catalog itself, asks the user in a native dialog, // verifies the publisher signature against the name's owner and installs. // Resolves `{ ok, version, publisher }` or `{ ok:false, error }` (also // "cancelled"). Pages can feature-detect it: absent on older builds. // Needs a real click: the isolated world reads the page's user activation, // which page script cannot fake. installExtension: (id) => (navigator.userActivation && !navigator.userActivation.isActive ? Promise.resolve({ ok: false, error: "installing needs a click on the page" }) : ipcRenderer.invoke("bcnr:installExtension", String(id || ""))), }); // Install-as-app relay. Theseus fires a synthetic `beforeinstallprompt` in // pages whose manifest is installable (webapps.js); the page's prompt() // dispatches a DOM event that this isolated world hears, asks main for the // install dialog, and answers with another DOM event. Nothing is exposed // on window; the page only ever sees Chrome's event shape. Top frame only. try { if (window.top === window) { document.addEventListener("theseus:webapp-prompt", () => { ipcRenderer.invoke("webapp-prompt").then((r) => { document.dispatchEvent(new Event(r === "accepted" ? "theseus:webapp-accepted" : "theseus:webapp-dismissed")); }).catch(() => { try { document.dispatchEvent(new Event("theseus:webapp-dismissed")); } catch {} }); }); } } catch {} // ---- page dialogs (alert / confirm / prompt) ---- // Chromium's stock boxes are bare OS message boxes titled with the package // name. The main world's alert/confirm/prompt are replaced with wrappers that // hand the call to this isolated world through a DOM event (synchronous, no // global left on window for pages to fingerprint), which asks main over // sendSync and writes the answer back on the element. If nothing answers, // the wrapper falls through to the original function. Theseus's own views // share the session and get the same wrappers; main decides by sender. try { const { webFrame } = require("electron"); document.addEventListener("theseus:jsdialog", (e) => { const el = e.target; if (!el || typeof el.getAttribute !== "function" || el.localName !== "theseus-dialog") return; let r = null; try { r = ipcRenderer.sendSync("js-dialog", { kind: el.getAttribute("data-kind"), message: el.getAttribute("data-message") || "", def: el.hasAttribute("data-default") ? el.getAttribute("data-default") : null, }); } catch { r = null; } if (r && typeof r === "object" && r.handled) { el.setAttribute("data-handled", r.value == null ? "null" : "1"); el.setAttribute("data-result", r.value == null ? "" : String(r.value)); } }, true); webFrame.executeJavaScript([ "(() => {", " const natives = { alert: window.alert, confirm: window.confirm, prompt: window.prompt };", " const ask = (kind, message, def) => {", " const root = document.documentElement; if (!root) return null;", " const el = document.createElement('theseus-dialog'); el.hidden = true;", " el.setAttribute('data-kind', kind); el.setAttribute('data-message', message);", " if (def != null) el.setAttribute('data-default', def);", " root.appendChild(el);", " try { el.dispatchEvent(new Event('theseus:jsdialog', { bubbles: true })); } finally { el.remove(); }", " if (!el.hasAttribute('data-handled')) return null;", " return { value: el.getAttribute('data-handled') === 'null' ? null : el.getAttribute('data-result') };", " };", " const wrap = (kind) => function (message, def) {", " const msg = arguments.length ? String(message) : '';", " const r = ask(kind, msg, kind === 'prompt' ? (def == null ? '' : String(def)) : null);", " if (!r) return natives[kind].apply(window, arguments);", " if (kind === 'alert') return undefined;", " if (kind === 'confirm') return r.value === '1';", " return r.value;", " };", " for (const k of ['alert', 'confirm', 'prompt']) {", " try { const f = wrap(k); Object.defineProperty(f, 'name', { value: k }); f.toString = () => 'function ' + k + '() { [native code] }'; window[k] = f; } catch (e) {}", " }", "})();", ].join("\n")).catch(() => {}); } catch {}