bcnr.installExtension is in every page's main world, and install links were honoured from any page and any frame, with no user gesture. Any site could raise the install sheet timed so that a double-click landed on Install, whose two buttons are always in the same place; an installed community extension runs in the main process at once. The call and the links now work only from theseus.x's top frame, the call needs a real click (checked in the isolated world), and Theseus's own sheets ignore every choice except Cancel for 800 ms, like the approval overlay.
112 lines
6.5 KiB
JavaScript
112 lines
6.5 KiB
JavaScript
// 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");
|
|
|
|
// 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 {}
|