import type { Context, Config } from "@netlify/edge-functions"; /* * loomiai.io access gate. * * Chris 2026-07-31: "we just want a password to pop up so you can only view * main page with contact info." * * So: the main page and the assets it needs are open to anyone. Every other * path returns 401, which is what makes the browser's native username/password * box pop up. The main page must stay open — business verification (Stripe, * Meta, Google, D&B) fails if the reviewer hits a password wall. * * Netlify's own password protection is a Pro feature; this team is on the free * plan (nf_team_dev), so the gate is an edge function instead. Free, and it * lives in the repo rather than in a dashboard toggle. * * The password is NOT in this file. Set it in the Netlify UI: * Site configuration -> Environment variables * SITE_USER (optional, defaults to "loomi") * SITE_PASSWORD (required) */ function safeEqual(a: string, b: string): boolean { const enc = new TextEncoder(); const x = enc.encode(a); const y = enc.encode(b); if (x.length !== y.length) return false; let diff = 0; for (let i = 0; i < x.length; i++) diff |= x[i] ^ y[i]; return diff === 0; } function challenge(): Response { return new Response("Authentication required.", { status: 401, headers: { "WWW-Authenticate": 'Basic realm="Loomi.AI", charset="UTF-8"', "Cache-Control": "no-store", }, }); } export default async (req: Request, context: Context) => { const expectedUser = Netlify.env.get("SITE_USER") || "loomi"; const expectedPass = Netlify.env.get("SITE_PASSWORD"); // Fail closed. If the password was never set, lock the path rather than // silently letting the whole site through. if (!expectedPass) { return new Response("Access control is not configured.", { status: 503, headers: { "Cache-Control": "no-store" }, }); } const header = req.headers.get("authorization") || ""; if (!header.startsWith("Basic ")) return challenge(); let decoded = ""; try { decoded = atob(header.slice(6)); } catch { return challenge(); } const sep = decoded.indexOf(":"); if (sep === -1) return challenge(); const user = decoded.slice(0, sep); const pass = decoded.slice(sep + 1); // Both compared in constant time, and both always compared, so response // timing does not reveal whether the username alone was right. const okUser = safeEqual(user, expectedUser); const okPass = safeEqual(pass, expectedPass); if (!okUser || !okPass) return challenge(); return context.next(); }; export const config: Config = { path: "/*", excludedPath: [ "/", // the main page — public, on purpose "/index.html", "/jarvis-hero.mp4", // hero video the main page loads "/api/*", // live price tickers on the main page "/favicon.ico", "/robots.txt", "/sitemap.xml", "/.well-known/*", // domain + TLS verification challenges ], };