fix: floor the entries of the site maps and the fraud list, and stop a failed save from failing silently (closes #362)
All checks were successful
check / check (push) Successful in 32s
e2e / e2e-chrome (push) Successful in 1m45s
e2e / e2e-firefox (push) Successful in 33s

allowedSites was checked as a container while its entries were dereferenced unchecked. A stored {"0x...": "notalist"} passed the state gate and rendered a completely healthy popup, then threw "base.map is not a function" inside saveState()'s per-hostname merge, so every save from that moment on failed and the user went on operating a wallet that was persisting nothing. Measured against the previous head: the popup showed the main view with no page errors, and chrome.storage.local.set was never called at all. deniedSites has the identical shape; fraudContracts the same class with a milder consequence, throwing "(state.fraudContracts || []).map is not a function" on the send screen; and the sweep for the class turned up selectedToken, which is truthiness-gated on restore and then dereferenced as text, blanking the popup outright with "tokenId.toLowerCase is not a function".

All four now get the floor issue 311 settled -- the container AND its entries, with a malformed entry dropped -- through textList() and siteMap() beside the existing tokenRefs() in persistedState.js, rather than a third mechanism. Site-map keys are written with defineProperty for the same reason networkEndpoints' keys are: a stored own "__proto__" key would otherwise be handed to the prototype setter. The background's allowed.includes(hostname) gate is covered by the same floor, where a stored string would have answered a substring match rather than merely throwing.

A save that fails is no longer swallowed. onSaveFailure() in state.js reports every failed save, awaited or not -- the save queue has to attach a rejection handler to keep advancing, which is what made a failure disappear entirely -- and the popup raises a persistent "NOT SAVED" banner naming the reason. The popup's background refresh loop no longer turns a save failure into an unhandled rejection instead of a report. Both halves are needed: the floor only covers the causes it knows about, and storage can still fail for a quota or a revoked permission.

The field-by-field categorisation in the header of stateSchema.js, and its mirror in README.md, were re-verified against the code and moved with the change; the fields left on a loose floor now carry the reason each one is still safe. The popup boot harness moved to tests/support/popupBoot.js so the new tests drive the real entry point rather than duplicating it.
This commit is contained in:
2026-08-23 18:22:01 +00:00
parent ad6aa7b20d
commit 44b0a153f0
11 changed files with 954 additions and 304 deletions

25
TODO.md
View File

@@ -45,6 +45,31 @@ but the review is broader than any of them.
# Completed Steps
- 2026-08-23: A persisted container whose ENTRIES were dereferenced unchecked no
longer reaches a `.map()` or a `.toLowerCase()`
([#362](https://git.eeqj.de/sneak/AutistMask/issues/362)). `allowedSites` was
the worst shape available: a stored `{"0x…": "notalist"}` passed the gate,
rendered a completely healthy popup, and then threw inside `saveState()`'s
per-hostname merge, so every save from that moment on failed silently and the
user went on operating a wallet that was persisting nothing — measured as
`chrome.storage.local.set` never being called at all. `deniedSites` has the
same shape, `fraudContracts` the same class with a milder consequence (a
broken send screen, since the boot path only reaches it through
`loadHomeTxs()`, which catches), and the sweep for the class turned up
`selectedToken`, which blanked the popup outright when restoring onto
address-token. All four now get the floor
[#311](https://git.eeqj.de/sneak/AutistMask/issues/311) settled — container
AND entries, malformed entries dropped — through `textList()` and `siteMap()`
beside the existing `tokenRefs()` in `src/shared/persistedState.js`. Site-map
keys are written with `defineProperty` for the same reason `networkEndpoints`'
are. Separately, a save that fails is no longer swallowed: `onSaveFailure()`
in `src/shared/state.js` reports every failed save, awaited or not, and the
popup raises a persistent "NOT SAVED" banner; the background refresh loop no
longer turns a failure into an unhandled rejection instead of a report. The
field-by-field categorisation in the header of `src/shared/stateSchema.js`,
and its mirror in `README.md`, were re-verified against the code and moved
with the change.
- 2026-08-23: A swap whose output token the calldata never named is said to be
unknown instead of being called ETH
([#353](https://git.eeqj.de/sneak/AutistMask/issues/353)). `tokenInfo(null)`