修复鉴权投影并发与退款 outbox 冲突

绑定认证工作集投影版本并在 SpacetimeDB 内执行 CAS

为认证 procedure 增加 runtime service identity 校验

防止退款 outbox 跨进程覆盖并校验临时文件事实

更新后端契约与项目决策记录
This commit is contained in:
2026-08-27 14:47:22 +08:00
parent 72d7214646
commit 36a4d37d24
10 changed files with 441 additions and 52 deletions
@@ -7751,5 +7751,5 @@ CI 上 `background_agent_runtime_recovers_stale_running_before_pending_task` 在
## 2026-08-27 API 鉴权读取与节点本地 outbox 恢复切片
- 生产 Bearer 中间件的会话有效性改由 SpacetimeDB typed `validate_auth_session` procedure 在事务内校验 `user_account.token_version`、会话归属、撤销时间和过期时间;`InMemoryAuthStore` 仅保留启动恢复工作集及测试夹具,不作为生产请求鉴权读取源。该切片不等于登录、刷新、验证码和微信 state 的全量迁移,后续写路径仍需继续收口。
- `sync_auth_store_projection` 增加 `auth_store_projection_meta.updated_at` 单调水位检查,早到的整包快照失败关闭;正式认证表仍是权威源,水位只是迁移期跨 API 实例的延迟快照保护。
- `validate_auth_session`、认证投影导出和同步 procedure 都从 `ctx.sender()` 取调用方并要求现役 runtime service identity。`sync_auth_store_projection` 使用 API 工作集启动恢复或上次成功同步绑定的 `base_updated_at_micros` 做事务内 CAS,并要求 `updated_at_micros` 严格递增;基线不一致或版本不晚于当前值的整包快照失败关闭,只有确认本次同步前没有遗留未确认本地变更且同步期间无其它本地认证变更时才从正式表恢复,否则保留旧基线继续失败关闭,避免自动恢复覆盖并发未提交变更。同步成功但期间出现新本地变更时最多连续补同步三轮,仍未稳定则失败关闭,并保留待重试 revision。正式认证表仍是权威源,只是迁移期跨 API 实例的延迟快照保护。
- tracking outbox 与 wallet refund outbox worker 启动即执行恢复;退款 outbox 会恢复崩溃遗留 `tmp-*` 文件并隔离损坏 / 冲突文件。两者仍是节点本地 durable fallback,不能用粘性会话替代共享持久化;生产节点必须保留对应目录并纳入恢复演练。
@@ -413,7 +413,7 @@ Responses 的终态载荷既是工具调用的恢复源,也是正文的恢复
- Rust 结构体:`AuthStoreProjectionMeta`
- 源码:`server-rs/crates/spacetime-module/src/auth/tables.rs`
认证恢复策略:`api-server` 启动时只从 SpacetimeDB 正式认证表(`user_account` / `auth_identity` / `refresh_session`)导出 typed `AuthStoreProjectionView`,再恢复 `module-auth` 的进程内认证工作集;生产 Bearer 中间件不再从 `InMemoryAuthStore` 读取用户或会话,而是每次通过 typed `validate_auth_session` procedure 在 SpacetimeDB 事务内校验 `token_version`、会话归属、撤销时间和过期时间,SpacetimeDB 不可用时 fail closed 返回服务错误。测试构建仍可使用显式的内存测试夹具。运行中 refresh cookie 在本进程工作集内未命中时直接按失效处理,不再从 SpacetimeDB 导出整包认证状态刷新内存,避免旧投影把重复手机号或旧会话重新灌回进程。`module-auth` 只保留内存工作集和 projection 导入 / 导出能力,不再保留 JSON 快照导入 / 导出能力,也不写本地持久化文件;`auth-store.json` / `GENARRATIVE_AUTH_STORE_PATH` 不再是兼容恢复源。认证创建、登录会话、刷新、退出、改密、重置密码、绑定和资料变更等写操作仍必须在返回客户端前通过 `sync_auth_store_projection` 成功同步 SpacetimeDB 正式认证表;同步失败时接口返回错误,不允许把只存在于当前进程内存的账号或会话当成成功结果。投影同步使用 `auth_store_projection_meta.updated_at` 作为单调水位,拒绝早于当前水位的整包快照,避免两个 API 实例的延迟快照互相覆盖;这只是迁移期并发保护,不改变正式认证表的权威地位。新用户注册奖励、邀请码绑定和登录埋点必须排在认证同步成功之后,避免认证没落库时先写出钱包或邀请关系。若启动恢复阶段 SpacetimeDB 不可连接或超时,`api-server` 会按固定间隔持续重试认证工作集恢复,恢复成功后才开始监听 HTTP,避免一次短超时让进程永久停留在依赖不可用状态。
认证恢复策略:`api-server` 启动时只从 SpacetimeDB 正式认证表(`user_account` / `auth_identity` / `refresh_session`)导出 typed `AuthStoreProjectionView`,再恢复 `module-auth` 的进程内认证工作集;生产 Bearer 中间件不再从 `InMemoryAuthStore` 读取用户或会话,而是每次通过 typed `validate_auth_session` procedure 在 SpacetimeDB 事务内校验 `token_version`、会话归属、撤销时间和过期时间,SpacetimeDB 不可用时 fail closed 返回服务错误。`validate_auth_session`、投影导出和投影同步均从 `ctx.sender()` 派生调用方,并复用现役 runtime service identity 白名单;启动恢复先完成该服务身份初始化,普通 SpacetimeDB identity 不能读取或改写私有认证表。测试构建仍可使用显式的内存测试夹具。运行中 refresh cookie 在本进程工作集内未命中时直接按失效处理,不再从 SpacetimeDB 导出整包认证状态刷新内存,避免旧投影把重复手机号或旧会话重新灌回进程。`module-auth` 只保留内存工作集和 projection 导入 / 导出能力,不再保留 JSON 快照导入 / 导出能力,也不写本地持久化文件;`auth-store.json` / `GENARRATIVE_AUTH_STORE_PATH` 不再是兼容恢复源。认证创建、登录会话、刷新、退出、改密、重置密码、绑定和资料变更等写操作仍必须在返回客户端前通过 `sync_auth_store_projection` 成功同步 SpacetimeDB 正式认证表;同步失败时接口返回错误,不允许把只存在于当前进程内存的账号或会话当成成功结果。每个 API 工作集绑定启动恢复或上次成功同步得到的 `auth_store_projection_meta.updated_at` 版本作为 `base_updated_at_micros`SpacetimeDB 在同一事务内执行基线 CAS,并要求新的 `updated_at_micros` 严格递增;基线不一致或版本不晚于当前值时整包写入失败,冲突节点在确认本次同步前没有遗留未确认本地变更且同步期间没有其它本地认证变更后,才可丢弃本地工作集并从正式表恢复,不能用陈旧工作集删除、恢复或覆盖另一节点的新状态;若此前已有待重试 revision 或同期还有本地变更则保留旧基线并继续失败关闭,不用自动恢复覆盖未提交变更;同步成功但期间又出现新本地变更时最多连续补同步三轮,仍未稳定则失败关闭。这只是迁移期并发保护,不改变正式认证表的权威地位。新用户注册奖励、邀请码绑定和登录埋点必须排在认证同步成功之后,避免认证没落库时先写出钱包或邀请关系。若启动恢复阶段 SpacetimeDB 不可连接或超时,`api-server` 会按固定间隔持续重试认证工作集恢复,恢复成功后才开始监听 HTTP,避免一次短超时让进程永久停留在依赖不可用状态。
`auth_store_snapshot` 表和旧 `import_auth_store_snapshot_json` / `export_auth_store_snapshot_from_tables` procedure 已删除。认证投影同步只读写 `user_account``auth_identity``refresh_session``auth_store_projection_meta``auth_identity` 不再写 `phone_e164``display_name``avatar_url`,这些账号资料只以 `user_account` 为准。