# A03 账号生命周期：服务端基础交接

本段接好成熟框架会话撤销与现有业务许可，并阻止已停用账号继续写入个人记录。未开放账号注销、未新增终态存储、未修改额度或邀请领域，也未发布到生产。

本报告为此前会话生命周期基础段。后续注销后端进展见[注销后端与清理](dramivio-account-deletion-implementation.md)；本段测试数量保留历史范围。

## 实现

| 文件 | 本段变化 |
| --- | --- |
| `services/platform/src/auth.mjs` | Better Auth 1.7.7 官方 session.delete.before 导入回调；官方批量撤销 before 回调；会话创建、续期及身份读写检查已有 owner；保留官方 OTP、Cookie、会话接口 |
| `services/platform/src/runtime.mjs` | 查询既有安装批准，再调用已有 revokeInstallationSession 事务；批量操作按官方会话 owner 查询身份与批准的交集；普通查询释放连接后才启动业务事务 |
| `services/platform/src/state.mjs` | 复用 entitlement_owner；H00/F00 写事务按原有表锁 → owner → 官方身份 session FOR SHARE → consumer_state_owner；等锁后判断终态及真实会话有效性；个人状态读取也检查 owner |
| `services/platform/src/platform.mjs` | 已停用个人 API 返回 403；内部身份桥返回 user:null；四个个人写操作显式传入官方 session.id，客户端 body 不能替换身份 |
| `services/platform/tests/lifecycle.test.mjs` | 真实隔离 PostgreSQL、官方 HTTP、单连接数据库、超过 100 个会话、故障与终态竞态回归 |
| `services/platform/tests/state.test.mjs`、`platform.test.mjs` | 显式执行所需 E00 migration；没有捕获缺表错误并假装账号活跃 |

初始化顺序明确为 Auth → Catalog → E00 → State → Referrals → Search。独立调用 State 的项目也必须完成 E00 migration；缺表会真实失败。刚完成邮箱验证、尚无业务 owner 行的用户可以正常开始第一项业务操作，写入时创建并锁住既有 owner。

## 已确认行为

- 无安装批准的普通 Web 会话可以正常退出。
- 退出、撤销指定会话、撤销其他会话、撤销全部会话，会撤销已有安装批准并释放其 pending 播放预留；旧 Cookie 不能继续提交进度。
- 停用账号不能创建新官方会话，不能读取或续期旧会话，不能通过身份桥重新登录业务层，不能将游客额度并入旧账号。
- 一个 report 即使已经通过鉴权，只要等 owner 行锁时发生停用，事务仍返回 account_terminal；sequence、位置、观看区间不会写入。
- 账号仍活跃但本设备会话已撤销或过期时，等锁中的 report、begin、following/update、history/delete 也会返回 login_required 并回滚。身份 ID 来自官方 get-session，不能用请求 body 中的新会话 ID 替换。
- 写事务持有真实身份行的 FOR SHARE 锁，官方身份 DELETE 必须等该事务完成；因此不会在身份已删除后再提交旧写入。这里校验数据库既有行，没有新增会话存储或协议。
- 正常账号仍使用官方续期。读取 active 快照后才发生停用的迟到续期，也由 session.update.before 阻止；get-session.after 还会拒绝迟到的身份结果。
- 不修改用户原注册时间，也不重置额度、邀请关系或旧业务归属。

## 复审关闭的框架边界

Better Auth 默认 findMany 最多 100 行，而批量删除可以删除全部身份行。已使用官方 `advanced.database.defaultFindManyLimit = Number.MAX_SAFE_INTEGER`，没有换成另一个任意业务上限。该选项影响框架未显式设置 limit 的 findMany；本产品开放的会话操作按当前用户查询全部会话元数据。

框架还会吞掉批量删除前快照的读取异常。官方批量路由 before 现在先以官方 get-session 的 disableRefresh/disableCookieCache 读取当前身份，再查询既有批准与身份行的交集并撤销。此步骤失败即停止身份批量删除。单条 session.delete.before 仍保留；已撤销批准直接跳过，避免重复业务事务。

官方 sign-out 本身会吞掉数据库删除错误、清除客户端 Cookie，并返回 200。保留该框架行为；200 不能证明服务端会话已撤销。实际故障测试确认原 Cookie 仍可能有效，客户端必须使用既有的一次旧 Cookie 对账，并将无法确认的结果展示为未确认。单条与批量 revoke 的失败不能当作已完成。

根代理追加了一个具体竞态调查：已鉴权的 HTTP report 等 owner 锁期间，官方 Web 会话被真正删除，旧实现仍返回 200，并写入 sequence=1、position=18。已在同一业务事务内用官方会话行关闭这一 P2；没有以入口复查代替事务检查。普通直接导入的 State 调用可以不提供 authSessionId，因为它是受信服务组合，不能把这种调用方式当作公共认证入口。

## 验证

- A03 定向：22/22，真实 PostgreSQL 18 RAM 容器与随机 loopback 端口，7.664 秒。
- 单连接 pool 的官方退出、指定/其他/全部撤销真实执行成功，没有嵌套占用连接。
- 105 个其他会话 + 当前会话：官方列表返回 106，撤销其他后仅剩当前；再次建立 105 个会话后撤销全部，身份行、未撤批准、pending 预留均为 0。
- 实际 SDK 批量快照 SELECT 单次失败：域批准仍已先撤销，预留释放。
- 域撤销事务失败：官方批量请求失败，身份行保留，批准及预留未被伪报为成功；恢复数据库后可使用原会话重试。
- 单条退出、指定会话撤销的快照 SELECT 失败：框架没有删除身份行，原 Cookie / 官方列表仍可确认会话存在，批准及 pending 也保留；显式重试后真正撤销。这个行为与批量删除不同，没有为它添加重复的单条 before 回调。
- 新增撤销竞态检查覆盖四种 HTTP 写操作、已鉴权后过期、缺失及错 owner 会话、客户端替换会话 ID 无效。真实 pg_stat_activity 确认 Auth DELETE 会等待写事务的会话行共享锁；受信直接域调用仍兼容。
- 作者全平台累计测试 133/133，13.743 秒，19 个顶层组，包含根代理的真实官方双邮箱验证测试 `email-change.test.mjs`。此前批量与单条故障的独立证据见 [第一段独立复审报告](dramivio-account-lifecycle-review.md)。追加会话撤销竞态由根代理另写隔离脚本完成对照复现与当前版本 7 组独立验证，见[在途写入追加复验](dramivio-account-session-write-fence.md)。本段与第一段子 agent 证据分别记录。

## 范围

批准记录及来源能力由隔离测试的可信夹具创建。正式 App 证明发行者与真实受控媒体许可尚未接入，不宣称这里已经发放 App 50 集许可、撤销外部供应商已签发的视频令牌，或实现完整离线库。

读取全量会话及逐条撤销批准有对应元数据与事务成本；本段验证了 106 个会话，没有完成大规模压力测试。身份删除与业务批准撤销沿用两个成熟事务，失败可先收紧部分许可，不能宣称跨框架原子提交；并发新证明发行必须在后续真实发行者接入时验证。

账号注销按钮、永久删除、双邮箱页面、下载许可与前端本机清理不属于本段；由后续独立实施与验收接入。本段不启用 Better Auth deleteUser。
