From 02f14143eb840811d7fc17349d66faccede9bf2a Mon Sep 17 00:00:00 2001 From: clawbot <35+clawbot@noreply.example.org> Date: Wed, 11 Nov 2026 02:04:13 +0100 Subject: [PATCH] docs: TODO.md holds open work only (closes #769) Removes the Completed Steps work log and the v0.1.0 tagging date from Status. Every open item the log mentioned is a tracker issue. The Workflow bullet that named the removed section now says to record no finished work in this file. Model: opus-5-5 --- TODO.md | 1939 +------------------------------------------------------ 1 file changed, 4 insertions(+), 1935 deletions(-) diff --git a/TODO.md b/TODO.md index f668cc4..aa309d8 100644 --- a/TODO.md +++ b/TODO.md @@ -3,8 +3,8 @@ - `git pull` `next` and cut a branch from it — one branch per issue, named `issue--`. Never branch from `main`. - Do the work as one commit whose title ends with ` (closes #N)`. -- Add no entry to Completed Steps. What landed is recorded on the tracker and in - `git log`, where each commit title names the issue it closes. +- Record no finished work in this file. What landed is recorded on the tracker + and in `git log`, where each commit title names the issue it closes. - 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 @@ -20,8 +20,8 @@ # 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 +pre-1.0, working towards the 1.0.0 milestone. The milestone is in flight on +`next`; its `next` -> `main` PR is [#388](https://git.eeqj.de/sneak/AutistMask/pulls/388). `make build` produces `dist/chrome/` and `dist/firefox/` with `DEBUG` compiled off, and checks them against the build's own receipt to hold exactly the regular files and symlinks @@ -41,1937 +41,6 @@ Cut 1.0.0 once the [1.0.0 milestone](https://git.eeqj.de/sneak/AutistMask/milestone/6) is empty, then continue tagging as milestones land. -# Completed Steps - -- 2026-10-08: The browser suites no longer read a screen before the page has - shown it ([#502](https://git.eeqj.de/sneak/AutistMask/issues/502)). Their wait - for a screen used to pass as soon as the element laid out, which every view - does until the page's stylesheet has applied, so an approval test could read - the prompt's fields before the page's script had filled them. `visible()` in - `tests/e2e/harness.js` and `waitVisible()` in `tests/e2e/firefox/driver.js` - now also wait for the page to finish loading and for neither the element nor - anything around it to carry the `hidden` class that `showView()` puts on every - view but the current one. No caller changed. - -- 2026-10-08: Switching the network in Settings now tells open pages - ([#500](https://git.eeqj.de/sneak/AutistMask/issues/500)). Once the switch is - saved, Settings asks the background to send `chainChanged` with the new chain - id to every open tab, through the same function an approved site request uses. - Choosing the network already active changes nothing and sends nothing. Only - the extension's own pages can ask for this. `tests/chainSwitchGate.test.js` - drives the real Settings view against the background, and the Chrome suite - checks that the test page hears both switches. - -- 2026-10-07: A site can no longer switch the wallet's network by itself - ([#408](https://git.eeqj.de/sneak/AutistMask/issues/408)). A connected site's - `wallet_switchEthereumChain` request for the other supported network opens a - prompt in its own window, through the same approval machinery as the - transaction and signature prompts, naming the site, the current network and - the requested one. The network, its endpoints, the balances and the caches - change, and `chainChanged` is sent, only when the user approves it; rejecting - or closing the prompt answers 4001. One such prompt per site at a time. The - approval window no longer shows the connection prompt while it waits for the - background to describe the approval, because that prompt's "Allow" answers on - the same port as the new one; `tests/approvalWindow.test.js` checks that it - shows no screen until then. `tests/chainSwitchGate.test.js` and both browser - suites drive the prompt. - -- 2026-10-07: Two contradictions between the documents and the code are resolved - as ruled on [#165](https://git.eeqj.de/sneak/AutistMask/issues/165). The - README's 1.0 non-goal now puts Ethereum mainnet and the Sepolia testnet in - scope and other chains out of it. The injected provider's flag named after the - other wallet stays, as an interface-compatibility flag many dApps check, and - is written down as an exception to the rule against naming competitors in the - README's Policies section, in `RULES.md` and in a comment beside it in - `src/content/inpage.js`. `script/check-censored` already allows that flag only - in that file and its built copies, so it is unchanged. - -- 2026-10-07: Two holes in what `tests/persistedFieldContract.test.js` checks - are closed ([#379](https://git.eeqj.de/sneak/AutistMask/issues/379)). The - check that every swept field is driven both truthy and falsy counts only - `hostile` and `falsy` values, which the sweep drives onto every restorable - view, and no longer a `hostileRestore` value, which reaches only the views its - entry names; the stale index 5 moves into `hostile` for `selectedWallet` and - `selectedAddress`, as the one value of either still truthy after the floor. - Each `hostileRestore` entry declares whether its boot lands on its view or - falls back to Home, and is held to it. The limits that remain, fields that - share a slot on one boot and anything no stored record reaches by itself, are - restated as built in the file's header and the README, which now also say - which boots are held to where the popup lands and that a healthy profile is - booted onto every restorable view. - -- 2026-10-07: Every password error in the popup is shown the same way - ([#493](https://git.eeqj.de/sneak/AutistMask/issues/493)): with `showError()` - and `hideError()` in a fixed-height error line below the password field, as - the send confirmation and approval screens already did. The add wallet screen - showed a missing, short or mismatched password in the flash line, and the - private key export, recovery phrase and delete wallet screens each had a line - of their own above the field. On the add wallet screen the line sits beside - the Import button, so the button stays where it was at 360x600. Each line - clears when the screen is shown again and when the password is tried again. - The add wallet screen's other messages, such as an invalid recovery phrase, a - duplicate wallet and the address scan, stay in the flash line. - `tests/passwordErrorLines.test.js` drives all four screens in the popup. - -- 2026-10-07: Pre-1.0 security review of the extension - ([#383](https://git.eeqj.de/sneak/AutistMask/issues/383)), reading the tree at - `99292b9` for key handling, the DEBUG mode policy, and what the background - accepts from pages, the configured RPC endpoint and the explorer, with what - the approval screens show from it; the site permission model and storage were - read as well. Its summary on that issue lists ten findings, each filed as its - own issue, and all ten are fixed on `next`; one, - [#399](https://git.eeqj.de/sneak/AutistMask/issues/399), put funds at risk. - Three decisions it raised are still open with the owner: the Argon2id cost for - the vault key ([#401](https://git.eeqj.de/sneak/AutistMask/issues/401)), a - connected site switching the network with no prompt - ([#408](https://git.eeqj.de/sneak/AutistMask/issues/408)), and `eth_sign` - signing as a personal message - ([#409](https://git.eeqj.de/sneak/AutistMask/issues/409)). Not covered: the - end-to-end suites were not run, the bundled phishing blocklist and token list - were not checked entry by entry, `ethers` and `libsodium-wrappers-sumo` were - taken as audited, and nothing was tried against a real network with real funds - ([#385](https://git.eeqj.de/sneak/AutistMask/issues/385)). The planned - independent second check of each finding did not run; the findings rest on the - reviewer's own reading of the code. - -- 2026-10-07: Stale branches pruned from `origin` - ([#167](https://git.eeqj.de/sneak/AutistMask/issues/167)). The issue - classifies each branch it lists, with the evidence. The eighteen still on - `origin` that it classifies as landed, or superseded by merged pull requests, - were deleted. `feat/message-signing` and `fix/59-transaction-view-ui-policies` - had been deleted on 2026-09-09; both landed and stay deleted. Four are kept - because their work is not in `next`: `fix/consistent-error-display`, - `fix/87-consistent-error-display` and `fix/87-consistent-error-display-v2`, - the change for [#87](https://git.eeqj.de/sneak/AutistMask/issues/87) that - never landed, as reference for - [#493](https://git.eeqj.de/sneak/AutistMask/issues/493); and - `chore/token-list-enrichment`, deleted on 2026-09-09 and re-created at - `f7a2437`, whose `scripts/` tooling waits on - [#495](https://git.eeqj.de/sneak/AutistMask/issues/495). - -- 2026-10-07: The README says that the wallet never clears the clipboard after - the private key or the recovery phrase is copied, and why - ([#492](https://git.eeqj.de/sneak/AutistMask/issues/492)): the clipboard is - the user's, clearing it would go against what they expect, and it could - destroy something else they copied since. It is under ExportPrivKey, with a - line under ShowRecoveryPhrase, and names the warning each password screen - actually shows, which says nothing about the clipboard. - -- 2026-10-07: The Firefox end-to-end suite no longer tolerates any uncaught - extension error ([#487](https://git.eeqj.de/sneak/AutistMask/issues/487)). Its - one entry, Firefox reporting a popup promise that settled after the page - unloaded, had lost its cause with - [#275](https://git.eeqj.de/sneak/AutistMask/issues/275); the entry and the - code that printed tolerated errors are gone from `tests/e2e/firefox/run.js`, - and so is the README paragraph that described it. - -- 2026-10-07: The toolbar icons in `icons/` can be redrawn in the tree - ([#378](https://git.eeqj.de/sneak/AutistMask/issues/378)): `make icons` runs - `script/lib/icons.js`, which draws the mark and writes each PNG with node's - own `zlib`, no new dependency, leaving alone a file that already holds the - drawn image. `tests/icons.test.js` requires each committed file to hold - exactly that image, the same IHDR and every pixel. The compressed bytes are - not compared, because node's bundled zlib does not compress them as the stock - zlib that made the committed files did. `build.js` now copies each manifest - from the same path `copyIcons()` reads its icons from. - -- 2026-10-07: The README's end-to-end limits now match what the two browser - suites do to the extension - ([#293](https://git.eeqj.de/sneak/AutistMask/issues/293)). The `window.close` - override the issue named went with - [#275](https://git.eeqj.de/sneak/AutistMask/issues/275), so it needs no entry. - Added: both suites load the popup in a tab rather than from the toolbar; - Chrome waits 1.5 seconds before each site-connection request, records what - approval windows send and click, grants clipboard permission, writes text - straight into the page in two layout tests, and forces the leave during a - decrypt; Firefox forces the site-connection prompt into a window. Corrected: - Firefox's one tolerated error, whose cause that fix removed (the entry goes in - [#487](https://git.eeqj.de/sneak/AutistMask/issues/487)), the phishing - blocklist the extension no longer fetches, and two stale figures. - -- 2026-10-07: Back from Settings no longer shows Settings again after the - settings gear was pressed on the recovery phrase or a delete wallet screen - opened from Settings, with or without a reopen in between - ([#481](https://git.eeqj.de/sneak/AutistMask/issues/481)). Those screens take - themselves off the Back stack when left, which leaves Settings under Settings; - `goBack()` now skips an entry for the screen already showing. - `tests/showPhrase.test.js`, `tests/deleteWalletLostPassword.test.js` and - `tests/backNavigation.test.js` drive each path. - -- 2026-10-07: `script/discard-dist-on-failure` returns the failed step's own - exit status even when it cannot write its message, to a closed stderr or to a - pipe nobody reads any more - ([#342](https://git.eeqj.de/sneak/AutistMask/issues/342)); it used to return 2 - or 141 instead. An interrupt while a step runs now removes nothing and says - nothing whichever shell `/bin/sh` is, even when the step catches it and exits - with a status of its own, as `script/check-censored` does: the wrapper exits - with 130 once the step has ended. Under bash such a step used to have `dist/` - removed. A failed `check-censored --require-dist` still removes `dist/` like - any other step, because a `dist/` not cleared of the name `RULES.md` bars must - not ship. The header states both. `script/test-verify-build` runs the wrapper - with stderr closed and with stderr a pipe nobody reads, and interrupts it - under dash and under bash. - -- 2026-10-06: Back from Settings no longer lands on the delete wallet or - lost-password screen after either was left by the settings gear - ([#480](https://git.eeqj.de/sneak/AutistMask/issues/480)), the defect - [#461](https://git.eeqj.de/sneak/AutistMask/issues/461) fixed for the two - secret screens. Leaving drops the screen's wallet selection, so its leave - handler now also takes it off the Back stack, as a reopened popup already - does. `tests/deleteWalletLostPassword.test.js` drives the gear and then Back - on both screens. - -- 2026-10-06: `script/bootstrap` no longer reports success while node cannot - find a package listed in `dependencies` or `devDependencies` of `package.json` - ([#263](https://git.eeqj.de/sneak/AutistMask/issues/263)). After the install - it asks node for each one's `package.json`, and fails naming the missing - package and the fix. yarn skips the install whenever - `node_modules/.yarn-integrity` matches `yarn.lock`, so a package deleted from - `node_modules` stayed deleted while bootstrap said it was complete. The - fresh-clone failure the issue reports did not reproduce. - -- 2026-10-06: Back from Settings no longer lands on the private key export or - recovery phrase screen after either was left by the settings gear - ([#461](https://git.eeqj.de/sneak/AutistMask/issues/461)). Leaving drops the - screen's selection, so its leave handler now also takes it off the Back stack, - as a reopened popup already does. `tests/exportPrivkey.test.js` and - `tests/showPhrase.test.js` drive the gear and then Back, and - `leavePrivkeyScreen()` in `tests/e2e/run.js` expects the address screen after - Settings. - -- 2026-10-06: A token symbol or name read off a contract is no longer stored cut - between the two halves of an emoji - ([#458](https://git.eeqj.de/sneak/AutistMask/issues/458)). `lookupTokenInfo()` - cut both by UTF-16 units; it now counts code points, as `displaySymbol()` has - since [#329](https://git.eeqj.de/sneak/AutistMask/issues/329). - `tests/tokenLookupTruncation.test.js` looks up a token whose symbol and name - are made of emoji outside the Basic Multilingual Plane. A symbol already - stored broken is not repaired. - -- 2026-10-06: Reloading or closing the popup no longer logs a request it cancels - as a failure in the transaction lists and ENS name lookups on the address and - token screens, the address scan after a wallet is created, the endpoint checks - in Settings, the wait screen's receipt check, the Send screen's Max fee - estimate, or the token lookup on the two add-token screens - ([#475](https://git.eeqj.de/sneak/AutistMask/issues/475)). Each checks the - signal the popup aborts on `pagehide` - ([#218](https://git.eeqj.de/sneak/AutistMask/issues/218)) before reporting a - failure; a real failure is still logged. `scanForAddresses()`, - `resolveEnsNames()` and `lookupTokenInfo()` take the signal. The e2e suite - reloads the popup on the address screen with Blockscout held, and during the - address scan with that scan held; jest tests cover each of these with the - popup open and closed. Left out: the transaction detail screen and the - confirmation screen (its fee estimate and its recipient checks), because they - discard the popup context that carries the signal. - -- 2026-10-06: The private key export screen opens again in the same popup - session ([#460](https://git.eeqj.de/sneak/AutistMask/issues/460)). `show()` - found the address line through the element inside it, which its own rendering - replaced, so the second open threw before it navigated. The line now carries - the `export-privkey-address` id itself. `tests/exportPrivkey.test.js` opens - the screen twice, its DOM stub now takes an element out of the document when - its parent's contents are replaced, and the `#253` e2e case no longer reopens - the popup before its second open. - -- 2026-10-06: The canonical files are re-vendored from `sneak/prompts` at - `dd4027b` ([#472](https://git.eeqj.de/sneak/AutistMask/issues/472)). The - `Dockerfile` has separate `lint` and `test` phases, and its last stage depends - on both before it runs `make build`. `script/lint` and `script/test` each - build one phase, uncached. `script/check-censored` runs in the `lint` phase - and `script/test-verify-build` in the `test` phase. `script/check` runs the - two phases and `script/fmt-check`. `script/cibuild` bootstraps, runs - `script/check`, then builds the image uncached. Given no `VERSION` build - argument, as in a plain `docker build .`, the image build takes one from - `git describe` on the `.git` in the build context, which `.dockerignore` now - sends without its `config`. The vendored `check.yml` has no `timeout-minutes`, - so the `check` job's cap is gone. - -- 2026-10-06: Reloading or closing the popup mid-refresh no longer logs the - requests that cancels as failures - ([#218](https://git.eeqj.de/sneak/AutistMask/issues/218)). Chrome cancels a - closing page's open requests just after `pagehide`, and in the page a - cancelled `fetch()` fails with the same "Failed to fetch" as a server that - cannot be reached, so the home screen's transaction list and the balance - refresh logged an error for each, and the e2e suite failed on them. The popup - now aborts an `AbortController` on `pagehide`, and those two check its signal - before reporting a failure. New e2e tests reload the popup with Blockscout - held and require nothing logged, and fail the transaction list for real and - require the failure reported; `tests/balanceRefreshCancelled.test.js` covers - the balance refresh. The address and token screens and the address scan after - a new wallet still log a cancelled request - ([#475](https://git.eeqj.de/sneak/AutistMask/issues/475)). - -- 2026-10-05: `make build` no longer prints the Node `DEP0205` - `module.register()` deprecation warning - ([#355](https://git.eeqj.de/sneak/AutistMask/issues/355)). The call came from - `@tailwindcss/node`, which the Tailwind CLI loads; `tailwindcss` and - `@tailwindcss/cli` are now pinned at 4.3.1, the first release that uses - `module.registerHooks()` where Node has it. The compiled CSS computes to the - same values; it drops the unused `.start` and `.end` rules and writes - `calc(var(--spacing) * 1)` as `var(--spacing)` and `calc(var(--spacing) * 0)` - as `0`. - -- 2026-10-05: `make dev` watches - ([#332](https://git.eeqj.de/sneak/AutistMask/issues/332)). It used to pass - `--watch` to a `build.js` that read no arguments, so it built once and exited. - `build.js --watch` now builds, then builds again after every change to a file - under `src/`, `manifest/` or `icons/`; any other argument fails. It watches - each directory rather than using Node's recursive watch, which on Linux stops - seeing a file an editor saves by renaming a new copy over it. It writes no - build receipt, so nothing can verify what it builds; `make build` remains the - way to produce a `dist/` to hand on. `tests/buildWatch.test.js` covers the - watch loop against a temp directory. - -- 2026-10-05: Every CI job has a `timeout-minutes` cap - ([#294](https://git.eeqj.de/sneak/AutistMask/issues/294)): `check` 10 minutes, - `e2e-firefox` 15 and `e2e-chrome` 20, each over two and a half times the job's - slowest cold-cache run. A hung build or browser now ends its job instead of - holding the shared runner for hours. - -- 2026-10-05: `PROXY_METHODS` in `src/background/index.js` no longer lists - `eth_chainId` and `net_version` - ([#326](https://git.eeqj.de/sneak/AutistMask/issues/326)). `handleRpc` answers - both itself before its proxy branch, so the list named two methods that are - never sent to the RPC endpoint. No other entry is answered earlier. - `tests/proxyMethods.test.js` sends every listed method from a page and fails - on any that does not reach the RPC endpoint. - -- 2026-10-05: The popup's Content Security Policy no longer allows inline style - ([#328](https://git.eeqj.de/sneak/AutistMask/issues/328)): `style-src` is - `'self'` in both manifests, pinned in `tests/manifest.test.js`. The 42 - `style="..."` attributes in `src/popup/index.html` and in the markup the view - helpers build are now Tailwind classes, each computing to the value it - replaced. The 16 address dot colours are written out as whole classes, because - Tailwind builds only the classes it finds in the source. The Settings debug - well is shown and hidden with the `hidden` class, since clearing an inline - `display` no longer uncovers it. Script that sets `element.style` is - unaffected. - -- 2026-10-05: `.prettierignore` no longer lists an AI vendor's tool directory - ([#363](https://git.eeqj.de/sneak/AutistMask/issues/363)). The directory is - not tracked, so the line ignored nothing. - -- 2026-10-05: The Chrome end-to-end suite drives both ways the wait for a - transaction's receipt ends on the error screen - ([#315](https://git.eeqj.de/sneak/AutistMask/issues/315)): lookups that still - find no receipt 60 seconds after the broadcast end it with the timeout - message, and six lookups that fail in a row end it with the message naming the - unreachable network. Done then returns to the address screen. Both cases wait - in real time, about a minute each. Playwright's clock would apply to every - later test in the run and cannot be removed, and moving the stored broadcast - time back can be undone by the save the popup makes every ten seconds. - -- 2026-10-05: The Chrome end-to-end suite covers the last of the - [#150](https://git.eeqj.de/sneak/AutistMask/issues/150) and - [#151](https://git.eeqj.de/sneak/AutistMask/issues/151) items - ([#295](https://git.eeqj.de/sneak/AutistMask/issues/295)): a token added on - Add Token by its contract address is listed on the address screen; - TransactionDetail opened from the token screen leaves that screen on the - persisted navigation stack, and Back returns to it; and the token contract row - links to the explorer's token page, read off the link rather than followed. - The network stub answers `symbol()` and `name()` for the stub token, which - adding it reads. - -- 2026-10-05: Each control that leads to a signature or to the private key has a - test that it refuses a defective wallet before asking for a password - ([#254](https://git.eeqj.de/sneak/AutistMask/issues/254)): Send on the main, - address and token screens, Export Private Key, and both approval screens, as - drawn and as clicked. Send on the confirmation screen refuses it too now, - because the popup reopens onto that screen from a saved view. The comments - that said the wallet's key cannot be derived now say that - `getSignerForAddress` refuses it, and the module comment in - `src/shared/walletDefects.js` names both earlier import paths. - -- 2026-10-05: The Chrome end-to-end suite drives the private key export screen - as it drives the recovery phrase screen - ([#253](https://git.eeqj.de/sneak/AutistMask/issues/253)): the correct - password shows the key, leaving by the settings gear empties the screen, and - leaving while the password is still being checked never puts the key on it. - The cases use the imported key wallet rather than the HD one. Leaving drops - the address the screen was showing, and an HD wallet's key cannot be derived - without it, so on an HD wallet a late decrypt fails by itself and would never - exercise the check that discards it. The screen cannot yet be opened twice in - one popup session ([#460](https://git.eeqj.de/sneak/AutistMask/issues/460)), - so the cases reopen the popup before the second open. - -- 2026-10-05: The StateRecovery screen is driven in a real browser under the - shipped CSP, in both end-to-end suites - ([#361](https://git.eeqj.de/sneak/AutistMask/issues/361)). A stored record - this build cannot read opens the popup on it; its export text box holds the - record exactly as stored; a near-miss confirmation phrase erases nothing; and - the exact phrase erases the record and reloads into Welcome. The cases run - before any wallet exists: with no wallet nothing saves on a timer, so no save - can write a good record over the unreadable one, and the erase leaves the - popup on Welcome for wallet creation. - -- 2026-10-05: Escaping in the popup's views follows its own rule with no - exceptions ([#329](https://git.eeqj.de/sneak/AutistMask/issues/329)). The - decimals and holder count on a token's screen, and every USD figure (the ETH - price, each total and each balance row's value), went into `innerHTML` - unescaped; they are escaped now. None could carry markup, but `formatUsd()` - writes a value under a cent as `< $0.01`. `displaySymbol()` counts a symbol in - code points rather than UTF-16 units, so a cut never splits an emoji into a - half that renders as U+FFFD. `explorerLink()`, also named in the issue, was - already removed by [#168](https://git.eeqj.de/sneak/AutistMask/issues/168). - -- 2026-10-05: A Chrome end-to-end test that fails no longer takes later tests - down with it ([#318](https://git.eeqj.de/sneak/AutistMask/issues/318)). Each - test that turns a fixture switch on for itself alone (a held or failing gas - estimate, a seeded native transfer or receipt, a token's lying `decimals()` or - markup symbol) turns it off again in a `finally`, and the two tests that drive - the popup's own send end on the address screen whether they pass or not, - reopening the popup to leave a wait for a receipt. The lying-`decimals()` test - checks that nothing was broadcast as soon as the send ends, before it waits - for the failure screen, so a broadcast fails it in seconds rather than after a - 60-second wait. The fixture's `decimals()` override tells 0 from no override, - so a token with no decimal places can be fixtured. - -- 2026-10-05: Chrome draws the popup in its monospace font - ([#418](https://git.eeqj.de/sneak/AutistMask/issues/418)), as Firefox does. - Chrome adds a stylesheet of its own to extension pages that sets the font on - `body`, and it beat Tailwind's `font-mono`: Tailwind 4 puts its classes in a - cascade layer, and a rule outside any layer wins over them. `body` now carries - `font-mono!`, which marks the class important. Both end-to-end suites check - the popup's font. The same stylesheet also makes Chrome draw the popup's text - at 12px rather than the 14px `text-sm` asks for; that is unchanged, and filed - as [#456](https://git.eeqj.de/sneak/AutistMask/issues/456). - -- 2026-10-05: Dead code removed and copied view helpers shared - ([#168](https://git.eeqj.de/sneak/AutistMask/issues/168)). AddressDetail and - AddressToken each defined their own `isoDate()` and `timeAgo()` in place of - the ones in `src/popup/views/helpers.js`, so a fix to the shared pair would - not have reached them. The copies were identical; every screen now uses the - shared pair. `blockieHtml()` and `tokenLabel()`, each defined twice, live in - `helpers.js` too. Removed as never called: `explorerLink()` (the views build - explorer links with `explorerUrl()`), `ETHEREUM_SEPOLIA_CHAIN_ID` (the chain - id lives in `src/shared/networks.js`), and `getWalletValue()` and - `getTotalValue()`: Home's "Total:" is the active address's total, as - `README.md` says. `addressColor()` and `etherscanAddressUrl()` are no longer - exported. Nothing the user sees changed. - -- 2026-10-05: A prompt raised while another approval window has focus opens a - window of its own ([#290](https://git.eeqj.de/sneak/AutistMask/issues/290)). - The background centred each approval window on the last focused window, which - could be an earlier approval window still open; headless Chrome reports one as - 1280x720, so the new window came out where the browser refused to create it, - and the request failed with no window at all. It now centres only on a browser - window, and when the browser refuses the position it asks again without one - and lets the browser place the window. In the Chrome end-to-end suite a test - could raise its prompt while the previous test's window was still closing, and - then either hit that refusal or take the closing window for its own. After a - test that passed, the runner now waits a few seconds for approval windows to - close and fails the test if one is still open; after a test that failed, it - closes them. - -- 2026-10-05: The Send screen has a "Max" button - ([#198](https://git.eeqj.de/sneak/AutistMask/issues/198)). Emptying an ETH - address took guessing an amount and being refused by the confirmation screen's - balance check. Max fills in a token's whole balance, cut to the 18 decimal - places the confirmation screen accepts, or for ETH the exact balance minus the - fee reserve that check gates on, never the four-decimal balance shown; a fee - estimate that finishes after the Send screen was left, or its address, holding - or recipient changed, fills nothing in. The confirmation screen works a max - ETH amount out again from its own fee estimate and signs it with that - estimate's fee fields: fetched again at signing, a fee that had risen since - would leave amount plus fee above the balance, and the node would refuse the - send. A token's maximum is still refused when ETH cannot pay the fee. Where - there is nothing to fill in, a flash message says why. - -- 2026-10-05: A token scale of zero decimals is tested - ([#325](https://git.eeqj.de/sneak/AutistMask/issues/325)). - `resolveTokenDecimals()` already used a scale of 0 from the bundled list or - from a tracked token, but no test said so: turning either of its `d !== null` - checks into a plain truthiness check left every test green while a - zero-decimal token fell through to the next source, or to "decimals unknown". - The approval tests now assert a scale of 0 from each of those two sources, - both where it is resolved and on the approval screen's Amount line. The second - half of the issue, one shared `toDecimals()`, had already landed with - [#349](https://git.eeqj.de/sneak/AutistMask/issues/349). - -- 2026-10-05: The e2e suite waits for a save to land before it closes the popup - ([#446](https://git.eeqj.de/sneak/AutistMask/issues/446)). The Settings round - trip switched the theme and the network and closed the popup at once, and a - close before the save lands loses the switch; with the network left on - Sepolia, a dozen later tests failed too. Each Settings switch and spam-filter - toggle is now waited for in storage before the close, and `reopenPopup()` - waits until the view it expects to reopen on is the saved one. The restore - half of the round trip and the second filter toggle change a setting right - after a reopen, while the reopened popup's own saves may still be running; - they rely on the fix for - [#448](https://git.eeqj.de/sneak/AutistMask/issues/448). - -- 2026-10-05: A change made while an earlier save from the same page is still - running is stored ([#448](https://git.eeqj.de/sneak/AutistMask/issues/448)). - `saveStateOnce()` took its baseline from the page's state after the write, so - a change made while the save waited on storage counted as already stored and - the save queued after it wrote nothing. A setting changed during the read was - lost; so was a wallet added, a site revoked or an endpoint changed during the - write, and a wallet deleted then stayed in storage. The save now copies the - page's fields when it starts, writes from that copy, and keeps the copy as the - baseline. - -- 2026-10-05: The extension no longer opens a window for a site-connection - prompt already answered - ([#287](https://git.eeqj.de/sneak/AutistMask/issues/287)). When the prompt was - decided before the toolbar popup raised for it had loaded, that popup was torn - down, `chrome.action.openPopup()` rejected, and the background opened its - fallback window for the answered approval and then removed it. In the Chrome - end-to-end suite the next test could take that window for its own prompt and - lose it under its wait. `openApprovalWindow()` now opens nothing for an - approval that is no longer pending. The blocklist test's Reject, whose window - closes itself, is clicked as the other site Reject is, with the click - witnessed. Making `e2e-chrome` a required check is still blocked: other - reports of the Chrome suite failing under load are open, among them - [#290](https://git.eeqj.de/sneak/AutistMask/issues/290) and - [#446](https://git.eeqj.de/sneak/AutistMask/issues/446), as `README.md` says. - -- 2026-10-05: The lost-password delete confirmation refuses an empty field and - ignores characters that paint nothing - ([#336](https://git.eeqj.de/sneak/AutistMask/issues/336)). A wallet named only - with spaces compared equal to an empty field, so typing nothing would have - deleted it, and a zero-width space in a name made the name impossible to type - back. An empty field is now refused whatever the name is, the same invisible - characters `src/shared/symbolSpoof.js` strips are removed from both sides, and - a name that shows nothing is shown and typed back as "Wallet N". - -- 2026-10-05: A `holders_count` that is not a whole number in plain digits is - unknown, not read in part - ([#251](https://git.eeqj.de/sneak/AutistMask/issues/251)). `parseInt` read - `1,000` as 1, `0x10` as 0 and `1e3` as 1, a reported low count that hides the - token in the transaction history and the send-screen token selector. A count - above `Number.MAX_SAFE_INTEGER` is unknown too, not rounded or `Infinity`. The - balance list's `holders !== null` check, which did nothing, is dropped. - `README.md` and `docs/README.md` now say how each filter treats an unknown - count and that the token screen leaves out its "Holders:" row then, and - `README.md` lists `src/shared/holders.js`. - -- 2026-10-04: A popup boot in the tests loads transactions without failing - ([#429](https://git.eeqj.de/sneak/AutistMask/issues/429)). The stand-in for - `filterTransactions` in `tests/support/popupBoot.js` returned a bare list, - while the real one returns `{ transactions, newFraudContracts }`, so every - boot onto Home, AddressDetail or AddressToken failed inside its transaction - loading and logged `loadHomeTxs failed` or `loadTransactions failed`; the rest - of that code never ran. The stand-in now returns the real shape, and - `tests/persistedFieldContract.test.js` boots onto each of the three and - asserts neither message is logged. `make test` time did not change measurably. - -- 2026-10-04: The native token's label follows the network - ([#372](https://git.eeqj.de/sneak/AutistMask/issues/372)). `networks.js` gives - each network a `nativeCurrency` and nothing read it: every screen wrote `ETH`, - so on Sepolia the balance, the value and the fee all read `ETH`. Every native - figure now reads `nativeCurrency`, which is `ETH` on mainnet and `SepoliaETH` - on Sepolia: the balance lists, Send and confirmation screens and the - contract-recipient warning the active network's; the approval, wait, success, - error and transaction detail screens, the transaction history and the refusal - of a fee above the limit that of the network the transaction's chain id names. - A token claiming any network's `nativeCurrency` is dropped as a fake, as one - claiming `ETH` already was, and the transaction detail screen calls an entry a - token transfer when it has a token contract, not by its symbol. -- 2026-10-04: A popup that is already open when the stored profile becomes - unreadable moves to the recovery screen - ([#373](https://git.eeqj.de/sneak/AutistMask/issues/373)). It used to stay on - the last good profile, with the "NOT SAVED" banner at most, until reopened. - Every save already ran the check the popup runs at open, so the popup finds - the record at the next navigation or ten-second refresh, whether or not the - network answers; a save that fails that check now raises the recovery screen - and stops the refresh. The screen it replaces is left as any navigation leaves - it, so a revealed phrase or key or a typed password is wiped. Once up, nothing - else in that popup can replace it, and a later save or a transaction wait that - ends does not clear an export or a typed confirmation. It is never saved as - the current view, so a popup opened after the record is erased in another - window opens normally. Any other failed save still gets the banner and leaves - the screen alone. -- 2026-10-04: A swap whose deadline is later than a JavaScript date can hold is - decoded ([#437](https://git.eeqj.de/sneak/AutistMask/issues/437)). A date - reaches only to 275760-09-13, so a later deadline, such as the `uint256` - maximum, made the `Deadline` line throw, and the approval screen showed the - swap as an undecoded contract call with nothing saying why. That line now - reads `After 275760-09-13 00:00:00 (no deadline in practice)`. -- 2026-10-04: The swap decoder reads two router zeros the way the router does - ([#415](https://git.eeqj.de/sneak/AutistMask/issues/415)). A V2 exact-in - `amountIn` of zero means an earlier step already sent the tokens to the pair; - `Amount` showed `0.0000` for it and now reads - `Whatever an earlier step sent to the pair (V2 already paid)`. A - `BALANCE_CHECK_ERC20` with a zero `minBalance` guarantees nothing, yet it - replaced the minimum an earlier swap step stated, so `Min. received` read - `None (no minimum guaranteed)`; it now sets the output side only when that - side holds no minimum at the point the check is reached. A nonzero - `minBalance` still sets the output side. -- 2026-10-04: The signature screen shows a personal message as the bytes that - are signed ([#403](https://git.eeqj.de/sneak/AutistMask/issues/403)). It - showed only the decoded text, with bidirectional and zero-width characters - acting on it, so a site could make the message read differently from what is - signed, and a message that was not hex was shown as NUL characters. The hex is - now shown as "Raw data" alongside the decoded text, the text is laid out left - to right in byte order, control characters, line and paragraph separators and - characters that paint nothing are shown as `U+XXXX` marks, and a message that - is not hex by the rule signing reads it with is shown as plain text with - "Sign" disabled, since such a message has no bytes to sign. -- 2026-10-04: A token that reports more than 80 decimal places has no known - scale ([#350](https://git.eeqj.de/sneak/AutistMask/issues/350)). The shared - scale check `toDecimals()` accepted any `uint8`, but `formatUnits()` throws - above 80, so such a token left a swap or an ERC-20 call on the approval screen - undecoded, with nothing saying why. The check now stops at 80, and both - approval paths show the base-unit amount with the scale stated as unknown. The - balance list and the history list use the same check, so the same token no - longer stops an address's token balances from refreshing or its history from - loading. -- 2026-10-04: Debug mode no longer writes RPC API keys to the console - ([#410](https://git.eeqj.de/sneak/AutistMask/issues/410)). `debugFetch` logged - every request's full URL and body, so an RPC endpoint with a key in its path - or query string printed that key on every request. It now logs the HTTP - method, the URL's origin and, for a JSON-RPC call, the method name. The - balance refresh and token lookup log the RPC endpoint by its origin too. A - failed RPC call's error line prints the error's short message, which names the - HTTP status, not its full message, which carries the request URL. A failed - endpoint check in settings names the endpoint by its origin, not the `fetch` - error's message, which carries the whole URL, password included, for a URL - with a user name and password. The README's DEBUG Mode Policy says what debug - mode logs. - -- 2026-10-04: A site has at most one connection prompt and one signature prompt - open at a time ([#405](https://git.eeqj.de/sneak/AutistMask/issues/405)). Each - `eth_requestAccounts` or `personal_sign` call opened another approval window, - so a page calling in a loop could cover the screen with identical prompts. A - further request of the same kind from a site whose prompt is still unanswered - is now refused with EIP-1193 `-32002`, the code a second transaction already - gets, and opens no window. Signing by `personal_sign`, `eth_sign` and - `eth_signTypedData_v4` counts as one kind. Other sites are not affected, and - the site may ask again once the user has answered. A connection prompt whose - toolbar popup closed before it connected, and which nothing shows any more, is - shown again when the site asks again. - -- 2026-10-04: A nonce the site supplies with `eth_sendTransaction` is ignored - ([#404](https://git.eeqj.de/sneak/AutistMask/issues/404)). It was passed on to - the transaction, so a site could replace one of the user's pending - transactions (same nonce, higher fee) or leave the new one stuck behind a gap, - and the approval screen showed it as a bare number. `nonce` is no longer one - of the fields taken from the request in `src/shared/approvalTx.js`, so the - transaction always gets the account's next nonce from the node, and that is - the nonce the approval screen shows and the popup signs. - -- 2026-10-04: Remembered site permissions are held by full origin - ([#402](https://git.eeqj.de/sneak/AutistMask/issues/402)). `allowedSites` and - `deniedSites` stored the hostname alone, so a grant to `https://dapp.example` - also authorised `http://dapp.example` and every port on that host, and the - prompts named only the hostname. Both lists now store and match the origin - (`scheme://host[:port]`), the key the connections approved without "Remember" - already used, in `src/background/index.js` and in Settings, whose site lists - and `AUTISTMASK_REMOVE_SITE` carry the origin too. The connection, transaction - and signature prompts show the origin. Entries saved by hostname before this - change are not migrated (pre-1.0): they match no site, and Settings lists them - until they are removed. - -- 2026-10-04: A page's request is credited only to the site the browser says - sent it ([#407](https://git.eeqj.de/sneak/AutistMask/issues/407)). Where the - browser does not give the sender's origin (Firefox before 126), the background - used the tab's page, so a frame from another site would have been treated as - the site embedding it, and with no tab it used an origin the page wrote into - the message. It now uses the URL of the frame that sent the message, and - refuses the request with code 4100 when the browser gives neither. The content - script no longer writes an origin into the message. - -- 2026-10-04: `make test` takes 8-13s on the shared build host, down from - 17-25s, measured in alternating runs before and after the change - ([#428](https://git.eeqj.de/sneak/AutistMask/issues/428)). Each popup boot in - the tests (`tests/support/popupBoot.js`) resets jest's module registry so that - everything under `src/` loads fresh, and that also reloaded `ethers`, - `libsodium-wrappers-sumo`, `qrcode` and `ethereum-blockies-base64` every time. - Those four libraries are now loaded once per test file and handed to every - boot. No test or assertion changed. - -- 2026-10-04: A token whose scale is unknown reads the same on the Send screen - as on the confirmation screen - ([#377](https://git.eeqj.de/sneak/AutistMask/issues/377)). When two addresses' - explorer reports disagree on a token's `decimals`, the Send screen showed the - stored figure while the confirmation screen it leads to said - `unknown (SYMBOL)`; both now say `unknown (SYMBOL)`, from one function in - `src/popup/views/send.js`. The confirmation screen's fee-unknown message names - its cause: for an unknown scale it says the wallet does not know how many - decimal places the token uses and that the transaction cannot be sent, instead - of asking the user to go back and try again, which cannot help. For any other - cause it is unchanged. - -- 2026-10-04: A Uniswap V2 exact-out swap (Universal Router command `0x09`) is - decoded on the approval screen - ([#283](https://git.eeqj.de/sneak/AutistMask/issues/283)). `decode()` in - `src/shared/uniswap.js` had no arm for it, so the screen named the step and - showed no token or amount. The input side is the path's first token with - `amountInMax`, the output side the last token with `amountOut`. When the - transaction has such a step, the `Amount` figure reads `Up to `, - whichever step set it, there and on the wait, success and error screens, - except where it reads `Unlimited` (an unbounded `PERMIT2_PERMIT`, or any - amount at or above the `uint160` maximum) or `All available (V4 open delta)`. - In every swap, `UNWRAP_WETH` makes `Token Out` ETH only when the output side - is WETH, on mainnet or Sepolia, or when no step set the output side; otherwise - `Token Out` and `Min. received` keep the output side's own token and figure. - V3 exact-out (`0x01`) is still not decoded. - -- 2026-10-04: `make test` runs jest in three worker processes - ([#426](https://git.eeqj.de/sneak/AutistMask/issues/426)). The `test` and - `test:verbose` scripts in `package.json` ran `jest --forceExit`, which starts - one worker per CPU core: about 47 processes and 7-8 GiB per run on the shared - 48-core build host. They now pass `--maxWorkers=3`, and the suite takes 23-29s - there: inside the 30-second cap in `script/test`, which is unchanged, but not - by much, because `tests/persistedFieldContract.test.js` alone takes most of it - ([#428](https://git.eeqj.de/sneak/AutistMask/issues/428)). One or two workers - went past the cap. `make check`, the pre-commit hook and `script/cibuild` all - run the suite through these scripts. - -- 2026-10-04: The error container on each dApp approval screen keeps its height - when an error appears - ([#297](https://git.eeqj.de/sneak/AutistMask/issues/297)). `#approve-tx-error` - and `#approve-sign-error` reserved 20px, but their border and padding took - 10px of it, so a one-line error grew them to 26px and pushed the buttons below - down 6px. They now reserve 30px. A new test in `tests/e2e/run.js` shows each - of the six password error containers on its own screen, empty and then with an - error, and fails if one changes height or the element below it moves. Some of - the longer messages these two containers can show still take two lines. - -- 2026-10-04: A transaction with no `to` says "This transaction creates a new - contract. It has no recipient." on its recipient line and in its transaction - history row ([#250](https://git.eeqj.de/sneak/AutistMask/issues/250)). The - wait, success and error screens, the transaction detail view and the history - rows on Home, AddressDetail and AddressToken showed a blank address there, - with a colour dot whose colour was `undefined`; the approval screen showed - "(contract creation)". A transaction with a real `to` is unchanged. - -- 2026-10-04: The Send and confirmation screens no longer show an ETH balance, a - token balance or a network fee below 0.000001 as zero - ([#343](https://git.eeqj.de/sneak/AutistMask/issues/343)). The stored balances - (`src/shared/balances.js`) and the confirmation screen's fee were each cut to - six decimal places by a rule of their own, and a token holding cut to zero was - dropped. Balances are now stored exactly, whatever decimals a token declares, - and every nonzero token holding is kept; the balance check reads a token - balance to its first 18 places, the most an amount can have. The balance - lists, the send-screen token selector, the address total and the - remove-address warning leave out a holding below 0.000001 themselves, as - before. The Send screen's `Current balance`, and the confirmation screen's - balance, fee, reserve and insufficient-balance messages, go through - `truncateAmountNeverZero()` in `src/shared/amountDisplay.js`, the helper the - approval screen already used. The confirmation and approval screens both - render the fee through `formatFee()` in `src/popup/views/helpers.js`, which - prices the exact fee in USD, so the same fee reads the same on both, USD value - included. - -- 2026-10-04: The flash line keeps to the one line it reserves at any message - length ([#252](https://git.eeqj.de/sneak/AutistMask/issues/252)). A message - that wrapped pushed the whole screen below it down. `#flash-msg` no longer - wraps: text too long for the line is cut with an ellipsis, and `showFlash()` - puts the whole message in the line's title. Every message is also reworded to - at most 50 characters so none is cut; none carries a wallet name or text from - a server, and the add-token screens flash a fixed line for any error other - than a contract that is not a token. A new test in `tests/e2e/run.js` puts a - message several lines long on the line and fails if the line or the screen - below it moves. The two approval-screen error boxes are left to - [#297](https://git.eeqj.de/sneak/AutistMask/issues/297). - -- 2026-10-04: A method the wallet does not implement is refused with EIP-1193 - code `4200` ([#279](https://git.eeqj.de/sneak/AutistMask/issues/279)). The - background's `Unsupported method: ` error carried no code, so a site - probing for an optional method could not tell "not implemented" from "the call - failed". The message is unchanged; the background's other errors with no code - are untouched. - -- 2026-10-04: Settings lists the sites connected without "Remember", and - removing a site there disconnects it - ([#406](https://git.eeqj.de/sneak/AutistMask/issues/406)). Such a connection - lives only in the background's in-memory `connectedSites` map, so Settings - never showed it and the user could not end it; `AUTISTMASK_REMOVE_SITE`, sent - on every remove, did nothing. Settings now asks the background for those sites - (`AUTISTMASK_GET_CONNECTED_SITES`) and lists them under Connected Sites. - Removing a site from Allowed Sites or Connected Sites drops its remembered - entry under every address and sends `AUTISTMASK_REMOVE_SITE` with the - hostname; the background deletes every `connectedSites` entry for that - hostname and sends `accountsChanged` with an empty list to its open tabs. Only - the extension's own pages may send either message. Removing a denied site no - longer sends it, since forgetting a refusal ends no connection. -- 2026-10-04: Removing an address or deleting a wallet ends every site - connection approved without "Remember" for the addresses removed - ([#245](https://git.eeqj.de/sneak/AutistMask/issues/245)). Such a connection - lives only in the background's in-memory `connectedSites` map. Both removal - paths dropped the remembered `allowedSites`/`deniedSites` entries, but nothing - told the background, so its entry was cleared only as a side effect: every - change of active address empties the whole map, and removing the active - address changes it. `dropSitePermissions()` in `src/shared/walletDelete.js`, - shared by both paths, now also sends `AUTISTMASK_ADDRESSES_REMOVED` with the - removed addresses, and the background deletes their `connectedSites` entries; - only the extension's own pages may send it. -- 2026-10-03: The typed-data signing screen warns for a token permission, and - names the primary type ethers signs - ([#400](https://git.eeqj.de/sneak/AutistMask/issues/400)). A Permit or Permit2 - signature lets its spender take tokens from the signer's address, and the - screen listed it as plain key/value lines, exactly like a sign-in message. For - typed data signed as `Permit` (EIP-2612's, DAI's older one, or any other of - that name) or as one of Permit2's six signature types, - `src/popup/views/approval.js` now shows a red warning at the top of the - message naming the spender and each token and amount, read only from the - fields the signed type declares (a `Permit`'s token is the domain's - `verifyingContract`), with `Unlimited` for the largest amount the field holds, - the existing unknown-scale wording otherwise, and `Unknown` for whatever those - fields do not give. The screen printed the page's `primaryType`, but ethers - signs the type it derives from `types`; the screen now shows the derived type, - and typed data whose stated type is missing or differs, or that cannot be - read, is shown with an error line and Sign disabled, and is refused again - where signing starts. The warning names no deadline or expiry: those fields - mean different things across the shapes, and a date could read as the - permission ending when it does not. -- 2026-09-21: The network fee a transaction can commit is bounded by the product - of the gas limit and the fee per gas, not by each field alone, and the - wallet's own send is bounded the same way - ([#399](https://git.eeqj.de/sneak/AutistMask/issues/399)). The two per-field - ceilings in `src/shared/approvalVerify.js` were checked independently, so a - gas limit and a fee that were each under their own ceiling still multiplied to - thousands of ETH — a fee a gas-consuming contract really collects — while the - comment claimed the ceiling caught exactly that. `assertWithinCeilings` now - also refuses a transaction whose gas limit times its fee per gas - (`maxFeePerGas` for a type-2 transaction, `gasPrice` for a legacy or type-1 - one) exceeds `MAX_TOTAL_FEE`, a new constant of 1 ETH beside the existing - ceilings, so both callers — where the dApp transaction is populated and where - the signed artifact is verified — reject it with a full sentence naming the - fee and the limit. The wallet's own send in `src/popup/views/confirmTx.js` - pinned no fee fields, so ethers filled them from whatever the configured node - answered with nothing bounding them; it now populates the transaction and runs - the same check before signing, showing the same error in the confirmation - screen's reserved errors box so nothing on screen moves. Deliberately out of - scope: comparing a supplied fee against the node's own suggested fee, which - the absolute bound already makes unnecessary for the balance-draining case. 1 - ETH is a plain constant, one line to change; the owner may prefer another - figure. -- 2026-09-21: The test recovery phrase no longer survives in a release bundle, - and the committed-key guard matches by content - ([#351](https://git.eeqj.de/sneak/AutistMask/issues/351)). `DEBUG_MNEMONIC` in - `src/shared/constants.js` is now behind the `__BUILD_DEBUG__` define, so a - release build folds the phrase to `null` and no emitted bundle carries it; it - used to survive as dead text because `module.exports` keeps the const alive. - `script/verify-build` now fails a release build if the phrase appears in any - emitted file, so the fold cannot silently regress. `tests/extensionId.test.js` - scans the content of every tracked file for a PEM private-key header instead - of matching filename extensions alone. -- 2026-09-21: A transaction response is honoured only for a transaction - approval, and the three remaining approval-settlement paths are pinned - ([#262](https://git.eeqj.de/sneak/AutistMask/issues/262)). The liveness fix - the issue asks for — settle `4001` on release when the window it would be - retried in is gone — already landed with - [#271](https://git.eeqj.de/sneak/AutistMask/issues/271); this closes the rest. - `AUTISTMASK_TX_RESPONSE` now refuses any approval that is not a transaction - approval, so a reject no longer retires a sign or connection approval and a - signed artifact never runs the broadcast path against one, which before only - failed closed by throwing deeper in. Tests pin the site-connection port's - approve, reject and disconnect paths against a transaction approval - broadcasting behind them: each is declined and the dApp still receives its - broadcast result. -- 2026-09-21: `docs/RELEASE.md`, linked from `README.md`, states the release - procedure as a numbered list a newcomer can follow: confirm `main` is green in - CI, confirm the one version in the three files matches the intended tag, - `make package` from a clean checkout, verify `SHA256SUMS`, tag `vX.Y.Z`, then - distribute per browser. Each step names who performs it (owner-only steps - marked) and the check that it worked. The distribution step is written as - pending the owner's choice on - [#386](https://git.eeqj.de/sneak/AutistMask/issues/386), with the Firefox and - Chrome options named but none settled. Docs only - ([#387](https://git.eeqj.de/sneak/AutistMask/issues/387)). - -- 2026-09-21: Adding a second wallet no longer accepts a different password with - nothing saying it is a separate one - ([#374](https://git.eeqj.de/sneak/AutistMask/issues/374)). Each wallet has its - own encrypted secret, so per-wallet passwords are by design; the add-wallet - screen said only "Choose a password". A note now appears on that screen when - the profile already holds a wallet, stating that each wallet has its own - password and this one need not match any already in use. It is shown only - then, since the first wallet has no other password to differ from, and it - stays consistent with the no-reset reality of - [#312](https://git.eeqj.de/sneak/AutistMask/issues/312) by promising no - recovery or reset. - -- 2026-09-21: The dApp approval and transaction-status screens resolve a token's - symbol from the bundled list, then the tokens the user tracks, then the block - explorer's report — the same sources and precedence the amount line already - used for the token's scale - ([#323](https://git.eeqj.de/sneak/AutistMask/issues/323), folding in - [#354](https://git.eeqj.de/sneak/AutistMask/issues/354)). A token the user - added by hand, or holds a balance of, is now named rather than labelled - `Unknown token`, and a non-bundled ERC-20 is no longer carried onto the wait - screen as `ETH`. A tracked or explorer-reported name stays subject to the - spoof rule, so resolving a symbol is not a new way to wear a known ticker. -- 2026-09-21: The debug/testnet banner no longer shows the internal view id to - the user in a release build - ([#375](https://git.eeqj.de/sneak/AutistMask/issues/375)). The banner appended - the active view's id (e.g. `[TESTNET] (approve-tx)`), which is developer - vocabulary sitting directly above the approval screen's carefully worded - authorization text. The suffix is now gated on the compile-time `DEBUG` - constant rather than `isDebug()`, so it survives only in a debug build; a - testnet or the runtime debug toggle still raises the banner but without the - view id. - -- 2026-09-21: The Confirm Delete button on the delete-wallet screen no longer - stays dead after a successful delete - ([#335](https://git.eeqj.de/sneak/AutistMask/issues/335)). The password route - disabled the button before the decrypt and never re-enabled it, so a second - delete in the same popup session needed a reopen; the lost-password route - re-enabled its own button in its leave hook, so the two screens behaved - differently. Both now reset through the shared `finishDelete()`, the one path - both routes take, so they behave the same and the button is live for the next - delete. - -- 2026-09-21: The EIP-6963 provider UUID is generated fresh on each page load - and never persisted ([#398](https://git.eeqj.de/sneak/AutistMask/issues/398)). - It was created once and stored, then announced verbatim to every page on every - load and across restarts, so any site — connected or not — could read it as a - stable cross-site, cross-session identifier for the install, contradicting the - "no tracking" promise. inpage.js now announces a per-load - `crypto.randomUUID()` and the `eip6963Uuid` storage key and the - `AUTISTMASK_PROVIDER_UUID` content-script message are gone. That key was a - standalone storage entry, never part of the versioned `autistmask` profile, so - the state schema is untouched and no existing profile is affected. - -- 2026-09-21: `README.md` no longer says a genuine zero always renders `0.0000` - ([#369](https://git.eeqj.de/sneak/AutistMask/issues/369)). The amount rule - still renders one as `0.0000`, but two swap lines say a zero in words instead: - `Min. received` reads `None (no minimum guaranteed)` for a zero minimum, and - `Amount` reads `All available (V4 open delta)` when the amount it shows is a - V4 exact-in `amountIn` of zero. A new list says what the decoded ERC-20 and - swap amount lines on the transaction approval screen can read: a formatted - quantity, base units with decimals unknown, `Unlimited`, - `All available (V4 open delta)` and `None (no minimum guaranteed)`, which a - zero `minBalance` on a `BALANCE_CHECK_ERC20` step now reads instead of - `0.0000` at a known scale. The README also names - `Unknown (not named in the calldata)` on the swap's token lines, and points to - SignApproval for the token permission warning's own wording. Docs only. - -- 2026-08-30: An address no longer wraps, or is shortened to fit, in any of the - common views ([#380](https://git.eeqj.de/sneak/AutistMask/issues/380)). The - wallet list was the reported case: the address shared one row with the - `[info]` and `[x]` controls and folded onto a second line, which turns one - 42-character string the user is meant to compare into two shorter ones — the - shape an address-poisoning attack wants. The fix is layout, not CSS: every - address in the popup now sits alone on a full-width row, with the colour dot, - the wallet title, the ENS name and the explorer link moved onto a strip above - it, and the transaction rows carry the counterparty's whole address instead of - a `truncateMiddle()`d one squeezed in beside the amount. `truncateMiddle()` - keeps its 10-character cap and its 32-character floor moved into - `renderAddressHtml()`, so the guarantee outlives having no callers. The e2e - suite measures every rendered address in a real Chromium — whole, one line - box, inside its row and inside the popup — across Home, the address, token, - receive, send and transaction detail screens, the confirmation screen and the - dApp transaction prompt. -- 2026-08-23: Both manifests declare toolbar icons, and real PNGs at - 16/32/48/128 ship inside both archives - ([#371](https://git.eeqj.de/sneak/AutistMask/issues/371)). Neither manifest - had an `icons` block, so both browsers drew a generic puzzle piece — the first - thing the owner sees on every launch, and how a user tells a real extension - from a look-alike. The sizes `build.js` copies into each browser directory are - read out of the manifest that ships next to them rather than from a second - list, so a declared size `icons/` does not hold fails `make build`; - `script/lib/package.js` already resolves `.png` references, so an icon that - reached a manifest but not the archive fails packaging. The artwork is - original: a flat dark-navy rounded field with a teal triangular "A", drawn - from geometry and rasterised into PNG, nothing traced or downloaded. -- 2026-08-23: A persisted container whose ENTRIES were dereferenced unchecked no - longer reaches a `.map()` or a `.toLowerCase()` - ([#362](https://git.eeqj.de/sneak/AutistMask/issues/362)). `allowedSites` was - the worst shape available: a stored `{"0x…": "notalist"}` passed the gate, - rendered a completely healthy popup, and then threw inside `saveState()`'s - per-hostname merge, so every save from that moment on failed silently and the - user went on operating a wallet that was persisting nothing — measured as - `chrome.storage.local.set` never being called at all. `deniedSites` has the - same shape, `fraudContracts` the same class with a milder consequence, and the - sweep for the class turned up `selectedToken`, `rpcUrl` (handed whole to - `new JsonRpcProvider()`, which throws synchronously outside any `try`), the - entries of `viewData` (four restore branches gate on one truthy field and then - dereference an address), and `selectedWallet`/`selectedAddress` (a stored - `"map"` is TRUTHY against a real Array, so the restore guard does not - short-circuit). All of them are now floored in `src/shared/persistedState.js` - or refused by the per-branch guards in `src/popup/viewRouter.js`. Separately, - a save that fails is no longer swallowed: `onSaveFailure()` in - `src/shared/state.js` reports every failed save, awaited or not, and the popup - raises a persistent "NOT SAVED" banner. The hand-written per-field - justification in the header of `src/shared/stateSchema.js` — which had shipped - a false claim in three consecutive changes — is replaced by - `tests/persistedFieldContract.test.js`, one row per persisted field, each - proven by driving the real code with hostile values — and, for a field whose - only defence is that nothing dereferences it, by booting the real popup entry - point over that value onto every view the popup can reopen onto, since that is - the path this whole class of defect lives on. Each such field is driven at - both polarities — a value nothing writes is wrong-typed and so truthy, so a - falsy slot is driven too, or the field is proven unable to be falsy after the - floor. The claim is narrow and stated as such: no structural dereference on - the code paths a wholly-corrupted profile takes, which is not every path a - stored record takes — a pairing of values the four slots do not produce, a - view only forward navigation opens, anything behind a click, and everything a - healthy profile reaches are all undriven. Within that boundary the verdict is - unconditional, including a dereference that takes two corrupted fields at - once, since the assertion is on the combined boot and the per-field re-boot - can only decorate the message. A field with no row and a field that gains a - floor while its row still claims it has none also fail `make check`. -- 2026-08-23: A swap amount and the token it is counted in now always come from - the same hop, on both sides of the approval screen - ([#359](https://git.eeqj.de/sneak/AutistMask/issues/359) and - [#364](https://git.eeqj.de/sneak/AutistMask/issues/364), the output and input - halves of one gate, fixed as one unit). `src/shared/uniswap.js` gated the - token and the amount on truthiness and independently; an address is never - falsy once set but an amount of `0n` is, so a hop supplying a zero amount - fixed the token and left the amount open, and the next hop's figure was then - rendered against the first hop's token at that token's scale — 0.5 WETH shown - as `500000000000.0000 USDT`, and an earlier hop's `Min. received` shown for a - final leg that guarantees nothing. Both sides are now set as a pair through - explicit presence, a zero slippage floor reads `None (no minimum guaranteed)`, - and V4's `OPEN_DELTA` (an `amountIn` of zero, which `V4Router` reads as "swap - the whole open credit") reads `All available (V4 open delta)` instead of - `0.0000`. -- 2026-08-23: A swap whose input token the calldata never named is said to be - unknown instead of being called ETH - ([#357](https://git.eeqj.de/sneak/AutistMask/issues/357)), the twin on the - input side of [#353](https://git.eeqj.de/sneak/AutistMask/issues/353). A null - `inputToken` rendered as `Token In: ETH (native)` and titled the swap - `Swap ETH -> X`, asserting the user was paying native ETH when nothing in the - calldata said so. The null-means-ETH collapse is now gone from `tokenInfo()` - itself rather than guarded at each call site: null is refused, and native ETH - keeps arriving as the explicit zero address that `WRAP_ETH` and V4's - `Currency.wrap(address(0))` both use. -- 2026-08-23: A swap whose output token the calldata never named is said to be - unknown instead of being called ETH - ([#353](https://git.eeqj.de/sneak/AutistMask/issues/353)). `tokenInfo(null)` - answers `{symbol: "ETH", decimals: 18}`, and a V4 step could take the - `Min. received` figure while naming no output currency, so the approval screen - stated the wrong asset at the wrong scale. Null is not how V4 spells native - ETH: v4-core's `type Currency is address` wraps `address(0)` for it, which - reaches the decoder as the truthy string - `0x0000000000000000000000000000000000000000` and is named ETH there already. - `Token Out` now reads `Unknown (not named in the calldata)` and - `Min. received` falls to the base-unit refusal from - [#340](https://git.eeqj.de/sneak/AutistMask/issues/340). - -- 2026-08-23: The stored profile carries a schema version, and a record the - wallet cannot read produces a screen instead of a blank popup - ([#311](https://git.eeqj.de/sneak/AutistMask/issues/311)). `saveState()` and - `updateState()` both stamp `STATE_SCHEMA_VERSION` - (`src/shared/stateSchema.js`), and every read goes through - `assertStateUsable()` on the raw bytes before normalization gets a chance to - paper over them. Version 1 is the shape that shipped unversioned, so the - profile every existing install holds loads normally and is migrated in place - by being stamped on the first write — an upgrade shows nobody a wipe prompt - for a wallet that is fine. A record this build cannot vouch for is refused - instead: not normalized, not written back, not half-loaded. The popup shows - the new StateRecovery screen, which names the problem, exports the raw record - verbatim into the page (and downloads it where the browser allows), and offers - an erase behind a typed `ERASE MY WALLET` — both controls, because an export - with no reset leaves the user stuck and a reset with no export destroys the - only copy of possibly recoverable key material. The background refuses the - same record and answers dApps `-32007` — a code EIP-1474 leaves unassigned, - unlike `-32000`..`-32006` — with a message saying the saved data cannot be - read and that nothing was signed or sent, rather than the generic `-32603` - that every request used to get. Fields the gate deliberately does not check - produced the same blank popup on their own: `trackedTokens` and - `activeAddress` were floored on truthiness rather than on type, and - `trackedTokens`' ENTRIES and each address's `tokenBalances` were not floored - at all — `[1, 2]` is a list, and the dereference is `t.address.toLowerCase()` - one level below the container. All of them are type-checked now, entries - included, and the header of `src/shared/stateSchema.js` lists which fields of - the record get a type check and which get a `saved.x || default` or a verbatim - passthrough, rather than asserting a rule the module does not follow. - `networkById()` now throws on an id it does not know instead of quietly - answering mainnet, and the gate's key tests are all own-property tests: - `networkId` is an object key into `networkEndpoints`, so an unvalidated - `"__proto__"` used to set that map's prototype and drop the user's endpoint - silently. The three corrupt blobs from the issue drive the real popup entry - point in `tests/stateRecovery.test.js` and the real worker in - `tests/stateUnusableRpc.test.js`; each rendered nothing at all and answered - `-32603` before this. `src/popup/restorableViews.js` moved to - `src/shared/restorableViews.js`, since `persistedState.js` requires it and - that module is in the background bundle. - -- 2026-08-23: An explorer that reports no `decimals` for a token no longer has a - scale invented for it before storage - ([#349](https://git.eeqj.de/sneak/AutistMask/issues/349)). - `fetchTokenBalances()` did `parseInt(item.token.decimals || "18", 10)` on the - way in, so a token whose `decimals()` reverts was written to - `tokenBalances[].decimals` as a fabricated `18` that no reader could tell from - a real one. That is upstream of the resolve-or-refuse rule - ([#306](https://git.eeqj.de/sneak/AutistMask/issues/306), - [#340](https://git.eeqj.de/sneak/AutistMask/issues/340)): both approval paths - read this stored value as an authoritative source, so the guess walked past - refusals that were intact and simply never fired. The stored value is now the - explorer's own answer or `null`, and both the ERC-20 amount line and the swap - lines reach `unknownDecimalsAmount()` on it. The history list's token - transfers carried the same `|| "18"` and now state base units with the scale - unknown rather than a quantity. A holding whose scale nothing knows carries - `balance: null` — unknown, not zero — and the balance list, the USD total, the - Send screen and the confirmation screen each say so instead of printing - `0.0000` for money that is really there. The uint8 check is one shared - `toDecimals()` rather than three copies, and it answers `0` for a real scale - of zero: `|| "18"` collapsed that to eighteen, the trap of - [#246](https://git.eeqj.de/sneak/AutistMask/issues/246). Existing installs - hold `18`s that cannot be told apart retroactively; they display exactly as - they do today until the next balance refresh, which rewrites `tokenBalances` - wholesale and needs no user action. No `|| 18` or `?? 18` fallback remains - anywhere in `src/`; the literal `18`s that do remain are real data, not - defaults — 432 per-token `decimals: 18` entries in the bundled - `src/shared/tokenList.js`, and, outside that file, only native ETH's - protocol-defined scale in `src/shared/uniswap.js` and the fixed-point - comparison scale in `src/shared/txValidation.js`. `tokenBalances[].decimals` - is the explorer's answer alone and not the scale a screen renders at, so the - Send screen resolves through `resolveTokenDecimals()` like every other - consumer: reading the stored field raw carried a `null` into `estimateGas()` - for a bundled token such as WETH, which reported an unestimable network fee - and left Send disabled behind a message no retry could clear. Send resolves - with `wallets`, which adds the cross-address disagreement check the balance - list does not make, so the two can differ; where they do, the stored quantity - was computed at a scale Send has refused, and it is withdrawn with it. An - unknown scale is an unknown balance, and the user is told that rather than - that the fee could not be estimated. - -- 2026-08-23: The background no longer reads or writes the shared `state` - singleton ([#324](https://git.eeqj.de/sneak/AutistMask/issues/324)), which - also closes the cold-worker wrong-chain send - ([#320](https://git.eeqj.de/sneak/AutistMask/issues/320)). One in-memory copy - loaded once is the popup's lifetime, not the MV3 worker's: the worker is - killed when idle, nothing loaded state at module scope, and an unpopulated - read was answered out of `DEFAULT_STATE` in silence. Five defects traced to - that, and every point fix added a `loadState()` that created the next one — a - load detaches the objects an in-flight handler is holding. The background now - has its own storage layer (`src/background/state.js`): `getState()` for a - detached per-call read, `updateState()` for a queued read-modify-write. - `backgroundRefresh()` refreshes a private copy and applies the balances that - came back by address, so a wallet added, renamed or deleted during the round - trip survives. The transaction attempt takes its chain id and its endpoint - from one snapshot, so a committed chain switch can no longer move the endpoint - under an artifact already verified against the old chain. `getProvider()` now - REQUIRES the network id, which is what closes - [#320](https://git.eeqj.de/sneak/AutistMask/issues/320) at the shape rather - than at the call site. The prohibition is enforced by `build.js`, which fails - the build when esbuild's own metafile reports `src/shared/state.js` as an - input of either background bundle — the resolution the shipped bundle was - actually built from, so no specifier syntax and no resolution rule can slip - past it, and `make build` runs in CI. A bundled entry point under - `src/background/` with no line in the table fails the build too, so a second - worker entry point is protected by default rather than only if whoever adds it - knows the table exists. The assertion itself is pinned by - `tests/buildForbiddenInputs.test.js`, including every way its table can rot: a - key no bundled entry point matched, a forbidden module this build bundled - nowhere, and an entry that lists no modules (which would otherwise empty the - lint rule's forbidden set as well, and is refused at require time). Its bound - is that it is keyed by path, so a COPY of the singleton at another path is - outside it — loud for three of the five defects and silent for the other two; - the bounds are recorded in full where the table lives - (`script/lib/forbiddenBundleInputs.js`). An ESLint rule that walks the require - graph textually gives the same answer in the editor, before a full bundle; it - reads the same table, and it is fast feedback rather than the guarantee. The - shapes it catches are pinned by `tests/backgroundStateLintRule.test.js`, and - so are the two it misses — a computed specifier and a symlink — as asserted - non-reports, which the build fails on. Reading an unloaded singleton now - throws `StateNotLoadedError` instead of serving defaults. The - `chrome.storage.local` stubs in eight test files aliased instead of - structured-cloning, which could let an assertion pass on a build that never - wrote anything; every test that drives real persistence now goes through - `tests/support/storageStub.js`. -- 2026-08-23: A swap always names its output token - ([#346](https://git.eeqj.de/sneak/AutistMask/issues/346)). The `Token Out` - detail line in `src/shared/uniswap.js` was pushed only when a symbol was - known, so a swap whose output token is absent from the bundled list — every - newly listed token — showed a `Min. received` figure with nothing saying what - was being received. The line is now keyed on the token's address and falls - back to it when there is no symbol, exactly as the `Token In` line already - did. It composes with the unknown-scale refusal from - [#340](https://git.eeqj.de/sneak/AutistMask/issues/340): the address says - which token, the base-unit figure says how much and states that the scale is - unknown. -- 2026-08-23: The swap approval screen no longer guesses 18 decimals for a token - outside the bundled list - ([#340](https://git.eeqj.de/sneak/AutistMask/issues/340)). `tokenInfo()` in - `src/shared/uniswap.js` returned `decimals: 18` for any such token — the same - assumption [#306](https://git.eeqj.de/sneak/AutistMask/issues/306) removed - from the ERC-20 amount line — so a 1,000-unit swap of a 6-decimal token was - stated as `0.000000001`, and every newly listed token reached it. The swap's - `Amount` and `Min. received` lines now resolve the scale through - `resolveTokenDecimals()`, the same bundled-list-then-tracked-then-explorer - order the ERC-20 line uses, and where nothing knows it they render - `unknownDecimalsAmount()` — base units with the scale stated — instead of a - number. No new data source and no network call: the scale comes only from what - the wallet already holds. An unbounded permit is still shown as `Unlimited`. - `README.md` records the rule as a Display Consistency exception. -- 2026-08-23: A failed release build no longer leaves a loadable debug bundle in - `dist/` ([#333](https://git.eeqj.de/sneak/AutistMask/issues/333)). With - `AUTISTMASK_DEBUG=1` exported, `make build` compiled a debug bundle and failed - on it in `script/verify-build` — but the bundle stayed on disk, loadable, with - every wallet it creates using the publicly committed test recovery phrase. - Every step of `make build` now runs through `script/discard-dist-on-failure`, - which removes `dist/` when a step fails and says on stderr that it did and - why; a removal it cannot complete is reported just as loudly. - `make build-debug` is deliberately not wrapped: its output is not mistakable - for a release build and is the evidence of the failure. - `script/test-verify-build` asserts the state of `dist/` on disk after a - failing and a succeeding step, not just the exit status, and reads `make -n` - to check the wrapper is on the release path and only there. -- 2026-08-23: `README.md` and `script/verify-build`'s own comments now state the - emitted-tree guarantee at the width the code actually enforces - ([#331](https://git.eeqj.de/sneak/AutistMask/issues/331)). The tree walk is - `-type f -o -type l`, so the guarantee covers regular files and symlinks under - `dist/`; fifos, sockets, device nodes and empty directories are not checked, - because a build emits none of them, none can carry a shippable payload, and - `grep` on a fifo would hang rather than fail. The exclusion is deliberate and - unchanged — the README said "nothing under `dist/` that the build did not - write", which was broader than that. Documentation only; no executable line - changed. -- 2026-08-23: An amount below the 4-decimal display floor no longer reads as - zero on the approval screens - ([#322](https://git.eeqj.de/sneak/AutistMask/issues/322)). With the token's - true scale resolved, the 4-decimal truncation still printed a small amount as - `0.0000` — 1 base unit of an 18-decimal token, 500 of an 8-decimal one — so a - real transfer, allowance or swap was stated as nothing on the one screen whose - job is to say what is being authorized, and a swap's `Min. received` claimed - the user might receive nothing. Three copies of that truncation existed; they - now share `src/shared/amountDisplay.js`. Everything the approval and - confirmation screens render (`src/popup/views/approval.js`, - `src/shared/uniswap.js`) extends to the first significant digit when the - truncated figure would otherwise read as zero, keeping the amount in token - units rather than switching to base units mid-line. The history and balance - lists (`src/shared/transactions.js`) keep the unfloored rule, which is out of - scope by the issue's definition of done. `README.md`'s Display Consistency - section records the exception. -- 2026-08-23: The extension can be installed once and kept - ([#310](https://git.eeqj.de/sneak/AutistMask/issues/310)). There was no - packaging target anywhere, no artifact, and `manifest/chrome.json` carried no - `key` — so an unpacked Chrome load derived its extension id, and therefore its - `chrome.storage.local` partition, from the absolute checkout path: moving or - re-cloning the checkout presented an empty wallet with no error. The manifest - now carries a fixed `key` (public half only; the private half is a credential - and is not in this repo, and no target generates one into the tree), pinning - the id to `gipbhkogfopeahplcjhipkgpcimdpkip`. `make package` produces - `release/autistmask-chrome-.zip` and - `release/autistmask-firefox-.xpi` plus `SHA256SUMS`, - deterministically and via `make build` so the archives can only be made from a - `dist/` already verified against that build's receipt as a release build; - every path the manifests and the popup HTML reference is resolved and required - to be inside the archive, and the archive is read back off disk and compared - member by member — `dist/styles.css` sits at the `dist/` root outside both - browser directories and is reported as deliberately not shipped rather than - dropped by a glob. One version: `script/lib/version.js` fails the build when - `package.json` and the two manifests disagree, instead of reading from one of - them. `BUILD_COMMIT` now carries `-dirty` when the working tree does not match - `HEAD` (and `-unknown` when git cannot say), while the full hash behind the - About screen's commit link stays clean so the link still resolves. Two real - browser observations back it: `tests/e2e/storagePartition.js` loads the - extension from two different paths in one Chrome profile with and without - `key` and records what each does, and `tests/e2e/firefox/reinstall.js` - installs the packaged XPI in a real Firefox, creates a wallet, restarts the - browser on the same profile, adds the add-on again and decrypts the vault back - to the original recovery phrase — and then observes that an explicit uninstall - DESTROYS that storage, which is correct browser behaviour but means Remove is - irreversible for a wallet, now stated in README.md. Deliberately not done: AMO - signing, CRX packing, tagging and any upload — the Firefox artifact is - UNSIGNED and README.md now states that release Firefox and ESR refuse it, that - Developer Edition or an Unbranded build is required, and that a temporary - add-on does not survive a browser restart. -- 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 - duplicate xpub, so a user who had the phrase but not the password could - neither leave nor come back: the only way out was clearing extension storage - through browser internals, which takes every other wallet with it. - DeleteWallet now offers "I have lost my password", a screen that destroys the - wallet after the user types its name back — no password, because requiring one - to _discard_ a secret protects nobody. An attacker at the popup who wants the - wallet gone can uninstall the extension; the only person such a gate stopped - was the owner who forgot it. That was chosen over allowing a duplicate xpub to - re-encrypt in place: re-import would have had to be built three times over - (`hd` and `xprv` by xpub, `key` by address), would make the user retype the - recovery phrase into a live popup to change a password, and reaches no state - that delete-then-import does not already reach through `scanForAddresses()`. - Both routes share one `finishDelete()`, so the selection repair, the - site-permission cleanup and the `AUTISTMASK_ACTIVE_CHANGED` broadcast cannot - diverge between them, and the new screen is excluded from `RESTORABLE_VIEWS` — - a popup reopened by accident must not land on a button that erases key - material. AddWallet's password hint now says, per import mode, that the - password cannot be recovered or reset and what the only backup is; the hint - line reserves its height so switching tabs cannot move the password fields. - The test drives the real view against a `chrome.storage.local` stub that - structured-clones on both `set` and `get` and asserts against the read-back, - so it fails on the deletion of `saveState()` and not only on an in-memory - splice. -- 2026-08-20: `make build` can no longer hand back a debug build, and - `script/verify-build` can no longer be satisfied by bytes the build did not - produce ([#309](https://git.eeqj.de/sneak/AutistMask/issues/309)). The - verifier used to compute its expectation from `AUTISTMASK_DEBUG` in its own - environment, so an operator with that exported who ran the release target got - a debug bundle — every wallet it creates carrying the publicly committed test - phrase — certified green at exit 0. The expected mode is now the required - argument `--expect release|debug`, with no default and nothing read from the - environment, and the `Makefile` scrubs the flag from the verifier while - deliberately leaving it reaching the compiler, so that shell fails the build - loudly instead of quietly getting something other than what it asked for. - Provenance was the other half: the check was a marker grep over a file list - read back out of `dist/`, so a 26-byte file containing only - `autistmask-build-debug=off` verified `ok`, `manifest.json` and the content - script that runs on every page were never read at all, and an entire - hand-written `dist/` passed. `build.js` now records every file it emits, with - its sha256 and whether it is one of the bundles containing `constants.js`, - into a receipt whose path the `Makefile` makes fresh per invocation outside - the repo and deletes afterwards; `dist/constants-bundles.txt` is gone, and - `dist/` is cleared before a build so it holds only what that build wrote. The - standalone `make verify-build` target went with it: re-verifying a `dist/` - from the `dist/` itself is the thing that was broken. What this establishes is - narrow and stated as such in README.md — `dist/` is byte for byte the output - of the `build.js` run that just finished — and it is not signing, which is - [#310](https://git.eeqj.de/sneak/AutistMask/issues/310). - `script/test-verify-build` grew from 18 cases to 39, including one per - demonstrated bypass and the `make -n` read-back that proves the recipes pass - the mode as an argument. -- 2026-08-20: A hostile ERC-20 symbol no longer renders as live HTML in the - popup ([#307](https://git.eeqj.de/sneak/AutistMask/issues/307)). A token - symbol is whatever the contract's `symbol()` returns, the block explorer - passes it through unfiltered, and `balanceLine()` interpolated it into an - `innerHTML` string — so a token with the 1,000 holders the spam filter asks - for, airdropped to the victim, could paint a full-viewport cross-origin iframe - over the wallet's own UI, on the screens where the user types their password. - `escapeHtml` moved to `src/shared/html.js` as a pure string replace over `&`, - `<`, `>`, `"` and `'`: the old implementation round-tripped through a detached - element's `textContent`, which does not escape quotes, and it was already - being used inside `data-copy="..."`. Every interpolation into an `innerHTML` - string across `src/popup/views/` was audited, not just the reported one — the - transaction lists' direction label, the wallet name and ENS name in the Home - list, the `href` in the explorer link, and the confirmation screen's warning - line were all unescaped as well. Both manifests now declare - `default-src 'self'` with `frame-src 'none'`; the four directives that had to - stay looser than `'self'` are named and justified in the Content Security - Policy section of README.md, and `tests/manifest.test.js` pins the whole set - exactly. A display cap of 12 characters bounds the symbol, matching the bound - `lookupTokenInfo()` already applied on the contract-read path. Not repurposed - for any of this: `isSpoofedSymbol()`, which answers a different question and - would have been the wrong control. -- 2026-08-20: A page asking which chain the wallet is on is told the chain the - user is actually on ([#317](https://git.eeqj.de/sneak/AutistMask/issues/317)). - `eth_chainId` and `net_version` answered from `currentNetwork()`, which reads - the module-level `state` singleton that nothing populates at module scope, so - a service worker revived by the page's own message answered out of - `DEFAULT_STATE` and reported mainnet `0x1`/`1` to a user on Sepolia — a dApp - building its interaction for the wrong chain. Both now answer from - `getState()`, the per-call detached storage read the other read handlers use, - rather than from the singleton: these two are reachable by any page on every - provider init, and mutating the shared singleton on that path would detach the - wallet objects an in-flight `backgroundRefresh()` is mutating. The read side - of the background was audited with it: the remaining singleton reads are the - chain switch, the transaction verification path and `backgroundRefresh`, which - each already load, and everything else answers from storage per call through - `getState()`. One stale read is left named but unfixed, outside this issue's - scope: `handleSendTransaction` builds its provider with no network name, so - `getProvider()` falls back to the same unloaded singleton for ethers' static - network hint. -- 2026-08-20: The dApp approval screen no longer shows a token transfer it - cannot scale as `0.0000` - ([#306](https://git.eeqj.de/sneak/AutistMask/issues/306)). `decodeCalldata` - read decimals from the 512-entry bundled token list alone and fell back to 18, - so every token outside it — most of them, including anything the user added by - contract address — was displayed at the wrong scale: a `transfer` of 5,000 - units of a 6-decimal token read as `0.0000`, and a user who reads zero - confirms the drain. The new `src/shared/approvalAmount.js` resolves the scale - from the bundled list, then `state.trackedTokens`, then the decimals the block - explorer already reported in `addr.tokenBalances`, and refuses one the - explorer's own entries disagree about. Where no source knows it, the amount - line is not formatted at all: it shows the base-unit integer and states that - the scale is unknown, for `approve` as well as `transfer`. An unbounded - allowance still reads `Unlimited`, which needs no scale. -- 2026-08-20: A web page can no longer switch the wallet's chain, and switching - no longer destroys the user's endpoints - ([#308](https://git.eeqj.de/sneak/AutistMask/issues/308)). - `wallet_switchEthereumChain` was answered for any origin at all, with no - connection check and no prompt: any page could clear the `[TESTNET]` banner - under a user who believed they were on Sepolia. It now takes the same - `allowedSites`/`connectedSites` gate the signing methods take, ahead of the - same-chain and unsupported-chain answers, and refuses an unconnected origin - with `4100`. The switch itself also overwrote `state.rpcUrl` and - `state.blockscoutUrl` with the network defaults, so a user running their own - node lost that url permanently and silently to a public endpoint that then - sees every address they hold. Endpoints are now remembered per network in - `state.networkEndpoints`, snapshotted from the network being left and restored - for the network being entered; `state.rpcUrl` stays the live value for the - active network, so no reader changed. A profile written before the map existed - has its stored pair adopted for the network it was stored under, and loses - nothing. The handler now loads state before it switches - ([#316](https://git.eeqj.de/sneak/AutistMask/issues/316)): the service worker - populates nothing at module scope, so a worker revived by the page's own - message held `DEFAULT_STATE`, and the switch persisted every field of it — - wiping every wallet, every site approval and every tracked token from storage - along with the endpoint. -- 2026-08-20: The wallet's own ERC-20 send signs the amount it displayed - ([#305](https://git.eeqj.de/sneak/AutistMask/issues/305)). The confirmation - screen renders from the block explorer's cached decimals; the transfer was - encoded from `decimals()` read off the contract at signing time, and nothing - compared the two, so a token whose on-chain scale disagreed — an upgradeable - or proxy token, a stale explorer entry, a compromised Blockscout — signed an - amount that was never on screen, off by a power of ten per decimal place of - disagreement. The scale is now carried forward on the pending transaction from - the same balance entry the screen's amount, balance and symbol come from, and - the contract's answer is read at signing time only to be compared with it: a - disagreement is a refusal naming both numbers, never a preference for either - (`src/shared/transferAmount.js`, the `confirmTx` counterpart to - `approvalVerify.js`). The gas estimate encodes from the same carried value and - no longer reads `decimals()` at all. Nothing in the e2e suite had ever clicked - `#btn-confirm-send`, which is how this shipped: the popup's own Send → - ConfirmTx → Sign & Send → WaitTx path now runs end to end to a broadcast, with - the `transfer()` amount decoded out of the raw signed bytes and asserted - against what the screen displayed, and a companion case where the contract - starts answering a different scale after the screen was built and nothing - reaches the RPC. Reverting only the signing-side comparison turns that second - case red and leaves the other 53 green. -- 2026-08-17: The Settings screen is driven in a browser, and every element id - the popup looks up is checked statically. Nothing exercised Settings in the - e2e suite, and jest runs with no DOM, so the densest run of `$("...")` lookups - in the codebase was unverified at runtime. Seven new cases in - `tests/e2e/run.js` reach Settings, assert the About well and the wallet list - were actually written, assert the four Token Spam Protection checkboxes are - real checkboxes defaulted on, and assert the theme and network selectors offer - the choices `src/shared/networks.js` and `index.html` define. The selectors - are then driven to `dark` and `sepolia` — neither is the first `