视觉按最新原型,业务按已确认PRD。现有代码为满足设计而改造。执行见按设计实施与任务卡;当前代码与验证范围见消费者功能页。
A03 账号生命周期后端限定复审
复审日期:2026-10-03。结论:本报告审查的第一段版本未发现残留的实证 P1/P2。 两个 bulk P2 已通过独立前后复现闭环;单条快照失败另外完成两组实际补证,确认身份删除未执行。后续新增的在途写入 P2 不属于本子 agent 的通过结论;根代理已另行完成 7 组实际独立复验,见追加报告。
本次范围仅为 短剧库/services/platform/src/ 下的 auth.mjs、runtime.mjs、state.mjs、platform.mjs,以及相关 BetterAuth 1.7.7 安装源和生命周期测试。业务文件由作者修改;复审者仅保存快照、读代码、编写并运行隔离复现。未审核并行 RN 新页面,未操作线上账号、发送真实邮件、安装依赖或进行原生编译、发布。邮箱更换与账号删除的完整产品流程不在本段通过结论内。
验证来源
| 来源 | 内容 | 结果 |
|---|---|---|
| 作者验证,原候选 | 生命周期测试 | 12/12,通过;这一结果没有覆盖超过 100 条会话和 SDK 批量快照读失败 |
| 作者验证,最终冻结版本 | tests/lifecycle.test.mjs |
17/17,通过,7.179596 秒,进程退出码 0 |
| 作者验证,最终冻结版本 | 全平台测试,18 个顶层组 | 127/127,通过,12.952760 秒,进程退出码 0 |
| 作者验证,最终冻结版本 | 四个业务文件及新增测试的语法检查 | 全部退出码 0 |
| 本复审者独立验证,原候选 | 3 组真实 PostgreSQL + 官方 HTTP + 连接池 max=1 复现 | 复现下述两个 P2,脚本退出码 0 |
| 本复审者独立验证,最终冻结版本 | 同样 3 组检查,验证修复后的结果 | 3/3 闭环通过,脚本退出码 0 |
| 本复审者独立验证,最终冻结版本 | 2 组单条快照故障,各包含原 cookie 确认与显式重试 | 2/2 通过,脚本退出码 0 |
作者测试数量与耗时来自作者提供的完成结果,本复审者未重跑其全平台测试。独立复现另行构造场景,仅复用项目的隔离 PostgreSQL 启动工具;未调用作者的生命周期测试模块。每次运行使用新建的 helper-owned Docker PostgreSQL,结束后关闭 HTTP、连接池并移除该容器。
独立证据位于 /Users/alienmu/Documents/Codex/2026-10-02/bang/work/account-lifecycle-review/:
reproduce.mjs:独立复现及修复断言。- 根目录四个
.mjs:原 12 项候选快照;fixed/中四个.mjs:最终修复快照。只改模块导入与迁移文件引用的绝对路径以加载原安装包,业务逻辑原样保留。 observed.jsonl:原候选的 3 组观测结果。observed-fixed.jsonl:修复版本的 3 组观测结果。observed-single.jsonl:2 组单条故障及各自重试,共 4 行观测结果。
复现先通过官方 OTP HTTP 在隔离环境注册、登录,再通过实际框架 internalAdapter 创建总计 101 条真实会话。测试批准仅由受信域 fixture 对实际 BA session ID 构造 reviewed_installation,不代表存在生产 App 安装证明发行器。输出只记录计数与状态,不输出 OTP、cookie 或会话 token。
已关闭的发现
P2:超过 100 条会话时,官方批量撤销覆盖不完整。
BetterAuth bulk hook 快照 未指定 limit;core adapter 默认 100。Kysely deleteMany 删除全部匹配行。revokeOtherSessions 的 internal listSessions 也使用这一默认限制。
独立复现用官方列表确定遗漏行,为该真实会话建立批准和 pending reservation,再请求批量撤销。结果如下:
| 官方请求 | 原候选 HTTP/身份结果 | 原候选批准与预留 | 修复版本结果 |
|---|---|---|---|
revoke-sessions,101 条会话 |
200,status:true;身份会话剩 0 |
遗漏批准未撤销,预留仍 pending | 列表返回 101;身份剩 0;批准已撤销,预留 released |
revoke-other-sessions,101 条会话 |
200,status:true;身份会话剩 2,应仅保留当前 1 条 |
遗漏批准未撤销,预留仍 pending | 身份仅剩当前 1 条;批准已撤销,预留 released |
修复采用 官方 defaultFindManyLimit 配置 Number.MAX_SAFE_INTEGER,避免另设任意固定采样上限。作者另外验证了 105 条 Others 和补充 105 条 All 场景。独立前后观测已确认关闭。
P2:SDK 批量快照读失败被吞掉,可能跳过 before callback 后继续删除身份会话。
with-hooks.mjs 对快照 findMany 的异常空 catch,随后仍执行 deleteMany。原 runtime 的批准 lookup 故障测试不能证明这一框架层故障安全。
独立 fixture 对 SDK 的实际批量快照 SELECT 注入一次错误,并断言命中次数为 1。原候选仍返回 200/status:true,身份行全部删除,但批准未撤销、reservation 仍 pending。最终版本同一注入下身份行删除,批准已撤销、reservation 为 released。
修复在 官方 bulk 请求 before 中先通过官方 getSession() 获取当前 owner/session;runtime 从现有批准表与官方 session 表 JOIN 取得实际集合并先撤销 E00 域批准。Others 排除当前会话并匹配有效会话;All 覆盖该 owner。owner 来源不读取客户端 body。单条删除 hook 继续保留。独立故障观测已确认关闭。
单条快照故障的范围纠正
单删快照也有空 catch,但当前 PostgreSQL、无 secondaryStorage 路径的 实际 delete 条件 要求 entityToDelete 存在。快照失败会令该值为 null,故官方身份行也不删除;不能将 bulk 的“身份删除、域漏撤”结果套到 single。
独立注入实际 token/limit=1 的 SDK 快照 SELECT 各一次,并分别验证当前会话 sign-out 与同 owner 的 target revoke-session:
| 请求与故障 | HTTP 返回 | 目标身份与原 cookie | 批准与预留 | 显式重试后 |
|---|---|---|---|---|
| sign-out,快照读取失败 | 200,success:true |
目标 BA 行仍 1;原 cookie GET 200 且账号有效 | 批准未撤,预留 pending | BA 行 0;批准已撤;预留 released |
| revoke-session,快照读取失败 | 200,status:true |
目标 BA 行仍 1;原 target cookie GET 200 且账号有效 | 批准未撤,预留 pending | BA 行 0;批准已撤;预留 released |
两组均断言错误注入次数为 1。它们确认 HTTP 200 仍不能证明删除完成,同时原 single hook 没有造成跨域失配;未要求新增 single before 或业务重复读取。App 必须根据后续官方 get/list 的实际目标状态确认结果;本报告未代替根代理的 RN 审核。
冻结代码核对
- runtime.mjs:单删仅查已有批准,无批准的 Web 会话跳过;有批准时校验 owner,并等待 E00 撤销。普通查询释放连接后才进入域事务。四条普通 sign-out/revoke 路径没有 BA 外层事务;作者及独立 max=1 运行未出现嵌套连接死锁。这一结论不扩展到任意自行包裹的框架内部事务调用。
- auth.mjs:
session.create.before使用实际 session.userId;session.update.before使用框架当前 session 上下文。SDK get-session 在续期前设置该上下文,字段与时序对应。 - auth.mjs:身份读写前直接调用官方 route,强制
disableCookieCache和disableRefresh,再检查终态;get-session after 再拒绝迟到结果。直接函数调用明确 跳过全局 hooks,因此不会递归。显式清单覆盖 get-session、update-user、update-session、change-email 和两个 email-OTP change 路径;不能将该清单描述为自动覆盖所有官方路由。 - state.mjs:保持原 catalog/history 表锁在前,再创建或锁现有 E00 owner 行,验证 kind、terminal、merged 状态,然后锁 consumer_state_owner。following、begin、report、history delete 共用这一入口。缺行先 insert-on-conflict,避免空 SELECT 无行锁的竞态;缺 E00 表抛错,不降级为活跃账号。没有新增 store/hash。作者实际等锁后终态用例验证 report 返回 403,sequence/position 不落新值。
- platform.mjs:个人命令在身份解析后检查终态;错误映射 兼容 APIError.statusCode,个人 API 保留 403,内部身份桥接的 account_terminal 转为 200/
user:null。
必须保留的框架语义与范围
官方 sign-out 吞掉会话删除异常,清客户端 cookie 并继续返回成功。因此 sign-out 200 不能证明服务端会话已撤销;现作者故障测试已明确这一点。运行时撤销 callback 抛错时,revokeSession/revokeSessions 返回 500,revokeOtherSessions 的错误直接传播;SDK 自己吞掉的单条快照错误则呈现上表的 200/未执行。未发现 get-session after 抛 APIError 被误包装成 HTTP 200 的问题。
E00 撤销事务与官方身份删除不是一个跨框架原子事务。域撤销先完成、后续身份删除失败,或批量操作中途失败时,可能已经撤销部分批准;这种顺序只收紧许可,不能宣称全有或全无,也不能依据名义响应盲目确认所有目标。新 bulk before 关闭的是已复现的默认截断和快照故障结果。
全量会话列表现在会读取该用户实际集合,本次未做大规模会话资源压力测试。max=1 证据覆盖普通官方操作,不保证任意新增外层事务的资源行为。复现资源均属于隔离 fixture,结束后清理。
本结论不证明生产安装 attestation/reviewed-installation 的真实发行器、离线许可、原生缓存立即撤销、配额策略或管理员流程;未启用账号删除。RN 页面、退出后的本机清理与未知结果确认由根代理另行审核,不属于此后端报告的通过范围。