release: package the extension, pin the Chrome extension id, and prove the wallet survives a reinstall (closes #310)
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.
This commit is contained in:
60
build.js
60
build.js
@@ -3,6 +3,7 @@ const path = require("path");
|
||||
const crypto = require("crypto");
|
||||
const { execSync } = require("child_process");
|
||||
const esbuild = require("esbuild");
|
||||
const { resolveVersion } = require("./script/lib/version");
|
||||
|
||||
const DIST = path.join(__dirname, "dist");
|
||||
const DIST_CHROME = path.join(DIST, "chrome");
|
||||
@@ -160,28 +161,53 @@ function isDebugBuild() {
|
||||
return process.env.AUTISTMASK_DEBUG === "1";
|
||||
}
|
||||
|
||||
// A short git output, or null when git cannot answer. Distinguishing "git said
|
||||
// nothing" from "git could not be asked" matters below: a working tree whose
|
||||
// state is unknown must not be stamped as clean.
|
||||
function git(args) {
|
||||
try {
|
||||
return execSync(`git ${args}`, {
|
||||
encoding: "utf8",
|
||||
stdio: ["ignore", "pipe", "ignore"],
|
||||
}).trim();
|
||||
} catch {
|
||||
// not a git repo, or git not available
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// The working-tree state, as a suffix for the displayed commit: "" when the
|
||||
// tree matches HEAD, "-dirty" when it does not, "-unknown" when git answered
|
||||
// the hash but not the status. Without this a build from a modified tree
|
||||
// stamped a clean hash, so the About screen named a commit whose contents were
|
||||
// not what was running — the one thing that stamp exists to establish.
|
||||
//
|
||||
// git status --porcelain honours .gitignore, so dist/ and node_modules/ do not
|
||||
// make every build dirty; an untracked file that is NOT ignored does, and
|
||||
// correctly: it may well be in the bundle.
|
||||
function worktreeSuffix() {
|
||||
const status = git("status --porcelain");
|
||||
if (status === null) return "-unknown";
|
||||
return status === "" ? "" : "-dirty";
|
||||
}
|
||||
|
||||
function getBuildInfo() {
|
||||
const pkg = JSON.parse(
|
||||
fs.readFileSync(path.join(__dirname, "package.json"), "utf8"),
|
||||
);
|
||||
let commitHash = "unknown";
|
||||
try {
|
||||
commitHash = execSync("git rev-parse --short HEAD", {
|
||||
encoding: "utf8",
|
||||
}).trim();
|
||||
} catch {
|
||||
// not a git repo or git not available
|
||||
}
|
||||
let commitHashFull = "unknown";
|
||||
try {
|
||||
commitHashFull = execSync("git rev-parse HEAD", {
|
||||
encoding: "utf8",
|
||||
}).trim();
|
||||
} catch {
|
||||
// not a git repo or git not available
|
||||
}
|
||||
const commitHashFull = git("rev-parse HEAD") || "unknown";
|
||||
const shortHash = git("rev-parse --short HEAD") || "unknown";
|
||||
// The full hash is left clean because it is the href of the commit link in
|
||||
// the About screen, and "abc123-dirty" is not a commit anyone can fetch.
|
||||
// The displayed short hash carries the marker, so the screen says the tree
|
||||
// was modified while still linking somewhere real.
|
||||
const commitHash =
|
||||
shortHash === "unknown" ? shortHash : shortHash + worktreeSuffix();
|
||||
return {
|
||||
version: pkg.version,
|
||||
// Fails the build when package.json and the two manifests disagree;
|
||||
// see script/lib/version.js. Called before anything is emitted, so a
|
||||
// tree with no single version never reaches dist/.
|
||||
version: resolveVersion(__dirname),
|
||||
license: pkg.license,
|
||||
author: pkg.author,
|
||||
commitHash,
|
||||
|
||||
Reference in New Issue
Block a user