import 'server-only'; import { createHash, timingSafeEqual } from 'node:crypto'; import { cookies } from 'next/headers'; /** * Admin gate. The browser never sees the token: the login form posts it to a server action which compares it * (constant time) with `process.env.CA_ADMIN_TOKEN` and sets an httpOnly cookie holding a SHA-256 of the * token. Pages check the cookie (again constant time) and server actions call the API with the real token. */ export const ADMIN_COOKIE = 'ca_admin'; export const ADMIN_COOKIE_MAX_AGE = 12 * 3600; export function adminToken(): string | null { const v = process.env.CA_ADMIN_TOKEN?.trim(); return v ? v : null; } export function isAdminConfigured(): boolean { return adminToken() !== null; } function sha256(s: string): Buffer { return createHash('sha256').update(s, 'utf8').digest(); } /** Constant-time comparison of two strings (hashed first so lengths never leak). */ export function safeEqual(a: string, b: string): boolean { return timingSafeEqual(sha256(a), sha256(b)); } /** Cookie value = sha256(token) hex, salted with a fixed context string so it is never equal to the token. */ export function cookieValueFor(token: string): string { return createHash('sha256').update(`countryatlas-admin:${token}`, 'utf8').digest('hex'); } export function verifyToken(input: string): boolean { const expected = adminToken(); if (!expected) return false; return safeEqual(input, expected); } export async function isAdminAuthed(): Promise { const expected = adminToken(); if (!expected) return false; const store = await cookies(); const c = store.get(ADMIN_COOKIE)?.value; if (!c) return false; return safeEqual(c, cookieValueFor(expected)); }