TypeScript 61.9%
HTML 37.2%
SQL 0.7%
1import 'server-only';2import { cookies } from 'next/headers';3import { signPayload, verifyPayload } from './crypto';45export const PENDING_COOKIE = 'ri_pending';6const TTL_S = 15 * 60;78export type PendingStage = 'verify' | 'totp' | 'email_code';910export interface Pending {11 uid: string;12 email: string;13 stage: PendingStage;14 next: string | null;15 /** true when the device was not trusted at password time (drives new-login e-mail) */16 newDevice: boolean;17}1819export async function setPending(p: Pending): Promise<void> {20 (await cookies()).set(PENDING_COOKIE, signPayload(p as unknown as Record<string, unknown>, TTL_S), { httpOnly: true, secure: process.env.NODE_ENV === 'production', sameSite: 'lax', path: '/', maxAge: TTL_S });21}2223export async function getPending(): Promise<Pending | null> {24 const raw = (await cookies()).get(PENDING_COOKIE)?.value;25 const p = verifyPayload<Record<string, unknown>>(raw);26 if (!p || typeof p.uid !== 'string' || typeof p.email !== 'string' || typeof p.stage !== 'string') return null;27 return { uid: p.uid, email: p.email, stage: p.stage as PendingStage, next: typeof p.next === 'string' ? p.next : null, newDevice: Boolean(p.newDevice) };28}2930export async function clearPending(): Promise<void> {31 (await cookies()).delete(PENDING_COOKIE);32}3334/** Only allow same-origin relative redirects. */35export function safeNext(next: string | null | undefined, fallback = '/collections'): string {36 if (!next || !next.startsWith('/') || next.startsWith('//') || next.includes('\\')) return fallback;37 return next;38}39