Auto-update backend (H1): releases collection + gated manifest #6
4 changed files with 38 additions and 6 deletions
2
.gitignore
vendored
2
.gitignore
vendored
|
|
@ -4,3 +4,5 @@ pb_data/
|
|||
# Local env / secrets
|
||||
.env
|
||||
.env.local
|
||||
# Local-only secrets (e.g. a minted superuser token for admin scripting)
|
||||
*.local.token
|
||||
|
|
|
|||
|
|
@ -30,11 +30,12 @@ Unique index on `(platform, variant, buildNumber)`.
|
|||
**Access rules.** Read is gated:
|
||||
`@request.auth.id != '' && (variant = 'clean' || @request.auth.nsfwEnabled = true)`
|
||||
— any signed-in account reads **clean** rows; only an `nsfwEnabled` account reads
|
||||
**adult** rows. Because the view rule needs auth, the attached `file` is served
|
||||
as a **protected file**: downloading needs a short-lived file token
|
||||
(`POST /api/files/token`), and minting/using it re-checks the same rule — so
|
||||
adult bytes never reach a non-flagged account. `create`/`update`/`delete` are
|
||||
**superuser-only** (null rules); publishing goes through the admin API.
|
||||
**adult** rows. 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** — so a
|
||||
non-flagged account can't download an adult artifact (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
|
||||
|
||||
|
|
|
|||
|
|
@ -34,7 +34,11 @@ migrate((app) => {
|
|||
{ type: "text", name: "version", required: true, max: 40 },
|
||||
{ type: "number", name: "buildNumber", required: true, min: 1, onlyInt: true },
|
||||
// The artifact: desktop = a .zip of the release bundle, android = the .apk.
|
||||
{ type: "file", name: "file", required: true, maxSelect: 1, maxSize: 524288000 },
|
||||
// `protected` is what actually gates the bytes: a protected file is served
|
||||
// only with a short-lived file token whose grant re-checks the view rule
|
||||
// below (so a non-nsfw account can't fetch an adult artifact). WITHOUT this
|
||||
// flag the file URL is public regardless of the view rule.
|
||||
{ type: "file", name: "file", required: true, maxSelect: 1, maxSize: 524288000, protected: true },
|
||||
// Lowercase hex SHA-256 of the artifact; the app verifies it before install.
|
||||
{ type: "text", name: "sha256", required: true, min: 64, max: 64 },
|
||||
{ type: "number", name: "size", required: true, min: 0, onlyInt: true },
|
||||
|
|
|
|||
25
pb_migrations/1786500001_releases_protect_file.js
Normal file
25
pb_migrations/1786500001_releases_protect_file.js
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
/// <reference path="../pb_data/types.d.ts" />
|
||||
|
||||
// Fix gating on the releases `file` field (issue #16).
|
||||
//
|
||||
// The original migration created the file field without `protected: true`, which
|
||||
// left the artifact URL PUBLIC — a non-nsfw account (and even an anonymous
|
||||
// request) could download the adult build, defeating the whole point of the
|
||||
// gating rules. Protection in PocketBase is a per-field flag, not derived from
|
||||
// the collection view rule: with it on, the file is served only via a
|
||||
// short-lived file token whose grant re-checks the view rule.
|
||||
//
|
||||
// This alters the field on already-deployed instances; a fresh install gets it
|
||||
// correct straight from 1786500000.
|
||||
|
||||
migrate((app) => {
|
||||
const c = app.findCollectionByNameOrId("releases")
|
||||
const f = c.fields.getByName("file")
|
||||
f.protected = true
|
||||
app.save(c)
|
||||
}, (app) => {
|
||||
const c = app.findCollectionByNameOrId("releases")
|
||||
const f = c.fields.getByName("file")
|
||||
f.protected = false
|
||||
app.save(c)
|
||||
})
|
||||
Loading…
Reference in a new issue