Scert/.claude/docs/ai/async-tasks/api-handoff.md
xuans 79e0e42bf9 更新证书恢复整改清单与接口文档
标记证书历史版本恢复前后端开发、前端测试和生产构建进度,补充恢复接口的 history_id、202、幂等、权限和本地恢复边界说明。
2026-10-03 02:34:12 +08:00

199 lines
19 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 证书异步任务接口接线说明
更新日期:2026年10月2日。依据当前源码;本批尚未部署,不代表生产验收通过。
## 业务范围
证书申请、当前证书部署、证书导入、本地证书校验及申请/部署手动重试先持久化任务,再交给后台工作线程执行。浏览器断线不取消已接收工作,前端通过任务接口确认最终结果。提交成功、证书保存成功、本地校验成功、逐服务器推送成功、整体任务成功是不同层级,不能混用。
## 接口与权限
所有接口要求完成初始化、有效登录会话及来源校验;前端沿用现有带会话的 fetchAdmin,不在 URL 中携带凭据。任务与域名接口返回 private, no-store 和 Vary: Cookie,不使用共享缓存。
| 方法与路径 | 行为 | 权限及返回 |
|---|---|---|
| POST /api/v1/domains/:id/issue | 提交申请及关联部署,无请求体 | 可见域名、证书管理、部署执行、DNS 凭据使用权限;202 |
| POST /api/v1/domains/:id/deploy | 提交已有证书部署,无请求体 | 可见域名、部署执行权限;202 |
| POST /api/v1/domains/:id/certificate/import | 上传证书后提交导入及部署任务 | 可见域名、证书管理、部署执行权限;不要求 DNS 凭据权限;202 |
| POST /api/v1/domains/:id/certificate/validate | 提交当前本地证书校验,无请求体 | 可见域名、私钥读取权限;不额外要求证书管理、DNS 或部署执行权限;202 |
| POST /api/v1/renewal/run | 提交管理证书及业务域名的全局续期检查,无请求体 | 当前有效超级管理员;202,不再同步返回 report |
| POST /api/v1/tasks/:id/retry | 同一任务新增一次尝试,无请求体 | 重试、证书管理、部署执行权限;申请类还检查 DNS 凭据;202 |
| DELETE /api/v1/tasks/:id | 请求取消 | 任务可见及取消权限;后台任务返回 202,非异步历史任务可能返回 200 |
| GET /api/v1/tasks | 当前身份可见任务列表 | 支持 domain_id 和 limit;返回 200 与 items 数组 |
| GET /api/v1/tasks/:id | 单个任务最新状态 | 返回 200 与任务对象本身,不包 task 字段 |
| GET /api/v1/tasks/:id/attempts | 任务尝试历史 | 返回 200 与 items 数组 |
| GET /api/v1/tasks/:id/deployment-results | 逐尝试的逐服务器结果与完整报告 | 返回 200、items 和 reports 数组;按任务、最近尝试编号及尝试 ID 精确匹配 |
| GET /api/v1/tasks/:id/validation-results | 读取本地校验公开元数据 | 任务读取权限;返回 200 与 items 数组,仓储查询实时复核可见范围 |
列表 limit 默认 50,有效范围为 1~200,越界由仓储回退至 50。不存在和无权访问的任务都返回 403,不能根据接口泄露其他项目资源是否存在。
### 异步提交响应
执行槽取得后还会重新检查当前账户、域名项目归属及操作权限。排队后撤权、停用账户、删除/迁移域名或权限依赖异常时,不执行证书工作流;已接收任务最终为 failed、stage 为 authorize,消息为“任务执行前权限或资源归属校验未通过,未执行证书操作”。任务及尝试、失败审计统一收尾,不附带虚构的部署报告。
上述终态以未发生取消且事务收尾成功为前提;同时收到取消时沿用取消语义,数据库收尾失败则保留中断状态供启动恢复,不伪造失败持久化成功。
该失败通过后续任务查询显示,不改变已经返回的 202,也不要求前端自动重新提交。若操作者已失去读取权限,查询仍可能返回 403;不得据此显示成功。重试按本次重试者重新授权,不沿用首次提交者身份。此复核不等同于操作开始后撤权自动中止,正在执行的任务仍使用取消入口。
返回 202 和 Location: /api/v1/tasks/任务编号。以下仅为字段示例,编号不对应真实环境:
```json
{
"accepted": true,
"task": {
"id": "example-task-id",
"domain_id": "example-domain-id",
"scope_type": "project",
"project_id": "example-project-id",
"task_type": "certificate_deploy",
"status": "queued",
"stage": "queued",
"message": "等待执行证书任务",
"created_at": "2026-10-02T08:00:00Z",
"updated_at": "2026-10-02T08:00:00Z"
}
}
```
202 的 task 是提交时快照,不保证仍处于 queued。重试响应可能已经是 running。前端必须使用后续 GET 状态作为依据,不凭提交响应显示成功。
### 幂等与错误
- 首次申请、部署、导入和校验支持 Idempotency-Key:长度 8~128,只允许英文字母、数字、连字符、下划线及英文句点。校验前端使用 certificate-validate 前缀,不将接口路径中的斜线写入请求头。前端当前为每次明确操作生成新键,网络重发若需要去重必须复用原键。
- 同一域名、同一键命中已结束任务返回 200、task 和 replayed: true;命中活动任务返回 409、task 和 replayed: true,不再次执行。跨操作复用同一键返回 409。
- 同类活动任务、仍在收尾、成功任务重试或不支持的任务类型返回 409。部署的 partial_success 允许重试;申请类只允许 failed 或 cancelled。
- 取消 202 只说明接收取消意图,状态为 cancelling;不能提前显示已取消或允许重试。已经完成的外部操作仍可最终成功,并提示取消未撤销该操作。
- 400:幂等键等输入无效;401:登录失效;403:无权访问或来源错误;409:初始化未完成或状态冲突;500:持久化等内部错误;501:工作流未配置;503:权限服务未就绪、服务停止或队列已满。
- 错误响应一般含 error,可能带 request_id 或 task;网关也可能返回非 JSON 页面。解析失败只能提示暂时无法确认,不能推断任务已失败或自动再次提交。
## 全局手动续期检查
本入口继续调用原续期调度器,保留管理证书、业务域名、调度互斥锁、续期子任务、紧急重试等待与取消传播;不缩减为只检查业务域名。自动调度行为不因 HTTP 后台化改变。
- 系统任务固定为 task_type=renewal_check、domain_id=global-renewal-check、scope_type=system、project_id 为空;它既不是业务域名,也不是单次管理证书签发任务。
- 提交与取得执行槽后均重新核验当前超级管理员。普通管理员及普通用户不能读取该任务、尝试或完成审计,也不能取消;排队期间停用或降级则不执行续期。
- 支持合法 Idempotency-Key。同键命中原续期任务返回 200、task、replayed=true;另一键遇到活动全局检查返回 409、原 task、replayed=true;新任务返回 202、task、accepted=true 和 Location。前端必须确认任务类型,不能把任意 409 当作已受理。
- 同步旧版的 200 加 report 不是新版受理响应,前端必须提示结果无法确认,不能显示检查完成。响应丢失时保留原幂等键;确认受理后,下一次明确操作才生成新键。
- DELETE /api/v1/tasks/:id 接收取消意图后,继续轮询终态。已发生的续期或部署不会因父任务取消自动撤销;取消和超时摘要保留已返回的执行统计。
- 终态摘要保存检查、临期、选中、紧急、成功、失败和异常数量,与任务尝试及完成审计同事务提交;通过现有 task.message 和 attempt.message 展示。没有新增结构化续期 report 接口,也不伪造部署报告或保存底层敏感错误。
- 有失败或异常时为 failed,已有成功项时可为 partial_success;取消为 cancelled、超时为 failed,不因下游返回空错误而显示整体成功。异常中断提示核查已生成的子任务,不自动重放。
- 不支持通过通用 retry 重试 renewal_check;完成后如需再次检查,由超级管理员明确重新提交。逐域名部署、校验与恢复结果仍以对应子任务为准。
- 400 表示幂等键无效,403 表示超级管理员权限校验未通过,501 表示续期工作流未配置,503 表示任务依赖未就绪、服务停止或队列已满,500 表示持久化等错误。队列拒绝时可能已保存失败任务,不能假定从未创建任务。
前端使用独立 RenewalAction.js 管理提交状态和幂等键,复用任务详情跟踪结果;禁止重复点击时并发提交,不能仅凭受理响应显示续期完成。
## 证书历史版本恢复
- `GET /api/v1/domains/:id/certificate/versions` 返回当前证书及可恢复的本地历史版本;前端使用 `history_id` 作为稳定恢复标识,不使用列表序号或展示版本号。
- `POST /api/v1/domains/:id/certificate/versions/:history_id/restore` 不接收证书内容,使用 `Idempotency-Key` 提交恢复任务;成功返回 `202 Accepted`、`accepted=true`、`task` 和 `Location: /api/v1/tasks/任务编号`。
- 恢复任务必须通过域名可见性、证书管理、部署执行和私钥读取权限复核;当前证书、过期版本、未生效版本、域名不匹配或证书类型不匹配不得恢复。
- 同域名存在活动恢复任务时返回冲突及原任务信息;相同幂等键重放时返回原任务,不重复执行。前端必须继续查询任务详情,不能凭 202 或 200 直接显示恢复完成。
- 恢复范围仅为 Scert 本地托管证书历史;任务完成不代表远端服务器已经部署新证书,远端部署必须使用独立部署任务并单独确认。
- 响应及错误信息不得暴露私钥内容、私钥路径、证书存储本地路径或服务器路径。
## 数据模型
| 模型 | 字段 |
|---|---|
| Task | id、domain_id、task_type、status、stage、message、scope_type、project_id、created_at、updated_at;可选 operator_id、certificate_version_id、idempotency_key、started_at、finished_at |
| TaskAttempt | id、task_id、attempt_number、stage、status、message、scope_type、project_id、created_at;可选 started_at、finished_at |
| TaskDeploymentResult | task_id、attempt_id、attempt_number、result_index、server_id、server、status、message、created_at;可选 backup_id |
| TaskDeploymentReport | task_id、attempt_id、attempt_number、report、created_at;report 保留汇总、逐目标结果、verification_status 与 recovery_status |
| TaskValidationResult | task_id、attempt_id、attempt_number、validation、created_at;validation 包含 domains、serial_number、fingerprint_sha256、not_before、not_after、chain_length |
编号和时间为字符串,时间使用带时区的 RFC3339 表示;attempt_number 与 result_index 为整数。Task 与 TaskAttempt 的状态含义如下:
| 状态 | 中文含义 | 是否终态 |
|---|---|---|
| queued | 排队中 | 否 |
| running | 执行中 | 否 |
| retry_wait | 等待重试 | 否 |
| cancelling | 正在取消并等待收尾 | 否 |
| success | 已完成 | 是 |
| failed | 执行失败或结果未可靠确认 | 是 |
| cancelled | 已停止;不保证外部副作用全部撤销 | 是 |
| partial_success | 部分完成 | 是 |
未知状态不得默认视为成功。逐服务器结果不包含完整的站点验证及恢复报告,不能据此推导 HTTPS 校验成功。
### 完整报告读取
deployment-results 响应保留旧 items,新增 reports;每次尝试独立保存,重试不覆盖历史。report.results 即使零目标也返回数组。示例省略逐目标明细:
```json
{
"items": [],
"reports": [{
"task_id": "task-example",
"attempt_id": "attempt-example",
"attempt_number": 1,
"created_at": "2026-10-02T09:00:00Z",
"report": {
"total": 0,
"success": 0,
"failed": 0,
"status": "failed",
"verification_status": "failed",
"recovery_status": "success",
"results": []
}
}]
}
```
- 完整报告写入和读取均按公开诊断规则处理;逐目标 failure_code 保留,原始远端错误不公开。
- 前端只在任务终态与最近尝试一致后展示报告,以任务终态覆盖报告展示状态,不能由外部操作成功推断收尾成功。
- 旧服务未提供 reports 或旧历史无完整报告时兼容逐目标汇总,但不补造验证/恢复状态。reports 存在而不是数组时视为读取异常。
- 报告读取在仓储查询中实时检查任务项目权限;只读成员可读,撤权后不得凭旧会话读取。
## 执行及前端交互边界
### 本地证书校验
- 提交返回 202,task_type 为 certificate_validate。提交与取得执行槽之后均复核私钥读取权限和域名归属;响应断开不取消已接收工作,不返回证书或私钥路径。
- 只有成功校验才保存 validation;失败、排队取消、关停、执行异常不生成通过结果。校验结果、任务、尝试和完成审计共用一个事务,不生成空的部署报告。
- validation 中 domains 为字符串数组,serial_number 和 fingerprint_sha256 为字符串,not_before 和 not_after 为带时区时间,chain_length 为整数。结果读取形式为 items 数组,每条记录关联原 task_id、attempt_id 和 attempt_number。
- 前端仅为校验任务额外读取 validation-results;任务与最近一次尝试均为 success,且三个关联字段全部一致时展示结果。切换、关闭详情和撤权后清除缓存;历史成功任务没有详情时明确说明未留存,不推断当前证书仍然有效。
- 结果只证明执行时的本地证书、私钥匹配及信任链校验,不代表远端部署、HTTPS 或生产验收通过。
- 校验不接入通用手动重试入口,POST /tasks/:id/retry 返回 409。用户需要重新校验时,应从域名校验入口明确创建新任务。
### 公共执行约束
- 当前后台最多接收 32 个在途任务,4 个执行槽,总执行预算 30 分钟;这些是实现限制,不是保证耗时。
- 队列满或关闭时拒绝执行,并尝试保存失败终态;持久化失败保留中断状态,不伪造已完成。
- 服务关闭停止接收、发送取消信号并等待工作线程收尾。启动恢复将中断记录标为失败,不自动重放外部签发、部署。
- HTTP 异步收尾路径将任务、本次尝试、完整报告、逐目标结果及完成审计放入同一事务;任一写入失败整体回滚并保留中断状态,不伪造终态。其他后台工作流仍需独立核查,不能据此声明全项目所有写入原子化。
- 后台证书共用执行器现同样采用事务收尾,覆盖续期、当前证书部署、导入、恢复及校验入口。内部失败重试会保存本次失败尝试及完整报告,父任务保持 retry_wait,不生成完成审计;最终失败审计统一使用 error,与 HTTP 路径一致。
- 后台重试等待期间取消,保留此前尝试及报告,只关闭父任务并保存取消审计;尚有活动尝试时拒绝绕过尝试单独收尾。报告、结果或审计落库失败不继续重试外部操作,保留活动状态供恢复。此约束不等于所有后台模块均已完成事务化。
- 前端每三秒查询一次,单次周期十秒超时且不重叠;卸载后停止轮询、取消请求。
- 切换任务时丢弃旧选择的迟到响应;撤权清除详情及部署结果。一般查询故障保留最后有效状态并提示未知,不改变任务终态。
- 三个详情 GET 不是一次原子快照;任务终态与最大 attempt_number 的状态不一致时暂不合并,下一周期重新查询。
- 任务从活动变终态后刷新域名、证书、运行状态和审计,但不覆盖正在编辑的设置表单。
## 回归与未完成项
### 证书导入接线补充
- 上传使用 multipart/form-data:certificate、private_key 必填,issuer_bundle 可选;整个请求最多 20 MiB,包含表单开销,不是每个文件分别获得 20 MiB 配额。错误上传参数返回 400。
- 兼容 JSON 路径字段 certificate_path、private_key_path、issuer_bundle_path;前两项必填,第三项可省略。路径在实际导入时仍受原有读取校验约束,异步接收不代表路径有效,不放宽任意本地文件读取权限。
- 返回与申请相同的 202、accepted、task 和 Location;task_type 为 certificate_import。不返回同步证书路径,不允许收到 202 就显示“证书已导入”。缺少 task.id 应提示结果未知,不能自动重新提交。
- 前端每次打开导入表单生成一个 Idempotency-Key,响应断开后继续使用原键。409 且 replayed=true 表示原任务仍活动,应跟踪原 task.id;已完成任务重放返回 200。相同键不比较上传内容,不用于更换文件或启动新的导入。
- 在途同域名任务冲突、幂等重放、无任务仓储、队列满或服务正在关闭时,本次上传副本由提交路径回收;成功入队后所有权交给工作线程,正常结束、授权拒绝、取消、关停和可恢复异常均回收副本。客户端断开不影响已接收任务。JSON 指定的源文件不在回收范围内。
- 进程强制终止后的上传残留清理尚未完成实机验证,不将正常关停回收扩大为崩溃清理验收通过。
- 已执行导入的最终完整报告新增 certificate_imported: true,表示证书已经保存,不表示部署成功;false 时字段可省略。该标志与任务终态及报告同事务持久化,不含源文件路径和私钥材料。
- 部署失败或部分成功且证书已保存时,任务消息明确提示检查结果后重试部署,无需重新导入。导入任务不支持 POST /tasks/:id/retry,返回 409,不能重放已回收的上传文件;修复目标后使用已有证书部署入口。
- 前端刷新时读取最近尝试的完整报告恢复保存标志;任务查询失败只显示无法确认,不将上传失败、部署失败、取消和保存状态混为一谈。
导入验证场景:正常上传及请求结束后继续执行、排队取消、服务关停、队列拒绝、异常、无效幂等键、重复上传回收、保存后部署失败、部分成功、禁止重试丢失的上传文件,以及排队后的账户/权限/资源变化。
已加入自动用例:断线后继续执行、排队取消、取消收尾禁止重试、队列满、异常脱敏、读取和保存失败、跨任务尝试拒绝、终态事务回滚、已完成与部分成功不被取消意图覆盖、前端迟到响应和网络恢复。
- [~] 当前 Linux 自动回归持续执行;竞争检测尚无可靠通过结果。
- [x] 完整验证及恢复报告持久化与页面读取已实现,关联回归通过;导入接线后最新前端 260 项及生产构建通过,未部署。
- [x] HTTP 异步完成审计、逐服务器结果与任务终态同事务,已覆盖三类保存失败整体回滚;其他工作流及三库实机复审仍待完成。
- [x] 后台证书共用执行器新增五处故障回滚、每次重试报告、等待取消及无活动尝试收尾回归;应用、仓储、HTTP 三包原生 Linux 测试、全后端测试及 go vet 均通过,未正式部署。
- [x] 生产导入入口及前端任务跟踪已接入异步执行;专项回归通过,未正式部署。
- [x] 校验入口、独立结果持久化和详情展示已接入异步任务;前端幂等键契约缺陷已先复现后修复,最新 266 项测试及生产构建通过,尚未正式部署。
- [ ] 版本恢复及手动续期等其他长操作尚未全部异步化;当前历史版本恢复的真实实现仍拒绝恢复,不能用模拟工作流测试替代功能验收。
- [ ] 正式构建部署后验证超过反代等待窗口的真实操作、断线查询、取消回滚和服务重启;本说明不替代生产验收。