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
191 lines
7.2 KiB
JavaScript
191 lines
7.2 KiB
JavaScript
// ESLint flat config. Static analysis for make check; formatting stays with
|
|
// prettier (script/fmt-check), so nothing here touches style.
|
|
//
|
|
// The sources are CommonJS and are bundled per entrypoint by build.js, so the
|
|
// globals differ by tree and are declared per tree below. Getting that wrong in
|
|
// either direction defeats the point: too few globals buries a real no-undef in
|
|
// false positives, too many hides the next unimported identifier.
|
|
|
|
const js = require("@eslint/js");
|
|
const globals = require("globals");
|
|
const backgroundState = require("./script/lib/eslint/noStateSingletonInBackground");
|
|
const {
|
|
BACKGROUND_ENTRY_PREFIX,
|
|
} = require("./script/lib/forbiddenBundleInputs");
|
|
|
|
// The extension APIs. MV3 Chrome exposes `chrome`; Firefox exposes both, and
|
|
// the code feature-detects between them.
|
|
const extensionGlobals = {
|
|
chrome: "readonly",
|
|
browser: "readonly",
|
|
};
|
|
|
|
const commonjs = {
|
|
ecmaVersion: 2024,
|
|
sourceType: "commonjs",
|
|
};
|
|
|
|
module.exports = [
|
|
{
|
|
ignores: ["dist/", "node_modules/"],
|
|
},
|
|
|
|
js.configs.recommended,
|
|
|
|
{
|
|
rules: {
|
|
// The two rules this config exists for. Both are already
|
|
// error-level in the recommended set; restated so a future
|
|
// recommended-set change cannot silently downgrade them.
|
|
"no-undef": "error",
|
|
// `_`-prefixed arguments are the deliberate "present for the
|
|
// interface, unused here" marker: the popup views share one
|
|
// init(ctx) signature and three of the eight do not read ctx.
|
|
// An unused catch binding is written `catch {`, which the repo
|
|
// already does, so caught errors stay checked.
|
|
"no-unused-vars": ["error", { argsIgnorePattern: "^_" }],
|
|
|
|
// Off tree-wide: it requires every rethrow to carry `{ cause }`,
|
|
// at 3 sites today (src/shared/balances.js 207 and 215,
|
|
// tests/e2e/firefox/run.js 131). That is a change to what the
|
|
// wallet's error paths actually throw, and it is a decision of its
|
|
// own rather than a side effect of turning a linter on — so it is
|
|
// off everywhere, including for new code, until that decision is
|
|
// made. Unlike no-useless-assignment below, this is not an
|
|
// accommodation of particular sites and must not be scoped to
|
|
// them.
|
|
"preserve-caught-error": "off",
|
|
},
|
|
},
|
|
|
|
// no-useless-assignment stays on everywhere except the two files that
|
|
// wipe decrypted key material: the `password = null` and
|
|
// `decryptedSecret = null` assignments after use are dead by construction
|
|
// — that is what a best-effort wipe is — and the rule's fix is to delete
|
|
// the wipe. 9 sites: approval.js 582, 593, 618, 648, 692, 703, 728, 764
|
|
// and confirmTx.js 459. Everything else in the tree is still checked, so
|
|
// an ordinary dead store elsewhere is still an error.
|
|
{
|
|
files: ["src/popup/views/approval.js", "src/popup/views/confirmTx.js"],
|
|
rules: {
|
|
"no-useless-assignment": "off",
|
|
},
|
|
},
|
|
|
|
// Popup and content scripts: page/window context.
|
|
{
|
|
files: ["src/popup/**/*.js", "src/content/**/*.js"],
|
|
languageOptions: {
|
|
...commonjs,
|
|
globals: { ...globals.browser, ...extensionGlobals },
|
|
},
|
|
},
|
|
|
|
// MV3 background: a service worker, with no window and no document.
|
|
//
|
|
// It also may not reach src/shared/state.js. That module's `state` export
|
|
// is a per-bundle singleton loaded once and mutated in place, which is the
|
|
// popup's lifetime and not the worker's: the worker is killed when idle,
|
|
// nothing loads state at module scope, and an unpopulated read used to be
|
|
// served DEFAULT_STATE silently. Five defects came from background code
|
|
// reading or writing it (https://git.eeqj.de/sneak/AutistMask/issues/324),
|
|
// and each point fix added a loadState() that created the next one. The
|
|
// rule below checks reachability through the whole require graph, not just
|
|
// the direct require, because a re-export from any shared module the
|
|
// background already pulls in would put the singleton back in the bundle
|
|
// with no background file naming it.
|
|
//
|
|
// It is not the guarantee: build.js asserts the same prohibition against
|
|
// esbuild's own metafile, from the shared table in
|
|
// script/lib/forbiddenBundleInputs.js. This is the early report.
|
|
//
|
|
// The glob comes from that same file, because build.js uses the prefix to
|
|
// decide which entry points must be listed in the table at all: the two
|
|
// layers must not disagree about which files are "the background".
|
|
{
|
|
files: [`${BACKGROUND_ENTRY_PREFIX}**/*.js`],
|
|
plugins: { background: backgroundState },
|
|
languageOptions: {
|
|
...commonjs,
|
|
globals: { ...globals.serviceworker, ...extensionGlobals },
|
|
},
|
|
rules: {
|
|
"background/no-state-singleton-in-background": "error",
|
|
},
|
|
},
|
|
|
|
// src/shared is bundled into both, so it may only use what both provide:
|
|
// the service worker globals are the intersection, plus the extension APIs.
|
|
{
|
|
files: ["src/shared/**/*.js"],
|
|
languageOptions: {
|
|
...commonjs,
|
|
globals: { ...globals.serviceworker, ...extensionGlobals },
|
|
},
|
|
},
|
|
|
|
// src/shared/ens.js is the documented exception to the line above: its own
|
|
// header says POPUP ONLY, it caches in localStorage, and only popup views
|
|
// require it. Linting it as a service worker would be wrong about the file.
|
|
{
|
|
files: ["src/shared/ens.js"],
|
|
languageOptions: {
|
|
...commonjs,
|
|
globals: { ...globals.browser, ...extensionGlobals },
|
|
},
|
|
},
|
|
|
|
// Unit tests, and the helpers they require: jest on node.
|
|
{
|
|
files: ["tests/**/*.test.js", "tests/support/**/*.js"],
|
|
languageOptions: {
|
|
...commonjs,
|
|
globals: { ...globals.node, ...globals.jest },
|
|
},
|
|
},
|
|
|
|
// The build script is a plain node program.
|
|
{
|
|
files: ["build.js"],
|
|
languageOptions: {
|
|
...commonjs,
|
|
globals: { ...globals.node },
|
|
},
|
|
},
|
|
|
|
// The helpers the script/ entrypoints call: plain node programs too, run
|
|
// from a shell script rather than from yarn, and never bundled.
|
|
{
|
|
files: ["script/lib/**/*.js"],
|
|
languageOptions: {
|
|
...commonjs,
|
|
globals: { ...globals.node },
|
|
},
|
|
},
|
|
|
|
// The e2e harnesses are node programs that also carry, inline, the
|
|
// callbacks they ship into the browser via page.evaluate — so both
|
|
// contexts really are present in the same file and both sets of globals
|
|
// are in scope somewhere in it.
|
|
{
|
|
files: ["tests/e2e/**/*.js"],
|
|
languageOptions: {
|
|
...commonjs,
|
|
globals: {
|
|
...globals.node,
|
|
...globals.browser,
|
|
...extensionGlobals,
|
|
},
|
|
},
|
|
},
|
|
|
|
// This config file itself.
|
|
{
|
|
files: ["eslint.config.js"],
|
|
languageOptions: {
|
|
...commonjs,
|
|
globals: { ...globals.node },
|
|
},
|
|
},
|
|
];
|