chore(repo): initialize WebDrop monorepo
Signed-off-by: JustMe <mr.toing@outlook.com>
This commit is contained in:
commit
53768a195d
39 files changed
+9063
No files matched your search
+191
@@ -0,0 +1,191 @@
|
||||
# API v1
|
||||
|
||||
基础路径 `/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-Label` | raw 模式条目标签,≤255 B |
|
||||
| `X-WD-Max-Downloads` | 第 0 帧上带,条目的下载上限,0 为不限 |
|
||||
|
||||
raw 模式的读请求不需要令牌,但仍需要 `X-WD-Endpoint`。
|
||||
|
||||
WebSocket 无法设置头,令牌通过 `Sec-WebSocket-Protocol` 传递:`wd.v1, wd.role.<role>, wd.auth.<base64url(令牌)>, wd.ep.<base64url(S)>`,服务端回选 `wd.v1`。不要放进 URL,否则会被代理记录。
|
||||
|
||||
## 2. 权限矩阵
|
||||
|
||||
| 操作 | 需要 |
|
||||
|---|---|
|
||||
| 读日志、拉取条目 | `read`(enc)或知道 id(raw),并占有席位 |
|
||||
| 追加帧 | `write`(`members_can_upload` 为 true 时)或 `owner` |
|
||||
| 删除条目 | `owner`;`delete_policy=own` 时终端哈希等于条目作者;`anyone` 时任意已入席终端 |
|
||||
| 改设置、删频道、看和释放席位 | `owner` |
|
||||
|
||||
## 3. 接口
|
||||
|
||||
### `GET /api/v1/info`
|
||||
返回服务端能力:协议版本、限制、验证码配置(`captcha.provider`、`captcha.enabled`、`captcha.api_endpoint`,即 Cap widget 要连接的地址)、传输能力。验证码的 secret 不会出现在这里。`transports.http3` 只是部署层(反向代理或 Kestrel 配置)声明已启用 HTTP/3,服务端进程本身不内置 QUIC。
|
||||
|
||||
### `POST /api/v1/channels`
|
||||
创建频道。服务端启用验证码时需要 `X-WD-Cap`;验证服务不可用时返回 `503 captcha_unavailable`,与「用户没通过验证」(`428 captcha_required`)区分。
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "<channel_id>",
|
||||
"mode": "enc",
|
||||
"settings": { "seats": 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` 仅在成员可上传时提供。创建者的终端(`X-WD-Endpoint`)自动入席。返回 `201`,响应体与「读取频道信息」相同(`ChannelInfo`)。
|
||||
|
||||
### `GET /api/v1/channels/{id}`
|
||||
返回 `ChannelInfo`:设置、模式、席位占用、`oldest_seq`、`last_seq`、时间。**不占席位**,任意角色可调用,用于加入前预览和核对模式。
|
||||
|
||||
### `POST /api/v1/channels/{id}/seats`
|
||||
加入频道,占用一个席位。幂等:同一终端重复调用返回同一席位。席位已满返回 `403 seats_full`。除本接口和上面的「读取频道信息」外,其余频道接口都要求终端已入席,否则返回 `403 not_joined`。
|
||||
|
||||
### `PATCH /api/v1/channels/{id}/settings`
|
||||
所有者修改设置。`ttl_seconds` 的修改从现在起算。
|
||||
|
||||
### `DELETE /api/v1/channels/{id}`
|
||||
所有者删除整个频道和全部密文。
|
||||
|
||||
### `POST /api/v1/channels/{id}/frames`
|
||||
追加一帧。请求体是完整封装(`application/octet-stream`)。
|
||||
|
||||
| 响应 | 含义 |
|
||||
|---|---|
|
||||
| `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":"seats"}` 席位变更。
|
||||
|
||||
上传仍走 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` 视为续传不计数。条目尚未 FINAL 时,响应会随帧到达持续输出,直到 FINAL。达到下载上限后返回 `410`,并在 5 分钟宽限期后删除密文,宽限期内已开始的拉取可继续续传。
|
||||
|
||||
### `DELETE /api/v1/channels/{id}/items/{item_id}`
|
||||
按 `delete_policy` 授权后删除条目,并在日志中追加 tombstone。
|
||||
|
||||
### 席位
|
||||
- `GET /api/v1/channels/{id}/seats`:所有者查看(终端哈希、加密设备名、加入时间)。
|
||||
- `PUT /api/v1/channels/{id}/seats/me`:终端上传加密设备名(≤256 B)。
|
||||
- `DELETE /api/v1/channels/{id}/seats/{endpoint_hash}`:所有者释放席位。
|
||||
|
||||
## 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` 视为续传不计数;达到上限后返回 `410 gone`,5 分钟宽限期内已开始的拉取可继续续传。必须带 `X-WD-Endpoint`,同样会登记访客。
|
||||
|
||||
## 5. 错误
|
||||
|
||||
错误响应体是 JSON `{"error": "<code>"}`,不回显任何用户提交的内容。
|
||||
|
||||
| code | HTTP | 含义 |
|
||||
|---|---|---|
|
||||
| `bad_request` | 400 | 封装格式或参数错误 |
|
||||
| `unauthorized` | 401 | 令牌缺失或错误 |
|
||||
| `forbidden` | 403 | 角色不够 |
|
||||
| `not_joined` | 403 | 终端未入席,或席位已被所有者释放 |
|
||||
| `visitors_full` | 403 | 便捷分享的访客名额已满,且这个终端未登记 |
|
||||
| `sealed` | 409 | 便捷分享已封存,不能再修改;封存后再用所有者令牌发请求也是这个错误 |
|
||||
| `seats_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 | 限流 |
|
||||
Reference in new issue
Block a user