milestone 1.0.0: approval-path security, build-integrity guard, e2e harness and filter coverage #190

Open
clawbot wants to merge 44 commits from next into main
Collaborator

Milestone PR: next -> main. next carries one squashed commit per closed issue and is kept green and mergeable, so this can be merged without notice.

41 units landed. Verified on the head of next at d5595c0: make check green — 27 suites, 659 tests, script/test-verify-build 18 cases, prettier clean. make test-e2e 37/37 (Chrome), make test-e2e-firefox 3/3. Every unit passed an independent adversarial review before landing; several needed three to five rounds because each round found a real defect.

Approval and signing path

  • Verification now compares the signed artifact against what the popup displayed, not the dApp's sparse request, so nonce, gas limit and fee fields the user read are actually verified (#216); from is pinned to the address named at approval time.
  • Every approval field is compared and unknown transaction types are refused (#174). This closed a live EIP-7702 type-4 artifact that matched every displayed field while delegating the signer's own EOA.
  • One approval can no longer produce two broadcasts. Closing the window, switching address or a late reject can no longer report "user rejected" to a dApp whose transaction is in flight (#174).
  • The password no longer crosses the extension messaging boundary (#157), now with a permanent test floor under it.

Key and secret handling

  • The exported private key is wiped from the DOM on any view exit, including a decrypt that resolves after you leave (#221).
  • Password-gated recovery phrase display (#161); base58 checksum enforced and non-master extended keys refused (#210); an already-imported non-master xprv wallet is explained rather than throwing mid-send (#234).
  • libsodium runs on WebAssembly under the extension CSP instead of silently falling back to asm.js (#182).

Token filtering

  • A fake token impersonating ETH is filtered from the balance list, not just history and send (#235); invisible characters are folded first, so one space no longer defeats the check (#260).
  • An unknown holder count is distinguished from zero, so a legitimate token is not hidden (#230); address-poisoning filters covered (#160).

Correctness and UI

  • The network fee counts in the confirm-screen balance check (#154); a WaitTx timeout no longer overwrites a rendered success screen (#155); one history row per value movement (#177).
  • Address removal from an HD wallet behind a confirmation (#162); wallet state repaired on delete (#156); the restored view stack filtered (#224); one password-failure message everywhere (#172); the dust threshold explains a rejection instead of snapping back (#233).

Verification infrastructure

  • Chrome e2e harness driving the real popup (#181), extended to ConfirmTx with a gate assertion that dies if the spend check reads the estimate instead of the reserve (#238), and to all four EIP-1193 dApp round trips with signature recovery (#183).
  • Firefox e2e harness via geckodriver on a pinned MV2 temporary add-on (#184).
  • verify-build failure-mode battery wired into make check (#227); DEBUG asserted off in every emitted bundle (#170); the e2e unstubbed-request guard no longer passes an empty batch (#187).

Known gaps, all tracked

  • Firefox dApp connectivity is still non-functional — Chrome callback APIs against the promise-only browser namespace (#153, in flight).
  • EIP-1193 error codes do not reach the page, so a dApp cannot detect a user rejection (#274, in flight).
  • Approving a site connection races the popup teardown (#275).
  • Seven bundled tokens are filtered as spoofs of their own duplicate symbol (#276, in flight).
  • The phishing blocklist URL still names a competitor and its documented upstream is gone — your decision, and it gates the 1.0.0 tag (#219).
  • A real dApp with real funds against mainnet remains an uncovered human pass.
Milestone PR: `next` -> `main`. `next` carries one squashed commit per closed issue and is kept green and mergeable, so this can be merged without notice. **41 units landed.** Verified on the head of `next` at `d5595c0`: `make check` green — 27 suites, 659 tests, `script/test-verify-build` 18 cases, prettier clean. `make test-e2e` 37/37 (Chrome), `make test-e2e-firefox` 3/3. Every unit passed an independent adversarial review before landing; several needed three to five rounds because each round found a real defect. ## Approval and signing path - Verification now compares the signed artifact against **what the popup displayed**, not the dApp's sparse request, so nonce, gas limit and fee fields the user read are actually verified ([#216](https://git.eeqj.de/sneak/AutistMask/issues/216)); `from` is pinned to the address named at approval time. - Every approval field is compared and unknown transaction types are refused ([#174](https://git.eeqj.de/sneak/AutistMask/issues/174)). This closed a live EIP-7702 type-4 artifact that matched every displayed field while delegating the signer's own EOA. - One approval can no longer produce two broadcasts. Closing the window, switching address or a late reject can no longer report "user rejected" to a dApp whose transaction is in flight ([#174](https://git.eeqj.de/sneak/AutistMask/issues/174)). - The password no longer crosses the extension messaging boundary ([#157](https://git.eeqj.de/sneak/AutistMask/issues/157)), now with a permanent test floor under it. ## Key and secret handling - The exported private key is wiped from the DOM on any view exit, including a decrypt that resolves after you leave ([#221](https://git.eeqj.de/sneak/AutistMask/issues/221)). - Password-gated recovery phrase display ([#161](https://git.eeqj.de/sneak/AutistMask/issues/161)); base58 checksum enforced and non-master extended keys refused ([#210](https://git.eeqj.de/sneak/AutistMask/issues/210)); an already-imported non-master xprv wallet is explained rather than throwing mid-send ([#234](https://git.eeqj.de/sneak/AutistMask/issues/234)). - libsodium runs on WebAssembly under the extension CSP instead of silently falling back to asm.js ([#182](https://git.eeqj.de/sneak/AutistMask/issues/182)). ## Token filtering - A fake token impersonating ETH is filtered from the balance list, not just history and send ([#235](https://git.eeqj.de/sneak/AutistMask/issues/235)); invisible characters are folded first, so one space no longer defeats the check ([#260](https://git.eeqj.de/sneak/AutistMask/issues/260)). - An unknown holder count is distinguished from zero, so a legitimate token is not hidden ([#230](https://git.eeqj.de/sneak/AutistMask/issues/230)); address-poisoning filters covered ([#160](https://git.eeqj.de/sneak/AutistMask/issues/160)). ## Correctness and UI - The network fee counts in the confirm-screen balance check ([#154](https://git.eeqj.de/sneak/AutistMask/issues/154)); a WaitTx timeout no longer overwrites a rendered success screen ([#155](https://git.eeqj.de/sneak/AutistMask/issues/155)); one history row per value movement ([#177](https://git.eeqj.de/sneak/AutistMask/issues/177)). - Address removal from an HD wallet behind a confirmation ([#162](https://git.eeqj.de/sneak/AutistMask/issues/162)); wallet state repaired on delete ([#156](https://git.eeqj.de/sneak/AutistMask/issues/156)); the restored view stack filtered ([#224](https://git.eeqj.de/sneak/AutistMask/issues/224)); one password-failure message everywhere ([#172](https://git.eeqj.de/sneak/AutistMask/issues/172)); the dust threshold explains a rejection instead of snapping back ([#233](https://git.eeqj.de/sneak/AutistMask/issues/233)). ## Verification infrastructure - Chrome e2e harness driving the real popup ([#181](https://git.eeqj.de/sneak/AutistMask/issues/181)), extended to ConfirmTx with a gate assertion that dies if the spend check reads the estimate instead of the reserve ([#238](https://git.eeqj.de/sneak/AutistMask/issues/238)), and to all four EIP-1193 dApp round trips with signature recovery ([#183](https://git.eeqj.de/sneak/AutistMask/issues/183)). - Firefox e2e harness via geckodriver on a pinned MV2 temporary add-on ([#184](https://git.eeqj.de/sneak/AutistMask/issues/184)). - `verify-build` failure-mode battery wired into `make check` ([#227](https://git.eeqj.de/sneak/AutistMask/issues/227)); DEBUG asserted off in every emitted bundle ([#170](https://git.eeqj.de/sneak/AutistMask/issues/170)); the e2e unstubbed-request guard no longer passes an empty batch ([#187](https://git.eeqj.de/sneak/AutistMask/issues/187)). ## Known gaps, all tracked - Firefox dApp connectivity is still non-functional — Chrome callback APIs against the promise-only `browser` namespace ([#153](https://git.eeqj.de/sneak/AutistMask/issues/153), in flight). - EIP-1193 error codes do not reach the page, so a dApp cannot detect a user rejection ([#274](https://git.eeqj.de/sneak/AutistMask/issues/274), in flight). - Approving a site connection races the popup teardown ([#275](https://git.eeqj.de/sneak/AutistMask/issues/275)). - Seven bundled tokens are filtered as spoofs of their own duplicate symbol ([#276](https://git.eeqj.de/sneak/AutistMask/issues/276), in flight). - The phishing blocklist URL still names a competitor and its documented upstream is gone — **your decision**, and it gates the 1.0.0 tag ([#219](https://git.eeqj.de/sneak/AutistMask/issues/219)). - A real dApp with real funds against mainnet remains an uncovered human pass.
clawbot added 1 commit 2026-08-10 15:49:44 +02:00
Runs the real popup in a pinned containerized Chrome and fails on any uncaught
page error or console.error. Also fixes the two defects it caught: the missing
showView import in addToken.js and the missing addressDotHtml import in
transactionDetail.js.

closes #150
closes #151
clawbot self-assigned this 2026-08-10 15:49:54 +02:00
clawbot added the needs-review label 2026-08-10 15:50:32 +02:00
clawbot added 1 commit 2026-08-10 15:53:18 +02:00
test: cover the address-poisoning filters in transactions.js (closes #160)
Some checks failed
check / check (push) Has been cancelled
188882d635
47 tests over src/shared/transactions.js: both real address-poisoning attacks
as fixtures, each of the four filters on and off, threshold boundaries, no
false positives, and the per-address merge/dedup path. No source file changed.
clawbot added 1 commit 2026-08-10 15:59:31 +02:00
security: decrypt and sign dApp approvals in the popup (closes #157)
All checks were successful
check / check (push) Successful in 24s
ad9162d057
The password no longer crosses the extension messaging boundary: the popup
decrypts and signs, and sends only the raw signed transaction or the signature.
The background re-derives the signer from the artifact and checks it against
the approval it holds before broadcasting, so it is not a blind relay.
clawbot added 1 commit 2026-08-10 16:15:25 +02:00
build: assert DEBUG is off in every emitted bundle as a post-build check (closes #170)
All checks were successful
check / check (push) Successful in 18s
e9fa8bec47
build.js records which emitted bundles contain src/shared/constants.js, and
constants.js carries a marker constant-folded from DEBUG itself. script/verify-build
cross-checks the two and fails on every way of not knowing, so deleting the
__BUILD_DEBUG__ define now breaks the build instead of shipping a live debug branch.
clawbot removed their assignment 2026-08-10 16:16:05 +02:00
sneak was assigned by clawbot 2026-08-10 16:16:06 +02:00
clawbot added merge-ready and removed needs-review labels 2026-08-10 16:16:09 +02:00
clawbot added 1 commit 2026-08-11 14:15:06 +02:00
docs: rewrite TODO.md workflow for the branch-per-issue model on next (closes #191)
All checks were successful
check / check (push) Successful in 22s
d93eda31a0
clawbot added 1 commit 2026-08-11 14:23:08 +02:00
fix: repair wallet state on delete (closes #156)
Some checks failed
check / check (push) Has been cancelled
b882cede9f
clawbot added 1 commit 2026-08-11 14:25:58 +02:00
clawbot added 1 commit 2026-08-11 14:31:47 +02:00
docs: rebuild the README Screen Map from the code (closes #164)
Some checks failed
check / check (push) Has been cancelled
b9bc226ae1
clawbot added 1 commit 2026-08-11 14:41:17 +02:00
docs: correct three README claims contradicted by the code (closes #213)
Some checks failed
check / check (push) Has been cancelled
cf5f582be9
clawbot added 1 commit 2026-08-11 14:51:24 +02:00
fix: derive hasWallet from the wallet list on load (closes #195)
Some checks failed
check / check (push) Has been cancelled
9b957ffd69
clawbot added 1 commit 2026-08-11 14:55:09 +02:00
clawbot added 1 commit 2026-08-11 14:56:31 +02:00
fix: one transaction history row per value movement (closes #177)
Some checks failed
check / check (push) Has been cancelled
f271bcd7b4
clawbot added 1 commit 2026-08-11 14:57:53 +02:00
clawbot added 1 commit 2026-08-11 15:06:37 +02:00
test: known-answer coverage for HD derivation and the vault (closes #159)
Some checks failed
check / check (push) Has been cancelled
f455b0ae7f
clawbot added 1 commit 2026-08-11 15:16:51 +02:00
clawbot added 1 commit 2026-08-11 15:25:19 +02:00
feat: password-gated recovery phrase display for HD wallets (closes #161)
Some checks failed
check / check (push) Has been cancelled
3e5d6323ce
clawbot added 1 commit 2026-08-11 15:26:45 +02:00
clawbot added 1 commit 2026-08-11 15:31:53 +02:00
clawbot added 1 commit 2026-08-11 15:34:19 +02:00
fix: move the UTC Timestamps checkbox into the Display well (closes #212)
Some checks failed
check / check (push) Has been cancelled
edea22f7ed
clawbot added 1 commit 2026-08-11 15:38:08 +02:00
clawbot added 1 commit 2026-08-11 15:38:29 +02:00
fix: drive background refresh and phishing update from alarms (closes #158)
Some checks failed
check / check (push) Has been cancelled
6f6bc2e7b5
clawbot added 1 commit 2026-08-11 15:41:40 +02:00
fix: count the network fee in the confirm-screen balance check (closes #154)
All checks were successful
check / check (push) Successful in 25s
158278d251
clawbot added 1 commit 2026-08-12 10:20:41 +02:00
clawbot added 1 commit 2026-08-12 10:30:18 +02:00
fix: run libsodium on WebAssembly under the extension CSP (closes #182)
All checks were successful
check / check (push) Successful in 26s
bf1dbec87c
clawbot added 1 commit 2026-08-12 10:34:47 +02:00
clawbot added 1 commit 2026-08-12 10:41:50 +02:00
clawbot added 1 commit 2026-08-12 10:54:39 +02:00
fix: wipe the exported private key from the DOM on any view leave (closes #221)
All checks were successful
check / check (push) Successful in 29s
23712b53cb
clawbot added 1 commit 2026-08-12 10:58:38 +02:00
fix: WaitTx timeout no longer overwrites a rendered success screen (closes #155)
All checks were successful
check / check (push) Successful in 30s
afe6ddaea0
clawbot added 1 commit 2026-08-12 11:05:10 +02:00
clawbot added 1 commit 2026-08-12 11:10:39 +02:00
fix: filter a fake ETH token from the balance list too (closes #235)
All checks were successful
check / check (push) Successful in 30s
1f41a07df2
clawbot added 1 commit 2026-08-12 11:16:31 +02:00
feat: remove an address from an HD wallet, behind a confirmation (closes #162)
All checks were successful
check / check (push) Successful in 36s
937f699fb1
Address rows on Home gain an [x] control, on wallets that derive addresses from
an extended key and hold more than one, opening a DeleteAddress confirmation
screen.

Removal cannot destroy anything: the key material stays. Derivation indices are
not renumbered, so the next "+" derives the next unused index rather than
resurrecting the removed one. The confirmation states the real route back --
delete the whole wallet in Settings, which asks for the password and destroys
the stored recovery phrase, then import it again -- and notes that the scan
which follows only finds addresses with on-chain activity. The copy varies by
wallet type, since an xprv wallet has no recovery phrase.

Removing an address that holds a balance is allowed, with a warning naming no
figure; the funds are at the address on-chain and stay there either way.
Selection and active address move only when the removed address was the one
selected, and site permissions are dropped for it alone.

The state transition shares its address comparison, permission cleanup and
active-changed broadcast with the wallet-level removal.
clawbot added 1 commit 2026-08-12 11:21:03 +02:00
harden: verify all approval fields and make failed signing retryable (closes #174)
All checks were successful
check / check (push) Successful in 30s
0a1786b406
approvalVerify now compares every field of the signed artifact against the
approval, not a subset. Transaction types are allowlisted to 0/1/2 and any
field the module does not check is refused outright, so a future transaction
type cannot smuggle consequential fields past verification -- an EIP-7702
type-4 artifact that delegates the signer's own EOA while matching every
displayed field was accepted before this change. The serialized bytes handed
to broadcastTransaction are compared against the parsed artifact, so the
guarantee covers the bytes that actually go to the node.

Signing failures in the popup are retryable again. To make that safe, an
approval is claimed synchronously before the first await and every path that
resolves or removes one goes through a single chokepoint that refuses a claimed
approval. Without it, closing the approval window, switching the active address
or a late reject would report "User rejected the request." to the dApp while
the broadcast completed -- the user then redoes the transfer at a fresh nonce
and it sends twice.

Failure copy distinguishes the stage reached, so a user is never told to start
again from the site when the first attempt may already have reached the network.
clawbot added 1 commit 2026-08-12 11:29:10 +02:00
fix: explain a rejected dust threshold instead of silently snapping back (closes #233)
All checks were successful
check / check (push) Successful in 33s
c6a1f97247
The dust-threshold field was the only validated input in Settings that rejected
without saying anything: the value silently changed back to the stored one with
no explanation. It now flashes "Please enter a whole number of gwei, zero or
greater." alongside the existing resync, matching the idiom the RPC URL field
already uses.

The parse moves to its own module and accepts plain decimal digits only, zero
or greater. Hex and exponent notation are refused rather than accepted: Number()
reads "0x10" as 16 and "1e3" as 1000, neither of which the previous parseInt
produced, and storing a number the user did not type is the same silent
substitution this change exists to remove.

The message must fit one line of the reserved flash area -- a wrapped message
pushes the settings view down, which the No Layout Shift policy forbids. That is
pinned by an end-to-end test measuring the rendered line height and the position
of the elements below it, in a single round trip because the flash clears after
two seconds.
clawbot added 1 commit 2026-08-12 11:35:21 +02:00
test: drive ConfirmTx in the e2e suite, gate assertion included (closes #238)
All checks were successful
check / check (push) Successful in 38s
5af89a1b63
ConfirmTx -- the screen that decides what gets signed -- had no automated
coverage of its own behaviour. The arithmetic underneath was well tested; the
wiring was not, so a mutant making the spend gate read the displayed fee
estimate instead of the reserve would have reintroduced the #154 overspend with
the suite still green.

Nine end-to-end tests now drive it for both the native and ERC-20 paths,
covering the pending, funded, over-balance and estimate-failed states, and
asserting that the gate reads the reserve rather than the estimate. Swapping the
two makes the suite fail. The view height is asserted constant across every
state transition rather than merely printed.

Reaching the screen needs a funded balance and a gas estimate, so the route
interception gains fixtures for both. Testing the estimate-failed state means
provoking the console error the code is supposed to emit, which the harness
otherwise fails a run on; an expectation mechanism consumes exactly one matching
record, is scoped to the declaring test, and fails that test if nothing matched,
so it cannot mask an unrelated error.
clawbot added 1 commit 2026-08-12 11:50:40 +02:00
test: close the empty-batch hole in the e2e unstubbed-request guard (closes #187)
Some checks failed
check / check (push) Has been cancelled
18b47cd579
The guard that reports unrecognised POST bodies used batch.every(), which is
vacuously true on an empty array, so a POST with body [] was answered 200 []
and escaped the one mechanism whose job is to make unrecognised outbound
traffic fail the suite rather than pass silently. Unreachable in practice
today, which is exactly the qualifier that stops being true later.

The comment explaining the guard also described a mechanism that does not
exist: playwright-core decodes a binary body lossily rather than returning
null, so such a body reaches the JSON parse as mojibake and is reported by the
catch, while only an absent or empty body decodes to null and is reported by
the type guard. Both are reported; the comment now describes the two real
routes.
clawbot added 1 commit 2026-08-12 12:03:41 +02:00
fix: one password-failure message across every screen (closes #172)
Some checks failed
check / check (push) Has been cancelled
09b602579a
A rejected password was reported three different ways depending on which screen
you were on, including the fragment "Wrong password." which is not a sentence.
All six decryptWithPassword call sites now show the same full sentence.

Strings only -- a wrong password still fails closed on every screen and still
resolves no pending approval.

A test pins the invariant per call site: each decryptWithPassword call is walked
out to its enclosing try and forward to that block's catch, and the prose shown
there must equal the canonical sentence. Per-file matching was not enough, since
a file with two call sites kept passing while one of them diverged.
clawbot added 1 commit 2026-08-12 12:07:50 +02:00
fix: filter the restored view stack against RESTORABLE_VIEWS (closes #224)
Some checks failed
check / check (push) Has been cancelled
a08ba6a66d
The persisted view stack was restored verbatim. RESTORABLE_VIEWS stopped the
popup opening ONTO a view it will not re-render, but nothing kept such a view
out of the stack, so Back could land on a screen whose content was deliberately
never restored. No secret leaks -- those views are blank precisely because
nothing is restored into them; this is a navigation defect.

loadState() now truncates the stored stack at the first entry outside
RESTORABLE_VIEWS, dropping it and everything above it. Truncating rather than
splicing keeps the result a prefix of what was stored, so every surviving entry
keeps the Back target it had; splicing would silently re-point the entry above
the hole at a different screen. Filtering on load rather than on save is what
makes it retroactive for stacks already in storage, and leaves the live
in-session stack whole, which it should be.

The general case where Back lands on a blank screen even for restorable views,
because goBack() re-renders nothing, is separate and tracked at #268.
clawbot added 1 commit 2026-08-12 12:15:27 +02:00
Verification compared the signed artifact against the dApp's request object.
For every field the dApp omitted -- normally nonce, gas limit and all the fee
fields, since the popup filled them in -- the number the user actually read on
screen was verified by nothing, and only absolute ceilings stood behind it.

The transaction is now populated in the background before the approval window
opens, and that populated object is both what the popup displays and what the
signed artifact is verified against. Every consequential field becomes an
equality comparison; the ceilings remain as a backstop. Population failing
means no approval and no window, and the error goes to the requesting page --
earlier than before, where the same estimate failed after the password had been
typed.

The account is pinned too: `from` is compared against the address named at
approval time rather than whichever address is active at signing, so switching
accounts mid-flow refuses instead of signing from an account the approval did
not name. The message-signing path had the same defect and gets the same fix.

Nonce selection moves earlier as a consequence; the concurrent-approval case
that follows from it is tracked at #271.
clawbot added 1 commit 2026-08-12 12:20:15 +02:00
test: containerized Firefox end-to-end harness (closes #184)
All checks were successful
check / check (push) Successful in 34s
52c7c1b060
Drives the real popup in a real Firefox with dist/firefox/ installed as an
unpacked MV2 temporary add-on via geckodriver. make test-e2e-firefox, outside
make check like the Chrome suite. Zero npm dependencies: plain fetch and
child_process against geckodriver's HTTP API. Base image, Firefox tarball and
geckodriver are each pinned by digest and verified at build time.

Error capture reads the privileged console service through Marionette's chrome
context, not WebDriver BiDi. BiDi delivers nothing at all for extension pages,
so a BiDi-based harness would observe zero events and report success -- the
vacuous-check shape this repo has shipped twice. Both the driver and the README
say so where someone would be tempted to simplify.

Demonstrated to discriminate: a background page that throws at the top of the
file, a missing import, and an async throw where every UI assertion still
passes each fail the run.

Three limits are measured and documented rather than papered over: capture is
poll-based so an error is attributed to a step, not a moment; the console ring
buffer holds 250 messages and evicts the oldest, measured against a clean-run
peak of 4; and the drained window ends roughly 1.5s after the last step, with
observed jitter rather than a hard boundary. Content-script capture is marked
unverified because --network none leaves no page to inject into, and that same
choice inverts coverage of network-dependent code.
clawbot added 1 commit 2026-08-12 13:06:57 +02:00
fix: fold invisible characters before the known-symbol spoof check (closes #260)
All checks were successful
check / check (push) Successful in 36s
e4c3708b84
A token calling itself " ETH " missed the known-symbol table entirely, so the
spoof check reported it was not a spoof -- while HTML collapsed the whitespace
and displayed it as ETH next to the user's real ETH. One space defeated the
filter.

The symbol is now folded before the lookup: NFKC, remove what paints nothing,
trim, uppercase. The rule is "remove what paints nothing"; the Unicode classes
are how that is spelled, which is why U+007F is named separately -- it is a
control, reached by no class, and measures identical to no character at all.

Every width in the module comment was measured in the pinned browser rather
than reasoned about, and the boundary is pinned from both sides: widening to
all control characters fails the visible-controls test, narrowing back fails
the invisible-characters test. Two default-ignorable code points do paint a
box and are folded anyway, which can only hide a token that does not resemble
the symbol it folds to -- the harmless direction, recorded rather than glossed.

Confusables that are distinct letters, bidi reordering and interior whitespace
are knowingly left open and asserted open by tests.
clawbot added 1 commit 2026-08-12 13:24:00 +02:00
test: drive the EIP-1193 dApp approval round trips in the browser (closes #183)
All checks were successful
check / check (push) Successful in 29s
d5595c0151
The dApp signing path was the largest unverified surface in the milestone: the
only place where the content script, the inpage provider, the background worker
and the popup all have to work together, with unit tests covering each side in
isolation and none covering the seam.

A page served by the harness speaks EIP-1193 to the real provider -- asserted by
EIP-6963 object identity, not by shape -- and eth_requestAccounts, personal_sign,
eth_signTypedData_v4 and eth_sendTransaction are each driven through to approval
and to rejection.

Every signature is recovered and compared to the approved address; the broadcast
transaction is parsed from the bytes captured at eth_sendRawTransaction and
checked for signer, recipient, value, calldata and chain. A signature that
merely came back would pass against a wrong key, a wrong message or a wrong
chain, so each assertion was demonstrated failing against a variant that is
wrong in exactly one of those ways.

The password is asserted absent from every message crossing the extension
boundary, which gives #157's fix a permanent floor rather than a one-time
review.

Two defects this surfaced are tracked separately: EIP-1193 error codes never
reach the page (#274), and approving a site connection races the popup teardown
(#275). Neither is asserted as correct here. A real dApp with real funds against
mainnet remains an uncovered human pass and is documented as such.
clawbot added 1 commit 2026-08-12 13:31:56 +02:00
fix: a shared ticker no longer hides one of its two real tokens (closes #276)
All checks were successful
check / check (push) Successful in 33s
c755a5e944
Seven bundled tokens were filtered as spoofs at their own address, so a user
holding FRAX, TON, REUSD, EURE, MSUSD, MUSD or JPYC could not see or spend the
one the wallet happened not to pick.

The known-symbol table is derived from the bundled token list, first-wins in
market-cap order, so a symbol that appears twice silently condemned its second
contract. Both are real tokens from the same fetch and neither is stale --
three pairs are one issuer's old and new contract, four are unrelated issuers
sharing a ticker. Picking a winner would have been guessing, and dropping the
ambiguous symbols would have ended spoof filtering for those tickers entirely.

The table now maps a symbol to the set of addresses that legitimately bear it.
A contract outside the set is still a spoof, so the check is not weakened: a
third contract bearing any of the seven shared tickers is refused, and that is
tested. The filter decides what is fake, not what is worth holding, so a legacy
contract stays in the set -- it still holds real balances.

A test walks the whole bundled list asserting no token is filtered at its own
address, which is the guard whose absence let this ship.
clawbot added 1 commit 2026-08-12 13:47:35 +02:00
fix: carry EIP-1193 error codes through to the page (closes #274)
All checks were successful
check / check (push) Successful in 28s
9dcd875dd4
The provider rebuilt every rejection as a bare Error carrying only a message,
so a dApp checking err.code === 4001 saw undefined and could not tell a user's
deliberate refusal from a failure. Well-behaved sites therefore showed an error
or retried instead of accepting the refusal. The code was produced correctly
and did cross the extension boundary; it was lost in the last hop.

Rejections now reach the page as a ProviderRpcError carrying code, and data
where present. The code is passed through verbatim rather than matched against
a whitelist, so a code added upstream later needs no change here. An error that
genuinely has no code stays a plain Error with no code property at all, rather
than advertising code: undefined -- 'code' in err is what a careful dApp asks.

Messages are unchanged for every path, verified byte-for-byte against the
previous provider across every background error shape.

The end-to-end assertion that printed the observed code now requires it.
clawbot added 1 commit 2026-08-14 06:14:11 +02:00
fix: render the view "Back" lands on after the popup is reopened (closes #268)
All checks were successful
check / check (push) Successful in 1m27s
0be20d7270
All checks were successful
check / check (push) Successful in 1m27s
Required
Details
This pull request can be merged automatically.
You are not authorized to merge this pull request.
View command line instructions

Checkout

From your project repository, check out a new branch and test the changes.
git fetch -u origin next:next
git checkout next
Sign in to join this conversation.
No Reviewers
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: sneak/AutistMask#190