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
|
|
|
#!/usr/bin/env bash
|
|
|
|
|
# Publish a built artifact into the `releases` collection (issue #16).
|
|
|
|
|
#
|
|
|
|
|
# The collection is superuser-only, so this authenticates as a PocketBase
|
|
|
|
|
# superuser and uploads the artifact + metadata via the admin REST API. Run it
|
|
|
|
|
# on the machine that built the artifact (it computes the SHA-256 + size that the
|
|
|
|
|
# app verifies before installing).
|
|
|
|
|
#
|
|
|
|
|
# Auth (pick one, via env — never pass secrets on the command line):
|
|
|
|
|
# PB_ADMIN_TOKEN=<superuser token> # preferred; nothing sensitive stored
|
|
|
|
|
# PB_ADMIN_EMAIL + PB_ADMIN_PASSWORD # script exchanges them for a token
|
|
|
|
|
#
|
|
|
|
|
# Usage:
|
|
|
|
|
# PB_ADMIN_TOKEN=... ./scripts/publish-release.sh \
|
|
|
|
|
# --platform linux --variant clean --version 1.0.1 --build 2 \
|
|
|
|
|
# --file build/amber-linux-clean.zip --notes "Bug fixes"
|
|
|
|
|
#
|
|
|
|
|
# --pb-url defaults to https://pb.petruzalekr.cz (override for a local/stage PB).
|
|
|
|
|
set -euo pipefail
|
|
|
|
|
|
|
|
|
|
PB_URL="https://pb.petruzalekr.cz"
|
|
|
|
|
PLATFORM="" VARIANT="" VERSION="" BUILD="" FILE="" NOTES=""
|
|
|
|
|
|
|
|
|
|
while [[ $# -gt 0 ]]; do
|
|
|
|
|
case "$1" in
|
|
|
|
|
--platform) PLATFORM="$2"; shift 2 ;;
|
|
|
|
|
--variant) VARIANT="$2"; shift 2 ;;
|
|
|
|
|
--version) VERSION="$2"; shift 2 ;;
|
|
|
|
|
--build) BUILD="$2"; shift 2 ;;
|
|
|
|
|
--file) FILE="$2"; shift 2 ;;
|
|
|
|
|
--notes) NOTES="$2"; shift 2 ;;
|
|
|
|
|
--pb-url) PB_URL="$2"; shift 2 ;;
|
|
|
|
|
*) echo "unknown arg: $1" >&2; exit 2 ;;
|
|
|
|
|
esac
|
|
|
|
|
done
|
|
|
|
|
|
|
|
|
|
die() { echo "error: $*" >&2; exit 1; }
|
|
|
|
|
[[ -n "$PLATFORM" && -n "$VARIANT" && -n "$VERSION" && -n "$BUILD" && -n "$FILE" ]] \
|
|
|
|
|
|| die "need --platform --variant --version --build --file"
|
|
|
|
|
[[ "$PLATFORM" =~ ^(windows|linux|android)$ ]] || die "platform must be windows|linux|android"
|
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 18:32:02 +00:00
|
|
|
[[ "$VARIANT" =~ ^clean$ ]] || die "variant must be clean (18+ builds are gone)"
|
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
|
|
|
[[ "$BUILD" =~ ^[0-9]+$ ]] || die "build must be an integer"
|
|
|
|
|
[[ -f "$FILE" ]] || die "file not found: $FILE"
|
|
|
|
|
|
|
|
|
|
# ---- auth: reuse a provided token, else exchange email+password for one -------
|
|
|
|
|
TOKEN="${PB_ADMIN_TOKEN:-}"
|
|
|
|
|
if [[ -z "$TOKEN" ]]; then
|
|
|
|
|
[[ -n "${PB_ADMIN_EMAIL:-}" && -n "${PB_ADMIN_PASSWORD:-}" ]] \
|
|
|
|
|
|| die "set PB_ADMIN_TOKEN, or PB_ADMIN_EMAIL + PB_ADMIN_PASSWORD"
|
|
|
|
|
TOKEN=$(curl -sf -X POST "$PB_URL/api/collections/_superusers/auth-with-password" \
|
|
|
|
|
-H 'Content-Type: application/json' \
|
|
|
|
|
-d "{\"identity\":\"$PB_ADMIN_EMAIL\",\"password\":\"$PB_ADMIN_PASSWORD\"}" \
|
|
|
|
|
| grep -oE '"token":"[^"]+"' | head -1 | sed 's/"token":"//;s/"//') \
|
|
|
|
|
|| die "superuser auth failed"
|
|
|
|
|
[[ -n "$TOKEN" ]] || die "superuser auth returned no token"
|
|
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
SHA=$(sha256sum "$FILE" | cut -d' ' -f1)
|
|
|
|
|
SIZE=$(stat -c%s "$FILE")
|
|
|
|
|
echo "publishing: $PLATFORM/$VARIANT v$VERSION (build $BUILD)"
|
|
|
|
|
echo " file: $FILE size: $SIZE sha256: $SHA"
|
|
|
|
|
|
|
|
|
|
# ---- multipart upload to the releases collection -----------------------------
|
|
|
|
|
HTTP=$(curl -s -o /tmp/publish_resp.json -w '%{http_code}' \
|
|
|
|
|
-X POST "$PB_URL/api/collections/releases/records" \
|
|
|
|
|
-H "Authorization: $TOKEN" \
|
|
|
|
|
-F "platform=$PLATFORM" \
|
|
|
|
|
-F "variant=$VARIANT" \
|
|
|
|
|
-F "version=$VERSION" \
|
|
|
|
|
-F "buildNumber=$BUILD" \
|
|
|
|
|
-F "sha256=$SHA" \
|
|
|
|
|
-F "size=$SIZE" \
|
|
|
|
|
-F "notes=$NOTES" \
|
|
|
|
|
-F "file=@$FILE")
|
|
|
|
|
|
|
|
|
|
if [[ "$HTTP" == "200" ]]; then
|
|
|
|
|
echo "✓ published (record $(grep -oE '"id":"[^"]+"' /tmp/publish_resp.json | head -1 | sed 's/"id":"//;s/"//'))"
|
|
|
|
|
rm -f /tmp/publish_resp.json
|
|
|
|
|
else
|
|
|
|
|
echo "✗ publish failed (HTTP $HTTP):" >&2
|
|
|
|
|
cat /tmp/publish_resp.json >&2; echo >&2
|
|
|
|
|
rm -f /tmp/publish_resp.json
|
|
|
|
|
exit 1
|
|
|
|
|
fi
|