feat(api): bind encrypted workspaces to accounts and devices
This commit is contained in:
1 parent
019b15041a
commit
c7d8ef15bc
49 files changed
+1904
-278
No files matched your search
+379
-14
@@ -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' }
|
||||
|
||||
Reference in new issue
Block a user