Aegis: a plain Connect now lasts for the session
Connecting without ticking "Always allow" stored nothing, so the site's very next call (personal_sign, signTransaction, ...) failed with "not connected". Plain Connect now grants the origin in memory until Theseus restarts; "Always allow" still persists. Every bridge (BCH, Tron, EVM, Solana) checks grants the same way, revoke clears both, and the connected-sites list shows EVM/Solana grants and which ones are session-only.
This commit is contained in:
parent
da56187b39
commit
4c3d27f1b3
2 changed files with 91 additions and 48 deletions
|
|
@ -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) => {
|
api.onMessage("revoke", (p, m) => {
|
||||||
fromPanel(m);
|
fromPanel(m);
|
||||||
const perms = permissions(api);
|
revokeOrigin(api, String(p && p.origin || ""));
|
||||||
delete perms[String(p && p.origin || "")];
|
emitState();
|
||||||
api.storage.set("permissions", perms);
|
return grantsForPanel(api);
|
||||||
return perms;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// ---- security: quick-access PIN + policy flags ------------------------
|
// ---- 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 BCH_ALLOWANCES = [100000, 1000000, 10000000]; // 0.001, 0.01, 0.1 BCH
|
||||||
const pendingByOrigin = new Set();
|
const pendingByOrigin = new Set();
|
||||||
function permissions(api) { const p = api.storage.get("permissions", {}); return p && typeof p === "object" ? p : {}; }
|
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) {
|
async function withOriginLock(origin, fn) {
|
||||||
if (pendingByOrigin.has(origin)) throw new Error("a wallet request from this site is already waiting for approval");
|
if (pendingByOrigin.has(origin)) throw new Error("a wallet request from this site is already waiting for approval");
|
||||||
pendingByOrigin.add(origin);
|
pendingByOrigin.add(origin);
|
||||||
|
|
@ -2643,8 +2711,7 @@ function registerPageMessages(api) {
|
||||||
const origin = fromPage(m);
|
const origin = fromPage(m);
|
||||||
if (!isDappOrigin(origin)) throw new Error("this page cannot use the wallet bridge");
|
if (!isDappOrigin(origin)) throw new Error("this page cannot use the wallet bridge");
|
||||||
const rt = legacyBchRuntime();
|
const rt = legacyBchRuntime();
|
||||||
const perms = permissions(api);
|
if (grantFor(api, origin, "bch")) return rt.adapter.current().address;
|
||||||
if (perms[origin] && perms[origin].readAddress) return rt.adapter.current().address;
|
|
||||||
return withOriginLock(origin, async () => {
|
return withOriginLock(origin, async () => {
|
||||||
const pick = await api.approvalModal({
|
const pick = await api.approvalModal({
|
||||||
title: "Share your Bitcoin Cash address?",
|
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" },
|
checkbox: { id: "always", label: "Always allow this site to see my address" },
|
||||||
});
|
});
|
||||||
if (!pick.startsWith("allow")) throw new Error("user rejected");
|
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;
|
return rt.adapter.current().address;
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
@ -2803,10 +2870,8 @@ function registerPageMessages(api) {
|
||||||
api.onMessage("trx.requestAccounts", async (_p, m) => {
|
api.onMessage("trx.requestAccounts", async (_p, m) => {
|
||||||
const origin = fromPage(m);
|
const origin = fromPage(m);
|
||||||
const rt = activeTronRuntime();
|
const rt = activeTronRuntime();
|
||||||
const perms = permissions(api);
|
|
||||||
const alreadyOK = perms[origin] && perms[origin].trx && perms[origin].trx.readAddress;
|
|
||||||
const snap = rt.adapter.snapshot();
|
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 () => {
|
return withOriginLock(origin, async () => {
|
||||||
const pick = await api.approvalModal({
|
const pick = await api.approvalModal({
|
||||||
title: "Connect this site to your Tron wallet?",
|
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" },
|
checkbox: { id: "always", label: "Always allow this site to see this address" },
|
||||||
});
|
});
|
||||||
if (!pick.startsWith("allow")) throw new Error("user rejected");
|
if (!pick.startsWith("allow")) throw new Error("user rejected");
|
||||||
if (pick === "allow+always") {
|
setGrant(api, origin, "trx", { readAddress: true, network: snap.network }, pick === "allow+always");
|
||||||
perms[origin] = { ...(perms[origin] || {}), trx: { readAddress: true, network: snap.network } };
|
|
||||||
api.storage.set("permissions", perms);
|
|
||||||
emitState();
|
|
||||||
}
|
|
||||||
return { code: 200, address: snap.address, network: snap.network };
|
return { code: 200, address: snap.address, network: snap.network };
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
api.onMessage("trx.getAccount", (_p, m) => {
|
api.onMessage("trx.getAccount", (_p, m) => {
|
||||||
const origin = fromPage(m);
|
const origin = fromPage(m);
|
||||||
const perms = permissions(api);
|
if (!grantFor(api, origin, "trx")) throw new Error("not connected — call tron_requestAccounts first");
|
||||||
if (!(perms[origin] && perms[origin].trx && perms[origin].trx.readAddress)) throw new Error("not connected — call tron_requestAccounts first");
|
|
||||||
const rt = activeTronRuntime();
|
const rt = activeTronRuntime();
|
||||||
const snap = rt.adapter.snapshot();
|
const snap = rt.adapter.snapshot();
|
||||||
return { address: snap.address, network: snap.network };
|
return { address: snap.address, network: snap.network };
|
||||||
|
|
@ -2843,8 +2903,7 @@ function registerPageMessages(api) {
|
||||||
api.onMessage("trx.signTransaction", async (p, m) => {
|
api.onMessage("trx.signTransaction", async (p, m) => {
|
||||||
const origin = fromPage(m);
|
const origin = fromPage(m);
|
||||||
const rt = activeTronRuntime();
|
const rt = activeTronRuntime();
|
||||||
const perms = permissions(api);
|
if (!grantFor(api, origin, "trx")) throw new Error("not connected — call tron_requestAccounts first");
|
||||||
if (!(perms[origin] && perms[origin].trx && perms[origin].trx.readAddress)) throw new Error("not connected — call tron_requestAccounts first");
|
|
||||||
const tx = p && p.transaction;
|
const tx = p && p.transaction;
|
||||||
if (!tx || typeof tx !== "object" || !tx.raw_data_hex || !tx.raw_data) throw new Error("bad transaction");
|
if (!tx || typeof tx !== "object" || !tx.raw_data_hex || !tx.raw_data) throw new Error("bad transaction");
|
||||||
return withOriginLock(origin, async () => {
|
return withOriginLock(origin, async () => {
|
||||||
|
|
@ -2876,8 +2935,7 @@ function registerPageMessages(api) {
|
||||||
api.onMessage("trx.sendRawTransaction", async (p, m) => {
|
api.onMessage("trx.sendRawTransaction", async (p, m) => {
|
||||||
const origin = fromPage(m);
|
const origin = fromPage(m);
|
||||||
const rt = activeTronRuntime();
|
const rt = activeTronRuntime();
|
||||||
const perms = permissions(api);
|
if (!grantFor(api, origin, "trx")) throw new Error("not connected — call tron_requestAccounts first");
|
||||||
if (!(perms[origin] && perms[origin].trx && perms[origin].trx.readAddress)) throw new Error("not connected — call tron_requestAccounts first");
|
|
||||||
const signedTx = p && p.transaction;
|
const signedTx = p && p.transaction;
|
||||||
if (!signedTx || !signedTx.raw_data_hex || !Array.isArray(signedTx.signature)) throw new Error("bad signed tx");
|
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
|
// 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) => {
|
api.onMessage("trx.signMessageV2", async (p, m) => {
|
||||||
const origin = fromPage(m);
|
const origin = fromPage(m);
|
||||||
const rt = activeTronRuntime();
|
const rt = activeTronRuntime();
|
||||||
const perms = permissions(api);
|
if (!grantFor(api, origin, "trx")) throw new Error("not connected — call tron_requestAccounts first");
|
||||||
if (!(perms[origin] && perms[origin].trx && perms[origin].trx.readAddress)) throw new Error("not connected — call tron_requestAccounts first");
|
|
||||||
const message = String(p && p.message != null ? p.message : "");
|
const message = String(p && p.message != null ? p.message : "");
|
||||||
if (message.length > 4096) throw new Error("message too long");
|
if (message.length > 4096) throw new Error("message too long");
|
||||||
return withOriginLock(origin, async () => {
|
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;
|
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");
|
throw new Error("no Ethereum wallet available — add one in the Aegis sidebar");
|
||||||
}
|
}
|
||||||
function ethConnectedFor(origin) {
|
function ethConnectedFor(origin) { return !!grantFor(api, origin, "eth"); }
|
||||||
const p = permissions(api)[origin];
|
|
||||||
return !!(p && p.eth && p.eth.readAddress);
|
|
||||||
}
|
|
||||||
api.onMessage("eth.requestAccounts", async (_p, m) => {
|
api.onMessage("eth.requestAccounts", async (_p, m) => {
|
||||||
const origin = fromPage(m);
|
const origin = fromPage(m);
|
||||||
const rt = activeEthRuntime();
|
const rt = activeEthRuntime();
|
||||||
const snap = rt.adapter.snapshot();
|
const snap = rt.adapter.snapshot();
|
||||||
const chainIdHex = "0x" + Number(snap.chainId).toString(16);
|
const chainIdHex = "0x" + Number(snap.chainId).toString(16);
|
||||||
const networkVersion = String(snap.chainId);
|
const networkVersion = String(snap.chainId);
|
||||||
const perms = permissions(api);
|
if (ethConnectedFor(origin)) return { address: snap.address, chainIdHex, networkVersion };
|
||||||
if (perms[origin] && perms[origin].eth && perms[origin].eth.readAddress) {
|
|
||||||
return { address: snap.address, chainIdHex, networkVersion };
|
|
||||||
}
|
|
||||||
return withOriginLock(origin, async () => {
|
return withOriginLock(origin, async () => {
|
||||||
const pick = await api.approvalModal({
|
const pick = await api.approvalModal({
|
||||||
title: "Connect this site to your Ethereum wallet?",
|
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" },
|
checkbox: { id: "always", label: "Always allow this site to see this address" },
|
||||||
});
|
});
|
||||||
if (!pick.startsWith("allow")) throw new Error("user rejected");
|
if (!pick.startsWith("allow")) throw new Error("user rejected");
|
||||||
if (pick === "allow+always") {
|
setGrant(api, origin, "eth", { readAddress: true, chainId: snap.chainId }, pick === "allow+always");
|
||||||
perms[origin] = { ...(perms[origin] || {}), eth: { readAddress: true, chainId: snap.chainId } };
|
|
||||||
api.storage.set("permissions", perms);
|
|
||||||
emitState();
|
|
||||||
}
|
|
||||||
return { address: snap.address, chainIdHex, networkVersion };
|
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;
|
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");
|
throw new Error("no Solana wallet available — add one in the Aegis sidebar");
|
||||||
}
|
}
|
||||||
function solConnectedFor(origin) {
|
function solConnectedFor(origin) { return !!grantFor(api, origin, "sol"); }
|
||||||
const p = permissions(api)[origin];
|
|
||||||
return !!(p && p.sol && p.sol.readAddress);
|
|
||||||
}
|
|
||||||
api.onMessage("sol.connect", async (_p, m) => {
|
api.onMessage("sol.connect", async (_p, m) => {
|
||||||
const origin = fromPage(m);
|
const origin = fromPage(m);
|
||||||
const rt = activeSolRuntime();
|
const rt = activeSolRuntime();
|
||||||
|
|
@ -3206,12 +3250,7 @@ function registerPageMessages(api) {
|
||||||
checkbox: { id: "always", label: "Always allow this site to see this address" },
|
checkbox: { id: "always", label: "Always allow this site to see this address" },
|
||||||
});
|
});
|
||||||
if (!pick.startsWith("allow")) throw new Error("user rejected");
|
if (!pick.startsWith("allow")) throw new Error("user rejected");
|
||||||
if (pick === "allow+always") {
|
setGrant(api, origin, "sol", { readAddress: true, network: snap.network }, pick === "allow+always");
|
||||||
const perms = permissions(api);
|
|
||||||
perms[origin] = { ...(perms[origin] || {}), sol: { readAddress: true, network: snap.network } };
|
|
||||||
api.storage.set("permissions", perms);
|
|
||||||
emitState();
|
|
||||||
}
|
|
||||||
return { address: snap.address, network: snap.network };
|
return { address: snap.address, network: snap.network };
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -5289,7 +5289,7 @@ async function renderSites() {
|
||||||
try { perms = await S.invoke("permissions"); } catch {}
|
try { perms = await S.invoke("permissions"); } catch {}
|
||||||
const origins = Object.keys(perms).filter((o) => {
|
const origins = Object.keys(perms).filter((o) => {
|
||||||
const p = perms[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");
|
const el = $("sites");
|
||||||
if (!origins.length) { el.innerHTML = `<div class="hint">None yet.</div>`; return; }
|
if (!origins.length) { el.innerHTML = `<div class="hint">None yet.</div>`; return; }
|
||||||
|
|
@ -5298,6 +5298,10 @@ async function renderSites() {
|
||||||
if (p.readAddress) what.push("BCH address");
|
if (p.readAddress) what.push("BCH address");
|
||||||
if (p.sendTx) what.push(`BCH payments: ${fmtBig(Math.max(0, p.sendTx.capSats - (p.sendTx.usedSats || 0)), 8)} of ${fmtBig(p.sendTx.capSats, 8)} BCH left`);
|
if (p.sendTx) what.push(`BCH payments: ${fmtBig(Math.max(0, p.sendTx.capSats - (p.sendTx.usedSats || 0)), 8)} of ${fmtBig(p.sendTx.capSats, 8)} BCH left`);
|
||||||
if (p.trx && p.trx.readAddress) what.push("Tron " + (p.trx.network === "nile" ? "Nile testnet" : "mainnet") + " address");
|
if (p.trx && p.trx.readAddress) what.push("Tron " + (p.trx.network === "nile" ? "Nile testnet" : "mainnet") + " address");
|
||||||
|
if (p.eth && p.eth.readAddress) what.push("EVM address" + (p.eth.chainId ? ` (chain ${p.eth.chainId})` : ""));
|
||||||
|
if (p.sol && p.sol.readAddress) what.push("Solana address");
|
||||||
|
// A plain "Connect" lasts until Theseus restarts; only "Always allow" is stored.
|
||||||
|
if (p.session) what.push("this session only");
|
||||||
return `<div class="tx" style="grid-template-columns:1fr auto;cursor:default"><div><div class="mono">${esc(o)}</div><div class="hint">${esc(what.join(" · "))}</div></div><button class="btn sm" data-origin="${esc(o)}">Revoke</button></div>`;
|
return `<div class="tx" style="grid-template-columns:1fr auto;cursor:default"><div><div class="mono">${esc(o)}</div><div class="hint">${esc(what.join(" · "))}</div></div><button class="btn sm" data-origin="${esc(o)}">Revoke</button></div>`;
|
||||||
}).join("");
|
}).join("");
|
||||||
el.querySelectorAll("button[data-origin]").forEach((b) => b.addEventListener("click", async () => {
|
el.querySelectorAll("button[data-origin]").forEach((b) => b.addEventListener("click", async () => {
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue