import "server-only"; import { createHash } from "node:crypto"; import { sqlPublic as sql } from "@/db"; const BOT = /bot|crawl|spider|slurp|facebookexternalhit|preview|curl\/|wget|python-requests|httpx|go-http-client|headless|lighthouse|pingdom|uptime|monitor|scrapy|semrush|ahrefs|mj12|dotbot|petalbot|bytespider|gptbot|claudebot|ccbot|anthropic|openai/i; export function isBot(ua: string | null) { return !ua || BOT.test(ua); } /** Visitor hash: sha256(ip, user-agent, daily salt) — not reversible, rotates daily, one event per visitor per paper per day. */ export function visitorHash(ip: string, ua: string | null) { const salt = process.env.SESSION_SECRET ?? "salt"; const day = new Date().toISOString().slice(0, 10); return createHash("sha256").update(`${salt}|${day}|${ip}|${ua ?? ""}`).digest("hex").slice(0, 32); } export async function recordEvent(kind: "view" | "download", paperId: string, h: { ip: string; ua: string | null; referrer?: string | null; locale?: string; versionId?: string | null }) { let host: string | null = null; try { host = h.referrer ? new URL(h.referrer).hostname : null; } catch { host = null; } await sql`INSERT INTO events (paper_id, version_id, kind, visitor_hash, referrer_host, locale, is_bot) VALUES (${paperId}, ${h.versionId ?? null}, ${kind}, ${visitorHash(h.ip, h.ua)}, ${host}, ${h.locale ?? null}, ${isBot(h.ua)}) ON CONFLICT DO NOTHING`; }