/** * Supabase auth plugin for macros. * * Authenticates users via Supabase Auth and stores sessions in the * framework's session table. One file: login page + endpoint + session. */ import { createAuthPlugin, addSession, clearFrameworkSessionCookies, getSessionEmail, getFrameworkSessionCookieValues, getH3App, readBody, setFrameworkSessionCookie, } from "@agent-native/core/server"; import { createClient } from "@supabase/supabase-js"; import { defineEventHandler, getMethod } from "h3"; // Above a normal Neon serverless cold-wake (~1-2s) but well under both the // core DB op timeout and Netlify's function limit, so a slow-but-fine lookup // still resolves instead of false-timing-out on every cold start. const SESSION_LOOKUP_TIMEOUT_MS = 4_000; let _supabase: ReturnType | null = null; function getSupabase() { if (_supabase) return _supabase; const url = process.env.SUPABASE_URL ?? process.env.VITE_SUPABASE_URL; const key = process.env.SUPABASE_ANON_KEY ?? process.env.VITE_SUPABASE_ANON_KEY; if (!url || !key) return null; _supabase = createClient(url, key, { auth: { autoRefreshToken: false, persistSession: false }, }); return _supabase; } const LOGIN_HTML = ` Macros — Sign in

Welcome

Sign in to your account

`; function jsonResponse(body: object, status = 200) { return new Response(JSON.stringify(body), { status, headers: { "Content-Type": "application/json" }, }); } async function getSessionEmailWithTimeout( token: string, ): Promise { let timeout: ReturnType | undefined; try { return await Promise.race([ getSessionEmail(token), new Promise<"timeout">((resolve) => { timeout = setTimeout( () => resolve("timeout"), SESSION_LOOKUP_TIMEOUT_MS, ); }), ]); } finally { if (timeout) clearTimeout(timeout); } } export default (nitroApp: any) => { const authInit = createAuthPlugin({ loginHtml: LOGIN_HTML, // Resolve sessions from the framework's legacy session table, where // supabase-login stores them via addSession(). Providing a custom // getSession marks this template as BYOA — the framework will not // silently bypass auth in dev mode. getSession: async (event) => { const cookies = getFrameworkSessionCookieValues(event); if (cookies.length === 0) return null; for (const cookie of cookies) { const email = await getSessionEmailWithTimeout(cookie); if (email === "timeout") { // Transient slow/cold DB — do NOT destroy a possibly-valid // session. Treat this request as unauthenticated (the framework // serves the login page instead of hanging), but keep the cookie // so the next request succeeds once the DB warms. return null; } if (email) return { email, token: cookie }; } // Every cookie resolved to a definitive "no such / expired session" — // safe to clear so the user isn't stuck presenting a dead cookie. clearFrameworkSessionCookies(event); return null; }, })(nitroApp); const app = getH3App(nitroApp); app.use( "/_agent-native/auth/supabase-login", defineEventHandler(async (event) => { try { if (getMethod(event) !== "POST") { return jsonResponse({ error: "Method not allowed" }, 405); } const { email, password } = await readBody<{ email?: string; password?: string; }>(event); if (!email || !password) return jsonResponse( { error: "Email and password are required" }, 400, ); const supabase = getSupabase(); if (!supabase) return jsonResponse({ error: "Auth is not configured" }, 500); const { data, error } = await supabase.auth.signInWithPassword({ email, password, }); if (error || !data.user) return jsonResponse({ error: "Invalid email or password" }, 401); const token = globalThis.crypto.randomUUID(); await addSession(token, data.user.email ?? email); setFrameworkSessionCookie(event, token); return { ok: true, email: data.user.email }; } catch { return jsonResponse({ error: "Login failed" }, 500); } }), ); return authInit; };