ESA Pages 体验之旅(8):四次 fetch 限制下的接口设计
ESA 函数调用外部服务时,需要同时考虑运行时能力和子请求预算。函数和 Pages 文档列出单次最多四次 fetch,Fetch 说明还要求计算重定向。
一次邮箱验证码登录,内部可能连续读写多条记录;发码还会调用邮件服务。这次认证实验分别记录框架方法和实际网络请求,再比较成熟框架、HTTP 数据适配器和完整认证服务。评估目标是保留 ESA、现有 TableStore 与免维护服务器的部署方式。
记录实际 HTTP 请求
一次适配器方法可以执行一个 SQL join,也可能拆成两次 HTTP 查询;一次批量 HTTP 又可以包含多行操作。只有在实际网络出口记录请求,才能核对 ESA 的预算。
本次固定 Better Auth 1.7.12,先观察方法轨迹,再通过 HTTP 协议替身验证完整验证码流程。PostgREST 原型已经将一次性消费合并为一次 DELETE RETURNING,关闭数据库限流、保留内存限流时,首次登录仍发出九次 HTTP,老用户八次,错误验证码七次。测试使用本机服务,不代表真实 Supabase 已验收。
按这些方法逐个发远程请求,完整流程会超出四次。错误路径也要计数,例如错误验证码需要读取状态并记录尝试,本组实验用了七次请求。
认证方案怎样影响请求预算
Better Auth 是可部署在自己服务端的开源认证框架,不等于必须接收费认证 SaaS。框架能接收标准 Request,只能说明入口契约接近;驱动加载、原子性、会话撤销和完整请求数仍需验收。各组结果的验证范围如下。
| 路线 | 本次完整流程的结果 | 证据范围与取舍 |
|---|---|---|
| Better Auth + PostgREST | 关闭数据库限流时,新用户 9 次、老用户 8 次 HTTP | 真实 handler + 本机 HTTP 协议替身;已合并单次原子消费,仍超预算 |
| Auth.js + Supabase 数据适配器 | 数据库会话新老用户各 5 次;JWT 基础登录 4 次,已有有效 Cookie 时 5 次 | 真实 handler + 本机替身;数字 OTP 保护与 JWT 撤销还需定制 |
| pg 直连 PostgreSQL | 未进入连接;ESA 加载 node:net / node:tls、驱动的 node:fs 失败 | 隔离 ESA 模块检查,未购买数据库 |
| 托管 Supabase Auth | 正常发码、校验、取用户、刷新、退出各 1 次 HTTP | 官方功能、源码与固定响应检查;没有运行真实 GoTrue 或阿里云托管登录 |
| 独立 Node + Better Auth + SQLite | 本机 10 个操作各 1 次代理请求 | 完整 handler 与持久数据库对照;新增服务器维护 |
| ESA + 自有邮箱 OTP + TableStore | 发码按 2 次、验证最多 4 次设计 | 本地真实邮件与存储已检查,ESA 生产登录入口仍待验收 |
HTTP 数据 API 仅替换数据库传输;完整 Auth 服务才会把多步操作移到下游。同为「走 HTTP」,ESA 发出的次数可能差很多。Auth.js 的现成适配器也确实减少代码,但六位数字码的猜测限制、重发失效、碰撞和已有会话路径需要一并验证。固定版本、完整调用表、PostgREST 修复及托管成本见认证框架与 HTTP 接入对比弹窗。
四次限制与调试入口的差异
隔离 ESA 调试入口曾连续执行五次真实 fetch 全部成功;带框架的测试包并发四、五、六次也返回 200,实测结果超过了文档所列预算。
但没有覆盖正式自定义域名、其他节点和长期运行,也没有平台确认限制取消。当前设计继续按公开的四次预算。原型中拒绝第五次请求的保护器是本地实验条件,与 ESA 现场的实际拒绝要分开表述。
网络预算还包括依赖自动发出的请求。Promise.all 只改变并发方式,不会把五个请求算成一个;把一个函数拆成几个互相调用的函数,也会增加边界、延迟和恢复责任。
合并读取,保留条件写入
请求开始时,如果能计算多个完整主键,而且读取互不依赖,可以一次批读。已经知道验证码键、身份键和历史用户键,就不必先后调用三个 GetRow。
但读取以后才能决定的条件写,不能为了节省次数提前做。例如先校验挑战,再按版本消费;只有消费成功的执行才能继续。唯一身份占用也必须由存储条件保证,不能只在应用里先查一次。
这次实际 TableStore 实现将验证码校验安排为:批读、条件消费、创建或读取用户、最终条件批写。新用户和已有映射用户各四次,历史用户首次补映射三次;邮件在另一个发码请求中发送。真实云存储验证与本机协议测试分别记录,ESA 生产登录尚未完成验收。旧用户、旧会话与收藏的兼容实现弹窗保留了字段、条件更新、真实 TableStore 请求轨迹与回退要求。
这份计数对应当前的邮箱验证和存储模型。增加 OAuth 授权码交换、撤销检查或新索引后,需要重新核算。
处理批写的部分成功
TableStore 的批写可能部分成功。收到 HTTP 200 后,应用仍需逐行检查;必要身份关系和会话全部成功,才可以交付登录令牌。
若用户已创建、身份映射失败,可能留下未关联记录;若映射成功而会话失败,下次应沿同一映射恢复。网络超时还可能意味着结果未知。四次已经用满时,不能随意追加「回滚请求」,更不能在没有整体事务的情况下宣称已经回滚。
因此预算必须连着原子性、幂等和恢复一起设计。仅把请求压到四次,却让同一验证码能并发成功两次,或者在部分写入时返回成功,都没有满足业务要求。
限制 SDK 的自动重试
固定 Supabase Auth SDK 2.118.0 的本机实验中,正常刷新一次请求;服务连续返回四次 503、第五次成功时,SDK 发出了五次请求。autoRefreshToken: false 关闭的是后台刷新,显式 refreshSession 仍会重试。
接入 SDK 时需要同时限制外发次数、总等待时间和重定向。达到请求上限后,也要停止库内部的退避等待。自有 HTTP 封装可以在第一次错误后返回;写操作是否允许重试,则取决于服务端的幂等或条件更新能力。
日志、缓存读写和失败上报也要算进总链路,不能先把四次全分给数据库,最后再补一次同步日志。
将完整业务交给 HTTP 服务
HTTP 数据接口只替换了数据库传输,认证框架依然在 ESA 内执行多步操作。完整认证 HTTP 服务则把校验、数据库与会话处理放到同一个服务内部,ESA 正常路径可以只调用一次。
托管 Supabase Auth 的正常发码、校验、取用户、刷新和退出,官方 API 与固定响应检查各为一次;真实阿里云托管登录尚未验收,重试和撤销检查也可能追加调用。独立 Node 认证服务的本机代理实验同样每操作一次,但新增了服务器维护。
完整服务减少了 ESA 发出的请求,数据库操作和费用仍在下游。当前项目不新增服务器,继续维护范围有限的邮箱 OTP:DirectMail 负责投递,ESA 负责挑战与会话,TableStore 保存共享状态。随机挑战、验证码 HMAC 摘要、发送配额、最多五次尝试、版本条件消费及会话撤销都由应用维护。邮件服务与自有认证的职责弹窗说明配置、历史费用口径与维护范围;以后需要 MFA、复杂恢复或企业 SSO,应重新评估。
实验主要记录于 2026-10-24,后续身份迁移保留各自日期。完整源码见认证验证工作区弹窗,运行方式见HTTP 实验说明弹窗,适配器原始计数见完整验证 JSON弹窗。成功、失败和重放路径分别计数后,才能判断批量请求是否足够,或是否需要把完整操作移到下游服务。
旧身份与收藏怎样兼容
业务数据归属稳定用户 ID,邮箱等登录身份单独映射到该 ID。历史用户沿用旧 ID,成功验证邮箱后才补建映射;新用户采用随机 ID。旧 Cookie 继续读取,收藏 owner、本机缓存和匿名认领进度保持原归属。增加渠道不按相同邮箱自动合并账号,绑定还需证明当前会话与新渠道的控制权。
最终批写只有必要映射与会话都成功,才交付登录令牌。部分失败可能留下未关联用户或未使用会话,后续应恢复相同身份关系。新随机 ID 开始写入后,回退版本也必须识别映射与两种会话,不能简单恢复旧邮箱散列算法。完整实现见身份迁移与部分失败恢复弹窗。
认证框架、HTTP 接入与数据库适配实验
先把「能运行」和「能接数据库」分开
Better Auth 的 Web handler 可以接收标准 Request 并返回 Response,这使它有机会运行在边缘环境。但认证请求还需要密码学、异步上下文、Cookie,以及存储适配器。只看到框架支持 Web API,还不能认定某个平台的最终打包产物可用。
这次固定 Better Auth 1.7.12、TableStore Node SDK 5.6.6,在 Node 26.12.0 下做本地对照,再部署一个独立 ESA 函数。测试验证码写入内存信箱,没有向真实用户发邮件,也没有改动现有站点的登录实现。
验证分成三层:先观察框架调用适配器的方法,再检查数据库实际执行什么,最后测网络请求。三层数据不能混用。
例如,一次 findOne(session, join user) 在 SQL 适配器里可能变成关联查询,在自定义 HTTP 存储里可能需要先找会话、再找用户。反过来,一个批量 RPC 也可能承载多个操作。数了适配器方法,还没有数完边缘平台的子请求。
邮箱验证码登录实际调用了什么
我通过真实 HTTP handler 依次执行发验证码、新用户登录、读会话、重复使用验证码、退出、重放旧 Cookie,再检查老用户登录和错误验证码。验证码采用哈希存储;为了连续测完这些路径,实验提高了限流阈值,数据库限流开启和关闭分别记录。
下表记录公共内存适配器方法数与邮件回调数。它适合判断「一次方法对应一次远端请求」的朴素适配是否合适,不能当作实际 TableStore 请求数,更不是所有数据库实现的理论下限。
| 操作 | 不启用数据库限流 | 启用数据库限流 |
|---|---|---|
| 发送验证码 | 3 | 5 |
| 新用户验证码登录 | 9 | 11 |
| 老用户验证码登录 | 8 | 10 |
| 读取会话 | 1 | 3 |
| 退出登录 | 4 | 6 |
| 输入错误验证码 | 7 | 9 |
发送验证码的 3 次包括两次数据库操作和一次邮件回调。首次登录的 9 次全部是数据库方法,涉及验证记录的查找、清理、原子消费、用户查找或创建,以及会话创建;事务边界另行记录。
首次登录的方法顺序如下。
其中一个实际节选是:
findMany verification
findMany verification
deleteMany verification
[transaction]
findMany verification
consumeOne verification
deleteMany verification
findOne user
create user
create session
这些调用并不都能简单删除。验证码除了「匹配正确」,还要处理过期、尝试次数和一次性消费。Better Auth 的适配器契约也不只包含普通增删改查,还涉及 consumeOne、incrementOne、事务及关联查询能力。
错误路径尤其容易漏算:这次错误验证码就触发了 7 次适配器操作。只测登录成功,可能会把失败重试这一条高频路径漏掉。
把每个方法映射成一个远端请求,再人为设置四次预算,首次登录会在第 5 次候选请求抛错,handler 返回 500。这证明这种直接映射不满足四次预算;它还不是 ESA 在第五次真实请求时拒绝的证据。
四次限制与调试入口的差异
阿里云的 函数和 Pages 文档仍写每次最多四个 fetch,Fetch 说明还把重定向计入请求。按这份公开限制设计,9 次适配器调用直接对应 9 次 TableStore 请求显然不合适。
但现场测试出现了差异:隔离函数通过默认调试域名,连续执行五次真实 fetch 全部成功;在带框架的包里,并发四、五、六次请求也全部返回 200。这个结果应当保留,不能为了迎合文档,把实验写成「第五次必然失败」。
同样,调试入口成功也不足以宣布限制已经取消。这次没有验证正式自定义域名、不同节点或长期额度;入口配置和文档更新时间的差异,都需要继续向平台确认。生产方案暂时按公开预算保留余量,会比依赖一次超限成功更稳妥。
同组实验还记录了打包和环境分支问题。
云端实验还遇到了两个和调用次数无关的兼容问题:
- 环境探测访问
Deno.env.toObject()时失败。ESA 暴露部分 Deno 全局,测试包显式使用平台的node:process,避免误选环境分支。 - 从
better-auth/adapters/memory转导出入口打包,适配器调用变成了「不是函数」。同一个 bundle 在 Node 下也失败,说明不能直接归因于 ESA。改用公开的@better-auth/memory-adapter@1.7.12包入口后通过。这里只记录这个版本与打包组合,没有证据把它定为所有环境都会遇到的上游 bug。
最终约 540 KB 的测试包,在本地 bundle 和云端调试入口都通过了登录、退出、旧 Cookie 失效和验证码重放检查。这里的内存状态只存在于一次隔离执行中,证明的是框架运行兼容,不能拿来保存正式用户。
TableStore 的困难不在普通读写
TableStore 并不是完全没有认证所需的原子能力。两组真实并发实验使用临时表,结束后删除测试表:
| 场景 | 结果 | 实际 API 构成 |
|---|---|---|
| 16 个请求尝试「键不存在时创建」 | 1 成功,15 条件冲突 | 16 次 PutRow |
| 16 个请求读到同一快照,再按版本条件删除 | 1 成功,15 条件冲突 | 16 次 GetRow + 16 次 DeleteRow |
第二种可以作为一次性消费的原语:先取得记录,再用版本条件删除,只有删除成功的请求才能返回已消费的快照。它需要正确处理「别人已经消费」和「读取后记录被替换」,不能先返回成功再异步删除。
但两个原语距离完整适配器还有一段路。按邮箱找用户、按 token 找会话、按用户撤销全部会话、按 identifier 找验证记录,都需要合理的主键或索引。创建用户、占用唯一身份、写索引和创建会话如果中途失败,还要恢复一致性。
局部事务可以扩展原子操作范围,但受表和分区限制,开始与提交本身也增加调用。不能把几条没有共同事务的批写,当成整体成功或整体回滚。
另外,当前 SQL DML 文档已经说明非事务表支持 INSERT、UPDATE 和 DELETE,「TableStore SQL 只读」不再是准确结论。不过 UPDATE/DELETE 要求完整主键等值条件,通用 UNIQUE、FOREIGN KEY 等约束也不受支持。它和关系数据库之间,仍隔着 Better Auth 需要的查询及一致性语义。
如果只实现当前业务用到的几个方法,代码可能不长;但框架升级、新插件多查一个字段时,维护者仍要检查整个契约。按邮箱 OTP、索引恢复、并发检查及回归测试的范围估算,生产级适配可能需要 7—12 个开发工作日。这是工程预算,不是这次实验的实际工时;再做通用查询和更多插件,还要另算。
对小项目来说,TableStore 的账单可能便宜,自己维护认证数据库适配层的成本却未必低。
pg 直连在数据库查询之前就失败了
使用现成 SQL 适配器,看起来只差一份便宜的 PostgreSQL。为此,我固定 pg@8.23.1,在独立 ESA 函数里验证了模块加载;并用 node:process 作支持的 Node API 对照。
| 测试 | 本次结果 | 能说明什么 |
|---|---|---|
静态导入 node:process |
HTTP 200 | 上传、鉴权和 Node 模块对照链路正常 |
静态导入 node:net、node:tls |
HTTP 596,Unsupported scheme "node" |
当前运行时无法加载标准 TCP/TLS 模块 |
| pg 最终 bundle 在 Node 加载 | Client 构造成功,没有连接数据库 | 本地打包本身可以运行 |
| 同一 pg bundle 在 ESA 加载 | HTTP 596,node:fs 加载失败 |
驱动还有其他不兼容依赖,尚未进入连接阶段 |
标准 pg 的连接依赖 Node 网络能力,见驱动连接说明。ESA 的运行时手册只提供部分 Node API。文档没有列出某个模块,不足以证明不支持;这次的实际加载错误补充了证据。
当前 ESA 调试入口在加载 pg 模块时失败,尚未进入数据库连接或查询。此前 Better Auth 的内存流程能运行,因此失败需要定位到驱动依赖;更换数据库价格规格无法解决模块加载问题。测试未购买数据库,临时函数已删除。
Supabase 数据 API 的完整调用数
Supabase 的 PostgREST 数据接口可以避开 TCP 驱动。它把数据库操作暴露为 HTTP,例如读取用户、更新记录、删除并返回结果。但认证框架如果仍在 ESA 里运行,多次数据库操作也就仍是多次外部请求。
找到的 Better Auth PostgREST 社区实现,在当前框架版本下存在输入转换未等待、写入序列化为空对象、过滤及排序契约不匹配等问题。隔离原型修复了这些问题,并明确拒绝不能通过普通 PostgREST 表接口实现的交互事务;它不是经过完整认证适配器测试的发布包。
随后用真实 Better Auth handler、PostgrestClient 和本机 HTTP 服务跑完整邮箱 OTP。底层 SQLite 仅作为协议替身,不代表 PostgreSQL、RLS 或真实 Supabase 已经验证。客户端实际发出数与服务端收到数逐项核对,结果如下:
| 操作 | 单请求原子消费,内存限流 | 单请求原子消费,数据库限流 |
|---|---|---|
| 首次发码,含一次 HTTP 发信替身 | 3 | 5 |
| 新用户提交验证码 | 9 | 11 |
| 老用户提交验证码 | 8 | 11 |
| 读取有效会话 | 1 | 3 |
| 错误验证码 | 7 | 10 |
| 退出 | 4 | 6 |
这里已让原子消费使用一次 DELETE ... RETURNING;使用框架的条件删除 fallback 时,新老用户分别是 10、9 次。少一次消费请求有帮助,却没有让完整流程接近四次预算。内存限流也是较有利的实验条件,不能当作多节点共享限流方案。
四次保护器在本地拒绝第五次请求,新老用户的登录均失败,因此没有继续创建云端 Supabase 或部署该适配原型。这个结果证明「修复 CRUD 适配器并保留现有框架流程」不满足预算,不证明所有可重新设计的认证接口都做不到。
PostgREST 修复、许可与九项契约检查随下列工作区公开。
公开实验结果可从实验结果索引逐项取得。
请读取 /files/esa-pages-journey-8/kits/auth-http-evaluations/ 下的 README.md、MANIFEST.json、AGENT_PROMPT.md 和 FILES.json,按文件索引取得所需实验。先核对依赖版本和请求计数层次,只执行本机验证,不直接修改业务认证或创建云资源。
Auth.js 有现成适配器,邮箱 OTP 仍有额外工作
Auth.js 的 Supabase 适配器已经发布,不需要从空白实现。它通过 Supabase JS 访问独立的 next_auth schema,文档明确说明它与 Supabase Auth 无关,也不由 Supabase 团队直接维护或支持。
这一次固定 @auth/core@0.41.3、@auth/supabase-adapter@1.11.3 和 Supabase JS 2.118.0,保留原始适配器和 CSRF 检查,仍通过真实 handler 与本机 HTTP 协议替身测量。
数字码可以接,但默认是邮件链接
Auth.js 的邮箱登录默认发送 Magic Link,也就是点击邮件中的一次性链接。通过 generateVerificationToken 和发送回调,可以改为六位数字码,再由自定义表单提交给邮箱 callback。这个接法已经在本地 handler 中登录成功,不能写成「Auth.js 完全不支持邮箱验证码」。
但改短 token 不会自动补齐 OTP 产品行为。连续输错八次后,正确码仍可登录;连续重发后,新旧码也都有效。发送冷却、猜测次数及「只接受最新码」需要单独决定和实现。
还有一个容易漏掉的约束:官方 schema将 token 本身设为唯一键,而发码逻辑只用 token 和服务端 secret 算散列,没有加入邮箱。两个邮箱生成相同六位码时,会得到相同散列,发生全局唯一键冲突。本地约束检查复现了第二次写入被拒绝;处理短码碰撞也属于这条定制路线的工作。
JWT 基础登录四次,已有会话时五次
| 一次入站请求的操作 | 默认数据库会话 | JWT 会话 |
|---|---|---|
| 发码,含一次 HTTP 发信替身 | 3 | 3 |
| 新用户提交码,无已有 Cookie | 5 | 4 |
| 老用户提交码,无已有 Cookie | 5 | 4 |
| 带有效会话 Cookie 再次登录 | 6 | 5 |
| 读取尚未需要刷新的有效会话 | 1 | 0 |
| 退出 | 1 | 0 |
邮箱 callback 和登录注册处理各查一次用户,老用户再更新邮箱验证时间。JWT 省掉最后一次会话写入;已有 Cookie 时,又会按会话里的用户 ID 多读一次。对应位置见邮箱 callback和登录注册处理。
本地四次保护器拒绝数据库会话的第五次请求时,验证码已消费、用户已创建、会话未写入,最终进入错误跳转。不能只看到 HTTP 302 就认为登录成功。
JWT 也有撤销取舍:退出清除当前 Cookie,不会自动让已保存的旧 JWT 失效。实验中旧 JWT 仍能读取会话,数据库会话的旧 Cookie 则失效。这是默认策略的差别,不是正常浏览器没有清除 Cookie。
因此,现成适配器确实省代码,但完整数字 OTP 仍需定制和重新核算预算,当前没有直接迁移业务。
数据库会话的实际轨迹与完整 handler 如下。
新用户数据库会话的实际轨迹是:
useVerificationToken DELETE verification_tokens ... RETURNING ...
getUserByEmail GET users ...
getUserByEmail GET users ...
createUser POST users ...
createSession POST sessions ...
直接使用托管 Supabase Auth,变化在服务边界
另一种接法是把认证交给 Supabase 自带的 Auth。下图中的 PostgREST 与 Auth 虽然都走 HTTP,承担的工作不同。
AnalyticDB 托管 Supabase提供 Auth API,与 Supabase SDK 兼容;它的邮件配置文档也说明可以配置自定义 SMTP。这里讨论的是 AnalyticDB PostgreSQL 版,不是需要另购 RDS 的 RDS Supabase,也不是海外 Supabase Cloud 的套餐。
Supabase 原生邮箱 OTP通过 signInWithOtp 发码、verifyOtp({ email, token, type: 'email' }) 校验。邮件模板展示 {{ .Token }} 后,用户输入六位验证码;不需要自己改造长 token,也不需要再加 Better Auth 或 Auth.js。
| 一次 ESA 入站请求的操作 | Auth HTTP 接口 | 正常 fetch 数 |
|---|---|---|
| 发码,允许新邮箱注册 | POST /auth/v1/otp |
1 |
| 校验码并建立会话 | POST /auth/v1/verify |
1 |
| 验证 access token 并读取用户 | GET /auth/v1/user |
1 |
| 刷新会话 | POST /auth/v1/token?grant_type=refresh_token |
1 |
| 退出当前设备 | POST /auth/v1/logout?scope=local |
1 |
认证服务内部仍然查数据库、生成挑战、发邮件和创建会话,但这些动作不再逐条占用原 ESA 执行的 fetch。新用户的自动建号也由后端完成,发码和邮箱已验证仍是两个状态。
这里完成的是官方功能核对、SDK 源码核读与本机固定 HTTP 响应检查,没有运行真实 GoTrue 或阿里云 Supabase。请求数有依据,真实投递、验证码过期、重放、并发、限流和托管版本尚未云端验收,不能把这一行「1」写成整套认证已经验证通过。
正常一次,不代表 SDK 永远只发一次
固定 @supabase/auth-js@2.118.0 后,17 项本机请求检查表明:正常发码、校验、显式 token 取用户、刷新和退出各一次;源码与最终 Web bundle 结果一致。先 setSession 再退出则是两次,因为 setSession 会额外读用户。
更影响 ESA 的是刷新失败。SDK 的 _refreshAccessToken 会指数退避;autoRefreshToken: false 只关闭后台刷新,显式 refreshSession 仍会重试。本机让服务连续返回四次 503、第五次成功,实际发出 5 次 请求。
无自动重试的 HTTP 传输在第一次 503 后就返回失败,只发一次。因此,ESA 接入可以采用有超时、不跟随重定向、不自动重试的 HTTP 边界;如果保留 SDK,需要同时限制外发次数和总等待时间,不能只在第五次阻止网络,却让 SDK 继续退避。
这没有关闭服务端验证码验证或限流,只是控制调用方怎样处理暂时失败。
SDK 请求与重试检查的源码如下。
托管服务仍需验收费用与会话语义
免费规格仅用于开发测试,正式邮件可能涉及公网出网与 SMTP 成本。默认退出范围是 global,按设备退出需显式用 local;已签发的 access token 也可能继续有效,立即撤销检查会增加调用。发送冷却、猜测次数与代理后的真实 IP 应按实际托管版本核对。
托管 Auth 还需要核对邮件、网络和会话配置。
邮件、网络和免费规格仍有成本
现有 DirectMail 可以作为自定义 SMTP 的候选。杭州服务地址为 smtpdm.aliyun.com,TLS 端口为 465;SMTP 需要给发信地址设置专用密码,不是复用 HTTP API 的 RAM AccessKey Secret。见SMTP 地址和发信配置流程。SMTP 由托管 Auth 连接,ESA 不执行 TCP/TLS 发信。
不过,邮件接入把项目公网访问列为前提,出网配置给出的路径是 EIP 或 NAT 网关/SNAT,可能产生网络费用。是否能复用已有出网能力要另查,不能因为免费规格就认定发信链路免费。
AnalyticDB 的免费 1 核 2 GB 项目也明确仅供开发测试,不能用于生产。默认测试 SMTP 有送达、速率及可用性限制。正式使用仍要算计算、存储、网络和邮件账单,本次没有购买这些资源。
退出与限流需要按实际版本确认
Supabase JS 的退出默认范围是 global,会影响其他设备。按设备退出应显式设置 local;退出撤销 refresh token,已签发 access token 则可能在过期前继续有效。
若要保持现有应用「撤销后旧会话立即失效」的语义,官方建议检查 JWT 的 session_id 是否仍对应 auth.sessions 记录。受限服务端 RPC 是一种候选,但会增加调用,本次没有实现。正常校验用户一次、再读一笔业务数据一次,并不代表加上刷新、撤销检查和身份迁移后仍只有两次。
上游提供邮箱发送冷却、项目发信额度和验证接口 IP 限流,不能直接等同于「每个挑战最多错五次」。阿里云允许通过 GET /auth/v1/settings 查询实际配置、通过 PATCH /auth/v1/modify/settings 调整支持的字段;不要把海外 Cloud 默认配额及 IP 转发规则直接套过去。代理之后的真实访客 IP、公开 HTTPS 域名和白名单,也都属于部署验证范围。
独立 Node 认证服务作为技术对照
另一条路线是让 ESA 继续提供页面和搜索,把完整认证 handler 放进香港的 Node 服务。边缘收到一次认证请求,只代理一次;验证码、用户和会话的数据库操作,都在认证服务内部执行。
这次用 Better Auth 的现成 SQLite 适配跑了同样的流程,并补了关闭数据库连接、重新连接后读取会话的检查。随后通过真实本地 HTTP 服务做代理实验:10 个操作,共 10 次代理网络请求,每个操作一次;退出后原 Cookie 无法再读到用户。
这个「一次」只计算边缘到认证服务的边界。认证服务内部仍然执行多条 SQL、可能调用发信服务,也需要部署与监控;它没有让下游工作或费用消失。
SQLite 适合这里的原因是减少存储适配工作,又能先控制小项目成本。它必须使用可靠的持久磁盘,并安排备份和恢复。函数计算的临时文件与内存会随实例释放,官方说明并不把它们当作持久存储。SQLite 官方也解释了网络文件系统的同步、锁和可靠性问题,所以不应简单把 SQLite 文件放在 FC 临时目录、OSS 挂载或未经验证的 NAS 里。
这条路线可以全部使用自有代码与阿里云资源,不需要认证 SaaS 或第三方托管数据库。代价是一台服务器的运维和单机可用性:要更新系统、监控进程、做一致备份,并真正演练恢复。
2026-10-24 查询的香港轻量服务器原价为 1 GB 34 元/月、2 GB 56 元/月,示例 RDS MySQL Serverless 连续运行约 256.74 元/月,备份、发信等另计。
以下保留当次只读查询的套餐、时间与费用算式。
独立认证服务与托管数据库的成本
2026-10-24,我通过阿里云只读 ListPlans 查询中国香港通用型 Linux 套餐:1 GB/30 GiB 的包月原价为 34 元,2 GB/40 GiB 为 56 元。OriginPrice 的含义见 官方 API 字段说明。列表价格不保证库存或最终订单,额外备份、磁盘和发信另计。
如果允许独立服务器,可以把 2 GB、56 元/月作为评估起点,但不能把最低内存规格当作已经压测的容量保证。SQLite 软件本身不收费;如果已有合适的香港服务器,也可以复用。自托管 PostgreSQL 或 MySQL 同样没有数据库授权费,但会增加进程、内存和维护工作,是否值得要看并发与运维需要。
托管数据库省运维,但未必比小服务器便宜。以 RDS MySQL Serverless 官方价格为例,香港基础系列、单节点、0.5 RCU、20 GB、持续运行 730 小时:
| 部分 | 算式 | 月费用 |
|---|---|---|
| 计算 | 0.5 × 0.5994 × 730 | 218.781 元 |
| 存储 | 20 × 0.0026 × 730 | 37.96 元 |
| 合计 | 计算 + 存储 | 约 256.74 元 |
这还没有加认证服务的计算、日志和其他费用。自动暂停能减少计算账单,但官方给出的唤醒时间约 6—40 秒,可能超过 ESA 的 10 秒网关等待时间。对随时可能发生的登录请求,这个延迟不能忽略。
独立 Node + SQLite 在允许维护服务器时是可行对照,但它不符合本项目「保留 Pages、免维护服务器」的目标。不能因为数据库适配容易,就把一台服务器的运维视为免费。TableStore 也可以继续保存收藏,不必为了认证框架一次性迁走所有业务数据。
DirectMail 与自有邮箱认证的职责
发邮件和认证,分别由谁负责
DirectMail 负责把应用提交的邮件交给收件方邮件系统。它不替应用决定验证码是否正确,不保存用户归属,也不负责签发这个项目的设备 Cookie。应用负责随机挑战、码的摘要、尝试次数、一次性消费、账号与会话;TableStore 保存跨节点共享的状态。
Better Auth 则是可以部署在自己服务端的开源认证框架,提供用户、会话、身份关联和邮箱验证码插件,采用 MIT 许可证。引入开源框架不等于把账号交给收费 SaaS,仍需安排执行环境、数据库与邮件服务。
QQ、Outlook 等邮箱的 SMTP,以及 nodemailer 这样的客户端库,又是另外两个层次。库负责协议调用,实际投递仍依赖邮件服务商;注册了一个邮箱,也不代表可以任意使用自己域名作为发件身份。这个项目选择 DirectMail,是为了使用自有发信子域、HTTP API、投递记录与可配置的发送额度,不再维护 SMTP 连接适配。
当前发件人为 noreply@mail.search.shengsheng.fun,使用杭州地域触发邮件。发信子域配置所有权验证、SPF、DKIM 等记录,DMARC 用来声明对齐与处理策略;这些配置与网站自身的 HTTPS 证书是不同的事情。无需为了一个发件地址先购买企业邮箱。发信域名配置
真实测试记录中的 SendOk 和 250 Send Mail OK,说明观察到的邮件被相应投递链路接受;它们不保证每家收件服务都进入收件箱,更不代表用户一定读到。退信、垃圾箱、域名信誉和发送频率仍需要观察,重发也不能靠客户端按钮独自限流。
按 2026-10-29 的DirectMail 计费文档,按量价为 2 元/1,000 封;主账号共 2,000 封免费额度,每天最多免费 200 封。后者不是每天永久赠送 200 封,投递到收件系统失败的邮件也不一定免计费,需按文档的入信规则判断。
项目自己的初始全站发送上限为每天 200 次,两者恰好数字相同,含义却不同。若按 30 天、每天 200 封计算,不计免费额度抵扣时,基础按量估算为 12 元;这是发信上限场景,不含函数、数据库与其他功能,也不是实际月账单。新项目先按量使用,没有因为存在资源包就提前购买。
短信、QQ 或微信以后可以作为独立身份渠道增加,各自核对申请资格、签名模板、授权协议与接口约束。它们不会改变「先识别稳定用户,再把某个登录方式映射过去」的原则;当前也没有把新渠道提前接入。
当前先保留受限范围的自有认证
现有实现由 DirectMail 投递邮件,ESA 中的应用代码负责挑战、校验、用户与设备会话,TableStore 保存共享状态。当前仅实现邮箱 OTP,正式登录入口暂未开放;没有用户名密码、社交账号绑定、MFA 或复杂账号恢复。
它的范围比一个通用认证框架小:随机挑战与六位码一起验证,验证码保存 HMAC 摘要;同邮箱有发送冷却及配额,挑战最多五次尝试,使用版本条件更新处理一次性消费。设备持有随机会话令牌,服务端保存摘要,退出删除会话,旧 Cookie 随即失效。发码按两次、校验按四次外部请求设计,本地真实存储和邮件接入已经检查,ESA 生产入口仍需验收。
在这个范围内,我倾向于暂时保留现有实现。原因是业务已经能表达当前需求,框架适配反而带来请求预算和存储语义的额外成本;托管 Supabase Auth 虽然更省调用,其正式费用、出网、邮件和会话语义还没完成真实接入验证。这不是「自研一定比框架更安全」的判断,后续修复、测试和安全维护仍由应用承担。
| 路线 | 这次评估后的使用条件 |
|---|---|
| ESA + 自有邮箱 OTP + TableStore + DirectMail | 当前范围内继续维护,保持并发、限额、撤销与四次预算的回归检查 |
| Better Auth + 自写 TableStore / PostgREST 适配 | 当前不选;通用适配责任重,已有完整 OTP 流程超预算 |
| Auth.js + Supabase 数据适配器 | 保留为备选;数字码保护、已有会话路径和 JWT 撤销仍需定制 |
| ESA + 托管 Supabase Auth | 更有希望的托管方向;先核实正式总费用、网络、发信及撤销需求,再做隔离云端验证 |
| 独立 Node + SQLite / SQL | 允许维护服务器时再考虑;当前不为登录新增服务器 |
如果后续要做 MFA、复杂恢复、多个组织、企业 SSO、完整设备管理,维护范围就明显变了。届时应重新比较托管 Auth 的总成本,而不是为了守住第一版选择持续扩建一套通用框架。
稳定用户身份与旧数据迁移
多渠道登录要保留什么边界
多渠道登录应共用一套用户与会话模型。邮箱、手机号和第三方身份都是证明用户身份的方式,业务数据应该归属一个稳定的用户 ID。
上图展示模型的扩展方向。邮箱对应的用户与身份映射已经在代码中拆开,QQ、微信尚未接入;多渠道绑定与账号合并仍需单独实现。
| 对象 | 职责 | 需要保留的约束 |
|---|---|---|
| User | 账号主体及状态 | ID 不因邮箱、手机号、昵称变化而改变 |
| Identity | 某个渠道的已验证身份 | 按提供方、发行方/应用范围和 subject 唯一;映射到同一个 user_id |
| Session | 当前设备的登录状态 | 共用创建、过期、撤销和权限检查规则 |
| Challenge / OAuth transaction | 验证码或授权交互的临时状态 | 有用途、有效期、重放防护和单次消费边界 |
例如用户换邮箱,收藏不应跟着换主键;已登录用户绑定另一个渠道,需要证明新渠道的控制权;新身份已经属于另一个用户,应明确提示冲突,而不是覆盖原账号。两个提供方返回相同邮箱,也不应该直接写一条「邮箱相同就合并」的规则。
Better Auth 的用户与账号关联机制和 Supabase 的身份关联说明都值得作为模型与失败路径的参考。但参考一套模型,不等于已经获得该框架的安全保证,自动合并策略也不能不经判断照搬。
协议部分优先复用成熟实现。例如 openid-client v6提供 OAuth 2.0 / OIDC 客户端能力,采用 Web Crypto 和 Fetch API;它负责协议交互,不负责应用的账号表、绑定冲突或收藏归属。是否适配 ESA、QQ/微信具体接口及四次预算,仍要按真实提供方验证,本次没有引入该库。
登录与绑定也要区分:已登录用户绑定新身份,需同时证明当前会话和新渠道的控制权;新身份已属于其他用户时明确提示冲突;解绑最后一个可恢复渠道前需要补救方式。两个已有账号的合并是另一个业务操作,不应隐藏在普通登录里完成。
最初的实现把邮箱哈希当作用户主键。准备增加第二种身份前,我先把「如何找到用户」移到身份映射。历史 ID 保留为不透明的稳定标识,新用户使用独立随机 ID;以后改邮箱时,业务归属不必跟着重建。改绑入口、证明新身份的流程和会话失效策略仍是后续工作。
每新增一个渠道还要重新核算外部授权码交换、用户资料请求、身份索引查询、唯一占用和会话写入。当前邮箱 OTP 在四次内,不保证未来 OAuth callback 也在四次内。若无法同时保证原子性、恢复与预算,就应重新选择服务边界,不能用拆成多个 ESA 函数的方法掩盖失败状态。
匿名收藏认领、旧 Cookie 停用、失败重试和回退都属于迁移工作,登录接口返回成功并不代表这些数据已经正确归属。
把用户拆出来,同时保留旧账号和收藏
这次迁移的目标很小:继续只开放邮箱验证码,把用户与登录身份分开,为以后增加渠道留出位置。真正容易出错的地方是已有数据。账号 ID 已经出现在会话、收藏 owner、浏览器缓存和匿名集合认领进度里;只改用户表,会把这些关系留在旧键上。
旧 ID 可以继续表示同一个用户
旧账号使用 SHA-256('account:' + normalizedEmail) 作为 ID。它的来源和邮箱耦合,但这个已经生成的值仍然可以作为稳定用户标识。解耦需要改变的是以后的查找方式,不要求给老用户换一个 ID。
迁移后继续使用原来的永久表,增加一类身份映射记录:
| 记录 | 当前内容与职责 |
|---|---|
account:<userId> |
用户主体。新用户保存 { id, createdAt },历史 { id, email } 原样保留 |
identity:<digest> |
{ provider, issuer, subject, userId },负责已验证身份到稳定用户的映射 |
session:<tokenDigest> |
设备会话,放在原来的临时表中,保存 userId、登录身份快照和过期时间 |
邮箱映射以 provider = email、issuer = sheng-search、规范化邮箱作为 subject。issuer 表示身份的应用发行范围,不随访问域名或发信域名变化。映射键是三项组成数组后序列化的 SHA-256,避免简单字符串拼接产生歧义,也给未来不同提供方、不同应用范围的主体留下区分。
用户新注册时生成独立的 256 位随机 ID;历史账号则在下一次成功验证邮箱时补建映射。查找顺序也很明确:先信任有效映射;没有映射,才检查旧邮箱哈希用户。旧记录存在且 ID、邮箱匹配,就沿用它;两者都不存在时,才创建新用户。映射指向的用户缺失时直接停止登录,不能回退成另建一个账号。
图里的 U0 是历史 64 字符 ID 的示意值。新增的是「邮箱指向 U0」这条关系,原来的 U0 和它拥有的数据都保留。
旧会话继续读,新会话按新结构写
数据库里的旧会话和新会话形状不同。下面用 U0 和固定毫秒值示意字段,省略真实令牌及邮箱:
// 旧会话
{ "account": { "id": "U0", "email": "reader@example.invalid" }, "expiresAt": 1800000000000 }
// 新会话
{
"userId": "U0",
"identity": { "provider": "email", "issuer": "sheng-search", "subject": "reader@example.invalid" },
"expiresAt": 1800000000000
}
恢复登录时先读同一张会话表并检查过期时间,再按结构分支:有旧 account 快照就继续返回它;新结构则从 userId 和当前支持的邮箱身份构造 { id, email }。Cookie 名称、HttpOnly、SameSite、Path、HTTPS Secure 和退出时删除服务端会话的规则保持一致。因此,老用户不需要因为结构调整被强制退出。
读取登录状态本身不补写身份映射。补建发生在邮箱验证码验证成功之后,既有会话读请求仍只需要一次存储访问。新会话中的 identity 是登录时的快照,不代表每次读会话都重新验证邮箱,也不能据此宣称后续改绑、封禁或权限变更会自动实时生效。
收藏、缓存和认领进度跟随稳定 ID
收藏继续以 account:<userId> 作为 owner;历史用户仍然得到 U0,就仍然读写 account:U0。浏览器里该用户的缓存与待同步队列继续使用同一账号后缀。匿名集合的认领记录也保留原来的 accountId、游标和 done,不因为补映射重新导入一次。
这还保护了账号收藏的删除标记:已完成的匿名合并不会重新开始,把取消收藏的资料带回来。客户端传来的 ownerId 仍然只用于校验当前身份,服务端从已验证会话选择写入目标,不能让浏览器指定另一个用户进行认领。
这里没有全量搬迁收藏。若以后确实要换用户 ID,才需要逐项迁移这些归属;如果只是增加邮箱之外的登录方式,优先让新身份指向同一个用户。
身份映射怎样放进四次请求
新增一条 Identity 之后,直觉上的实现是先查验证码,再查身份,再查用户,最后逐条写入。每个方法分别访问 TableStore,就会超出当前预算。能合并的是已知完整键、互不依赖的读取;验证码消费与唯一身份占用仍然需要条件写入。
TableStore 的批量读取支持一次读取多个表。验证请求开始时,规范化邮箱、挑战键、邮箱映射键和旧用户键都可以算出来,于是一次 BatchGetRow 读取挑战、映射和旧用户。此时只是预读,验证码尚未校验成功,不能据此创建身份。
后面的顺序由安全条件决定:
- 检查挑战的随机令牌、有效期、剩余次数和验证码摘要,再按 revision 条件消费。并发请求只有条件更新成功的一方能继续。
- 已有映射时,再读它指向的用户;没有映射但旧用户存在时,直接复用批读结果;全新用户才条件创建随机 ID 用户。
- 最后一次
BatchWriteRow条件占用缺失的身份映射,并创建设备会话。映射使用EXPECT_NOT_EXIST,不覆盖其他请求已经占用的关系。 - 检查所有必要行成功后,才把随机会话令牌通过 Cookie 交给浏览器。
| 一次校验请求 | 批读 | 条件消费 | 用户操作 | 最终批写 | 合计 |
|---|---|---|---|---|---|
| 全新用户 | 1 | 1 | 1,创建 | 1 | 4 |
| 已有映射用户 | 1 | 1 | 1,读取 | 1 | 4 |
| 历史用户首次补映射 | 1 | 1 | 0,已读到旧用户 | 1 | 3 |
这里计算的是一次提交验证码的请求。发邮件在另一个发码请求里发生,正常为一次配额预留加一次邮件 HTTP API。账号收藏仍是一次会话读取加一次收藏操作,没有为了迁移额外查询身份表。
实际适配还要核对 SDK 的返回结构:项目固定的 TableStore Node SDK 5.6.6,批读响应按表分组,是二维数组;批写响应则展开为一维数组。存储层把结果还原到原始键的位置,逐行检查失败,避免将「读取失败」误判为「没有注册」。新增 ots:BatchGetRow 权限也只限于现有两张账号表,身份映射放永久表,不能跟着临时会话的 TTL 消失。
这份预算没有靠删除验证码原子消费、关闭限流或绕过来源校验实现。SDK 配置 maxRetries: 0,自定义 fetch 传输也不主动重试;若端点发生重定向,仍会额外占用 ESA 子请求,正式入口需要继续验证。
批写少一次往返,不能当作事务
用户先创建成功,再尝试写入映射,避免新映射指向创建失败的用户。但身份和会话即便在一次批量更新中提交,也可能只有部分行成功。HTTP 200 不是这组业务操作整体成功的证明。
| 实际状态 | 登录接口的处理 |
|---|---|
| 映射成功,会话失败 | 不发 Cookie;重新获取验证码后,沿映射回到同一个用户 |
| 映射条件冲突,会话行成功 | 返回 409,不覆盖获胜映射,也不交付新令牌 |
| 用户已创建,映射未成功 | 停止登录;可能留下未关联用户记录,不在当前请求里追加网络回滚 |
| 网络超时,写入结果未知 | 不报告成功;后续新挑战重新读取权威映射,不能当作已经回滚 |
没有交付给浏览器的随机会话令牌不会出现在错误响应里,对应会话按有效期和 TTL 清理。未关联用户记录可能永久保留,这是当前范围接受的成本;后续清理必须确认没有身份和业务引用,不能仅凭创建时间删除。
证据来自真实存储与最终产物两层
2026-10-24,隔离命名空间的真实 TableStore 验证记录了下列三条路径。这里从原始 trace 只筛出 label 和 fetches,保留操作顺序:
[
{ "label": "legacy-verify", "fetches": ["/BatchGetRow", "/UpdateRow", "/BatchWriteRow"] },
{ "label": "new-verify", "fetches": ["/BatchGetRow", "/UpdateRow", "/UpdateRow", "/BatchWriteRow"] },
{ "label": "returning-verify", "fetches": ["/BatchGetRow", "/UpdateRow", "/GetRow", "/BatchWriteRow"] }
]
同组验证还检查了旧 Cookie 读取原收藏、补映射后保持原 owner、验证码重放拒绝、退出后旧会话失效,以及八个并发条件占用只有一个成功。邮件使用回调替身,未发送真实邮件;全部 16 条隔离测试记录删除后逐条复查,没有读取或改写已有用户记录。
已有映射路径使用隔离夹具重置的受控挑战,目的是重复进入该分支统计请求数;实际重发冷却和真实邮件投递未在这组云测试中验收。
项目的 pnpm check 当时通过 117 项测试,覆盖映射冲突、部分写入恢复、旧会话与匿名认领等分支。最终 ESM bundle 另以固定 OTS 协议响应检查批读、批写、新老用户登录和会话恢复。真实云存储验证与本机最终产物验证互相补充,仍不等于 ESA 生产节点已经验收。
配套工作区保留身份解析、两种会话读取和存储适配的源码、模型回归与原始脱敏轨迹。模型测试不运行完整验证码登录,也不证明真实数据库的原子性;原应用的完整 OTP 与最终 bundle 验证范围单独记在结果中。
请读取 /files/esa-pages-journey-8/kits/identity-migration/ 下的 README.md、MANIFEST.json、AGENT_PROMPT.md 和 FILES.json。先在独立目录运行模型回归和类型检查,核对旧 ID、旧会话、业务 owner 和部分失败边界,再验证目标存储契约;不要把模型示例直接部署为完整登录服务。
发布新写入逻辑后,回退也要认识映射
旧版登录端始终按邮箱派生用户 ID,新版会给新用户生成随机 ID。两者长期并行写入,即使各自都能读 Cookie,也可能为同一个邮箱建立两套归属。因此,认证 API 的写入版本应明确切换,不能只看灰度页面是否正常。
同样,新用户开始采用随机 ID 后,回退其他功能时也要保留身份解析和两种会话的读取能力。若确实要回退整个认证模型,需要先暂停相关写入、准备逐用户映射和业务归属迁移;直接删除身份映射再恢复旧算法,会让新用户登录到另一个 ID。
完整验证工作区的源码
适配器轨迹、HTTP 流程、并发原语与 bundle 验证脚本
默认测试不发真实邮件、不连接云数据库。TableStore 并发原语测试是独立可选命令,会创建并删除临时表;HTTP 对比使用本机协议替身或固定响应,不能作为正式认证后端直接部署。
请读取 /files/esa-pages-journey-8/kits/better-auth-evaluation/ 下的 README.md、MANIFEST.json、AGENT_PROMPT.md 和 FILES.json,按文件索引取得源码。先在独立目录运行本地验证,再按目标部署平台评估;不要直接替换业务登录代码或连接生产表。
HTTP 认证实验的复现说明
运行方式、实验环境与各组数据能证明的范围
HTTP 认证方案的隔离实验
三个实验共用文章和结果索引,各自有独立 package.json。固定 Node 26.12.0、pnpm 12.11.2;只允许本机验证。
运行
进入 copy/postgrest、copy/authjs 或 copy/supabase,先运行 pnpm install --frozen-lockfile --ignore-scripts,再按该目录 README 执行。PostgREST 的两条命令为 pnpm probe:contract、pnpm probe;Auth.js 与 Supabase 还包含 Web 平台 build 及 bundle 检查。
数据能证明什么
- PostgREST:真实 Better Auth handler、真实本机 HTTP、SQLite 协议子集;不是实际 Supabase PostgreSQL 或生产适配器。单请求原子消费下,新用户 9、老用户 8 次;数据库限流增加请求。
- Auth.js:原始发布包和官方 Supabase 适配器,真实 handler、本机 HTTP / SQL 替身;默认数据库会话 5 次,JWT 基础登录 4 次,已有会话再登录增加一次。数字码 hook 不包括完整防猜测策略。
- Supabase Auth:原始 SDK 与固定 HTTP 响应,只数客户端网络,不运行认证服务。成功路径各 1 次;刷新遇到四个 503 后实际发送第 5 次。
- pg-runtime.json:先前独立 ESA 函数的加载对照,只保留状态和不支持的模块;未连接 PostgreSQL。
预算保护器是本地反例,不是 ESA 原生第五次报错。所有示例码、邮箱和 secret 均为测试数据;不要部署为登录后端,不要连接真实用户或发信。阿里云 Supabase 的公网、SMTP、计费和生产能力仍需独立验收。
许可
PostgREST 修复原型保留 incamax 原作者的 MIT 许可及来源提交。依赖许可证见各发布包,工作区只是研究材料,不替代上游正式适配器。
Better Auth 适配器的完整验证记录
调用轨迹、请求计数和断言结果,保留完整 JSON
{
"evaluatedOn": "2026-10-24",
"betterAuth": "1.7.12",
"node": "26.12.0",
"adapterCallTraces": [
{
"options": {
"name": "adapter-defaults",
"joins": false
},
"reports": [
{
"name": "send-first-otp",
"status": 200,
"candidateRequests": 3,
"trace": [
{
"kind": "database",
"operation": "create",
"model": "verification"
},
{
"kind": "database",
"operation": "findOne",
"model": "user",
"join": []
},
{
"kind": "mail",
"operation": "send"
}
],
"hasSession": false,
"nullResult": false
},
{
"name": "register-with-otp",
"status": 200,
"candidateRequests": 9,
"trace": [
{
"kind": "database",
"operation": "findMany",
"model": "verification"
},
{
"kind": "database",
"operation": "findMany",
"model": "verification"
},
{
"kind": "database",
"operation": "deleteMany",
"model": "verification"
},
{
"kind": "transaction"
},
{
"kind": "database",
"operation": "findMany",
"model": "verification"
},
{
"kind": "database",
"operation": "consumeOne",
"model": "verification"
},
{
"kind": "database",
"operation": "deleteMany",
"model": "verification"
},
{
"kind": "database",
"operation": "findOne",
"model": "user",
"join": []
},
{
"kind": "database",
"operation": "create",
"model": "user"
},
{
"kind": "database",
"operation": "create",
"model": "session"
}
],
"hasSession": false,
"nullResult": false
},
{
"name": "read-session",
"status": 200,
"candidateRequests": 1,
"trace": [
{
"kind": "database",
"operation": "findOne",
"model": "session",
"join": [
"user"
]
}
],
"hasSession": true,
"nullResult": false
},
{
"name": "replay-consumed-otp",
"status": 400,
"candidateRequests": 4,
"trace": [
{
"kind": "database",
"operation": "findMany",
"model": "verification"
},
{
"kind": "database",
"operation": "findMany",
"model": "verification"
},
{
"kind": "database",
"operation": "deleteMany",
"model": "verification"
},
{
"kind": "transaction"
},
{
"kind": "database",
"operation": "findMany",
"model": "verification"
}
],
"error": "INVALID_OTP",
"hasSession": false,
"nullResult": false
},
{
"name": "sign-out",
"status": 200,
"candidateRequests": 4,
"trace": [
{
"kind": "database",
"operation": "findOne",
"model": "session",
"join": [
"user"
]
},
{
"kind": "database",
"operation": "findMany",
"model": "session"
},
{
"kind": "database",
"operation": "delete",
"model": "session"
},
{
"kind": "database",
"operation": "findMany",
"model": "account"
}
],
"hasSession": false,
"nullResult": false
},
{
"name": "replay-session-after-sign-out",
"status": 200,
"candidateRequests": 1,
"trace": [
{
"kind": "database",
"operation": "findOne",
"model": "session",
"join": [
"user"
]
}
],
"hasSession": false,
"nullResult": true
},
{
"name": "send-returning-user-otp",
"status": 200,
"candidateRequests": 3,
"trace": [
{
"kind": "database",
"operation": "create",
"model": "verification"
},
{
"kind": "database",
"operation": "findOne",
"model": "user",
"join": []
},
{
"kind": "mail",
"operation": "send"
}
],
"hasSession": false,
"nullResult": false
},
{
"name": "sign-in-returning-user",
"status": 200,
"candidateRequests": 8,
"trace": [
{
"kind": "database",
"operation": "findMany",
"model": "verification"
},
{
"kind": "database",
"operation": "findMany",
"model": "verification"
},
{
"kind": "database",
"operation": "deleteMany",
"model": "verification"
},
{
"kind": "transaction"
},
{
"kind": "database",
"operation": "findMany",
"model": "verification"
},
{
"kind": "database",
"operation": "consumeOne",
"model": "verification"
},
{
"kind": "database",
"operation": "deleteMany",
"model": "verification"
},
{
"kind": "database",
"operation": "findOne",
"model": "user",
"join": []
},
{
"kind": "database",
"operation": "create",
"model": "session"
}
],
"hasSession": false,
"nullResult": false
},
{
"name": "send-otp-for-invalid-attempt",
"status": 200,
"candidateRequests": 3,
"trace": [
{
"kind": "database",
"operation": "create",
"model": "verification"
},
{
"kind": "database",
"operation": "findOne",
"model": "user",
"join": []
},
{
"kind": "mail",
"operation": "send"
}
],
"hasSession": false,
"nullResult": false
},
{
"name": "invalid-otp",
"status": 400,
"candidateRequests": 7,
"trace": [
{
"kind": "database",
"operation": "findMany",
"model": "verification"
},
{
"kind": "database",
"operation": "findMany",
"model": "verification"
},
{
"kind": "database",
"operation": "deleteMany",
"model": "verification"
},
{
"kind": "transaction"
},
{
"kind": "database",
"operation": "findMany",
"model": "verification"
},
{
"kind": "database",
"operation": "consumeOne",
"model": "verification"
},
{
"kind": "database",
"operation": "deleteMany",
"model": "verification"
},
{
"kind": "database",
"operation": "create",
"model": "verification"
}
],
"error": "INVALID_OTP",
"hasSession": false,
"nullResult": false
}
]
},
{
"options": {
"name": "joins-enabled",
"joins": true
},
"reports": [
{
"name": "send-first-otp",
"status": 200,
"candidateRequests": 3,
"trace": [
{
"kind": "database",
"operation": "create",
"model": "verification"
},
{
"kind": "database",
"operation": "findOne",
"model": "user",
"join": []
},
{
"kind": "mail",
"operation": "send"
}
],
"hasSession": false,
"nullResult": false
},
{
"name": "register-with-otp",
"status": 200,
"candidateRequests": 9,
"trace": [
{
"kind": "database",
"operation": "findMany",
"model": "verification"
},
{
"kind": "database",
"operation": "findMany",
"model": "verification"
},
{
"kind": "database",
"operation": "deleteMany",
"model": "verification"
},
{
"kind": "transaction"
},
{
"kind": "database",
"operation": "findMany",
"model": "verification"
},
{
"kind": "database",
"operation": "consumeOne",
"model": "verification"
},
{
"kind": "database",
"operation": "deleteMany",
"model": "verification"
},
{
"kind": "database",
"operation": "findOne",
"model": "user",
"join": []
},
{
"kind": "database",
"operation": "create",
"model": "user"
},
{
"kind": "database",
"operation": "create",
"model": "session"
}
],
"hasSession": false,
"nullResult": false
},
{
"name": "read-session",
"status": 200,
"candidateRequests": 1,
"trace": [
{
"kind": "database",
"operation": "findOne",
"model": "session",
"join": [
"user"
]
}
],
"hasSession": true,
"nullResult": false
},
{
"name": "replay-consumed-otp",
"status": 400,
"candidateRequests": 4,
"trace": [
{
"kind": "database",
"operation": "findMany",
"model": "verification"
},
{
"kind": "database",
"operation": "findMany",
"model": "verification"
},
{
"kind": "database",
"operation": "deleteMany",
"model": "verification"
},
{
"kind": "transaction"
},
{
"kind": "database",
"operation": "findMany",
"model": "verification"
}
],
"error": "INVALID_OTP",
"hasSession": false,
"nullResult": false
},
{
"name": "sign-out",
"status": 200,
"candidateRequests": 4,
"trace": [
{
"kind": "database",
"operation": "findOne",
"model": "session",
"join": [
"user"
]
},
{
"kind": "database",
"operation": "findMany",
"model": "session"
},
{
"kind": "database",
"operation": "delete",
"model": "session"
},
{
"kind": "database",
"operation": "findMany",
"model": "account"
}
],
"hasSession": false,
"nullResult": false
},
{
"name": "replay-session-after-sign-out",
"status": 200,
"candidateRequests": 1,
"trace": [
{
"kind": "database",
"operation": "findOne",
"model": "session",
"join": [
"user"
]
}
],
"hasSession": false,
"nullResult": true
},
{
"name": "send-returning-user-otp",
"status": 200,
"candidateRequests": 3,
"trace": [
{
"kind": "database",
"operation": "create",
"model": "verification"
},
{
"kind": "database",
"operation": "findOne",
"model": "user",
"join": []
},
{
"kind": "mail",
"operation": "send"
}
],
"hasSession": false,
"nullResult": false
},
{
"name": "sign-in-returning-user",
"status": 200,
"candidateRequests": 8,
"trace": [
{
"kind": "database",
"operation": "findMany",
"model": "verification"
},
{
"kind": "database",
"operation": "findMany",
"model": "verification"
},
{
"kind": "database",
"operation": "deleteMany",
"model": "verification"
},
{
"kind": "transaction"
},
{
"kind": "database",
"operation": "findMany",
"model": "verification"
},
{
"kind": "database",
"operation": "consumeOne",
"model": "verification"
},
{
"kind": "database",
"operation": "deleteMany",
"model": "verification"
},
{
"kind": "database",
"operation": "findOne",
"model": "user",
"join": []
},
{
"kind": "database",
"operation": "create",
"model": "session"
}
],
"hasSession": false,
"nullResult": false
},
{
"name": "send-otp-for-invalid-attempt",
"status": 200,
"candidateRequests": 3,
"trace": [
{
"kind": "database",
"operation": "create",
"model": "verification"
},
{
"kind": "database",
"operation": "findOne",
"model": "user",
"join": []
},
{
"kind": "mail",
"operation": "send"
}
],
"hasSession": false,
"nullResult": false
},
{
"name": "invalid-otp",
"status": 400,
"candidateRequests": 7,
"trace": [
{
"kind": "database",
"operation": "findMany",
"model": "verification"
},
{
"kind": "database",
"operation": "findMany",
"model": "verification"
},
{
"kind": "database",
"operation": "deleteMany",
"model": "verification"
},
{
"kind": "transaction"
},
{
"kind": "database",
"operation": "findMany",
"model": "verification"
},
{
"kind": "database",
"operation": "consumeOne",
"model": "verification"
},
{
"kind": "database",
"operation": "deleteMany",
"model": "verification"
},
{
"kind": "database",
"operation": "create",
"model": "verification"
}
],
"error": "INVALID_OTP",
"hasSession": false,
"nullResult": false
}
]
},
{
"options": {
"name": "persistent-rate-limit",
"databaseRateLimit": true
},
"reports": [
{
"name": "send-first-otp",
"status": 200,
"candidateRequests": 5,
"trace": [
{
"kind": "database",
"operation": "findMany",
"model": "rateLimit"
},
{
"kind": "database",
"operation": "create",
"model": "rateLimit"
},
{
"kind": "database",
"operation": "create",
"model": "verification"
},
{
"kind": "database",
"operation": "findOne",
"model": "user",
"join": []
},
{
"kind": "mail",
"operation": "send"
}
],
"hasSession": false,
"nullResult": false
},
{
"name": "register-with-otp",
"status": 200,
"candidateRequests": 11,
"trace": [
{
"kind": "database",
"operation": "findMany",
"model": "rateLimit"
},
{
"kind": "database",
"operation": "create",
"model": "rateLimit"
},
{
"kind": "database",
"operation": "findMany",
"model": "verification"
},
{
"kind": "database",
"operation": "findMany",
"model": "verification"
},
{
"kind": "database",
"operation": "deleteMany",
"model": "verification"
},
{
"kind": "transaction"
},
{
"kind": "database",
"operation": "findMany",
"model": "verification"
},
{
"kind": "database",
"operation": "consumeOne",
"model": "verification"
},
{
"kind": "database",
"operation": "deleteMany",
"model": "verification"
},
{
"kind": "database",
"operation": "findOne",
"model": "user",
"join": []
},
{
"kind": "database",
"operation": "create",
"model": "user"
},
{
"kind": "database",
"operation": "create",
"model": "session"
}
],
"hasSession": false,
"nullResult": false
},
{
"name": "read-session",
"status": 200,
"candidateRequests": 3,
"trace": [
{
"kind": "database",
"operation": "findMany",
"model": "rateLimit"
},
{
"kind": "database",
"operation": "create",
"model": "rateLimit"
},
{
"kind": "database",
"operation": "findOne",
"model": "session",
"join": [
"user"
]
}
],
"hasSession": true,
"nullResult": false
},
{
"name": "replay-consumed-otp",
"status": 400,
"candidateRequests": 6,
"trace": [
{
"kind": "database",
"operation": "findMany",
"model": "rateLimit"
},
{
"kind": "database",
"operation": "incrementOne",
"model": "rateLimit"
},
{
"kind": "database",
"operation": "findMany",
"model": "verification"
},
{
"kind": "database",
"operation": "findMany",
"model": "verification"
},
{
"kind": "database",
"operation": "deleteMany",
"model": "verification"
},
{
"kind": "transaction"
},
{
"kind": "database",
"operation": "findMany",
"model": "verification"
}
],
"error": "INVALID_OTP",
"hasSession": false,
"nullResult": false
},
{
"name": "sign-out",
"status": 200,
"candidateRequests": 6,
"trace": [
{
"kind": "database",
"operation": "findMany",
"model": "rateLimit"
},
{
"kind": "database",
"operation": "create",
"model": "rateLimit"
},
{
"kind": "database",
"operation": "findOne",
"model": "session",
"join": [
"user"
]
},
{
"kind": "database",
"operation": "findMany",
"model": "session"
},
{
"kind": "database",
"operation": "delete",
"model": "session"
},
{
"kind": "database",
"operation": "findMany",
"model": "account"
}
],
"hasSession": false,
"nullResult": false
},
{
"name": "replay-session-after-sign-out",
"status": 200,
"candidateRequests": 3,
"trace": [
{
"kind": "database",
"operation": "findMany",
"model": "rateLimit"
},
{
"kind": "database",
"operation": "incrementOne",
"model": "rateLimit"
},
{
"kind": "database",
"operation": "findOne",
"model": "session",
"join": [
"user"
]
}
],
"hasSession": false,
"nullResult": true
},
{
"name": "send-returning-user-otp",
"status": 200,
"candidateRequests": 5,
"trace": [
{
"kind": "database",
"operation": "findMany",
"model": "rateLimit"
},
{
"kind": "database",
"operation": "incrementOne",
"model": "rateLimit"
},
{
"kind": "database",
"operation": "create",
"model": "verification"
},
{
"kind": "database",
"operation": "findOne",
"model": "user",
"join": []
},
{
"kind": "mail",
"operation": "send"
}
],
"hasSession": false,
"nullResult": false
},
{
"name": "sign-in-returning-user",
"status": 200,
"candidateRequests": 10,
"trace": [
{
"kind": "database",
"operation": "findMany",
"model": "rateLimit"
},
{
"kind": "database",
"operation": "incrementOne",
"model": "rateLimit"
},
{
"kind": "database",
"operation": "findMany",
"model": "verification"
},
{
"kind": "database",
"operation": "findMany",
"model": "verification"
},
{
"kind": "database",
"operation": "deleteMany",
"model": "verification"
},
{
"kind": "transaction"
},
{
"kind": "database",
"operation": "findMany",
"model": "verification"
},
{
"kind": "database",
"operation": "consumeOne",
"model": "verification"
},
{
"kind": "database",
"operation": "deleteMany",
"model": "verification"
},
{
"kind": "database",
"operation": "findOne",
"model": "user",
"join": []
},
{
"kind": "database",
"operation": "create",
"model": "session"
}
],
"hasSession": false,
"nullResult": false
},
{
"name": "send-otp-for-invalid-attempt",
"status": 200,
"candidateRequests": 5,
"trace": [
{
"kind": "database",
"operation": "findMany",
"model": "rateLimit"
},
{
"kind": "database",
"operation": "incrementOne",
"model": "rateLimit"
},
{
"kind": "database",
"operation": "create",
"model": "verification"
},
{
"kind": "database",
"operation": "findOne",
"model": "user",
"join": []
},
{
"kind": "mail",
"operation": "send"
}
],
"hasSession": false,
"nullResult": false
},
{
"name": "invalid-otp",
"status": 400,
"candidateRequests": 9,
"trace": [
{
"kind": "database",
"operation": "findMany",
"model": "rateLimit"
},
{
"kind": "database",
"operation": "incrementOne",
"model": "rateLimit"
},
{
"kind": "database",
"operation": "findMany",
"model": "verification"
},
{
"kind": "database",
"operation": "findMany",
"model": "verification"
},
{
"kind": "database",
"operation": "deleteMany",
"model": "verification"
},
{
"kind": "transaction"
},
{
"kind": "database",
"operation": "findMany",
"model": "verification"
},
{
"kind": "database",
"operation": "consumeOne",
"model": "verification"
},
{
"kind": "database",
"operation": "deleteMany",
"model": "verification"
},
{
"kind": "database",
"operation": "create",
"model": "verification"
}
],
"error": "INVALID_OTP",
"hasSession": false,
"nullResult": false
}
]
},
{
"options": {
"name": "one-operation-one-request-budget",
"limit": 4
},
"reports": [
{
"name": "send-first-otp",
"status": 200,
"candidateRequests": 3,
"trace": [
{
"kind": "database",
"operation": "create",
"model": "verification"
},
{
"kind": "database",
"operation": "findOne",
"model": "user",
"join": []
},
{
"kind": "mail",
"operation": "send"
}
],
"hasSession": false,
"nullResult": false
},
{
"name": "register-with-otp",
"status": 500,
"candidateRequests": 5,
"trace": [
{
"kind": "database",
"operation": "findMany",
"model": "verification"
},
{
"kind": "database",
"operation": "findMany",
"model": "verification"
},
{
"kind": "database",
"operation": "deleteMany",
"model": "verification"
},
{
"kind": "transaction"
},
{
"kind": "database",
"operation": "findMany",
"model": "verification"
},
{
"kind": "database",
"operation": "consumeOne",
"model": "verification"
}
],
"error": "HTTP_500_EMPTY_BODY",
"hasSession": false,
"nullResult": false
}
]
}
],
"sqlite": {
"survivesConnectionReopen": true,
"reports": [
{
"joins": false,
"statements": 50,
"sql": [
"insert into \"verification\" (\"identifier\", \"value\", \"expiresAt\", \"createdAt\", \"updatedAt\", \"id\") values (?, ?, ?, ?, ?, ?) returning *",
"select \"primary\".* from (select * from \"user\" where \"user\".\"email\" = ?) as \"primary\"",
"select \"primary\".* from (select * from \"verification\" where \"verification\".\"identifier\" = ? order by \"createdAt\" desc limit ?) as \"primary\" order by \"createdAt\" desc",
"select \"primary\".* from (select * from \"verification\" where \"verification\".\"expiresAt\" < ? limit ?) as \"primary\"",
"delete from \"verification\" where \"verification\".\"expiresAt\" < ?",
"begin",
"select \"primary\".* from (select * from \"verification\" where \"verification\".\"identifier\" = ? order by \"createdAt\" desc limit ?) as \"primary\" order by \"createdAt\" desc",
"delete from \"verification\" where \"verification\".\"id\" in (select \"verification\".\"id\" from \"verification\" where \"verification\".\"id\" = ? limit ?) and \"verification\".\"id\" = ? returning *",
"delete from \"verification\" where \"verification\".\"identifier\" = ?",
"commit",
"select \"primary\".* from (select * from \"user\" where \"user\".\"email\" = ?) as \"primary\"",
"insert into \"user\" (\"name\", \"email\", \"emailVerified\", \"createdAt\", \"updatedAt\", \"id\") values (?, ?, ?, ?, ?, ?) returning *",
"insert into \"session\" (\"expiresAt\", \"token\", \"createdAt\", \"updatedAt\", \"ipAddress\", \"userAgent\", \"userId\", \"id\") values (?, ?, ?, ?, ?, ?, ?, ?) returning *",
"select \"primary\".* from (select * from \"session\" where \"session\".\"token\" = ?) as \"primary\"",
"select \"primary\".* from (select * from \"user\" where \"user\".\"id\" = ?) as \"primary\"",
"select \"primary\".* from (select * from \"verification\" where \"verification\".\"identifier\" = ? order by \"createdAt\" desc limit ?) as \"primary\" order by \"createdAt\" desc",
"select \"primary\".* from (select * from \"verification\" where \"verification\".\"expiresAt\" < ? limit ?) as \"primary\"",
"delete from \"verification\" where \"verification\".\"expiresAt\" < ?",
"begin",
"select \"primary\".* from (select * from \"verification\" where \"verification\".\"identifier\" = ? order by \"createdAt\" desc limit ?) as \"primary\" order by \"createdAt\" desc",
"commit",
"select \"primary\".* from (select * from \"session\" where \"session\".\"token\" = ?) as \"primary\"",
"select \"primary\".* from (select * from \"user\" where \"user\".\"id\" = ?) as \"primary\"",
"select \"primary\".* from (select * from \"session\" where \"session\".\"token\" = ? limit ?) as \"primary\"",
"delete from \"session\" where \"session\".\"token\" = ?",
"select \"primary\".* from (select * from \"account\" where \"account\".\"userId\" = ? limit ?) as \"primary\"",
"select \"primary\".* from (select * from \"session\" where \"session\".\"token\" = ?) as \"primary\"",
"insert into \"verification\" (\"identifier\", \"value\", \"expiresAt\", \"createdAt\", \"updatedAt\", \"id\") values (?, ?, ?, ?, ?, ?) returning *",
"select \"primary\".* from (select * from \"user\" where \"user\".\"email\" = ?) as \"primary\"",
"select \"primary\".* from (select * from \"verification\" where \"verification\".\"identifier\" = ? order by \"createdAt\" desc limit ?) as \"primary\" order by \"createdAt\" desc",
"select \"primary\".* from (select * from \"verification\" where \"verification\".\"expiresAt\" < ? limit ?) as \"primary\"",
"delete from \"verification\" where \"verification\".\"expiresAt\" < ?",
"begin",
"select \"primary\".* from (select * from \"verification\" where \"verification\".\"identifier\" = ? order by \"createdAt\" desc limit ?) as \"primary\" order by \"createdAt\" desc",
"delete from \"verification\" where \"verification\".\"id\" in (select \"verification\".\"id\" from \"verification\" where \"verification\".\"id\" = ? limit ?) and \"verification\".\"id\" = ? returning *",
"delete from \"verification\" where \"verification\".\"identifier\" = ?",
"commit",
"select \"primary\".* from (select * from \"user\" where \"user\".\"email\" = ?) as \"primary\"",
"insert into \"session\" (\"expiresAt\", \"token\", \"createdAt\", \"updatedAt\", \"ipAddress\", \"userAgent\", \"userId\", \"id\") values (?, ?, ?, ?, ?, ?, ?, ?) returning *",
"insert into \"verification\" (\"identifier\", \"value\", \"expiresAt\", \"createdAt\", \"updatedAt\", \"id\") values (?, ?, ?, ?, ?, ?) returning *",
"select \"primary\".* from (select * from \"user\" where \"user\".\"email\" = ?) as \"primary\"",
"select \"primary\".* from (select * from \"verification\" where \"verification\".\"identifier\" = ? order by \"createdAt\" desc limit ?) as \"primary\" order by \"createdAt\" desc",
"select \"primary\".* from (select * from \"verification\" where \"verification\".\"expiresAt\" < ? limit ?) as \"primary\"",
"delete from \"verification\" where \"verification\".\"expiresAt\" < ?",
"begin",
"select \"primary\".* from (select * from \"verification\" where \"verification\".\"identifier\" = ? order by \"createdAt\" desc limit ?) as \"primary\" order by \"createdAt\" desc",
"delete from \"verification\" where \"verification\".\"id\" in (select \"verification\".\"id\" from \"verification\" where \"verification\".\"id\" = ? limit ?) and \"verification\".\"id\" = ? returning *",
"delete from \"verification\" where \"verification\".\"identifier\" = ?",
"commit",
"insert into \"verification\" (\"identifier\", \"value\", \"expiresAt\", \"createdAt\", \"updatedAt\", \"id\") values (?, ?, ?, ?, ?, ?) returning *"
]
},
{
"joins": true,
"statements": 50,
"sql": [
"insert into \"verification\" (\"identifier\", \"value\", \"expiresAt\", \"createdAt\", \"updatedAt\", \"id\") values (?, ?, ?, ?, ?, ?) returning *",
"select \"primary\".* from (select * from \"user\" where \"user\".\"email\" = ?) as \"primary\"",
"select \"primary\".* from (select * from \"verification\" where \"verification\".\"identifier\" = ? order by \"createdAt\" desc limit ?) as \"primary\" order by \"createdAt\" desc",
"select \"primary\".* from (select * from \"verification\" where \"verification\".\"expiresAt\" < ? limit ?) as \"primary\"",
"delete from \"verification\" where \"verification\".\"expiresAt\" < ?",
"begin",
"select \"primary\".* from (select * from \"verification\" where \"verification\".\"identifier\" = ? order by \"createdAt\" desc limit ?) as \"primary\" order by \"createdAt\" desc",
"delete from \"verification\" where \"verification\".\"id\" in (select \"verification\".\"id\" from \"verification\" where \"verification\".\"id\" = ? limit ?) and \"verification\".\"id\" = ? returning *",
"delete from \"verification\" where \"verification\".\"identifier\" = ?",
"commit",
"select \"primary\".* from (select * from \"user\" where \"user\".\"email\" = ?) as \"primary\"",
"insert into \"user\" (\"name\", \"email\", \"emailVerified\", \"createdAt\", \"updatedAt\", \"id\") values (?, ?, ?, ?, ?, ?) returning *",
"insert into \"session\" (\"expiresAt\", \"token\", \"createdAt\", \"updatedAt\", \"ipAddress\", \"userAgent\", \"userId\", \"id\") values (?, ?, ?, ?, ?, ?, ?, ?) returning *",
"select \"primary\".* from (select * from \"session\" where \"session\".\"token\" = ?) as \"primary\"",
"select \"primary\".* from (select * from \"user\" where \"user\".\"id\" = ?) as \"primary\"",
"select \"primary\".* from (select * from \"verification\" where \"verification\".\"identifier\" = ? order by \"createdAt\" desc limit ?) as \"primary\" order by \"createdAt\" desc",
"select \"primary\".* from (select * from \"verification\" where \"verification\".\"expiresAt\" < ? limit ?) as \"primary\"",
"delete from \"verification\" where \"verification\".\"expiresAt\" < ?",
"begin",
"select \"primary\".* from (select * from \"verification\" where \"verification\".\"identifier\" = ? order by \"createdAt\" desc limit ?) as \"primary\" order by \"createdAt\" desc",
"commit",
"select \"primary\".* from (select * from \"session\" where \"session\".\"token\" = ?) as \"primary\"",
"select \"primary\".* from (select * from \"user\" where \"user\".\"id\" = ?) as \"primary\"",
"select \"primary\".* from (select * from \"session\" where \"session\".\"token\" = ? limit ?) as \"primary\"",
"delete from \"session\" where \"session\".\"token\" = ?",
"select \"primary\".* from (select * from \"account\" where \"account\".\"userId\" = ? limit ?) as \"primary\"",
"select \"primary\".* from (select * from \"session\" where \"session\".\"token\" = ?) as \"primary\"",
"insert into \"verification\" (\"identifier\", \"value\", \"expiresAt\", \"createdAt\", \"updatedAt\", \"id\") values (?, ?, ?, ?, ?, ?) returning *",
"select \"primary\".* from (select * from \"user\" where \"user\".\"email\" = ?) as \"primary\"",
"select \"primary\".* from (select * from \"verification\" where \"verification\".\"identifier\" = ? order by \"createdAt\" desc limit ?) as \"primary\" order by \"createdAt\" desc",
"select \"primary\".* from (select * from \"verification\" where \"verification\".\"expiresAt\" < ? limit ?) as \"primary\"",
"delete from \"verification\" where \"verification\".\"expiresAt\" < ?",
"begin",
"select \"primary\".* from (select * from \"verification\" where \"verification\".\"identifier\" = ? order by \"createdAt\" desc limit ?) as \"primary\" order by \"createdAt\" desc",
"delete from \"verification\" where \"verification\".\"id\" in (select \"verification\".\"id\" from \"verification\" where \"verification\".\"id\" = ? limit ?) and \"verification\".\"id\" = ? returning *",
"delete from \"verification\" where \"verification\".\"identifier\" = ?",
"commit",
"select \"primary\".* from (select * from \"user\" where \"user\".\"email\" = ?) as \"primary\"",
"insert into \"session\" (\"expiresAt\", \"token\", \"createdAt\", \"updatedAt\", \"ipAddress\", \"userAgent\", \"userId\", \"id\") values (?, ?, ?, ?, ?, ?, ?, ?) returning *",
"insert into \"verification\" (\"identifier\", \"value\", \"expiresAt\", \"createdAt\", \"updatedAt\", \"id\") values (?, ?, ?, ?, ?, ?) returning *",
"select \"primary\".* from (select * from \"user\" where \"user\".\"email\" = ?) as \"primary\"",
"select \"primary\".* from (select * from \"verification\" where \"verification\".\"identifier\" = ? order by \"createdAt\" desc limit ?) as \"primary\" order by \"createdAt\" desc",
"select \"primary\".* from (select * from \"verification\" where \"verification\".\"expiresAt\" < ? limit ?) as \"primary\"",
"delete from \"verification\" where \"verification\".\"expiresAt\" < ?",
"begin",
"select \"primary\".* from (select * from \"verification\" where \"verification\".\"identifier\" = ? order by \"createdAt\" desc limit ?) as \"primary\" order by \"createdAt\" desc",
"delete from \"verification\" where \"verification\".\"id\" in (select \"verification\".\"id\" from \"verification\" where \"verification\".\"id\" = ? limit ?) and \"verification\".\"id\" = ? returning *",
"delete from \"verification\" where \"verification\".\"identifier\" = ?",
"commit",
"insert into \"verification\" (\"identifier\", \"value\", \"expiresAt\", \"createdAt\", \"updatedAt\", \"id\") values (?, ?, ?, ?, ?, ?) returning *"
]
}
]
},
"proxy": {
"transport": "real-local-http",
"networkRequests": 10,
"reports": [
{
"name": "send-first-otp",
"status": 200,
"candidateRequests": 1,
"trace": [
{
"kind": "http",
"operation": "proxy-to-auth-service"
}
],
"hasSession": false,
"nullResult": false
},
{
"name": "register-with-otp",
"status": 200,
"candidateRequests": 1,
"trace": [
{
"kind": "http",
"operation": "proxy-to-auth-service"
}
],
"hasSession": false,
"nullResult": false
},
{
"name": "read-session",
"status": 200,
"candidateRequests": 1,
"trace": [
{
"kind": "http",
"operation": "proxy-to-auth-service"
}
],
"hasSession": true,
"nullResult": false
},
{
"name": "replay-consumed-otp",
"status": 400,
"candidateRequests": 1,
"trace": [
{
"kind": "http",
"operation": "proxy-to-auth-service"
}
],
"error": "INVALID_OTP",
"hasSession": false,
"nullResult": false
},
{
"name": "sign-out",
"status": 200,
"candidateRequests": 1,
"trace": [
{
"kind": "http",
"operation": "proxy-to-auth-service"
}
],
"hasSession": false,
"nullResult": false
},
{
"name": "replay-session-after-sign-out",
"status": 200,
"candidateRequests": 1,
"trace": [
{
"kind": "http",
"operation": "proxy-to-auth-service"
}
],
"hasSession": false,
"nullResult": true
},
{
"name": "send-returning-user-otp",
"status": 200,
"candidateRequests": 1,
"trace": [
{
"kind": "http",
"operation": "proxy-to-auth-service"
}
],
"hasSession": false,
"nullResult": false
},
{
"name": "sign-in-returning-user",
"status": 200,
"candidateRequests": 1,
"trace": [
{
"kind": "http",
"operation": "proxy-to-auth-service"
}
],
"hasSession": false,
"nullResult": false
},
{
"name": "send-otp-for-invalid-attempt",
"status": 200,
"candidateRequests": 1,
"trace": [
{
"kind": "http",
"operation": "proxy-to-auth-service"
}
],
"hasSession": false,
"nullResult": false
},
{
"name": "invalid-otp",
"status": 400,
"candidateRequests": 1,
"trace": [
{
"kind": "http",
"operation": "proxy-to-auth-service"
}
],
"error": "INVALID_OTP",
"hasSession": false,
"nullResult": false
}
]
},
"cloud": {
"entry": "temporary-default-debug-domain",
"reports": [
{
"path": "/health",
"status": 200,
"result": {
"ok": true,
"version": "1.7.12"
}
},
{
"path": "/",
"status": 200,
"reports": [
{
"name": "send-first-otp",
"status": 200,
"candidateRequests": 3,
"trace": [
{
"kind": "database",
"operation": "create",
"model": "verification"
},
{
"kind": "database",
"operation": "findOne",
"model": "user",
"join": []
},
{
"kind": "mail",
"operation": "send"
}
],
"hasSession": false,
"nullResult": false
},
{
"name": "register-with-otp",
"status": 200,
"candidateRequests": 9,
"trace": [
{
"kind": "database",
"operation": "findMany",
"model": "verification"
},
{
"kind": "database",
"operation": "findMany",
"model": "verification"
},
{
"kind": "database",
"operation": "deleteMany",
"model": "verification"
},
{
"kind": "transaction"
},
{
"kind": "database",
"operation": "findMany",
"model": "verification"
},
{
"kind": "database",
"operation": "consumeOne",
"model": "verification"
},
{
"kind": "database",
"operation": "deleteMany",
"model": "verification"
},
{
"kind": "database",
"operation": "findOne",
"model": "user",
"join": []
},
{
"kind": "database",
"operation": "create",
"model": "user"
},
{
"kind": "database",
"operation": "create",
"model": "session"
}
],
"hasSession": false,
"nullResult": false
},
{
"name": "read-session",
"status": 200,
"candidateRequests": 1,
"trace": [
{
"kind": "database",
"operation": "findOne",
"model": "session",
"join": [
"user"
]
}
],
"hasSession": true,
"nullResult": false
},
{
"name": "replay-consumed-otp",
"status": 400,
"candidateRequests": 4,
"trace": [
{
"kind": "database",
"operation": "findMany",
"model": "verification"
},
{
"kind": "database",
"operation": "findMany",
"model": "verification"
},
{
"kind": "database",
"operation": "deleteMany",
"model": "verification"
},
{
"kind": "transaction"
},
{
"kind": "database",
"operation": "findMany",
"model": "verification"
}
],
"error": "INVALID_OTP",
"hasSession": false,
"nullResult": false
},
{
"name": "sign-out",
"status": 200,
"candidateRequests": 4,
"trace": [
{
"kind": "database",
"operation": "findOne",
"model": "session",
"join": [
"user"
]
},
{
"kind": "database",
"operation": "findMany",
"model": "session"
},
{
"kind": "database",
"operation": "delete",
"model": "session"
},
{
"kind": "database",
"operation": "findMany",
"model": "account"
}
],
"hasSession": false,
"nullResult": false
},
{
"name": "replay-session-after-sign-out",
"status": 200,
"candidateRequests": 1,
"trace": [
{
"kind": "database",
"operation": "findOne",
"model": "session",
"join": [
"user"
]
}
],
"hasSession": false,
"nullResult": true
},
{
"name": "send-returning-user-otp",
"status": 200,
"candidateRequests": 3,
"trace": [
{
"kind": "database",
"operation": "create",
"model": "verification"
},
{
"kind": "database",
"operation": "findOne",
"model": "user",
"join": []
},
{
"kind": "mail",
"operation": "send"
}
],
"hasSession": false,
"nullResult": false
},
{
"name": "sign-in-returning-user",
"status": 200,
"candidateRequests": 8,
"trace": [
{
"kind": "database",
"operation": "findMany",
"model": "verification"
},
{
"kind": "database",
"operation": "findMany",
"model": "verification"
},
{
"kind": "database",
"operation": "deleteMany",
"model": "verification"
},
{
"kind": "transaction"
},
{
"kind": "database",
"operation": "findMany",
"model": "verification"
},
{
"kind": "database",
"operation": "consumeOne",
"model": "verification"
},
{
"kind": "database",
"operation": "deleteMany",
"model": "verification"
},
{
"kind": "database",
"operation": "findOne",
"model": "user",
"join": []
},
{
"kind": "database",
"operation": "create",
"model": "session"
}
],
"hasSession": false,
"nullResult": false
},
{
"name": "send-otp-for-invalid-attempt",
"status": 200,
"candidateRequests": 3,
"trace": [
{
"kind": "database",
"operation": "create",
"model": "verification"
},
{
"kind": "database",
"operation": "findOne",
"model": "user",
"join": []
},
{
"kind": "mail",
"operation": "send"
}
],
"hasSession": false,
"nullResult": false
},
{
"name": "invalid-otp",
"status": 400,
"candidateRequests": 7,
"trace": [
{
"kind": "database",
"operation": "findMany",
"model": "verification"
},
{
"kind": "database",
"operation": "findMany",
"model": "verification"
},
{
"kind": "database",
"operation": "deleteMany",
"model": "verification"
},
{
"kind": "transaction"
},
{
"kind": "database",
"operation": "findMany",
"model": "verification"
},
{
"kind": "database",
"operation": "consumeOne",
"model": "verification"
},
{
"kind": "database",
"operation": "deleteMany",
"model": "verification"
},
{
"kind": "database",
"operation": "create",
"model": "verification"
}
],
"error": "INVALID_OTP",
"hasSession": false,
"nullResult": false
}
]
},
{
"path": "/budget?count=4",
"status": 200,
"result": {
"count": 4,
"responses": [
{
"status": 200
},
{
"status": 200
},
{
"status": 200
},
{
"status": 200
}
]
}
},
{
"path": "/budget?count=5",
"status": 200,
"result": {
"count": 5,
"responses": [
{
"status": 200
},
{
"status": 200
},
{
"status": 200
},
{
"status": 200
},
{
"status": 200
}
]
}
},
{
"path": "/budget?count=6",
"status": 200,
"result": {
"count": 6,
"responses": [
{
"status": 200
},
{
"status": 200
},
{
"status": 200
},
{
"status": 200
},
{
"status": 200
},
{
"status": 200
}
]
}
}
]
},
"tableStore": {
"contenders": 16,
"tests": [
{
"name": "concurrent-create-unique-key",
"successful": 1,
"rejected": 15,
"apiRequests": 16
},
{
"name": "read-and-conditional-consume",
"successful": 1,
"rejected": 15,
"apiRequests": 32
}
],
"cleanedUp": true
},
"temporaryResourcesDeleted": true,
"notTested": [
"production custom-domain quotas",
"cloud auth-service load",
"backup restoration",
"real mail delivery",
"additional login providers"
]
}