# A03 Web 注销与 Payload 清理独立只读复审

复审时间：2026-10-03（Asia/Shanghai）。独立 Agent：gpt-5.6-luna；reasoning_effort=max。root 负责业务实现和修复，复审 Agent 仅读候选代码。  
仓库：`短剧库`  
范围：`internal/webui/consumer-deletion-api.js`、`consumer-deletion.js`、`consumer-auth.js`，Payload 清理任务，`services/platform/src/platform.mjs` 及其本机桥接边界。

本轮只读。没有修改仓库源码、删除设备或数据库文件、连接真实数据库、启动生产 API、发送 SMTP，也没有把现有测试通过当作本轮证据。仓库及上级有效范围内没有找到项目 `AGENTS.md`；发现的同级/依赖目录规则未套用到本仓库。

## 结论

### 修复前 P2（已关闭）：恢复中的旧注销记录可能在启动竞态下导致当前账号反复刷新

这是当前代码可达的 Web 账号隔离问题，已实际复现的影响是当前账号前端身份失效与反复刷新；没有证据表明它会把 B 的数据提交给 A 的注销操作。

证据链：

1. `internal/webui/main.js:34` 先调用 `app.shell.init()`，之后 `main.js:41-43` 才 `await initializeViewer()`，最后 `main.js:61` 才进入 `app.shell.access()`。`internal/webui/consumer-shell.js:102-108` 在 `init()` 内先初始化 auth，再初始化 account deletion。
2. `internal/webui/consumer-deletion.js:169` 在 deletion 初始化时直接读取 `sessionStorage` 的恢复记录并 `open()`。恢复记录分支 `consumer-deletion.js:146-155` 不要求当前 auth 已完成，也不要求 `app.auth.bridged()`；只有当 `app.auth.user()` 已有值且用户不同才丢弃记录（151 行）。
3. 启动阶段 `consumer-auth.js:11-12` 的 `user()` 仍可能因 `enabled=false` 返回空值。此时 `consumer-deletion.js:8-9` 的 `current()` 把“没有 auth user”视为允许继续。
4. 恢复首先调用只带随机 `operationId` 的状态查询；Go 层 `internal/app/consumer_account.go:17-23` 明确允许注销后查询状态。若旧操作返回 `deleting/deleted`，`consumer-deletion.js:118-123` 会调用 `app.auth.expire()`。`consumer-auth.js:26` 的 `expire()` 把已经初始化的 `app.viewer.consumerUserId` 也算作 `hadIdentity`，继而 `reloadIdentity()`。

复现前提是：标签页残留 A 的合法 `dramivio.accountDeletion`、浏览器随后已有 B 的 viewer/session，且状态响应在 auth 的首次刷新前返回。修复前独立顺序检查确认了上述四个条件的代码顺序；修复后的真实 Chrome 复验见“修复后关闭证据”。root 实际复现 B 前端反复刷新；未观察到服务端注销 B 的 cookie 会话，A 的本地进度队列按 A 的 user id 被清理。服务端状态接口没有接受客户端 user id，故没有观察到 B 被删除的路径。

修复前最小方向是：恢复状态可在注销后继续查询，但必须等待一次权威 auth/viewer 对齐，或在终态 `expire()` 前确认当前 auth/viewer 为空或同属恢复记录 owner；当前版本已落实对应 owner/session 门禁。

### 未确认部署条件（不计入当前 P2 gate）：后台 worker 对 `localhost` 别名和直接监听缺少同一层的强制边界

正常文档接法是安全的：`services/platform/src/runtime.mjs:65-72` 的 `startPlatform()` 只允许 `127.0.0.1`/`::1`；`internal/app/consumer_platform.go:53-67` 也只接受解析为回环 IP 的地址；README 明确要求不要公开 `/internal`。

