TypeScript 97.4%
SQL 1%
JavaScript 0.9%
CSS 0.6%
1import "server-only";2import { betterAuth } from "better-auth";3import { drizzleAdapter } from "better-auth/adapters/drizzle";4import { nextCookies } from "better-auth/next-js";5import argon2 from "argon2";6import { getDb, users, sessions, accounts, verifications, userPreferences } from "@/db";7import { getEmailService } from "@/lib/email";8import { writeAudit } from "@/lib/audit";9import { APP_URL, IS_PROD } from "@/lib/env";10import { log } from "@/lib/log";1112const secret = process.env.AUTH_SECRET;13if ((!secret || secret.length < 32) && IS_PROD) {14 throw new Error("AUTH_SECRET must be set (32+ chars)");15}1617/** Argon2id — OWASP-recommended parameters (19 MiB, t=2, p=1). */18const ARGON2_OPTS = { type: argon2.argon2id, memoryCost: 19_456, timeCost: 2, parallelism: 1 } as const;1920export const auth = betterAuth({21 appName: "PolyLLM",22 baseURL: APP_URL,23 secret: secret ?? "dev-only-auth-secret-change-me-0123456789abcdef0123456789abcdef",24 trustedOrigins: [APP_URL, "http://localhost:3000"],25 database: drizzleAdapter(getDb(), {26 provider: "pg",27 usePlural: true,28 schema: { users, sessions, accounts, verifications },29 }),30 emailAndPassword: {31 enabled: true,32 minPasswordLength: 10,33 maxPasswordLength: 128,34 requireEmailVerification: true,35 autoSignIn: false,36 revokeSessionsOnPasswordReset: true,37 resetPasswordTokenExpiresIn: 60 * 60,38 password: {39 hash: (password) => argon2.hash(password, ARGON2_OPTS),40 verify: ({ hash, password }) => argon2.verify(hash, password).catch(() => false),41 },42 sendResetPassword: async ({ user, url }) => {43 await getEmailService().sendPasswordReset(user.email, url);44 await writeAudit({ userId: user.id, action: "password.reset_requested" });45 },46 onPasswordReset: async ({ user }) => {47 await writeAudit({ userId: user.id, action: "password.reset" });48 await getEmailService().sendPasswordChanged(user.email).catch(() => {});49 },50 },51 emailVerification: {52 sendOnSignUp: true,53 sendOnSignIn: true,54 autoSignInAfterVerification: true,55 expiresIn: 60 * 60 * 24,56 sendVerificationEmail: async ({ user, url }) => {57 await getEmailService().sendVerification(user.email, url);58 },59 afterEmailVerification: async (user) => {60 await writeAudit({ userId: user.id, action: "email.verified" });61 await getEmailService()62 .sendWelcome(user.email, user.name ?? "")63 .catch((e) => log.warn("welcome email failed", { error: (e as Error).message }));64 },65 },66 user: {67 additionalFields: {68 role: { type: "string", required: false, defaultValue: "user", input: false },69 onboardingCompletedAt: { type: "date", required: false, input: false },70 },71 changeEmail: {72 enabled: true,73 sendChangeEmailVerification: async ({ user, newEmail, url }: { user: { email: string; id: string }; newEmail: string; url: string }) => {74 await getEmailService().sendEmailChangeApproval(user.email, url, newEmail);75 await writeAudit({ userId: user.id, action: "email.change_requested" });76 },77 },78 deleteUser: {79 enabled: true,80 sendDeleteAccountVerification: async ({ user, url }) => {81 await getEmailService().sendAccountDeletion(user.email, url);82 },83 beforeDelete: async (user) => {84 await writeAudit({ userId: user.id, action: "account.deleted" });85 },86 },87 },88 session: {89 expiresIn: 60 * 60 * 24 * 30,90 updateAge: 60 * 60 * 24,91 cookieCache: { enabled: true, maxAge: 5 * 60 },92 },93 rateLimit: {94 enabled: true,95 window: 60,96 max: 100,97 customRules: {98 "/sign-in/email": { window: 60, max: 8 },99 "/sign-up/email": { window: 60, max: 4 },100 "/forget-password": { window: 60, max: 4 },101 "/request-password-reset": { window: 60, max: 4 },102 "/send-verification-email": { window: 60, max: 3 },103 "/change-password": { window: 60, max: 5 },104 "/change-email": { window: 60, max: 3 },105 "/delete-user": { window: 60, max: 3 },106 },107 },108 advanced: {109 cookiePrefix: "polyllm",110 useSecureCookies: IS_PROD,111 },112 databaseHooks: {113 user: {114 create: {115 after: async (user) => {116 await getDb().insert(userPreferences).values({ userId: user.id }).onConflictDoNothing();117 await writeAudit({ userId: user.id, action: "account.created" });118 },119 },120 },121 session: {122 create: {123 after: async (session) => {124 await writeAudit({125 userId: session.userId,126 action: "login",127 ipAddress: session.ipAddress ?? null,128 userAgent: session.userAgent ?? null,129 });130 },131 },132 },133 },134 plugins: [nextCookies()],135});136137export type Session = typeof auth.$Infer.Session;138export type AuthUser = Session["user"];139