PR #171 moved dApp approval decryption
and signing into the popup, so the user's plaintext password no longer crosses
the extension messaging boundary. It landed carrying an explicit "Chrome not
verified interactively" caveat, because at the time we believed no agent could
drive a browser. That belief was wrong - see #173 (comment).
Items 2-5 of the definition of done on #157 require live eth_sendTransaction, personal_sign and eth_signTypedData_v4 round trips. Those are the highest
value checks on the whole milestone, because they exercise a wallet's signing
path, and they are currently the single largest unverified surface in 1.0.0.
This is also the only part of the extension where the content script, the inpage
provider, the background worker and the popup all have to work together. Unit
tests cover each side in isolation and none of them cover the seam.
Definition of done
A local test page, served from the harness (never from a remote origin),
loads the real inpage provider and speaks EIP-1193 to window.ethereum.
Driven end to end through the real content script, the real background
worker and the real approval popup, with the RPC stubbed by route
interception:
eth_requestAccounts - the site approval prompt appears, approving
returns the selected address
personal_sign - the approve-sign screen appears, the correct message
and origin are displayed, entering the password produces a signature
eth_signTypedData_v4 - as above for typed data
eth_sendTransaction - the approve-tx screen appears with the correct
to/value/data, and approving produces a signed transaction handed to the
stubbed RPC
Every signature produced is verified to recover to the expected address.
A test that merely asserts "a signature came back" is worthless here; it
would pass against a wrong key, a wrong message or a wrong chain. Recover
and compare.
Rejection paths are covered: rejecting each prompt returns an EIP-1193 user
rejection error to the calling page rather than hanging or resolving.
The password never appears in any message crossing the boundary. Assert
this directly by observing the messages, so the regression that #157 fixed cannot silently return.
This is the check that gives the fix a permanent floor instead of a
one-time review.
Demonstrated to fail, per the standing rule on this repo: show each new
assertion failing against a deliberately broken variant (for example a
signature recovered against the wrong address, and a build that puts the
password back in the message payload), with the captured output in the PR.
A green run is not evidence.
Notes
Report honestly on anything that turns out not to be drivable rather than
weakening an assertion to make it pass. If some part of the flow cannot be
reached from the harness, say which and why, and it stays on the human list.
This does NOT discharge a real dApp with real funds against mainnet. That
remains a human pass before the 1.0.0 tag and should not be presented as
covered.
Once this lands, post the result on #171 so the caveat recorded there is
closed out explicitly rather than left dangling.
Blocked on the Chrome harness landing:
https://git.eeqj.de/sneak/AutistMask/issues/181
## Why
PR https://git.eeqj.de/sneak/AutistMask/pulls/171 moved dApp approval decryption
and signing into the popup, so the user's plaintext password no longer crosses
the extension messaging boundary. It landed carrying an explicit "Chrome not
verified interactively" caveat, because at the time we believed no agent could
drive a browser. That belief was wrong - see
https://git.eeqj.de/sneak/AutistMask/issues/173#issuecomment-49609.
Items 2-5 of the definition of done on
https://git.eeqj.de/sneak/AutistMask/issues/157 require live `eth_sendTransaction`,
`personal_sign` and `eth_signTypedData_v4` round trips. Those are the highest
value checks on the whole milestone, because they exercise a wallet's signing
path, and they are currently the single largest unverified surface in 1.0.0.
This is also the only part of the extension where the content script, the inpage
provider, the background worker and the popup all have to work together. Unit
tests cover each side in isolation and none of them cover the seam.
## Definition of done
1. A local test page, served from the harness (never from a remote origin),
loads the real inpage provider and speaks EIP-1193 to `window.ethereum`.
2. Driven end to end through the real content script, the real background
worker and the real approval popup, with the RPC stubbed by route
interception:
- `eth_requestAccounts` - the site approval prompt appears, approving
returns the selected address
- `personal_sign` - the approve-sign screen appears, the correct message
and origin are displayed, entering the password produces a signature
- `eth_signTypedData_v4` - as above for typed data
- `eth_sendTransaction` - the approve-tx screen appears with the correct
to/value/data, and approving produces a signed transaction handed to the
stubbed RPC
3. **Every signature produced is verified to recover to the expected address.**
A test that merely asserts "a signature came back" is worthless here; it
would pass against a wrong key, a wrong message or a wrong chain. Recover
and compare.
4. Rejection paths are covered: rejecting each prompt returns an EIP-1193 user
rejection error to the calling page rather than hanging or resolving.
5. **The password never appears in any message crossing the boundary.** Assert
this directly by observing the messages, so the regression that
https://git.eeqj.de/sneak/AutistMask/issues/157 fixed cannot silently return.
This is the check that gives the fix a permanent floor instead of a
one-time review.
6. Demonstrated to fail, per the standing rule on this repo: show each new
assertion failing against a deliberately broken variant (for example a
signature recovered against the wrong address, and a build that puts the
password back in the message payload), with the captured output in the PR.
A green run is not evidence.
## Notes
- Report honestly on anything that turns out not to be drivable rather than
weakening an assertion to make it pass. If some part of the flow cannot be
reached from the harness, say which and why, and it stays on the human list.
- This does NOT discharge a real dApp with real funds against mainnet. That
remains a human pass before the 1.0.0 tag and should not be presented as
covered.
- Once this lands, post the result on
https://git.eeqj.de/sneak/AutistMask/pulls/171 so the caveat recorded there is
closed out explicitly rather than left dangling.
clawbot
added this to the 1.0.0 milestone 2026-08-09 16:15:49 +02:00
The test page is served by the existing route handler in tests/e2e/network.js — a GET on a reserved-TLD origin fulfilled from a fixture string, so nothing leaves the container and --host-resolver-rules=MAP * ~NOTFOUND still holds. The real inpage provider arrives by the shipped manifest's MAIN-world content script, not by anything the fixture loads itself.
Driving is fire-and-forget from the page: the fixture parks each window.ethereum.request() promise under a key, the test then drives the real approval window that the background opens, then reads the settled value back. That is what makes it possible to observe the prompt while the call is still in flight.
eth_sendRawTransaction gets a stub that records the artifact and answers with its real hash (ethers refuses a mismatch), so the signed transaction handed to the RPC is available to the test verbatim.
Every signature is recovered independently in the test with verifyMessage/verifyTypedData/Transaction.from().from and compared against the address read out of extension storage. The background already verifies too; the test does not lean on that.
The password check observes the popup-to-background channel directly, by wrapping chrome.runtime.sendMessage in the approval window before Approve is clicked and requiring the resulting AUTISTMASK_SIGN_RESPONSE/AUTISTMASK_TX_RESPONSE record to be present — so it cannot pass by observing nothing.
Test-only unit. No file under src/ changes.
Plan, before code:
- The test page is served by the existing route handler in `tests/e2e/network.js` — a `GET` on a reserved-TLD origin fulfilled from a fixture string, so nothing leaves the container and `--host-resolver-rules=MAP * ~NOTFOUND` still holds. The real inpage provider arrives by the shipped manifest's `MAIN`-world content script, not by anything the fixture loads itself.
- Driving is fire-and-forget from the page: the fixture parks each `window.ethereum.request()` promise under a key, the test then drives the real approval window that the background opens, then reads the settled value back. That is what makes it possible to observe the prompt while the call is still in flight.
- `eth_sendRawTransaction` gets a stub that records the artifact and answers with its real hash (ethers refuses a mismatch), so the signed transaction handed to the RPC is available to the test verbatim.
- Every signature is recovered independently in the test with `verifyMessage`/`verifyTypedData`/`Transaction.from().from` and compared against the address read out of extension storage. The background already verifies too; the test does not lean on that.
- The password check observes the popup-to-background channel directly, by wrapping `chrome.runtime.sendMessage` in the approval window before Approve is clicked and requiring the resulting `AUTISTMASK_SIGN_RESPONSE`/`AUTISTMASK_TX_RESPONSE` record to be present — so it cannot pass by observing nothing.
- Test-only unit. No file under `src/` changes.
Built and pushed: #273 (branch test/issue-183-dapp-round-trips, base next).
Ten tests, Chrome e2e suite 27 -> 37. All six definition-of-done items are covered; the full captured failing output for item 6 is in the PR body rather than duplicated here.
Verification: make check green (test-verify-build: 18 case(s) passed), make test-e2e37/37 tests passed, both re-run after rebasing onto 52c7c1b, which brought in #216 and changed the transaction approval path these tests drive. Test-only: no file under src/ changes. Six deliberately broken variants — a corrupted recovered signer, the password put back in the payload, a provider that resolves instead of rejecting, and approval screens showing the wrong origin, message, value and call data — each made the relevant assertion fail, and were reverted.
Two things could not be driven, per the note on reporting honestly:
The browser-action popup itself. The site-connection prompt goes through chrome.action.openPopup(), and headless Chromium's browser-action popup is never a page Playwright can see — confirmed over 60s with the action URL already set. That one prompt is driven at the URL the extension puts on the action instead: same page, same approval id, same code path, but whether a real toolbar click shows it is not observable here. The sign and transaction prompts open real windows and needed none of this.
A real dApp with real funds against a real network. The RPC is stubbed throughout. Stated as an uncovered human pass in both README.md and TODO.md.
Two findings raised in the PR rather than fixed: the EIP-1193 code 4001 crosses the messaging boundary but does not survive src/content/inpage.js, which rebuilds the rejection as a bare Error, so the calling page catches a message and no code; and for a site connection the approve message races the popup teardown, which the disconnect handler settles as a rejection.
Built and pushed: https://git.eeqj.de/sneak/AutistMask/pulls/273 (branch `test/issue-183-dapp-round-trips`, base `next`).
Ten tests, Chrome e2e suite 27 -> 37. All six definition-of-done items are covered; the full captured failing output for item 6 is in the PR body rather than duplicated here.
Verification: `make check` green (`test-verify-build: 18 case(s) passed`), `make test-e2e` `37/37 tests passed`, both re-run after rebasing onto `52c7c1b`, which brought in https://git.eeqj.de/sneak/AutistMask/pulls/216 and changed the transaction approval path these tests drive. Test-only: no file under `src/` changes. Six deliberately broken variants — a corrupted recovered signer, the password put back in the payload, a provider that resolves instead of rejecting, and approval screens showing the wrong origin, message, value and call data — each made the relevant assertion fail, and were reverted.
Two things could not be driven, per the note on reporting honestly:
- The browser-action popup itself. The site-connection prompt goes through `chrome.action.openPopup()`, and headless Chromium's browser-action popup is never a page Playwright can see — confirmed over 60s with the action URL already set. That one prompt is driven at the URL the extension puts on the action instead: same page, same approval id, same code path, but whether a real toolbar click shows it is not observable here. The sign and transaction prompts open real windows and needed none of this.
- A real dApp with real funds against a real network. The RPC is stubbed throughout. Stated as an uncovered human pass in both `README.md` and `TODO.md`.
Two findings raised in the PR rather than fixed: the EIP-1193 code 4001 crosses the messaging boundary but does not survive `src/content/inpage.js`, which rebuilds the rejection as a bare `Error`, so the calling page catches a message and no `code`; and for a site connection the approve message races the popup teardown, which the disconnect handler settles as a rejection.
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Blocked on the Chrome harness landing:
#181
Why
PR #171 moved dApp approval decryption
and signing into the popup, so the user's plaintext password no longer crosses
the extension messaging boundary. It landed carrying an explicit "Chrome not
verified interactively" caveat, because at the time we believed no agent could
drive a browser. That belief was wrong - see
#173 (comment).
Items 2-5 of the definition of done on
#157 require live
eth_sendTransaction,personal_signandeth_signTypedData_v4round trips. Those are the highestvalue checks on the whole milestone, because they exercise a wallet's signing
path, and they are currently the single largest unverified surface in 1.0.0.
This is also the only part of the extension where the content script, the inpage
provider, the background worker and the popup all have to work together. Unit
tests cover each side in isolation and none of them cover the seam.
Definition of done
loads the real inpage provider and speaks EIP-1193 to
window.ethereum.worker and the real approval popup, with the RPC stubbed by route
interception:
eth_requestAccounts- the site approval prompt appears, approvingreturns the selected address
personal_sign- the approve-sign screen appears, the correct messageand origin are displayed, entering the password produces a signature
eth_signTypedData_v4- as above for typed dataeth_sendTransaction- the approve-tx screen appears with the correctto/value/data, and approving produces a signed transaction handed to the
stubbed RPC
A test that merely asserts "a signature came back" is worthless here; it
would pass against a wrong key, a wrong message or a wrong chain. Recover
and compare.
rejection error to the calling page rather than hanging or resolving.
this directly by observing the messages, so the regression that
#157 fixed cannot silently return.
This is the check that gives the fix a permanent floor instead of a
one-time review.
assertion failing against a deliberately broken variant (for example a
signature recovered against the wrong address, and a build that puts the
password back in the message payload), with the captured output in the PR.
A green run is not evidence.
Notes
weakening an assertion to make it pass. If some part of the flow cannot be
reached from the harness, say which and why, and it stays on the human list.
remains a human pass before the 1.0.0 tag and should not be presented as
covered.
#171 so the caveat recorded there is
closed out explicitly rather than left dangling.
Plan, before code:
tests/e2e/network.js— aGETon a reserved-TLD origin fulfilled from a fixture string, so nothing leaves the container and--host-resolver-rules=MAP * ~NOTFOUNDstill holds. The real inpage provider arrives by the shipped manifest'sMAIN-world content script, not by anything the fixture loads itself.window.ethereum.request()promise under a key, the test then drives the real approval window that the background opens, then reads the settled value back. That is what makes it possible to observe the prompt while the call is still in flight.eth_sendRawTransactiongets a stub that records the artifact and answers with its real hash (ethers refuses a mismatch), so the signed transaction handed to the RPC is available to the test verbatim.verifyMessage/verifyTypedData/Transaction.from().fromand compared against the address read out of extension storage. The background already verifies too; the test does not lean on that.chrome.runtime.sendMessagein the approval window before Approve is clicked and requiring the resultingAUTISTMASK_SIGN_RESPONSE/AUTISTMASK_TX_RESPONSErecord to be present — so it cannot pass by observing nothing.src/changes.Built and pushed: #273 (branch
test/issue-183-dapp-round-trips, basenext).Ten tests, Chrome e2e suite 27 -> 37. All six definition-of-done items are covered; the full captured failing output for item 6 is in the PR body rather than duplicated here.
Verification:
make checkgreen (test-verify-build: 18 case(s) passed),make test-e2e37/37 tests passed, both re-run after rebasing onto52c7c1b, which brought in #216 and changed the transaction approval path these tests drive. Test-only: no file undersrc/changes. Six deliberately broken variants — a corrupted recovered signer, the password put back in the payload, a provider that resolves instead of rejecting, and approval screens showing the wrong origin, message, value and call data — each made the relevant assertion fail, and were reverted.Two things could not be driven, per the note on reporting honestly:
chrome.action.openPopup(), and headless Chromium's browser-action popup is never a page Playwright can see — confirmed over 60s with the action URL already set. That one prompt is driven at the URL the extension puts on the action instead: same page, same approval id, same code path, but whether a real toolbar click shows it is not observable here. The sign and transaction prompts open real windows and needed none of this.README.mdandTODO.md.Two findings raised in the PR rather than fixed: the EIP-1193 code 4001 crosses the messaging boundary but does not survive
src/content/inpage.js, which rebuilds the rejection as a bareError, so the calling page catches a message and nocode; and for a site connection the approve message races the popup teardown, which the disconnect handler settles as a rejection.