Files
autofirmer-expanded/deploy/README.md
T
Brandon LiandClaude Opus 5 035f93d220 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>
2026-08-31 12:50:01 -05:00

4.2 KiB

The extension update server

Chrome has no update channel for an unpacked extension: it reads the folder off disk once and never looks again. This is the supported alternative — the extension is signed into a CRX, an update manifest is published next to it, and Chrome installs and updates it from there like any other extension.

droplet 159.223.129.19                          Windows box
  key.pem  ──signs──> autofirmer.crx  ──HTTPS──> Chrome (policy install)
                      updates.xml    <──polls───

Nothing here serves the dashboard. That still runs on the trading machine, next to the clicker, exactly as before.

The pieces

/opt/autofirmer-updates/key.pem The signing key. The extension's identity.
/opt/autofirmer-updates/public/ autofirmer.crx and updates.xml, served as-is
deploy/pack-extension.sh Signs extension/ and writes both files
deploy/docker-compose.yml An nginx:alpine that serves that folder and nothing else
deploy/nginx/ext.conf.template Rendered into the autotrader stack's conf.d
scripts/install-extension-policy.ps1 The Windows side: one registry entry

Extension id: lkjbblagjjbldlkeaaffhiopdlhniaan, derived from the public half of the signing key and repeated in three places that must agree — key in extension/manifest.json, appid in updates.xml, and the forcelist entry in the policy script. pack-extension.sh refuses to publish when the first two have drifted apart, because the failure otherwise is silent: Chrome simply never sees an update that applies to it.

Losing key.pem means losing the extension. A new key is a new id, which means a new policy entry on every machine and a fresh install rather than an update. It exists in one place, deliberately — it is not in the repo, and a compromised copy would let someone else publish an extension your browsers would trust and install.

Ingress

This stack owns no ingress. The droplet already runs one nginx on 80/443 for master.juicerroom.com and git.juicerroom.com, from /opt/autotrader/deploy; this joins that network, and deploy.sh renders a server block into that stack's nginx/conf.d. Two consequences:

  • The rendered ext.conf is untracked in the autotrader repo. It survives that stack's pulls and deploys, but not its bootstrap-tls.sh, which clears conf.d — re-run ./deploy.sh here if that ever happens.
  • deploy.sh reloads the shared nginx, so a mistake in the template would be a mistake in front of the autotrader dashboard too. It runs nginx -t first and refuses to reload on a bad config, and will not install the TLS server block until the certificate actually exists.

First run

cp .env.example .env      # set EXT_HOST
./bootstrap-tls.sh        # DNS check, certificate, then deploy.sh

bootstrap-tls.sh borrows the autotrader stack's certbot container and Let's Encrypt account, reading ACME_EMAIL from its .env rather than keeping a second copy. The A record has to point at the droplet first.

Publishing a release

git pull && ./deploy/deploy.sh

deploy.sh packs whatever is in extension/ at the version its manifest.json declares, so bump that version or nothing updates — Chrome compares versions and ignores a CRX that is not newer than what it has.

The browsers pick it up on Chrome's own schedule, roughly every five hours. They also pick it up within seconds if the dashboard is running, because the extension asks Chrome for a check as soon as the dashboard reports a newer version than the one it is running.

Installing it on a machine

From an elevated PowerShell on the Windows box:

powershell -NoProfile -ExecutionPolicy Bypass -File scripts\install-extension-policy.ps1

Then remove the unpacked copy from chrome://extensions — it now has the same id, and Chrome will not load both — and restart Chrome. -Remove undoes it; -Browser Edge or -Browser Brave writes the same entry for those instead.

Development still works the old way: load extension/ unpacked on a machine with no policy entry. It keeps the same id (that is what key in the manifest is for), and there it reloads itself from disk rather than asking Chrome for an update.