import 'server-only'; import { cookies } from 'next/headers'; import { signPayload, verifyPayload } from './crypto'; export const PENDING_COOKIE = 'ri_pending'; const TTL_S = 15 * 60; export type PendingStage = 'verify' | 'totp' | 'email_code'; export interface Pending { uid: string; email: string; stage: PendingStage; next: string | null; /** true when the device was not trusted at password time (drives new-login e-mail) */ newDevice: boolean; } export async function setPending(p: Pending): Promise { (await cookies()).set(PENDING_COOKIE, signPayload(p as unknown as Record, TTL_S), { httpOnly: true, secure: process.env.NODE_ENV === 'production', sameSite: 'lax', path: '/', maxAge: TTL_S }); } export async function getPending(): Promise { const raw = (await cookies()).get(PENDING_COOKIE)?.value; const p = verifyPayload>(raw); if (!p || typeof p.uid !== 'string' || typeof p.email !== 'string' || typeof p.stage !== 'string') return null; return { uid: p.uid, email: p.email, stage: p.stage as PendingStage, next: typeof p.next === 'string' ? p.next : null, newDevice: Boolean(p.newDevice) }; } export async function clearPending(): Promise { (await cookies()).delete(PENDING_COOKIE); } /** Only allow same-origin relative redirects. */ export function safeNext(next: string | null | undefined, fallback = '/collections'): string { if (!next || !next.startsWith('/') || next.startsWith('//') || next.includes('\\')) return fallback; return next; }