Skip to content
Clone
b868469
93 lines 6.0 KB Markdown

API 契约交叉审计 · 汇总

5 个 agent 按 tag 分域并行审计 view/app 的 288 个调用点,方法见 README。 每域原始报告:chat.md、space-account.md、pulls-reviews.md、issues-repo.md、git-misc.md。

总览

域调用点操作确认可疑QBPRG
chat / notifications62583401020
space / auth / account / user8777166011410
pulls / reviews / protection54541400010
issues / repos / branches / tags / releases65635312200
git 底层(diff/blob/tree/commit/blame/lfs/remote/archive/cleanup/submodule/conflict/push)30303510110
合计2982822822244810

28 条确认问题里,18 条已在本轮修掉,10 条是后端能力缺口(见「未修」)。

共性根因(比单条 bug 更值得记住)

  1. OPS 表的 query/body 标志位曾经是常量,params 从不发给生成客户端 —— 上一轮修掉后, 所有 query 才第一次真正上线;本轮 4 条 Q 类(skip/max_count、sort=position、limit=200×2) 都是「参数第一次被发出去」后才暴露出来的。
  2. 生成的 URL builder 对 path 参数零编码(355 个 builder 无一调用 encodeURIComponent)。 分支名、标签名、release tag 合法允许 /,于是建得出来、改不掉也删不掉。
  3. listOf / page?.items 兜底把形状错误降级为静默空数据:list_pins 裸数组当分页、 list_emojis 分页当数组(后者直接崩)、my_suspensions 裸数组当分页、MemberSummary 扁平当嵌套 —— 四例都是「不报错、只是永远空着」。
  4. utoipa 文档与 handler 返回类型会脱节:list_pins 的 body = Page<MessageDto> 与实际 Vec<MessageDto> 不符,生成客户端因此骗了前端。文档即契约,注释写错等于契约写错。
  5. 单层 Option<T> 被前端当三态用:null 与「缺省」在服务端同义, 于是「清空」动作静默失效(房间限流、仓库简介/主页、保护规则 pattern、封禁到期)。

已修(18)

后端(lib/service/space/src/routes.rs):space 服务有 10 个 handler + utoipa 声明 + 生成客户端, 但路由没挂载 —— 挂上即可,无契约变更:

方法路径handler
POST/spaces/{s}/membersadd_member
GET/PATCH/DELETE/spaces/{s}/members/{member_id}get_member / update_member / remove_member
GET/spaces/{s}/banslist_bans
GET/PATCH/spaces/{s}/bans/{user_id}get_ban / update_ban
GET/POST/spaces/{s}/inviteslist_invites / create_invite
DELETE/spaces/{s}/invites/{invite_id}revoke_invite

前端:

类位置修法
Pservices/api/index.tspath 参数统一 encodeURIComponent(一处修全量 355 个 builder 的斜杠问题)
Qrepo/branches/{branches-tab,tags-section}.tsxPAGE_LIMIT 200 → 100(服务端上限)
Qspace-admin.tsADMIN_PAGE_LIMIT 200 → 100
Qrepo/model/commits.tsskip/max_count → offset/limit
Qspace-admin.ts删除 sort: 'position'(该端点不接受)
Qhistory/ui/commits-tab.tsxoffset 到 5000 上限即停(越界是 400 不是截断)
Rchat/model/api.tslist_pins 按裸数组读;list_emojis 按 page.items 读
Rsettings/model/api.tsmy_suspensions 按裸数组读
Rchat/issues/repo-protection/space-admin 四处新增 lib/member-row.ts:扁平 MemberSummary → { user }
Rpulls/model/api.tsupdateBranchPull 返回 {pull,result},取 .pull
Rhistory/ui/commit-view.tsx根提交不再拿 EMPTY_TREE 求 diff(服务端 rev_parse 404),改为显式空补丁 + 文案
Bissues/ui/issue-form-modal.tsxattachment_ids 只在 create 发,PATCH 不再带(UpdateIssueCmd 无此字段)
Brepo/.../general-settings.tsx简介/主页用空串清空(后端单层 Option<String>)
Bchat/ui/chat-room.tsx限流清空发 0 而不是 null(后端 0..=3600 合法,null=不变)
Bspace-admin.ts封禁带 expires_at 时串一次 update_ban(BanMemberBody 只有 reason)
Pspace-admin.ts成员三件套先 findSpaceMember 取会籍 UUID,再填 {member_id}(原来填数字 user_id)

未修(10 + 22 可疑)

后端能力缺口(本轮不改,需要产品/后端决定):

  1. diff_raw 无法对空树求差:根提交的补丁需要 git diff-tree --root 之类支持,当前前端只能降级。
  2. BanMemberBody 没有 expires_at:现在靠封禁后补一次 PATCH 实现到期时间,两次调用非原子。
  3. CreateInviteBody 没有 email/extra_role_ids:邀请表单里的「按邮箱邀请」「附带角色」被静默丢弃。
  4. list_pins 的 utoipa 响应类型写成 Page<MessageDto>,实际是 Vec<MessageDto>;下次重生成客户端前应修正注释。
  5. UpdateBranchPolicyCmd 的 protected_file_patterns 等是单层 Option,与 README 宣称的三态 PATCH 不符。

前端待决策(改动涉及 UI 取舍,本轮未动):

  • 邀请弹窗的 email / 角色选择(#3 的用户可见部分);
  • protection.ts 的 update_policy 仍把未改动的 pattern 补成 null(等价于「不变」,属误导性写法);
  • chat.update_room_setting / update_thread 的 null 分支(当前无调用方触发);
  • listRoomMembers 本地空实现(后端确实没有该端点,不是契约错配)。

复现审计

# 机械扫描(Q/B/P/R 四类,秒级)
python3 scripts/gen-api-ops.py          # 确认 OPS 表与生成客户端一致
# 语义复核(逐 handler 结构体比对)按 README 分域进行
bash scripts/contract-check.sh --client-only