diff --git a/src/email.ts b/src/email.ts new file mode 100644 index 0000000..a622fc3 --- /dev/null +++ b/src/email.ts @@ -0,0 +1,80 @@ +import type { Env } from "./env"; + +export type AuthEmailKind = "verify-email" | "reset-password" | "email-otp" | "magic-link"; + +export interface AuthEmailInput { + kind: AuthEmailKind; + to: string; + url?: string; + otp?: string; +} + +export interface EmailMessage { + to: string; + subject: string; + text: string; +} + +export function buildAuthEmail(input: AuthEmailInput): EmailMessage { + if (input.kind === "email-otp") { + return { + to: input.to, + subject: "Your sign-in code", + text: `Your sign-in code is ${input.otp ?? ""}.`, + }; + } + + if (input.kind === "reset-password") { + return { + to: input.to, + subject: "Reset your password", + text: `Use this link to reset your password: ${input.url ?? ""}`, + }; + } + + if (input.kind === "magic-link") { + return { + to: input.to, + subject: "Sign in to your account", + text: `Use this link to sign in: ${input.url ?? ""}`, + }; + } + + return { + to: input.to, + subject: "Verify your email", + text: `Use this link to verify your email: ${input.url ?? ""}`, + }; +} + +export async function sendEmail(env: Env, message: EmailMessage): Promise { + if (!env.MAIL_PROVIDER) { + throw new Error("MAIL_PROVIDER is not configured"); + } + + if (env.MAIL_PROVIDER !== "resend") { + throw new Error(`Unsupported MAIL_PROVIDER: ${env.MAIL_PROVIDER}`); + } + + if (!env.RESEND_API_KEY || !env.MAIL_FROM) { + throw new Error("RESEND_API_KEY and MAIL_FROM are required for resend"); + } + + const response = await fetch("https://api.resend.com/emails", { + method: "POST", + headers: { + Authorization: `Bearer ${env.RESEND_API_KEY}`, + "Content-Type": "application/json", + }, + body: JSON.stringify({ + from: env.MAIL_FROM, + to: message.to, + subject: message.subject, + text: message.text, + }), + }); + + if (!response.ok) { + throw new Error(`Email provider failed: ${response.status}`); + } +} diff --git a/tests/auth-config.test.ts b/tests/auth-config.test.ts index 25ed2bd..f4ec485 100644 --- a/tests/auth-config.test.ts +++ b/tests/auth-config.test.ts @@ -7,6 +7,7 @@ import { trustedOrigins, type Env, } from "../src/env"; +import { buildAuthEmail, sendEmail } from "../src/email"; const env: Env = { BETTER_AUTH_URL: "http://localhost:8788", @@ -40,3 +41,32 @@ describe("auth env helpers", () => { expect(optionalEnv({}, "MAIL_FROM")).toBeUndefined(); }); }); + +describe("auth email adapter", () => { + it("builds a verification email", () => { + const message = buildAuthEmail({ + kind: "verify-email", + to: "user@example.com", + url: "https://auth.example.com/verify", + }); + expect(message.subject).toBe("Verify your email"); + expect(message.to).toBe("user@example.com"); + expect(message.text).toContain("https://auth.example.com/verify"); + }); + + it("fails clearly when no mail provider is configured", async () => { + await expect( + sendEmail( + { + BETTER_AUTH_URL: "http://localhost:8788", + TRUSTED_ORIGINS: "http://localhost:8787", + }, + { + to: "user@example.com", + subject: "Test", + text: "Test", + }, + ), + ).rejects.toThrow("MAIL_PROVIDER is not configured"); + }); +});