diff --git a/bundled-addons/aegis/index.js b/bundled-addons/aegis/index.js index 83d4a337..e71c40f7 100644 --- a/bundled-addons/aegis/index.js +++ b/bundled-addons/aegis/index.js @@ -1495,8 +1495,17 @@ function plainLabel(v, max = 80) { return String(v ?? "").replace(INVISIBLE_RE, "").replace(/\s+/g, " ").trim().slice(0, max); } function fromPanel(m) { if (!m || m.from !== "panel") throw new Error("panel-only message"); } +// The origin a page's permissions are filed under, and what the approval +// overlay names. A page served from BNS and a page served from the ordinary +// web are different sites even when the name is the same — Theseus reports +// both as "https://name", and BNS carries registrations under ICANN TLDs on +// purpose, so whoever registers a colliding name in BNS used to inherit the +// real site's wallet connection and its silent-payment allowance (and the +// other way round). A BNS page is filed under "bns://name" instead. Hosts +// that do not say which registry served the page keep the old behaviour. function fromPage(m) { if (!m || m.from !== "page" || !m.origin) throw new Error("page-only message"); + if (m.registry === "bns") return String(m.origin).replace(/^https?:\/\//i, "bns://"); return m.origin; } @@ -2770,6 +2779,7 @@ function registerPanelMessages(api) { syncInjectPolicy(api); return injectState(); }); + try { migrateRegistryOrigins(api); } catch {} try { syncInjectPolicy(api); } catch {} api.onMessage("permissions", (_p, m) => { fromPanel(m); return grantsForPanel(api); }); @@ -3365,9 +3375,28 @@ function injectAllowList(api) { } function injectMode(api) { return api.storage.get(INJECT_MODE_KEY, "all") === "allowed" ? "allowed" : "all"; } function syncInjectPolicy(api) { - const origins = Array.from(new Set([...injectAllowList(api), ...Object.keys(permissions(api))])); + // Visibility is by host name: the host matches a bns:// page against the + // https form, so a grant filed under "bns://name" is listed as that. + const origins = Array.from(new Set([...injectAllowList(api), ...Object.keys(permissions(api))].map(normalizeOrigin).filter(Boolean))); api.storage.set("__pageInjectPolicy", { mode: injectMode(api), origins }); } +// Once, when the host first tells BNS pages apart: grants saved before then +// are filed under "https://name" and may have been given to either registry's +// site. A saved connection only discloses an address and still prompts for +// everything else, so those stay (the BNS site of that name simply asks +// again). A payment allowance spends without a prompt — those are dropped, +// because nobody can say which site it was really granted to. +function migrateRegistryOrigins(api) { + if (!(api.features && api.features.pageRegistry)) return; + if (api.storage.get("aegis/migrations/registryOrigins", false)) return; + const perms = permissions(api); + let dropped = 0; + for (const o of Object.keys(perms)) { + if (perms[o] && perms[o].sendTx) { delete perms[o].sendTx; dropped++; } + } + if (dropped) { api.storage.set("permissions", perms); api.log(`registry-separated origins: ${dropped} payment allowance(s) reset`); } + api.storage.set("aegis/migrations/registryOrigins", { at: Date.now(), dropped }); +} function normalizeOrigin(raw) { try { const u = new URL(String(raw || "").replace(/^bns:\/\//i, "https://")); @@ -3771,7 +3800,7 @@ async function withOriginLock(origin, fn) { // protection is downstream and unchanged: an approval overlay on every // action plus per-origin permissions. This only keeps non-web schemes out. function isDappOrigin(origin) { - try { const u = new URL(origin); return u.protocol === "https:" || u.protocol === "http:"; } + try { const u = new URL(origin); return u.protocol === "https:" || u.protocol === "http:" || u.protocol === "bns:"; } catch { return false; } } function legacyBchRuntime() {