import { Resend } from "resend"; import { logger } from "./logger.js"; // Thin Resend wrapper used by Better Auth for verification + password reset // emails. Dev fallback: when RESEND_API_KEY is unset, log the body to stderr // instead of throwing — that lets worktree sign-ups continue to work without // production-level email credentials. const log = logger.child({ scope: "email" }); const FROM = process.env.SUPERLOG_FROM_EMAIL ?? "Superlog "; const REPLY_TO = process.env.SUPERLOG_REPLY_TO_EMAIL; let client: Resend | null = null; function getClient(): Resend | null { if (client) return client; const key = process.env.RESEND_API_KEY; if (!key) return null; client = new Resend(key); return client; } export type EmailMessage = { to: string; subject: string; html: string; text: string; }; export async function sendEmail(message: EmailMessage): Promise { const c = getClient(); if (!c) { log.warn( { to: message.to, subject: message.subject }, "RESEND_API_KEY not set — printing email body instead of sending", ); log.info({ body: message.text }, "email body (dev fallback)"); return; } const res = await c.emails.send({ from: FROM, to: [message.to], replyTo: REPLY_TO, subject: message.subject, html: message.html, text: message.text, }); if (res.error) { log.error({ err: res.error, to: message.to }, "resend send failed"); throw new Error(`resend: ${res.error.message ?? "unknown"}`); } } function escape(value: string): string { return value .replace(/&/g, "&") .replace(//g, ">") .replace(/"/g, """); } export function verificationEmailBody(url: string): { html: string; text: string } { const safe = escape(url); return { text: `Confirm your email to start using Superlog.\n\n${url}\n\nIf you didn't sign up, you can ignore this email.`, html: `

Confirm your email to start using Superlog.

${safe}

If you didn't sign up, you can ignore this email.

`, }; } export function passwordResetEmailBody(url: string): { html: string; text: string } { const safe = escape(url); return { text: `Reset your Superlog password.\n\n${url}\n\nThis link expires in 1 hour. If you didn't request a reset, ignore this email.`, html: `

Reset your Superlog password.

${safe}

This link expires in 1 hour. If you didn't request a reset, ignore this email.

`, }; } export function orgInvitationEmailBody(args: { url: string; orgName: string; inviterEmail: string; inviterName?: string | null; role: string; }): { html: string; text: string } { const safe = escape(args.url); const org = escape(args.orgName); const inviter = escape(args.inviterName?.trim() ? args.inviterName : args.inviterEmail); const role = escape(args.role); return { text: `${args.inviterName?.trim() || args.inviterEmail} invited you to join ${args.orgName} on Superlog as ${args.role}.\n\nAccept: ${args.url}\n\nIf you weren't expecting this invite, you can ignore it.`, html: `

${inviter} invited you to join ${org} on Superlog as ${role}.

Accept invitation

If you weren't expecting this invite, you can ignore it.

`, }; }