Chrome key.manifest/chrome.json now carries a fixed key — the public half of a 2048-bit RSA keypair, DER SPKI, base64. Without it an unpacked extension's id, and therefore its chrome.storage.local partition, 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 at gipbhkogfopeahplcjhipkgpcimdpkip, asserted as a literal in tests/extensionId.test.js (recomputing it from whatever key happens 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/*.key are gitignored, and tests/extensionId.test.js fails 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 — changing key later changes the extension id and orphans the storage partition.
make package / script/package. Runs make build first — deliberately, rather than re-implementing it: that target is the only audited path to a release build (receipt outside the repo, AUTISTMASK_DEBUG scrubbed from the verifier's environment, --expect release stated as an argument, script/discard-dist-on-failure on every step), and script/test-verify-build asserts that wiring by reading make -n back. A second path to dist/ 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 package twice and diffing SHA256SUMS — identical.
The dist/ root trap, handled explicitly.dist/styles.css sits 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; the dist/ root files are printed as deliberately not shipped rather than dropped by a glob:
Not shipped (dist/ root, outside every browser directory, and referenced by
nothing inside one): styles.css
autistmask-chrome-0.1.0.zip: 7 member(s), 1807533 bytes, sha256 90ee04bc...
autistmask-firefox-0.1.0.xpi: 7 member(s), 1807169 bytes, sha256 ac629b65...
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.js is a ~200-line deterministic zip writer and reader on stdlib zlib. Output verified against Info-ZIP unzip -t and Python zipfile (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.js requires package.json, manifest/chrome.json and manifest/firefox.json to agree, and build.js calls it before anything is emitted. Demonstration — package.json set to 0.2.0, manifests left at 0.1.0:
$ make package
Building AutistMask extension...
Build failed: the declared versions disagree, so this tree has no version:
package.json=0.2.0, manifest/chrome.json=0.1.0, manifest/firefox.json=0.1.0.
Set all of them to the same value: the manifests are what the browser reports
and package.json is what the About screen shows, and a build that picked one of
them would ship the disagreement.
make: *** [Makefile:45: package] Error 2
make check goes red on the same tree (tests/version.test.js, 2 failures). Reverted afterwards; the branch is at 0.1.0 everywhere.
BUILD_COMMIT dirty. Now <short>-dirty when git status --porcelain is non-empty, <short>-unknown when git answers the hash but not the status. Observed on a modified tree: commitHash: 'cef6aaa-dirty'. BUILD_COMMIT_FULL stays clean deliberately — it is the href of the About screen's commit link, and abc123-dirty is 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 next at 669c443.
make test-e2e — exit 0. Existing suite 55/55, plus the new tests/e2e/storagePartition.js 5/5. Two unpacked loads from two different paths in one profile, the shipped manifest and then the same build with key stripped:
# with `key`: /tmp/autistmask-keyed-a-MGYu4G/chrome -> gipbhkogfopeahplcjhipkgpcimdpkip
# with `key`: /tmp/autistmask-keyed-b-YMM2Mi/chrome -> gipbhkogfopeahplcjhipkgpcimdpkip
# with `key`: sentinel read back: written-by-the-first-load
# without `key`: /tmp/autistmask-bare-a-9MA6Pa/chrome -> goiibkladdekmnhflfmglfkkkmknjnil
# without `key`: /tmp/autistmask-bare-b-h5p3zu/chrome -> kjpehecppkpndiimfalijhkcpopedfoj
# without `key`: sentinel read back: null
# both keyed copies loaded at once: 1 extension id(s): gipbhkogfopeahplcjhipkgpcimdpkip
So: with key, storage IS shared across paths; without it, it is not — the second load reads null where 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 new tests/e2e/firefox/reinstall.js 7/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:
ok 1 - the artifact installs and reports the manifest's gecko id
ok 2 - a wallet created through the UI is in extension storage
ok 3 - the add-on installs again after a browser restart with the same id
ok 4 - after a restart and re-add, the extension does not come up as a fresh install
ok 5 - the vault, xpub and first address survive the restart unchanged
ok 6 - the vault still decrypts with the original password to the original recovery phrase
# after an explicit uninstall: {"present":false}
ok 7 - an explicit uninstall DESTROYS the vault (observed Firefox behaviour: Remove is irreversible, unlike a restart)
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 of extensions.webextensions.uuids after 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.md says so
No AMO attempt: self-distribution needs credentials this unit does not have. README.md now states plainly that release Firefox and ESR refuse the artifact with no working override, that Developer Edition, Nightly or an Unbranded build with xpinstall.signatures.required=false is 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-build 46 cases, check-censored 163 files, ESLint and prettier in the pinned container.
make test-e2e: green, exit 0.
make test-e2e-firefox: green, exit 0.
Rebased onto next at 669c443; conflicts in README.md and TODO.md (the make clean line and the Completed Steps head) resolved by hand, make check re-run green afterwards.
No stray containers: every docker run is --rm, docker ps -a empty 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.
Closes https://git.eeqj.de/sneak/AutistMask/issues/310.
## What changed
**Chrome `key`.** `manifest/chrome.json` now carries a fixed `key` — the public half of a 2048-bit RSA keypair, DER SPKI, base64. Without it an unpacked extension's id, and therefore its `chrome.storage.local` partition, 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 at `gipbhkogfopeahplcjhipkgpcimdpkip`, asserted as a literal in `tests/extensionId.test.js` (recomputing it from whatever `key` happens 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`/`*.key` are gitignored, and `tests/extensionId.test.js` fails 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 — changing `key` later changes the extension id and orphans the storage partition.
**`make package` / `script/package`.** Runs `make build` first — deliberately, rather than re-implementing it: that target is the only audited path to a release build (receipt outside the repo, `AUTISTMASK_DEBUG` scrubbed from the verifier's environment, `--expect release` stated as an argument, `script/discard-dist-on-failure` on every step), and `script/test-verify-build` asserts that wiring by reading `make -n` back. A second path to `dist/` for the release artifacts specifically is the last place that should exist. Then it writes:
```
release/autistmask-chrome-0.1.0.zip
release/autistmask-firefox-0.1.0.xpi
release/SHA256SUMS
```
Deterministic: entries sorted, timestamps fixed at the format epoch, compression level fixed. Verified by running `make package` twice and diffing `SHA256SUMS` — identical.
**The `dist/` root trap, handled explicitly.** `dist/styles.css` sits 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; the `dist/` root files are printed as deliberately not shipped rather than dropped by a glob:
```
Not shipped (dist/ root, outside every browser directory, and referenced by
nothing inside one): styles.css
autistmask-chrome-0.1.0.zip: 7 member(s), 1807533 bytes, sha256 90ee04bc...
autistmask-firefox-0.1.0.xpi: 7 member(s), 1807169 bytes, sha256 ac629b65...
```
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.js` is a ~200-line deterministic zip writer and reader on stdlib `zlib`. Output verified against Info-ZIP `unzip -t` and Python `zipfile` (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.js` requires `package.json`, `manifest/chrome.json` and `manifest/firefox.json` to agree, and `build.js` calls it before anything is emitted. Demonstration — `package.json` set to `0.2.0`, manifests left at `0.1.0`:
```
$ make package
Building AutistMask extension...
Build failed: the declared versions disagree, so this tree has no version:
package.json=0.2.0, manifest/chrome.json=0.1.0, manifest/firefox.json=0.1.0.
Set all of them to the same value: the manifests are what the browser reports
and package.json is what the About screen shows, and a build that picked one of
them would ship the disagreement.
make: *** [Makefile:45: package] Error 2
```
`make check` goes red on the same tree (`tests/version.test.js`, 2 failures). Reverted afterwards; the branch is at `0.1.0` everywhere.
**`BUILD_COMMIT` dirty.** Now `<short>-dirty` when `git status --porcelain` is non-empty, `<short>-unknown` when git answers the hash but not the status. Observed on a modified tree: `commitHash: 'cef6aaa-dirty'`. `BUILD_COMMIT_FULL` stays clean deliberately — it is the href of the About screen's commit link, and `abc123-dirty` is 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 `next` at `669c443`.
**`make test-e2e` — exit 0.** Existing suite 55/55, plus the new `tests/e2e/storagePartition.js` 5/5. Two unpacked loads from two different paths in one profile, the shipped manifest and then the same build with `key` stripped:
```
# with `key`: /tmp/autistmask-keyed-a-MGYu4G/chrome -> gipbhkogfopeahplcjhipkgpcimdpkip
# with `key`: /tmp/autistmask-keyed-b-YMM2Mi/chrome -> gipbhkogfopeahplcjhipkgpcimdpkip
# with `key`: sentinel read back: written-by-the-first-load
# without `key`: /tmp/autistmask-bare-a-9MA6Pa/chrome -> goiibkladdekmnhflfmglfkkkmknjnil
# without `key`: /tmp/autistmask-bare-b-h5p3zu/chrome -> kjpehecppkpndiimfalijhkcpopedfoj
# without `key`: sentinel read back: null
# both keyed copies loaded at once: 1 extension id(s): gipbhkogfopeahplcjhipkgpcimdpkip
```
So: **with `key`, storage IS shared across paths; without it, it is not** — the second load reads `null` where 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 new `tests/e2e/firefox/reinstall.js` 7/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:
```
ok 1 - the artifact installs and reports the manifest's gecko id
ok 2 - a wallet created through the UI is in extension storage
ok 3 - the add-on installs again after a browser restart with the same id
ok 4 - after a restart and re-add, the extension does not come up as a fresh install
ok 5 - the vault, xpub and first address survive the restart unchanged
ok 6 - the vault still decrypts with the original password to the original recovery phrase
# after an explicit uninstall: {"present":false}
ok 7 - an explicit uninstall DESTROYS the vault (observed Firefox behaviour: Remove is irreversible, unlike a restart)
```
**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 of `extensions.webextensions.uuids` after 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.md` says so
No AMO attempt: self-distribution needs credentials this unit does not have. `README.md` now states plainly that release Firefox and ESR refuse the artifact with no working override, that Developer Edition, Nightly or an Unbranded build with `xpinstall.signatures.required=false` is 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-build` 46 cases, `check-censored` 163 files, ESLint and prettier in the pinned container.
- `make test-e2e`: **green**, exit 0.
- `make test-e2e-firefox`: **green**, exit 0.
- Rebased onto `next` at `669c443`; conflicts in `README.md` and `TODO.md` (the `make clean` line and the Completed Steps head) resolved by hand, `make check` re-run green afterwards.
- No stray containers: every `docker run` is `--rm`, `docker ps -a` empty 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.
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.
clawbot
self-assigned this 2026-08-23 16:02:55 +02:00
PASS — reviewed independently in a fresh clone at a874299: make check, make test-e2e and make test-e2e-firefox all green here; storage-partition behaviour, all three version-disagreement cases, the AUTISTMASK_DEBUG=1 failure-plus-dist/-removal boundary and the dirty BUILD_COMMIT stamp were each reproduced by hand, the artifacts were unpacked with unzip and verified self-contained, my SHA256SUMS matched 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:
The publicly committed test recovery phrase (DEBUG_MNEMONIC, src/shared/constants.js) survives as a dead string literal in both release artifacts' bundles. Pre-existing and unreachable — both bundles verify as autistmask-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.
The committed-private-key guard in tests/extensionId.test.js matches on filename extension only (.pem, .key, .p12, .pfx), so key material committed under any other name would pass it.
PASS — reviewed independently in a fresh clone at `a874299`: `make check`, `make test-e2e` and `make test-e2e-firefox` all green here; storage-partition behaviour, all three version-disagreement cases, the `AUTISTMASK_DEBUG=1` failure-plus-`dist/`-removal boundary and the dirty `BUILD_COMMIT` stamp were each reproduced by hand, the artifacts were unpacked with `unzip` and verified self-contained, my `SHA256SUMS` matched 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:
- The publicly committed test recovery phrase (`DEBUG_MNEMONIC`, `src/shared/constants.js`) survives as a dead string literal in both release artifacts' bundles. Pre-existing and unreachable — both bundles verify as `autistmask-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.
- The committed-private-key guard in `tests/extensionId.test.js` matches on filename extension only (`.pem`, `.key`, `.p12`, `.pfx`), so key material committed under any other name would pass it.
clawbot
merged commit 769f6a5289 into next2026-08-23 16:13:23 +02:00
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.
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.