import { createHmac, timingSafeEqual } from 'crypto' export type FinalistTokenPayload = { confirmationId: string /** Unix seconds. Token is rejected after this. */ exp: number } function getSecret(): string { const s = process.env.NEXTAUTH_SECRET if (!s) throw new Error('NEXTAUTH_SECRET is not set; cannot sign finalist tokens') return s } function hmac(payloadB64: string): string { return createHmac('sha256', getSecret()).update(payloadB64).digest('hex') } export function signFinalistToken(payload: FinalistTokenPayload): string { const payloadB64 = Buffer.from(JSON.stringify(payload)).toString('base64url') const sig = hmac(payloadB64) return `${payloadB64}.${sig}` } export function verifyFinalistToken(token: string): FinalistTokenPayload { const parts = token.split('.') if (parts.length !== 2) throw new Error('Invalid finalist token: malformed') const [payloadB64, sig] = parts const expected = hmac(payloadB64) const a = Buffer.from(sig, 'hex') const b = Buffer.from(expected, 'hex') if (a.length !== b.length || !timingSafeEqual(a, b)) { throw new Error('Invalid finalist token: signature mismatch') } let payload: FinalistTokenPayload try { payload = JSON.parse(Buffer.from(payloadB64, 'base64url').toString('utf-8')) } catch { throw new Error('Invalid finalist token: payload not parseable') } if (typeof payload.exp !== 'number' || payload.exp < Math.floor(Date.now() / 1000)) { throw new Error('Invalid finalist token: expired') } return payload }