feat(api): bind encrypted workspaces to accounts and devices

This commit is contained in:
justme committed 2026-10-13 01:38:19 +08:00
1 parent 019b15041a
commit c7d8ef15bc
49 files changed
+1904 -278

No files matched your search

+379 -14
View File
@@ -1,7 +1,7 @@
openapi: 3.1.0
info:
title: WebDrop API
version: 1.0.0
version: 2.1.0
servers:
- url: /api/v1
@@ -12,8 +12,79 @@ tags:
- name: frame
- name: item
- name: share
- name: account
paths:
/accounts/channels:
get:
tags: [account]
security: [{ accountBearer: [] }]
summary: 领取当前账号已加入频道的加密密钥包
responses:
'200':
description: 仅返回已加入频道,不占设备席位
content:
application/json:
schema: { type: array, items: { $ref: '#/components/schemas/AccountChannel' } }
'401': { $ref: '#/components/responses/Error' }
/accounts/devices:
get:
tags: [account]
security: [{ accountBearer: [] }]
summary: 读取当前账号的设备会话
responses:
'200':
description: 设备列表
content:
application/json:
schema: { type: array, items: { $ref: '#/components/schemas/AccountDevice' } }
'401': { $ref: '#/components/responses/Error' }
/accounts/devices/{id}:
parameters:
- { name: id, in: path, required: true, schema: { type: string, format: uuid } }
patch:
tags: [account]
security: [{ accountBearer: [] }]
summary: 重命名本账号设备
requestBody:
required: true
content:
application/json:
schema:
type: object
required: [name]
properties: { name: { type: string, minLength: 1, maxLength: 80 } }
responses:
'204': { description: 已更新 }
'400': { $ref: '#/components/responses/Error' }
'401': { $ref: '#/components/responses/Error' }
delete:
tags: [account]
security: [{ accountBearer: [] }]
summary: 撤销该设备全部 sessions 与频道席位,保留账号成员资格
responses:
'204': { description: 已撤销 }
'401': { $ref: '#/components/responses/Error' }
/channels/{id}/key-package:
put:
tags: [channel]
security: [{ bearer: [] }]
summary: 频道主补齐给自己账号的加密密钥包
parameters:
- { $ref: '#/components/parameters/ChannelId' }
- { $ref: '#/components/parameters/Role' }
requestBody:
required: true
content:
application/json:
schema:
type: object
required: [payload]
properties: { payload: { type: string, minLength: 1, maxLength: 16384 } }
responses:
'204': { description: 已保存 }
'401': { $ref: '#/components/responses/Error' }
'403': { $ref: '#/components/responses/Error' }
/info:
get:
tags: [server]
@@ -26,14 +97,183 @@ paths:
application/json:
schema: { $ref: '#/components/schemas/ServerInfo' }
/accounts:
post:
tags: [account]
security: []
summary: 创建账号
description: 仅接收口令 verifier 和客户端加密保险库,不接收密码。
requestBody:
required: true
content:
application/json:
schema: { $ref: '#/components/schemas/AccountRegister' }
responses:
'204': { description: 已创建 }
'400': { $ref: '#/components/responses/Error' }
'409': { $ref: '#/components/responses/Error' }
'429': { $ref: '#/components/responses/Error' }
/accounts/salt:
post:
tags: [account]
security: []
summary: 查询账号口令派生 salt
requestBody:
required: true
content:
application/json:
schema: { $ref: '#/components/schemas/AccountSaltRequest' }
responses:
'200':
description: 客户端 PBKDF2 salt
content:
application/json:
schema: { $ref: '#/components/schemas/AccountSalt' }
'400': { $ref: '#/components/responses/Error' }
/accounts/session:
post:
tags: [account]
security: []
summary: 验证客户端派生凭据并创建 session
requestBody:
required: true
content:
application/json:
schema: { $ref: '#/components/schemas/AccountSessionRequest' }
responses:
'200':
description: 已登录
content:
application/json:
schema: { $ref: '#/components/schemas/AccountSession' }
'401': { $ref: '#/components/responses/Error' }
'400': { $ref: '#/components/responses/Error' }
delete:
tags: [account]
security: [{ accountBearer: [] }]
summary: 撤销当前账号 session
responses:
'204': { description: 已撤销 }
/accounts/vault:
get:
tags: [account]
security: [{ accountBearer: [] }]
summary: 读取客户端加密保险库
responses:
'200':
description: 当前保险库和 revision
content:
application/json:
schema: { $ref: '#/components/schemas/AccountVault' }
'401': { $ref: '#/components/responses/Error' }
put:
tags: [account]
security: [{ accountBearer: [] }]
summary: 以 revision 乐观并发更新保险库
requestBody:
required: true
content:
application/json:
schema: { $ref: '#/components/schemas/AccountVault' }
responses:
'200':
description: 已更新
content:
application/json:
schema: { $ref: '#/components/schemas/AccountVault' }
'401': { $ref: '#/components/responses/Error' }
'409': { $ref: '#/components/responses/Error' }
/accounts/users/{username}/key:
get:
tags: [account]
security: [{ accountBearer: [] }]
summary: 查询被邀请用户公钥
parameters:
- { name: username, in: path, required: true, schema: { type: string } }
responses:
'200':
description: 公钥
content:
application/json:
schema: { $ref: '#/components/schemas/PublicKey' }
'404': { $ref: '#/components/responses/Error' }
/accounts/invitations:
get:
tags: [account]
security: [{ accountBearer: [] }]
summary: 当前账号收到的邀请
responses:
'200':
description: 邀请列表
content:
application/json:
schema: { type: array, items: { $ref: '#/components/schemas/ChannelInvitation' } }
/channels/{id}/members:
get:
tags: [channel]
security: [{ bearer: [] }]
summary: 读取频道内账号成员及角色
parameters:
- { $ref: '#/components/parameters/ChannelId' }
- { $ref: '#/components/parameters/Role' }
- { $ref: '#/components/parameters/Endpoint' }
responses:
'200':
description: 成员列表
content:
application/json:
schema: { type: array, items: { $ref: '#/components/schemas/ChannelMember' } }
/channels/{id}/invitations:
parameters:
- { $ref: '#/components/parameters/ChannelId' }
- { $ref: '#/components/parameters/RoleOwner' }
get:
tags: [channel]
security: [{ bearer: [], accountBearer: [] }]
summary: 频道主查询邀请
responses:
'200':
description: 邀请列表
content:
application/json:
schema: { type: array, items: { $ref: '#/components/schemas/ChannelInvitation' } }
post:
tags: [channel]
security: [{ bearer: [], accountBearer: [] }]
summary: 邀请已注册用户
requestBody:
required: true
content:
application/json:
schema: { $ref: '#/components/schemas/InvitationRequest' }
responses:
'204': { description: 已邀请 }
'404': { $ref: '#/components/responses/Error' }
/channels/{id}/invitations/{username}:
delete:
tags: [channel]
security: [{ bearer: [], accountBearer: [] }]
summary: 撤销邀请和成员授权并移除其设备
parameters:
- { $ref: '#/components/parameters/ChannelId' }
- { $ref: '#/components/parameters/RoleOwner' }
- { name: username, in: path, required: true, schema: { type: string } }
responses:
'204': { description: 已移除 }
'403': { $ref: '#/components/responses/Error' }
/channels:
post:
tags: [channel]
security: []
security: [{ accountBearer: [] }]
summary: 创建频道
description: |
服务端启用验证码时需要 `X-WD-Cap`(Cap 的 token,见 protocol §11),并需要终端头 `X-WD-Endpoint`。
创建者的终端自动入席,占一个席位。
创建者的账号自动成为该频道 owner,其终端自动入席。
parameters:
- { $ref: '#/components/parameters/Captcha' }
- { $ref: '#/components/parameters/Endpoint' }
@@ -170,6 +410,30 @@ paths:
'403': { $ref: '#/components/responses/Error' }
'413': { $ref: '#/components/responses/Error' }
/channels/{id}/seats/{endpointHash}/label:
put:
tags: [seat]
summary: 所有者重命名设备
description: 请求体是设备名称(≤256 B);enc 模式以目标 endpoint hash 作为 AAD 加密,raw 模式为明文。
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' }
requestBody:
required: true
content:
application/octet-stream:
schema: { type: string, format: binary, maxLength: 256 }
responses:
'204': { description: 名称已更新 }
'403': { $ref: '#/components/responses/Error' }
'404': { $ref: '#/components/responses/Error' }
'413': { $ref: '#/components/responses/Error' }
/channels/{id}/seats/{endpointHash}:
delete:
tags: [seat]
@@ -196,7 +460,7 @@ paths:
description: |
请求体是完整的帧封装(26 字节头 + 负载),格式见 `protocol.md` §4。
幂等键为 `(channel_id, item_id, frame_index)`。内容相同则 200 返回原 seq,内容不同则 409。
第 0 帧可带 `X-WD-Max-Downloads` 和(raw 模式)`X-WD-Label`。
第 0 帧可带 `X-WD-Max-Downloads`;名称与线程字段统一在条目元数据中。
security: [{ bearer: [] }]
parameters:
- { $ref: '#/components/parameters/ChannelId' }
@@ -207,11 +471,6 @@ paths:
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:
@@ -389,7 +648,7 @@ paths:
/shares:
post:
tags: [share]
security: []
security: [{ accountBearer: [] }]
summary: 创建便捷分享草稿
description: 启用验证码时需携带 Cap token。创建者 endpoint 可选,创建者不计入访客数。
parameters:
@@ -583,6 +842,11 @@ paths:
components:
securitySchemes:
accountBearer:
type: apiKey
in: header
name: X-WD-Account
description: WebDrop 账号登录后颁发的随机 session token。
bearer:
type: http
scheme: bearer
@@ -652,6 +916,88 @@ components:
schema: { $ref: '#/components/schemas/Error' }
schemas:
AccountChannel:
type: object
required: [channel_id, role, payload, created_at]
properties:
channel_id: { $ref: '#/components/schemas/Base64Url' }
role: { type: string, enum: [owner, member] }
payload: { type: string, minLength: 1, maxLength: 16384 }
created_at: { type: integer, format: int64 }
AccountDevice:
type: object
required: [id, name, created_at, last_seen, current]
properties:
id: { type: string, format: uuid }
name: { type: string, minLength: 1, maxLength: 80 }
created_at: { type: integer, format: int64 }
last_seen: { type: integer, format: int64 }
current: { type: boolean }
PublicKey:
type: object
required: [public_key]
properties:
public_key: { type: string, maxLength: 1024 }
ChannelInvitation:
type: object
required: [channel_id, username, payload, created_at]
properties:
channel_id: { $ref: '#/components/schemas/Base64Url' }
username: { type: string }
payload: { type: string, maxLength: 16384, description: 客户端加密的成员链接 }
created_at: { type: integer, format: int64 }
InvitationRequest:
type: object
required: [username, payload]
properties:
username: { type: string }
payload: { type: string, maxLength: 16384 }
ChannelMember:
type: object
required: [username, role, joined_at]
properties:
username: { type: string }
role: { type: string, enum: [owner, member] }
joined_at: { type: integer, format: int64 }
AccountRegister:
type: object
required: [username, salt, verifier, vault, public_key]
properties:
username: { type: string, minLength: 3, maxLength: 32, pattern: '^[A-Za-z0-9_-]+$' }
salt: { type: string, contentEncoding: base64, minLength: 24, maxLength: 24 }
verifier: { type: string, contentEncoding: base64, minLength: 44, maxLength: 44 }
vault: { type: string, contentEncoding: base64, maxLength: 1733336 }
public_key: { type: string, maxLength: 1024, description: P-256 ECDH 公钥 JWK 的 JSON 字符串 }
AccountSaltRequest:
type: object
required: [username]
properties:
username: { type: string, minLength: 3, maxLength: 32 }
AccountSalt:
type: object
required: [salt]
properties:
salt: { type: string, contentEncoding: base64 }
AccountSessionRequest:
type: object
required: [username, verifier]
properties:
device_id: { type: string, format: uuid }
device_name: { type: string, minLength: 1, maxLength: 80 }
username: { type: string }
verifier: { type: string, contentEncoding: base64 }
AccountSession:
type: object
required: [username, token]
properties:
username: { type: string }
token: { type: string, contentEncoding: base64 }
AccountVault:
type: object
required: [revision, vault]
properties:
revision: { type: integer, format: int64, minimum: 0 }
vault: { type: string, contentEncoding: base64, maxLength: 1733336 }
Base64Url:
type: string
pattern: '^[A-Za-z0-9_-]+$'
@@ -675,6 +1021,13 @@ components:
- rate_limited
- visitors_full
- sealed
- account_required
- account_invalid
- invitation_required
- channel_password_required
- channel_password_invalid
- username_unavailable
- vault_conflict
Error:
type: object
@@ -691,10 +1044,10 @@ components:
ServerInfo:
type: object
required: [protocol, contract_version, limits, captcha, transports]
properties:
protocol: { type: integer, const: 1 }
contract_version: { type: string, example: 1.0.0, description: API 契约的 SemVer。破坏性变更递增主版本。 }
required: [protocol, contract_version, limits, captcha, transports]
properties:
protocol: { type: integer, const: 1 }
contract_version: { type: string, example: 2.0.0, description: API 契约的 SemVer。破坏性变更递增主版本。 }
captcha:
type: object
required: [provider, enabled]
@@ -731,6 +1084,9 @@ components:
type: object
required: [seats, members_can_upload, delete_policy, ttl_seconds, max_items, file_max_downloads, ui_restrict_copy_download]
properties:
join_policy: { type: string, enum: [open, invite], default: open }
allow_guests: { type: boolean, default: true }
name: { type: string, maxLength: 48, description: "共享频道名,所有成员和服务端可见" }
seats: { type: integer, minimum: 1, default: 2 }
members_can_upload: { type: boolean, default: true }
delete_policy: { $ref: '#/components/schemas/DeletePolicy' }
@@ -743,6 +1099,10 @@ components:
type: object
description: ChannelSettings 的全部字段均可选
properties:
join_policy: { type: string, enum: [open, invite] }
allow_guests: { type: boolean }
join_password: { type: string, maxLength: 256, description: 至少 8 字符;空字符串清除;省略保持原值 }
name: { type: string, maxLength: 48, description: "更新共享频道名;空字符串清除名称" }
seats: { type: integer, minimum: 1 }
members_can_upload: { type: boolean }
delete_policy: { $ref: '#/components/schemas/DeletePolicy' }
@@ -755,6 +1115,8 @@ components:
type: object
required: [id, mode, settings, owner_hash]
properties:
key_package: { type: string, minLength: 1, maxLength: 16384, description: 用创建者账号公钥封装的 owner 凭据 }
join_password: { type: string, minLength: 8, maxLength: 256 }
id: { $ref: '#/components/schemas/Base64Url' }
mode: { $ref: '#/components/schemas/ChannelMode' }
settings: { $ref: '#/components/schemas/ChannelSettings' }
@@ -803,6 +1165,7 @@ components:
type: object
required: [item_id, bytes, frames, max_downloads, downloads_left]
properties:
complete: { type: boolean }
item_id: { $ref: '#/components/schemas/Base64Url' }
bytes: { type: integer, format: int64 }
frames: { type: integer }
@@ -828,6 +1191,8 @@ components:
type: object
required: [id, mode, settings, seats_used, oldest_seq, last_seq, created_at, expires_at]
properties:
password_required: { type: boolean }
creator_account: { type: string }
id: { $ref: '#/components/schemas/Base64Url' }
mode: { $ref: '#/components/schemas/ChannelMode' }
settings: { $ref: '#/components/schemas/ChannelSettings' }