diff --git a/pb_hooks/device_auth.pb.js b/pb_hooks/device_auth.pb.js index 4f94483..cb5fc7a 100644 --- a/pb_hooks/device_auth.pb.js +++ b/pb_hooks/device_auth.pb.js @@ -14,26 +14,24 @@ // stores and forwards it as an opaque blob and never holds the key — see // `pb_hooks/device_page.pb.js` and the app's `device_key_transfer.dart`. -const DEVICE_TTL_SECONDS = 300 // 5 min to approve a fresh request -const PICKUP_TTL_SECONDS = 120 // after approval, the TV has this long to fetch - -// Human code: 8 chars, unambiguous alphabet (Crockford minus I L O U), shown as -// XXXX-XXXX. Stored without the dash; the app + page normalize before lookup. -const CODE_ALPHABET = "0123456789ABCDEFGHJKMNPQRSTVWXYZ" - -function nowSeconds() { - return Math.floor(Date.now() / 1000) -} - -function genCode() { - return $security.randomStringWithAlphabet(8, CODE_ALPHABET) -} +// NOTE: PocketBase runs every routerAdd/cronAdd handler in its OWN isolated JSVM, +// so file-level consts/functions are NOT visible inside the callbacks — referencing +// them throws `ReferenceError: x is not defined` at request time. So everything a +// handler needs (nowSeconds, the code alphabet, the TTLs) is defined *inside* it. +// Keep it that way; don't hoist helpers back to file scope. // ── POST /api/device-auth/request ───────────────────────────────────────────── // Public. The TV creates a sign-in request and gets back the code to show, the // pollSecret only it will hold, and the id + expiry. Body: { deviceName, // devicePubKey }. routerAdd("POST", "/api/device-auth/request", (e) => { + const nowSeconds = () => Math.floor(Date.now() / 1000) + const DEVICE_TTL_SECONDS = 300 // 5 min to approve a fresh request + // Human code: 8 chars, unambiguous alphabet (Crockford minus I L O U), shown as + // XXXX-XXXX. Stored without the dash; the app + page normalize before lookup. + const CODE_ALPHABET = "0123456789ABCDEFGHJKMNPQRSTVWXYZ" + const genCode = () => $security.randomStringWithAlphabet(8, CODE_ALPHABET) + const data = new DynamicModel({ deviceName: "", devicePubKey: "" }) e.bindBody(data) @@ -77,6 +75,7 @@ routerAdd("POST", "/api/device-auth/request", (e) => { // and to read the TV's ephemeral public key it encrypts the vault key to. Returns // nothing sensitive (no pollSecret, no token). routerAdd("GET", "/api/device-auth/info", (e) => { + const nowSeconds = () => Math.floor(Date.now() / 1000) const code = (e.request.url.query().get("code") || "").toUpperCase().trim() if (!code) throw new BadRequestError("missing code") @@ -103,6 +102,7 @@ routerAdd("GET", "/api/device-auth/info", (e) => { // The minted token + the E2E vault-key ciphertext are only ever returned here, to // a caller holding the matching pollSecret. routerAdd("GET", "/api/device-auth/poll", (e) => { + const nowSeconds = () => Math.floor(Date.now() / 1000) const id = e.request.url.query().get("id") || "" const secret = e.request.url.query().get("secret") || "" if (!id || !secret) throw new BadRequestError("missing id/secret") @@ -169,6 +169,8 @@ routerAdd("GET", "/api/device-auth/poll", (e) => { // token for the caller and stores the E2E vault-key ciphertext the page produced. // Body: { code, keyCiphertext }. routerAdd("POST", "/api/device-auth/approve", (e) => { + const nowSeconds = () => Math.floor(Date.now() / 1000) + const PICKUP_TTL_SECONDS = 120 // after approval, the TV has this long to fetch const data = new DynamicModel({ code: "", keyCiphertext: "" }) e.bindBody(data) const code = (data.code || "").toUpperCase().trim() @@ -233,6 +235,7 @@ routerAdd("POST", "/api/device-auth/decline", (e) => { // pending/denied rows. The device list keeps the approved rows (status + user + // created), just without any live secret at rest. cronAdd("device_auth_cleanup", "*/5 * * * *", () => { + const nowSeconds = () => Math.floor(Date.now() / 1000) const now = nowSeconds() try { const stale = $app.findRecordsByFilter( diff --git a/pb_hooks/device_page.pb.js b/pb_hooks/device_page.pb.js index 9c26169..fc3cb3c 100644 --- a/pb_hooks/device_page.pb.js +++ b/pb_hooks/device_page.pb.js @@ -20,11 +20,11 @@ // reaches the server (E2E for this one blob). The minted token is delivered by the // poll route, not here. +// NOTE: PocketBase runs this route handler in its own isolated JSVM — a file-level +// `const DEVICE_APPROVE_HTML` is invisible inside it (ReferenceError at request +// time). So the whole self-contained page is defined *inside* the handler. routerAdd("GET", "/device", (e) => { - return e.html(200, DEVICE_APPROVE_HTML) -}) - -const DEVICE_APPROVE_HTML = ` + const DEVICE_APPROVE_HTML = ` @@ -277,3 +277,5 @@ loadInfo(); ` + return e.html(200, DEVICE_APPROVE_HTML) +})