Auto-update backend (H1): releases collection + gated manifest #6

Merged
richiexec merged 2 commits from feature/auto-update into main 2026-07-20 11:50:31 +00:00
Owner

Part of #16 (auto-update; backend half). App half is the amber-app PR of the same name.

What

  • releases collection (pb_migrations/1786500000_releases.js): platform, variant, version, buildNumber, file, sha256, size, notes. Read gated (variant='clean' || @request.auth.nsfwEnabled=true); writes superuser-only.
  • file is protected: true (1786500001_…): a follow-up migration after E2E showed the artifact URL was public without it. Now the file is served only via a short-lived file token whose grant re-checks the view rule.
  • GET /api/update/manifest?platform=… (pb_hooks/update.pb.js, auth): returns the latest build, variant chosen server-side from nsfwEnabled — the client can't request adult.
  • scripts/publish-release.sh: uploads a built artifact as superuser (sha256 + size computed).
  • docs/auto-update-contract.md: collection, endpoint, download flow, gating.

Verified (live, against the deployed PB)

15/15 E2E with throwaway users + artifacts (all cleaned up):

  • publish works; duplicate (platform,variant,build) rejected by the unique index
  • manifest → clean for a non-flagged account, adult for nsfwEnabled; sha/buildNumber match the files
  • clean account sees 0 adult rows and downloads its clean artifact with integrity intact
  • clean account is blocked from the adult file (HTTP 404) — the leak this PR's protected fix closed

🤖 Generated with Claude Code

Part of **#16** (auto-update; backend half). App half is the amber-app PR of the same name. ## What - **`releases` collection** (`pb_migrations/1786500000_releases.js`): `platform`, `variant`, `version`, `buildNumber`, `file`, `sha256`, `size`, `notes`. Read gated (`variant='clean' || @request.auth.nsfwEnabled=true`); writes superuser-only. - **`file` is `protected: true`** (`1786500001_…`): a follow-up migration after E2E showed the artifact URL was public without it. Now the file is served only via a short-lived file token whose grant re-checks the view rule. - **`GET /api/update/manifest?platform=…`** (`pb_hooks/update.pb.js`, auth): returns the latest build, **variant chosen server-side from `nsfwEnabled`** — the client can't request adult. - **`scripts/publish-release.sh`**: uploads a built artifact as superuser (sha256 + size computed). - **`docs/auto-update-contract.md`**: collection, endpoint, download flow, gating. ## Verified (live, against the deployed PB) 15/15 E2E with throwaway users + artifacts (all cleaned up): - publish works; duplicate `(platform,variant,build)` rejected by the unique index - manifest → **clean** for a non-flagged account, **adult** for `nsfwEnabled`; sha/buildNumber match the files - clean account sees **0** adult rows and downloads its clean artifact with integrity intact - **clean account is blocked from the adult file (HTTP 404)** — the leak this PR's `protected` fix closed 🤖 Generated with [Claude Code](https://claude.com/claude-code)
richiexec added 2 commits 2026-07-20 10:01:16 +00:00
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/).
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.
richiexec merged commit 9d442ee853 into main 2026-07-20 11:50:31 +00:00
richiexec deleted branch feature/auto-update 2026-07-20 11:50:31 +00:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference: richiexec/amber-backend#6
No description provided.