harden: stop the background reading the shared state singleton, and enforce it at build time (closes #324)
Five defects, one of which destroyed every wallet, came from src/background reading and writing the module-level state singleton the MV3 worker never populates, which silently served DEFAULT_STATE. Each point fix created the next defect. The background now has its own per-call getState() and a queued read-modify-write updateState(); the singleton is unreachable from it, and an unpopulated read throws instead of serving defaults. The prohibition is enforced by the build, not by review: build.js asserts over esbuild's own metafile that no forbidden module is an input of a background bundle, so every specifier syntax esbuild resolves is covered, and both halves of the table are checked for rot -- a stale key, a stale module, an empty list, or an unlisted entry point under src/background/ all fail the build. The ESLint rule remains as fast local feedback and reads the same shared table. Known bounds are documented where the table lives. Also closes #320: getProvider() now requires a validated network id, so a cold worker no longer prepares a non-mainnet dApp transaction for mainnet and gets refused by the wallet's own verifier. backgroundRefresh() no longer mutates address objects across a network round trip, the broadcast path takes its endpoint and chain id from one snapshot, and eight test storage stubs now structured-clone on get as the real chrome.storage.local does. closes #320
This commit was merged in pull request #344.
This commit is contained in:
@@ -13,13 +13,15 @@
|
||||
// never persisting it looks identical from `state`, and a build that never
|
||||
// wrote at all would pass a check that only reads `state` back.
|
||||
//
|
||||
// That makes the storage stub load-bearing, so it is a real store that
|
||||
// structured-clones on both `set` and `get`. A stub whose `get` hands back
|
||||
// the same object its `set` was given aliases the caller's own array: the
|
||||
// test then reads its own in-memory mutation and calls it persistence, and
|
||||
// passes against a build that persists nothing (see issue #324). The
|
||||
// aliasing is closed off explicitly by the first test below rather than
|
||||
// left as an assumption about `structuredClone`.
|
||||
// That makes the storage stub load-bearing, so it is the shared one from
|
||||
// tests/support/storageStub.js, a real store that structured-clones on both
|
||||
// `set` and `get`. A stub whose `get` hands back the same object its `set`
|
||||
// was given aliases the caller's own array: the test then reads its own
|
||||
// in-memory mutation and calls it persistence, and passes against a build
|
||||
// that persists nothing
|
||||
// (https://git.eeqj.de/sneak/AutistMask/issues/324). The aliasing is closed
|
||||
// off explicitly by the first test below rather than left as an assumption
|
||||
// about `structuredClone`.
|
||||
//
|
||||
// The view is driven against a minimal DOM stub, in the same shape as
|
||||
// tests/exportPrivkey.test.js: the module reads and writes named nodes and
|
||||
@@ -34,6 +36,7 @@ jest.mock("../src/shared/vault", () => ({
|
||||
}));
|
||||
|
||||
const { RESTORABLE_VIEWS } = require("../src/popup/restorableViews");
|
||||
const { makeStorageStub } = require("./support/storageStub");
|
||||
|
||||
const VIEW = "delete-wallet-lost-password";
|
||||
|
||||
@@ -94,35 +97,6 @@ function makeDocument() {
|
||||
};
|
||||
}
|
||||
|
||||
// --------------------------------------------------------- storage stub
|
||||
|
||||
// A store that behaves the way `chrome.storage.local` does: what goes in is
|
||||
// serialized, so the caller keeps no handle on what came to rest there, and
|
||||
// what comes out is a fresh object the caller may mutate freely.
|
||||
function makeStorage() {
|
||||
let store = {};
|
||||
return {
|
||||
get: async (keys) => {
|
||||
const wanted =
|
||||
keys === undefined || keys === null
|
||||
? Object.keys(store)
|
||||
: [].concat(keys);
|
||||
const out = {};
|
||||
for (const key of wanted) {
|
||||
if (key in store) out[key] = structuredClone(store[key]);
|
||||
}
|
||||
return out;
|
||||
},
|
||||
set: async (items) => {
|
||||
for (const [key, value] of Object.entries(items)) {
|
||||
store[key] = structuredClone(value);
|
||||
}
|
||||
},
|
||||
// Test-only: what the extension would find on a cold start.
|
||||
_raw: () => structuredClone(store),
|
||||
};
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------ harness
|
||||
|
||||
function wallet(name, secret, addresses) {
|
||||
@@ -144,10 +118,10 @@ function load() {
|
||||
jest.resetModules();
|
||||
mockSettingsShow.mockClear();
|
||||
|
||||
const storage = makeStorage();
|
||||
const storage = makeStorageStub();
|
||||
const sent = [];
|
||||
globalThis.chrome = {
|
||||
storage: { local: storage },
|
||||
storage: { local: storage.local },
|
||||
runtime: { sendMessage: (msg) => sent.push(msg) },
|
||||
};
|
||||
globalThis.document = makeDocument();
|
||||
@@ -205,7 +179,7 @@ async function openLostPassword(deleteWallet, walletIdx) {
|
||||
// every other test in this file against a build that never writes.
|
||||
describe("the storage stub", () => {
|
||||
test("does not hand back the object it was given", async () => {
|
||||
const storage = makeStorage();
|
||||
const storage = makeStorageStub();
|
||||
const written = { wallets: [{ name: "Wallet 1" }] };
|
||||
|
||||
await storage.set({ autistmask: written });
|
||||
@@ -393,8 +367,8 @@ describe("deleting without the password", () => {
|
||||
|
||||
// The deleted wallet's secret is gone from storage entirely, not
|
||||
// merely unreferenced by the wallet list.
|
||||
expect(JSON.stringify(storage._raw())).not.toContain("secret-two");
|
||||
expect(JSON.stringify(storage._raw())).not.toContain("xpub-Wallet 2");
|
||||
expect(JSON.stringify(storage.read())).not.toContain("secret-two");
|
||||
expect(JSON.stringify(storage.read())).not.toContain("xpub-Wallet 2");
|
||||
});
|
||||
|
||||
test("only the deleted wallet's site permissions are dropped", async () => {
|
||||
@@ -462,7 +436,7 @@ describe("deleting without the password", () => {
|
||||
expect(saved.activeAddress).toBeNull();
|
||||
expect(saved.allowedSites).toEqual({});
|
||||
expect(state.currentView).toBe("welcome");
|
||||
expect(JSON.stringify(storage._raw())).not.toContain("secret-one");
|
||||
expect(JSON.stringify(storage.read())).not.toContain("secret-one");
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user