diff --git a/bundled-addons/aegis/index.js b/bundled-addons/aegis/index.js index e78e808c..9310a18f 100644 --- a/bundled-addons/aegis/index.js +++ b/bundled-addons/aegis/index.js @@ -2292,13 +2292,12 @@ function registerPanelMessages(api) { }; }); - api.onMessage("permissions", (_p, m) => { fromPanel(m); return permissions(api); }); + api.onMessage("permissions", (_p, m) => { fromPanel(m); return grantsForPanel(api); }); api.onMessage("revoke", (p, m) => { fromPanel(m); - const perms = permissions(api); - delete perms[String(p && p.origin || "")]; - api.storage.set("permissions", perms); - return perms; + revokeOrigin(api, String(p && p.origin || "")); + emitState(); + return grantsForPanel(api); }); // ---- security: quick-access PIN + policy flags ------------------------ @@ -2602,6 +2601,75 @@ function describePlan(plan) { const BCH_ALLOWANCES = [100000, 1000000, 10000000]; // 0.001, 0.01, 0.1 BCH const pendingByOrigin = new Set(); function permissions(api) { const p = api.storage.get("permissions", {}); return p && typeof p === "object" ? p : {}; } + +// ---- grants: session + persisted ------------------------------------------ +// A plain "Connect" grants the origin for this browser session (memory only); +// ticking "Always allow" persists it under api.storage "permissions". Both +// look identical to the handlers via grantFor(). Scopes: "bch" (the +// window.bitcoincash bridge keeps its flat readAddress), "eth", "sol", "trx". +// ETH grants also carry the origin's chainId — chain is per origin, never a +// global the sidebar or another site can flip under a connected dapp. +// Before this, a plain "Connect" stored nothing, so the very next call from +// the site failed with "not connected". +const sessionGrants = new Map(); // origin -> { readAddress?, eth?, sol?, trx? } +function grantFor(api, origin, scope) { + const persisted = permissions(api)[origin] || {}; + const session = sessionGrants.get(origin) || {}; + if (scope === "bch") { + if (persisted.readAddress) return { readAddress: true, persisted: true }; + if (session.readAddress) return { readAddress: true, persisted: false }; + return null; + } + const p = persisted[scope], s = session[scope]; + if (p && p.readAddress) return { ...p, persisted: true }; + if (s && s.readAddress) return { ...s, persisted: false }; + return null; +} +function setGrant(api, origin, scope, value, persist) { + const wrap = scope === "bch" ? value : { [scope]: value }; + if (persist) { + const perms = permissions(api); + perms[origin] = { ...(perms[origin] || {}), ...wrap }; + api.storage.set("permissions", perms); + } else { + sessionGrants.set(origin, { ...(sessionGrants.get(origin) || {}), ...wrap }); + } + emitState(); +} +// Patch fields (e.g. chainId) on whichever grant the origin currently holds. +function patchGrant(api, origin, scope, patch) { + const g = grantFor(api, origin, scope); + if (!g) return false; + const { persisted, ...rest } = g; + setGrant(api, origin, scope, { ...rest, ...patch }, persisted); + return true; +} +// An unconnected site that asked for a chain (addChain / switch before +// connect) gets it remembered for its eventual eth_requestAccounts — no +// address is disclosed by this. +function rememberPendingChain(origin, chainId) { + const cur = sessionGrants.get(origin) || {}; + sessionGrants.set(origin, { ...cur, eth: { ...(cur.eth || {}), chainId } }); +} +function revokeOrigin(api, origin) { + const perms = permissions(api); + delete perms[origin]; + api.storage.set("permissions", perms); + sessionGrants.delete(origin); +} +// Panel view: persisted grants plus session-only ones. An origin with no +// persisted entry is flagged `session`; one with both gets the session +// scopes merged in under the persisted ones. +function grantsForPanel(api) { + const out = JSON.parse(JSON.stringify(permissions(api))); + for (const [origin, g] of sessionGrants) { + const hasGrant = g.readAddress || g.eth?.readAddress || g.sol?.readAddress || g.trx?.readAddress; + if (!hasGrant) continue; + out[origin] = out[origin] ? { ...g, ...out[origin] } : { ...g, session: true }; + } + return out; +} + async function withOriginLock(origin, fn) { if (pendingByOrigin.has(origin)) throw new Error("a wallet request from this site is already waiting for approval"); pendingByOrigin.add(origin); @@ -2643,8 +2711,7 @@ function registerPageMessages(api) { const origin = fromPage(m); if (!isDappOrigin(origin)) throw new Error("this page cannot use the wallet bridge"); const rt = legacyBchRuntime(); - const perms = permissions(api); - if (perms[origin] && perms[origin].readAddress) return rt.adapter.current().address; + if (grantFor(api, origin, "bch")) return rt.adapter.current().address; return withOriginLock(origin, async () => { const pick = await api.approvalModal({ title: "Share your Bitcoin Cash address?", @@ -2655,7 +2722,7 @@ function registerPageMessages(api) { checkbox: { id: "always", label: "Always allow this site to see my address" }, }); if (!pick.startsWith("allow")) throw new Error("user rejected"); - if (pick === "allow+always") { perms[origin] = { ...(perms[origin] || {}), readAddress: true }; api.storage.set("permissions", perms); emitState(); } + setGrant(api, origin, "bch", { readAddress: true }, pick === "allow+always"); return rt.adapter.current().address; }); }); @@ -2803,10 +2870,8 @@ function registerPageMessages(api) { api.onMessage("trx.requestAccounts", async (_p, m) => { const origin = fromPage(m); const rt = activeTronRuntime(); - const perms = permissions(api); - const alreadyOK = perms[origin] && perms[origin].trx && perms[origin].trx.readAddress; const snap = rt.adapter.snapshot(); - if (alreadyOK) return { code: 200, address: snap.address, network: snap.network }; + if (grantFor(api, origin, "trx")) return { code: 200, address: snap.address, network: snap.network }; return withOriginLock(origin, async () => { const pick = await api.approvalModal({ title: "Connect this site to your Tron wallet?", @@ -2821,18 +2886,13 @@ function registerPageMessages(api) { checkbox: { id: "always", label: "Always allow this site to see this address" }, }); if (!pick.startsWith("allow")) throw new Error("user rejected"); - if (pick === "allow+always") { - perms[origin] = { ...(perms[origin] || {}), trx: { readAddress: true, network: snap.network } }; - api.storage.set("permissions", perms); - emitState(); - } + setGrant(api, origin, "trx", { readAddress: true, network: snap.network }, pick === "allow+always"); return { code: 200, address: snap.address, network: snap.network }; }); }); api.onMessage("trx.getAccount", (_p, m) => { const origin = fromPage(m); - const perms = permissions(api); - if (!(perms[origin] && perms[origin].trx && perms[origin].trx.readAddress)) throw new Error("not connected — call tron_requestAccounts first"); + if (!grantFor(api, origin, "trx")) throw new Error("not connected — call tron_requestAccounts first"); const rt = activeTronRuntime(); const snap = rt.adapter.snapshot(); return { address: snap.address, network: snap.network }; @@ -2843,8 +2903,7 @@ function registerPageMessages(api) { api.onMessage("trx.signTransaction", async (p, m) => { const origin = fromPage(m); const rt = activeTronRuntime(); - const perms = permissions(api); - if (!(perms[origin] && perms[origin].trx && perms[origin].trx.readAddress)) throw new Error("not connected — call tron_requestAccounts first"); + if (!grantFor(api, origin, "trx")) throw new Error("not connected — call tron_requestAccounts first"); const tx = p && p.transaction; if (!tx || typeof tx !== "object" || !tx.raw_data_hex || !tx.raw_data) throw new Error("bad transaction"); return withOriginLock(origin, async () => { @@ -2876,8 +2935,7 @@ function registerPageMessages(api) { api.onMessage("trx.sendRawTransaction", async (p, m) => { const origin = fromPage(m); const rt = activeTronRuntime(); - const perms = permissions(api); - if (!(perms[origin] && perms[origin].trx && perms[origin].trx.readAddress)) throw new Error("not connected — call tron_requestAccounts first"); + if (!grantFor(api, origin, "trx")) throw new Error("not connected — call tron_requestAccounts first"); const signedTx = p && p.transaction; if (!signedTx || !signedTx.raw_data_hex || !Array.isArray(signedTx.signature)) throw new Error("bad signed tx"); // No approval here — broadcasting a *signed* tx does not add any risk @@ -2888,8 +2946,7 @@ function registerPageMessages(api) { api.onMessage("trx.signMessageV2", async (p, m) => { const origin = fromPage(m); const rt = activeTronRuntime(); - const perms = permissions(api); - if (!(perms[origin] && perms[origin].trx && perms[origin].trx.readAddress)) throw new Error("not connected — call tron_requestAccounts first"); + if (!grantFor(api, origin, "trx")) throw new Error("not connected — call tron_requestAccounts first"); const message = String(p && p.message != null ? p.message : ""); if (message.length > 4096) throw new Error("message too long"); return withOriginLock(origin, async () => { @@ -2921,20 +2978,14 @@ function registerPageMessages(api) { for (const rt of ctx.runtimes.values()) if (rt.entry.chain === "eth" && rt.phase === "ready") return rt; throw new Error("no Ethereum wallet available — add one in the Aegis sidebar"); } - function ethConnectedFor(origin) { - const p = permissions(api)[origin]; - return !!(p && p.eth && p.eth.readAddress); - } + function ethConnectedFor(origin) { return !!grantFor(api, origin, "eth"); } api.onMessage("eth.requestAccounts", async (_p, m) => { const origin = fromPage(m); const rt = activeEthRuntime(); const snap = rt.adapter.snapshot(); const chainIdHex = "0x" + Number(snap.chainId).toString(16); const networkVersion = String(snap.chainId); - const perms = permissions(api); - if (perms[origin] && perms[origin].eth && perms[origin].eth.readAddress) { - return { address: snap.address, chainIdHex, networkVersion }; - } + if (ethConnectedFor(origin)) return { address: snap.address, chainIdHex, networkVersion }; return withOriginLock(origin, async () => { const pick = await api.approvalModal({ title: "Connect this site to your Ethereum wallet?", @@ -2949,11 +3000,7 @@ function registerPageMessages(api) { checkbox: { id: "always", label: "Always allow this site to see this address" }, }); if (!pick.startsWith("allow")) throw new Error("user rejected"); - if (pick === "allow+always") { - perms[origin] = { ...(perms[origin] || {}), eth: { readAddress: true, chainId: snap.chainId } }; - api.storage.set("permissions", perms); - emitState(); - } + setGrant(api, origin, "eth", { readAddress: true, chainId: snap.chainId }, pick === "allow+always"); return { address: snap.address, chainIdHex, networkVersion }; }); }); @@ -3183,10 +3230,7 @@ function registerPageMessages(api) { for (const rt of ctx.runtimes.values()) if (rt.entry.chain === "sol" && rt.phase === "ready") return rt; throw new Error("no Solana wallet available — add one in the Aegis sidebar"); } - function solConnectedFor(origin) { - const p = permissions(api)[origin]; - return !!(p && p.sol && p.sol.readAddress); - } + function solConnectedFor(origin) { return !!grantFor(api, origin, "sol"); } api.onMessage("sol.connect", async (_p, m) => { const origin = fromPage(m); const rt = activeSolRuntime(); @@ -3206,12 +3250,7 @@ function registerPageMessages(api) { checkbox: { id: "always", label: "Always allow this site to see this address" }, }); if (!pick.startsWith("allow")) throw new Error("user rejected"); - if (pick === "allow+always") { - const perms = permissions(api); - perms[origin] = { ...(perms[origin] || {}), sol: { readAddress: true, network: snap.network } }; - api.storage.set("permissions", perms); - emitState(); - } + setGrant(api, origin, "sol", { readAddress: true, network: snap.network }, pick === "allow+always"); return { address: snap.address, network: snap.network }; }); }); diff --git a/bundled-addons/aegis/panel.js b/bundled-addons/aegis/panel.js index dc501fa4..1fb92afd 100644 --- a/bundled-addons/aegis/panel.js +++ b/bundled-addons/aegis/panel.js @@ -5289,7 +5289,7 @@ async function renderSites() { try { perms = await S.invoke("permissions"); } catch {} const origins = Object.keys(perms).filter((o) => { const p = perms[o]; - return p && (p.readAddress || p.sendTx || (p.trx && p.trx.readAddress)); + return p && (p.readAddress || p.sendTx || (p.trx && p.trx.readAddress) || (p.eth && p.eth.readAddress) || (p.sol && p.sol.readAddress)); }); const el = $("sites"); if (!origins.length) { el.innerHTML = `