PocketBase runs every routerAdd/cronAdd handler in its OWN isolated JSVM, so the
file-level helpers/consts (nowSeconds, genCode, CODE_ALPHABET, the TTLs, and the
DEVICE_APPROVE_HTML page) were invisible inside the callbacks — every route threw
`ReferenceError: <name> is not defined` at request time (POST /request, the
cleanup cron, GET /device). A runtime-only trap that only surfaces on a live
PocketBase, which the original PR couldn't exercise.
Define what each handler needs *inside* it (local scope works). No behavior
change; syntax-checked with `node --check`.
Fixes the deploy of device-auth (amber-app #12).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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>