fix: floor malformed allowedSites, fraudContracts and selectedToken entries (closes #362)
check / check (push) Successful in 42s
e2e / e2e-chrome (push) Successful in 1m45s
e2e / e2e-firefox (push) Successful in 31s

A stored allowedSites whose value was not a list rendered a working popup and then made every subsequent save fail silently, so the user operated a wallet that persisted nothing -- worse than a blank popup, which is at least visibly broken. fraudContracts and selectedToken had the same shape: a container floored by truthiness or not at all, while its entries were dereferenced. Entries are now floored as well as containers, following the idiom #311 established, and a failed save raises a persistent banner instead of vanishing into a swallowed rejection.

The per-field justifications that used to live in a hand-written header are replaced by a contract test that drives each field's hostile and falsy values through a real popup boot, so a claim about a field answers to the code rather than to prose. Its guarantee is stated narrowly and deliberately: no structural dereference on the code paths a wholly-corrupted profile takes, which is not every path a stored record takes. The paths it does not drive are named where the claim is made, and are tracked in #379.
This commit was merged in pull request #366.
This commit is contained in:
2026-08-23 23:06:17 +02:00
parent 45500e66cf
commit a098bb0c32
14 changed files with 2100 additions and 354 deletions
+22 -2
View File
@@ -1,9 +1,14 @@
// AutistMask popup entry point.
// Loads state, initializes views, triggers first render.
const { state, saveState, loadState } = require("../shared/state");
const {
state,
saveState,
onSaveFailure,
loadState,
} = require("../shared/state");
const { StateUnusableError } = require("../shared/stateSchema");
const { setRuntimeDebug } = require("../shared/log");
const { log, setRuntimeDebug } = require("../shared/log");
const { refreshPrices } = require("../shared/prices");
const { refreshBalances } = require("../shared/balances");
const {
@@ -11,6 +16,7 @@ const {
showView,
updateDebugBanner,
setBackRenderer,
showSaveFailureBanner,
pushCurrentView,
goBack,
} = require("./views/helpers");
@@ -61,6 +67,14 @@ async function doRefreshAndRender() {
state.lastBalanceRefresh = Date.now();
await saveState();
renderWalletList();
} catch (e) {
// Every call site fires this and walks away — the boot below, the ten
// second interval, and eight views through ctx — so it must never
// reject: an unhandled rejection is not a report of anything. The save
// inside it reports its own failure through onSaveFailure() (see
// src/shared/state.js); what is left here is a failed network round
// trip, which the next tick retries.
log.errorf("popup: background refresh failed:", e);
} finally {
refreshInFlight = false;
}
@@ -136,6 +150,12 @@ function fallbackView() {
}
async function init() {
// First, before anything can save: showView() saves on every navigation
// without awaiting, so a save that fails from here on has somewhere to be
// reported rather than being swallowed by the save queue
// (https://git.eeqj.de/sneak/AutistMask/issues/362). Registered ahead of
// the approval-window branch below too, since that window saves as well.
onSaveFailure(showSaveFailureBanner);
try {
await loadState();
} catch (e) {