diff --git a/apps/server/worker/env.d.ts b/apps/server/worker/env.d.ts index bb5992f1..7836fc47 100644 --- a/apps/server/worker/env.d.ts +++ b/apps/server/worker/env.d.ts @@ -72,6 +72,8 @@ type WorkerBindings = { PEPPER: string // BOOTSTRAP_TOKEN:可选,seed/bootstrap 接口门控(配置则强制 X-Bootstrap-Token 匹配,防公网滥用) BOOTSTRAP_TOKEN?: string + // RESEND_API_KEY:可选,EMAIL_PROVIDER=resend 时经 Resend API 发事务邮件。 + RESEND_API_KEY?: string // Turnstile 必须成对配置:site key 是公开 runtime var,secret 是 Workers Secret。 // 两者都未配置时 dev/test 跳过;只配置任一项时认证配置与校验均 fail closed。 TURNSTILE_SITE_KEY?: string diff --git a/apps/server/worker/queues/email.ts b/apps/server/worker/queues/email.ts index c508a678..b495e366 100644 --- a/apps/server/worker/queues/email.ts +++ b/apps/server/worker/queues/email.ts @@ -62,6 +62,39 @@ export class CloudflareEmailProvider implements EmailProvider { } } +// Resend Provider:经 HTTPS API 发信,适用于无 Cloudflare Email Service 配额的自托管部署。 +// API Key 走 Workers Secret RESEND_API_KEY,不进代码与 D1。 +export class ResendEmailProvider implements EmailProvider { + readonly name = 'resend' + private readonly apiKey: string + + constructor(apiKey: string) { + this.apiKey = apiKey + } + + async send(input: EmailSendInput): Promise { + const resp = await fetch('https://api.resend.com/emails', { + method: 'POST', + headers: { + Authorization: `Bearer ${this.apiKey}`, + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + from: `${input.from.name} <${input.from.email}>`, + to: [input.to], + subject: input.subject, + html: input.html, + text: input.text, + }), + signal: AbortSignal.timeout(15000), + }) + if (!resp.ok) { + const body = await resp.text().catch(() => '') + throw new Error(`resend_send_failed: ${resp.status} ${body.slice(0, 200)}`) + } + } +} + // 事务邮件模板(Mustache 子集)。按 type + locale 选模板。 // 真实部署模板由 R2 语言包提供(见 07 章 4);此处内置 en/zh-Hans 兜底,保证无外部依赖可渲染。 type EmailTemplate = { @@ -564,6 +597,11 @@ export function resolveEmailProvider(env: Env): EmailProvider { if (!isDevOrTestEnvironment(env)) throw new Error('email_provider_not_configured') return new TestEmailProvider(env) } + if (env.EMAIL_PROVIDER === 'resend') { + const apiKey = (env as unknown as { RESEND_API_KEY?: string }).RESEND_API_KEY + if (!apiKey) throw new Error('email_provider_not_configured') + return new ResendEmailProvider(apiKey) + } return new CloudflareEmailProvider(env.EMAIL) } diff --git a/packages/types/src/env.ts b/packages/types/src/env.ts index 14fa3d64..0bce8ff1 100644 --- a/packages/types/src/env.ts +++ b/packages/types/src/env.ts @@ -17,7 +17,7 @@ export type EmailQueueMessage = { payload: Record } -export type EmailProviderName = 'cloudflare' | 'test' +export type EmailProviderName = 'cloudflare' | 'resend' | 'test' export type SmsProviderName = 'twilio' | 'vonage' | 'infobip' | 'messagebird' | 'test' export type WhatsappProviderName = 'twilio' | 'meta' | 'test'