// Session « mot de passe partagé » (spec §12) : cookie HMAC signé, sans stockage serveur. import { createHmac, timingSafeEqual } from 'node:crypto' import type { H3Event } from 'h3' const COOKIE_NAME = 'fl_session' const SESSION_TTL_MS = 1000 * 60 * 60 * 24 * 30 // 30 jours function sign(secret: string, payload: string): string { return createHmac('sha256', secret).update(payload).digest('base64url') } export function createSessionToken(secret: string): string { const exp = String(Date.now() + SESSION_TTL_MS) return `${exp}.${sign(secret, exp)}` } export function isValidSessionToken(secret: string, token: string | undefined): boolean { if (!token) return false const [exp, sig] = token.split('.') if (!exp || !sig) return false if (Number(exp) < Date.now()) return false const expected = sign(secret, exp) const a = Buffer.from(sig) const b = Buffer.from(expected) return a.length === b.length && timingSafeEqual(a, b) } export function setSessionCookie(event: H3Event): void { const { sessionSecret } = useRuntimeConfig(event) setCookie(event, COOKIE_NAME, createSessionToken(sessionSecret), { httpOnly: true, sameSite: 'lax', path: '/', maxAge: SESSION_TTL_MS / 1000, }) } export function clearSessionCookie(event: H3Event): void { deleteCookie(event, COOKIE_NAME, { path: '/' }) } export function isAuthenticated(event: H3Event): boolean { const config = useRuntimeConfig(event) // Jeton interne : utilisé par le rendu print (Playwright) en phase 2. const headerToken = getHeader(event, 'x-internal-token') ?? getQuery(event)['internal-token'] if (headerToken && headerToken === config.internalToken) return true return isValidSessionToken(config.sessionSecret, getCookie(event, COOKIE_NAME)) }