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., wd.auth., wd.ep.`, 服务端回选 `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 }