import { scrypt, randomBytes, randomUUID, timingSafeEqual } from "crypto"; import { promisify } from "util"; import { query } from "@/lib/db-postgres"; const scryptAsync = promisify(scrypt) as ( password: string, salt: string, keylen: number, ) => Promise; /** * Hash a password with scrypt (Node stdlib — no native deps, Alpine-safe). * Format: `scrypt$$`. */ export async function hashPassword(password: string): Promise { const salt = randomBytes(16).toString("hex"); const derived = await scryptAsync(password, salt, 64); return `scrypt$${salt}$${derived.toString("hex")}`; } export async function verifyPassword( password: string, stored: string, ): Promise { const parts = stored.split("$"); if (parts.length !== 3 || parts[0] !== "scrypt") return false; const [, salt, keyHex] = parts; const keyBuf = Buffer.from(keyHex, "hex"); const derived = await scryptAsync(password, salt, 64); return keyBuf.length === derived.length && timingSafeEqual(keyBuf, derived); } // ── Session cookie ─────────────────────────────────────────────────────────── // Must match the cookie NextAuth issues for OAuth (see lib/auth/authOptions.ts) // so getServerSession reads sessions created here exactly the same way. const nextAuthUrl = (process.env.NEXTAUTH_URL ?? "").trim(); const isLocalNextAuth = nextAuthUrl.startsWith("http://localhost") || nextAuthUrl.startsWith("http://127.0.0.1") || (process.env.NODE_ENV === "development" && !nextAuthUrl); export const SESSION_COOKIE_NAME = isLocalNextAuth ? "next-auth.session-token" : "__Secure-next-auth.session-token"; const SESSION_MAX_AGE_MS = 30 * 24 * 60 * 60 * 1000; // 30 days export function sessionCookieOptions(expires: Date) { return { httpOnly: true, sameSite: "lax" as const, path: "/", secure: !isLocalNextAuth, expires, ...(isLocalNextAuth ? {} : { domain: ".vibnai.com" }), }; } /** * Create a database-backed NextAuth session row for a user and return the token * to set as the session cookie. This is the same `sessions` table the Prisma * adapter uses for OAuth, so the resulting session works everywhere * (`authSession()`, `useSession()`, sign-out, etc.). */ export async function createDbSession( userId: string, ): Promise<{ token: string; expires: Date }> { const token = randomBytes(32).toString("hex"); const expires = new Date(Date.now() + SESSION_MAX_AGE_MS); await query( `INSERT INTO sessions (id, session_token, user_id, expires) VALUES ($1, $2, $3, $4)`, [randomUUID(), token, userId, expires], ); return { token, expires }; }