Files
AutistMask/TODO.md
sneak 037c91da13
Some checks failed
e2e / e2e-chrome (push) Has been cancelled
e2e / e2e-firefox (push) Has been cancelled
check / check (push) Has been cancelled
build: run the browser e2e suites in CI (closes #259)
Nothing automatic ran either e2e suite, so every browser-level guarantee in this
wallet -- WebAssembly under the shipped CSP, the recovery-phrase and private-key
DOM wipes, the ConfirmTx spend gate, the dApp approval round trips -- held only
when a human or an agent remembered to run it by hand.

.gitea/workflows/e2e.yml adds two jobs, e2e-chrome and e2e-firefox, one per
browser so a Chrome failure cannot hide the Firefox result. They are separate
from the check workflow: REPO_POLICIES.md caps make test at 20 seconds and
script/cibuild is a docker build whose Dockerfile runs make check, so neither the
cap nor the local fast path is touched. make check is byte-for-byte unchanged.

Neither suite could run on the runner as it stood, and the reason is not
docker-in-docker. The runner executes a job inside a container against the HOST's
docker daemon, and the job's checkout lives on a docker volume rather than a host
path, so `docker run -v "$PWD:/work"` is resolved by the host, silently succeeds
and mounts an empty directory -- measured on this runner. The runner image's node
is also too old to install this repo's dependencies. Both suites therefore ship
the repo to the daemon as a build context and build the extension inside the
pinned image, which leaves docker as the only prerequisite on a runner or a
laptop. The suites themselves are unchanged; only how the repo reaches the
container is.

Both scripts now build with --iidfile and run the image by ID rather than by tag,
so two clones running a suite at once on the same host cannot swap it under each
other.

The jobs report, they do not gate. Whether a check blocks a merge is Gitea branch
protection, which this repo does not configure, so a failure is a red mark a
reviewer must account for. Nothing can pass vacuously: no continue-on-error, no
`|| true`, and both scripts exit non-zero when docker is missing, when the image
build fails and when the browser fails to start.
2026-08-14 04:12:23 +00:00

24 KiB
Raw Blame History

Workflow

  • git pull next and cut a branch from it — one branch per issue, named issue-<N>-<slug>. Never branch from main.
  • Do the work as one commit whose title ends with (closes #N), with the TODO.md update in that same commit.
  • Move Next Step to the top of Completed Steps; move the top item of Future Steps into Next Step.
  • Run make fmt, then make check. A feature branch may be red; next and main may not.
  • Rebase onto current next immediately before pushing — other branches land on next continuously — and re-run make check after resolving, because a clean textual merge can still break the build.
  • Push the branch and open one PR per issue with base next. Never base main.
  • An independent reviewer who did not write the change gates the merge. On a passed review the PR is squash-merged into next.
  • next is the branch for the next milestone. It is kept green and mergeable to main at any moment, without notice.
  • main receives exactly one PR per milestone, from next. Releases are tagged from main.

Status

pre-1.0, working towards the 1.0.0 milestone. Tagged v0.1.0 on 2026-02-27. The milestone is in flight on next; its next -> main PR is #190. make check verified green on next at e9fa8be on 2026-08-10, and make build produces dist/chrome/ and dist/firefox/ with every bundle verified to have DEBUG compiled off.

The backlog lives on the Gitea tracker, which is authoritative; this file does not duplicate it. Full policy file set present. Real-browser end-to-end suites (make test-e2e for Chrome, make test-e2e-firefox for Firefox) now sit alongside make check, which cannot see a runtime ReferenceError in a popup view, and .gitea/workflows/e2e.yml runs both of them on every push.

Next Step

Land #152: add ESLint to script/lint. make check is prettier --check only today and cannot catch undefined identifiers, which is how #150 and #151 shipped.

Completed Steps

  • 2026-08-14: CI runs the browser end-to-end suites. .gitea/workflows/e2e.yml runs script/test-e2e and script/test-e2e-firefox as two jobs on every push, separate from check, so make check and its 20-second make test cap are untouched. Every browser-level guarantee in this repo — the WASM-under-CSP check, the recovery-phrase and private-key DOM wipes, the ConfirmTx spend gate, the dApp approval round trips — was enforced only when a human remembered to run it by hand. The suites could not run on the runner as they stood: the runner executes a job in a container against the host's docker daemon, so docker run -v "$PWD:/work" mounts an empty directory (measured), and the runner image's node cannot install this repo's dependencies. Both suites now ship the repo to the daemon as a build context and build the extension inside the pinned image, so docker is the only prerequisite on a runner or a laptop, and both run the image by ID rather than by tag so concurrent clones cannot swap it. The jobs report rather than gate — this repo configures no branch protection — and a deliberately broken assertion was pushed to a scratch branch to prove the job goes red (#259).
  • 2026-08-12: EIP-1193 error codes now reach the page. src/content/inpage.js rebuilt every failure as new Error(error.message), so the code the background produced and the content script relayed intact was dropped in the last hop and a dApp checking err.code === 4001 saw undefined — a wallet the user deliberately declined was indistinguishable from one that broke. The provider now rejects with a ProviderRpcError carrying code and, where the boundary sent one, data, passed through verbatim rather than matched against a list, so 4001, 4100 and 4902 all arrive and a future code needs no edit here. An error the background sent with no code stays a plain Error with no code property, and message is unchanged in every case. All four request entry points (request, enable, send, sendAsync) are covered by tests/inpageErrors.test.js, and the e2e probe that printed the missing code now requires it on the page's Error as well as on the wire, for all four rejected flows (#274).
  • 2026-08-12: KNOWN_SYMBOLS now maps a symbol to the set of contract addresses that bear it, not to one of them. A ticker is not unique: seven of the 512 bundled tokens — FRAX, REUSD, TON, EURE, MSUSD, MUSD and JPYC — share a symbol with another bundled entry at a different real contract, and the table, built from the list first-wins, kept only the earlier one. The other seven were judged spoofs of their own symbol at their own address and hidden from the balance list, the history and the send selector, so a holder could not spend them. Both contracts of each pair come from the same CoinGecko fetch of 2026-02-27, so neither was stale and neither was dropped. isSpoofedSymbol() asks set membership instead of equality, which does not loosen the rule — a contract outside the set is still a spoof — and a test now walks TOKENS asserting no bundled token is filtered at its own address, which is the walk the suite lacked (#276).
  • 2026-08-12: The dApp approval round trips are driven end to end in the browser. A test page served by the harness speaks EIP-1193 to the real inpage provider through the real content script, background worker and approval popup for eth_requestAccounts, personal_sign, eth_signTypedData_v4 and eth_sendTransaction. Every signature is recovered and compared against the active address, the transaction is checked against the bytes handed to the stubbed RPC, each rejection must reach the page as a rejection, and the password must appear in no message the approval window sends — the assertion that gives #157 a permanent floor. This does not discharge a real dApp with real funds against mainnet (#183).
  • 2026-08-12: The known-symbol spoof rule now judges the symbol a user actually sees. isSpoofedSymbol() normalizes before the lookup — NFKC, then every character that paints nothing removed (the format and default-ignorable characters, plus U+007F), then trimmed — so " ETH ", a no-break space, a zero-width space, a Hangul filler, a variation selector, a DELETE and a fullwidth are all caught on the balance list, the history and the send selector at once. Confusables that are distinct letters (Cyrillic Е), bidi reordering and the visible C0/C1 controls — which measure 48.00px, a box, in the pinned e2e Chromium where an invisible prefix measures 32.00px — stay knowingly open and are asserted as open in the suite. No bundled symbol contains whitespace or a non-ASCII character, so nothing legitimate is newly filtered; the balance list's token-type gate also became case-insensitive, which no longer drops a real holding if an explorer writes erc-20 (#260).
  • 2026-08-12: A containerized Firefox end-to-end harness (make test-e2e-firefox) drives the real popup in a real Firefox with the MV2 build installed as a temporary add-on. Zero npm dependencies — a WebDriver client over fetch against geckodriver — with node, Firefox 153.0.3 and geckodriver 0.36.0 all pinned by digest. Uncaught errors are read from the privileged console service in Marionette's chrome context, because BiDi log.entryAdded reports nothing at all for extension pages; each drain reads and clears the console in one chrome round trip, so no error is destroyed unread by the drain itself, and errors logged during add-on install and background startup are folded into step 1 instead of being cleared. The two measured limits are documented rather than claimed away: the console ring buffer holds 250 messages (a clean run peaks at 4), and the drained window ends ≈1.5s after the last step returns. Demonstrated discriminating by exiting 1 on a throw at the top of src/background/index.js, on a build with one import removed, on a setTimeout throw whose UI assertions all pass, on an unhandled Promise.reject and on an undefined identifier in home.js, and 0 on the branch as it stands (#184).
  • 2026-08-12: The transaction a dApp asks for is now populated in the background before the approval window opens, so the object the user is shown is the object the signed artifact is verified against — nonce, gas limit and every fee field are compared exactly instead of being left to the ceilings, which stay as a backstop against what a lying RPC node can talk the wallet into displaying. The approval also pins the address it was raised for, so an address switch between approval and signing refuses rather than signing from an account the screen never named, and a request naming an address that is not the active one is refused outright. The approval screen now shows the fee, gas limit, network and nonce it vouches for (#216).
  • 2026-08-12: The restored navigation stack is filtered against RESTORABLE_VIEWS on load, truncated at the first entry the popup would not render so that every surviving entry keeps the Back target it had. Back after reopening can no longer land on a view the popup declined to restore, such as export-privkey or show-phrase (#224). Restorable views in the stack are still unhidden without being re-rendered; that is tracked separately in (#268).
  • 2026-08-12: One wording for a rejected password on every screen that asks for one — the send confirmation and the delete-wallet confirmation no longer say "Wrong password." (a fragment, which RULES.md Language & Labeling forbids) and the two reveal screens no longer say "not correct", so all five decryptWithPassword call sites now show the sentence the dApp approval paths introduced. Strings only, no behaviour change, and each error container measured at a 360px viewport in the pinned Playwright container (#172).
  • 2026-08-12: Closed the empty-array hole in the end-to-end unstubbed-request guard. batch.every() is vacuously true on [], so a POST with body [] was answered 200 [] instead of failing the suite; the guard now rejects an empty batch, demonstrated green-before/red-after with a throwaway probe. The comment claiming postData() returns null for undecodable bodies was corrected to the two real paths — an absent or empty body decodes to null, a binary body decodes lossily into invalid JSON (#187).
  • 2026-08-12: The transaction confirmation screen has browser coverage. The end-to-end suite reaches ConfirmTx for both the native ETH and the ERC-20 path off a funded-balance fixture, and asserts the pending, funded, over-balance and estimate-failed states, the fee block quoting the estimate and the reserve separately, and a constant view height across every one of those transitions. The load-bearing assertion is that the spend gate reads the reserve and not the displayed estimate: swapping the two fails the suite (#238).
  • 2026-08-12: The dust threshold field now explains a rejection instead of snapping back in silence, with the parse in a pure, unit-tested module that accepts plain decimal digits only — hex and exponent notation are refused rather than read as 16 and 1000 (#233).
  • 2026-08-12: Approval verification became an allowlist — transaction type restricted to 0/1/2 so an EIP-7702 delegation can no longer ride along on an approved transfer, every consequential field compared, the artifact re-serialized from the checked fields alone and its exact bytes required to be the canonical encoding of what was broadcast. One approval now yields at most one broadcast, and every path that retires a pending approval — popup close, active-address change, a late reject — goes through a single chokepoint that refuses to settle an attempt already claimed for signing and broadcast (#174).
  • 2026-08-12: An address can be removed from an HD or xprv wallet behind a confirmation screen that states nothing is destroyed, sharing the deletion state transitions with wallet deletion so the selection, site permissions and active-address broadcast follow the same rules (#162).
  • 2026-08-12: The known-symbol spoof rule moved into src/shared/symbolSpoof.js and is now the only copy. The balance list had exempted symbols the token list maps to null"ETH" alone — so a fake ETH ERC-20 was hidden from the transaction history and the Send selector but listed as a holding named ETH. A symbol with no legitimate contract may now be borne by no contract on any of the three surfaces, and the native exemption is "has no contract address", so a second null-mapped symbol needs no call-site change. The user's real ETH balance is read over RPC and never passes through the rule (#235).
  • 2026-08-12: script/verify-build's failure modes are now a committed target, script/test-verify-build, run by make check. It asserts the exit status and the message of every case against a fixture tree in a temp dir, and drops privileges (proving the runner against a mode-000 file first) for the cases that only mean something when file permissions are in force (#227).
  • 2026-08-12: WaitTx lifecycle: a receipt and the 60-second timeout can no longer both render on one tick, no timer or in-flight lookup outlives its wait, a failed receipt lookup no longer counts as a timeout (but six in a row end the wait, reported as an unreachable network rather than as a timeout), and the wait now resumes after a popup close (#155).
  • 2026-08-12: The private key export screen now wipes the key from the page whenever it is left by any route, and a decrypt still in flight when the screen is left is discarded instead of written; the same onViewLeave() cleanup was extended to every other screen holding secret material in the DOM (AddWallet, ConfirmTx, DeleteWallet, ApproveTx, ApproveSign) (#221).
  • 2026-08-12: An xprv wallet already in storage that was imported from a non-master key is detected from the depth of its stored xpub, explained in the wallet list, and blocked from signing, sending and private-key export instead of throwing on the send screen (#234).
  • 2026-08-12: An unreported holders_count is now parsed as null rather than 0, so the low-holder rule declines to judge an unknown count instead of hiding a legitimate token as spam, in both the transaction history and the Send token selector (#230).
  • 2026-08-12: Bundled token list documentation no longer states a count. The four "top 250" claims in README.md and the "roughly 500" claim in docs/README.md are replaced with a description of how the list is actually selected — a point-in-time CoinGecko snapshot of the highest-market-cap Ethereum mainnet ERC-20s — with TOKENS in src/shared/tokenList.js named as the authoritative set (#239).
  • 2026-08-11: libsodium runs on WebAssembly in the shipped builds — 'wasm-unsafe-eval' added to both manifest CSPs after measuring the wasm2js fallback at 20x the Argon2id cost, pinned in both directions by tests/manifest.test.js and observed in the real popup by the e2e suite (#182).
  • 2026-08-11: Known-symbol spoof verification became a Settings toggle (hideSpoofedSymbols), on by default, governing the transaction-history filter and the fraud-contract learning it feeds (#176).
  • 2026-08-11: script/verify-build now walks dist/ NUL-delimited and asserts dist/ is a real directory, so a path with a trailing space or a newline can no longer carry a debug marker past the unlisted-bundle check (#223).
  • 2026-08-11: UTC Timestamps checkbox moved from the Token Spam Protection well into Display, next to the theme selector (#212).
  • 2026-08-11: Network fee counted in the confirmation-screen balance check for both ETH and ERC-20 sends, reserving what the node actually charges a type-2 transaction, with the arithmetic in a pure, unit-tested src/shared/txValidation.js (#154).
  • 2026-08-11: A dust threshold of 0 now means "hide nothing" instead of falling back to the 100,000 gwei default, and every address comparison in src/shared/transactions.js goes through one case-normalising helper so a checksummed genuine contract is no longer read as a spoof (#179).
  • 2026-08-11: Password-gated recovery phrase display for HD wallets, reached from the wallet row in Settings, wiped on leaving the screen and excluded from the views the popup can reopen onto (#161).
  • 2026-08-11: Extended-key import hardened — the base58 checksum is now enforced on every xprv and xpub, and a non-master key is refused with an explanation instead of being derived beneath (#210).
  • 2026-08-11: the balance refresh and the 24-hour phishing list refresh moved from setInterval to the extension alarms API, with the phishing delta and its fetch timestamps persisted to extension storage, so neither job dies with the MV3 service worker. Each job's freshness guard was decoupled from its alarm period at the same time — timed to the period, a guard vetoes its own scheduled tick and halves the real refresh rate (#158).
  • 2026-08-11: Policy compliance sweep — conditional verbose test rerun, local Tailwind binary instead of npx, --frozen-lockfile on make install, and the Makefile-only targets documented in the README (#166).
  • 2026-08-11: script/verify-build diagnostics corrected: the both-markers message now states what is and is not proven, an unreadable bundle is diagnosed as an I/O fault rather than as changed output, the *.js assumption lives only in build.js, and the unlisted-bundle scan hard-fails when it cannot enumerate dist/ (#180).
  • 2026-08-11: Known-answer test coverage for the crypto core — BIP-39/BIP-32 derivation in wallet.js and the Argon2id vault in vault.js (#159).
  • 2026-08-11: Three README.md claims corrected against the code — blocklist attribution, token-display rule, navigation model (#213).
  • 2026-08-11: README Screen Map rebuilt from the code — every screen, element and transition re-verified against src/popup/ (#164).
  • 2026-08-11: docs/README.md rewritten against the code: no competitor names, all five network destinations documented, password/Settings/Add Wallet sections corrected (#163).
  • 2026-08-11: loadState() now derives hasWallet from the wallet list instead of trusting the persisted flag, so a profile already saved inconsistent no longer stays broken on every load (#195).
  • 2026-08-11: Wallet deletion repairs its own state — hasWallet follows the remaining wallets, the selection only moves when it was deleted, and the active-address change is broadcast to connected sites (#156).
  • 2026-08-11: One row per on-chain value movement in transaction history: the merge moved into the pure mergeTransactions and the zero-ETH native side of a plain ERC-20 transfer absorbed into its token row (#177).
  • 2026-08-11: TODO.md Workflow rewritten to the branch-and-PR-per-issue model on next, with Status and Next Step refreshed (#191).
  • 2026-08-09: DEBUG became a build-time constant defaulting to off, injected as the __BUILD_DEBUG__ esbuild define and turned on with AUTISTMASK_DEBUG=1, so a plain make build no longer hands every newly created wallet the publicly committed test recovery phrase (#149).
  • 2026-08-09: dApp approval signing moved into the popup — the password no longer crosses the extension messaging boundary; the background broadcasts and resolves approvals only, and verifies the signed artifact against the approval it holds (#157).
  • 2026-08-09: Post-build assertion that every emitted bundle containing constants.js has DEBUG compiled off, via script/verify-build on the make build path (#170).
  • 2026-08-09: Containerized Chrome end-to-end harness (make test-e2e / script/test-e2e) driving the real popup with all network intercepted, plus the two used-but-not-imported crashes it caught: AddToken unreachable (#150) and TransactionDetail broken for every ERC-20 transfer (#151). Harness demonstrated failing before the fixes and passing after (#181). Interception covers the MV3 background service worker, not just the popup page, and a launch-time canary aborts the suite if worker traffic starts escaping.
  • 2026-08-09: Reviewed the repo end to end and filed the 1.0.0 backlog (#149-#168).
  • 2026-08-09: Test coverage for the address-poisoning defense in src/shared/transactions.js (#160)
  • 2026-07-26: About well in settings with build info, repo link and the version click easter egg (#145); proper view navigation stack (#146).
  • 2026-07-07 Adopted scripts-to-rule-them-all: script/ entrypoints, Makefile shims, README Entrypoints section (#148)
  • 2026-03-01: USD display suppressed on testnets (#142); estimated USD for ETH in approve-tx view (#141).
  • Sepolia testnet support (#137); etherscan links go to token-specific URLs (#136).
  • Transaction detail improvements: Type field and on-chain details (#130), txid-first reordering (#133), swap display corrections (#128), expanded confirm-tx warnings (#118).
  • Dark mode theme setting (Light/Dark/System) with contrast fixes (#126); timestamps include timezone offset (#120); layout shift audit, reserved space for error messages (#124).
  • Copy-flash visual feedback with timing tune (#113, #121); cross-wallet-type duplicate detection (#115).
  • 2026-02-27: v0.1.0 tagged.
  • 2026-02-24: Initial scaffolding: popup UI, BIP-39 wallet creation via ethers.js, wallet persistence, real ETH balances over RPC, ENS forward and reverse resolution.

Future Steps

Only work that has no issue of its own belongs here; everything else is on the tracker.

  • Pre-1.0 security review of the extension (key handling, DEBUG mode policy, RPC input validation) before any 1.0rc tag. Individual filed issues are parts of it, but the review is broader than any of them.
  • Cut 1.0.0 once the milestone is empty, then continue tagging as milestones land.