import "server-only"; import { betterAuth } from "better-auth"; import { drizzleAdapter } from "better-auth/adapters/drizzle"; import { nextCookies } from "better-auth/next-js"; import argon2 from "argon2"; import { getDb, users, sessions, accounts, verifications, userPreferences } from "@/db"; import { getEmailService } from "@/lib/email"; import { writeAudit } from "@/lib/audit"; import { APP_URL, IS_PROD } from "@/lib/env"; import { log } from "@/lib/log"; const secret = process.env.AUTH_SECRET; if ((!secret || secret.length < 32) && IS_PROD) { throw new Error("AUTH_SECRET must be set (32+ chars)"); } /** Argon2id — OWASP-recommended parameters (19 MiB, t=2, p=1). */ const ARGON2_OPTS = { type: argon2.argon2id, memoryCost: 19_456, timeCost: 2, parallelism: 1 } as const; export const auth = betterAuth({ appName: "PolyLLM", baseURL: APP_URL, secret: secret ?? "dev-only-auth-secret-change-me-0123456789abcdef0123456789abcdef", trustedOrigins: [APP_URL, "http://localhost:3000"], database: drizzleAdapter(getDb(), { provider: "pg", usePlural: true, schema: { users, sessions, accounts, verifications }, }), emailAndPassword: { enabled: true, minPasswordLength: 10, maxPasswordLength: 128, requireEmailVerification: true, autoSignIn: false, revokeSessionsOnPasswordReset: true, resetPasswordTokenExpiresIn: 60 * 60, password: { hash: (password) => argon2.hash(password, ARGON2_OPTS), verify: ({ hash, password }) => argon2.verify(hash, password).catch(() => false), }, sendResetPassword: async ({ user, url }) => { await getEmailService().sendPasswordReset(user.email, url); await writeAudit({ userId: user.id, action: "password.reset_requested" }); }, onPasswordReset: async ({ user }) => { await writeAudit({ userId: user.id, action: "password.reset" }); await getEmailService().sendPasswordChanged(user.email).catch(() => {}); }, }, emailVerification: { sendOnSignUp: true, sendOnSignIn: true, autoSignInAfterVerification: true, expiresIn: 60 * 60 * 24, sendVerificationEmail: async ({ user, url }) => { await getEmailService().sendVerification(user.email, url); }, afterEmailVerification: async (user) => { await writeAudit({ userId: user.id, action: "email.verified" }); await getEmailService() .sendWelcome(user.email, user.name ?? "") .catch((e) => log.warn("welcome email failed", { error: (e as Error).message })); }, }, user: { additionalFields: { role: { type: "string", required: false, defaultValue: "user", input: false }, onboardingCompletedAt: { type: "date", required: false, input: false }, }, changeEmail: { enabled: true, sendChangeEmailVerification: async ({ user, newEmail, url }: { user: { email: string; id: string }; newEmail: string; url: string }) => { await getEmailService().sendEmailChangeApproval(user.email, url, newEmail); await writeAudit({ userId: user.id, action: "email.change_requested" }); }, }, deleteUser: { enabled: true, sendDeleteAccountVerification: async ({ user, url }) => { await getEmailService().sendAccountDeletion(user.email, url); }, beforeDelete: async (user) => { await writeAudit({ userId: user.id, action: "account.deleted" }); }, }, }, session: { expiresIn: 60 * 60 * 24 * 30, updateAge: 60 * 60 * 24, cookieCache: { enabled: true, maxAge: 5 * 60 }, }, rateLimit: { enabled: true, window: 60, max: 100, customRules: { "/sign-in/email": { window: 60, max: 8 }, "/sign-up/email": { window: 60, max: 4 }, "/forget-password": { window: 60, max: 4 }, "/request-password-reset": { window: 60, max: 4 }, "/send-verification-email": { window: 60, max: 3 }, "/change-password": { window: 60, max: 5 }, "/change-email": { window: 60, max: 3 }, "/delete-user": { window: 60, max: 3 }, }, }, advanced: { cookiePrefix: "polyllm", useSecureCookies: IS_PROD, }, databaseHooks: { user: { create: { after: async (user) => { await getDb().insert(userPreferences).values({ userId: user.id }).onConflictDoNothing(); await writeAudit({ userId: user.id, action: "account.created" }); }, }, }, session: { create: { after: async (session) => { await writeAudit({ userId: session.userId, action: "login", ipAddress: session.ipAddress ?? null, userAgent: session.userAgent ?? null, }); }, }, }, }, plugins: [nextCookies()], }); export type Session = typeof auth.$Infer.Session; export type AuthUser = Session["user"];