# 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](https://git.eeqj.de/sneak/AutistMask/pulls/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](https://git.eeqj.de/sneak/AutistMask/issues), 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.

# Next Step

Land [#152](https://git.eeqj.de/sneak/AutistMask/issues/152): add ESLint to
`script/lint`. `make check` is `prettier --check` only today and cannot catch
undefined identifiers, which is how
[#150](https://git.eeqj.de/sneak/AutistMask/issues/150) and
[#151](https://git.eeqj.de/sneak/AutistMask/issues/151) shipped.

# Completed Steps

- 2026-08-14: Approving a site connection is no longer a race against the popup
  closing. The decision now rides the approval port the popup already holds,
  which is the same channel the close disconnects, so it is delivered ahead of
  that disconnect however fast the teardown is; `windows.onRemoved` no longer
  decides a site approval whose port is connected, since that event is ordered
  against nothing either. Rejecting and closing without deciding both still
  report a rejection, and the popup delays its own close by nothing. The e2e
  harness's deferred-`window.close()` accommodation is gone with it, so the two
  site-prompt tests now drive the shipped decide-then-close in a real Chromium;
  against the unfixed code the approval came back to the page as
  `{"settled":"rejected","code":4001}`
  ([#275](https://git.eeqj.de/sneak/AutistMask/issues/275)).
- 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](https://git.eeqj.de/sneak/AutistMask/issues/274)).
- 2026-08-12: "Back" now renders the screen it lands on instead of only unhiding
  it. A reopened popup renders the wallet list and the one screen it restores
  onto, so every screen further down the stack was still the blank template from
  `index.html`, and Back walked straight onto it — an empty address, no
  balances, no QR code. The Back path now goes through the same per-view
  dispatch and data guards as the restore (`src/popup/viewRouter.js`, shared
  with `restoreView()`), falling back to Home when the state the target would
  render is gone. It renders only a view this page load has not rendered yet:
  `viewRouter.js` records every view that reaches `showView()`, which is where
  forward navigation and the restore both end, so Back onto a view already on
  the page unhides it and nothing more. That is what keeps a second render from
  re-fetching and overwriting what the view holds — an unsaved edit in Settings,
  a transaction list already loaded. Home is the exception and is always
  re-rendered, as it was before. Covered by unit tests on the real `goBack()`
  and by three end-to-end cases against the real popup, each demonstrated
  failing on the unfixed build
  ([#268](https://git.eeqj.de/sneak/AutistMask/issues/268)).
- 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](https://git.eeqj.de/sneak/AutistMask/issues/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](https://git.eeqj.de/sneak/AutistMask/issues/157) a permanent
  floor. This does not discharge a real dApp with real funds against mainnet
  ([#183](https://git.eeqj.de/sneak/AutistMask/issues/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](https://git.eeqj.de/sneak/AutistMask/issues/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](https://git.eeqj.de/sneak/AutistMask/issues/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](https://git.eeqj.de/sneak/AutistMask/issues/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](https://git.eeqj.de/sneak/AutistMask/issues/224)). Restorable views in
  the stack are still unhidden without being re-rendered; that is tracked
  separately in ([#268](https://git.eeqj.de/sneak/AutistMask/issues/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](https://git.eeqj.de/sneak/AutistMask/issues/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](https://git.eeqj.de/sneak/AutistMask/issues/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](https://git.eeqj.de/sneak/AutistMask/issues/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](https://git.eeqj.de/sneak/AutistMask/issues/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](https://git.eeqj.de/sneak/AutistMask/issues/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](https://git.eeqj.de/sneak/AutistMask/issues/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](https://git.eeqj.de/sneak/AutistMask/issues/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](https://git.eeqj.de/sneak/AutistMask/issues/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](https://git.eeqj.de/sneak/AutistMask/issues/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](https://git.eeqj.de/sneak/AutistMask/issues/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](https://git.eeqj.de/sneak/AutistMask/issues/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](https://git.eeqj.de/sneak/AutistMask/issues/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](https://git.eeqj.de/sneak/AutistMask/issues/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](https://git.eeqj.de/sneak/AutistMask/issues/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](https://git.eeqj.de/sneak/AutistMask/issues/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](https://git.eeqj.de/sneak/AutistMask/issues/223)).
- 2026-08-11: UTC Timestamps checkbox moved from the Token Spam Protection well
  into Display, next to the theme selector
  ([#212](https://git.eeqj.de/sneak/AutistMask/issues/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](https://git.eeqj.de/sneak/AutistMask/issues/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](https://git.eeqj.de/sneak/AutistMask/issues/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](https://git.eeqj.de/sneak/AutistMask/issues/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](https://git.eeqj.de/sneak/AutistMask/issues/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](https://git.eeqj.de/sneak/AutistMask/issues/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](https://git.eeqj.de/sneak/AutistMask/issues/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](https://git.eeqj.de/sneak/AutistMask/issues/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](https://git.eeqj.de/sneak/AutistMask/issues/159)).
- 2026-08-11: Three `README.md` claims corrected against the code — blocklist
  attribution, token-display rule, navigation model
  ([#213](https://git.eeqj.de/sneak/AutistMask/issues/213)).
- 2026-08-11: README Screen Map rebuilt from the code — every screen, element
  and transition re-verified against `src/popup/`
  ([#164](https://git.eeqj.de/sneak/AutistMask/issues/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](https://git.eeqj.de/sneak/AutistMask/issues/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](https://git.eeqj.de/sneak/AutistMask/issues/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](https://git.eeqj.de/sneak/AutistMask/issues/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](https://git.eeqj.de/sneak/AutistMask/issues/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](https://git.eeqj.de/sneak/AutistMask/issues/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](https://git.eeqj.de/sneak/AutistMask/issues/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.
- Decide whether docker-in-docker makes `make test-e2e` and
  `make test-e2e-firefox` runnable in the Gitea workflow. Extending the Chrome
  suite itself is tracked as
  [#183](https://git.eeqj.de/sneak/AutistMask/issues/183).
- Cut 1.0.0 once the milestone is empty, then continue tagging as milestones
  land.
