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/).
A new device (TV) signs into an account with no password typed on it and
no pairing code, approved from an already-logged-in phone/PC.
- device_auth collection (migration): code (unique), deviceName, status,
user, expiresAt + hidden authToken/keyCiphertext/pollSecret/devicePubKey.
Owner-scoped list/view/delete only (the device list + revoke); create/
update locked to the hooks.
- pb_hooks/device_auth.pb.js: public request + info + pollSecret-gated
poll routes (the TV is unauthenticated), auth-gated approve (mints
e.auth.newAuthToken(), stores the E2E vault-key ciphertext) + decline,
and a cleanup cron that scrubs delivered tokens and stale rows.
- pb_hooks/device_page.pb.js: self-contained approve page at GET /device.
Signs in, derives the addon-config vault key (#20) with WebCrypto PBKDF2,
seals {salt,keyB64} to the TV's X25519 pubkey (vendored TweetNaCl +
WebCrypto HKDF/AES-GCM), and approves. The server never sees the key.
- Dockerfile ships pb_hooks (--hooksDir); verify.py covers the schema +
that the routes are served.
Crypto matches the app byte-for-byte (proven by the app's fixed-vector
tests). Full contract: docs/device-auth-contract.md. JWT-revocation
limitation documented (v1).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>