import { createHmac, timingSafeEqual } from 'crypto'; type DeviceRole = 'camera' | 'client'; export type DeviceTokenPayload = { userId: string; deviceId: string; role: DeviceRole; exp: number; }; const secret = process.env.BETTER_AUTH_SECRET; if (!secret) { throw new Error('BETTER_AUTH_SECRET is required for device token signing'); } const base64UrlEncode = (input: string): string => Buffer.from(input, 'utf8').toString('base64url'); const base64UrlDecode = (input: string): string => Buffer.from(input, 'base64url').toString('utf8'); const sign = (data: string): string => createHmac('sha256', secret).update(data).digest('base64url'); export const createDeviceToken = ( payload: Omit, ttlSeconds = 60 * 60 * 24 * 30, ): string => { const body: DeviceTokenPayload = { ...payload, exp: Math.floor(Date.now() / 1000) + ttlSeconds, }; const encodedPayload = base64UrlEncode(JSON.stringify(body)); const signature = sign(encodedPayload); return `${encodedPayload}.${signature}`; }; export const verifyDeviceToken = (token: string): DeviceTokenPayload | null => { const [encodedPayload, providedSignature] = token.split('.'); if (!encodedPayload || !providedSignature) { return null; } const expectedSignature = sign(encodedPayload); const providedBuffer = Buffer.from(providedSignature, 'utf8'); const expectedBuffer = Buffer.from(expectedSignature, 'utf8'); if (providedBuffer.length !== expectedBuffer.length) { return null; } if (!timingSafeEqual(providedBuffer, expectedBuffer)) { return null; } let parsedPayload: unknown; try { parsedPayload = JSON.parse(base64UrlDecode(encodedPayload)); } catch { return null; } if (!parsedPayload || typeof parsedPayload !== 'object') { return null; } const payload = parsedPayload as Partial; if ( typeof payload.userId !== 'string' || typeof payload.deviceId !== 'string' || (payload.role !== 'camera' && payload.role !== 'client') || typeof payload.exp !== 'number' ) { return null; } if (payload.exp <= Math.floor(Date.now() / 1000)) { return null; } return payload as DeviceTokenPayload; };