fix: floor the entries of the site maps and the fraud list, and stop a failed save from failing silently (closes #362)
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:
28
README.md
28
README.md
@@ -1016,17 +1016,33 @@ generic `-32603` every request used to answer.
|
||||
|
||||
Every other field of the record is floored in `normalizePersisted()` rather than
|
||||
gated. That floor is a type check for the fields something dereferences
|
||||
structurally — `trackedTokens`, each address's `tokenBalances`, `networkId`,
|
||||
`networkEndpoints`, `activeAddress`, `viewStack` — and it checks the ENTRIES as
|
||||
well as the container, because `[1, 2]` is a list and `t.address` is one level
|
||||
below an `Array.isArray()`. The remaining fields get a `saved.x || default` or a
|
||||
present-or-default passthrough that takes the stored value verbatim, with no
|
||||
type check at all; which field is in which category is listed in the header of
|
||||
structurally — `trackedTokens`, each address's `tokenBalances`, `allowedSites`,
|
||||
`deniedSites`, `fraudContracts`, `viewStack`, `networkEndpoints`, plus the
|
||||
scalars `networkId`, `activeAddress` and `selectedToken` — and it checks the
|
||||
ENTRIES as well as the container, because `[1, 2]` is a list,
|
||||
`{"0x…": "notalist"}` is an object, and the dereference is one level below the
|
||||
container check. A malformed entry is dropped. The remaining fields get a
|
||||
`saved.x || default` or a present-or-default passthrough that takes the stored
|
||||
value verbatim, with no type check at all; which field is in which category, and
|
||||
why the loose floor is still enough for each of them, is listed in the header of
|
||||
`src/shared/stateSchema.js`. A truthy value of the wrong type in a field that IS
|
||||
dereferenced walks through truthiness and throws on the first read, which is the
|
||||
blank popup again by a longer route — so adding a field means choosing between
|
||||
the two by what reads it.
|
||||
|
||||
The `allowedSites` case is why the entry check is not optional. A stored
|
||||
`{"0x…": "notalist"}` is a well-formed object holding a malformed entry: it
|
||||
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 and
|
||||
the user went on operating a wallet that was persisting nothing
|
||||
([#362](https://git.eeqj.de/sneak/AutistMask/issues/362)). A save that fails is
|
||||
now also reported rather than swallowed: `onSaveFailure()` in
|
||||
`src/shared/state.js` is called for every failed save, awaited or not, and the
|
||||
popup puts up a persistent "NOT SAVED" banner (`showSaveFailureBanner()` in
|
||||
`src/popup/views/helpers.js`). Storage can still fail for reasons no floor
|
||||
covers — a quota, a revoked permission, a record a newer build wrote — and the
|
||||
wallet must never look healthy while that is true.
|
||||
|
||||
The `networkId` check is not cosmetic: that value is an object KEY into
|
||||
`state.networkEndpoints`, so an unvalidated `"__proto__"` would set the map's
|
||||
prototype instead of an own key and the user's endpoint would silently not be
|
||||
|
||||
Reference in New Issue
Block a user