Ship the extension as a signed CRX from our own update server

The dashboard-hosting stack from the previous commit is gone; it was answering
a question nobody asked. What was actually needed is Chrome's real update
channel, which an unpacked extension does not have and cannot be given.

So the extension is now signed into a CRX with an update manifest beside it,
served from the droplet, and installed on the trading machines by policy
(ExtensionInstallForcelist). From there Chrome installs it, updates it on its
own schedule, and puts it back if it goes missing. The manifest gained the
`key` that fixes the extension id, so an unpacked development load keeps the
same identity as the published build.

The self-update check now branches on how it was installed: policy installs ask
Chrome to check the update server now rather than waiting out its five-hour
cycle, and unpacked loads keep the reload-from-disk path. chrome.management
.getSelf() tells the two apart and needs no permission to do it.

The signing key lives at /opt/autofirmer-updates/key.pem on the droplet and
nowhere else. It is the extension's identity: a different key is a different
extension, and no browser would take it as an update.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Brandon Li
2026-08-31 12:50:01 -05:00
co-authored by Claude Opus 5
parent ece6aab9cf
commit 035f93d220
20 changed files with 443 additions and 288 deletions
+61
View File
@@ -0,0 +1,61 @@
#!/usr/bin/env bash
# Sign extension/ into a CRX and write the update manifest Chrome polls.
#
# The signing key is the extension's identity: the id Chrome derives from it is
# baked into every installed copy and into the Windows policy. Sign with a
# different key and you have published a different extension that no browser
# will ever accept as an update — so this refuses to mint one by accident.
set -euo pipefail
cd "$(dirname "$0")"
log() { printf '\n\033[1;34m==> %s\033[0m\n' "$1"; }
fail() { printf '\n\033[1;31mERROR: %s\033[0m\n' "$1" >&2; exit 1; }
[ -f .env ] || fail "no .env — copy .env.example to .env and fill it in"
set -a; . ./.env; set +a
: "${EXT_HOST:?set EXT_HOST in .env}"
UPDATES_DIR="${UPDATES_DIR:-/opt/autofirmer-updates}"
KEY="$UPDATES_DIR/key.pem"
PUBLIC="$UPDATES_DIR/public"
EXTENSION="$(cd .. && pwd)/extension"
[ -f "$KEY" ] || fail "no signing key at $KEY. A new one would change the extension id and orphan every install. See deploy/README.md before generating one."
mkdir -p "$PUBLIC"
VERSION=$(python3 -c 'import json,sys; print(json.load(open(sys.argv[1]))["version"])' "$EXTENSION/manifest.json")
[ -n "$VERSION" ] || fail "could not read the version out of the manifest"
# The id is the first 16 bytes of the SHA-256 of the public key, each hex digit
# shifted from 0-9a-f into a-p. Derived from the *manifest* here so it can be
# checked against what the packer writes: if the manifest's `key` and the
# signing key are not a pair, updates silently never apply, and this is the only
# moment that mismatch is visible.
manifest_id() {
python3 -c 'import json,sys; print(json.load(open(sys.argv[1])).get("key",""))' "$EXTENSION/manifest.json" \
| base64 -d 2>/dev/null | openssl dgst -sha256 -binary | xxd -p -c32 | cut -c1-32 | tr '0-9a-f' 'a-p'
}
log "Packing v$VERSION"
# Node lives in a container so the droplet needs no toolchain of its own. The
# key is mounted read-only: crx3 writes a new one when it cannot find the file,
# and that is the one failure mode worth making impossible.
docker run --rm \
-v "$EXTENSION:/ext:ro" \
-v "$KEY:/key.pem:ro" \
-v "$PUBLIC:/out" \
node:24-slim sh -c "cd /tmp && npm i --silent crx3@1 >/dev/null 2>&1 && \
node_modules/.bin/crx3 -p /key.pem -o /out/autofirmer.crx -x /out/updates.xml \
--crxURL https://$EXT_HOST/autofirmer.crx --appVersion '$VERSION' -- /ext" \
|| fail "packing failed"
EXPECTED=$(manifest_id)
if [ -n "$EXPECTED" ] && ! grep -q "appid=\"$EXPECTED\"" "$PUBLIC/updates.xml"; then
fail "the manifest's \`key\` and $KEY are not a pair — the update manifest advertises a different extension than the one people have installed"
fi
log "Published"
echo " id: ${EXPECTED:-<no key in manifest>}"
echo " version: $VERSION"
echo " crx: https://$EXT_HOST/autofirmer.crx"
echo " updates: https://$EXT_HOST/updates.xml"