feat(api)!: fold devices into users and report member presence

Contract 7.0.0 (breaking). Devices become an attribute of the account;
members are managed per user, and only unauthenticated guests keep a
per-terminal registration.

- rename seats -> max_members, seats_used -> members_used,
  seats_full -> members_full, SeatJoined -> JoinResult, the `seats`
  stream control message -> `members`, and POST /channels/{id}/seats ->
  POST /channels/{id}/join (stored channel settings are migrated on start)
- drop per-terminal encrypted device labels and the seat list/release
  endpoints; add GET/DELETE /channels/{id}/guests for guest terminals
- authenticate account members by account session only: the terminal key
  is optional for logged-in accounts, no seat row is written for them,
  and gated-download grants are counted per user (per terminal for guests)
- GET /channels/{id}/members now returns online, devices and
  online_devices per account; presence is tracked in memory from active
  WebSocket/SSE subscriptions and is not pushed over the stream
- owner invitation list no longer returns accounts that already joined
This commit is contained in:
justme committed 2026-10-14 22:18:59 +08:00
1 parent 0fd2a269d9
commit d946fc5e8a
35 files changed
+517 -490

No files matched your search

+18 -18
View File
@@ -1,5 +1,5 @@
// Generated from backend DTOs; do not edit.
// backend-contract-sha256: ba787115471d5d33fb0c33c95af014b5cd0d535455bd4bea037cfdcc4f048828
// backend-contract-sha256: 54ea3f00fa672e0241efbb9fc8e7ad9e8fc68415ec124e08f70657c2d57d9ff9
export interface AccountChannelBody {
channel_id: string;
@@ -79,10 +79,10 @@ export interface ChannelInfo {
expires_at: number;
id: string;
last_seq: number;
members_used: number;
mode: ChannelMode;
oldest_seq: number;
password_required: boolean;
seats_used: number;
settings: ChannelSettings;
}
export type ChannelMode = 'enc' | 'raw';
@@ -93,9 +93,9 @@ export interface ChannelSettings {
file_max_downloads: number;
join_policy: JoinPolicy;
max_items: number;
max_members: number;
members_can_upload: boolean;
name?: string | null;
seats: number;
ttl_seconds: number;
ui_restrict_copy_download: boolean;
}
@@ -106,9 +106,9 @@ export interface ChannelSettingsPatch {
join_password?: string | null;
join_policy?: JoinPolicy | null;
max_items?: number | null;
max_members?: number | null;
members_can_upload?: boolean | null;
name?: string | null;
seats?: number | null;
ttl_seconds?: number | null;
ui_restrict_copy_download?: boolean | null;
}
@@ -171,6 +171,10 @@ export interface GoneLogBody {
error: string;
oldest_seq: number;
}
export interface GuestInfo {
endpoint_hash: string;
joined_at: number;
}
export interface InvitationBody {
channel_id: string;
created_at: number;
@@ -198,11 +202,19 @@ export type JoinPolicy = 'open' | 'invite';
export interface JoinRequest {
password?: string | null;
}
export interface JoinResult {
joined_at: number;
members_max: number;
members_used: number;
}
export interface KeyPackageRequest {
payload: string;
}
export interface MemberBody {
devices: number;
joined_at: number;
online: boolean;
online_devices: number;
role: string;
username: string;
}
@@ -260,18 +272,6 @@ export interface PublicKeyBody {
public_key: string;
signing_public_key?: string | null;
}
export interface SeatInfo {
account_name?: string | null;
endpoint_hash: string;
joined_at: number;
label?: string | null;
}
export interface SeatJoined {
endpoint_hash: string;
joined_at: number;
seats_max: number;
seats_used: number;
}
export interface ServerInfoBody {
captcha: CaptchaBody;
choices: SettingChoicesBody;
@@ -295,8 +295,8 @@ export interface LimitsBody {
max_channel_bytes: number;
max_frames_per_item: number;
max_items: number;
max_members: number;
max_payload_bytes: number;
max_seats: number;
max_ttl_seconds: number;
max_visitors: number;
max_workspace_bytes: number;
@@ -305,8 +305,8 @@ export interface LimitsBody {
}
export interface SettingChoicesBody {
file_max_downloads: number[];
max_members: number[];
max_visitors: number[];
seats: number[];
ttl_seconds: number[];
}
export interface ShareInfo {