release: package the extension, pin the Chrome extension id, and prove the wallet survives a reinstall (closes #310) #347
Reference in New Issue
Block a user
Delete Branch "release/310-packaging-artifacts"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Closes #310.
What changed
Chrome
key.manifest/chrome.jsonnow carries a fixedkey— the public half of a 2048-bit RSA keypair, DER SPKI, base64. Without it an unpacked extension's id, and therefore itschrome.storage.localpartition, is derived from the absolute load path: moving or re-cloning the checkout presents an empty wallet with no error. The id is now pinned atgipbhkogfopeahplcjhipkgpcimdpkip, asserted as a literal intests/extensionId.test.js(recomputing it from whateverkeyhappens to be in the manifest would pass after someone replaced the key, which is the change that orphans every wallet).Where a packer would get the private key. It is NOT in this repo, no target generates one into the working tree,
*.pem/*.keyare gitignored, andtests/extensionId.test.jsfails if such a file is ever committed. It is not needed to build, load or test anything here — it only signs a CRX. I generated the pair outside the repo and left the private half at/srv/code/_clawbot_secrets/autistmask-chrome-crx-key.pem, mode 0600, on this host. A packer would take it from there or from wherever you move it:chrome --pack-extension=dist/chrome --pack-extension-key=<path to the .pem>. Decision for you: if you would rather own a key you generated yourself, regenerate NOW, before any wallet exists — changingkeylater changes the extension id and orphans the storage partition.make package/script/package. Runsmake buildfirst — deliberately, rather than re-implementing it: that target is the only audited path to a release build (receipt outside the repo,AUTISTMASK_DEBUGscrubbed from the verifier's environment,--expect releasestated as an argument,script/discard-dist-on-failureon every step), andscript/test-verify-buildasserts that wiring by readingmake -nback. A second path todist/for the release artifacts specifically is the last place that should exist. Then it writes:Deterministic: entries sorted, timestamps fixed at the format epoch, compression level fixed. Verified by running
make packagetwice and diffingSHA256SUMS— identical.The
dist/root trap, handled explicitly.dist/styles.csssits outside both browser directories. The packager resolves every path referenced by the manifest and by every HTML document, requires each to be inside the archive, and fails hard on a reference that climbs out of the extension root; thedist/root files are printed as deliberately not shipped rather than dropped by a glob:The archive is then read back off disk, every member's CRC checked, every member's sha256 compared against the directory, and the self-containment check re-run against the archive's own contents. An archive nobody opened is a claim, not an artifact.
No new dependency.
script/lib/zip.jsis a ~200-line deterministic zip writer and reader on stdlibzlib. Output verified against Info-ZIPunzip -tand Pythonzipfile(both read all 7 members, no errors), and the XPI is loaded by a real Firefox in the suite below.One version, enforced.
script/lib/version.jsrequirespackage.json,manifest/chrome.jsonandmanifest/firefox.jsonto agree, andbuild.jscalls it before anything is emitted. Demonstration —package.jsonset to0.2.0, manifests left at0.1.0:make checkgoes red on the same tree (tests/version.test.js, 2 failures). Reverted afterwards; the branch is at0.1.0everywhere.BUILD_COMMITdirty. Now<short>-dirtywhengit status --porcelainis non-empty,<short>-unknownwhen git answers the hash but not the status. Observed on a modified tree:commitHash: 'cef6aaa-dirty'.BUILD_COMMIT_FULLstays clean deliberately — it is the href of the About screen's commit link, andabc123-dirtyis not a commit anyone can fetch. The displayed short hash carries the marker.Browser test results
Both run on this branch after the rebase onto
nextat669c443.make test-e2e— exit 0. Existing suite 55/55, plus the newtests/e2e/storagePartition.js5/5. Two unpacked loads from two different paths in one profile, the shipped manifest and then the same build withkeystripped:So: with
key, storage IS shared across paths; without it, it is not — the second load readsnullwhere the first wrote. Loading both keyed copies simultaneously in one profile yields one id, not two: Chrome does not load a second copy of an id it already has. The assertions are annotated as observations, so a Chrome that ever changes this fails the run rather than passing it.make test-e2e-firefox— exit 0. Existing suite 8/8, plus the newtests/e2e/firefox/reinstall.js7/7, which installs the packaged XPI (/work/release/autistmask-firefox-0.1.0.xpi) — the only place a real Firefox is asked to load the artifact that would actually be handed to someone. Two questions, two answers:Finding, and a deviation from the issue's wording. The DoD says "install, create a wallet, uninstall, re-install, assert the vault is still there". I wrote that first and it FAILED: Firefox destroys an add-on's storage on an explicit uninstall, so the popup came back on the welcome screen with no wallet. That is correct browser behaviour and not a defect here, but it means the literal DoD assertion is false and I have not asserted it. What I assert instead is the operation the issue is actually about for Firefox — a browser restart on the same profile with the temporary add-on added again, which is what daily use is, since a temporary add-on is unloaded on exit. That passes, including decrypting the vault back to the original recovery phrase through the real Show Recovery Phrase screen. The explicit-uninstall result is kept as an asserted observation and stated in
README.md: on Firefox, about:addons "Remove" is irreversible except from the recovery phrase.Two harness facts this turned up, both now commented at the point they bite: uninstalling while the popup is the session's only window discards the browsing context and every later WebDriver command fails with "no such window"; and the
moz-extension://uuid is re-minted per install, with a navigation to a stale one hanging rather than failing, so the uuid is read live out ofextensions.webextensions.uuidsafter every install.Driver.newSession()takes an optional profile directory so two browser runs can share one profile — that is what makes "survives a restart" askable at all.Firefox is unsigned, and
README.mdsays soNo AMO attempt: self-distribution needs credentials this unit does not have.
README.mdnow states plainly that release Firefox and ESR refuse the artifact with no working override, that Developer Edition, Nightly or an Unbranded build withxpinstall.signatures.required=falseis required, and that a temporary add-on does not survive a browser restart. If AMO signing is wanted for 1.0 that is a separate issue and your decision — I have not filed it.Verification
make check: green on this branch after the rebase — 45 suites, 881 tests,test-verify-build46 cases,check-censored163 files, ESLint and prettier in the pinned container.make test-e2e: green, exit 0.make test-e2e-firefox: green, exit 0.nextat669c443; conflicts inREADME.mdandTODO.md(themake cleanline and the Completed Steps head) resolved by hand,make checkre-run green afterwards.docker runis--rm,docker ps -aempty afterwards. No prune of any kind.Out of scope, deliberately
Tagging, CRX packing, AMO signing and any upload. Producing and verifying the artifact is here; publishing is yours.
PASS — reviewed independently in a fresh clone at
a874299:make check,make test-e2eandmake test-e2e-firefoxall green here; storage-partition behaviour, all three version-disagreement cases, theAUTISTMASK_DEBUG=1failure-plus-dist/-removal boundary and the dirtyBUILD_COMMITstamp were each reproduced by hand, the artifacts were unpacked withunzipand verified self-contained, mySHA256SUMSmatched the PR body byte-for-byte from a different absolute path, and no private key material exists in the tree or is reachable from any build target. The Firefox uninstall claim is true — I observed{"present":false}myself — and asserting the literal DoD item's real (negative) outcome while substituting the restart cycle is the honest answer, not a dodge.Two disclosures, neither a defect of this PR and neither warranting rework:
DEBUG_MNEMONIC,src/shared/constants.js) survives as a dead string literal in both release artifacts' bundles. Pre-existing and unreachable — both bundles verify asautistmask-build-debug=off— but this PR is the first thing to ship an artifact at all, so it is now a 12-word BIP-39 string sitting in a distributed wallet build. Worth a separate issue.tests/extensionId.test.jsmatches on filename extension only (.pem,.key,.p12,.pfx), so key material committed under any other name would pass it.