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
+12 -18
View File
@@ -1,6 +1,6 @@
#!/usr/bin/env bash
# Rebuild the dashboard image, restart the container, point the shared nginx at
# it. Safe to re-run any time — this is the normal "ship a change" command.
# Pack the current extension, serve it, and point the shared nginx at it.
# Safe to re-run any time — this is the normal "publish a release" command.
set -euo pipefail
cd "$(dirname "$0")"
@@ -9,44 +9,38 @@ 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
: "${FIRMER_HOST:?set FIRMER_HOST in .env}"
: "${EXT_HOST:?set EXT_HOST in .env}"
SHARED_STACK="${SHARED_STACK:-/opt/autotrader/deploy}"
command -v envsubst >/dev/null || fail "envsubst missing — apt-get install -y gettext-base"
[ -d "$SHARED_STACK/nginx/conf.d" ] || fail "no nginx/conf.d under $SHARED_STACK"
log "Building and starting"
docker compose up -d --build
./pack-extension.sh
log "Waiting for the container to report healthy"
for _ in $(seq 1 30); do
state=$(docker inspect autofirmer --format '{{.State.Health.Status}}' 2>/dev/null || echo missing)
[ "$state" = healthy ] && break
sleep 2
done
[ "${state:-}" = healthy ] || fail "autofirmer is $state — check: docker logs autofirmer"
log "Starting the file server"
docker compose up -d
# Rendered into the *other* stack's conf.d, because that is the directory its
# nginx has mounted. The file is untracked there, and survives that stack's
# deploys and pulls; only its bootstrap-tls.sh wipes conf.d, and re-running this
# pulls and deploys; only its bootstrap-tls.sh wipes conf.d, and re-running this
# script puts it back.
#
# Held back until the certificate exists: nginx refuses to load a server block
# whose ssl_certificate is missing, and a broken file in that directory would
# also abort the autotrader deploys that reload the same nginx.
if docker run --rm -v autotrader-vps_letsencrypt:/le alpine \
test -f "/le/live/$FIRMER_HOST/fullchain.pem" 2>/dev/null; then
template=nginx/firmer.conf.template
test -f "/le/live/$EXT_HOST/fullchain.pem" 2>/dev/null; then
template=nginx/ext.conf.template
else
log "No certificate for $FIRMER_HOST yet — rendering the HTTP-only config"
log "No certificate for $EXT_HOST yet — rendering the HTTP-only config"
log "Run ./bootstrap-tls.sh once DNS points here, then this script again"
template=nginx/firmer-acme.conf.template
template=nginx/ext-acme.conf.template
fi
log "Rendering $template"
# Named explicitly so envsubst leaves nginx's own $host, $remote_addr and the
# rest alone.
envsubst '${FIRMER_HOST}' < "$template" > "$SHARED_STACK/nginx/conf.d/firmer.conf"
envsubst '${EXT_HOST}' < "$template" > "$SHARED_STACK/nginx/conf.d/ext.conf"
log "Reloading the shared nginx"
docker exec nginx nginx -t