import { env } from "cloudflare:workers"; import { betterAuth } from "better-auth"; import { drizzleAdapter } from "better-auth/adapters/drizzle"; import { tanstackStartCookies } from "better-auth/tanstack-start"; import { db } from "@/db"; import { z } from "zod"; import { baseAuthConfig } from "@/lib/auth-config"; import { getOrCreateDefaultHostedOrganization } from "@/server/auth/default-hosted-organization"; import { sendHostedPasswordResetEmail, sendHostedVerificationEmail, } from "@/server/email/loops"; const hostedBaseUrlSchema = z .string() .url() .refine((value) => { const url = new URL(value); return ( url.protocol === "https:" || (url.protocol === "http:" && url.hostname === "localhost") ); }, "BETTER_AUTH_URL must use https or localhost"); function createAuth() { const baseUrl = getHostedBaseUrl(); const bypassEmail = Reflect.get(env, "BYPASS_EMAIL_VERIFICATION") === "true"; const auth = betterAuth({ baseURL: baseUrl, secret: getHostedSecret(), ...baseAuthConfig, emailAndPassword: { ...baseAuthConfig.emailAndPassword, requireEmailVerification: !bypassEmail, resetPasswordTokenExpiresIn: 60 * 60, revokeSessionsOnPasswordReset: true, sendResetPassword: async ({ user, url }) => { await sendHostedPasswordResetEmail({ email: user.email, resetUrl: url, }); }, }, emailVerification: bypassEmail ? undefined : { sendOnSignUp: true, autoSignInAfterVerification: true, sendVerificationEmail: async ({ user, url }) => { await sendHostedVerificationEmail({ email: user.email, confirmationUrl: url, }); }, }, trustedOrigins: getTrustedOrigins(baseUrl), database: drizzleAdapter(db, { provider: "sqlite", }), plugins: [...baseAuthConfig.plugins, tanstackStartCookies()], databaseHooks: { session: { create: { before: async (session) => { // Inject Better Auth's createOrganization here so the helper can // stay reusable without importing auth.ts and creating a cycle. const organizationId = await getOrCreateDefaultHostedOrganization( session.userId, (body) => auth.api.createOrganization({ body }), ); return { data: { ...session, activeOrganizationId: organizationId, }, }; }, }, }, }, }); return auth; } let authInstance: ReturnType | null = null; function getTrustedOrigins(baseUrl: string) { const trustedOrigins = [baseUrl]; if (process.env.NODE_ENV !== "production") { trustedOrigins.push( "http://open-seo.localhost:1355", "http://*.open-seo.localhost:1355", "https://open-seo.localhost:1355", "https://*.open-seo.localhost:1355", ); } return trustedOrigins; } function getHostedBaseUrl() { const baseUrl = env.BETTER_AUTH_URL?.trim(); if (!baseUrl) { throw new Error("BETTER_AUTH_URL is required in hosted mode"); } return hostedBaseUrlSchema.parse(baseUrl); } function getHostedSecret() { const secret = env.BETTER_AUTH_SECRET?.trim(); if (!secret) { throw new Error("BETTER_AUTH_SECRET is required in hosted mode"); } if (secret.length < 32) { throw new Error("BETTER_AUTH_SECRET must be at least 32 characters"); } return secret; } function hasHostedAuthEmailConfig() { const loopsVars = [ "LOOPS_API_KEY", "LOOPS_TRANSACTIONAL_VERIFY_EMAIL_ID", "LOOPS_TRANSACTIONAL_RESET_PASSWORD_ID", ]; return loopsVars.every((name) => { const value: unknown = Reflect.get(env, name); return typeof value === "string" && value.trim() !== ""; }); } export function hasHostedAuthConfig() { try { getHostedBaseUrl(); getHostedSecret(); return ( Reflect.get(env, "BYPASS_EMAIL_VERIFICATION") === "true" || hasHostedAuthEmailConfig() ); } catch { return false; } } export function getAuth() { if (authInstance) { return authInstance; } authInstance = createAuth(); return authInstance; }