/** * Resolving which device is calling. * * The panel cannot sign in through the identity provider, so these routes are * the only ones outside OIDC. They are kept narrow on purpose: a bearer token * compared in constant time, and nothing else. */ import { prisma } from '@/lib/db'; import { hashToken, normaliseMac, tokenMatches } from './auth'; import { deviceHeader } from './headers'; export type DeviceRow = Awaited>; export async function authenticateDevice(request: Request): Promise | null> { const token = deviceHeader(request, 'access-token') ?? bearer(request); if (!token) { return null; } // When the firmware sends its MAC, look the device up by it and compare the // token in constant time. That is the path the spec asks for. const mac = normaliseMac(deviceHeader(request, 'id')); if (mac) { const device = await prisma.device.findUnique({ where: { macAddress: mac } }); if (device && device.isActive && tokenMatches(token, device.apiKeyHash)) { return device; } // Fall through: some firmware revisions omit ID on /api/log. } // Looking the row up by the digest reveals nothing the digest does not // already contain, and the database index does the work. const device = await prisma.device.findFirst({ where: { apiKeyHash: hashToken(token) } }); return device && device.isActive ? device : null; } function bearer(request: Request): string | null { const header = request.headers.get('authorization'); if (!header?.toLowerCase().startsWith('bearer ')) { return null; } return header.slice(7).trim() || null; }