amber-backend/docs/auto-update-contract.md
Claude fb9abaae17 Drop the 18+ account flag, its rule clause and the web page's 18+ field
Nothing read `users.nsfwEnabled` any more. `/api/amber/whoami` stopped returning
it, `GET /api/update/manifest` always answers `clean`, and the `releases` read
rule stopped mentioning it in migration 1793000000, when the nine 18+ rows (437
MB) were deleted. What was left was a per-account flag nobody writes and nobody
reads, sitting on the family's accounts and showing up in every schema dump and
access-rule review as if it still gated something. A flag that looks like a
protection but protects nothing costs the next reader the work of proving it dead.

One live reader did remain: pb_public/index.html showed a "Zdroj 18+" source
field and wrote `adultAddonUrl` into the encrypted addon config for a flagged
account, and picked a different child-profile label for one. Both are gone, so
the page offers the same three sources to everyone.

Migration 1797000000 drops the field, rewrites users.updateRule to
`id = @request.auth.id` (the clause it loses guarded a field that no longer
exists), and removes `adult` from releases.variant's allowed values. Checked
before writing it: 15 release rows, every one `clean`; three accounts carried the
flag (the owner's, one family member's, the retired test account); the separate
18+ app has no accounts and no PocketBase, so nothing outside Amber reads it.

`variant` itself stays and stays required: every installed Amber compares it
against its own build and refuses a mismatch SILENTLY, so dropping the field
would strand those installs with no error to see. publish-release.sh now accepts
only `clean`.

The status dashboard stops listing `adult-stremio-addon` and `stripchat-proxy`.
Both still run on the VPS; they are the owner's, not Amber's, and Amber never
reached them.

scripts/verify.py asserts the flag stays gone (field absent, no rule mentions it,
updateRule own-record-only). Run against a local PocketBase built from origin/main
it failed on exactly those three checks; with this migration applied, ALL CHECKS
PASSED.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-17 20:32:02 +02:00

4.1 KiB

Auto-update — contract (issue #16)

The app checks for a newer build and installs it. One variant ships, clean. The server used to pick between that and an 18+ one from an account flag; that flag and that build are both gone (pb_migrations/1797000000_drop_nsfw_enabled.js).

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, the only value. The field stays because every installed app compares it against its own and refuses a mismatch silently — removing it would strand those installs with no error to see.
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: windows = .exe installer, linux = .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 needs a signed-in account (@request.auth.id != '') and nothing more: one variant is published and there is nothing left to gate between. 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 (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, always variant: "clean".

  • No release for that platform → { "available": false }.
  • Otherwise:
    {
      "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):

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.

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 unpacking by hand — do not publish it.

Gating summary

There is nothing left to gate between: one variant is published, and the manifest hook returns it to everyone.

The 18+ flag is gone from users and adult is gone from variant's allowed values (pb_migrations/1797000000_drop_nsfw_enabled.js). The manifest hook serves clean to every signed-in account.