Commit Graph

366 Commits

Author SHA1 Message Date
3075c2319a mutation demo: break one assertion in each e2e suite (scratch branch, do not merge)
Some checks failed
check / check (push) Successful in 1m7s
e2e / e2e-chrome (push) Failing after 2m6s
e2e / e2e-firefox (push) Failing after 1m41s
2026-08-14 04:13:14 +00:00
037c91da13 build: run the browser e2e suites in CI (closes #259)
Some checks failed
e2e / e2e-chrome (push) Has been cancelled
e2e / e2e-firefox (push) Has been cancelled
check / check (push) Has been cancelled
Nothing automatic ran either e2e suite, so every browser-level guarantee in this
wallet -- WebAssembly under the shipped CSP, the recovery-phrase and private-key
DOM wipes, the ConfirmTx spend gate, the dApp approval round trips -- held only
when a human or an agent remembered to run it by hand.

.gitea/workflows/e2e.yml adds two jobs, e2e-chrome and e2e-firefox, one per
browser so a Chrome failure cannot hide the Firefox result. They are separate
from the check workflow: REPO_POLICIES.md caps make test at 20 seconds and
script/cibuild is a docker build whose Dockerfile runs make check, so neither the
cap nor the local fast path is touched. make check is byte-for-byte unchanged.

Neither suite could run on the runner as it stood, and the reason is not
docker-in-docker. The runner executes a job inside a container against the HOST's
docker daemon, and the job's checkout lives on a docker volume rather than a host
path, so `docker run -v "$PWD:/work"` is resolved by the host, silently succeeds
and mounts an empty directory -- measured on this runner. The runner image's node
is also too old to install this repo's dependencies. Both suites therefore ship
the repo to the daemon as a build context and build the extension inside the
pinned image, which leaves docker as the only prerequisite on a runner or a
laptop. The suites themselves are unchanged; only how the repo reaches the
container is.

Both scripts now build with --iidfile and run the image by ID rather than by tag,
so two clones running a suite at once on the same host cannot swap it under each
other.

The jobs report, they do not gate. Whether a check blocks a merge is Gitea branch
protection, which this repo does not configure, so a failure is a red mark a
reviewer must account for. Nothing can pass vacuously: no continue-on-error, no
`|| true`, and both scripts exit non-zero when docker is missing, when the image
build fails and when the browser fails to start.
2026-08-14 04:12:23 +00:00
9dcd875dd4 fix: carry EIP-1193 error codes through to the page (closes #274)
All checks were successful
check / check (push) Successful in 28s
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.
2026-08-12 13:47:33 +02:00
c755a5e944 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
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.
2026-08-12 13:31:55 +02:00
d5595c0151 test: drive the EIP-1193 dApp approval round trips in the browser (closes #183)
All checks were successful
check / check (push) Successful in 29s
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.
2026-08-12 13:23:57 +02:00
e4c3708b84 fix: fold invisible characters before the known-symbol spoof check (closes #260)
All checks were successful
check / check (push) Successful in 36s
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.
2026-08-12 13:06:54 +02:00
52c7c1b060 test: containerized Firefox end-to-end harness (closes #184)
All checks were successful
check / check (push) Successful in 34s
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.
2026-08-12 12:20:14 +02:00
918e581ef3 harden: verify the signed transaction against what the popup displayed (closes #216)
Some checks failed
check / check (push) Has been cancelled
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.
2026-08-12 12:15:25 +02:00
a08ba6a66d fix: filter the restored view stack against RESTORABLE_VIEWS (closes #224)
Some checks failed
check / check (push) Has been cancelled
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.
2026-08-12 12:07:48 +02:00
09b602579a fix: one password-failure message across every screen (closes #172)
Some checks failed
check / check (push) Has been cancelled
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.
2026-08-12 12:03:39 +02:00
18b47cd579 test: close the empty-batch hole in the e2e unstubbed-request guard (closes #187)
Some checks failed
check / check (push) Has been cancelled
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.
2026-08-12 11:50:38 +02:00
5af89a1b63 test: drive ConfirmTx in the e2e suite, gate assertion included (closes #238)
All checks were successful
check / check (push) Successful in 38s
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.
2026-08-12 11:35:20 +02:00
c6a1f97247 fix: explain a rejected dust threshold instead of silently snapping back (closes #233)
All checks were successful
check / check (push) Successful in 33s
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.
2026-08-12 11:29:09 +02:00
0a1786b406 harden: verify all approval fields and make failed signing retryable (closes #174)
All checks were successful
check / check (push) Successful in 30s
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.
2026-08-12 11:21:02 +02:00
937f699fb1 feat: remove an address from an HD wallet, behind a confirmation (closes #162)
All checks were successful
check / check (push) Successful in 36s
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.
2026-08-12 11:16:29 +02:00
1f41a07df2 fix: filter a fake ETH token from the balance list too (closes #235)
All checks were successful
check / check (push) Successful in 30s
2026-08-12 11:10:38 +02:00
78a1cb067e test: commit a verify-build failure-mode battery and run it from make check (closes #227)
All checks were successful
check / check (push) Successful in 51s
2026-08-12 11:05:08 +02:00
afe6ddaea0 fix: WaitTx timeout no longer overwrites a rendered success screen (closes #155)
All checks were successful
check / check (push) Successful in 30s
2026-08-12 10:58:36 +02:00
23712b53cb 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
2026-08-12 10:54:37 +02:00
bd4bdcafc7 fix: explain a stored non-master xprv wallet instead of throwing at signing time (closes #234)
All checks were successful
check / check (push) Successful in 30s
2026-08-12 10:41:49 +02:00
ce4a0d7b8d fix: distinguish an unknown holder count from zero so a legitimate token is not filtered (closes #230)
All checks were successful
check / check (push) Successful in 39s
2026-08-12 10:34:45 +02:00
bf1dbec87c fix: run libsodium on WebAssembly under the extension CSP (closes #182)
All checks were successful
check / check (push) Successful in 26s
2026-08-12 10:30:15 +02:00
ba35282092 docs: describe the bundled token list by its criterion, not a drifting count (closes #239)
All checks were successful
check / check (push) Successful in 29s
2026-08-12 10:20:40 +02:00
158278d251 fix: count the network fee in the confirm-screen balance check (closes #154)
All checks were successful
check / check (push) Successful in 25s
2026-08-11 15:41:37 +02:00
6f6bc2e7b5 fix: drive background refresh and phishing update from alarms (closes #158)
Some checks failed
check / check (push) Has been cancelled
2026-08-11 15:38:28 +02:00
74c137dadf fix: add a Settings toggle for known-symbol spoof verification (closes #176)
Some checks failed
check / check (push) Has been cancelled
2026-08-11 15:38:05 +02:00
edea22f7ed fix: move the UTC Timestamps checkbox into the Display well (closes #212)
Some checks failed
check / check (push) Has been cancelled
2026-08-11 15:34:16 +02:00
b155c0fcd6 fix: enforce the base58 checksum and reject non-master extended keys (closes #210)
Some checks failed
check / check (push) Has been cancelled
2026-08-11 15:31:50 +02:00
fb9e8f5542 fix: NUL-delimit verify-build's dist walk so no path escapes the check (closes #223)
Some checks failed
check / check (push) Has been cancelled
2026-08-11 15:26:43 +02:00
3e5d6323ce feat: password-gated recovery phrase display for HD wallets (closes #161)
Some checks failed
check / check (push) Has been cancelled
2026-08-11 15:25:17 +02:00
12acf4dc8c fix: honour a dust threshold of 0 and compare addresses case-insensitively (closes #179)
Some checks failed
check / check (push) Has been cancelled
2026-08-11 15:16:48 +02:00
f455b0ae7f test: known-answer coverage for HD derivation and the vault (closes #159)
Some checks failed
check / check (push) Has been cancelled
2026-08-11 15:06:35 +02:00
86cdea5e4e chore: repo policy compliance sweep — test rerun, frozen lockfile, documented targets (closes #166)
Some checks failed
check / check (push) Has been cancelled
2026-08-11 14:57:51 +02:00
f271bcd7b4 fix: one transaction history row per value movement (closes #177)
Some checks failed
check / check (push) Has been cancelled
2026-08-11 14:56:30 +02:00
93e3f6e4e2 fix: correct verify-build diagnostics and close two robustness gaps (closes #180)
Some checks failed
check / check (push) Has been cancelled
2026-08-11 14:55:06 +02:00
9b957ffd69 fix: derive hasWallet from the wallet list on load (closes #195)
Some checks failed
check / check (push) Has been cancelled
2026-08-11 14:51:22 +02:00
cf5f582be9 docs: correct three README claims contradicted by the code (closes #213)
Some checks failed
check / check (push) Has been cancelled
2026-08-11 14:41:16 +02:00
b9bc226ae1 docs: rebuild the README Screen Map from the code (closes #164)
Some checks failed
check / check (push) Has been cancelled
2026-08-11 14:31:45 +02:00
19cb1ca1b0 docs: correct docs/README.md external services and remove competitor names (closes #163)
Some checks failed
check / check (push) Has been cancelled
2026-08-11 14:25:57 +02:00
b882cede9f fix: repair wallet state on delete (closes #156)
Some checks failed
check / check (push) Has been cancelled
2026-08-11 14:23:06 +02:00
d93eda31a0 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
2026-08-11 14:15:05 +02:00
e9fa8bec47 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
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.
2026-08-10 16:15:22 +02:00
ad9162d057 security: decrypt and sign dApp approvals in the popup (closes #157)
All checks were successful
check / check (push) Successful in 24s
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.
2026-08-10 15:59:30 +02:00
188882d635 test: cover the address-poisoning filters in transactions.js (closes #160)
Some checks failed
check / check (push) Has been cancelled
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.
2026-08-10 15:53:15 +02:00
e8ad8325c8 test: containerized Chrome end-to-end harness that drives the real popup (closes #181)
Some checks failed
check / check (push) Has been cancelled
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
2026-08-10 15:49:32 +02:00
f7f141a757 security: make DEBUG a build-time flag defaulting to off (closes #149) (#169)
Some checks failed
check / check (push) Has been cancelled
Fixes the highest-severity item in the repo: `src/shared/constants.js` had
`const DEBUG = true;`, so `generateMnemonic()` returned the publicly committed
`DEBUG_MNEMONIC` for every wallet created from a build of `main`, and the real
entropy path was dead code in every artifact we could produce.

## What changed

**`build.js`** — `AUTISTMASK_DEBUG` is read from the environment and injected
as a `__BUILD_DEBUG__` entry in the existing esbuild `define` map, next to the
other `__BUILD_*__` defines. Only the exact value `1` enables it; unset, empty,
`true`, or a typo all yield a release build, so the insecure direction requires
a deliberate opt-in and any mistake fails safe. The build prints
`Build mode: release (DEBUG off)` or
`Build mode: DEBUG (INSECURE - hardcoded test mnemonic, do not ship)`.

**`src/shared/constants.js`** — `DEBUG` now uses the same `typeof` guard that
`src/shared/buildInfo.js` already uses for the other build-time defines, and
defaults to `false` when the define is absent (jest, plain `require`).
`DEBUG_MNEMONIC` stays in the tree and stays exported.

**`Makefile`** — new `build-debug` target (`AUTISTMASK_DEBUG=1` + the same
build) so a debug build stays a one-liner for development.

**`README.md`** — new "Debug Builds" subsection under Getting Started, and the
DEBUG Mode Policy section now states that `DEBUG` is build-time-only and spells
out the boundary against the runtime toggle.

**`tests/wallet.test.js`** — new, covering both build modes.

**`TODO.md`** — refreshed in the same commit (details at the bottom).

No new `if (DEBUG)` branch was added and nothing about what DEBUG *does* changed:
still exactly the red banner plus the hardcoded test phrase, per the README
DEBUG Mode Policy and `RULES.md:76-80`.

## The interaction with the #145 settings toggle

This is the subtle part, so spelling out the reasoning.

There are two distinct debug flags in the tree after #145:

1. the compile-time `DEBUG` constant from `constants.js`, and
2. the runtime `debugMode` state flag, which the settings easter egg toggles
   and which `settings.js:379` pushes into `log.js` via `setRuntimeDebug()`.

`log.js` merges them: `isDebug()` is `DEBUG || _runtimeDebug`. That merged
value feeds exactly two things — the log level threshold (`log.js:24`) and the
red banner (`views/helpers.js:71`). Making the banner user-toggleable is the
intended behavior of #145, and this PR leaves it alone.

`generateMnemonic()` does **not** consult `isDebug()`. It reads the
compile-time `DEBUG` binding directly. That distinction is what makes a release
build coherent: with `__BUILD_DEBUG__` false, `DEBUG` is false in the bundle,
so no amount of clicking the version ten times and flipping the toggle can
reach `return DEBUG_MNEMONIC`. The user can turn the banner and verbose logging
on in a release build; they cannot turn the hardcoded phrase on.

The failure mode to guard against is someone later "tidying up" the two flags by
routing `wallet.js` through `isDebug()`, which would silently reintroduce this
exact vulnerability with the runtime toggle as the trigger. Three things now
guard that: a comment at the `wallet.js` call site saying it must stay the
compile-time constant and why, the same statement in the README DEBUG Mode
Policy, and a regression test that calls `setRuntimeDebug(true)`, asserts
`isDebug()` is genuinely true, and then asserts `generateMnemonic()` still
returns fresh entropy.

I considered instead making the runtime toggle unavailable in release builds,
but rejected it: that removes a feature #145 deliberately added, and it defends
the wrong boundary. The banner is not the dangerous part; the mnemonic path is,
and that one is already unreachable.

## Verification

`make check` — green, 5 suites, 55 tests, plus lint and fmt-check. It also ran
via the pre-commit hook on the commit itself.

The new tests, per the verification standard in the manager comment on the
issue (not just `a !== b`) — with the flag off: two successive
`generateMnemonic()` calls differ, both pass `isValidMnemonic`, both are 12
words, neither equals `DEBUG_MNEMONIC`, and the result derives a usable HD
wallet (`xpub` + a well-formed first address), so a broken implementation
returning a counter or a truncated phrase would fail. Same assertions again
with the runtime toggle forced on. With the flag on (`__BUILD_DEBUG__` defined
before a `jest.resetModules()` re-require): `DEBUG` is `true` and
`generateMnemonic()` returns `DEBUG_MNEMONIC`, so the debug path is proven
working rather than silently deleted.

Build artifacts — `make build` and `make build-debug` both produce `dist/chrome`
and `dist/firefox` successfully. Grepping the minified bundles for the emitted
`DEBUG` export value across all four bundles (chrome popup, chrome background,
firefox popup, firefox background):

    # after make build
    $ grep -roh 'DEBUG:![01]' dist/chrome dist/firefox | sort | uniq -c
          4 DEBUG:!1

    # after make build-debug
    $ grep -roh 'DEBUG:![01]' dist/chrome dist/firefox | sort | uniq -c
          4 DEBUG:!0

`!1` is minified `false`, `!0` is `true`. Also checked the fail-safe path:
`AUTISTMASK_DEBUG=true make build` prints `Build mode: release (DEBUG off)` and
likewise yields `4 DEBUG:!1`.

One thing a reviewer should know about the grep: the `DEBUG_MNEMONIC` string
literal is still present in the release bundle. That is not a leak of anything
(the phrase is in this public repo already) and it does not mean the branch is
live — esbuild cannot tree-shake a CommonJS `module.exports` object, so the
constant survives while `DEBUG` folds to `false`. The compiled function is
`function PL(){return ML?UL:f_.fromEntropy(globalThis.crypto.getRandomValues(new Uint8Array(16))).phrase}`
where `ML` is the `DEBUG:!1` export. So "the phrase string is absent" is *not*
the right test for a release build; "the exported `DEBUG` is `!1`" is, which is
what I checked.

## `TODO.md` refresh

Per the manager comment: Status rewritten (no branch in flight —
`feat/issue-144-settings-about` landed as #145, scripts-to-rule-them-all landed
as #148, so the `scripts/` question is resolved; `make check` recorded as
verified green on `main` at `23aeae4`); the completed "Verify main passes make
check" Future Step removed; Future Steps rewritten against the #149-#168
backlog in rough priority order, keeping branch pruning (now #167) and the
pre-1.0 security review (noting #149 and #157 are parts of it but it is
broader).

One deliberate deviation to flag rather than bury: the manager asked that Next
Step become this issue. Taken literally against the Workflow section, this
commit *completes* #149, which would normally move it into Completed Steps. I
followed the repo's existing convention for in-flight work instead — the
previous Next Step was phrased as "Land feat/issue-144-settings-about", so Next
Step is now "Land #149 ... PR open, awaiting review", which is accurate until
this merges. Whoever merges should move it to Completed Steps and promote the
first Future Step. Happy to change it if the reviewer prefers the strict
reading.

## Out of scope

`script/lint` being `prettier --check` only and unable to catch undefined
identifiers (#152) — noted in the TODO but not fixed here; I greped for `DEBUG`
consumers by hand rather than relying on lint, as advised. Nothing else in the
DEBUG consumer set (`log.js`, `helpers.js`, `state.js`, `settings.js`) changed
behavior.

Co-authored-by: sneak <sneak@sneak.berlin>
Reviewed-on: #169
Co-authored-by: clawbot <clawbot@noreply.example.org>
Co-committed-by: clawbot <clawbot@noreply.example.org>
2026-08-09 16:19:09 +02:00
23aeae4841 feat: add About well to settings with build info and debug easter egg (#145)
All checks were successful
check / check (push) Successful in 22s
## Summary

Add a new well at the bottom of the settings view displaying application info (license, author, version, build date, git commit hash linked to Gitea), and an easter egg that reveals a debug mode toggle after clicking the version number 10 times.

## Changes

- **`src/popup/index.html`**: Added About well with license, author, version, build date, and linked commit hash. Added hidden debug well with debug mode toggle.
- **`src/popup/views/settings.js`**: Populate About well from build-time constants. Implement version click counter (10 clicks reveals debug well). Wire up debug mode toggle to state and runtime logger.
- **`src/shared/buildInfo.js`** (new): Module exporting build-time constants (`BUILD_VERSION`, `BUILD_LICENSE`, `BUILD_AUTHOR`, `BUILD_COMMIT`, `BUILD_DATE`, `GITEA_COMMIT_URL`) injected by esbuild define.
- **`build.js`**: Read git commit hash (short + full) and version from package.json, pass as esbuild `define` constants. Also reads `GIT_COMMIT_SHORT`/`GIT_COMMIT_FULL` env vars for Docker builds.
- **`Dockerfile`**: Accept `GIT_COMMIT_SHORT` and `GIT_COMMIT_FULL` build args, set as env vars for the build step.
- **`Makefile`**: Pass git commit hashes as Docker build args.
- **`src/shared/state.js`**: Add `debugMode` boolean to state (persisted).
- **`src/shared/log.js`**: Add runtime debug flag that supplements the compile-time `DEBUG` constant. Debug mode toggle updates this flag immediately.

closes #144

Co-authored-by: clawbot <clawbot@noreply.git.eeqj.de>
Co-authored-by: clawbot <clawbot@sneak.cloud>
Co-authored-by: user <user@Mac.lan guest wan>
Co-authored-by: clawbot <clawbot@eeqj.de>
Co-authored-by: sneak <sneak@sneak.berlin>
Reviewed-on: #145
Co-authored-by: clawbot <clawbot@noreply.example.org>
Co-committed-by: clawbot <clawbot@noreply.example.org>
2026-07-26 18:39:09 +02:00
d046a24115 scripts-to-rule-them-all (#148)
All checks were successful
check / check (push) Successful in 5s
Reviewed-on: #148
Co-authored-by: sneak <sneak@sneak.berlin>
Co-committed-by: sneak <sneak@sneak.berlin>
2026-07-07 02:14:26 +02:00
3ffc0bec80 TODO (#147)
All checks were successful
check / check (push) Successful in 4s
Reviewed-on: #147
Co-authored-by: sneak <sneak@sneak.berlin>
Co-committed-by: sneak <sneak@sneak.berlin>
2026-07-06 21:36:13 +02:00
a22f33d511 fix: implement proper view navigation stack (#146)
All checks were successful
check / check (push) Successful in 25s
## Summary

Fixes the view stack pop bug where pressing Back in Settings (or any view) always returned to Main instead of the previous view.

Closes [issue #134](#134)

## Problem

The popup UI had no navigation stack. Every back button was hardcoded to a specific destination (usually Main). The reported path:

> Main → Address → Transaction → Settings (gear icon) → Back

...would go to Main instead of returning to the Transaction view.

## Solution

Implemented a proper view navigation stack (like iOS) as already described in the README:

- **`viewStack`** array added to persisted state — survives popup close/reopen
- **`pushCurrentView()`** — pushes the current view name onto the stack before any forward navigation
- **`goBack()`** — pops the stack and shows the previous view; falls back to Main if the stack is empty; re-renders the wallet list when returning to Main
- **`clearViewStack()`** — resets the stack for root transitions (e.g., after adding/deleting a wallet)

### What Changed

1. **helpers.js** — Added navigation stack functions (`pushCurrentView`, `goBack`, `clearViewStack`, `setRenderMain`)
2. **state.js** — Added `viewStack` to persisted state
3. **index.js** — All `ctx.show*()` wrappers now push before navigating forward; gear button uses stack for toggle behavior
4. **All view back buttons** — Replaced hardcoded destinations with `goBack()` (settings, addressDetail, addressToken, transactionDetail, send, receive, addToken, confirmTx, addWallet, settingsAddToken, deleteWallet, export-privkey)
5. **Direct `showView()` forward navigations** — Added `pushCurrentView()` calls before `showView("send")` in addressDetail, addressToken, and home; before `showView("export-privkey")` in addressDetail; before `deleteWallet.show()` in settings
6. **Reset-to-root transitions** — `clearViewStack()` called after adding a wallet (all 3 import types), after deleting the last wallet, and after transaction completion (Done button)

### Navigation Paths Verified

- **Main → Settings → Back** → returns to Main ✓
- **Main → Address → Settings → Back** → returns to Address ✓
- **Main → Address → Transaction → Settings → Back** → returns to Transaction ✓ (the reported bug)
- **Main → Address → Token → Send → ConfirmTx → Back → Back → Back → Back** → unwinds correctly through each view back to Main ✓
- **Main → Address → Token → Transaction → Settings → Back** → returns to Transaction ✓
- **Settings → Add Wallet → (add) → Main** → stack cleared, fresh root ✓
- **Settings → Delete Wallet → Back** → returns to Settings ✓
- **Settings → Delete Wallet → (confirm)** → stack reset to [main], settings shown ✓
- **Address → Send → ConfirmTx → (broadcast) → SuccessTx → Done** → stack reset, returns to address context ✓
- **Popup close/reopen** → viewStack persisted, back navigation still works ✓

Co-authored-by: user <user@Mac.lan guest wan>
Reviewed-on: #146
Co-authored-by: clawbot <clawbot@noreply.example.org>
Co-committed-by: clawbot <clawbot@noreply.example.org>
2026-03-02 00:15:01 +01:00