/** * POST /api/devices/protected/handshake * * Server-to-server endpoint: called by AdGuard/DoH-server (rebreak-mdm) * whenever a protected device makes a DNS query with its unique dnsToken. * * Auth: shared secret via `x-handshake-secret` header (NOT user JWT). * - Verified against runtimeConfig.handshakeSecret (Infisical: HANDSHAKE_SECRET). * * Body: { token: string } ← the 32-char hex dnsToken * * Behaviour (idempotent): * - pending → status=active, installedAt=NOW, lastDnsQueryAt=NOW, statusChanged=true * - active → lastDnsQueryAt=NOW, statusChanged=false * - degraded → lastDnsQueryAt=NOW, statusChanged=false (no auto-re-activate) * - revoked → 200 { ok: true, ignored: true } (silent, no info leak) * - unknown token → 404 { error: "TOKEN_NOT_FOUND" } * * Rate limiting: lastDnsQueryAt write is cheap + idempotent; AdGuard may call * this on every DNS query (multiple/sec). If that becomes expensive, add a * per-token 60s in-memory cooldown here. */ export default defineEventHandler(async (event) => { // ── Shared-secret auth ─────────────────────────────────────────────────── const config = useRuntimeConfig(event); const secret = getHeader(event, "x-handshake-secret"); if (!config.handshakeSecret || secret !== config.handshakeSecret) { throw createError({ statusCode: 401, data: { error: "UNAUTHORIZED" } }); } // ── Body ───────────────────────────────────────────────────────────────── const body = await readBody(event); const token = body?.token; if (!token || typeof token !== "string" || token.trim().length === 0) { throw createError({ statusCode: 400, data: { error: "TOKEN_REQUIRED" } }); } // ── DB lookup + update ─────────────────────────────────────────────────── const result = await handshakeProtectedDevice(token.trim()); if (!result.found) { throw createError({ statusCode: 404, data: { error: "TOKEN_NOT_FOUND" } }); } if (result.revoked) { // Silent ignore — don't confirm token existence to potential attacker return { ok: true, ignored: true }; } return { ok: true, statusChanged: result.statusChanged, status: result.status, }; });