fix: merge state per field instead of overwriting the whole blob (closes #304)
saveState() is now a read-modify-write that merges only the fields this page changed, diffed against a deep-cloned per-page baseline. wallets, allowedSites, deniedSites and networkEndpoints merge structurally by identity, so membership comes from fresh storage except for this page's own adds and deletes. Fixes a second extension page silently deleting a wallet, the background balance refresh clobbering a concurrent add or resurrecting a delete, and a stale page resurrecting a revoked site permission. Colliding wallet identities keep both records and log rather than silently dropping an encryptedSecret. Concurrent writers of the same leaf remain last-writer-wins by design.
This commit was merged in pull request #337.
This commit is contained in:
26
TODO.md
26
TODO.md
@@ -44,6 +44,32 @@ but the review is broader than any of them.
|
||||
|
||||
# Completed Steps
|
||||
|
||||
- 2026-08-20: A second extension page can no longer silently delete a wallet
|
||||
([#304](https://git.eeqj.de/sneak/AutistMask/issues/304)). `saveState()` wrote
|
||||
the entire state blob, and every extension page — the toolbar popup, a dApp
|
||||
approval window, `backgroundRefresh()` — holds its own in-memory `state`,
|
||||
loaded once, with `showView()` saving on every navigation; a second page that
|
||||
saved after a first had written something new overwrote it, no attacker or
|
||||
unusual input required. `saveState()` is now a read-modify-write: it re-reads
|
||||
storage, diffs the persisted fields against a deep-cloned `baseline` snapshot
|
||||
taken at the last `loadState()`/`saveState()` on that page, and writes only
|
||||
the fields that actually changed — everything else is carried forward from
|
||||
storage in its loaded-and-normalized shape (`normalizePersisted()`, shared
|
||||
with `loadState()`), so a legacy or malformed record a load has always
|
||||
self-healed in memory keeps getting written back even on a save that touched
|
||||
something else entirely. `showView()` fires `saveState()` on every navigation
|
||||
without awaiting it, so two saves from the same page can be in flight at once;
|
||||
a FIFO queue serializes them rather than letting a slow one finish after a
|
||||
later one and re-derive a stale answer. Deliberately not done: the live
|
||||
`state` of a field this page does not own is not rehydrated from what another
|
||||
page wrote, only the persisted record is — adopting a concurrently-written
|
||||
value into `state` reintroduced the same clobber one page later, caught by
|
||||
`tests/txStatus.test.js` red. Two writers of the same field still resolve
|
||||
last-writer-wins, documented at the merge point. `tests/stateMerge.test.js`
|
||||
covers the two-page save and the approval-window reproduction from the issue —
|
||||
add a wallet in one page, force a save from a second page loaded before it,
|
||||
both wallets survive — each demonstrated failing against the unfixed full-blob
|
||||
write.
|
||||
- 2026-08-20: A forgotten password no longer wedges the wallet
|
||||
([#312](https://git.eeqj.de/sneak/AutistMask/issues/312)). Deleting a wallet
|
||||
was password-gated and importing its recovery phrase again was refused as a
|
||||
|
||||
Reference in New Issue
Block a user