Files
MoShiro/docs/api/api-guidelines.md
T
justme e014a19554
ci/woodpecker/push/main-amd64-build Pipeline was successful
docs: reorganize into design, security, api, ops and dev; drop stale designs; refresh the diagrams [CI]
- Group docs by category with an index (docs/README.md); fix every link,
  code comment and the protocol conformance test that read docs/protocol.md.
- access.md and security.md rewritten to what is implemented: removed the
  unadopted member-proof chain, roles and recovery key that do not exist,
  the old implementation order and the budgets; gaps now carry a status.
- enrollment, deployment, api: the install flow is the static, hash-checked
  script with environment variables; no default admin.
- README and architecture: new topology and update-flow diagrams (operator
  passkey, CI signing, registry feed, auth and trust modules).
2026-10-08 19:37:56 +08:00

8.4 KiB
Raw Blame History

API 设计约束

本文是所有 Backend REST 端点都必须遵守的规则;具体端点见 api.md。规则参考了 Microsoft Azure REST API Guidelines 和 RFC 9457,并根据本项目做了取舍。内部协议(Probe↔Agent、Agent↔Backend)的兼容规则见 protocol.md 第 4 节。

1. 契约生成链路

Minimal API 端点(TypedResults 返回类型 + 显式元数据)
   │ dotnet build(Microsoft.Extensions.ApiDescription.Server)
   ▼
backend/openapi/v1.json(OpenAPI 3.1,提交到仓库)
   │ CI:git diff --exit-code,未提交的变更会导致失败
   ▼
