外观
VMax OpenAPI 接口契约设计
1. 契约地位
OpenAPI 3.1 文件应是 HTTP 接口的单一事实来源,并生成或校验 Swift/Go 类型。本文定义契约规则和首版资源语义,不替代未来的 openapi.yaml。
- 来源 MUST:所有写接口支持
Idempotency-Key,24 小时内对同一请求返回一致结果。 - 来源 MUST:响应包含
request_id;错误使用稳定机器码;消息拉取使用稳定游标;ACK 批量上限 100 条。 - 来源 MUST:认证是短期 token + 设备签名,不使用主密码、假密码或永久 API key。
- 来源 MUST:协议对象有明确版本;未知版本、不安全降级、超长或畸形输入被拒绝。
- 架构范围:iOS Swift/SwiftUI 客户端与 Go 服务端从同一 OpenAPI 3.1 契约生成或校验类型;不提供 Web 管理后台 API。
- MVP 能力:文本消息、身份、认证、预密钥、拉取/ACK 和 Push route;S3 附件端点属于 P1,MVP 实例必须声明
attachments=false。 - 已验证事实:当前仓库没有 OpenAPI 文件或运行实现,本文描述的是待实现契约。
2. 通用传输约束
- 仅允许 HTTPS/WSS;客户端不能关闭证书校验。
- HTTP API 首版前缀为
/v1。无前缀的/.well-known/vmax/config只用于实例发现。 Content-Type默认application/json;二进制字段采用 Base64URL 还是 CBOR,待总控决策。- 每个请求和响应均限制正文大小、数组长度、字符串长度和未知枚举;安全关键对象拒绝宽松转换。
- 时间使用 RFC 3339 UTC;服务端时间只用于队列、限流与展示参考,不参与端到端协议安全。
request_id只用于单次服务请求关联,不进入 E2EE 信封,不作为跨服务长期用户标识。
3. 版本与能力
3.1 实例发现
GET /.well-known/vmax/config 无用户认证,返回最小公开配置。下列数值只用于展示结构,不冻结生产上限或保留策略:
json
{
"config_version": 1,
"instance_id": "opaque",
"instance_name": "Example",
"api_versions": ["v1"],
"protocol_versions": [1],
"registration_mode": "invite",
"max_envelope_bytes": 65536,
"message_ttl": {"default_seconds": 604800, "min_seconds": 86400, "max_seconds": 2592000},
"push_mode": "official_gateway",
"features": {"attachments": false, "websocket": true},
"signing_public_key": "b64url...",
"signature": "b64url..."
}签名覆盖哪些字段、canonical encoding 与 instance_id 格式属于 ADR-003/ADR-004,待总控决策。客户端首次连接显示指纹并固定,后续变化必须阻断,不能静默接受。
3.2 API 与协议版本分离
- API 版本控制资源、认证和错误结构。
protocol_version控制 opaque E2EE 信封语义。服务端只做允许列表和外层一致性校验。- 能力协商只能建立新会话;不得在同一会话中静默切换密码套件。
- 对未知关键字段或未知协议版本返回明确错误,不尝试“尽力解密”或降级。
4. 认证
| 方法 | 认证 | 关键约束 |
|---|---|---|
POST /v1/auth/challenge | 设备公开标识 | challenge 60 秒、单次使用、绑定实例域与请求上下文 |
POST /v1/auth/token | 设备签名 | 验证 challenge 后签发短期 access token |
| token 刷新/重认证 | 待总控决策 | 必须支持设备撤销且不引入永久 bearer token |
这两个 POST 同样要求 Idempotency-Key。challenge 重放返回首次 challenge,即使其随后已过期;token 端点在首次事务已提交但响应丢失时必须返回同一认证会话的等价结果,不能消费第二次 challenge。不得在通用幂等表保存明文 token;安全重建、认证专用加密存储或等价业务结果的具体方式为待总控决策。challenge 响应不得泄露设备是否对应高价值身份;防枚举策略、token TTL 和刷新机制需通过威胁建模后冻结。
5. 核心端点
| 方法与路径 | 用途 | 幂等/隐私要求 |
|---|---|---|
POST /v1/identities | 创建随机身份 | 幂等;只接收公开身份包;地址格式待 ADR-003 |
GET /v1/identities/{address}/prekey-bundle | 获取公开预密钥包 | 限流、防枚举、最小字段;消费语义待协议决策 |
PUT /v1/devices/{device_id}/prekeys | 补充公开预密钥 | 幂等;批次签名与数量上限 |
POST /v1/messages | 提交密文信封 | 幂等、大小/TTL/版本/配额校验 |
GET /v1/messages?cursor=&limit= | 拉取待收信封 | 稳定 opaque 游标;只允许当前设备 |
POST /v1/messages/ack | 批量 delivered ACK | 幂等;最多 100 条;成功解密后调用 |
POST /v1/push/routes | 注册随机唤醒路由 | 幂等;消息实例不向 Gateway 发送内容 |
DELETE /v1/push/routes/{route_id} | 注销路由 | 幂等;删除后不再产生新唤醒 |
POST /v1/attachments/init | 初始化密文附件 | P1;只校验密文字节数/哈希/TTL |
当 attachments=false 时,客户端不得展示附件入口;服务端即使保留 P1 schema,也必须对附件端点返回稳定的 FEATURE_NOT_ENABLED,不得创建上传会话或联系 S3。图片、视频和文件不属于 MVP。
实例管理不暴露公网 CRUD API。注册模式、TTL/配额、Gateway 模式等策略由配置文件/secret 与一次性 vmax-cli 变更,变更后生成新的签名配置版本;CLI 不得查询消息内容。
6. 消息接口
6.1 提交
http
POST /v1/messages
Authorization: Bearer <short-lived-token>
Idempotency-Key: <client-generated-random-key>
Content-Type: application/jsonjson
{
"message_id": "opaque-client-id",
"recipient_device_id": "opaque-device-id",
"protocol_version": 1,
"expires_in_seconds": 604800,
"envelope": "b64url-opaque-bytes",
"padding_class": 2
}服务端从解码后的实际字节计算大小,不信任客户端提供的长度。成功返回 201;幂等重放返回与首次相同的安全响应:
json
{
"request_id": "req_opaque",
"message_id": "opaque-client-id",
"accepted_at": "2026-09-23T00:00:00Z",
"queue_expires_at": "2026-09-30T00:00:00Z"
}相同 Idempotency-Key 但请求指纹不同返回 409 IDEMPOTENCY_KEY_REUSED。同一 (recipient_device_id, message_id) 的相同信封应返回原结果;不同信封冲突必须拒绝并产生安全事件。
6.2 拉取
GET /v1/messages?cursor=<opaque>&limit=<1..100> 按 (accepted_at, internal_tiebreaker) 稳定排序。响应包含 items、next_cursor、has_more,不因拉取自动 ACK 或删除。游标过期/不可解析返回可恢复错误,客户端从安全检查点重新拉取并以 message_id 去重。
长轮询和 WebSocket 仅提示“可能有新数据”,不得携带正文或代替游标拉取。
6.3 ACK
json
{
"acks": [
{"message_id": "opaque-client-id", "status": "delivered"}
]
}ACK 只接受当前设备队列中的消息;一次最多 100 条。不存在、已过期和已 ACK 的项目要返回逐项稳定结果,避免整个批次因单项重复失败。read receipt 是端到端加密控制消息,不是服务端 ACK 状态。
7. 幂等契约
- Header 必填范围:所有创建、更新、删除和批量 ACK 接口。
- 键由客户端生成,服务端只存哈希;作用域绑定 actor、端点/操作和 API 主版本。
- 保证窗口:首次结果起至少 24 小时。响应通过 header 返回
Idempotency-Expires-At。 - 请求指纹基于规范化的业务字段;具体规范化格式跟随 ADR-004,未冻结前不得用于跨语言安全签名。
- 5xx 且事务未提交时可重试;事务已提交但响应丢失时必须重放成功结果。
8. 错误模型
json
{
"request_id": "req_opaque",
"error": {
"code": "MESSAGE_QUOTA_EXCEEDED",
"message": "recipient queue is full",
"retryable": false,
"retry_after_seconds": null,
"details": {}
}
}首版至少定义:认证失败、challenge 过期/重放、版本不支持、信封过大、TTL 越界、配额超限、幂等键复用、游标无效/过期、限流、资源冲突、依赖暂不可用。message 不含本地化业务文案或敏感值;客户端按 code 处理。
9. OpenAPI 质量门槛
- schema 对字符串/数组/二进制长度、枚举、必填字段和
additionalProperties明确约束。 - 生成的 Swift/Go 客户端在 CI 与契约文件同步;手写类型漂移视为失败。
- 兼容性检查阻止删除字段、收紧范围、改变状态码或复用错误码含义。
- 示例不得包含真实地址、域名、token、APNs token、密文或个人数据。
- 契约测试覆盖正常、边界、非法输入、重复请求、并发和故障提交后响应丢失。
- Swift 生成代码与 Go server stubs/validator 使用锁定版本的生成器;生成结果在 CI 重跑无 diff,防止客户端与服务端手写漂移。
- MVP 契约测试在没有 S3 服务和凭据的环境运行,并断言附件能力关闭、核心文本消息流程不依赖对象存储。
10. 运维健康接口
健康接口不属于管理后台,只提供机器可判定状态:
| 路径 | 语义 | 公开内容 |
|---|---|---|
GET /health/live | Go 进程事件循环可响应 | 固定状态与 request_id |
GET /health/ready | API 可安全接收核心流量 | 不暴露依赖地址或凭据 |
GET /health/dependencies | PostgreSQL、Redis 队列及启用能力的汇总状态 | 仅内部网络;组件类别和稳定错误码 |
PostgreSQL 不可写时 readiness 失败。Redis 不可用时,消息事务是否暂时继续接受取决于 outbox 积压上限:低于安全阈值可接收并告警,超过阈值则以稳定可重试错误保护数据库;阈值为待总控决策。MVP 中 S3 关闭不应被报告为故障。
11. 待总控决策
- 二进制与签名对象编码:JSON/Base64URL 或 CBOR;建议 opaque bytes + 明确媒体类型,验证跨 Swift/Go 固定向量。
- 身份地址与公开 ID:路径参数和错误防枚举行为跟随 ADR-003,不能由服务端线路自行冻结。
- token 刷新和撤销:选择短期 token + 重新 challenge,或有设备绑定的 refresh token;建议先威胁建模并验证丢失设备撤销。
- 游标密封方式:签名 JSON、加密 token 或版本化二进制;建议不可伪造、可轮换且不暴露表主键,验证篡改、过期和密钥轮换。
- 备份接口:首版不提供服务端聊天历史恢复;客户端备份格式和是否包含历史跟随 ADR-007。
- Redis 队列实现与健康退化阈值:选择库/Streams 方案并定义积压、重试和熔断错误;验证 Redis 清空后由 PostgreSQL outbox 重建。
- P1 附件契约:对象 key、分片、密文哈希和 TTL 在附件独立立项时评审;当前只保留能力开关,不冻结媒体 API 细节。