Auto-update backend: releases collection + gated manifest (#16)
Adds the server side of auto-update (issue #16, was epic #6's [H]).
- pb_migrations/1786500000_releases.js: a superuser-only 'releases' collection
(platform, variant, version, buildNumber, file, sha256, size, notes). Read is
gated in the rules — any signed-in account sees clean builds; only an
nsfwEnabled account sees adult ones — so PocketBase's native protected-file
serving hands adult bytes only to flagged accounts (no custom streaming).
- pb_hooks/update.pb.js: GET /api/update/manifest?platform=… (auth). Picks the
variant server-side from the caller's nsfwEnabled (adult) vs clean — the client
can't request adult — and returns the latest build's version/buildNumber/notes/
sha256/size + the protected downloadPath.
- scripts/publish-release.sh: uploads a built artifact as a superuser (computes
sha256 + size, multipart POST). Token or email+password via env.
- docs/auto-update-contract.md: the collection, endpoint, download flow, gating.
Migration + hook + script syntax-checked. Live verification pends deploying this
to the PB (collection auto-applies on boot, hook loads from pb_hooks/).
2026-07-20 09:36:03 +00:00
|
|
|
# Auto-update — contract (issue #16)
|
|
|
|
|
|
|
|
|
|
The app checks for a newer build and installs it, receiving the **adult** or
|
|
|
|
|
**clean** variant according to its account's admin-set `nsfwEnabled` flag.
|
|
|
|
|
|
|
|
|
|
Server pieces (this repo):
|
|
|
|
|
- `pb_migrations/1786500000_releases.js` — the `releases` collection.
|
|
|
|
|
- `pb_hooks/update.pb.js` — the `GET /api/update/manifest` route.
|
|
|
|
|
- `scripts/publish-release.sh` — uploads a built artifact as a superuser.
|
|
|
|
|
|
|
|
|
|
Client pieces (amber-app): `lib/data/update/update_service.dart` (version check +
|
|
|
|
|
manifest fetch), the desktop swap-installer, and the Android APK install channel.
|
|
|
|
|
|
|
|
|
|
## `releases` collection
|
|
|
|
|
|
|
|
|
|
| Field | Type | Notes |
|
|
|
|
|
|---------------|--------|-------|
|
|
|
|
|
| `platform` | select | `windows` \| `linux` \| `android`. |
|
|
|
|
|
| `variant` | select | `clean` \| `adult`. |
|
|
|
|
|
| `version` | text | Human semver shown to the user, e.g. `1.0.1`. |
|
|
|
|
|
| `buildNumber` | number | Monotonic int; the comparator the updater actually uses. |
|
2026-08-01 13:58:13 +00:00
|
|
|
| `file` | file | The artifact: windows = `.exe` installer, linux = `.zip` of the release bundle, android = `.apk`. Protected (see rules). |
|
Auto-update backend: releases collection + gated manifest (#16)
Adds the server side of auto-update (issue #16, was epic #6's [H]).
- pb_migrations/1786500000_releases.js: a superuser-only 'releases' collection
(platform, variant, version, buildNumber, file, sha256, size, notes). Read is
gated in the rules — any signed-in account sees clean builds; only an
nsfwEnabled account sees adult ones — so PocketBase's native protected-file
serving hands adult bytes only to flagged accounts (no custom streaming).
- pb_hooks/update.pb.js: GET /api/update/manifest?platform=… (auth). Picks the
variant server-side from the caller's nsfwEnabled (adult) vs clean — the client
can't request adult — and returns the latest build's version/buildNumber/notes/
sha256/size + the protected downloadPath.
- scripts/publish-release.sh: uploads a built artifact as a superuser (computes
sha256 + size, multipart POST). Token or email+password via env.
- docs/auto-update-contract.md: the collection, endpoint, download flow, gating.
Migration + hook + script syntax-checked. Live verification pends deploying this
to the PB (collection auto-applies on boot, hook loads from pb_hooks/).
2026-07-20 09:36:03 +00:00
|
|
|
| `sha256` | text | Lowercase hex SHA-256 of the artifact; verified before install. |
|
|
|
|
|
| `size` | number | Bytes. |
|
|
|
|
|
| `notes` | text | Optional release notes (shown in the update prompt). |
|
|
|
|
|
| `created`/`updated` | autodate | |
|
|
|
|
|
|
|
|
|
|
Unique index on `(platform, variant, buildNumber)`.
|
|
|
|
|
|
|
|
|
|
**Access rules.** Read is gated:
|
|
|
|
|
`@request.auth.id != '' && (variant = 'clean' || @request.auth.nsfwEnabled = true)`
|
|
|
|
|
— any signed-in account reads **clean** rows; only an `nsfwEnabled` account reads
|
2026-07-20 09:48:07 +00:00
|
|
|
**adult** rows. The `file` field is **`protected: true`**, which is what gates the
|
|
|
|
|
bytes: a protected file is served only with a short-lived file token
|
|
|
|
|
(`POST /api/files/token`) whose grant **re-checks the view rule above** — so a
|
|
|
|
|
non-flagged account can't download an adult artifact (and without the flag the
|
|
|
|
|
file URL would be public regardless of the view rule). `create`/`update`/`delete`
|
|
|
|
|
are **superuser-only** (null rules); publishing goes through the admin API.
|
Auto-update backend: releases collection + gated manifest (#16)
Adds the server side of auto-update (issue #16, was epic #6's [H]).
- pb_migrations/1786500000_releases.js: a superuser-only 'releases' collection
(platform, variant, version, buildNumber, file, sha256, size, notes). Read is
gated in the rules — any signed-in account sees clean builds; only an
nsfwEnabled account sees adult ones — so PocketBase's native protected-file
serving hands adult bytes only to flagged accounts (no custom streaming).
- pb_hooks/update.pb.js: GET /api/update/manifest?platform=… (auth). Picks the
variant server-side from the caller's nsfwEnabled (adult) vs clean — the client
can't request adult — and returns the latest build's version/buildNumber/notes/
sha256/size + the protected downloadPath.
- scripts/publish-release.sh: uploads a built artifact as a superuser (computes
sha256 + size, multipart POST). Token or email+password via env.
- docs/auto-update-contract.md: the collection, endpoint, download flow, gating.
Migration + hook + script syntax-checked. Live verification pends deploying this
to the PB (collection auto-applies on boot, hook loads from pb_hooks/).
2026-07-20 09:36:03 +00:00
|
|
|
|
|
|
|
|
## Endpoint
|
|
|
|
|
|
|
|
|
|
### `GET /api/update/manifest?platform=windows|linux|android` (auth: users)
|
|
|
|
|
Returns the latest build for the caller's platform. The **variant is chosen
|
|
|
|
|
server-side** from `nsfwEnabled` — the client cannot request adult.
|
|
|
|
|
|
|
|
|
|
- No release for that platform/variant → `{ "available": false }`.
|
|
|
|
|
- Otherwise:
|
|
|
|
|
```json
|
|
|
|
|
{
|
|
|
|
|
"available": true,
|
|
|
|
|
"platform": "linux",
|
|
|
|
|
"variant": "clean",
|
|
|
|
|
"version": "1.0.1",
|
|
|
|
|
"buildNumber": 2,
|
|
|
|
|
"notes": "…",
|
|
|
|
|
"sha256": "<64 hex>",
|
|
|
|
|
"size": 12345678,
|
|
|
|
|
"filename": "amber-linux-clean.zip",
|
|
|
|
|
"downloadPath": "/api/files/releases/<recordId>/<filename>"
|
|
|
|
|
}
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
**Download.** The app mints a file token (`POST /api/files/token`, its user
|
|
|
|
|
auth) and GETs `{base}{downloadPath}?token=<fileToken>`, then verifies `sha256`
|
|
|
|
|
before installing.
|
|
|
|
|
|
|
|
|
|
## Publishing a build
|
|
|
|
|
|
|
|
|
|
Build the artifact, then (on the build machine):
|
|
|
|
|
```bash
|
|
|
|
|
PB_ADMIN_TOKEN=<superuser token> ./scripts/publish-release.sh \
|
|
|
|
|
--platform linux --variant clean --version 1.0.1 --build 2 \
|
|
|
|
|
--file build/amber-linux-clean.zip --notes "What changed"
|
|
|
|
|
```
|
|
|
|
|
`PB_ADMIN_EMAIL` + `PB_ADMIN_PASSWORD` work instead of a token. The script
|
|
|
|
|
computes the SHA-256 + size and uploads via the superuser REST API. Re-publishing
|
|
|
|
|
the same `(platform, variant, buildNumber)` is rejected by the unique index —
|
|
|
|
|
bump `buildNumber` for each release.
|
|
|
|
|
|
2026-08-01 13:58:13 +00:00
|
|
|
**Windows publishes the installer**, `amber-setup-<variant>-<version>.exe`, not
|
|
|
|
|
the zip. One artifact serves both jobs: people download and run it, and the app's
|
|
|
|
|
updater runs the same file with `/SILENT`, which is why `DesktopInstaller`
|
|
|
|
|
dispatches on the extension. The zip `release_windows.ps1` still builds beside it
|
|
|
|
|
is for the flavour check and for unpacking by hand — do not publish it.
|
|
|
|
|
|
|
|
|
|
`check-flavor.py` cannot read a `.exe` (Inno compresses the payload) and refuses
|
|
|
|
|
rather than passing it. The Windows flavour gate is `Assert-Flavor` in
|
|
|
|
|
`release_windows.ps1`, which reads the staged folder before either artifact is
|
|
|
|
|
made from it; run it there, not here.
|
|
|
|
|
|
Auto-update backend: releases collection + gated manifest (#16)
Adds the server side of auto-update (issue #16, was epic #6's [H]).
- pb_migrations/1786500000_releases.js: a superuser-only 'releases' collection
(platform, variant, version, buildNumber, file, sha256, size, notes). Read is
gated in the rules — any signed-in account sees clean builds; only an
nsfwEnabled account sees adult ones — so PocketBase's native protected-file
serving hands adult bytes only to flagged accounts (no custom streaming).
- pb_hooks/update.pb.js: GET /api/update/manifest?platform=… (auth). Picks the
variant server-side from the caller's nsfwEnabled (adult) vs clean — the client
can't request adult — and returns the latest build's version/buildNumber/notes/
sha256/size + the protected downloadPath.
- scripts/publish-release.sh: uploads a built artifact as a superuser (computes
sha256 + size, multipart POST). Token or email+password via env.
- docs/auto-update-contract.md: the collection, endpoint, download flow, gating.
Migration + hook + script syntax-checked. Live verification pends deploying this
to the PB (collection auto-applies on boot, hook loads from pb_hooks/).
2026-07-20 09:36:03 +00:00
|
|
|
## Gating summary
|
|
|
|
|
|
|
|
|
|
`nsfwEnabled` (admin-set on the `users` record) is the single source of truth:
|
|
|
|
|
the manifest hook reads it to pick the variant, and the collection rules enforce
|
|
|
|
|
it independently at read/download time. A no-flag account only ever sees and
|
|
|
|
|
downloads clean builds.
|