import { createHash, createHmac, timingSafeEqual } from 'node:crypto'; import type { Cookies } from '@sveltejs/kit'; import { env } from '$env/dynamic/private'; export const AUTH_COOKIE_NAME = 'home_auth'; export const SESSION_MAX_AGE_SECONDS = 60 * 60 * 24 * 7; const SESSION_MAX_AGE_MS = SESSION_MAX_AGE_SECONDS * 1000; function configuredPassword(): string { if (!env.AUTH_PASSWORD) { throw new Error('AUTH_PASSWORD is not configured'); } return env.AUTH_PASSWORD; } function digest(value: string): Buffer { return createHash('sha256').update(value).digest(); } function sign(value: string): string { return createHmac('sha256', configuredPassword()) .update(value) .digest('base64url'); } export function verifyPassword(candidate: string): boolean { const expected = digest(configuredPassword()); const actual = digest(candidate); return timingSafeEqual(actual, expected); } export function createSession( now: Temporal.Instant = Temporal.Now.instant(), ): string { const expiresAt = now.add({ milliseconds: SESSION_MAX_AGE_MS, }).epochMilliseconds; const payload = String(expiresAt); return `${payload}.${sign(payload)}`; } export function isValidSession( value: string | undefined, now: Temporal.Instant = Temporal.Now.instant(), ): boolean { if (!value) return false; const [expiresAtText, signature, extra] = value.split('.'); if (!expiresAtText || !signature || extra) return false; const expiresAt = Number(expiresAtText); if (!Number.isSafeInteger(expiresAt) || expiresAt <= now.epochMilliseconds) return false; const expected = Buffer.from(sign(expiresAtText), 'base64url'); const actual = Buffer.from(signature, 'base64url'); if (actual.length !== expected.length) return false; return timingSafeEqual(actual, expected); } export function renewSession( cookies: Cookies, secure: boolean, now: Temporal.Instant = Temporal.Now.instant(), ): boolean { const current = cookies.get(AUTH_COOKIE_NAME); if (!isValidSession(current, now)) return false; cookies.set(AUTH_COOKIE_NAME, createSession(now), { path: '/', httpOnly: true, secure, sameSite: 'lax', maxAge: SESSION_MAX_AGE_SECONDS, }); return true; } export function setSession( cookies: Cookies, secure: boolean, now: Temporal.Instant = Temporal.Now.instant(), ): void { cookies.set(AUTH_COOKIE_NAME, createSession(now), { path: '/', httpOnly: true, secure, sameSite: 'lax', maxAge: SESSION_MAX_AGE_SECONDS, }); }