Files
WebDrop/docs/api.md
T

11 KiB

API v1

基础路径 /api/v1。所有请求和响应的二进制数据格式见 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)区分。

{
  "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。读取频道信息和 owner 管理接口不占席位;频道内容、席位信息/设备名等其余操作要求终端已入席,否则返回 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 只有同一终端此前对同一条目成功启动过 from=0 才能续传,否则返回 410 gone。条目尚未 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。没有 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

{
  "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

{ "manifest_item_id": "<b64url>" }

按 protocol §15.5 校验后封存。成功返回 200 ShareInfo(此时 state 为 sealed)。清单条目不存在、不是单帧非 GATED 的完整条目,或草稿里还有没传完的条目,返回 400 bad_request。已封存的分享再次调用返回 409 sealed。

GET /api/v1/shares/{id}

{
  "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 令牌缺失或错误
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 限流