861 lines
31 KiB
YAML
861 lines
31 KiB
YAML
openapi: 3.1.0
|
|
info:
|
|
title: WebDrop API
|
|
version: 1.0.0
|
|
servers:
|
|
- url: /api/v1
|
|
|
|
tags:
|
|
- name: server
|
|
- name: channel
|
|
- name: seat
|
|
- name: frame
|
|
- name: item
|
|
- name: share
|
|
|
|
paths:
|
|
/info:
|
|
get:
|
|
tags: [server]
|
|
security: []
|
|
summary: 服务端能力与限制
|
|
responses:
|
|
'200':
|
|
description: OK
|
|
content:
|
|
application/json:
|
|
schema: { $ref: '#/components/schemas/ServerInfo' }
|
|
|
|
/channels:
|
|
post:
|
|
tags: [channel]
|
|
security: []
|
|
summary: 创建频道
|
|
description: |
|
|
服务端启用验证码时需要 `X-WD-Cap`(Cap 的 token,见 protocol §11),并需要终端头 `X-WD-Endpoint`。
|
|
创建者的终端自动入席,占一个席位。
|
|
parameters:
|
|
- { $ref: '#/components/parameters/Captcha' }
|
|
- { $ref: '#/components/parameters/Endpoint' }
|
|
requestBody:
|
|
required: true
|
|
content:
|
|
application/json:
|
|
schema: { $ref: '#/components/schemas/CreateChannelRequest' }
|
|
responses:
|
|
'201':
|
|
description: 已创建
|
|
content:
|
|
application/json:
|
|
schema: { $ref: '#/components/schemas/ChannelInfo' }
|
|
'400': { $ref: '#/components/responses/Error' }
|
|
'409': { $ref: '#/components/responses/Error' }
|
|
'428': { $ref: '#/components/responses/Error' }
|
|
'429': { $ref: '#/components/responses/Error' }
|
|
'503': { $ref: '#/components/responses/Error' }
|
|
|
|
/channels/{id}:
|
|
parameters:
|
|
- { $ref: '#/components/parameters/ChannelId' }
|
|
get:
|
|
tags: [channel]
|
|
summary: 读取频道信息(不占席位)
|
|
description: 任意角色可调用。用于加入前预览席位占用和核对模式。
|
|
security: [{ bearer: [] }]
|
|
parameters:
|
|
- { $ref: '#/components/parameters/Role' }
|
|
responses:
|
|
'200':
|
|
description: OK
|
|
content:
|
|
application/json:
|
|
schema: { $ref: '#/components/schemas/ChannelInfo' }
|
|
'401': { $ref: '#/components/responses/Error' }
|
|
'404': { $ref: '#/components/responses/Error' }
|
|
delete:
|
|
tags: [channel]
|
|
summary: 删除整个频道及全部密文(所有者)
|
|
security: [{ bearer: [] }]
|
|
parameters:
|
|
- { $ref: '#/components/parameters/RoleOwner' }
|
|
responses:
|
|
'204': { description: 已删除 }
|
|
'401': { $ref: '#/components/responses/Error' }
|
|
'403': { $ref: '#/components/responses/Error' }
|
|
'404': { $ref: '#/components/responses/Error' }
|
|
|
|
/channels/{id}/settings:
|
|
patch:
|
|
tags: [channel]
|
|
summary: 修改设置(所有者)
|
|
description: |
|
|
字段均可选,只修改提供的字段。`ttl_seconds` 的变更从现在起算。
|
|
成功后向订阅者推送 `{"t":"settings"}`。
|
|
security: [{ bearer: [] }]
|
|
parameters:
|
|
- { $ref: '#/components/parameters/ChannelId' }
|
|
- { $ref: '#/components/parameters/RoleOwner' }
|
|
requestBody:
|
|
required: true
|
|
content:
|
|
application/json:
|
|
schema: { $ref: '#/components/schemas/ChannelSettingsPatch' }
|
|
responses:
|
|
'200':
|
|
description: OK
|
|
content:
|
|
application/json:
|
|
schema: { $ref: '#/components/schemas/ChannelInfo' }
|
|
'400': { $ref: '#/components/responses/Error' }
|
|
'403': { $ref: '#/components/responses/Error' }
|
|
|
|
/channels/{id}/seats:
|
|
parameters:
|
|
- { $ref: '#/components/parameters/ChannelId' }
|
|
post:
|
|
tags: [seat]
|
|
summary: 加入频道(占用席位)
|
|
description: |
|
|
幂等:同一终端重复调用返回同一席位,不重复占用。
|
|
席位已满返回 403 `seats_full`。
|
|
除本接口和「读取频道信息」外,其余频道接口都要求终端已入席,否则 403 `not_joined`。
|
|
security: [{ bearer: [] }]
|
|
parameters:
|
|
- { $ref: '#/components/parameters/Role' }
|
|
- { $ref: '#/components/parameters/Endpoint' }
|
|
responses:
|
|
'200':
|
|
description: 已入席
|
|
content:
|
|
application/json:
|
|
schema: { $ref: '#/components/schemas/SeatJoined' }
|
|
'401': { $ref: '#/components/responses/Error' }
|
|
'403': { $ref: '#/components/responses/Error' }
|
|
'404': { $ref: '#/components/responses/Error' }
|
|
get:
|
|
tags: [seat]
|
|
summary: 查看席位列表(所有者)
|
|
security: [{ bearer: [] }]
|
|
parameters:
|
|
- { $ref: '#/components/parameters/RoleOwner' }
|
|
responses:
|
|
'200':
|
|
description: OK
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: array
|
|
items: { $ref: '#/components/schemas/Seat' }
|
|
'403': { $ref: '#/components/responses/Error' }
|
|
|
|
/channels/{id}/seats/me:
|
|
put:
|
|
tags: [seat]
|
|
summary: 终端上传自己的加密设备名
|
|
description: 请求体是用 `K_enc` 加密后的设备名字节,≤256 B。服务端不解析。
|
|
security: [{ bearer: [] }]
|
|
parameters:
|
|
- { $ref: '#/components/parameters/ChannelId' }
|
|
- { $ref: '#/components/parameters/Role' }
|
|
- { $ref: '#/components/parameters/Endpoint' }
|
|
requestBody:
|
|
required: true
|
|
content:
|
|
application/octet-stream:
|
|
schema: { type: string, format: binary, maxLength: 256 }
|
|
responses:
|
|
'204': { description: 已保存 }
|
|
'403': { $ref: '#/components/responses/Error' }
|
|
'413': { $ref: '#/components/responses/Error' }
|
|
|
|
/channels/{id}/seats/{endpointHash}:
|
|
delete:
|
|
tags: [seat]
|
|
summary: 释放席位(所有者)
|
|
description: 被释放的终端后续请求返回 `not_joined`。向订阅者推送 `{"t":"seats"}`。
|
|
security: [{ bearer: [] }]
|
|
parameters:
|
|
- { $ref: '#/components/parameters/ChannelId' }
|
|
- name: endpointHash
|
|
in: path
|
|
required: true
|
|
description: 终端标识,即 `base64url(SHA-256(S))`
|
|
schema: { $ref: '#/components/schemas/Base64Url' }
|
|
- { $ref: '#/components/parameters/RoleOwner' }
|
|
responses:
|
|
'204': { description: 已释放 }
|
|
'403': { $ref: '#/components/responses/Error' }
|
|
'404': { $ref: '#/components/responses/Error' }
|
|
|
|
/channels/{id}/frames:
|
|
post:
|
|
tags: [frame]
|
|
summary: 追加一帧
|
|
description: |
|
|
请求体是完整的帧封装(26 字节头 + 负载),格式见 `protocol.md` §4。
|
|
幂等键为 `(channel_id, item_id, frame_index)`。内容相同则 200 返回原 seq,内容不同则 409。
|
|
第 0 帧可带 `X-WD-Max-Downloads` 和(raw 模式)`X-WD-Label`。
|
|
security: [{ bearer: [] }]
|
|
parameters:
|
|
- { $ref: '#/components/parameters/ChannelId' }
|
|
- { $ref: '#/components/parameters/RoleWriter' }
|
|
- { $ref: '#/components/parameters/Endpoint' }
|
|
- name: X-WD-Max-Downloads
|
|
in: header
|
|
required: false
|
|
schema: { type: integer, minimum: 0 }
|
|
description: 条目的下载上限,0 为不限。只对 GATED 条目生效。
|
|
- name: X-WD-Label
|
|
in: header
|
|
required: false
|
|
schema: { type: string, maxLength: 255 }
|
|
description: raw 模式的条目标签,明文。
|
|
requestBody:
|
|
required: true
|
|
content:
|
|
application/octet-stream:
|
|
schema: { type: string, format: binary }
|
|
responses:
|
|
'201':
|
|
description: 新追加
|
|
content:
|
|
application/json:
|
|
schema: { $ref: '#/components/schemas/AppendResult' }
|
|
'200':
|
|
description: 幂等重发
|
|
content:
|
|
application/json:
|
|
schema: { $ref: '#/components/schemas/AppendResult' }
|
|
'400': { $ref: '#/components/responses/Error' }
|
|
'403': { $ref: '#/components/responses/Error' }
|
|
'409': { $ref: '#/components/responses/Error' }
|
|
'413': { $ref: '#/components/responses/Error' }
|
|
'429': { $ref: '#/components/responses/Error' }
|
|
|
|
/channels/{id}/log:
|
|
get:
|
|
tags: [frame]
|
|
summary: 按 seq 拉取日志(长轮询、续传、全量同步)
|
|
description: |
|
|
返回日志记录的线格式拼接(`protocol.md` §8):每条 `seq(u64) ‖ kind(u8) ‖ body`。
|
|
`wait>0` 时无新记录会挂起,最长 25 秒,超时返回空体。
|
|
满足 `0 < from < oldest_seq` 时返回 410 并带 `oldest_seq`,客户端必须从 `from=0` 全量同步并丢弃本地已不存在的条目(不要从 `oldest_seq` 读起)。`from=0` 永远允许。
|
|
security: [{ bearer: [] }]
|
|
parameters:
|
|
- { $ref: '#/components/parameters/ChannelId' }
|
|
- { $ref: '#/components/parameters/Role' }
|
|
- { $ref: '#/components/parameters/Endpoint' }
|
|
- name: from
|
|
in: query
|
|
required: false
|
|
schema: { type: integer, format: int64, minimum: 0, default: 0 }
|
|
- name: limit
|
|
in: query
|
|
required: false
|
|
schema: { type: integer, minimum: 1, maximum: 1000, default: 200 }
|
|
- name: wait
|
|
in: query
|
|
required: false
|
|
schema: { type: integer, minimum: 0, maximum: 25, default: 0 }
|
|
description: 长轮询等待秒数
|
|
responses:
|
|
'200':
|
|
description: 记录拼接,可为空
|
|
headers:
|
|
X-WD-Last-Seq:
|
|
description: 频道当前最新 seq
|
|
schema: { type: integer, format: int64 }
|
|
content:
|
|
application/octet-stream:
|
|
schema: { type: string, format: binary }
|
|
'403': { $ref: '#/components/responses/Error' }
|
|
'410':
|
|
description: 0 < from < oldest_seq,需要从 0 全量同步
|
|
content:
|
|
application/json:
|
|
schema: { $ref: '#/components/schemas/GoneLog' }
|
|
|
|
/channels/{id}/ws:
|
|
get:
|
|
tags: [frame]
|
|
security: []
|
|
summary: WebSocket 订阅(降级链第一级)
|
|
description: |
|
|
升级为 WebSocket。浏览器不能设置头,凭据通过子协议传递:
|
|
`Sec-WebSocket-Protocol: wd.v1, wd.role.<role>, wd.auth.<base64url(令牌)>, wd.ep.<base64url(S)>`,
|
|
服务端回选 `wd.v1`。不要把令牌放在 URL 里。
|
|
|
|
**服务端 → 客户端**
|
|
- 二进制消息:一条日志记录(`seq ‖ kind ‖ body`)。
|
|
- 文本消息(JSON):`{"t":"hb","seq":N}` 心跳(每 20 秒);`{"t":"settings"}` 设置变更;`{"t":"seats"}` 席位变更。
|
|
|
|
客户端 45 秒收不到任何消息应判定死链,以指数退避(1~30 秒)加抖动重连,并带 `from=last_seq+1`。
|
|
上传不走 WebSocket,仍用 `POST /frames`。
|
|
parameters:
|
|
- { $ref: '#/components/parameters/ChannelId' }
|
|
- name: from
|
|
in: query
|
|
required: false
|
|
schema: { type: integer, format: int64, minimum: 0, default: 0 }
|
|
responses:
|
|
'101': { description: 已升级 }
|
|
'403': { $ref: '#/components/responses/Error' }
|
|
'410':
|
|
description: 0 < from < oldest_seq,需要从 0 全量同步
|
|
content:
|
|
application/json:
|
|
schema: { $ref: '#/components/schemas/GoneLog' }
|
|
|
|
/channels/{id}/events:
|
|
get:
|
|
tags: [frame]
|
|
summary: SSE 订阅(降级链第二级)
|
|
description: |
|
|
`text/event-stream`。每个事件的 `id:` 是 seq,`data:` 是 base64url 的日志记录;
|
|
控制消息与 WebSocket 的文本消息相同,事件名为 `control`。
|
|
`EventSource` 不能设置头,前端应使用 `fetch` 流式读取并带头部鉴权。
|
|
续传用 `from` 或 `Last-Event-ID`。
|
|
security: [{ bearer: [] }]
|
|
parameters:
|
|
- { $ref: '#/components/parameters/ChannelId' }
|
|
- { $ref: '#/components/parameters/Role' }
|
|
- { $ref: '#/components/parameters/Endpoint' }
|
|
- name: from
|
|
in: query
|
|
required: false
|
|
schema: { type: integer, format: int64, minimum: 0, default: 0 }
|
|
responses:
|
|
'200':
|
|
description: 事件流
|
|
content:
|
|
text/event-stream:
|
|
schema: { type: string }
|
|
'403': { $ref: '#/components/responses/Error' }
|
|
'410':
|
|
description: 0 < from < oldest_seq,需要从 0 全量同步
|
|
content:
|
|
application/json:
|
|
schema: { $ref: '#/components/schemas/GoneLog' }
|
|
|
|
/channels/{id}/items/{itemId}/frames:
|
|
get:
|
|
tags: [item]
|
|
summary: 拉取条目的帧(GATED 条目的取数方式)
|
|
description: |
|
|
返回封装拼接,每帧自带 `payload_len`,可流式解析。条目尚未 FINAL 时响应持续输出直到 FINAL。
|
|
`from=0` 计一次下载;`from>0` 视为续传,不计数。
|
|
达到下载上限后返回 410 `gone`,5 分钟宽限期后删除密文,宽限期内已开始的拉取可继续续传。
|
|
security: [{ bearer: [] }]
|
|
parameters:
|
|
- { $ref: '#/components/parameters/ChannelId' }
|
|
- { $ref: '#/components/parameters/ItemId' }
|
|
- { $ref: '#/components/parameters/Role' }
|
|
- { $ref: '#/components/parameters/Endpoint' }
|
|
- name: from
|
|
in: query
|
|
required: false
|
|
schema: { type: integer, format: int64, minimum: 0, default: 0 }
|
|
description: 起始 frame_index
|
|
responses:
|
|
'200':
|
|
description: 封装拼接
|
|
content:
|
|
application/octet-stream:
|
|
schema: { type: string, format: binary }
|
|
'403': { $ref: '#/components/responses/Error' }
|
|
'404': { $ref: '#/components/responses/Error' }
|
|
'410': { $ref: '#/components/responses/Error' }
|
|
|
|
/channels/{id}/items/{itemId}:
|
|
delete:
|
|
tags: [item]
|
|
summary: 删除条目
|
|
description: |
|
|
按频道的 `delete_policy` 授权:`owner_only` 仅所有者;`own` 时所有者或条目作者;`anyone` 时任意已入席终端。
|
|
成功后在日志追加 tombstone(`reason=0`)。
|
|
security: [{ bearer: [] }]
|
|
parameters:
|
|
- { $ref: '#/components/parameters/ChannelId' }
|
|
- { $ref: '#/components/parameters/ItemId' }
|
|
- { $ref: '#/components/parameters/Role' }
|
|
- { $ref: '#/components/parameters/Endpoint' }
|
|
responses:
|
|
'204': { description: 已删除 }
|
|
'403': { $ref: '#/components/responses/Error' }
|
|
'404': { $ref: '#/components/responses/Error' }
|
|
|
|
/shares:
|
|
post:
|
|
tags: [share]
|
|
security: []
|
|
summary: 创建便捷分享草稿
|
|
description: 启用验证码时需携带 Cap token。创建者 endpoint 可选,创建者不计入访客数。
|
|
parameters:
|
|
- { $ref: '#/components/parameters/Captcha' }
|
|
- { $ref: '#/components/parameters/ShareEndpoint' }
|
|
requestBody:
|
|
required: true
|
|
content:
|
|
application/json:
|
|
schema: { $ref: '#/components/schemas/CreateShareRequest' }
|
|
responses:
|
|
'201':
|
|
description: 已创建草稿
|
|
content:
|
|
application/json:
|
|
schema: { $ref: '#/components/schemas/ShareInfo' }
|
|
'400': { $ref: '#/components/responses/Error' }
|
|
'409': { $ref: '#/components/responses/Error' }
|
|
'428': { $ref: '#/components/responses/Error' }
|
|
'429': { $ref: '#/components/responses/Error' }
|
|
'503': { $ref: '#/components/responses/Error' }
|
|
|
|
/shares/{id}:
|
|
parameters:
|
|
- { $ref: '#/components/parameters/ShareId' }
|
|
get:
|
|
tags: [share]
|
|
summary: 读取分享信息
|
|
description: 已封存时使用 read;草稿仅 owner 可读,read 对草稿返回 not_found。
|
|
security: [{ bearer: [] }]
|
|
parameters:
|
|
- { $ref: '#/components/parameters/Role' }
|
|
responses:
|
|
'200':
|
|
description: 分享与条目下载状态
|
|
content:
|
|
application/json:
|
|
schema: { $ref: '#/components/schemas/ShareInfo' }
|
|
'401': { $ref: '#/components/responses/Error' }
|
|
'404': { $ref: '#/components/responses/Error' }
|
|
delete:
|
|
tags: [share]
|
|
summary: 删除草稿
|
|
security: [{ bearer: [] }]
|
|
parameters:
|
|
- { $ref: '#/components/parameters/RoleOwner' }
|
|
responses:
|
|
'204': { description: 草稿已删除 }
|
|
'403': { $ref: '#/components/responses/Error' }
|
|
'404': { $ref: '#/components/responses/Error' }
|
|
'409': { $ref: '#/components/responses/Error' }
|
|
|
|
/shares/{id}/settings:
|
|
patch:
|
|
tags: [share]
|
|
summary: 修改草稿设置
|
|
security: [{ bearer: [] }]
|
|
parameters:
|
|
- { $ref: '#/components/parameters/ShareId' }
|
|
- { $ref: '#/components/parameters/RoleOwner' }
|
|
requestBody:
|
|
required: true
|
|
content:
|
|
application/json:
|
|
schema: { $ref: '#/components/schemas/ShareSettingsPatch' }
|
|
responses:
|
|
'200':
|
|
description: 设置已更新
|
|
content:
|
|
application/json:
|
|
schema: { $ref: '#/components/schemas/ShareInfo' }
|
|
'400': { $ref: '#/components/responses/Error' }
|
|
'403': { $ref: '#/components/responses/Error' }
|
|
'409': { $ref: '#/components/responses/Error' }
|
|
|
|
/shares/{id}/frames:
|
|
post:
|
|
tags: [share]
|
|
summary: 向草稿追加加密帧
|
|
description: 不支持 SNAPSHOT。第 0 帧可用 X-WD-Max-Downloads 覆盖默认下载上限。响应的 seq 是 frame_index 确认值,不是日志游标。
|
|
security: [{ bearer: [] }]
|
|
parameters:
|
|
- { $ref: '#/components/parameters/ShareId' }
|
|
- { $ref: '#/components/parameters/RoleOwner' }
|
|
- name: X-WD-Max-Downloads
|
|
in: header
|
|
required: false
|
|
schema: { type: integer, format: int64, minimum: 0 }
|
|
requestBody:
|
|
required: true
|
|
content:
|
|
application/octet-stream:
|
|
schema: { type: string, format: binary }
|
|
responses:
|
|
'201':
|
|
description: 新帧已追加
|
|
content:
|
|
application/json:
|
|
schema: { $ref: '#/components/schemas/AppendResult' }
|
|
'200':
|
|
description: 幂等重发
|
|
content:
|
|
application/json:
|
|
schema: { $ref: '#/components/schemas/AppendResult' }
|
|
'400': { $ref: '#/components/responses/Error' }
|
|
'403': { $ref: '#/components/responses/Error' }
|
|
'409': { $ref: '#/components/responses/Error' }
|
|
'413': { $ref: '#/components/responses/Error' }
|
|
|
|
/shares/{id}/items/{itemId}:
|
|
delete:
|
|
tags: [share]
|
|
summary: 从草稿移除条目
|
|
security: [{ bearer: [] }]
|
|
parameters:
|
|
- { $ref: '#/components/parameters/ShareId' }
|
|
- { $ref: '#/components/parameters/ItemId' }
|
|
- { $ref: '#/components/parameters/RoleOwner' }
|
|
responses:
|
|
'204': { description: 条目已删除 }
|
|
'403': { $ref: '#/components/responses/Error' }
|
|
'404': { $ref: '#/components/responses/Error' }
|
|
'409': { $ref: '#/components/responses/Error' }
|
|
|
|
/shares/{id}/finish:
|
|
post:
|
|
tags: [share]
|
|
summary: 封存分享
|
|
description: 验证 manifest 是单帧非 GATED 完整条目,且草稿内所有条目完整;成功后永久禁用写权限。
|
|
security: [{ bearer: [] }]
|
|
parameters:
|
|
- { $ref: '#/components/parameters/ShareId' }
|
|
- { $ref: '#/components/parameters/RoleOwner' }
|
|
requestBody:
|
|
required: true
|
|
content:
|
|
application/json:
|
|
schema: { $ref: '#/components/schemas/FinishShareRequest' }
|
|
responses:
|
|
'200':
|
|
description: 分享已封存
|
|
content:
|
|
application/json:
|
|
schema: { $ref: '#/components/schemas/ShareInfo' }
|
|
'400': { $ref: '#/components/responses/Error' }
|
|
'409': { $ref: '#/components/responses/Error' }
|
|
|
|
/shares/{id}/manifest:
|
|
get:
|
|
tags: [share]
|
|
summary: 获取加密清单帧
|
|
security: [{ bearer: [] }]
|
|
parameters:
|
|
- { $ref: '#/components/parameters/ShareId' }
|
|
- { $ref: '#/components/parameters/Role' }
|
|
- { $ref: '#/components/parameters/Endpoint' }
|
|
responses:
|
|
'200':
|
|
description: 完整封装的二进制清单
|
|
content:
|
|
application/octet-stream:
|
|
schema: { type: string, format: binary }
|
|
'403': { $ref: '#/components/responses/Error' }
|
|
'404': { $ref: '#/components/responses/Error' }
|
|
'410': { $ref: '#/components/responses/Error' }
|
|
|
|
/shares/{id}/items/{itemId}/frames:
|
|
get:
|
|
tags: [share]
|
|
summary: 下载封存分享中的文件帧
|
|
description: from=0 计一次下载;from>0 为续传。首次成功读取会登记访客终端。
|
|
security: [{ bearer: [] }]
|
|
parameters:
|
|
- { $ref: '#/components/parameters/ShareId' }
|
|
- { $ref: '#/components/parameters/ItemId' }
|
|
- { $ref: '#/components/parameters/Role' }
|
|
- { $ref: '#/components/parameters/Endpoint' }
|
|
- name: from
|
|
in: query
|
|
required: false
|
|
schema: { type: integer, format: int64, minimum: 0, default: 0 }
|
|
responses:
|
|
'200':
|
|
description: 连续封装帧拼接
|
|
content:
|
|
application/octet-stream:
|
|
schema: { type: string, format: binary }
|
|
'403': { $ref: '#/components/responses/Error' }
|
|
'404': { $ref: '#/components/responses/Error' }
|
|
'410': { $ref: '#/components/responses/Error' }
|
|
|
|
components:
|
|
securitySchemes:
|
|
bearer:
|
|
type: http
|
|
scheme: bearer
|
|
description: |
|
|
令牌的 base64url(`T_read` / `T_write` / `T_owner`),与 `X-WD-Role` 配套。
|
|
raw 模式的读请求不需要令牌。
|
|
|
|
parameters:
|
|
ChannelId:
|
|
name: id
|
|
in: path
|
|
required: true
|
|
description: 频道 id,16 字节 base64url
|
|
schema: { $ref: '#/components/schemas/Base64Url' }
|
|
ShareId:
|
|
name: id
|
|
in: path
|
|
required: true
|
|
description: 便捷分享 id,16 字节 base64url
|
|
schema: { $ref: '#/components/schemas/Base64Url' }
|
|
ItemId:
|
|
name: itemId
|
|
in: path
|
|
required: true
|
|
description: 条目 id,16 字节 base64url
|
|
schema: { $ref: '#/components/schemas/Base64Url' }
|
|
Role:
|
|
name: X-WD-Role
|
|
in: header
|
|
required: true
|
|
description: 令牌所属角色
|
|
schema: { type: string, enum: [read, write, owner] }
|
|
RoleWriter:
|
|
name: X-WD-Role
|
|
in: header
|
|
required: true
|
|
schema: { type: string, enum: [write, owner] }
|
|
RoleOwner:
|
|
name: X-WD-Role
|
|
in: header
|
|
required: true
|
|
schema: { type: string, enum: [owner] }
|
|
Endpoint:
|
|
name: X-WD-Endpoint
|
|
in: header
|
|
required: true
|
|
description: 终端密钥 `S` 的 base64url。每个频道每个终端各生成一个,仅存本地。
|
|
schema: { $ref: '#/components/schemas/Base64Url' }
|
|
ShareEndpoint:
|
|
name: X-WD-Endpoint
|
|
in: header
|
|
required: false
|
|
description: 创建者终端密钥 S 的 base64url,可选,不占访客名额。
|
|
schema: { $ref: '#/components/schemas/Base64Url' }
|
|
Captcha:
|
|
name: X-WD-Cap
|
|
in: header
|
|
required: false
|
|
description: Cap 验证码 token。服务端 `/info` 的 `captcha.enabled` 为 true 时必填;token 一次有效。
|
|
schema: { type: string, maxLength: 2048 }
|
|
|
|
responses:
|
|
Error:
|
|
description: 错误。响应体不回显任何用户提交的内容。
|
|
content:
|
|
application/json:
|
|
schema: { $ref: '#/components/schemas/Error' }
|
|
|
|
schemas:
|
|
Base64Url:
|
|
type: string
|
|
pattern: '^[A-Za-z0-9_-]+$'
|
|
description: base64url,无填充
|
|
|
|
ErrorCode:
|
|
type: string
|
|
enum:
|
|
- bad_request
|
|
- unauthorized
|
|
- forbidden
|
|
- not_joined
|
|
- seats_full
|
|
- mode_mismatch
|
|
- not_found
|
|
- conflict
|
|
- gone
|
|
- too_large
|
|
- captcha_required
|
|
- captcha_unavailable
|
|
- rate_limited
|
|
- visitors_full
|
|
- sealed
|
|
|
|
Error:
|
|
type: object
|
|
required: [error]
|
|
properties:
|
|
error: { $ref: '#/components/schemas/ErrorCode' }
|
|
|
|
GoneLog:
|
|
type: object
|
|
required: [error, oldest_seq]
|
|
properties:
|
|
error: { type: string, const: gone }
|
|
oldest_seq: { type: integer, format: int64, description: "清理水位 + 1;0 < from < oldest_seq 的续传需要从 0 全量同步" }
|
|
|
|
ServerInfo:
|
|
type: object
|
|
required: [protocol, limits, captcha, transports]
|
|
properties:
|
|
protocol: { type: integer, const: 1 }
|
|
captcha:
|
|
type: object
|
|
required: [provider, enabled]
|
|
properties:
|
|
provider: { type: string, enum: [none, cap] }
|
|
enabled: { type: boolean }
|
|
api_endpoint:
|
|
type: string
|
|
description: Cap widget 要连接的地址(data-cap-api-endpoint),仅在启用时出现
|
|
transports:
|
|
type: object
|
|
properties:
|
|
websocket: { type: boolean }
|
|
sse: { type: boolean }
|
|
http3: { type: boolean }
|
|
limits:
|
|
type: object
|
|
properties:
|
|
max_payload_bytes: { type: integer }
|
|
max_frames_per_item: { type: integer }
|
|
max_channel_bytes: { type: integer, format: int64 }
|
|
max_seats: { type: integer }
|
|
max_ttl_seconds: { type: integer }
|
|
|
|
ChannelMode:
|
|
type: string
|
|
enum: [enc, raw]
|
|
|
|
DeletePolicy:
|
|
type: string
|
|
enum: [owner_only, own, anyone]
|
|
|
|
ChannelSettings:
|
|
type: object
|
|
required: [seats, members_can_upload, delete_policy, ttl_seconds, max_items, file_max_downloads, ui_restrict_copy_download]
|
|
properties:
|
|
seats: { type: integer, minimum: 1, default: 2 }
|
|
members_can_upload: { type: boolean, default: true }
|
|
delete_policy: { $ref: '#/components/schemas/DeletePolicy' }
|
|
ttl_seconds: { type: integer, minimum: 60, maximum: 604800, default: 86400 }
|
|
max_items: { type: integer, minimum: 1, default: 1000 }
|
|
file_max_downloads: { type: integer, minimum: 0, default: 10, description: "0 为不限,仅对 GATED 条目生效" }
|
|
ui_restrict_copy_download: { type: boolean, default: false, description: "仅供前端展示,服务端不强制" }
|
|
|
|
ChannelSettingsPatch:
|
|
type: object
|
|
description: ChannelSettings 的全部字段均可选
|
|
properties:
|
|
seats: { type: integer, minimum: 1 }
|
|
members_can_upload: { type: boolean }
|
|
delete_policy: { $ref: '#/components/schemas/DeletePolicy' }
|
|
ttl_seconds: { type: integer, minimum: 60, maximum: 604800 }
|
|
max_items: { type: integer, minimum: 1 }
|
|
file_max_downloads: { type: integer, minimum: 0 }
|
|
ui_restrict_copy_download: { type: boolean }
|
|
|
|
CreateChannelRequest:
|
|
type: object
|
|
required: [id, mode, settings, owner_hash]
|
|
properties:
|
|
id: { $ref: '#/components/schemas/Base64Url' }
|
|
mode: { $ref: '#/components/schemas/ChannelMode' }
|
|
settings: { $ref: '#/components/schemas/ChannelSettings' }
|
|
read_hash:
|
|
$ref: '#/components/schemas/Base64Url'
|
|
description: SHA-256(T_read),enc 模式必填,raw 模式省略
|
|
write_hash:
|
|
$ref: '#/components/schemas/Base64Url'
|
|
description: SHA-256(T_write 或 W),仅在成员可上传时提供
|
|
owner_hash:
|
|
$ref: '#/components/schemas/Base64Url'
|
|
description: SHA-256(T_owner)
|
|
|
|
ShareSettings:
|
|
type: object
|
|
required: [ttl_seconds, max_visitors, file_max_downloads]
|
|
properties:
|
|
ttl_seconds: { type: integer, minimum: 60, maximum: 604800, default: 86400 }
|
|
max_visitors: { type: integer, minimum: 0, maximum: 10000, default: 0, description: 0 为不限 }
|
|
file_max_downloads: { type: integer, minimum: 0, default: 10, description: 0 为不限 }
|
|
|
|
ShareSettingsPatch:
|
|
type: object
|
|
description: ShareSettings 的字段均可选。
|
|
properties:
|
|
ttl_seconds: { type: integer, minimum: 60, maximum: 604800 }
|
|
max_visitors: { type: integer, minimum: 0, maximum: 10000 }
|
|
file_max_downloads: { type: integer, minimum: 0 }
|
|
|
|
CreateShareRequest:
|
|
type: object
|
|
required: [id, read_hash, owner_hash]
|
|
properties:
|
|
id: { $ref: '#/components/schemas/Base64Url' }
|
|
read_hash: { $ref: '#/components/schemas/Base64Url', description: SHA-256(T_read) }
|
|
owner_hash: { $ref: '#/components/schemas/Base64Url', description: SHA-256(T_owner) }
|
|
settings: { $ref: '#/components/schemas/ShareSettings' }
|
|
|
|
FinishShareRequest:
|
|
type: object
|
|
required: [manifest_item_id]
|
|
properties:
|
|
manifest_item_id: { $ref: '#/components/schemas/Base64Url' }
|
|
|
|
ShareItemInfo:
|
|
type: object
|
|
required: [item_id, bytes, frames, max_downloads, downloads_left]
|
|
properties:
|
|
item_id: { $ref: '#/components/schemas/Base64Url' }
|
|
bytes: { type: integer, format: int64 }
|
|
frames: { type: integer }
|
|
max_downloads: { type: integer, format: int64, description: 0 为不限 }
|
|
downloads_left: { type: [integer, 'null'], format: int64, description: "null 表示不限" }
|
|
|
|
ShareInfo:
|
|
type: object
|
|
required: [id, state, expires_at, created_at, settings, items]
|
|
properties:
|
|
id: { $ref: '#/components/schemas/Base64Url' }
|
|
state: { type: string, enum: [draft, sealed] }
|
|
expires_at: { type: integer, format: int64 }
|
|
created_at: { type: integer, format: int64 }
|
|
sealed_at: { type: [integer, 'null'], format: int64 }
|
|
manifest_item_id: { type: [string, 'null'], description: 清单条目 id }
|
|
settings: { $ref: '#/components/schemas/ShareSettings' }
|
|
items:
|
|
type: array
|
|
items: { $ref: '#/components/schemas/ShareItemInfo' }
|
|
|
|
ChannelInfo:
|
|
type: object
|
|
required: [id, mode, settings, seats_used, oldest_seq, last_seq, created_at, expires_at]
|
|
properties:
|
|
id: { $ref: '#/components/schemas/Base64Url' }
|
|
mode: { $ref: '#/components/schemas/ChannelMode' }
|
|
settings: { $ref: '#/components/schemas/ChannelSettings' }
|
|
seats_used: { type: integer }
|
|
oldest_seq: { type: integer, format: int64 }
|
|
last_seq: { type: integer, format: int64 }
|
|
created_at: { type: integer, format: int64 }
|
|
expires_at: { type: integer, format: int64 }
|
|
|
|
SeatJoined:
|
|
type: object
|
|
required: [endpoint_hash, joined_at, seats_used, seats_max]
|
|
properties:
|
|
endpoint_hash: { $ref: '#/components/schemas/Base64Url' }
|
|
joined_at: { type: integer, format: int64 }
|
|
seats_used: { type: integer }
|
|
seats_max: { type: integer }
|
|
|
|
Seat:
|
|
type: object
|
|
required: [endpoint_hash, joined_at]
|
|
properties:
|
|
endpoint_hash: { $ref: '#/components/schemas/Base64Url' }
|
|
joined_at: { type: integer, format: int64 }
|
|
label:
|
|
type: [string, 'null']
|
|
description: 加密的设备名(base64url),用 K_enc 解密;未设置为 null
|
|
|
|
AppendResult:
|
|
type: object
|
|
required: [seq]
|
|
properties:
|
|
seq: { type: integer, format: int64 }
|