The wallet list was the reported case. An address there shared one row with the [info] and [x] controls, which took about a third of the width off it, so all 42 characters folded onto a second line. A folded address is not a cosmetic problem: it turns one string the user is meant to compare against a known value into two shorter ones, which is the shape an address-poisoning attack wants. The fix is the layout, not the CSS. renderAddressHtml() -- the single renderer behind every common view that shows an address -- now emits an identity strip (colour dot, wallet title, explorer link, with the ENS name below it) and then the address alone on a full-width row. The wallet list moves [info] and [x] up onto the "Address N" line, which was empty to its right. The transaction rows on Home, the address screen and the token screen carried a truncateMiddle()d counterparty squeezed in beside the amount; they now name it on the amount line where we know it, and carry the whole address on the row below. With the row to itself, an address fits at every nesting depth the popup uses, including the transaction detail wells, which are the narrowest containers it has. .am-address holds nowrap so it cannot fold again, and overflow-x so that if it ever does not fit -- wider glyphs, a zoom -- the user can still reach the last character rather than having it clipped away by #app's overflow-x-hidden with nothing to say it happened. No caller passes maxLen any more, so the 32-character floor that lived in those call sites moved into renderAddressHtml(). truncateMiddle() and its 10-character cap are unchanged: the guarantee has to outlive having no current callers. tests/e2e measures it in a real Chromium rather than asserting on markup: whether an address wrapped is a question about glyph advances and the width of the box it landed in, and nothing in the HTML answers it. Every rendered address is checked for being whole, occupying one line box, fitting its row and ending inside the popup's content box, with the document itself not scrolling sideways -- across Home with a two-address wallet, the address, token, receive, send and transaction detail screens, the confirmation screen and the dApp transaction prompt.
1088 lines
76 KiB
Markdown
1088 lines
76 KiB
Markdown
# Workflow
|
||
|
||
- `git pull` `next` and cut a branch from it — one branch per issue, named
|
||
`issue-<N>-<slug>`. Never branch from `main`.
|
||
- Do the work as one commit whose title ends with ` (closes #N)`, with the
|
||
`TODO.md` update in that same commit.
|
||
- Move Next Step to the top of Completed Steps; move the top item of Future
|
||
Steps into Next Step.
|
||
- Run `make fmt`, then `make check`. A feature branch may be red; `next` and
|
||
`main` may not.
|
||
- Rebase onto current `next` immediately before pushing — other branches land on
|
||
`next` continuously — and re-run `make check` after resolving, because a clean
|
||
textual merge can still break the build.
|
||
- Push the branch and open one PR per issue with base `next`. Never base `main`.
|
||
- An independent reviewer who did not write the change gates the merge. On a
|
||
passed review the PR is squash-merged into `next`.
|
||
- `next` is the branch for the next milestone. It is kept green and mergeable to
|
||
`main` at any moment, without notice.
|
||
- `main` receives exactly one PR per milestone, from `next`. Releases are tagged
|
||
from `main`.
|
||
|
||
# Status
|
||
|
||
pre-1.0, working towards the 1.0.0 milestone. Tagged v0.1.0 on 2026-02-27. The
|
||
milestone is in flight on `next`; its `next` -> `main` PR is
|
||
[#190](https://git.eeqj.de/sneak/AutistMask/pulls/190). `make check` verified
|
||
green on `next` at `e9fa8be` on 2026-08-10, and `make build` produces
|
||
`dist/chrome/` and `dist/firefox/`, verified against the build's own receipt to
|
||
hold exactly the regular files and symlinks that build emitted, with `DEBUG`
|
||
compiled off.
|
||
|
||
The backlog lives on the
|
||
[Gitea tracker](https://git.eeqj.de/sneak/AutistMask/issues), which is
|
||
authoritative; this file does not duplicate it. Full policy file set present.
|
||
Real-browser end-to-end suites (`make test-e2e` for Chrome,
|
||
`make test-e2e-firefox` for Firefox) sit alongside `make check`, which now does
|
||
static analysis as well as formatting, and `.gitea/workflows/e2e.yml` runs both
|
||
of them on every push.
|
||
|
||
# Next Step
|
||
|
||
Pre-1.0 security review of the extension (key handling, DEBUG mode policy, RPC
|
||
input validation) before any 1.0rc tag. Individual filed issues are parts of it,
|
||
but the review is broader than any of them.
|
||
|
||
# Completed Steps
|
||
|
||
- 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-<version>.zip` and
|
||
`release/autistmask-firefox-<version>.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 `<option>`, so
|
||
neither can be read back from the markup with no JavaScript involved — and
|
||
reasserted after a popup reopen before being restored the same way, and one
|
||
spam filter is toggled off and back on across a reopen each way. Those round
|
||
trips run the change handler, `saveState()`, `loadState()` and the
|
||
`init()`/`show()` assignments rather than just looking at the screen. `show()`
|
||
no longer guards its `settings-network` lookup with `if (networkSelect)`: a
|
||
missing element must fail loudly, which is the whole failure mode this unit
|
||
exists to catch. Each group records a coverage key and a final case demands
|
||
the exact set, so a shortened or skipped section reddens the run instead of
|
||
shrinking it. `tests/popupElementIds.test.js` is the general half and runs in
|
||
`make check` with no browser: every literal id reached through `$()`,
|
||
`document.getElementById()`, `showError()`/`hideError()` and `showView()` must
|
||
exist in `src/popup/index.html`, which no id in `index.html` may define twice.
|
||
Demonstrated on four deliberate breaks — a typo'd id (both halves red), a
|
||
handler bound to the wrong but existing element (only the functional e2e case
|
||
red), a typo in a view no browser suite opens (only the static guard red), and
|
||
the deletion of both persisted-value assignments in `settings.js` (only the
|
||
selector round-trip case red)
|
||
([#229](https://git.eeqj.de/sneak/AutistMask/issues/229)).
|
||
- 2026-08-17: The phishing blocklist is vendored at build time and censored, and
|
||
the runtime fetch is gone
|
||
([#219](https://git.eeqj.de/sneak/AutistMask/issues/219)).
|
||
`script/vendor-blocklist` fetches upstream at a pinned commit, verifies the
|
||
sha256 of the bytes it was served, and writes
|
||
`src/shared/phishingBlocklist.json` as truncated sha256 digests rather than
|
||
domain names — which is what removes the competitor's name from a list that
|
||
carried it 6,475 times, without dropping a single one of those domains.
|
||
`script/check-censored` runs in `make check` and again against `dist/` at the
|
||
end of every build, each permitted occurrence scoped to the one path allowed
|
||
to carry it; the name now appears only in the vendoring script, which defines
|
||
it once, in the provider-shim identifiers in `src/content/inpage.js`, and in
|
||
one ERC-20's on-chain name in `src/shared/tokenList.js`. Removing the fetch
|
||
retired the delta, the persistence and the 24-hour alarm from
|
||
[#158](https://git.eeqj.de/sneak/AutistMask/issues/158), and retired alarms
|
||
are now cleared rather than left running on existing installs. Two
|
||
consequences, both deliberate: the list no longer self-updates, so it is as
|
||
fresh as the last vendoring run that was released; and re-vendoring from
|
||
current upstream took it from 231,357 stale entries to 105,721 current ones,
|
||
because upstream prunes and the vendored snapshot never did. `dist/` fell from
|
||
18.9 MB to 8.9 MB. The e2e suite now drives the warning end to end from a real
|
||
blocklisted origin, and its service-worker interception canary has a new
|
||
anchor, because the startup fetch it used to watch for no longer exists.
|
||
- 2026-08-17: One wording for an empty password field on every screen that asks
|
||
for one. The private key export screen said "Password is required." where the
|
||
other five say "Please enter your password.", the same one-condition-two-
|
||
wordings split that [#172](https://git.eeqj.de/sneak/AutistMask/issues/172)
|
||
closed for a rejected password. Strings only, no behaviour change.
|
||
`tests/passwordMessages.test.js` now pins the empty-field guard per call site
|
||
as well as the decrypt handler, anchored on the `decryptWithPassword` sites so
|
||
the wallet-creation screen — where an empty field means a password being
|
||
chosen, a different condition — stays out of the set. Every error container
|
||
measured at a 360px viewport in the pinned Playwright container: the export
|
||
screen's container holds at 20px with the following section at the same offset
|
||
for the old string, the new string and the empty reserved state
|
||
([#265](https://git.eeqj.de/sneak/AutistMask/issues/265)).
|
||
- 2026-08-17: One shared extension-API module,
|
||
[`src/shared/browserApi.js`](src/shared/browserApi.js), is the only place in
|
||
the tree that names `browser` or `chrome`. Every call site returns a promise;
|
||
`runtime.lastError` is gone. The same commit gives the Firefox suite the four
|
||
dApp round trips — `eth_requestAccounts`, `personal_sign`,
|
||
`eth_sendTransaction` and a closed approval window rejecting with EIP-1193
|
||
4001 — against a page and a JSON-RPC node served from loopback, which survives
|
||
`--network none`. **The premise of
|
||
[#153](https://git.eeqj.de/sneak/AutistMask/issues/153) does not survive that
|
||
harness**: Firefox's `browser.*` honours a trailing Chrome-style callback and
|
||
populates `runtime.lastError`, both measured directly on Firefox 153.0.3, and
|
||
all four flows pass against the unconverted code. What landed is a uniformity
|
||
and coverage change, not a repair of a broken target. `storageGet()` and
|
||
`storageSet()` **reject** where `storage.local` is absent rather than
|
||
resolving `{}` and a no-op write — they carry the wallet, and defaulting would
|
||
read an existing wallet back as none. The one caller that genuinely degraded,
|
||
[`src/shared/phishingDomains.js`](src/shared/phishingDomains.js), took
|
||
`storageLocal()` directly and kept its own null check; it stores nothing at
|
||
all as of [#219](https://git.eeqj.de/sneak/AutistMask/issues/219) above.
|
||
- 2026-08-17: An address total no longer reports `$0.00` for holdings it cannot
|
||
price. Prices exist for the top 25 tokens only, so the priced-only sum was
|
||
printed as the total and an address holding nothing but unpriced ERC-20s was
|
||
shown as worth nothing — directly under "This address holds a balance." on the
|
||
address-removal confirmation. `getAddressValue()` in `src/shared/prices.js`
|
||
now returns `{ usd, partial }`, keeping worth-zero and worth-an-unknown-amount
|
||
apart the way an absent `holders_count` is kept apart from a count of zero,
|
||
and every screen renders it through the one `formatAddressTotal()`: the figure
|
||
when it covers everything, the figure marked `plus unpriced tokens` when it
|
||
covers part, and `Total: unpriced tokens only` when it would cover nothing.
|
||
Home, AddressDetail and the removal confirmation all read it, and
|
||
`getWalletValue()`/`getTotalValue()` carry `partial` up. Covered by
|
||
`tests/addressValue.test.js` — the only-unpriced, genuinely-zero and
|
||
fully-priced cases at the helper and at both call sites that return their
|
||
markup — demonstrated failing first
|
||
([#261](https://git.eeqj.de/sneak/AutistMask/issues/261)).
|
||
- 2026-08-17: `README.md` no longer advertises a defect the wallet does not
|
||
have. The End-to-End Tests section listed the EIP-1193 code being dropped in
|
||
the last hop into the page as a standing limit of the dApp coverage; that
|
||
stopped being true when
|
||
[#274](https://git.eeqj.de/sneak/AutistMask/issues/274) landed and did not
|
||
touch the README. The paragraph is deleted and the two remaining limits — the
|
||
stubbed RPC and the unobservable toolbar popup — were checked against the
|
||
current `src/content/inpage.js` and `tests/e2e/` and left as they are
|
||
([#285](https://git.eeqj.de/sneak/AutistMask/issues/285)).
|
||
- 2026-08-17: One transaction approval at a time. Populating in the background
|
||
before the window opens is what makes the displayed object the verified
|
||
object, and it also fixes the nonce: two `eth_sendTransaction` calls populated
|
||
concurrently took the same nonce from a node that had seen neither broadcast,
|
||
and the second could then never be sent, because the only way to give it a
|
||
fresh nonce is to populate it again after the user has read the old one off
|
||
the screen. A second request is now refused with EIP-1193 `-32002` while one
|
||
is unanswered — the slot is taken immediately before population, after the
|
||
authorization checks, so no second nonce is allocated, no second window opens,
|
||
and a page the wallet refuses anyway cannot hold the slot against the
|
||
connected site. The slot is freed at `settleApproval()`, the single point an
|
||
approval is retired, so every path that ends an approval ends the hold with
|
||
it; an approval whose window is gone and whose attempt has failed is settled
|
||
there rather than left waiting on a window that no longer exists, and an
|
||
approval window that could not be opened at all is answered with `-32603`
|
||
instead of holding the page's promise open. Signature approvals are not gated,
|
||
consuming no nonce. A collision that does happen is also reported accurately
|
||
now: a broadcast the node refused for the nonce, and an approval carrying a
|
||
nonce this worker has already broadcast for that address on that chain (caught
|
||
before the node is asked at all), both say the transaction did not reach the
|
||
network and to send it again, instead of warning that it may have sent. The
|
||
record is keyed by chain as well as address, because nonce spaces are per
|
||
chain and low nonces overlap across them. `already known` deliberately keeps
|
||
the ambiguous wording, because a node that says it has the transaction has it
|
||
([#271](https://git.eeqj.de/sneak/AutistMask/issues/271)).
|
||
- 2026-08-14: The parts of the
|
||
[#150](https://git.eeqj.de/sneak/AutistMask/issues/150) and
|
||
[#151](https://git.eeqj.de/sneak/AutistMask/issues/151) definition of done the
|
||
e2e suite did not cover are asserted. It had only shown that the two screens
|
||
open without throwing. Now: the Add Token round trip leaves the navigation
|
||
stack exactly as it found it, read out of extension storage rather than
|
||
inferred from which screen is up, so an orphaned entry — the second-order
|
||
damage of #150 — is caught where it happens rather than one Back press later;
|
||
a common-token quick-pick puts its contract address in the field; the native
|
||
ETH detail path renders with its own type, value and raw quantity and with the
|
||
token contract row still hidden, against a new `seedNativeTransfer` fixture,
|
||
since the normal-transactions endpoint answered `[]` unconditionally and there
|
||
was no non-ERC-20 row to open; and tapping the token contract address puts it
|
||
on the real clipboard, read back after a sentinel write. Each of the four was
|
||
demonstrated failing against a deliberately broken build
|
||
([#188](https://git.eeqj.de/sneak/AutistMask/issues/188)).
|
||
- 2026-08-14: `make check` does static analysis. `script/lint` ran
|
||
`prettier --check .`, byte-identical to `script/fmt-check`, so a wallet with
|
||
two shipped used-but-not-imported crashes behind it was green. ESLint is now
|
||
pinned in `package.json` with `@eslint/js` recommended as the base, flat
|
||
config in `eslint.config.js`, `no-undef` and `no-unused-vars` error-level, and
|
||
globals declared per tree — browser for the popup and content scripts, service
|
||
worker for `src/background/` and `src/shared/`, jest for `tests/`, node for
|
||
`build.js`. It found 41 unused bindings and 53 undefined identifiers; all are
|
||
fixed, and dropping a call to an unimported `foo()` into any `src/` file fails
|
||
`make lint`. Linting is also containerized now: `script/lint` builds the
|
||
Dockerfile's new `lint` stage, so the ESLint that decides whether this repo is
|
||
green is the pinned one and not the host's. The lint stage roughly doubles the
|
||
image build, so `script/test`'s hard timeout is now a bound on a hung suite
|
||
rather than a wall-clock budget: 30s on the host, where the suite runs in
|
||
about 8s, and `AUTISTMASK_TEST_TIMEOUT` raises it inside the image, where a
|
||
cold build pays install and contention costs the policy budget never described
|
||
([#152](https://git.eeqj.de/sneak/AutistMask/issues/152)).
|
||
- 2026-08-14: CI runs the browser end-to-end suites. `.gitea/workflows/e2e.yml`
|
||
runs `script/test-e2e` and `script/test-e2e-firefox` as two jobs on every
|
||
push, separate from `check`, so `make check` and its 20-second `make test` cap
|
||
are untouched. Every browser-level guarantee in this repo — the WASM-under-CSP
|
||
check, the recovery-phrase and private-key DOM wipes, the ConfirmTx spend
|
||
gate, the dApp approval round trips — was enforced only when a human
|
||
remembered to run it by hand. The suites could not run on the runner as they
|
||
stood: the runner executes a job in a container against the host's docker
|
||
daemon, so `docker run -v "$PWD:/work"` mounts an empty directory (measured),
|
||
and the runner image's node cannot install this repo's dependencies. Both
|
||
suites now ship the repo to the daemon as a build context and build the
|
||
extension inside the pinned image, so docker is the only prerequisite on a
|
||
runner or a laptop, and both run the image by ID rather than by tag so
|
||
concurrent clones cannot swap it. The jobs report rather than gate — this repo
|
||
configures no branch protection, and the Chrome suite is measurably flaky
|
||
under load, filed as [#287](https://git.eeqj.de/sneak/AutistMask/issues/287)
|
||
rather than papered over
|
||
([#259](https://git.eeqj.de/sneak/AutistMask/issues/259)).
|
||
- 2026-08-14: A background message handler that throws now rejects the page
|
||
instead of hanging it. `handleRpc(...).then(sendResponse)` had no `.catch()`,
|
||
and `sendResponse` is the only thing that settles the dApp's
|
||
`window.ethereum.request()` promise — so any throw inside `handleRpc` left
|
||
that promise pending forever, with no error and no timeout, indistinguishable
|
||
from a slow wallet. It now answers `{ code: -32603, message }` (the JSON-RPC
|
||
internal error EIP-1474 defines and EIP-1193 defers to; no EIP-1193 4xxx code
|
||
describes "the wallet broke" and none was invented) and logs the method and
|
||
the throw to the background console rather than swallowing them. The two async
|
||
IIFEs behind `AUTISTMASK_TX_RESPONSE` and `AUTISTMASK_SIGN_RESPONSE` were the
|
||
same shape one level down — every statement inside a `try`, but a throw out of
|
||
a `catch` block escaping unhandled — and each got a last-resort `.catch()`
|
||
settling the approval through `settleApproval()` and answering the popup. The
|
||
transaction one tracks which phase it escaped from and reports that, so an
|
||
escape before `broadcastTransaction()` says the request is gone rather than
|
||
that it may still have reached the network. Every other handler on the path is
|
||
synchronous. All four are driven by real failures — a rejecting storage read,
|
||
and a failure classifier that throws while classifying a genuine verification
|
||
or broadcast failure — and were demonstrated failing first, the RPC one with
|
||
`sendResponse` at zero calls
|
||
([#280](https://git.eeqj.de/sneak/AutistMask/issues/280)).
|
||
- 2026-08-14: Approving a site connection is no longer a race against the popup
|
||
closing. The decision now rides the approval port the popup already holds,
|
||
which is the same channel the close disconnects, so it is delivered ahead of
|
||
that disconnect however fast the teardown is; `windows.onRemoved` no longer
|
||
decides a site approval whose port is connected, since that event is ordered
|
||
against nothing either. Rejecting and closing without deciding both still
|
||
report a rejection, and the popup delays its own close by nothing. The e2e
|
||
harness's deferred-`window.close()` accommodation is gone with it, so the two
|
||
site-prompt tests now drive the shipped decide-then-close in a real Chromium;
|
||
against the unfixed code the approval came back to the page as
|
||
`{"settled":"rejected","code":4001}`
|
||
([#275](https://git.eeqj.de/sneak/AutistMask/issues/275)).
|
||
- 2026-08-12: EIP-1193 error codes now reach the page. `src/content/inpage.js`
|
||
rebuilt every failure as `new Error(error.message)`, so the code the
|
||
background produced and the content script relayed intact was dropped in the
|
||
last hop and a dApp checking `err.code === 4001` saw `undefined` — a wallet
|
||
the user deliberately declined was indistinguishable from one that broke. The
|
||
provider now rejects with a `ProviderRpcError` carrying `code` and, where the
|
||
boundary sent one, `data`, passed through verbatim rather than matched against
|
||
a list, so 4001, 4100 and 4902 all arrive and a future code needs no edit
|
||
here. An error the background sent with no code stays a plain `Error` with no
|
||
`code` property, and `message` is unchanged in every case. All four request
|
||
entry points (`request`, `enable`, `send`, `sendAsync`) are covered by
|
||
`tests/inpageErrors.test.js`, and the e2e probe that printed the missing code
|
||
now requires it on the page's Error as well as on the wire, for all four
|
||
rejected flows ([#274](https://git.eeqj.de/sneak/AutistMask/issues/274)).
|
||
- 2026-08-12: "Back" now renders the screen it lands on instead of only unhiding
|
||
it. A reopened popup renders the wallet list and the one screen it restores
|
||
onto, so every screen further down the stack was still the blank template from
|
||
`index.html`, and Back walked straight onto it — an empty address, no
|
||
balances, no QR code. The Back path now goes through the same per-view
|
||
dispatch and data guards as the restore (`src/popup/viewRouter.js`, shared
|
||
with `restoreView()`), falling back to Home when the state the target would
|
||
render is gone. It renders only a view this page load has not rendered yet:
|
||
`viewRouter.js` records every view that reaches `showView()`, which is where
|
||
forward navigation and the restore both end, so Back onto a view already on
|
||
the page unhides it and nothing more. That is what keeps a second render from
|
||
re-fetching and overwriting what the view holds — an unsaved edit in Settings,
|
||
a transaction list already loaded. Home is the exception and is always
|
||
re-rendered, as it was before. Covered by unit tests on the real `goBack()`
|
||
and by three end-to-end cases against the real popup, each demonstrated
|
||
failing on the unfixed build
|
||
([#268](https://git.eeqj.de/sneak/AutistMask/issues/268)).
|
||
- 2026-08-12: `KNOWN_SYMBOLS` now maps a symbol to the set of contract addresses
|
||
that bear it, not to one of them. A ticker is not unique: seven of the 512
|
||
bundled tokens — `FRAX`, `REUSD`, `TON`, `EURE`, `MSUSD`, `MUSD` and `JPYC` —
|
||
share a symbol with another bundled entry at a different real contract, and
|
||
the table, built from the list first-wins, kept only the earlier one. The
|
||
other seven were judged spoofs of their own symbol at their own address and
|
||
hidden from the balance list, the history and the send selector, so a holder
|
||
could not spend them. Both contracts of each pair come from the same CoinGecko
|
||
fetch of 2026-02-27, so neither was stale and neither was dropped.
|
||
`isSpoofedSymbol()` asks set membership instead of equality, which does not
|
||
loosen the rule — a contract outside the set is still a spoof — and a test now
|
||
walks `TOKENS` asserting no bundled token is filtered at its own address,
|
||
which is the walk the suite lacked
|
||
([#276](https://git.eeqj.de/sneak/AutistMask/issues/276)).
|
||
- 2026-08-12: The dApp approval round trips are driven end to end in the
|
||
browser. A test page served by the harness speaks EIP-1193 to the real inpage
|
||
provider through the real content script, background worker and approval popup
|
||
for `eth_requestAccounts`, `personal_sign`, `eth_signTypedData_v4` and
|
||
`eth_sendTransaction`. Every signature is recovered and compared against the
|
||
active address, the transaction is checked against the bytes handed to the
|
||
stubbed RPC, each rejection must reach the page as a rejection, and the
|
||
password must appear in no message the approval window sends — the assertion
|
||
that gives [#157](https://git.eeqj.de/sneak/AutistMask/issues/157) a permanent
|
||
floor. This does not discharge a real dApp with real funds against mainnet
|
||
([#183](https://git.eeqj.de/sneak/AutistMask/issues/183)).
|
||
- 2026-08-12: The known-symbol spoof rule now judges the symbol a user actually
|
||
sees. `isSpoofedSymbol()` normalizes before the lookup — NFKC, then every
|
||
character that paints nothing removed (the format and default-ignorable
|
||
characters, plus U+007F), then trimmed — so `" ETH "`, a no-break space, a
|
||
zero-width space, a Hangul filler, a variation selector, a DELETE and a
|
||
fullwidth `ETH` are all caught on the balance list, the history and the
|
||
send selector at once. Confusables that are distinct letters (Cyrillic `Е`),
|
||
bidi reordering and the visible C0/C1 controls — which measure 48.00px, a box,
|
||
in the pinned e2e Chromium where an invisible prefix measures 32.00px — stay
|
||
knowingly open and are asserted as open in the suite. No bundled symbol
|
||
contains whitespace or a non-ASCII character, so nothing legitimate is newly
|
||
filtered; the balance list's token-type gate also became case-insensitive,
|
||
which no longer drops a real holding if an explorer writes `erc-20`
|
||
([#260](https://git.eeqj.de/sneak/AutistMask/issues/260)).
|
||
- 2026-08-12: A containerized Firefox end-to-end harness
|
||
(`make test-e2e-firefox`) drives the real popup in a real Firefox with the MV2
|
||
build installed as a temporary add-on. Zero npm dependencies — a WebDriver
|
||
client over `fetch` against geckodriver — with `node`, Firefox 153.0.3 and
|
||
geckodriver 0.36.0 all pinned by digest. Uncaught errors are read from the
|
||
privileged console service in Marionette's chrome context, because BiDi
|
||
`log.entryAdded` reports nothing at all for extension pages; each drain reads
|
||
and clears the console in one chrome round trip, so no error is destroyed
|
||
unread by the drain itself, and errors logged during add-on install and
|
||
background startup are folded into step 1 instead of being cleared. The two
|
||
measured limits are documented rather than claimed away: the console ring
|
||
buffer holds 250 messages (a clean run peaks at 4), and the drained window
|
||
ends ≈1.5s after the last step returns. Demonstrated discriminating by exiting
|
||
1 on a `throw` at the top of `src/background/index.js`, on a build with one
|
||
import removed, on a `setTimeout` throw whose UI assertions all pass, on an
|
||
unhandled `Promise.reject` and on an undefined identifier in `home.js`, and 0
|
||
on the branch as it stands
|
||
([#184](https://git.eeqj.de/sneak/AutistMask/issues/184)).
|
||
- 2026-08-12: The transaction a dApp asks for is now populated in the background
|
||
before the approval window opens, so the object the user is shown is the
|
||
object the signed artifact is verified against — nonce, gas limit and every
|
||
fee field are compared exactly instead of being left to the ceilings, which
|
||
stay as a backstop against what a lying RPC node can talk the wallet into
|
||
displaying. The approval also pins the address it was raised for, so an
|
||
address switch between approval and signing refuses rather than signing from
|
||
an account the screen never named, and a request naming an address that is not
|
||
the active one is refused outright. The approval screen now shows the fee, gas
|
||
limit, network and nonce it vouches for
|
||
([#216](https://git.eeqj.de/sneak/AutistMask/issues/216)).
|
||
- 2026-08-12: The restored navigation stack is filtered against
|
||
`RESTORABLE_VIEWS` on load, truncated at the first entry the popup would not
|
||
render so that every surviving entry keeps the Back target it had. Back after
|
||
reopening can no longer land on a view the popup declined to restore, such as
|
||
`export-privkey` or `show-phrase`
|
||
([#224](https://git.eeqj.de/sneak/AutistMask/issues/224)). Restorable views in
|
||
the stack are still unhidden without being re-rendered; that is tracked
|
||
separately in ([#268](https://git.eeqj.de/sneak/AutistMask/issues/268)).
|
||
- 2026-08-12: One wording for a rejected password on every screen that asks for
|
||
one — the send confirmation and the delete-wallet confirmation no longer say
|
||
"Wrong password." (a fragment, which `RULES.md` Language & Labeling forbids)
|
||
and the two reveal screens no longer say "not correct", so all five
|
||
`decryptWithPassword` call sites now show the sentence the dApp approval paths
|
||
introduced. Strings only, no behaviour change, and each error container
|
||
measured at a 360px viewport in the pinned Playwright container
|
||
([#172](https://git.eeqj.de/sneak/AutistMask/issues/172)).
|
||
- 2026-08-12: Closed the empty-array hole in the end-to-end unstubbed-request
|
||
guard. `batch.every()` is vacuously true on `[]`, so a POST with body `[]` was
|
||
answered `200 []` instead of failing the suite; the guard now rejects an empty
|
||
batch, demonstrated green-before/red-after with a throwaway probe. The comment
|
||
claiming `postData()` returns `null` for undecodable bodies was corrected to
|
||
the two real paths — an absent or empty body decodes to `null`, a binary body
|
||
decodes lossily into invalid JSON
|
||
([#187](https://git.eeqj.de/sneak/AutistMask/issues/187)).
|
||
- 2026-08-12: The transaction confirmation screen has browser coverage. The
|
||
end-to-end suite reaches ConfirmTx for both the native ETH and the ERC-20 path
|
||
off a funded-balance fixture, and asserts the pending, funded, over-balance
|
||
and estimate-failed states, the fee block quoting the estimate and the reserve
|
||
separately, and a constant view height across every one of those transitions.
|
||
The load-bearing assertion is that the spend gate reads the reserve and not
|
||
the displayed estimate: swapping the two fails the suite
|
||
([#238](https://git.eeqj.de/sneak/AutistMask/issues/238)).
|
||
- 2026-08-12: The dust threshold field now explains a rejection instead of
|
||
snapping back in silence, with the parse in a pure, unit-tested module that
|
||
accepts plain decimal digits only — hex and exponent notation are refused
|
||
rather than read as 16 and 1000
|
||
([#233](https://git.eeqj.de/sneak/AutistMask/issues/233)).
|
||
- 2026-08-12: Approval verification became an allowlist — transaction type
|
||
restricted to 0/1/2 so an EIP-7702 delegation can no longer ride along on an
|
||
approved transfer, every consequential field compared, the artifact
|
||
re-serialized from the checked fields alone and its exact bytes required to be
|
||
the canonical encoding of what was broadcast. One approval now yields at most
|
||
one broadcast, and every path that retires a pending approval — popup close,
|
||
active-address change, a late reject — goes through a single chokepoint that
|
||
refuses to settle an attempt already claimed for signing and broadcast
|
||
([#174](https://git.eeqj.de/sneak/AutistMask/issues/174)).
|
||
- 2026-08-12: An address can be removed from an HD or xprv wallet behind a
|
||
confirmation screen that states nothing is destroyed, sharing the deletion
|
||
state transitions with wallet deletion so the selection, site permissions and
|
||
active-address broadcast follow the same rules
|
||
([#162](https://git.eeqj.de/sneak/AutistMask/issues/162)).
|
||
- 2026-08-12: The known-symbol spoof rule moved into `src/shared/symbolSpoof.js`
|
||
and is now the only copy. The balance list had exempted symbols the token list
|
||
maps to `null` — `"ETH"` alone — so a fake ETH ERC-20 was hidden from the
|
||
transaction history and the Send selector but listed as a holding named ETH. A
|
||
symbol with no legitimate contract may now be borne by no contract on any of
|
||
the three surfaces, and the native exemption is "has no contract address", so
|
||
a second null-mapped symbol needs no call-site change. The user's real ETH
|
||
balance is read over RPC and never passes through the rule
|
||
([#235](https://git.eeqj.de/sneak/AutistMask/issues/235)).
|
||
- 2026-08-12: `script/verify-build`'s failure modes are now a committed target,
|
||
`script/test-verify-build`, run by `make check`. It asserts the exit status
|
||
and the message of every case against a fixture tree in a temp dir, and drops
|
||
privileges (proving the runner against a mode-000 file first) for the cases
|
||
that only mean something when file permissions are in force
|
||
([#227](https://git.eeqj.de/sneak/AutistMask/issues/227)).
|
||
- 2026-08-12: WaitTx lifecycle: a receipt and the 60-second timeout can no
|
||
longer both render on one tick, no timer or in-flight lookup outlives its
|
||
wait, a failed receipt lookup no longer counts as a timeout (but six in a row
|
||
end the wait, reported as an unreachable network rather than as a timeout),
|
||
and the wait now resumes after a popup close
|
||
([#155](https://git.eeqj.de/sneak/AutistMask/issues/155)).
|
||
- 2026-08-12: The private key export screen now wipes the key from the page
|
||
whenever it is left by any route, and a decrypt still in flight when the
|
||
screen is left is discarded instead of written; the same `onViewLeave()`
|
||
cleanup was extended to every other screen holding secret material in the DOM
|
||
(AddWallet, ConfirmTx, DeleteWallet, ApproveTx, ApproveSign)
|
||
([#221](https://git.eeqj.de/sneak/AutistMask/issues/221)).
|
||
- 2026-08-12: An xprv wallet already in storage that was imported from a
|
||
non-master key is detected from the depth of its stored `xpub`, explained in
|
||
the wallet list, and blocked from signing, sending and private-key export
|
||
instead of throwing on the send screen
|
||
([#234](https://git.eeqj.de/sneak/AutistMask/issues/234)).
|
||
- 2026-08-12: An unreported `holders_count` is now parsed as `null` rather than
|
||
`0`, so the low-holder rule declines to judge an unknown count instead of
|
||
hiding a legitimate token as spam, in both the transaction history and the
|
||
Send token selector ([#230](https://git.eeqj.de/sneak/AutistMask/issues/230)).
|
||
- 2026-08-12: Bundled token list documentation no longer states a count. The
|
||
four "top 250" claims in `README.md` and the "roughly 500" claim in
|
||
`docs/README.md` are replaced with a description of how the list is actually
|
||
selected — a point-in-time CoinGecko snapshot of the highest-market-cap
|
||
Ethereum mainnet ERC-20s — with `TOKENS` in `src/shared/tokenList.js` named as
|
||
the authoritative set
|
||
([#239](https://git.eeqj.de/sneak/AutistMask/issues/239)).
|
||
- 2026-08-11: libsodium runs on WebAssembly in the shipped builds —
|
||
`'wasm-unsafe-eval'` added to both manifest CSPs after measuring the wasm2js
|
||
fallback at 20x the Argon2id cost, pinned in both directions by
|
||
`tests/manifest.test.js` and observed in the real popup by the e2e suite
|
||
([#182](https://git.eeqj.de/sneak/AutistMask/issues/182)).
|
||
- 2026-08-11: Known-symbol spoof verification became a Settings toggle
|
||
(`hideSpoofedSymbols`), on by default, governing the transaction-history
|
||
filter and the fraud-contract learning it feeds
|
||
([#176](https://git.eeqj.de/sneak/AutistMask/issues/176)).
|
||
- 2026-08-11: `script/verify-build` now walks `dist/` NUL-delimited and asserts
|
||
`dist/` is a real directory, so a path with a trailing space or a newline can
|
||
no longer carry a debug marker past the unlisted-bundle check
|
||
([#223](https://git.eeqj.de/sneak/AutistMask/issues/223)).
|
||
- 2026-08-11: UTC Timestamps checkbox moved from the Token Spam Protection well
|
||
into Display, next to the theme selector
|
||
([#212](https://git.eeqj.de/sneak/AutistMask/issues/212)).
|
||
- 2026-08-11: Network fee counted in the confirmation-screen balance check for
|
||
both ETH and ERC-20 sends, reserving what the node actually charges a type-2
|
||
transaction, with the arithmetic in a pure, unit-tested
|
||
`src/shared/txValidation.js`
|
||
([#154](https://git.eeqj.de/sneak/AutistMask/issues/154)).
|
||
- 2026-08-11: A dust threshold of `0` now means "hide nothing" instead of
|
||
falling back to the 100,000 gwei default, and every address comparison in
|
||
`src/shared/transactions.js` goes through one case-normalising helper so a
|
||
checksummed genuine contract is no longer read as a spoof
|
||
([#179](https://git.eeqj.de/sneak/AutistMask/issues/179)).
|
||
- 2026-08-11: Password-gated recovery phrase display for HD wallets, reached
|
||
from the wallet row in Settings, wiped on leaving the screen and excluded from
|
||
the views the popup can reopen onto
|
||
([#161](https://git.eeqj.de/sneak/AutistMask/issues/161)).
|
||
- 2026-08-11: Extended-key import hardened — the base58 checksum is now enforced
|
||
on every xprv and xpub, and a non-master key is refused with an explanation
|
||
instead of being derived beneath
|
||
([#210](https://git.eeqj.de/sneak/AutistMask/issues/210)).
|
||
- 2026-08-11: the balance refresh and the 24-hour phishing list refresh moved
|
||
from `setInterval` to the extension alarms API, with the phishing delta and
|
||
its fetch timestamps persisted to extension storage, so neither job dies with
|
||
the MV3 service worker. Each job's freshness guard was decoupled from its
|
||
alarm period at the same time — timed to the period, a guard vetoes its own
|
||
scheduled tick and halves the real refresh rate
|
||
([#158](https://git.eeqj.de/sneak/AutistMask/issues/158)).
|
||
- 2026-08-11: Policy compliance sweep — conditional verbose test rerun, local
|
||
Tailwind binary instead of `npx`, `--frozen-lockfile` on `make install`, and
|
||
the Makefile-only targets documented in the README
|
||
([#166](https://git.eeqj.de/sneak/AutistMask/issues/166)).
|
||
- 2026-08-11: `script/verify-build` diagnostics corrected: the both-markers
|
||
message now states what is and is not proven, an unreadable bundle is
|
||
diagnosed as an I/O fault rather than as changed output, the `*.js` assumption
|
||
lives only in `build.js`, and the unlisted-bundle scan hard-fails when it
|
||
cannot enumerate `dist/`
|
||
([#180](https://git.eeqj.de/sneak/AutistMask/issues/180)).
|
||
- 2026-08-11: Known-answer test coverage for the crypto core — BIP-39/BIP-32
|
||
derivation in `wallet.js` and the Argon2id vault in `vault.js`
|
||
([#159](https://git.eeqj.de/sneak/AutistMask/issues/159)).
|
||
- 2026-08-11: Three `README.md` claims corrected against the code — blocklist
|
||
attribution, token-display rule, navigation model
|
||
([#213](https://git.eeqj.de/sneak/AutistMask/issues/213)).
|
||
- 2026-08-11: README Screen Map rebuilt from the code — every screen, element
|
||
and transition re-verified against `src/popup/`
|
||
([#164](https://git.eeqj.de/sneak/AutistMask/issues/164)).
|
||
- 2026-08-11: `docs/README.md` rewritten against the code: no competitor names,
|
||
all five network destinations documented, password/Settings/Add Wallet
|
||
sections corrected ([#163](https://git.eeqj.de/sneak/AutistMask/issues/163)).
|
||
- 2026-08-11: `loadState()` now derives `hasWallet` from the wallet list instead
|
||
of trusting the persisted flag, so a profile already saved inconsistent no
|
||
longer stays broken on every load
|
||
([#195](https://git.eeqj.de/sneak/AutistMask/issues/195)).
|
||
- 2026-08-11: Wallet deletion repairs its own state — `hasWallet` follows the
|
||
remaining wallets, the selection only moves when it was deleted, and the
|
||
active-address change is broadcast to connected sites
|
||
([#156](https://git.eeqj.de/sneak/AutistMask/issues/156)).
|
||
- 2026-08-11: One row per on-chain value movement in transaction history: the
|
||
merge moved into the pure `mergeTransactions` and the zero-ETH native side of
|
||
a plain ERC-20 transfer absorbed into its token row
|
||
([#177](https://git.eeqj.de/sneak/AutistMask/issues/177)).
|
||
- 2026-08-11: `TODO.md` Workflow rewritten to the branch-and-PR-per-issue model
|
||
on `next`, with Status and Next Step refreshed
|
||
([#191](https://git.eeqj.de/sneak/AutistMask/issues/191)).
|
||
- 2026-08-09: `DEBUG` became a build-time constant defaulting to off, injected
|
||
as the `__BUILD_DEBUG__` esbuild define and turned on with
|
||
`AUTISTMASK_DEBUG=1`, so a plain `make build` no longer hands every newly
|
||
created wallet the publicly committed test recovery phrase
|
||
([#149](https://git.eeqj.de/sneak/AutistMask/issues/149)).
|
||
- 2026-08-09: dApp approval signing moved into the popup — the password no
|
||
longer crosses the extension messaging boundary; the background broadcasts and
|
||
resolves approvals only, and verifies the signed artifact against the approval
|
||
it holds (#157).
|
||
- 2026-08-09: Post-build assertion that every emitted bundle containing
|
||
`constants.js` has `DEBUG` compiled off, via `script/verify-build` on the
|
||
`make build` path (#170).
|
||
- 2026-08-09: Containerized Chrome end-to-end harness (`make test-e2e` /
|
||
`script/test-e2e`) driving the real popup with all network intercepted, plus
|
||
the two used-but-not-imported crashes it caught: AddToken unreachable (#150)
|
||
and TransactionDetail broken for every ERC-20 transfer (#151). Harness
|
||
demonstrated failing before the fixes and passing after (#181). Interception
|
||
covers the MV3 background service worker, not just the popup page, and a
|
||
launch-time canary aborts the suite if worker traffic starts escaping.
|
||
- 2026-08-09: Reviewed the repo end to end and filed the 1.0.0 backlog
|
||
(#149-#168).
|
||
- 2026-08-09: Test coverage for the address-poisoning defense in
|
||
`src/shared/transactions.js` (#160)
|
||
- 2026-07-26: About well in settings with build info, repo link and the version
|
||
click easter egg (#145); proper view navigation stack (#146).
|
||
- 2026-07-07 Adopted scripts-to-rule-them-all: `script/` entrypoints, Makefile
|
||
shims, README Entrypoints section (#148)
|
||
- 2026-03-01: USD display suppressed on testnets (#142); estimated USD for ETH
|
||
in approve-tx view (#141).
|
||
- Sepolia testnet support (#137); etherscan links go to token-specific URLs
|
||
(#136).
|
||
- Transaction detail improvements: Type field and on-chain details (#130),
|
||
txid-first reordering (#133), swap display corrections (#128), expanded
|
||
confirm-tx warnings (#118).
|
||
- Dark mode theme setting (Light/Dark/System) with contrast fixes (#126);
|
||
timestamps include timezone offset (#120); layout shift audit, reserved space
|
||
for error messages (#124).
|
||
- Copy-flash visual feedback with timing tune (#113, #121); cross-wallet-type
|
||
duplicate detection (#115).
|
||
- 2026-02-27: v0.1.0 tagged.
|
||
- 2026-02-24: Initial scaffolding: popup UI, BIP-39 wallet creation via
|
||
ethers.js, wallet persistence, real ETH balances over RPC, ENS forward and
|
||
reverse resolution.
|
||
|
||
# Future Steps
|
||
|
||
Only work that has no issue of its own belongs here; everything else is on the
|
||
tracker.
|
||
|
||
- Cut 1.0.0 once the milestone is empty, then continue tagging as milestones
|
||
land.
|