Add site connection permissions, approval flow, and active address
Some checks failed
check / check (push) Has been cancelled
Some checks failed
check / check (push) Has been cancelled
- Add activeAddress, allowedSites, deniedSites, rememberSiteChoice to persisted state - Replace auto-connect with permission checks: allowed sites connect automatically, denied sites are rejected, unknown sites trigger an approval popup - Add approval popup UI with hostname display, active address preview, remember checkbox, and allow/deny buttons - Add ACTIVE/[select] indicator on address rows in the main view to set the active web3 address - Add allowed/denied site list management in settings with delete buttons - Broadcast accountsChanged to connected dapps when active address changes - Handle approval window close as implicit denial
This commit is contained in:
@@ -15,29 +15,51 @@ const storageApi =
|
||||
: chrome.storage.local;
|
||||
const runtime =
|
||||
typeof browser !== "undefined" ? browser.runtime : chrome.runtime;
|
||||
const windowsApi =
|
||||
typeof browser !== "undefined" ? browser.windows : chrome.windows;
|
||||
const tabsApi = typeof browser !== "undefined" ? browser.tabs : chrome.tabs;
|
||||
|
||||
// Connected sites: { origin: [address, ...] }
|
||||
// Connected sites (in-memory, non-persisted): { origin: true }
|
||||
const connectedSites = {};
|
||||
|
||||
// Pending approval requests: { id: { origin, hostname, resolve } }
|
||||
const pendingApprovals = {};
|
||||
let nextApprovalId = 1;
|
||||
|
||||
async function getState() {
|
||||
const result = await storageApi.get("autistmask");
|
||||
return result.autistmask || { wallets: [], rpcUrl: DEFAULT_RPC_URL };
|
||||
return (
|
||||
result.autistmask || {
|
||||
wallets: [],
|
||||
rpcUrl: DEFAULT_RPC_URL,
|
||||
activeAddress: null,
|
||||
allowedSites: [],
|
||||
deniedSites: [],
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
async function getAccounts() {
|
||||
const state = await getState();
|
||||
const accounts = [];
|
||||
for (const wallet of state.wallets) {
|
||||
for (const addr of wallet.addresses) {
|
||||
accounts.push(addr.address);
|
||||
}
|
||||
async function getActiveAddress() {
|
||||
const s = await getState();
|
||||
if (s.activeAddress) return s.activeAddress;
|
||||
// Fall back to first address
|
||||
if (s.wallets.length > 0 && s.wallets[0].addresses.length > 0) {
|
||||
return s.wallets[0].addresses[0].address;
|
||||
}
|
||||
return accounts;
|
||||
return null;
|
||||
}
|
||||
|
||||
async function getRpcUrl() {
|
||||
const state = await getState();
|
||||
return state.rpcUrl || DEFAULT_RPC_URL;
|
||||
const s = await getState();
|
||||
return s.rpcUrl || DEFAULT_RPC_URL;
|
||||
}
|
||||
|
||||
function extractHostname(origin) {
|
||||
try {
|
||||
return new URL(origin).hostname;
|
||||
} catch {
|
||||
return origin;
|
||||
}
|
||||
}
|
||||
|
||||
// Proxy an RPC call to the Ethereum node
|
||||
@@ -60,6 +82,86 @@ async function proxyRpc(method, params) {
|
||||
return json.result;
|
||||
}
|
||||
|
||||
// Open an approval popup and return a promise that resolves with the user decision
|
||||
function requestApproval(origin, hostname) {
|
||||
return new Promise((resolve) => {
|
||||
const id = nextApprovalId++;
|
||||
pendingApprovals[id] = { origin, hostname, resolve };
|
||||
|
||||
const popupUrl = runtime.getURL("src/popup/index.html?approval=" + id);
|
||||
windowsApi.create(
|
||||
{
|
||||
url: popupUrl,
|
||||
type: "popup",
|
||||
width: 400,
|
||||
height: 500,
|
||||
},
|
||||
(win) => {
|
||||
if (win) {
|
||||
pendingApprovals[id].windowId = win.id;
|
||||
}
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
// Handle connection requests (eth_requestAccounts, wallet_requestPermissions)
|
||||
async function handleConnectionRequest(origin) {
|
||||
const s = await getState();
|
||||
const activeAddress = await getActiveAddress();
|
||||
if (!activeAddress) {
|
||||
return { error: { message: "No accounts available" } };
|
||||
}
|
||||
|
||||
const hostname = extractHostname(origin);
|
||||
|
||||
// Check denied list
|
||||
if (s.deniedSites.includes(hostname)) {
|
||||
return {
|
||||
error: {
|
||||
code: 4001,
|
||||
message: "User rejected the request.",
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
// Check allowed list or in-memory connected
|
||||
if (s.allowedSites.includes(hostname) || connectedSites[origin]) {
|
||||
return { result: [activeAddress] };
|
||||
}
|
||||
|
||||
// Open approval popup
|
||||
const decision = await requestApproval(origin, hostname);
|
||||
|
||||
if (decision.approved) {
|
||||
if (decision.remember) {
|
||||
// Reload state to get latest, add to allowed, persist
|
||||
await loadState();
|
||||
if (!state.allowedSites.includes(hostname)) {
|
||||
state.allowedSites.push(hostname);
|
||||
}
|
||||
await saveState();
|
||||
} else {
|
||||
connectedSites[origin] = true;
|
||||
}
|
||||
return { result: [activeAddress] };
|
||||
} else {
|
||||
if (decision.remember) {
|
||||
await loadState();
|
||||
if (!state.deniedSites.includes(hostname)) {
|
||||
state.deniedSites.push(hostname);
|
||||
}
|
||||
await saveState();
|
||||
}
|
||||
return {
|
||||
error: {
|
||||
code: 4001,
|
||||
message: "User rejected the request.",
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Methods that are safe to proxy directly to the RPC node
|
||||
const PROXY_METHODS = [
|
||||
"eth_blockNumber",
|
||||
@@ -86,15 +188,20 @@ const PROXY_METHODS = [
|
||||
];
|
||||
|
||||
async function handleRpc(method, params, origin) {
|
||||
// Methods that need wallet involvement
|
||||
if (method === "eth_requestAccounts" || method === "eth_accounts") {
|
||||
const accounts = await getAccounts();
|
||||
if (accounts.length === 0) {
|
||||
return { error: { message: "No accounts available" } };
|
||||
// Connection requests — go through approval flow
|
||||
if (method === "eth_requestAccounts") {
|
||||
return handleConnectionRequest(origin);
|
||||
}
|
||||
|
||||
if (method === "eth_accounts") {
|
||||
const s = await getState();
|
||||
const activeAddress = await getActiveAddress();
|
||||
if (!activeAddress) return { result: [] };
|
||||
const hostname = extractHostname(origin);
|
||||
if (s.allowedSites.includes(hostname) || connectedSites[origin]) {
|
||||
return { result: [activeAddress] };
|
||||
}
|
||||
// Auto-connect for now (approval flow is a future TODO)
|
||||
connectedSites[origin] = accounts;
|
||||
return { result: accounts };
|
||||
return { result: [] };
|
||||
}
|
||||
|
||||
if (method === "eth_chainId") {
|
||||
@@ -128,11 +235,8 @@ async function handleRpc(method, params, origin) {
|
||||
}
|
||||
|
||||
if (method === "wallet_requestPermissions") {
|
||||
const accounts = await getAccounts();
|
||||
if (accounts.length === 0) {
|
||||
return { error: { message: "No accounts available" } };
|
||||
}
|
||||
connectedSites[origin] = accounts;
|
||||
const connResult = await handleConnectionRequest(origin);
|
||||
if (connResult.error) return connResult;
|
||||
return {
|
||||
result: [
|
||||
{
|
||||
@@ -140,7 +244,7 @@ async function handleRpc(method, params, origin) {
|
||||
caveats: [
|
||||
{
|
||||
type: "restrictReturnedAccounts",
|
||||
value: accounts,
|
||||
value: connResult.result,
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -149,8 +253,12 @@ async function handleRpc(method, params, origin) {
|
||||
}
|
||||
|
||||
if (method === "wallet_getPermissions") {
|
||||
const accounts = connectedSites[origin] || [];
|
||||
if (accounts.length === 0) {
|
||||
const s = await getState();
|
||||
const activeAddress = await getActiveAddress();
|
||||
const hostname = extractHostname(origin);
|
||||
const isConnected =
|
||||
s.allowedSites.includes(hostname) || connectedSites[origin];
|
||||
if (!isConnected || !activeAddress) {
|
||||
return { result: [] };
|
||||
}
|
||||
return {
|
||||
@@ -160,7 +268,7 @@ async function handleRpc(method, params, origin) {
|
||||
caveats: [
|
||||
{
|
||||
type: "restrictReturnedAccounts",
|
||||
value: accounts,
|
||||
value: [activeAddress],
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -169,14 +277,12 @@ async function handleRpc(method, params, origin) {
|
||||
}
|
||||
|
||||
if (method === "personal_sign" || method === "eth_sign") {
|
||||
// TODO: implement signature approval flow
|
||||
return {
|
||||
error: { message: "Signing not yet implemented in AutistMask." },
|
||||
};
|
||||
}
|
||||
|
||||
if (method === "eth_signTypedData_v4" || method === "eth_signTypedData") {
|
||||
// TODO: implement typed data signing
|
||||
return {
|
||||
error: {
|
||||
message:
|
||||
@@ -186,8 +292,6 @@ async function handleRpc(method, params, origin) {
|
||||
}
|
||||
|
||||
if (method === "eth_sendTransaction") {
|
||||
// TODO: implement transaction signing approval flow
|
||||
// For now, return an error directing the user to use the popup
|
||||
return {
|
||||
error: {
|
||||
message:
|
||||
@@ -209,6 +313,30 @@ async function handleRpc(method, params, origin) {
|
||||
return { error: { message: "Unsupported method: " + method } };
|
||||
}
|
||||
|
||||
// Broadcast accountsChanged to all tabs
|
||||
async function broadcastAccountsChanged() {
|
||||
const activeAddress = await getActiveAddress();
|
||||
const accounts = activeAddress ? [activeAddress] : [];
|
||||
tabsApi.query({}, (tabs) => {
|
||||
for (const tab of tabs) {
|
||||
tabsApi.sendMessage(
|
||||
tab.id,
|
||||
{
|
||||
type: "AUTISTMASK_EVENT",
|
||||
eventName: "accountsChanged",
|
||||
data: accounts,
|
||||
},
|
||||
() => {
|
||||
// Ignore errors for tabs without content script
|
||||
if (runtime.lastError) {
|
||||
// expected for tabs without our content script
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Background balance refresh: every 60 seconds when the popup isn't open.
|
||||
// When the popup IS open, its 10-second interval keeps lastBalanceRefresh
|
||||
// fresh, so this naturally skips.
|
||||
@@ -227,14 +355,59 @@ async function backgroundRefresh() {
|
||||
|
||||
setInterval(backgroundRefresh, BACKGROUND_REFRESH_INTERVAL);
|
||||
|
||||
// Listen for messages from content scripts
|
||||
runtime.onMessage.addListener((msg, sender, sendResponse) => {
|
||||
if (msg.type !== "AUTISTMASK_RPC") return;
|
||||
|
||||
handleRpc(msg.method, msg.params, msg.origin).then((response) => {
|
||||
sendResponse(response);
|
||||
// When approval window is closed without a response, treat as rejection
|
||||
if (windowsApi && windowsApi.onRemoved) {
|
||||
windowsApi.onRemoved.addListener((windowId) => {
|
||||
for (const [id, approval] of Object.entries(pendingApprovals)) {
|
||||
if (approval.windowId === windowId) {
|
||||
approval.resolve({ approved: false, remember: false });
|
||||
delete pendingApprovals[id];
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Return true to indicate async response
|
||||
return true;
|
||||
// Listen for messages from content scripts and popup
|
||||
runtime.onMessage.addListener((msg, sender, sendResponse) => {
|
||||
if (msg.type === "AUTISTMASK_RPC") {
|
||||
handleRpc(msg.method, msg.params, msg.origin).then((response) => {
|
||||
sendResponse(response);
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
if (msg.type === "AUTISTMASK_GET_APPROVAL") {
|
||||
const approval = pendingApprovals[msg.id];
|
||||
if (approval) {
|
||||
sendResponse({
|
||||
hostname: approval.hostname,
|
||||
origin: approval.origin,
|
||||
});
|
||||
} else {
|
||||
sendResponse(null);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
if (msg.type === "AUTISTMASK_APPROVAL_RESPONSE") {
|
||||
const approval = pendingApprovals[msg.id];
|
||||
if (approval) {
|
||||
approval.resolve({
|
||||
approved: msg.approved,
|
||||
remember: msg.remember,
|
||||
});
|
||||
delete pendingApprovals[msg.id];
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
if (msg.type === "AUTISTMASK_ACTIVE_CHANGED") {
|
||||
broadcastAccountsChanged();
|
||||
return false;
|
||||
}
|
||||
|
||||
if (msg.type === "AUTISTMASK_REMOVE_SITE") {
|
||||
// Popup already saved state; nothing else needed
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user