238 lines
22 KiB
Markdown
238 lines
22 KiB
Markdown
# API 契约 6.0
|
||
|
||
账户资料使用 `GET/PATCH /accounts/profile`;头像为 PNG/JPEG/WebP data URI,禁止远程 URL 和 SVG。不提供旧资料导入。
|
||
空间结构使用 `GET /spaces/{id}/structure`,包含 revision、channels、groups、folders、locations。频道与分组分别通过 `/spaces/{id}/channels` 和 `/spaces/{id}/groups` 创建;响应同时包含完整结构快照和服务端新建实体及 ID。其他字段通过 PATCH 重命名;频道归属通过 `PUT /spaces/{id}/channels/{channelId}/group` 修改。文件夹通过 `/spaces/{id}/folders` 创建及 PATCH 重命名,文件归属通过 `PUT /spaces/{id}/locations` 修改。
|
||
频道、分组和目录写操作需要账号会话和空间写权限;允许上传的成员可以关联和移动自己的资源,文件移动必须满足每个条目的作者或管理权限,跨空间、跨频道的错误分组/目录关联被拒绝。结构修改后通知订阅设备重新拉取。
|
||
|
||
URL 基础路径保留 `/api/v1`,`/info.contract_version` 为 `8.2.0`(8.2 新增身份组 `/spaces/{id}/roles`:结构快照多了 `roles`、`rolesSeeded`,内容事件记录多了 `signerAccount`;8.1 新增 `DELETE /spaces/{id}/groups/{groupId}`:删除分组,组内频道回到未分组)。v8 让频道强制登录:去掉未登录访客(设置 `allow_guests`、`/guests` 接口、访客终端席位),频道与空间的所有接口都要求账号会话,缺少时返回 `401 account_required`;便捷分享(`/shares`)不受影响。v7 把「设备」收束为账号的属性:成员只按用户管理,`seats` 系列命名改为 `max_members` / `members_used` / `members_full`,去掉设备名与设备清单接口,登录账号不再需要终端密钥。v6 引入账号签名根密钥、设备证书和频道对象签名事件;不兼容的旧主版本客户端会被契约预检拒绝。账号资料与空间结构由服务端管理。创建频道/分享以及所有 owner 管理操作必须携带有效账号 session;频道链接只对登录账号有效,未登录的人先登录再按加入规则加入。
|
||
|
||
`GET /info` 的 `defaults` 包含 `channel`、`share` 创建设置;省略创建请求的 settings 时服务端使用相同默认值。`choices.max_members` 与 `limits.max_members` 表示成员人数范围,不代表设备数。频道设置 JSON 字段为 `max_members`(旧存储里的 `seats` 在启动时迁移)。其他选项按部署限制收束,前端只格式化选项文案,不提供替代业务默认值。
|
||
|
||
基础路径 `/api/v1`。所有请求和响应的二进制数据格式见 [protocol.md](protocol.md)。前端托管在独立域名,服务端需要配置 CORS 白名单。
|
||
|
||
## 1. 鉴权与通用头
|
||
|
||
| 头 | 说明 |
|
||
|---|---|
|
||
| `X-WD-Role` | `read` / `write` / `owner`,指明 `Authorization` 里的令牌属于哪种角色 |
|
||
| `Authorization` | `Bearer <base64url(令牌)>` |
|
||
| `X-WD-Endpoint` | `base64url(S)`,终端密钥。**只用于便捷分享**(未登录的创建者与读者);频道与空间不使用,可省略 |
|
||
| `X-WD-Cap` | 创建频道时的 Cap 验证码 token(服务端启用验证码时必填,见 protocol §11) |
|
||
| `X-WD-Max-Downloads` | 第 0 帧上带,条目的下载上限,0 为不限 |
|
||
| `X-WD-Account` | 账号 session token。与频道/分享的能力令牌分开;频道、空间、创建和管理请求必填 |
|
||
| `X-WD-Event-*` | 已登记签名设备提交频道对象 FINAL 帧时必填,绑定对象 ID、域/epoch、事件类型与完整密文帧 manifest,见 protocol §21 |
|
||
|
||
raw 模式的读请求不需要令牌,但仍需要账号会话。
|
||
|
||
WebSocket 无法设置头,令牌通过 `Sec-WebSocket-Protocol` 传递:`wd.v1, wd.role.<role>, wd.auth.<base64url(令牌)>, wd.account.<base64url(账号 session)>`,服务端回选 `wd.v1`。不要放进 URL,否则会被代理记录。
|
||
|
||
## 2. 权限矩阵
|
||
|
||
| 操作 | 需要 |
|
||
|---|---|
|
||
| 读日志、拉取条目 | `read`(enc)或知道 id(raw),并已是该频道的成员(账号) |
|
||
| 追加帧 | `write`(`members_can_upload` 为 true 时)或 `owner` |
|
||
| 删除条目 | `owner`;`delete_policy=own` 时账号等于条目作者;`anyone` 时任意成员 |
|
||
| 改设置、删频道、撤销成员 | `owner`;这些管理操作不占成员名额 |
|
||
|
||
## 3. 接口
|
||
|
||
### 账号与保险库
|
||
|
||
用户名为 3–32 位 ASCII 字母、数字、`_` 或 `-`,大小写不敏感。客户端 PBKDF2-SHA256(600,000 次)派生口令根,再分别派生登录凭据和 AES-GCM 保险库密钥。登录凭据仅经 HTTPS 发送;服务端再用随机 salt 和 PBKDF2-SHA256(210,000 次)存储慢哈希,数据库里的哈希不能直接用于登录。服务端不接收口令根或保险库密钥。二进制字段使用标准 Base64,保险库密文最大 1.3 MB。
|
||
|
||
- `POST /api/v1/accounts` 注册 `{username,salt,verifier,vault,public_key,signing_public_key}`。`public_key` 是 P-256 ECDH 公钥,`signing_public_key` 是 P-256 ECDSA 账号根公钥;对应私钥只保存在客户端加密保险库中。API v6 不兼容旧账号保险库协议,不自动迁移旧签名身份。
|
||
- `POST /api/v1/accounts/salt` 请求 `{username}`,返回 `{salt}`,供客户端派生登录凭据。
|
||
- `POST /api/v1/accounts/session` 请求 `{username,verifier,device_id?,device_name?}`。成功返回 `{username,token}`;session token 随机生成,只保存 SHA-256,期限 30 天。
|
||
- `GET /api/v1/accounts/vault` 使用 `Authorization: Bearer <token>` 读取 `{revision,vault}`。
|
||
- `PUT /api/v1/accounts/vault` 使用同一鉴权,发送 `{revision,vault}`。revision 乐观并发控制;冲突返回 `409 vault_conflict`,客户端必须先读取并合并,不能静默覆盖。
|
||
- `DELETE /api/v1/accounts/session` 撤销当前 bearer session;退出登录时调用。
|
||
- `GET /api/v1/accounts/users/{username}/key` 已登录用户查询 ECDH 邀请公钥和可选的账号签名根公钥。
|
||
- `PUT /api/v1/accounts/devices/signing-key` 当前设备提交 `{device_id,public_key,signature}`。服务端核对账号 session 绑定的 device_id,并验证账号根密钥对 P-256 设备公钥证书的签名后保存。
|
||
- `GET /api/v1/accounts/users/{username}/key` 返回账号 ECDH 公钥与签名根公钥;签名对象携带设备公钥和根签设备证书,客户端首次 TOFU 固定根公钥后离线验设备证书。
|
||
- `GET /api/v1/accounts/invitations` 返回当前账号的邀请,payload 为绑定收件用户名和频道 ID 的 ECDH + HKDF + AES-GCM 密文。
|
||
|
||
没有邮箱找回流程。忘记密码后保险库无法恢复。保险库仅同步资源凭据和草稿清单,不同步设备 S、设备名、缓存、原始文件或发送队列。浏览器存储按账号与访客分库;切换账号会中止订阅并清空内存会话。资源清单移除使用带时间戳的墓碑同步,避免旧设备将条目恢复。
|
||
|
||
签名根密钥保存在加密保险库;设备签名私钥以不可导出的 CryptoKey 保存在本机账户数据库,不随保险库同步。已登记签名设备写入 Workspace 对象时,FINAL 帧和删除请求必须带签名事件;尚未把签名要求扩展到频道结构和成员/权限变更。
|
||
|
||
### `GET /api/v1/info`
|
||
返回服务端能力:协议版本、限制、验证码配置(`captcha.provider`、`captcha.enabled`、`captcha.api_endpoint`,即 Cap widget 要连接的地址)、传输能力以及创建默认值/选项。`limits.max_workspace_bytes` 是单个空间全部资源的上限,`limits.max_workspace_channel_bytes` 是单个频道 Files 域的文件容量上限;频道结构的 `fileBytes` 为当前已用容量。前端只展示服务端返回的用量和限制。`defaults.channel_mode`、`defaults.channel` 和 `choices` 是前端创建表单的初始值与可选项;服务端独立校验提交的设置。验证码的 secret 不会出现在这里。`transports.http3` 只是部署层(反向代理或 Kestrel 配置)声明已启用 HTTP/3,服务端进程本身不内置 QUIC。
|
||
|
||
### `POST /api/v1/channels`
|
||
创建者必须登录。创建请求必须提供 `settings.name`,服务端拒绝缺失、空白或纯空格名称并返回 `400 space_name_required`,存储时去除首尾空格;名称长度最多 48 个字符。只有一套频道/成员/线程模型,`mode=enc|raw` 仅决定客户端内容是否端到端加密,不改变业务权限。创建者自动写入 owner 成员记录并占一个成员名额,同时登记当前设备终端;该账号的其他设备不增加名额。服务端启用验证码时还需要 `X-WD-Cap`。
|
||
|
||
```json
|
||
{
|
||
"id": "<channel_id>",
|
||
"mode": "enc",
|
||
"settings": { "name": "项目组", "max_members": 2, "members_can_upload": true, "delete_policy": "own",
|
||
"ttl_seconds": 86400, "max_items": 1000, "file_max_downloads": 10 },
|
||
"read_hash": "<sha256(T_read)>",
|
||
"write_hash": "<sha256(T_write)>",
|
||
"owner_hash": "<sha256(T_owner)>"
|
||
}
|
||
```
|
||
|
||
raw 模式省略 `read_hash`;`write_hash` 仅在成员可上传时提供。创建者自动成为频道的所有者成员。返回 `201`,响应体与「读取频道信息」相同(`ChannelInfo`)。
|
||
|
||
### `GET /api/v1/channels/{id}`
|
||
返回 `ChannelInfo`:设置(包含所有成员共享的频道名)、模式、成员名额占用(`members_used`)、`oldest_seq`、`last_seq`、时间。`members_used` 统计唯一账号成员,不统计账号的设备数。**不占成员名额**,需要账号会话,用于加入前预览和核对模式。频道名是普通设置,服务端可见。
|
||
|
||
### `POST /api/v1/channels/{id}/join`
|
||
加入频道,必须带账号会话(没有则 `401 account_required`)。只登记成员关系:账号第一次加入时占一个成员名额,同一账号的更多设备不增加名额,此后仅凭账号会话鉴权。同一账号重复调用幂等。成员名额已满时返回 `403 members_full`。读取频道信息和 owner 管理接口不占成员名额;访问频道内容前必须已加入,否则返回 `403 not_joined`。响应 `JoinResult`:`{joined_at, members_used, members_max}`,不再返回终端标识。
|
||
|
||
可发送 JSON `{password}`。`join_policy=open` 允许持链接的登录账号加入;`join_policy=invite` 要求该账号已受邀。频道主、已加入的账号成员和受邀账号无需再次输入频道密码。密码由服务端保存加盐 PBKDF2 哈希,从不通过频道信息返回。设备是账号的属性:同一账号可在任意多台设备登录,只占一个成员名额;账号成员的读写请求出示有效账号 session 即可,不再绑定到某个终端。
|
||
|
||
`GET /channels/{id}/members` 返回频道内账号成员及 `owner|member` 角色,每项附带账号级 `online`(该账号在本频道有活动 WebSocket/SSE 订阅)、`devices`(该账号的登录设备数,不小于 `online_devices`)与 `online_devices`(该账号此刻在本频道有订阅的设备数);设备是账号的属性,不单独作为成员。在线状态只来自内存里的订阅登记,不写库、不经订阅流推送,客户端按需轮询。`GET /channels/{id}/invitations` 不再返回已经成为成员的账号。邀请前,频道主调用 `GET /channels/{id}/invitations/{username}/crypto-plan` 获取服务端根据成员权限和历史设置计算的域/epoch/收件人计划。浏览器只为计划中的收件人生成并封装域密钥;随后 `POST /channels/{id}/invitations` 原子提交账号链接包与 `domain_envelopes`。服务端会重新计算计划并校验每个 epoch 和收件人,不接受前端自行增加授权或选择轮换范围。`DELETE /channels/{id}/invitations/{username}` 撤销邀请、成员授权与域密钥包,并阻止重新加入,直到再次邀请。不能移除频道主。移除访客用 `DELETE /channels/{id}/guests/{endpoint_hash}`,同样阻止该终端重新加入。
|
||
|
||
频道设置、成员权限、邀请、加入密码与加密模式统一作用于同一 Channel。每次上传、发言、线程/目录控制条目写入均经服务器校验账号会话、频道成员关系与写入权限,不能靠客户端隐藏按钮绕过。移除账号成员才会释放成员名额;撤销账号的某台登录设备只使该设备的会话失效,不影响成员身份。WebSocket 账号 session 经 `wd.account.<base64url(token)>` 子协议传递,并在推送循环中重新检查有效性。
|
||
|
||
### `PATCH /api/v1/channels/{id}/settings`
|
||
所有者修改设置。可通过 `name` 更新共享频道名,传空字符串清除名称;`max_members` 调整成员人数上限,不能改变已有成员身份;`ttl_seconds` 的修改从现在起算。
|
||
|
||
### `DELETE /api/v1/channels/{id}`
|
||
所有者删除整个频道和全部密文。
|
||
|
||
### `POST /api/v1/channels/{id}/frames`
|
||
追加一帧。请求体是完整封装(`application/octet-stream`)。当账号设备已登记签名证书且对象在 `/spaces/{id}/crypto/objects` 声明时,最终帧必须携带 `X-WD-Event-Id/Kind/Channel/Domain/Epoch/Frames/Manifest/Signature`(编辑可带 `X-WD-Event-Target`)。服务端验签并核对此前帧摘要与当前 FINAL 帧的清单;缺少或无效签名拒绝提交。签名随对象元数据进入 `CryptoState`,成员读取时仍按数据域权限过滤。
|
||
|
||
| 响应 | 含义 |
|
||
|---|---|
|
||
| `201 {"seq"}` | 新追加 |
|
||
| `200 {"seq"}` | 幂等重发,返回原 seq |
|
||
| `409` | 同一 `(item_id, frame_index)` 内容不同 |
|
||
| `403` | 无权、成员人数已满、模式或 ENC 标志不符 |
|
||
| `413` | 超出限制 |
|
||
| `429` | 限流 |
|
||
|
||
### `GET /api/v1/channels/{id}/log?from=<seq>&limit=&wait=<秒>`
|
||
按 seq 拉取日志,返回记录的线格式拼接(`application/octet-stream`)。`wait` 大于 0 时为长轮询,最长 25 秒。满足 `0 < from < oldest_seq` 时返回 `410 {"error":"gone","oldest_seq":N}`,客户端必须从 `from=0` 全量同步并丢弃本地已不存在的条目(不是从 `oldest_seq` 读起,见 protocol §8)。`from=0` 永远允许。
|
||
|
||
### `GET /api/v1/channels/{id}/ws?from=<seq>`
|
||
WebSocket 订阅(降级链的第一级)。服务端向下推送:
|
||
- 二进制消息:一条日志记录。
|
||
- 文本消息(JSON 控制消息):`{"t":"hb","seq":N}` 心跳,`{"t":"settings"}` 设置变更,`{"t":"members"}` 成员变更。
|
||
|
||
上传仍走 HTTP `POST`,便于幂等重试。
|
||
|
||
### `GET /api/v1/channels/{id}/events?from=<seq>`
|
||
SSE 订阅(降级链的第二级)。记录以 base64url 放在 `data:` 中,`id:` 是 seq,断线后用 `Last-Event-ID` 或 `from` 续传。由于 `EventSource` 不能设置头,前端用 `fetch` 流式读取。
|
||
|
||
### 降级与重连
|
||
WebSocket → SSE → 长轮询,每一级失败自动降级。心跳间隔 20 秒,45 秒收不到任何消息即判定死链。重连使用指数退避(1 秒到 30 秒)加抖动,并携带 `from=last_seq+1`。
|
||
|
||
### `GET /api/v1/channels/{id}/items/{item_id}/frames?from=<index>`
|
||
拉取条目的帧(GATED 条目的取数方式)。`from=0` 计一次下载;`from>0` 只有同一下载方(按用户)此前对同一条目成功启动过 `from=0` 才能续传,否则返回 `410 gone`。条目尚未 FINAL 时,响应会随帧到达持续输出,直到 FINAL。达到下载上限后返回 `410`,并在 5 分钟宽限期后删除密文,宽限期内已开始的拉取可继续续传。
|
||
|
||
### `DELETE /api/v1/channels/{id}/items/{item_id}`
|
||
按 `delete_policy` 授权后删除条目,并在日志中追加 tombstone。已登记签名设备还必须提交对应 `chat.delete`/`file.delete` 事件头;服务端验证其绑定的设备证书、权限和原对象帧 manifest 后保存删除签名,浏览器验签后才应用 tombstone。
|
||
|
||
### 已移除的接口
|
||
- v8:`GET /channels/{id}/guests`、`DELETE /channels/{id}/guests/{endpoint_hash}`(频道不再有未登录访客;成员按用户管理:`/members`、`/invitations`)。
|
||
- v7:`PUT /channels/{id}/seats/me`、`PUT /channels/{id}/seats/{endpoint_hash}/label`、`GET/DELETE /channels/{id}/seats`。设备名只在账号设备管理里存在(`/accounts/devices`)。
|
||
|
||
## 4. 便捷分享接口
|
||
|
||
便捷分享是与频道并列的另一种资源,语义见 [protocol.md §15](protocol.md)。**没有 WebSocket、SSE、日志、成员登记和成员上传**。接口前缀 `/api/v1/shares`,只支持 enc 模式,鉴权头只用 `read` 和 `owner` 两种角色(`write` 会被拒绝为 `bad_request`)。
|
||
|
||
| 接口 | 角色 | 阶段 | 说明 |
|
||
|---|---|---|---|
|
||
| `POST /shares` | 无(需验证码) | | 创建**草稿**。可带 `X-WD-Endpoint`:这个终端是创建者,不占访客名额 |
|
||
| `PATCH /shares/{id}/settings` | owner | 草稿 | 改有效期、访客上限、默认下载次数 |
|
||
| `POST /shares/{id}/frames` | owner | 草稿 | 追加帧。格式与频道的帧完全相同;不允许 SNAPSHOT 标志 |
|
||
| `DELETE /shares/{id}/items/{item_id}` | owner | 草稿 | 移除条目 |
|
||
| `DELETE /shares/{id}` | owner | 草稿 | 删除整个草稿 |
|
||
| `POST /shares/{id}/finish` | owner | 草稿 | **封存**,一次性、不可撤销 |
|
||
| `GET /shares/{id}` | read | 已封存 | 分享信息和每个文件的剩余下载次数(owner 在草稿阶段也可读) |
|
||
| `GET /shares/{id}/manifest` | read | 已封存 | 清单条目的封装字节;**首次读取登记访客** |
|
||
| `GET /shares/{id}/items/{item_id}/frames?from=<index>` | read | 已封存 | 下载文件,`from=0` 计一次下载;**首次读取同样登记访客** |
|
||
|
||
### `POST /api/v1/shares`
|
||
|
||
```json
|
||
{
|
||
"id": "<share_id>",
|
||
"read_hash": "<sha256(T_read)>",
|
||
"owner_hash": "<sha256(T_owner)>",
|
||
"settings": { "ttl_seconds": 86400, "max_visitors": 0, "file_max_downloads": 10 }
|
||
}
|
||
```
|
||
|
||
需要验证码(`X-WD-Cap`,与创建频道相同的规则),并受创建限流约束。返回 `201 ShareInfo`。草稿的 `expires_at` 是创建后 24 小时(服务端配置 `WebDrop:ShareDraftTtlSeconds`);`ttl_seconds` 是**封存之后**的有效期,封存时才开始计时。
|
||
|
||
| 设置 | 默认 | 范围 |
|
||
|---|---|---|
|
||
| `ttl_seconds` | 86400 | 60 ~ 604800 |
|
||
| `max_visitors` | 0(不限) | 0 ~ 10000 |
|
||
| `file_max_downloads` | 10 | 0(不限)或更大 |
|
||
|
||
### `POST /api/v1/shares/{id}/frames`
|
||
|
||
请求体是完整的帧封装,响应沿用频道的 `201`/`200 {"seq"}` 结构。便捷分享没有日志游标,这里的 `seq` 是该帧的 `frame_index`,用于确认追加与幂等重发。内容冲突返回 `409 conflict`,超限返回 `413 too_large`。额外规则:SNAPSHOT 标志返回 `400 bad_request`;分享已封存返回 `409 sealed`。第 0 帧可带 `X-WD-Max-Downloads` 设置该文件的下载次数上限。
|
||
|
||
### `POST /api/v1/shares/{id}/finish`
|
||
|
||
```json
|
||
{ "manifest_item_id": "<b64url>" }
|
||
```
|
||
|
||
按 protocol §15.5 校验后封存。成功返回 `200 ShareInfo`(此时 `state` 为 `sealed`)。清单条目不存在、不是单帧非 GATED 的完整条目,或草稿里还有没传完的条目,返回 `400 bad_request`。已封存的分享再次调用返回 `409 sealed`。
|
||
|
||
### `GET /api/v1/shares/{id}`
|
||
|
||
```json
|
||
{
|
||
"id": "…", "state": "sealed",
|
||
"expires_at": 1700000000000, "created_at": 1699990000000, "sealed_at": 1699999999000,
|
||
"manifest_item_id": "…",
|
||
"settings": { "ttl_seconds": 86400, "max_visitors": 0, "file_max_downloads": 10 },
|
||
"items": [ { "item_id": "…", "bytes": 12345, "frames": 3, "max_downloads": 5, "downloads_left": 4 } ]
|
||
}
|
||
```
|
||
|
||
`downloads_left` 为 `null` 表示不限。响应**永远不包含访客人数**。草稿对读角色返回 `404 not_found`。
|
||
|
||
### `GET /api/v1/shares/{id}/manifest`
|
||
|
||
返回清单条目的完整封装(`application/octet-stream`),必须带 `X-WD-Endpoint`。登记访客;访客名额已满且这个终端未登记时返回 `403 visitors_full`。
|
||
|
||
### `GET /api/v1/shares/{id}/items/{item_id}/frames`
|
||
|
||
与频道的 GATED 拉取一致:`from=0` 计一次下载;`from>0` 只有同一 `X-WD-Endpoint` 此前对同一文件成功启动过 `from=0` 才能续传,否则返回 `410 gone` 且不登记访客。达到上限后返回 `410 gone`,5 分钟宽限期内已开始的拉取可继续续传。必须带 `X-WD-Endpoint`,同样会登记访客。
|
||
|
||
## 5. 错误
|
||
|
||
错误响应体是 JSON `{"error": "<code>"}`,不回显任何用户提交的内容。
|
||
|
||
| code | HTTP | 含义 |
|
||
|---|---|---|
|
||
| `bad_request` | 400 | 封装格式或参数错误 |
|
||
| `unauthorized` | 401 | 令牌缺失或错误 |
|
||
| `account_required` | 401 | 需要账号会话:频道与空间只接受已登录账号,账号会话缺失或已失效 |
|
||
| `forbidden` | 403 | 角色不够 |
|
||
| `not_joined` | 403 | 账号不是该频道成员(未加入或已被所有者移除) |
|
||
| `visitors_full` | 403 | 便捷分享的访客名额已满,且这个终端未登记 |
|
||
| `sealed` | 409 | 便捷分享已封存,不能再修改;封存后再用所有者令牌发请求也是这个错误 |
|
||
| `members_full` | 403 | 成员人数已满 |
|
||
| `mode_mismatch` | 403 | 帧的 ENC 标志与频道模式不符 |
|
||
| `not_found` | 404 | 频道或条目不存在,或已过期 |
|
||
| `conflict` | 409 | 幂等冲突 |
|
||
| `gone` | 410 | 已删除、下载达上限,或 `0 < from < oldest_seq`(需要从 0 全量同步) |
|
||
| `too_large` | 413 | 超出限制 |
|
||
| `captcha_required` | 428 | 需要验证码:缺少 `X-WD-Cap`,或 token 无效、已用过 |
|
||
| `captcha_unavailable` | 503 | 验证码服务暂时不可用(服务端到 Cap 的调用失败),可稍后重试 |
|
||
| `rate_limited` | 429 | 限流 |
|
||
|
||
### 按账号恢复频道密钥
|
||
|
||
- `GET /api/v1/accounts/channels`:只需 `X-WD-Account`,返回当前账号已加入频道的 `{channel_id,role,payload,created_at}` 密钥包;不返回其他账号或未接受邀请的密钥包,不占成员名额。
|
||
- `PUT /api/v1/channels/{id}/key-package`:需要该频道 owner 能力和 owner 账号,仅更新当前 owner 自己的 `{payload}`,用于补齐已创建频道的账号密钥包。
|
||
- 创建频道可携带 `key_package`(1–16384 字符 opaque 密文),与频道和 owner 成员记录原子保存。前端必须提供;底层 API 允许省略供纯协议客户端使用。
|
||
- 发送邀请时原子保存收件账号的空间密钥包、频道数据域 envelopes、默认权限和 epoch 更新;接受后仍可跨设备领取。撤销邀请/成员会删除服务端密钥包与访问记录,但无法收回对方此前保存的密钥,成员移除后的 epoch 轮换仍未实现。删除空间时清理全部密钥包。
|
||
|
||
|
||
### 账号设备与资料
|
||
|
||
GET /accounts/devices 返回本账号设备 {id,name,created_at,last_seen,current}。PATCH /accounts/devices/{id} 发送 {name};DELETE 撤销该设备的全部登录 sessions,不移除账号成员。新客户端登录时携带稳定浏览器 device_id(UUID)和设备名,各设备单独记录。个人名称/头像存储在客户端加密保险库,修改后按 modifiedAt 合并,不写入服务端明文账号表。昵称不改变固定用户名或参与密钥派生。
|