import { createHmac, timingSafeEqual } from "node:crypto"; import { adapterEnv } from "@/config/env"; export type DevSession = { userId: string; workspaceId: string; exp: number }; /** * Sessão local sem senha. Existe apenas em `ADAPTER_ENV=development`. * Homologação pública usa Supabase Auth real — ver `stagingAuthEnabled`. * * A autoridade é `ADAPTER_ENV`, não `NODE_ENV`: a imagem roda sempre com * `NODE_ENV=production` (build otimizado), inclusive no compose local. E o * padrão protege o caso perigoso: sem `ADAPTER_ENV` explícito, um processo com * `NODE_ENV=production` resolve para `production` e esta função devolve false. */ export function devAuthEnabled(): boolean { return ( adapterEnv() === "development" && process.env.ADAPTER_DEV_AUTH === "1" && Boolean(process.env.ADAPTER_DEV_AUTH_SECRET) ); } function secret(): string { const value = process.env.ADAPTER_DEV_AUTH_SECRET; if (!value) throw new Error("ADAPTER_DEV_AUTH_SECRET ausente."); return value; } export function signDevSession(userId: string, workspaceId: string, ttlSeconds = 60 * 60 * 8): string { const body = Buffer.from(JSON.stringify({ userId, workspaceId, exp: Date.now() + ttlSeconds * 1000 })).toString("base64url"); const sig = createHmac("sha256", secret()).update(body).digest("base64url"); return `${body}.${sig}`; } export function verifyDevSession(token: string): DevSession | null { if (!devAuthEnabled()) return null; const [body, sig] = token.split("."); if (!body || !sig) return null; const expected = createHmac("sha256", secret()).update(body).digest("base64url"); const left = Buffer.from(sig); const right = Buffer.from(expected); if (left.length !== right.length || !timingSafeEqual(left, right)) return null; const parsed = JSON.parse(Buffer.from(body, "base64url").toString("utf8")) as DevSession; if (!parsed.exp || parsed.exp < Date.now()) return null; return parsed; }