Compare commits

..

1 Commits

Author SHA1 Message Date
3d87f8bda5 test: cover the address-poisoning filters in transactions.js (closes #160)
All checks were successful
check / check (push) Successful in 20s
src/shared/transactions.js had zero test coverage despite owning the four
anti-poisoning filters that README.md:730-814 documents as a core security
property: known token symbol verification, the 1,000-holder rule, the fraud
contract blocklist, and the dust threshold. A regression in any of them does
not crash, it just silently stops filtering, so the behaviour needs pinning
down in both directions.

Adds tests/transactions.test.js with fixtures built from the two real attacks
cited in the README: the fake "Ethereum"/"ETH" token at 0xD05339f9 with zero
holders, and the 1 gwei native dust transfer 0x2708ebdd from the look-alike
sender 0xC3c6B3b4. Every filter is proven to work when on and to be bypassed
when off, both thresholds are asserted at, just below, and just above their
boundary, and legitimate traffic (a plain ETH transfer and genuine USDC and
WETH transfers) is proven to survive all four rules.

Also covers the per-address merge and dedup that fetchRecentTransactions owns,
with debugFetch mocked at the module boundary. No test performs a network
request: global.fetch is a throwing stub and is asserted never to have been
called.

Several tests are named as documenting current behaviour where it diverges
from the README; no source file is modified here.
2026-08-09 05:05:15 +00:00
8 changed files with 1025 additions and 207 deletions

View File

@@ -1,4 +1,4 @@
.PHONY: bootstrap setup install test lint fmt fmt-check check docker hooks build build-debug clean dev .PHONY: bootstrap setup install test lint fmt fmt-check check docker hooks build clean dev
# Standard targets are thin shims; the implementations live in script/ # Standard targets are thin shims; the implementations live in script/
# per the scripts-to-rule-them-all pattern (see the Entrypoints section # per the scripts-to-rule-them-all pattern (see the Entrypoints section
@@ -38,13 +38,6 @@ build:
@echo "Building extension..." @echo "Building extension..."
@yarn run build 2>&1 @yarn run build 2>&1
# Development-only build: enables the red DEBUG / INSECURE banner and makes
# the hardcoded test recovery phrase the output of wallet creation. Never
# distribute the artifacts this produces.
build-debug:
@echo "Building extension (DEBUG)..."
@AUTISTMASK_DEBUG=1 yarn run build 2>&1
clean: clean:
@rm -rf dist/ @rm -rf dist/

View File

@@ -42,23 +42,6 @@ Load the extension:
- **Firefox**: Navigate to `about:debugging#/runtime/this-firefox`, click "Load - **Firefox**: Navigate to `about:debugging#/runtime/this-firefox`, click "Load
Temporary Add-on", and select `dist/firefox/manifest.json`. Temporary Add-on", and select `dist/firefox/manifest.json`.
### Debug Builds
`make build` always produces a release build: the build-time `DEBUG` constant is
`false`, so wallet creation uses real entropy and the red banner is off. To
produce a debug build instead, set `AUTISTMASK_DEBUG=1` in the environment:
```bash
make build-debug # or: AUTISTMASK_DEBUG=1 make build
```
Only the exact value `1` enables it; any other value (including unset, empty, or
`true`) yields a release build, so a typo cannot accidentally ship the debug
behavior. The build prints which mode it used. See the
[DEBUG Mode Policy](#debug-mode-policy) for what the flag changes. **Never
distribute a debug build** — every wallet it creates gets the same publicly
known test recovery phrase.
## Entrypoints ## Entrypoints
This repository adheres to the This repository adheres to the
@@ -685,19 +668,6 @@ flows, or alter program behavior beyond the banner and the hardcoded mnemonic.
Adding new DEBUG-conditional branches requires explicit approval from the Adding new DEBUG-conditional branches requires explicit approval from the
project owner. project owner.
`DEBUG` is a build-time constant, not a runtime setting. `build.js` injects it
into the bundle as the `__BUILD_DEBUG__` define — `false` unless the build was
run with `AUTISTMASK_DEBUG=1` (see [Debug Builds](#debug-builds)) — and
`src/shared/constants.js` reads it. It cannot be changed after the bundle is
produced.
The debug-mode toggle in settings is a separate, runtime-only flag. It raises
the log level and turns the banner on, and that is all it may ever do: it feeds
`isDebug()` in `src/shared/log.js`, which is deliberately not what
`generateMnemonic()` consults. Mnemonic generation reads the build-time `DEBUG`
constant directly, so no runtime toggle in a release build can reach the
hardcoded test phrase.
### Key Decisions ### Key Decisions
- **No framework**: The popup UI is vanilla JS and HTML. The extension is small - **No framework**: The popup UI is vanilla JS and HTML. The extension is small

67
TODO.md
View File

@@ -10,31 +10,26 @@
# Status # Status
pre-1.0, working towards the 1.0.0 milestone. Tagged v0.1.0 on 2026-02-27. No pre-1.0. Tagged v0.1.0 on 2026-02-27. Active development on branch
other branch is in flight: the settings About well landed as #145 on 2026-07-26 feat/issue-144-settings-about (another agent working as of 2026-07-06). Full
and scripts-to-rule-them-all landed as #148, so the `scripts/` directory policy file set present; make check on main not verified.
question is resolved. Full policy file set present. `make check` verified
passing on `main` at `23aeae4` on 2026-08-09. The 1.0.0 backlog is filed as
#149-#168.
# Next Step # Next Step
Land #149: make `DEBUG` a build-time constant that defaults to off, injected as Land feat/issue-144-settings-about: finish the settings About well (build info,
the `__BUILD_DEBUG__` esbuild define from `AUTISTMASK_DEBUG=1`, so a plain app name and repo link, release date, version click easter egg, git info derived
`make build` stops handing every newly created wallet the publicly committed inside Docker), resolve the untracked scripts/ directory (commit or gitignore),
test recovery phrase. Branch `fix/issue-149-debug-build-flag`; PR open, awaiting get review, merge to main.
review.
# Completed Steps # Completed Steps
- 2026-08-09: Reviewed the repo end to end and filed the 1.0.0 backlog - 2026-08-09: Test coverage for the address-poisoning defense in
(#149-#168). `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 - 2026-07-07 Adopted scripts-to-rule-them-all: `script/` entrypoints, Makefile
shims, README Entrypoints section (#148) shims, README Entrypoints section
- 2026-03-01: USD display suppressed on testnets (#142); estimated USD for ETH - 2026-03-01: About well in settings with build info and easter egg (in flight
in approve-tx view (#141). on feature branch); 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 - Sepolia testnet support (#137); etherscan links go to token-specific URLs
(#136). (#136).
- Transaction detail improvements: Type field and on-chain details (#130), - Transaction detail improvements: Type field and on-chain details (#130),
@@ -52,32 +47,12 @@ review.
# Future Steps # Future Steps
- Fix the two `ReferenceError` crashes that make whole screens unreachable: - Verify main passes make check after the feature branch merges (not verified
AddToken (#150) and TransactionDetail for every ERC-20 transfer (#151). 2026-07-06 because an agent was active in the tree); fix anything red. main
- Add ESLint to `script/lint` (#152). `make check` is `prettier --check` only must always be green.
and cannot catch undefined identifiers, which is how #150 and #151 shipped. - Prune stale branches: dozens of merged local and remote feature branches
- Make the Firefox target functional: Chrome callback APIs are used against the remain (fix/_, feature/_, tx-\*); delete merged ones locally and on origin.
promise-only `browser` namespace (#153). - Continue the issue backlog toward a feature-complete wallet, then cut further
- Send and transaction-flow correctness: gas fee excluded from the tags as milestones land.
insufficient-balance check (#154), WaitTx 60s timeout overwriting a rendered
success screen (#155), last-wallet deletion leaving inconsistent state (#156).
- Security: plaintext password crossing the extension messaging boundary during
dApp approvals (#157); MV3 service worker termination killing the background
refresh and the 24h phishing list update (#158).
- Test the crypto core — `wallet.js` derivation and `vault.js` encryption (#159)
— and the address-poisoning defense in `transactions.js` (#160).
- Wallet features for 1.0: show a wallet's recovery phrase behind the password
(#161), delete an address from an HD wallet (#162).
- Docs: `docs/README.md` contradicts the code on external services and names
competitors (#163); README Screen Map omits three shipped screens (#164).
- Owner decisions: Sepolia support versus "Non-Goals for 1.0", and `isMetaMask`
naming a competitor in shipped code (#165).
- Repo policy compliance sweep: test rerun pattern, `yarn`/`npx`, frozen
lockfile, undocumented Makefile targets (#166).
- Prune the 24 stale remote feature branches (#167).
- Remove dead exports and de-duplicate copy-pasted view helpers (#168).
- Pre-1.0 security review of the extension (key handling, DEBUG mode policy, RPC - Pre-1.0 security review of the extension (key handling, DEBUG mode policy, RPC
input validation) before any 1.0rc tag; #149 and #157 are parts of it, but the input validation) before any 1.0rc tag.
review is broader than either.
- Cut 1.0.0 once the milestone is empty, then continue tagging as milestones
land.

View File

@@ -11,14 +11,6 @@ function ensureDir(dir) {
fs.mkdirSync(dir, { recursive: true }); fs.mkdirSync(dir, { recursive: true });
} }
// DEBUG is a build-time flag, off unless explicitly requested. It is the only
// thing that makes the hardcoded test mnemonic reachable, so the opt-in must be
// exact: anything other than the literal "1" (unset, empty, "true", a typo)
// produces a release build. Failing towards the safe mode is deliberate.
function isDebugBuild() {
return process.env.AUTISTMASK_DEBUG === "1";
}
function getBuildInfo() { function getBuildInfo() {
const pkg = JSON.parse( const pkg = JSON.parse(
fs.readFileSync(path.join(__dirname, "package.json"), "utf8"), fs.readFileSync(path.join(__dirname, "package.json"), "utf8"),
@@ -55,15 +47,7 @@ async function build() {
const buildInfo = getBuildInfo(); const buildInfo = getBuildInfo();
console.log("Build info:", buildInfo); console.log("Build info:", buildInfo);
const debugBuild = isDebugBuild();
console.log(
debugBuild
? "Build mode: DEBUG (INSECURE - hardcoded test mnemonic, do not ship)"
: "Build mode: release (DEBUG off)",
);
const define = { const define = {
__BUILD_DEBUG__: JSON.stringify(debugBuild),
__BUILD_VERSION__: JSON.stringify(buildInfo.version), __BUILD_VERSION__: JSON.stringify(buildInfo.version),
__BUILD_LICENSE__: JSON.stringify(buildInfo.license), __BUILD_LICENSE__: JSON.stringify(buildInfo.license),
__BUILD_AUTHOR__: JSON.stringify(buildInfo.author), __BUILD_AUTHOR__: JSON.stringify(buildInfo.author),

View File

@@ -1,12 +1,4 @@
// DEBUG is a build-time constant injected by esbuild's define in build.js const DEBUG = true;
// (see src/shared/buildInfo.js for the same pattern). It is false unless the
// bundle was produced with AUTISTMASK_DEBUG=1, and it is false whenever the
// module is loaded outside a bundle (tests, plain require). It must never be
// derived from anything the user can change at runtime: it is what gates the
// hardcoded test mnemonic below.
/* global __BUILD_DEBUG__ */
const DEBUG = typeof __BUILD_DEBUG__ !== "undefined" ? __BUILD_DEBUG__ : false;
const DEBUG_MNEMONIC = const DEBUG_MNEMONIC =
"cube evolve unfold result inch risk jealous skill hotel bulb night wreck"; "cube evolve unfold result inch risk jealous skill hotel bulb night wreck";

View File

@@ -5,10 +5,6 @@ const { Mnemonic, HDNodeWallet, Wallet } = require("ethers");
const { DEBUG, DEBUG_MNEMONIC, BIP44_ETH_PATH } = require("./constants"); const { DEBUG, DEBUG_MNEMONIC, BIP44_ETH_PATH } = require("./constants");
function generateMnemonic() { function generateMnemonic() {
// This must stay the compile-time DEBUG constant. Do NOT switch it to
// isDebug() from log.js: that also ORs in the runtime debugMode flag the
// settings toggle drives, which would let a user of a release build turn
// the hardcoded, publicly known test phrase back on for real wallets.
if (DEBUG) return DEBUG_MNEMONIC; if (DEBUG) return DEBUG_MNEMONIC;
const m = Mnemonic.fromEntropy( const m = Mnemonic.fromEntropy(
globalThis.crypto.getRandomValues(new Uint8Array(16)), globalThis.crypto.getRandomValues(new Uint8Array(16)),

1002
tests/transactions.test.js Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -1,94 +0,0 @@
// Tests for the DEBUG build flag as it gates mnemonic generation.
//
// The modules read the __BUILD_DEBUG__ global that esbuild replaces at bundle
// time. Under jest the global is absent, which is exactly the release-build
// case; the debug-build case is exercised by defining the global and
// re-requiring the modules with a fresh registry.
const WORDS_IN_12_WORD_PHRASE = 12;
function loadWallet() {
const constants = require("../src/shared/constants");
const wallet = require("../src/shared/wallet");
const log = require("../src/shared/log");
return { constants, wallet, log };
}
describe("generateMnemonic in a release build", () => {
beforeEach(() => {
jest.resetModules();
delete globalThis.__BUILD_DEBUG__;
});
test("DEBUG defaults to false when the build define is absent", () => {
const { constants } = loadWallet();
expect(constants.DEBUG).toBe(false);
});
test("returns fresh, valid 12-word phrases that are not the test phrase", () => {
const { constants, wallet } = loadWallet();
const first = wallet.generateMnemonic();
const second = wallet.generateMnemonic();
expect(first).not.toBe(second);
for (const phrase of [first, second]) {
expect(wallet.isValidMnemonic(phrase)).toBe(true);
expect(phrase.split(" ")).toHaveLength(WORDS_IN_12_WORD_PHRASE);
expect(phrase).not.toBe(constants.DEBUG_MNEMONIC);
}
});
test("derives a usable HD wallet from the generated phrase", () => {
const { wallet } = loadWallet();
const { xpub, firstAddress } = wallet.hdWalletFromMnemonic(
wallet.generateMnemonic(),
);
expect(xpub.startsWith("xpub")).toBe(true);
expect(firstAddress).toMatch(/^0x[0-9a-fA-F]{40}$/);
});
test("the runtime debug toggle cannot re-enable the test phrase", () => {
const { constants, wallet, log } = loadWallet();
// What the settings easter-egg toggle does at runtime.
log.setRuntimeDebug(true);
expect(log.isDebug()).toBe(true);
const phrase = wallet.generateMnemonic();
expect(phrase).not.toBe(constants.DEBUG_MNEMONIC);
expect(wallet.isValidMnemonic(phrase)).toBe(true);
expect(phrase).not.toBe(wallet.generateMnemonic());
log.setRuntimeDebug(false);
});
});
describe("generateMnemonic in a debug build", () => {
beforeEach(() => {
jest.resetModules();
globalThis.__BUILD_DEBUG__ = true;
});
afterEach(() => {
delete globalThis.__BUILD_DEBUG__;
});
test("DEBUG is true and the test phrase is returned", () => {
const { constants, wallet } = loadWallet();
expect(constants.DEBUG).toBe(true);
expect(wallet.generateMnemonic()).toBe(constants.DEBUG_MNEMONIC);
});
test("the test phrase is itself a valid 12-word BIP-39 phrase", () => {
const { constants, wallet } = loadWallet();
expect(wallet.isValidMnemonic(constants.DEBUG_MNEMONIC)).toBe(true);
expect(constants.DEBUG_MNEMONIC.split(" ")).toHaveLength(
WORDS_IN_12_WORD_PHRASE,
);
});
});