// Lightweight auth for the MacLustr web console. // 10 fixed accounts: user1..user10 with passwords admin1..admin10. // Sessions are HMAC-SHA256 signed tokens stored in an httpOnly cookie. // Works in both the Edge (middleware) and Node (route handler) runtimes // using Web Crypto + base64url (no Buffer dependency). const SECRET = process.env.AUTH_SECRET || "maclustr-console-7f3a9c2e8b14-secret"; export const SESSION_COOKIE = "ml_session"; const SESSION_TTL_SECONDS = 60 * 60 * 12; // 12h export const USERS: Record = Object.fromEntries( Array.from({ length: 10 }, (_, i) => [`user${i + 1}`, `admin${i + 1}`]) ); export function checkCredentials(user: string, pass: string): boolean { const expected = USERS[user]; return expected !== undefined && expected === pass; } function b64url(bytes: Uint8Array): string { let s = ""; for (const b of bytes) s += String.fromCharCode(b); return btoa(s).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, ""); } async function hmac(data: string): Promise { const key = await crypto.subtle.importKey( "raw", new TextEncoder().encode(SECRET), { name: "HMAC", hash: "SHA-256" }, false, ["sign"] ); const sig = await crypto.subtle.sign("HMAC", key, new TextEncoder().encode(data)); return b64url(new Uint8Array(sig)); } export async function signSession(user: string): Promise { const exp = Math.floor(Date.now() / 1000) + SESSION_TTL_SECONDS; const payload = `${user}.${exp}`; const sig = await hmac(payload); return `${payload}.${sig}`; } /** Returns the username if the token is valid and unexpired, else null. */ export async function verifySession(token: string | undefined): Promise { if (!token) return null; const parts = token.split("."); if (parts.length !== 3) return null; const [user, expStr, sig] = parts; const expected = await hmac(`${user}.${expStr}`); if (sig.length !== expected.length) return null; let diff = 0; for (let i = 0; i < sig.length; i++) diff |= sig.charCodeAt(i) ^ expected.charCodeAt(i); if (diff !== 0) return null; const exp = parseInt(expStr, 10); if (!Number.isFinite(exp) || exp < Math.floor(Date.now() / 1000)) return null; return user; } /** Validate an HTTP Basic Auth header (used by API clients such as the iOS app). */ export function checkBasicAuth(header: string | null): boolean { if (!header || !header.startsWith("Basic ")) return false; try { const decoded = atob(header.slice(6)); const idx = decoded.indexOf(":"); if (idx < 0) return false; return checkCredentials(decoded.slice(0, idx), decoded.slice(idx + 1)); } catch { return false; } } export const SESSION_MAX_AGE = SESSION_TTL_SECONDS;