import { createHmac, timingSafeEqual } from 'crypto' const TOKEN_TTL_MS = 10 * 365 * 24 * 60 * 60 * 1000 function getSecret(): string { const secret = process.env.NEXTAUTH_SECRET if (!secret) { throw new Error('NEXTAUTH_SECRET is required for unsubscribe tokens') } return secret } export type UnsubscribeTokenPayload = { userId: string issuedAt: number } export function createUnsubscribeToken(userId: string): string { const issuedAt = Date.now() const inner = Buffer.from(`${userId}.${issuedAt}`, 'utf8').toString('base64url') const sig = createHmac('sha256', getSecret()).update(inner).digest('base64url') return `${inner}.${sig}` } export function verifyUnsubscribeToken(token: string): UnsubscribeTokenPayload | null { try { const trimmed = token.trim() const lastDot = trimmed.lastIndexOf('.') if (lastDot <= 0) return null const sig = trimmed.slice(lastDot + 1) const inner = trimmed.slice(0, lastDot) const expected = createHmac('sha256', getSecret()).update(inner).digest('base64url') const sigBuf = Buffer.from(sig) const expectedBuf = Buffer.from(expected) if (sigBuf.length !== expectedBuf.length || !timingSafeEqual(sigBuf, expectedBuf)) { return null } const decoded = Buffer.from(inner, 'base64url').toString('utf8') const firstDot = decoded.indexOf('.') if (firstDot <= 0) return null const userId = decoded.slice(0, firstDot) const issuedAt = Number(decoded.slice(firstDot + 1)) if (!userId || !Number.isFinite(issuedAt)) return null if (Date.now() - issuedAt > TOKEN_TTL_MS) return null return { userId, issuedAt } } catch { return null } } export function publicAppOrigin(): string { return (process.env.NEXTAUTH_URL || '').replace(/\/$/, '') } export function unsubscribePageUrl(token: string): string { return `${publicAppOrigin()}/unsubscribe?token=${encodeURIComponent(token)}` } export function unsubscribeOneClickUrl(token: string): string { return `${publicAppOrigin()}/api/marketing/unsubscribe?token=${encodeURIComponent(token)}` } /** Headers for future campaign messages (RFC 8058). */ export function listUnsubscribeHeaders(token: string): Record { const page = unsubscribePageUrl(token) const oneClick = unsubscribeOneClickUrl(token) return { 'List-Unsubscribe': `<${oneClick}>, <${page}>`, 'List-Unsubscribe-Post': 'List-Unsubscribe=One-Click', } }