← 返回博客

用 Auth.js / NextAuth Email 提供方对接 Rovela(2026)

自定义 Auth.js sendVerificationRequest,经 Rovela POST /emails 与 Idempotency-Key 发送魔法链接登录信——Auth.js 管令牌,Rovela 只投递;与 Next.js 魔法链接、Clerk、Supabase 指南区分。

更新于 2026年11月4日
用 Auth.js / NextAuth Email 提供方对接 Rovela(2026)

Auth.js 管魔法链接,Rovela 只负责投递

Auth.js(前身为 NextAuth.js)可通过 Email 提供方做邮箱登录:用户输入地址后,Auth.js 生成验证令牌,借助数据库 adapter 存哈希,并发送一次性登录 URL。官方已有若干厂商的 HTTP 邮件集成;对接 Rovela 时,按 Auth.js「自建 HTTP 邮件」文档实现自定义 sendVerificationRequest 即可。

本文演示该回调如何调用 Rovela 的扁平接口 POST https://api.rovela.dev/emails,并带上 Idempotency-Key:Auth.js / NextAuth 继续拥有会话与令牌,Rovela 负责入队、DKIM 签名与投递(发送邮件 API、快速开始、鉴权)。

Rovela 不替代 Auth.js。 它不创建用户、不签发会话、不算验证令牌哈希、也不判断链接是否有效。令牌生命周期由 Auth.js(加你的 adapter)负责;sendVerificationRequest 只是把已经生成好的 url 送到收件箱。

与下列文章区分:

Auth.js 做什么,Rovela 发什么

职责归属
Email 提供方配置、maxAge、adapter 表Auth.js + 你的数据库
生成 / 哈希验证令牌Auth.js
构造用户点击的回调 urlAuth.js
登录邮件的 HTML / 纯文本你的 sendVerificationRequest
POST /emails、DKIM、队列、退信Rovela
核销链接并建立会话Auth.js

Rovela 返回 200 表示已接受并入队,不等于「已进收件箱」(快速开始)。

前置条件

  1. 在 Rovela 控制台完成发信域名验证。未验证的 from 域名会返回 400(快速开始)。认证类邮件建议用 mail.example.com 一类子域(OTP / 魔法链接进垃圾箱)。
  2. 形如 re_… 的 API 密钥。生产发信优先 sending_access(鉴权)。
  3. Auth.js(或 NextAuth.js v4)并配置数据库 adapter。Email 提供方必须有 adapter(Auth.js Email、自建 HTTP 邮件)。
  4. ROVELA_API_KEY、AUTH_SECRET / NEXTAUTH_SECRET 与数据库凭证只放在服务端,不要打进客户端包。

环境变量

export ROVELA_API_KEY=re_your_sending_access_key
export ROVELA_FROM='Acme <signin@mail.example.com>'
export AUTH_SECRET="$(openssl rand -hex 32)"   # Auth.js / NextAuth
# adapter 所需的数据库 URL(Postgres 等)— Email 提供方必需

轮换 Rovela 密钥:先创建新密钥并部署,再吊销旧密钥(鉴权)。

注册自定义 Email 提供方

Auth.js 接受 type: "email" 且带自定义 sendVerificationRequest 的提供方对象。官方 HTTP 邮件指南常用自定义 id(如 http-email),随后用 signIn("http-email", { email })(或你所用框架的等价 API)触发(自建 HTTP 邮件)。

精简的 Auth.js / Next.js 示意(auth.ts)——此处省略 adapter 细节,但必须配置:

import NextAuth from "next-auth";
import { sendVerificationRequest } from "./lib/rovelaSendVerificationRequest";
// import 你的数据库 adapter,并作为 `adapter` 传入

export const { handlers, auth, signIn, signOut } = NextAuth({
  adapter, // Email 提供方必需
  providers: [
    {
      id: "http-email",
      name: "Email",
      type: "email",
      maxAge: 60 * 60 * 24, // 链接有效期 — Auth.js 常见默认可为 24h
      sendVerificationRequest,
    },
  ],
  // 按需配置 pages / callbacks
});

NextAuth.js v4 通常用 next-auth/providers/email 的 EmailProvider({ sendVerificationRequest, ... }) 包一层——回调参数(identifier、url、expires、provider、token、theme)同属这一族输入(NextAuth Email 提供方)。

sendVerificationRequest → Rovela POST /emails