但 `createPlatformServer()` 本身（`services/platform/src/platform.mjs:63-74`）只校验 `x-dramivio-bridge`，没有监听地址约束；调用方若绕过 `startPlatform()` 直接监听外部地址，bridge token 就是 `/internal/consumer/account/delete/process` 的唯一门槛。该路由虽然不接收 owner/id、只接收空 JSON（107-113 行），却能处理所有待清理账号。

此外 `services/admin/src/deletion-jobs.mjs:5-8` 接受 `http://localhost/`。独立检查确认它会构造并发送带 bridge token 的内部 process 请求；`localhost` 不是字面 IP 约束，若主机解析配置或封装层异常，存在把 bearer token 发往非回环目标的条件风险。当前仓库没有生产监听入口或真实部署证据，因此这是部署/防御纵深问题，不是已确认的公网暴露。

建议 worker 只接受字面回环 IP，并把 `createPlatformServer` 的使用限制为已校验 loopback 的启动函数；部署验收需检查实际监听地址、反向代理和 DNS/hosts 解析。

### 日志契约审阅（未确认泄露，不计入当前 P2 gate）

`services/platform/src/auth.mjs:131-140` 在 SMTP 失败回调传出 `{email, type, code}`。README 示例（`services/platform/README.md:21-27`）只记录 `code`，仓库没有找到把整个对象写日志的生产调用，也没有 OTP 或 SMTP 原始错误进入该回调。若接入方直接记录参数，邮箱 PII 会进日志；这需要在真实邮件适配接入时检查记录字段；目前没有实际记录邮箱或验证码的证据。建议将回调参数收窄为非 PII 字段，或在类型/适配层强制结构化脱敏。

## 修复后关闭证据

当前版本已针对上述启动竞态加入两道门：

- `internal/webui/consumer-deletion.js:8-10` 的 `current()` 同时检查已有 `app.viewer.consumerUserId` 是否属于恢复记录 owner。
- `internal/webui/consumer-deletion-api.js:87-90` 新增 `recoverySession()`，复用官方 `get-session`，允许匿名恢复（null）或同 owner 会话；`consumer-deletion.js:118-126` 在 `eraseLocal()`/`app.auth.expire()` 前先通过它，foreign、未知响应或作用域变化都会中止终态清理。

修复前的 Chrome 记录 `web-deletion-boot-before-fix.log` 显示旧 A 的 terminal recovery 在 B viewer/auth 初始化竞态下 navigation 为 5，预期 2。当前代码用真实 Chrome 重跑现有浏览器 harness（`web-consumer-deletion-smoke.cjs`），15/15 场景通过、pageErrors 为 0；其中 deleting/deleted 两个延迟 auth/config 启动场景均只保留显式 reload，旧恢复被清理，未触发 B 的新 identity reload。汇总证据为 `web-deletion-browser-checks.json`。

另外独立增加了不同顺序的真实 Chrome 场景：在浏览器 context 中设置 `fixture_auth=B` cookie，页面 reload 后 fixture 的官方 `get-session` 按该 cookie 已返回 B，但 `/api/ui/viewer` 延迟 600ms 尚未初始化；注销 status 延迟 150ms，consumer config 延迟 350ms。结果为 `navigationCount=2`（显式 reload 是唯一一次）、`pageErrors=0`、`sessionStorage` 旧恢复为 null、注销对话框关闭，B 最终正常 bridged。这个顺序直接覆盖“viewer 未初始化而 B cookie 已存在”的窗口。

因此，原启动竞态 P2 已关闭；本轮没有发现新的已确认 P1/P2。`localhost`/直接 `createServer` 监听和 `onDeliveryFailure` 邮箱字段仍只是未验证的部署/集成边界：正常 `startPlatform()` 回环约束、README 的 code-only 日志接法、以及当前无生产监听证据均已核对，不作为本轮 gate。

## 已核对且未发现 P1/P2 缺陷的边界

