Merge pull request 'fix(device-auth): inline helpers into each hook handler (JSVM scope)' (#4) from fix/device-auth-jsvm-scope into main
Reviewed-on: #4
This commit is contained in:
commit
3c8d60e614
2 changed files with 23 additions and 18 deletions
|
|
@ -14,26 +14,24 @@
|
||||||
// stores and forwards it as an opaque blob and never holds the key — see
|
// 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`.
|
// `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
|
// NOTE: PocketBase runs every routerAdd/cronAdd handler in its OWN isolated JSVM,
|
||||||
const PICKUP_TTL_SECONDS = 120 // after approval, the TV has this long to fetch
|
// 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
|
||||||
// Human code: 8 chars, unambiguous alphabet (Crockford minus I L O U), shown as
|
// handler needs (nowSeconds, the code alphabet, the TTLs) is defined *inside* it.
|
||||||
// XXXX-XXXX. Stored without the dash; the app + page normalize before lookup.
|
// Keep it that way; don't hoist helpers back to file scope.
|
||||||
const CODE_ALPHABET = "0123456789ABCDEFGHJKMNPQRSTVWXYZ"
|
|
||||||
|
|
||||||
function nowSeconds() {
|
|
||||||
return Math.floor(Date.now() / 1000)
|
|
||||||
}
|
|
||||||
|
|
||||||
function genCode() {
|
|
||||||
return $security.randomStringWithAlphabet(8, CODE_ALPHABET)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── POST /api/device-auth/request ─────────────────────────────────────────────
|
// ── POST /api/device-auth/request ─────────────────────────────────────────────
|
||||||
// Public. The TV creates a sign-in request and gets back the code to show, the
|
// 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,
|
// pollSecret only it will hold, and the id + expiry. Body: { deviceName,
|
||||||
// devicePubKey }.
|
// devicePubKey }.
|
||||||
routerAdd("POST", "/api/device-auth/request", (e) => {
|
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: "" })
|
const data = new DynamicModel({ deviceName: "", devicePubKey: "" })
|
||||||
e.bindBody(data)
|
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
|
// and to read the TV's ephemeral public key it encrypts the vault key to. Returns
|
||||||
// nothing sensitive (no pollSecret, no token).
|
// nothing sensitive (no pollSecret, no token).
|
||||||
routerAdd("GET", "/api/device-auth/info", (e) => {
|
routerAdd("GET", "/api/device-auth/info", (e) => {
|
||||||
|
const nowSeconds = () => Math.floor(Date.now() / 1000)
|
||||||
const code = (e.request.url.query().get("code") || "").toUpperCase().trim()
|
const code = (e.request.url.query().get("code") || "").toUpperCase().trim()
|
||||||
if (!code) throw new BadRequestError("missing code")
|
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
|
// The minted token + the E2E vault-key ciphertext are only ever returned here, to
|
||||||
// a caller holding the matching pollSecret.
|
// a caller holding the matching pollSecret.
|
||||||
routerAdd("GET", "/api/device-auth/poll", (e) => {
|
routerAdd("GET", "/api/device-auth/poll", (e) => {
|
||||||
|
const nowSeconds = () => Math.floor(Date.now() / 1000)
|
||||||
const id = e.request.url.query().get("id") || ""
|
const id = e.request.url.query().get("id") || ""
|
||||||
const secret = e.request.url.query().get("secret") || ""
|
const secret = e.request.url.query().get("secret") || ""
|
||||||
if (!id || !secret) throw new BadRequestError("missing id/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.
|
// token for the caller and stores the E2E vault-key ciphertext the page produced.
|
||||||
// Body: { code, keyCiphertext }.
|
// Body: { code, keyCiphertext }.
|
||||||
routerAdd("POST", "/api/device-auth/approve", (e) => {
|
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: "" })
|
const data = new DynamicModel({ code: "", keyCiphertext: "" })
|
||||||
e.bindBody(data)
|
e.bindBody(data)
|
||||||
const code = (data.code || "").toUpperCase().trim()
|
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 +
|
// pending/denied rows. The device list keeps the approved rows (status + user +
|
||||||
// created), just without any live secret at rest.
|
// created), just without any live secret at rest.
|
||||||
cronAdd("device_auth_cleanup", "*/5 * * * *", () => {
|
cronAdd("device_auth_cleanup", "*/5 * * * *", () => {
|
||||||
|
const nowSeconds = () => Math.floor(Date.now() / 1000)
|
||||||
const now = nowSeconds()
|
const now = nowSeconds()
|
||||||
try {
|
try {
|
||||||
const stale = $app.findRecordsByFilter(
|
const stale = $app.findRecordsByFilter(
|
||||||
|
|
|
||||||
|
|
@ -20,11 +20,11 @@
|
||||||
// reaches the server (E2E for this one blob). The minted token is delivered by the
|
// reaches the server (E2E for this one blob). The minted token is delivered by the
|
||||||
// poll route, not here.
|
// 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) => {
|
routerAdd("GET", "/device", (e) => {
|
||||||
return e.html(200, DEVICE_APPROVE_HTML)
|
const DEVICE_APPROVE_HTML = `<!doctype html>
|
||||||
})
|
|
||||||
|
|
||||||
const DEVICE_APPROVE_HTML = `<!doctype html>
|
|
||||||
<html lang="cs">
|
<html lang="cs">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
|
|
@ -277,3 +277,5 @@ loadInfo();
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>`
|
</html>`
|
||||||
|
return e.html(200, DEVICE_APPROVE_HTML)
|
||||||
|
})
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue