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>
91 lines
4.1 KiB
Markdown
91 lines
4.1 KiB
Markdown
# 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:
|
|
```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.
|
|
|
|
**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.
|