import crypto from 'node:crypto'; import { env } from './env'; const SESSION_COOKIE = 'gp_session'; const STATE_COOKIE = 'oauth_state'; const CSRF_COOKIE = 'gp_csrf'; export type SessionData = { user?: { id: number; username: string; email?: string; displayName?: string; }; }; function hmac(value: string) { if (!env.SESSION_SECRET) throw new Error('SESSION_SECRET missing'); return crypto.createHmac('sha256', env.SESSION_SECRET).update(value).digest('hex'); } export function encodeSession(data: SessionData): string { const json = JSON.stringify(data); const b64 = Buffer.from(json, 'utf-8').toString('base64url'); const sig = hmac(b64); return `${b64}.${sig}`; } export function decodeSession(token?: string | null): SessionData | null { if (!token) return null; const parts = token.split('.'); if (parts.length !== 2) return null; const [b64, sig] = parts; const expected = hmac(b64); // timing-safe compare if (!crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(expected))) return null; try { return JSON.parse(Buffer.from(b64, 'base64url').toString('utf-8')) as SessionData; } catch { return null; } } export function cookieAttrs({ httpOnly = true, maxAge, path = '/', sameSite = 'Lax', secure }: { httpOnly?: boolean; maxAge?: number; path?: string; sameSite?: 'Lax'|'Strict'|'None'; secure?: boolean } = {}) { const attrs = [`Path=${path}`, `SameSite=${sameSite}`]; if (httpOnly) attrs.push('HttpOnly'); const isProd = process.env.NODE_ENV === 'production'; const useSecure = secure ?? isProd; if (useSecure) attrs.push('Secure'); if (typeof maxAge === 'number') attrs.push(`Max-Age=${maxAge}`); return attrs.join('; '); } export function setSessionCookie(data: SessionData, maxAgeDays = 7): string { const token = encodeSession(data); const maxAge = maxAgeDays * 24 * 60 * 60; return `${SESSION_COOKIE}=${token}; ${cookieAttrs({ maxAge })}`; } export function clearSessionCookie(): string { return `${SESSION_COOKIE}=; ${cookieAttrs({})}; Max-Age=0`; } export function getSessionFromRequest(req: Request): SessionData | null { const cookie = parseCookies(req.headers.get('cookie'))[SESSION_COOKIE]; return decodeSession(cookie); } export function setTempCookie(name: string, value: string, maxAgeSeconds = 600): string { return `${name}=${value}; ${cookieAttrs({ maxAge: maxAgeSeconds })}`; } export function clearCookie(name: string): string { return `${name}=; ${cookieAttrs({})}; Max-Age=0`; } export function parseCookies(header: string | null | undefined): Record { const out: Record = {}; if (!header) return out; for (const part of header.split(';')) { const [k, ...rest] = part.trim().split('='); out[k] = decodeURIComponent(rest.join('=')); } return out; } export { SESSION_COOKIE, STATE_COOKIE, CSRF_COOKIE };