amber-backend/docs/auto-update-contract.md
Claude 6be486b275 Fix auto-update file gating: mark releases.file protected (#16)
The E2E caught a real leak: a non-nsfw (even anonymous) account could download
an adult artifact. In PocketBase, file protection is a per-FIELD flag, not
derived from the collection view rule — the original migration left releases.file
unprotected, so its URL was public despite the gated read rules.

- Add protected:true to the file field (correct for fresh installs).
- 1786500001_releases_protect_file.js: alter the field on the already-deployed
  instance (applied migrations don't re-run, so the fix needs its own migration).
- Doc: correct the gating explanation (protection is the field flag; the file
  token grant then re-checks the view rule).

With this, a protected file needs a file token whose grant re-checks the view
rule, so a clean account is denied the adult artifact.
2026-07-20 11:48:07 +02:00

85 lines
3.6 KiB
Markdown

# 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. |
| `file` | file | The artifact: desktop = `.zip` of the release bundle, android = `.apk`. Protected (see rules). |
| `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
**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.
## 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.
## 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.