视觉按最新原型,业务按已确认PRD。现有代码为满足设计而改造。执行见按设计实施与任务卡;当前代码与验证范围见消费者功能页。
A03 Web 注销与 Payload 清理独立只读复审
本页目录
结论修复前 P2(已关闭):恢复中的旧注销记录可能在启动竞态下导致当前账号反复刷新未确认部署条件(不计入当前 P2 gate):后台 worker 对localhost 别名和直接监听缺少同一层的强制边界日志契约审阅(未确认泄露,不计入当前 P2 gate)修复后关闭证据已核对且未发现 P1/P2 缺陷的边界独立检查记录未覆盖范围与当前缺口复审时间: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 的注销操作。
证据链:
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。internal/webui/consumer-deletion.js:169在 deletion 初始化时直接读取sessionStorage的恢复记录并open()。恢复记录分支consumer-deletion.js:146-155不要求当前 auth 已完成,也不要求app.auth.bridged();只有当app.auth.user()已有值且用户不同才丢弃记录(151 行)。- 启动阶段
consumer-auth.js:11-12的user()仍可能因enabled=false返回空值。此时consumer-deletion.js:8-9的current()把“没有 auth user”视为允许继续。 - 恢复首先调用只带随机
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 部署证明。
独立检查记录
node --check通过目标 Web、admin、platform 文件。- 独立 inline stub 检查 Payload runner:拒绝非回环地址;对 loopback 只发一次空 body POST;隐藏响应私有字段和 pending 错误;另确认当前实现接受
http://localhost/别名。 - 独立 inline stub 检查 Web deletion API:delete 响应丢失时只发一次不可逆 POST,再 GET 状态完成。
- 独立静态顺序检查确认
shell.init → initializeViewer → shell.access与恢复记录自动打开的启动顺序。 node --test internal/webui/consumer-deletion-api.test.cjs:14/14 通过,新增recoverySession覆盖 null、同 owner、foreign、503。- 实际 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 测试结果作为补充,独立结论仍以本轮代码检查和隔离复验为准。