- **未知响应不会重复 DELETE/POST。** `consumer-deletion-api.js:79-86` 对 delete 的不确定结果只查询状态，不重放不可逆 POST；提交前 `consumer-deletion.js:132-135` 先保存 `submitted`。独立 stub 检查模拟连接丢失，得到 1 次 POST、后续 GET，最终 `deleted`。
- **OTP、session token 没有浏览器持久化证据。** `consumer-deletion-api.js:9-15` 的恢复对象只含 origin、operationId、userId、sessionId、expiresAt、phase；OTP 只在 DOM 中使用并在 `consumer-deletion.js:24,107-115,160-166` 清除。`consumer-auth-api.js:1-3` 明确不持久化 token/OTP；Better Auth 配置 `services/platform/src/auth.mjs:139-141` 使用 `storeOTP:'hashed'`。账号会话撤销 token 仅在 `consumer-account-api.js:14-16` 的对话框 RAM 中。
- **关闭、刷新和晚回包有基本隔离。** `consumer-deletion.js:80-86,160-168` 使用 AbortController、reloading 标志和对象作用域；普通关闭会忘掉未提交阶段，`submitted/deleting` 会保留用于状态恢复。关闭 `verifying` 窗口可能丢失本次恢复记录，但不会再次提交删除；这是恢复体验缺口，不是重复删除证据。
- **Payload 任务没有 owner/email/input 注入。** `deletion-jobs.mjs:3,12-24,29-45` 只 POST 空 JSON，返回只保留 processed，错误统一为 `deletion_worker_unavailable`；任务输入为空，运行/查看/排队/取消均由 `administrator` 控制。`payload.config.mjs:3-15` 要求独立管理员库和显式平台配置，`config.mjs:131-146` 拒绝 Payload 与消费者库复用。
- **状态查询的匿名化是有意设计。** `account-deletion.mjs:82-90` 只返回状态/过期时间，`platform.mjs:104-113` 不返回 owner、email 或事件详情；随机 operation id 是唯一查询键。现有 Go 路由的同源 GET 门禁已核对，但本轮没有把它当成真实 HTTP 部署证明。

## 独立检查记录

1. `node --check` 通过目标 Web、admin、platform 文件。
2. 独立 inline stub 检查 Payload runner：拒绝非回环地址；对 loopback 只发一次空 body POST；隐藏响应私有字段和 pending 错误；另确认当前实现接受 `http://localhost/` 别名。
3. 独立 inline stub 检查 Web deletion API：delete 响应丢失时只发一次不可逆 POST，再 GET 状态完成。
4. 独立静态顺序检查确认 `shell.init → initializeViewer → shell.access` 与恢复记录自动打开的启动顺序。
5. `node --test internal/webui/consumer-deletion-api.test.cjs`：14/14 通过，新增 `recoverySession` 覆盖 null、同 owner、foreign、503。
6. 实际 Chrome 运行现有 browser harness：15/15 通过；另行运行带 B cookie、viewer 延迟而官方 session 已存在的启动顺序场景，navigation 仅 2 次且无 page error。

这些检查没有使用真实数据库、SMTP、移动设备或已发布服务；浏览器复验使用本机 Chrome 加 HTTP fixture。

## 未覆盖范围与当前缺口

- 没有真实 API/数据库发布或生产监听配置；不能据此声称后台清理已在真实环境工作。
- 没有执行 RN App 的 O00/O01；本仓库 Web 的“缓存”是 App 下载引导（`internal/webui/index.html:91-92`），未见 Web 的 service worker、CacheStorage、IndexedDB、media key/license 或离线视频实现。因此 A03 要求的本机 asset/license/key/queue 擦除仍须在 `juku-mobile` 单独验收。
- 未做真实生产浏览器、反向代理、DNS/hosts、容器网络或 SMTP 日志验证；本机 Chrome 的 HTTP fixture 只证明前端顺序和作用域门禁，不能替代发布环境验收。现有 API/browser 测试结果作为补充，独立结论仍以本轮代码检查和隔离复验为准。
