feat: add auth email adapter

This commit is contained in:
2026-06-10 02:27:02 -07:00
parent 30e2c0e486
commit 9e0f751489
2 changed files with 110 additions and 0 deletions

80
src/email.ts Normal file
View File

@@ -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<void> {
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}`);
}
}

View File

@@ -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");
});
});