Mirrors the autotrader stack's auto-deploy: fetch every minute, act when master moves, lock against overlap, rewrite .last-check every run so a silent failure is visible. Only extension/ and deploy/ changes publish - repacking for a docs edit would republish an identical CRX every minute. The guard is the part that earns its place. Chrome ignores a CRX that is not strictly newer, so shipping extension changes without bumping manifest.json is a no-op that looks exactly like success until someone notices the fix never arrived. That now fails loudly instead. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
181 lines
8.0 KiB
Markdown
181 lines
8.0 KiB
Markdown
# 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
|
|
|
|
```bash
|
|
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
|
|
|
|
Automatic. `autofirmer-publish.timer` fetches every minute and publishes when
|
|
master moves — the same shape as the autotrader stack's auto-deploy next door.
|
|
Install it once:
|
|
|
|
```bash
|
|
./deploy/install-autopublish.sh # as root
|
|
```
|
|
|
|
Only changes under `extension/` or `deploy/` trigger a publish; anything else is
|
|
pulled and left alone, because repacking for a README edit would republish a
|
|
byte-identical CRX every minute.
|
|
|
|
**Bump `version` in `extension/manifest.json` for every release.** Chrome ignores
|
|
a CRX that is not strictly newer than the copy it has, so republishing at the
|
|
same version is a silent no-op — the kind discovered days later, when a fix
|
|
everyone believed had shipped turns out never to have left the building.
|
|
`auto-publish.sh` refuses that case outright and logs why, rather than letting
|
|
it pass quietly.
|
|
|
|
By hand, any time:
|
|
|
|
```bash
|
|
git pull && ./deploy/deploy.sh
|
|
```
|
|
|
|
Watch it with `tail -f deploy/publish.log`, and `cat deploy/.last-check` for the
|
|
heartbeat — that file is rewritten on every run, whether or not anything came
|
|
down, which is the only way to tell "running, nothing to do" from "not running".
|
|
|
|
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
|
|
|
|
`setup-windows.bat` and `setup-linux.sh` both offer this as their last step, so
|
|
a fresh instance needs nothing extra. To do it by hand, or on a machine that was
|
|
set up before this existed:
|
|
|
|
```powershell
|
|
powershell -NoProfile -ExecutionPolicy Bypass -File scripts\install-extension-policy.ps1
|
|
```
|
|
|
|
```bash
|
|
sudo scripts/install-extension-policy.sh
|
|
```
|
|
|
|
Both configure every Chromium-family browser they find, and narrowing that to
|
|
one is `-Browser Chrome`.
|
|
|
|
### The machine has to be managed
|
|
|
|
This is the constraint the whole approach turns on, and it is worth stating
|
|
plainly. On Windows, Chromium force-installs an extension from outside its own
|
|
web store **only** when the instance is joined to an Active Directory domain,
|
|
joined to Entra ID, or enrolled in Chrome Enterprise Core. Edge says the same of
|
|
the Microsoft Store. On an ordinary machine the policy is read and then blanked:
|
|
|
|
```
|
|
Value ["[BLOCKED]lkjbblagjjbldlkeaaffhiopdlhniaan;https://ext.juicerroom.com/updates.xml"]
|
|
Error Error at ExtensionInstallForcelist[0]: Invalid extension ID.
|
|
Warning This computer is not detected as enterprise managed so policy can only
|
|
automatically install extensions hosted on the Chrome Webstore.
|
|
```
|
|
|
|
The "invalid extension ID" is just `[BLOCKED]` being parsed as the id. Nothing
|
|
about the CRX, the update manifest or the hive is wrong — the machine is what
|
|
Chrome is refusing. Both scripts check for this and write nothing rather than
|
|
leaving a permanent policy error and a "Managed by your organization" badge
|
|
behind; `-Force` overrides.
|
|
|
|
So the update server is only half a solution on an unmanaged box. The other half
|
|
is one of:
|
|
|
|
- **Load `extension/` unpacked.** No update channel, but it reloads itself from
|
|
disk when the dashboard reports a newer version, and the updater has already
|
|
pulled those files. Works today, any browser, nothing to enrol.
|
|
- **Enrol Chrome in Chrome Enterprise Core** (free, and it does not require a
|
|
domain-joined machine): one `CloudManagementEnrollmentToken` makes the browser
|
|
managed, and everything here then works as designed. Generate a token in the
|
|
Admin console under Devices > Chrome > Managed browsers, then:
|
|
|
|
```powershell
|
|
powershell -NoProfile -ExecutionPolicy Bypass -File scripts\enroll-chrome-management.ps1 -Token <token>
|
|
```
|
|
|
|
```bash
|
|
sudo scripts/enroll-chrome-management.sh <token>
|
|
```
|
|
|
|
Both need admin/root — the token is machine-scoped — and both are Chrome only;
|
|
Chromium has no cloud-management support and Edge is not covered by any of
|
|
this. Once enrolled you can either keep using `install-extension-policy`, or
|
|
force-install the extension from the Admin console instead, which reaches
|
|
every enrolled machine at once.
|
|
- **Publish to a web store.** Store-hosted extensions update themselves with no
|
|
policy at all — at the price of a listing and a review on every release.
|
|
|
|
The Windows script writes to the current user's policy hive and needs no
|
|
elevation; `-Scope Machine` covers every account on the box and does need an
|
|
elevated shell. Linux has no per-user equivalent — Chrome only reads managed
|
|
policy from `/etc` — so that one is root-only and machine-wide. Both take
|
|
`-Remove` / `--remove`.
|
|
|
|
Afterwards, remove the unpacked copy from `chrome://extensions` — it now has the
|
|
same id, and Chrome will not load both — and restart the browser.
|
|
|
|
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.
|