Auth.js 会传入收件人(identifier)和可点击的 url。不要让 Rovela 发明令牌。自定义 HTML 时注意转义主机名;日志中不要出现原始 API 密钥。

// lib/rovelaSendVerificationRequest.ts
const ROVELA_URL = "https://api.rovela.dev/emails";

export async function sendVerificationRequest(params: {
  identifier: string;
  url: string;
  expires: Date;
  token: string;
  // 另有 provider、theme、request — 见 Auth.js Email 类型
}) {
  const { identifier: to, url, expires, token } = params;
  const host = new URL(url).host;
  const apiKey = process.env.ROVELA_API_KEY!;
  const from = process.env.ROVELA_FROM!;

  // 同一 Auth.js token 在传输重试时保持稳定
  const idempotencyKey = `authjs-email-${token}`.slice(0, 256);

  const escapedHost = host.replace(/\./g, "&#8203;.");
  const html =
    `<p>登录 <strong>${escapedHost}</strong></p>` +
    `<p><a href="${url}">点击登录</a></p>` +
    `<p>此链接将于 ${expires.toISOString()} 过期。</p>` +
    `<p>若非本人操作,可忽略本邮件。</p>`;
  const text =
    `登录 ${host}\n${url}\n\n` +
    `过期时间: ${expires.toISOString()}\n`;

  const res = await fetch(ROVELA_URL, {
    method: "POST",
    headers: {
      Authorization: `Bearer ${apiKey}`,
      "Content-Type": "application/json",
      Accept: "application/json",
      "Idempotency-Key": idempotencyKey,
    },
    body: JSON.stringify({
      from,
      to,
      subject: `Sign in to ${host}`,
      html,
      text,
    }),
  });

  if (!res.ok) {
    const data = await res.json().catch(() => ({}));
    // 不要记录 API 密钥 — 只记状态与提供方消息
    console.error("rovela_send_failed", res.status, data.message);
    throw new Error(`Rovela send failed: ${res.status}`);
  }
}

与 Rovela 文档对齐的要点:

  • 尽量同时发送 html 与 text(发送邮件 API)。
  • Idempotency-Key(≤ 256 字符):24 小时内重试会返回原先的接受结果,而不会再发一封——宿主在超时后重试 Auth.js 请求时很有用(发送邮件 API — 幂等)。优先用 Auth.js 的 token(或其他按挑战稳定的 id)派生,不要每次请求随机 UUID。
  • from 必须属于已验证域名,否则返回 400。

在 sendVerificationRequest 中抛错会让 Auth.js 感知发信失败;这是刻意的——静默成功却没有投递更糟。

触发邮箱登录

提供方 id 为 http-email 时:

import { signIn } from "@/auth"; // 或 next-auth/react 客户端辅助

await signIn("http-email", { email: "user@company.com" });

Auth.js 生成令牌、经 adapter 持久化哈希、构造 url,再调用你的发信函数。用户点击后,由 Auth.js 对照 adapter 校验令牌并建立会话——这条路径不经过 Rovela。

投递与运维清单

  1. 为 ROVELA_FROM 所用子域配置 SPF / DKIM / DMARC(DKIM 配置、SaaS 的 DMARC)。
  2. 认证邮件使用独立子域,避免营销信拖累同一声誉(垃圾箱问题)。
  3. 生产发信优先 sending_access;只有需要读取消息事件时才用 full_access(鉴权)。
  4. 在应用层限制同一地址申请新链接的频率——Auth.js 的 maxAge 只限制链接寿命,不限制刷请求。
  5. 不要把验证 token 查询串写进可读面很广的 Rovela tags 或日志;邮件正文里已经带有一次性 URL。

排障

现象可能原因
Rovela 发信 400from 域名未验证,或缺少 html / text / template
Rovela 401Bearer 密钥缺失或无效(鉴权)
Auth.js 启动拒绝 Email 提供方未配置数据库 adapter
发信返回 200 但用户未收到已入队但延迟/被过滤——检查域名认证与垃圾箱;必要时用 full_access 查看事件
重试出现重复邮件缺少或不稳定的 Idempotency-Key

小结

为 Auth.js / NextAuth 的 Email 提供方实现自定义 sendVerificationRequest,用 fetch 调用 Rovela POST /emails,设置稳定的 Idempotency-Key,并把密钥留在服务端。Auth.js 仍是令牌与会话的唯一真相源;Rovela 只是事务邮件投递层。

下一步:快速开始 · 发送邮件 API · Auth.js HTTP 邮件指南