alert / confirm / prompt from a page came up as bare OS message boxes titled "theseus-navigator" (the package name), with no hint of who was asking and nothing of the browser's styling — the PDF Editor's "Delete signature?" was the reported case. The session preload replaces the page's three functions with wrappers that hand the call to the isolated world through a DOM event, which asks main synchronously and writes the answer back; pages see Chrome's return values (confirm → boolean, prompt → string or null) and no new global. Main answers from a sheet hanging under the toolbar, in the same surface as add-on approvals, that names who is asking: the site's host, the add-on's name for an add-on page or panel (identified by its path under the profile's extensions directory), or Theseus for its own pages. The sheet belongs to the tab that asked — hidden while another tab is in front, back when its tab returns — and a closing tab or window answers "cancel" so no renderer stays blocked. Windows without the chrome (installed apps, plain windows) get a native box with a proper title, and app.name now reads "Theseus Navigator" for whatever else still shows one.
108 lines
6.3 KiB
JavaScript
108 lines
6.3 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.
|
|
installExtension: (id) => 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 {}
|