feat(api)!: fold devices into users and report member presence
Contract 7.0.0 (breaking). Devices become an attribute of the account;
members are managed per user, and only unauthenticated guests keep a
per-terminal registration.
- rename seats -> max_members, seats_used -> members_used,
seats_full -> members_full, SeatJoined -> JoinResult, the `seats`
stream control message -> `members`, and POST /channels/{id}/seats ->
POST /channels/{id}/join (stored channel settings are migrated on start)
- drop per-terminal encrypted device labels and the seat list/release
endpoints; add GET/DELETE /channels/{id}/guests for guest terminals
- authenticate account members by account session only: the terminal key
is optional for logged-in accounts, no seat row is written for them,
and gated-download grants are counted per user (per terminal for guests)
- GET /channels/{id}/members now returns online, devices and
online_devices per account; presence is tracked in memory from active
WebSocket/SSE subscriptions and is not pushed over the stream
- owner invitation list no longer returns accounts that already joined
This commit is contained in:
1 parent
0fd2a269d9
commit
d946fc5e8a
35 files changed
+517
-490
No files matched your search
+26
-27
@@ -4,9 +4,9 @@
|
||||
空间结构使用 `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` 为 `6.0.0`。v6 引入账号签名根密钥、设备证书和频道对象签名事件;不兼容的旧主版本客户端会被契约预检拒绝。账号资料与空间结构由服务端管理。创建频道/分享以及所有 owner 管理操作必须携带有效账号 session;公开链接仍允许访客按频道加入规则访问。
|
||||
URL 基础路径保留 `/api/v1`,`/info.contract_version` 为 `7.0.0`。v7 把「设备」收束为账号的属性:成员只按用户管理,`seats` 系列命名改为 `max_members` / `members_used` / `members_full`,去掉设备名与设备清单接口,访客改走 `/guests`,登录账号不再需要终端密钥。v6 引入账号签名根密钥、设备证书和频道对象签名事件;不兼容的旧主版本客户端会被契约预检拒绝。账号资料与空间结构由服务端管理。创建频道/分享以及所有 owner 管理操作必须携带有效账号 session;公开链接仍允许访客按频道加入规则访问。
|
||||
|
||||
`GET /info` 的 `defaults` 包含 `channel`、`share` 创建设置;省略创建请求的 settings 时服务端使用相同默认值。`choices.seats` 与 `limits.max_seats` 表示成员人数范围,不代表设备数。频道设置 JSON 字段 `seats` 为保持已有频道配置可读而保留其名称;v5 起它限制的是成员数。其他选项按部署限制收束,前端只格式化选项文案,不提供替代业务默认值。
|
||||
`GET /info` 的 `defaults` 包含 `channel`、`share` 创建设置;省略创建请求的 settings 时服务端使用相同默认值。`choices.max_members` 与 `limits.max_members` 表示成员人数范围,不代表设备数。频道设置 JSON 字段为 `max_members`(旧存储里的 `seats` 在启动时迁移)。其他选项按部署限制收束,前端只格式化选项文案,不提供替代业务默认值。
|
||||
|
||||
基础路径 `/api/v1`。所有请求和响应的二进制数据格式见 [protocol.md](protocol.md)。前端托管在独立域名,服务端需要配置 CORS 白名单。
|
||||
|
||||
@@ -16,7 +16,7 @@ URL 基础路径保留 `/api/v1`,`/info.contract_version` 为 `6.0.0`。v6 引
|
||||
|---|---|
|
||||
| `X-WD-Role` | `read` / `write` / `owner`,指明 `Authorization` 里的令牌属于哪种角色 |
|
||||
| `Authorization` | `Bearer <base64url(令牌)>` |
|
||||
| `X-WD-Endpoint` | `base64url(S)`,终端密钥,用于席位 |
|
||||
| `X-WD-Endpoint` | `base64url(S)`,终端密钥。**只有未登录访客必填**;登录账号以 `X-WD-Account` 会话鉴权,可省略 |
|
||||
| `X-WD-Cap` | 创建频道时的 Cap 验证码 token(服务端启用验证码时必填,见 protocol §11) |
|
||||
| `X-WD-Max-Downloads` | 第 0 帧上带,条目的下载上限,0 为不限 |
|
||||
| `X-WD-Account` | 账号 session token。与频道/分享的能力令牌分开;创建、管理和账号成员请求必填,访客省略 |
|
||||
@@ -30,10 +30,10 @@ WebSocket 无法设置头,令牌通过 `Sec-WebSocket-Protocol` 传递:`wd.v1, w
|
||||
|
||||
| 操作 | 需要 |
|
||||
|---|---|
|
||||
| 读日志、拉取条目 | `read`(enc)或知道 id(raw),并占有席位 |
|
||||
| 读日志、拉取条目 | `read`(enc)或知道 id(raw),并已是成员(账号)或已加入的访客终端 |
|
||||
| 追加帧 | `write`(`members_can_upload` 为 true 时)或 `owner` |
|
||||
| 删除条目 | `owner`;`delete_policy=own` 时终端哈希等于条目作者;`anyone` 时任意已入席终端 |
|
||||
| 改设置、删频道、看和释放席位 | `owner`;这些管理操作不占席位 |
|
||||
| 删除条目 | `owner`;`delete_policy=own` 时账号等于条目作者(访客为终端哈希等于条目作者);`anyone` 时任意成员 |
|
||||
| 改设置、删频道、看和移除访客 | `owner`;这些管理操作不占成员名额 |
|
||||
|
||||
## 3. 接口
|
||||
|
||||
@@ -66,7 +66,7 @@ WebSocket 无法设置头,令牌通过 `Sec-WebSocket-Protocol` 传递:`wd.v1, w
|
||||
{
|
||||
"id": "<channel_id>",
|
||||
"mode": "enc",
|
||||
"settings": { "name": "工作设备", "seats": 2, "members_can_upload": true, "delete_policy": "own",
|
||||
"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)>",
|
||||
@@ -74,22 +74,22 @@ WebSocket 无法设置头,令牌通过 `Sec-WebSocket-Protocol` 传递:`wd.v1, w
|
||||
}
|
||||
```
|
||||
|
||||
raw 模式省略 `read_hash`;`write_hash` 仅在成员可上传时提供。创建者的终端(`X-WD-Endpoint`)自动入席。返回 `201`,响应体与「读取频道信息」相同(`ChannelInfo`)。
|
||||
raw 模式省略 `read_hash`;`write_hash` 仅在成员可上传时提供。创建者自动成为频道的所有者成员。返回 `201`,响应体与「读取频道信息」相同(`ChannelInfo`)。
|
||||
|
||||
### `GET /api/v1/channels/{id}`
|
||||
返回 `ChannelInfo`:设置(包含所有成员共享的频道名)、模式、成员名额占用(`seats_used`)、`oldest_seq`、`last_seq`、时间。`seats_used` 统计唯一账号成员和匿名访客终端,不统计账号的设备数。**不占成员名额**,任意角色可调用,用于加入前预览和核对模式。频道名是普通设置,服务端可见。
|
||||
返回 `ChannelInfo`:设置(包含所有成员共享的频道名)、模式、成员名额占用(`members_used`)、`oldest_seq`、`last_seq`、时间。`members_used` 统计唯一账号成员和匿名访客终端,不统计账号的设备数。**不占成员名额**,任意角色可调用,用于加入前预览和核对模式。频道名是普通设置,服务端可见。
|
||||
|
||||
### `POST /api/v1/channels/{id}/seats`
|
||||
为一个终端加入频道。每个设备终端各自建立席位以便认证、重命名和撤销,但席位不再受人数上限约束。账号第一次加入时占一个成员名额;同一账号的更多设备不增加名额。同一设备重复调用幂等。成员名额已满时,新的账号成员或访客终端返回 `403 seats_full`。匿名访客无法跨设备识别,每个访客终端各计一个成员名额。读取频道信息和 owner 管理接口不占成员名额;频道内容、席位信息/设备名等其余操作要求终端已入席,否则返回 `403 not_joined`。
|
||||
### `POST /api/v1/channels/{id}/join`
|
||||
加入频道。登录账号只登记成员关系:账号第一次加入时占一个成员名额,同一账号的更多设备不增加名额、也不登记终端,此后仅凭账号会话鉴权(`X-WD-Endpoint` 可省略)。未登录访客登记一个终端(`X-WD-Endpoint` 必填),每个访客终端各计一个成员名额。同一账号或同一访客终端重复调用幂等。成员名额已满时返回 `403 members_full`。读取频道信息和 owner 管理接口不占成员名额;访客访问频道内容前必须已加入,否则返回 `403 not_joined`。响应 `JoinResult`:`{joined_at, members_used, members_max}`,不再返回终端标识。
|
||||
|
||||
可发送 JSON `{password}`。默认 `join_policy=open,allow_guests=true`,允许持链接的陌生人设备加入。`allow_guests=false` 要求账号登录;`join_policy=invite` 要求该账号已受邀。频道主、已加入的账号成员和受邀账号无需再次输入频道密码。密码由服务端保存加盐 PBKDF2 哈希,从不通过频道信息返回。成员账号和设备终端身份独立:同一账号可拥有不限数量的设备席位,但只占一个成员名额;账号成员的读写请求必须出示对应账号 session 和该设备 S。
|
||||
可发送 JSON `{password}`。默认 `join_policy=open,allow_guests=true`,允许持链接的陌生人设备加入。`allow_guests=false` 要求账号登录;`join_policy=invite` 要求该账号已受邀。频道主、已加入的账号成员和受邀账号无需再次输入频道密码。密码由服务端保存加盐 PBKDF2 哈希,从不通过频道信息返回。设备是账号的属性:同一账号可在任意多台设备登录,只占一个成员名额;账号成员的读写请求出示有效账号 session 即可,不再绑定到某个终端。
|
||||
|
||||
`GET /channels/{id}/members` 返回频道内账号成员及 `owner|member` 角色。邀请前,频道主调用 `GET /channels/{id}/invitations/{username}/crypto-plan` 获取服务端根据成员权限和历史设置计算的域/epoch/收件人计划。浏览器只为计划中的收件人生成并封装域密钥;随后 `POST /channels/{id}/invitations` 原子提交账号链接包与 `domain_envelopes`。服务端会重新计算计划并校验每个 epoch 和收件人,不接受前端自行增加授权或选择轮换范围。`DELETE /channels/{id}/invitations/{username}` 撤销邀请、成员授权、域密钥包与该账号的设备席位,并阻止重新加入,直到再次邀请。不能移除频道主。设备面板踢出访客也会阻止该 S 重新加入。
|
||||
`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)>` 子协议传递,并在推送循环中重新检查有效性。
|
||||
频道设置、成员权限、邀请、加入密码与加密模式统一作用于同一 Channel。每次上传、发言、线程/目录控制条目写入均经服务器校验账号会话(或访客终端)、频道成员关系与写入权限,不能靠客户端隐藏按钮绕过。移除账号成员才会释放成员名额;撤销账号的某台登录设备只使该设备的会话失效,不影响成员身份。WebSocket 账号 session 经 `wd.account.<base64url(token)>` 子协议传递,并在推送循环中重新检查有效性。
|
||||
|
||||
### `PATCH /api/v1/channels/{id}/settings`
|
||||
所有者修改设置。可通过 `name` 更新共享频道名,传空字符串清除名称;`seats` 调整成员人数上限,不能改变已有成员身份;`ttl_seconds` 的修改从现在起算。
|
||||
所有者修改设置。可通过 `name` 更新共享频道名,传空字符串清除名称;`max_members` 调整成员人数上限,不能改变已有成员身份;`ttl_seconds` 的修改从现在起算。
|
||||
|
||||
### `DELETE /api/v1/channels/{id}`
|
||||
所有者删除整个频道和全部密文。
|
||||
@@ -112,7 +112,7 @@ raw 模式省略 `read_hash`;`write_hash` 仅在成员可上传时提供。创
|
||||
### `GET /api/v1/channels/{id}/ws?from=<seq>`
|
||||
WebSocket 订阅(降级链的第一级)。服务端向下推送:
|
||||
- 二进制消息:一条日志记录。
|
||||
- 文本消息(JSON 控制消息):`{"t":"hb","seq":N}` 心跳,`{"t":"settings"}` 设置变更,`{"t":"seats"}` 席位变更。
|
||||
- 文本消息(JSON 控制消息):`{"t":"hb","seq":N}` 心跳,`{"t":"settings"}` 设置变更,`{"t":"members"}` 成员/访客变更。
|
||||
|
||||
上传仍走 HTTP `POST`,便于幂等重试。
|
||||
|
||||
@@ -123,20 +123,19 @@ SSE 订阅(降级链的第二级)。记录以 base64url 放在 `data:` 中,`id:`
|
||||
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 分钟宽限期后删除密文,宽限期内已开始的拉取可继续续传。
|
||||
拉取条目的帧(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。
|
||||
|
||||
### 设备终端席位
|
||||
- `GET /api/v1/channels/{id}/seats`:所有者查看已加入设备(终端哈希、加密设备名、加入时间)。设备清单不等于成员人数;一个账号可有多个设备席位。
|
||||
- `PUT /api/v1/channels/{id}/seats/me`:终端上传设备名(≤256 B);enc 模式加密,raw 模式明文。
|
||||
- `PUT /api/v1/channels/{id}/seats/{endpoint_hash}/label`:所有者为指定设备更新名称;enc 模式的密文绑定目标 endpoint hash,raw 模式明文。设备也可用 `/seats/me` 更新自己的名称。
|
||||
- `DELETE /api/v1/channels/{id}/seats/{endpoint_hash}`:所有者移除该设备的席位,其活动订阅随后会被拒绝;账号成员身份和人数名额保留。撤销账号邀请/成员会移除该账号全部设备并释放成员名额。
|
||||
### 访客
|
||||
- `GET /api/v1/channels/{id}/guests`:所有者查看未登录访客的终端(`endpoint_hash`、`joined_at`)。账号成员按用户管理(`/members`、`/invitations`),其设备不对外列出。
|
||||
- `DELETE /api/v1/channels/{id}/guests/{endpoint_hash}`:所有者移除该访客终端并禁止它再加入,其活动订阅随后被拒绝(`not_joined`);对账号终端返回 `404`。
|
||||
- 已移除:`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`)。
|
||||
便捷分享是与频道并列的另一种资源,语义见 [protocol.md §15](protocol.md)。**没有 WebSocket、SSE、日志、成员登记和成员上传**。接口前缀 `/api/v1/shares`,只支持 enc 模式,鉴权头只用 `read` 和 `owner` 两种角色(`write` 会被拒绝为 `bad_request`)。
|
||||
|
||||
| 接口 | 角色 | 阶段 | 说明 |
|
||||
|---|---|---|---|
|
||||
@@ -212,10 +211,10 @@ WebSocket → SSE → 长轮询,每一级失败自动降级。心跳间隔 20
|
||||
| `bad_request` | 400 | 封装格式或参数错误 |
|
||||
| `unauthorized` | 401 | 令牌缺失或错误 |
|
||||
| `forbidden` | 403 | 角色不够 |
|
||||
| `not_joined` | 403 | 终端未入席,或席位已被所有者释放 |
|
||||
| `not_joined` | 403 | 访客终端未加入或已被所有者移除,或账号不是该频道成员 |
|
||||
| `visitors_full` | 403 | 便捷分享的访客名额已满,且这个终端未登记 |
|
||||
| `sealed` | 409 | 便捷分享已封存,不能再修改;封存后再用所有者令牌发请求也是这个错误 |
|
||||
| `seats_full` | 403 | 成员人数已满 |
|
||||
| `members_full` | 403 | 成员人数已满 |
|
||||
| `mode_mismatch` | 403 | 帧的 ENC 标志与频道模式不符 |
|
||||
| `not_found` | 404 | 频道或条目不存在,或已过期 |
|
||||
| `conflict` | 409 | 幂等冲突 |
|
||||
@@ -227,7 +226,7 @@ WebSocket → SSE → 长轮询,每一级失败自动降级。心跳间隔 20
|
||||
|
||||
### 按账号恢复频道密钥
|
||||
|
||||
- `GET /api/v1/accounts/channels`:只需 `X-WD-Account`,返回当前账号已加入频道的 `{channel_id,role,payload,created_at}` 密钥包;不返回其他账号或未接受邀请的密钥包,不占席位。
|
||||
- `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 轮换仍未实现。删除空间时清理全部密钥包。
|
||||
@@ -235,4 +234,4 @@ WebSocket → SSE → 长轮询,每一级失败自动降级。心跳间隔 20
|
||||
|
||||
### 账号设备与资料
|
||||
|
||||
GET /accounts/devices 返回本账号设备 {id,name,created_at,last_seen,current}。PATCH /accounts/devices/{id} 发送 {name};DELETE 撤销该设备全部登录 sessions 与频道席位,不移除账号成员。新客户端登录时携带稳定浏览器 device_id(UUID)和设备名,各设备单独记录。个人名称/头像存储在客户端加密保险库,修改后按 modifiedAt 合并,不写入服务端明文账号表。昵称不改变固定用户名或参与密钥派生。
|
||||
GET /accounts/devices 返回本账号设备 {id,name,created_at,last_seen,current}。PATCH /accounts/devices/{id} 发送 {name};DELETE 撤销该设备的全部登录 sessions,不移除账号成员。新客户端登录时携带稳定浏览器 device_id(UUID)和设备名,各设备单独记录。个人名称/头像存储在客户端加密保险库,修改后按 modifiedAt 合并,不写入服务端明文账号表。昵称不改变固定用户名或参与密钥派生。
|
||||
Reference in new issue
Block a user