harden: list and end site connections made without Remember in Settings (closes #406)
check / check (push) Successful in 2m25s
e2e / e2e-chrome (push) Successful in 3m15s
e2e / e2e-firefox (push) Successful in 2m21s

A site allowed without "Remember" lives only in the background's
in-memory connectedSites map. Settings never listed it, and
AUTISTMASK_REMOVE_SITE, sent on every remove, did nothing, so the user
could not end such a connection.

Settings now asks the background for those sites and lists them under
Connected Sites. Removing a site from Allowed Sites or Connected Sites
drops its remembered entry under every address and sends
AUTISTMASK_REMOVE_SITE with the hostname; the background deletes every
matching connectedSites entry and sends accountsChanged with an empty
list to the site's tabs. Only the extension's own pages may send either
message.

Model: opus-5-5
This commit was merged in pull request #419.
This commit is contained in:
2026-10-04 06:41:39 +02:00
parent 5f54fcbb24
commit 49a7da87e8
6 changed files with 308 additions and 39 deletions
+41 -1
View File
@@ -1110,6 +1110,24 @@ async function broadcastAccountsChanged() {
}
}
// Tell every open tab of a site Settings removed that it has no account.
async function broadcastSiteRemoved(hostname) {
let tabs;
try {
tabs = await tabsQuery({});
} catch {
return;
}
for (const tab of tabs) {
if (!tab.url || extractHostname(tab.url) !== hostname) continue;
tabsSendMessage(tab.id, {
type: "AUTISTMASK_EVENT",
eventName: "accountsChanged",
data: [],
}).catch(() => {});
}
}
// 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.
@@ -1306,6 +1324,8 @@ runtime.onMessage.addListener((msg, sender, sendResponse) => {
"AUTISTMASK_TX_RESPONSE",
"AUTISTMASK_SIGN_RESPONSE",
"AUTISTMASK_ADDRESSES_REMOVED",
"AUTISTMASK_GET_CONNECTED_SITES",
"AUTISTMASK_REMOVE_SITE",
];
if (POPUP_ONLY_TYPES.includes(msg.type) && !isExtensionSender(sender)) {
sendResponse({ error: "Unauthorized sender" });
@@ -1662,8 +1682,28 @@ runtime.onMessage.addListener((msg, sender, sendResponse) => {
return false;
}
// Settings lists the sites connected without "Remember", which only this
// worker knows. The origin is what precedes the key's last colon.
if (msg.type === "AUTISTMASK_GET_CONNECTED_SITES") {
sendResponse(
Object.keys(connectedSites).map((key) =>
extractHostname(key.slice(0, key.lastIndexOf(":"))),
),
);
return false;
}
// Settings removed this site and has already dropped its remembered
// entries. Its connections approved without "Remember" end here, under
// every address, and its open tabs are told it has no account.
if (msg.type === "AUTISTMASK_REMOVE_SITE") {
// Popup already saved state; nothing else needed
for (const key of Object.keys(connectedSites)) {
const origin = key.slice(0, key.lastIndexOf(":"));
if (extractHostname(origin) === msg.hostname) {
delete connectedSites[key];
}
}
broadcastSiteRemoved(msg.hostname);
return false;
}
});
+9
View File
@@ -1064,6 +1064,15 @@
<div id="settings-allowed-sites"></div>
</div>
<div class="bg-well p-3 mx-1 mb-3">
<h3 class="font-bold mb-1">Connected Sites</h3>
<p class="text-xs text-muted mb-2">
Sites you allowed without "Remember my choice".
Switching address disconnects them.
</p>
<div id="settings-connected-sites"></div>
</div>
<div class="bg-well p-3 mx-1 mb-3">
<h3 class="font-bold mb-1">Denied Sites</h3>
<p class="text-xs text-muted mb-2">
+51 -23
View File
@@ -29,46 +29,63 @@ const {
GITEA_COMMIT_URL,
} = require("../../shared/buildInfo");
const { notify } = require("../../shared/browserApi");
const { notify, sendMessage } = require("../../shared/browserApi");
let versionClickCount = 0;
let versionClickTimer = null;
function renderSiteList(containerId, siteMap, stateKey) {
// One row per hostname, however many addresses or origins it appears under,
// each with an [x] that hands it to onRemove.
function renderSiteList(containerId, hostnames, onRemove) {
const container = $(containerId);
const hostnames = [...new Set(Object.values(siteMap).flat())];
if (hostnames.length === 0) {
const unique = [...new Set(hostnames)];
if (unique.length === 0) {
container.innerHTML = '<p class="text-xs text-muted">None</p>';
return;
}
let html = "";
hostnames.forEach((hostname) => {
unique.forEach((hostname) => {
html += `<div class="flex justify-between items-center text-xs py-1 border-b border-border-light">`;
// A hostname the URL parser produced cannot carry a delimiter, so
// this is escaped for the rule rather than for a known hole — the
// rule being that nothing reaches innerHTML unescaped.
html += `<span>${escapeHtml(hostname)}</span>`;
html += `<button class="btn-remove-site border border-border px-1 hover:bg-fg hover:text-bg cursor-pointer" data-key="${escapeHtml(stateKey)}" data-hostname="${escapeHtml(hostname)}">[x]</button>`;
html += `<button class="btn-remove-site border border-border px-1 hover:bg-fg hover:text-bg cursor-pointer" data-hostname="${escapeHtml(hostname)}">[x]</button>`;
html += `</div>`;
});
container.innerHTML = html;
container.querySelectorAll(".btn-remove-site").forEach((btn) => {
btn.addEventListener("click", async () => {
const key = btn.dataset.key;
const host = btn.dataset.hostname;
for (const addr of Object.keys(state[key])) {
state[key][addr] = state[key][addr].filter((h) => h !== host);
if (state[key][addr].length === 0) {
delete state[key][addr];
}
}
await saveState();
notify({ type: "AUTISTMASK_REMOVE_SITE" });
renderSiteList(containerId, state[key], key);
});
btn.addEventListener("click", () => onRemove(btn.dataset.hostname));
});
}
// Drop a hostname from a remembered site list under every address.
function forgetHostname(siteMap, hostname) {
for (const addr of Object.keys(siteMap)) {
siteMap[addr] = siteMap[addr].filter((h) => h !== hostname);
if (siteMap[addr].length === 0) {
delete siteMap[addr];
}
}
}
// Removing a site from Allowed Sites or Connected Sites disconnects it: it is
// no longer allowed under any address, and the background ends its
// connections approved without "Remember" and tells its open tabs.
async function removeAllowedSite(hostname) {
forgetHostname(state.allowedSites, hostname);
await saveState();
notify({ type: "AUTISTMASK_REMOVE_SITE", hostname });
await renderSiteLists();
}
// Removing a denied site only forgets the refusal; it connects nothing.
async function removeDeniedSite(hostname) {
forgetHostname(state.deniedSites, hostname);
await saveState();
await renderSiteLists();
}
function renderTrackedTokens() {
const container = $("settings-tracked-tokens");
if (state.trackedTokens.length === 0) {
@@ -202,13 +219,24 @@ function show() {
showView("settings");
}
function renderSiteLists() {
async function renderSiteLists() {
renderSiteList(
"settings-allowed-sites",
state.allowedSites,
"allowedSites",
Object.values(state.allowedSites).flat(),
removeAllowedSite,
);
renderSiteList(
"settings-denied-sites",
Object.values(state.deniedSites).flat(),
removeDeniedSite,
);
// Sites allowed without "Remember" are held only by the background, in
// memory, so it is asked for them.
renderSiteList(
"settings-connected-sites",
await sendMessage({ type: "AUTISTMASK_GET_CONNECTED_SITES" }),
removeAllowedSite,
);
renderSiteList("settings-denied-sites", state.deniedSites, "deniedSites");
}
function init(ctx) {