web:openapi-typescript 生成类型,调用方的类型错误会直接暴露
  • 端点必须声明精确的返回类型,例如 Results<Ok<AgentDto>, NotFound<ProblemDetails>>,不能返回 IResult 或匿名对象,否则 OpenAPI 文档会不准确。
  • 每个端点都必须设置 WithName("agents_list")(即 operationId,snake_case、资源_动作)、WithSummary、WithTags。
  • 请求和响应 DTO 放在 MoShiro.Backend.Api/Contracts/,并注册到 Backend 的 JSON 源生成上下文。不要直接暴露 MoShiro.Protocol 中的线协议类型,因为 REST 与线协议的演进节奏不同。
  • 结构强类型,指标弱类型:资源结构(Agent、任务、测试等)必须是强类型 DTO;而指标值和评分统一使用字典 metrics: { "<metric_id>": number }、scores: { "<model_id>": number },键的含义、单位、版本由 /api/public/catalog/* 给出。这样新增指标或模型不需要改 OpenAPI,也不需要改前端类型。

2. 版本与兼容性

  • 当前版本为 v1,路径前缀为 /api/public、/api/admin、/api/agent/v1。出现破坏性变更时,新增 /api/v2/public、/api/v2/admin 等路径。v1 内只允许非破坏性变更:
非破坏性变更(v1 内允许) 破坏性变更(需要新建 v2)
新增端点 删除或重命名端点、字段
响应中新增字段 修改字段类型、单位或语义
新增可选的请求字段或查询参数 可选字段改为必填
枚举新增值 删除枚举值
放宽校验 收紧校验、修改默认值
新增错误码 修改已有错误码对应的 HTTP 状态
  • 客户端必须忽略未知字段,也必须能处理未知的枚举值。
  • 需要 v2 时,v1 和 v2 至少并存一个发布周期;OpenAPI 文档按版本分文件(v1.json、v2.json)。

3. URL 与方法

  • 按调用方分层:/api/public(只读展示)、/api/admin(管理)、/api/agent(节点)。同一个资源在不同层中可以有不同的 DTO,例如 public 层不暴露 pubkey、local_limits。
  • 路径全部小写,资源名用复数名词,多个单词用 - 连接:/api/admin/join-tokens。
  • 嵌套最多一层;子资源一旦有全局唯一 ID,就提供顶层路径(/probes/{probe_id})。
  • 查询参数和 JSON 字段使用 snake_case。
操作 方法 成功状态 说明
列表 GET 集合 200 分页
读取 GET 单个 200
创建 POST 集合 201 + Location 响应体返回完整资源
部分更新 PATCH 200 application/merge-patch+json(RFC 7396)
删除 DELETE 204 资源不存在时同样返回 204(幂等)
非 CRUD 动作 POST /{resource}/{id}/{verb} 200 或 204 例如 revoke、rotate-token
  • GET 请求不能产生副作用。PUT 不使用:配置类资源都通过 PATCH 修改。

4. 数据表示

类别 规则 示例
时间点 RFC 3339 UTC,以 Z 结尾,精确到毫秒。唯一例外:列式时序响应中的时间轴数组 t 使用 Unix 毫秒(见 query.md) 2026-09-27T10:00:00.000Z
时长 整数 + 单位后缀 interval_ms: 1000
延迟 带小数的毫秒值(指标字典中的单位见目录) rtt_p95_ms: 58.93
百分比 0–100 的数字 + _pct loss_pct: 0.5
计数 整数,不使用浮点数 attempts: 600
ID 带类型前缀的 ULID 字符串 prb_01J9Z...
枚举 snake_case 字符串 "udp_echo"
缺失值 字段始终存在;未知值写 null,不省略字段 "last_seen_at": null
布尔值 使用 is_/has_ 前缀,或者用意义明确的形容词 online、enabled

5. 错误

所有 4xx/5xx 响应都使用 application/problem+json(RFC 9457),并附加扩展字段:

{
  "type": "about:blank",
  "title": "Validation failed",
  "status": 400,
  "code": "validation_failed",
  "detail": "interval_ms must be >= 200",
  "trace_id": "00-4bf92f...-01",
  "errors": { "interval_ms": ["must be >= 200"] }
}
  • code 取自共享的 ErrorCode 枚举,是客户端做判断的唯一依据;title/detail 只供人阅读,内容可能变化。
  • errors 只在 400 validation_failed 中出现,键名是 JSON 字段路径。
  • 5xx 响应不能泄露异常信息或调用栈,只提供 trace_id,用于在日志中定位问题。
状态码 使用场景
400 请求格式错误或校验失败
401 未认证
404 资源不存在
403 已认证但作用域不足
409 名称冲突、状态冲突(例如 probe_offline)
410 资源曾经存在但已过保留期(例如 raw_expired)
412 If-Match 不匹配
413 请求体超过上限
422 语义上无法满足(例如 range_too_large)
429 超过限流,需要带 Retry-After
503 存储不可用,或正在关闭

6. 集合与查询

  • 分页由服务端驱动:?limit=(默认 50,最大 500)和 ?cursor=(不透明字符串);响应格式为 { "items": [...], "next_cursor": string | null }。排序必须稳定(按 ID 或时间 + ID)。
  • 时间范围参数统一使用 from/to,区间为左闭右开 [from, to)。缺省时 to = now,from = to − 1h。
  • 所有查询都有上限(点数、时间跨度、结果条数);超出时返回 422 并在 detail 中给出可行的替代方案,不能悄悄截断。

7. 并发控制

  • 配置类资源(Agent、Probe、任务)在响应中返回 ETag,PATCH/DELETE 支持 If-Match,不匹配时返回 412。没有带 If-Match 的请求按“最后写入者获胜”处理。

8. 安全

  • 只提供 HTTPS。认证方式有两种:
    • 浏览器使用 Cookie 会话:HttpOnly; Secure; SameSite=Strict;所有非 GET 请求都必须带 X-CSRF-Token 请求头;
    • 脚本使用 Authorization: Bearer <api_token>,作用域为 read 或 admin。
  • 凭据不能放在 URL 或查询参数中;SSE 依靠 Cookie 或同源令牌认证。
  • 登录接口需要限流(例如每个 IP 每分钟 10 次),失败时统一返回 401,不区分“用户不存在”和“密码错误”。
  • public 层根据站点设置和 public_visible 过滤数据:被隐藏的资源对匿名访客一律返回 404,而不是 403。
  • 令牌明文只在创建或轮换的响应中出现一次;这类响应带 Cache-Control: no-store。
  • 请求体上限为 64 KiB;查询接口有超时(默认 10 秒)。

9. 实时推送(SSE)

  • 实时数据使用 text/event-stream。event 名称就是数据类型(line、node、point、test);data 是单行 JSON,遵守第 4 节的数据表示规则。
  • 每 15 秒发送一行注释作为心跳。重连后服务端先推送当前快照,再推送增量,不补发历史。
  • 订阅的主题数和连接数都有上限,超出时返回 429 too_many_streams。
  • 推送的内容必须能由对应的 REST 查询重建:SSE 只负责“更快地获得变化”,不承载独有的数据。

10. 新增端点检查清单

  1. 符合第 3 节的路径与方法规则,并有 operationId、summary 和 tag;
  2. 返回类型精确(TypedResults),错误使用 problem+json,错误码来自 ErrorCode;
  3. DTO 符合第 4 节的数据表示规则,并注册到 JSON 源生成上下文;
  4. 列表支持分页,查询有上限;
  5. 选择正确的层(public / admin / agent),public 层只暴露可以公开的字段;
  6. 重新生成并提交 openapi/v1.json,在 PR 中说明是否属于破坏性变更;
  7. 测试覆盖:成功路径、校验失败、404、鉴权失败、隐藏资源;
  8. 同步更新 api.md。