import { SignJWT, jwtVerify, type JWTPayload } from "jose"; import { randomUUID } from "crypto"; const ALGORITHM = "HS256"; const PURPOSE = "rebreak.cooldown"; function getSecret(): Uint8Array { const raw = process.env.REBREAK_COOLDOWN_SECRET || process.env.NUXT_AUTH_SECRET || // Last-resort fallback — deterministic within a single process lifetime. // A proper secret MUST be set via Infisical in production. "rebreak-cooldown-insecure-fallback-replace-me"; return new TextEncoder().encode(raw); } export interface CooldownTokenPayload { userId: string; jti: string; cooldownEndsAt: string; // ISO-8601 } /** * Signs a short-lived JWT that the iOS app can present to prove it has * permission to disable the DNS protection (cooldown expired on the server). * * Lifetime: 5 minutes — short enough to prevent replay attacks. * The `jti` ties the token to the exact CooldownRequest row. */ export async function signCooldownToken( userId: string, jti: string, cooldownEndsAt: Date, ): Promise { const secret = getSecret(); const now = Math.floor(Date.now() / 1000); return new SignJWT({ sub: userId, jti, purpose: PURPOSE, cooldown_ends_at: cooldownEndsAt.toISOString(), } satisfies JWTPayload & { purpose: string; cooldown_ends_at: string }) .setProtectedHeader({ alg: ALGORITHM }) .setIssuedAt(now) .setExpirationTime(now + 5 * 60) // 5 minutes .sign(secret); } /** * Verifies the token and returns its payload or null if invalid/expired. */ export async function verifyCooldownToken( token: string, ): Promise { try { const { payload } = await jwtVerify(token, getSecret(), { algorithms: [ALGORITHM], }); if ( typeof payload.sub !== "string" || typeof payload.jti !== "string" || payload.purpose !== PURPOSE || typeof payload.cooldown_ends_at !== "string" ) { return null; } return { userId: payload.sub, jti: payload.jti, cooldownEndsAt: payload.cooldown_ends_at, }; } catch { return null; } } /** Convenience: generate a new JTI (UUID v4). */ export function generateJti(): string { return randomUUID(); }