release: produce a versioned per-browser artifact and pin the Chrome extension id (closes #310)
manifest/chrome.json now carries a fixed public key, so the extension id and the chrome.storage.local partition holding the wallet stay stable across checkout moves and re-clones instead of being derived from the absolute path. A release entrypoint produces a self-contained versioned artifact per browser, including the files that sit at dist/ root outside both browser directories. One version source of truth, enforced: the build fails naming the culprit when the two manifests and package.json disagree, and BUILD_COMMIT now marks a dirty tree as dirty. Firefox ships an unsigned XPI; the README states that release Firefox and ESR refuse it, that Developer Edition or Unbranded is required, and that Remove is irreversible except from the recovery phrase, which is asserted by test.
This commit was merged in pull request #347.
This commit is contained in:
153
README.md
153
README.md
@@ -44,7 +44,107 @@ Load the extension:
|
||||
- **Chrome**: Navigate to `chrome://extensions/`, enable "Developer mode", click
|
||||
"Load unpacked", and select the `dist/chrome/` directory.
|
||||
- **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`. Read
|
||||
[Installing on Firefox](#installing-on-firefox) before relying on this: a
|
||||
temporary add-on does not survive closing the browser.
|
||||
|
||||
### Release Artifacts
|
||||
|
||||
`make package` runs `make build` and then writes one self-contained, versioned
|
||||
archive per browser into `release/`, plus a `SHA256SUMS` for them:
|
||||
|
||||
```bash
|
||||
make package
|
||||
```
|
||||
|
||||
```
|
||||
release/autistmask-chrome-<version>.zip
|
||||
release/autistmask-firefox-<version>.xpi
|
||||
release/SHA256SUMS
|
||||
```
|
||||
|
||||
Nothing is published by this. Tagging, CRX packing and any upload are
|
||||
outward-facing acts and are the owner's alone.
|
||||
|
||||
The archives are deterministic — entries sorted, timestamps fixed, compression
|
||||
level fixed — so two builds of one commit produce byte-identical files and the
|
||||
recorded digest is a property of the input rather than of the clock.
|
||||
|
||||
**Self-containment is checked, not assumed.** `build.js` writes the compiled
|
||||
Tailwind output to `dist/styles.css` at the `dist/` ROOT, outside both browser
|
||||
directories, and copies it into each of them as `src/popup/styles.css`; a naive
|
||||
`zip -r dist/chrome` is therefore correct only by accident. So the packager
|
||||
resolves every path referenced by the manifest and by every HTML document in the
|
||||
archive, requires each to be inside the archive, and fails on any reference that
|
||||
climbs out of the extension root. Files left at the `dist/` root are printed 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: an archive nobody opened is a claim, not an artifact.
|
||||
|
||||
There is one version, and the build enforces it. `package.json`,
|
||||
`manifest/chrome.json` and `manifest/firefox.json` each declare one and none is
|
||||
derived from another — the manifests are copied to `dist/` verbatim, which is
|
||||
what `tests/manifest.test.js` asserts — so `script/lib/version.js` requires all
|
||||
three to agree and **fails the build when they do not**, naming each file and
|
||||
what it said. `tests/version.test.js` covers that rule in `make check`.
|
||||
|
||||
### Installing on Chrome
|
||||
|
||||
`manifest/chrome.json` carries a fixed `key`: the public half of an RSA keypair,
|
||||
base64-encoded DER. It exists for one reason. An unpacked Chrome extension with
|
||||
no `key` gets an extension id derived from the **absolute path it was loaded
|
||||
from**, and `chrome.storage.local` — which is where the wallet lives — is
|
||||
partitioned by that id. Move the checkout, re-clone it, or load a second copy
|
||||
from anywhere else, and the extension comes up on a fresh, empty storage
|
||||
partition: the wallet is simply gone, with no error and nothing in the UI to say
|
||||
so. With the `key` in place the id is derived from the key instead, and follows
|
||||
the extension wherever it is loaded from. That id is
|
||||
`gipbhkogfopeahplcjhipkgpcimdpkip`, pinned in `tests/extensionId.test.js` and
|
||||
observed against a real Chrome in `tests/e2e/storagePartition.js`.
|
||||
|
||||
**Changing `key` changes the extension id, and orphans every wallet stored under
|
||||
the old one.** It is a migration, not an edit.
|
||||
|
||||
The **private** half is a credential. It is not in this repository, no target
|
||||
generates one into the working tree, `*.pem` and `*.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 signs a CRX, and this repo does
|
||||
not pack one. A packer would take it from outside the repo, e.g.
|
||||
`chrome --pack-extension=dist/chrome --pack-extension-key=<path to the .pem>`.
|
||||
|
||||
### Installing on Firefox
|
||||
|
||||
**The XPI this repo produces is UNSIGNED, and release Firefox and Firefox ESR
|
||||
will refuse to install it.** Those builds enforce add-on signing with no working
|
||||
override — `xpinstall.signatures.required` does nothing on them — so a permanent
|
||||
install needs Firefox Developer Edition, Nightly, or an Unbranded build, with
|
||||
`xpinstall.signatures.required` set to `false` in `about:config`.
|
||||
|
||||
Signing means submitting to AMO (self-distribution is enough, and does not
|
||||
require listing), which needs credentials this repository does not have and is
|
||||
the owner's decision.
|
||||
|
||||
The other route is `about:debugging#/runtime/this-firefox` -> "Load Temporary
|
||||
Add-on", which works on every Firefox including release. **A temporary add-on is
|
||||
unloaded when Firefox exits**, so daily use means re-adding it by hand on every
|
||||
browser start.
|
||||
|
||||
**The wallet survives the restart.** `manifest/firefox.json` declares a fixed
|
||||
`browser_specific_settings.gecko.id`, and Firefox keys the extension's storage
|
||||
area on that id rather than on the install, so adding the temporary add-on again
|
||||
in the same profile finds the vault where it left it. That is asserted, not
|
||||
assumed: `tests/e2e/firefox/reinstall.js` installs the packaged XPI in a real
|
||||
Firefox, creates a wallet through the UI, quits the browser, starts it again on
|
||||
the same profile, adds the add-on again, and decrypts the vault with the
|
||||
original password back to the original recovery phrase. The `moz-extension://`
|
||||
origin the popup is served from is _not_ stable across installs and does not
|
||||
need to be — nothing durable is keyed on it.
|
||||
|
||||
**Removing the add-on does not.** An explicit uninstall — about:addons "Remove"
|
||||
— destroys the extension's storage, and the vault with it. That is ordinary,
|
||||
correct browser behaviour and it is observed in the same suite, but for a wallet
|
||||
it is worth saying out loud: **on Firefox, Remove is irreversible, and the
|
||||
recovery phrase is the only way back.**
|
||||
|
||||
### Debug Builds
|
||||
|
||||
@@ -146,6 +246,11 @@ provide:
|
||||
fails anywhere else. Part of `make check`, which inspects `dist/` when there
|
||||
is one and says loudly when there is not; `make build` re-runs it with
|
||||
`--require-dist`, so a build artifact is always covered
|
||||
- `script/package` — produce the release artifacts: `make build` first, so the
|
||||
archives can only ever be made from a `dist/` that has been verified against
|
||||
that build's own receipt as a RELEASE build, then one self-contained,
|
||||
versioned archive per browser into `release/` (see
|
||||
[Release Artifacts](#release-artifacts)). It packages and does not publish
|
||||
- `script/vendor-blocklist` — refresh `src/shared/phishingBlocklist.json` from
|
||||
its upstream, pinned to a commit and to the sha256 of the bytes that commit
|
||||
serves. Run deliberately, never as part of a build: the output is committed
|
||||
@@ -198,7 +303,7 @@ The Makefile shims to those. It also carries a few targets that have no
|
||||
- `make build-debug` — the same build with `AUTISTMASK_DEBUG=1`, verified as a
|
||||
debug build, and keeping its `dist/` on failure (see
|
||||
[Debug Builds](#debug-builds))
|
||||
- `make clean` — remove `dist/`
|
||||
- `make clean` — remove `dist/` and `release/`
|
||||
- `make dev` — build in watch mode
|
||||
|
||||
## End-to-End Tests
|
||||
@@ -334,6 +439,23 @@ class before a browser is involved, so this suite is no longer the only thing
|
||||
standing between it and a release — but a static rule only sees identifiers, and
|
||||
the runtime errors this suite catches are broader than one rule.
|
||||
|
||||
`make test-e2e` then runs a second program in the same image,
|
||||
`tests/e2e/storagePartition.js`, which answers where `chrome.storage.local`
|
||||
lives. It loads the built extension from one temporary directory in a fresh
|
||||
profile, writes a sentinel key the extension itself never touches, closes the
|
||||
browser, and loads a **copy at a different path into the same profile** — then
|
||||
does it again with `key` stripped out of the manifest, and reports what each
|
||||
pair actually did rather than what it expected. It needs its own browser
|
||||
sessions, four of them, because the whole subject is what happens ACROSS loads.
|
||||
|
||||
The observed behaviour, on the pinned Chromium: **with `key`, both paths get the
|
||||
same extension id and the second load reads the first load's storage. Without
|
||||
`key`, the two paths get different ids and the second load sees an empty
|
||||
partition** — the wallet, silently gone. Loading both keyed copies at once in
|
||||
one profile yields a single extension 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 instead of passing it.
|
||||
|
||||
### Firefox (`make test-e2e-firefox`)
|
||||
|
||||
`make test-e2e-firefox` builds `dist/firefox/` and drives the **real popup in a
|
||||
@@ -351,6 +473,33 @@ runner rather than believed from the extension, and the stub node has to answer
|
||||
`eth_sendRawTransaction` with the hash `ethers` computes for the artifact it
|
||||
sent, or `provider.broadcastTransaction()` refuses the answer.
|
||||
|
||||
`make test-e2e-firefox` then runs a second program in the same image,
|
||||
`tests/e2e/firefox/reinstall.js`, and it installs the **packaged XPI** rather
|
||||
than the unpacked directory — the only place a real Firefox is asked to load the
|
||||
artifact that would actually be handed to someone. It runs two browsers over one
|
||||
profile, because it asks two questions that have different answers:
|
||||
|
||||
- **Restart.** Install, create a wallet through the UI, record the vault, quit
|
||||
the browser, start it again on the same profile, add the add-on again. The
|
||||
extension must not come up as a fresh install; the vault, xpub and first
|
||||
address must be unchanged; and the vault must still **decrypt** with the
|
||||
original password to the original recovery phrase, through the real Show
|
||||
Recovery Phrase screen. "The ciphertext is still in storage" and "the wallet
|
||||
still works" are different claims and only the second one is worth anything.
|
||||
This is what a user does every day, because a temporary add-on is unloaded
|
||||
when Firefox exits.
|
||||
- **Removal.** Then, in the same browser, an explicit uninstall and a fresh
|
||||
install. Observed: **Firefox destroys the extension's storage on uninstall**,
|
||||
so the vault is gone. Recorded as an assertion, so a Firefox that ever changes
|
||||
it fails the run, and stated in
|
||||
[Installing on Firefox](#installing-on-firefox) because for a wallet it means
|
||||
Remove is irreversible except from the recovery phrase.
|
||||
|
||||
It also pins down something worth knowing about the harness: the
|
||||
`moz-extension://` uuid the popup is served from is not stable across installs,
|
||||
and navigating to a stale one does not fail — it hangs. The program reads the
|
||||
live uuid out of `extensions.webextensions.uuids` after every install.
|
||||
|
||||
Both suites build their own image, each with the repo and a fresh extension
|
||||
build baked in; what differs is the base. The Chrome image layers those on top
|
||||
of a published Playwright image, whereas this one is assembled from a `node`
|
||||
|
||||
Reference in New Issue
Block a user