There was no packaging target anywhere, no artifact, and no `key` in
`manifest/chrome.json` — so an unpacked Chrome load derived its extension
id, and therefore its `chrome.storage.local` partition, from the absolute
checkout path. Moving or re-cloning the checkout presented an empty
wallet, with no error and nothing in the UI to say so.
`manifest/chrome.json` now carries a fixed `key`: the public half of an
RSA keypair, which pins the extension id to
`gipbhkogfopeahplcjhipkgpcimdpkip`. The private half is a credential and
is not in this repo; no target generates one into the working tree, and
`tests/extensionId.test.js` fails if a `.pem` is ever committed. Changing
`key` changes the id and orphans every wallet stored under the old one.
`make package` (script/package) runs `make build` — the only audited path
to a release build — and writes one self-contained, versioned archive per
browser into `release/`, plus `SHA256SUMS`. The archives are deterministic:
entries sorted, timestamps fixed, compression level fixed, so two builds
of one commit are byte-identical. Self-containment is checked rather than
assumed: every path the manifests and the popup HTML reference is resolved
and required to be inside the archive, a reference that climbs out of the
extension root is a hard failure, and files left at the `dist/` root —
`dist/styles.css`, which build.js copies into each browser directory — are
reported as deliberately not shipped rather than dropped by a glob. The
archive is then read back off disk and compared member by member against
the directory it was built from. The zip writer and reader are stdlib zlib
in `script/lib/zip.js`; no new dependency, and nothing unpinned.
One version, enforced rather than generated. `script/lib/version.js`
requires `package.json`, `manifest/chrome.json` and `manifest/firefox.json`
to agree and fails the build naming each file and what it said, instead of
reading from one of the three. `BUILD_COMMIT` now carries `-dirty` when the
working tree does not match `HEAD`, and `-unknown` when git cannot say;
the full hash behind the About screen's commit link stays clean so the link
still resolves.
Two real-browser observations, both run through the pinned harnesses:
- `tests/e2e/storagePartition.js` loads the build from two different paths
in one Chrome profile. With `key`: same id, and the second load reads the
first load's storage. Without `key`: different ids, and the second load
sees an empty partition. Loading both keyed copies at once yields one id,
not two.
- `tests/e2e/firefox/reinstall.js` installs the packaged XPI in a real
Firefox, creates a wallet, quits the browser, restarts on the same
profile, adds the add-on again, and decrypts the vault back to the
original recovery phrase. It then observes that an explicit uninstall
DESTROYS that storage — correct browser behaviour, but for a wallet it
means Remove is irreversible except from the recovery phrase, so
README.md says so.
Firefox ships an UNSIGNED XPI. README.md states plainly that release
Firefox and ESR will refuse it, that Developer Edition, Nightly or an
Unbranded build is required, and that a temporary add-on does not survive
a browser restart. AMO signing, CRX packing, tagging and any upload are
deliberately out of scope.
A failed release build no longer leaves a complete, loadable debug bundle in dist/ whose every wallet uses the publicly committed test recovery phrase. Each step of the release build runs through script/discard-dist-on-failure, which removes dist/ on failure, says on stderr that it did and why, and returns the step's own status. build-debug is deliberately unwrapped. script/verify-build is untouched.
README, the script synopsis, its header paragraph and the check_dist_tree comment now all say the same thing: regular files and symlinks under dist/ are covered; fifos, sockets, device nodes and empty directories are not, and why. No behaviour change — the walk is untouched.
verify-build read its expectation from AUTISTMASK_DEBUG in its own environment
and the Makefile invoked it bare, so an operator with that variable exported
who ran the release target got an INSECURE debug build — every wallet it
creates uses the publicly committed test phrase — verified green, exit 0. It
also had no provenance: a 26-byte file containing the right marker string
passed, the content script and manifest.json were never inspected, and an
entire hand-written dist/ passed.
--expect release|debug and --receipt PATH are now both required, with no
defaults and nothing read from the environment. build.js records every file it
emits with its sha256 and writes the receipt; the Makefile mktemps it outside
the repo per invocation with a trap, and build.js refuses a receipt path inside
dist/. Verification runs three passes in a load-bearing order — receipt shape,
full dist/ walk, then per-file bytes — so an unwalkable subtree cannot make
files look absent. dist/constants-bundles.txt, which was an unsigned trust root
living inside the tree it vouched for, is gone.
What this proves is bounded and stated as such: dist/ is byte-for-byte the
output of the build.js run that just finished, within one make build
invocation. It proves nothing about the honesty of the source tree or build.js,
and nothing to anyone handed a dist/ from elsewhere — that is signing, #310.
The standalone make verify-build target is removed because its only input would
be dist/ itself, i.e. the artifact vouching for itself.
Verified: make check green, test-verify-build 39 cases (was 18), test-e2e 55/55
and test-e2e-firefox 8/8 with make build running uncached inside both images.
All four original bypasses now exit 1. Mutations: digests disabled fails
exactly 4 cases, dropping the dist/ walk fails exactly 8, restoring the ambient
fallback fails exactly 1.
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.
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.
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#150closes#151