export const APP_COOKIE = "app_session"; export const APP_SESSION_MAX_AGE = 60 * 60 * 24 * 7; export const DEFAULT_APP_USERNAME = "student"; export const DEFAULT_APP_PASSWORD = "password"; const encoder = new TextEncoder(); export function appUsername() { return process.env.APP_USERNAME || DEFAULT_APP_USERNAME; } export function appPassword() { return process.env.APP_PASSWORD || DEFAULT_APP_PASSWORD; } function safeEqual(a: string, b: string) { if (a.length !== b.length) return false; let diff = 0; for (let i = 0; i < a.length; i += 1) { diff |= a.charCodeAt(i) ^ b.charCodeAt(i); } return diff === 0; } async function sha256Hex(value: string) { const digest = await crypto.subtle.digest("SHA-256", encoder.encode(value)); return [...new Uint8Array(digest)] .map((byte) => byte.toString(16).padStart(2, "0")) .join(""); } export async function appSessionToken() { return sha256Hex(`${appUsername()}:${appPassword()}`); } export async function verifyAppCredentials(username: string, password: string) { return safeEqual(username, appUsername()) && safeEqual(password, appPassword()); } export async function isValidAppSession(value: string | undefined) { if (!value) return false; return safeEqual(value, await appSessionToken()); } function attributes(isHttps: boolean, maxAge: number) { const parts = [`Path=/`, `Max-Age=${maxAge}`, "HttpOnly"]; if (isHttps) { parts.push("Secure", "SameSite=None", "Partitioned"); } else { parts.push("SameSite=Lax"); } return parts.join("; "); } export function setAppSessionCookie(headers: Headers, value: string, isHttps: boolean) { headers.append( "set-cookie", `${APP_COOKIE}=${encodeURIComponent(value)}; ${attributes(isHttps, APP_SESSION_MAX_AGE)}` ); } export function clearAppSessionCookie(headers: Headers, isHttps: boolean) { headers.append("set-cookie", `${APP_COOKIE}=; ${attributes(isHttps, 0)}`); }