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

+71 -83
View File
@@ -1,6 +1,6 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"x-backend-contract-sha256": "ba787115471d5d33fb0c33c95af014b5cd0d535455bd4bea037cfdcc4f048828",
"x-backend-contract-sha256": "54ea3f00fa672e0241efbb9fc8e7ad9e8fc68415ec124e08f70657c2d57d9ff9",
"$defs": {
"AccountChannelBody": {
"type": "object",
@@ -322,6 +322,9 @@
"last_seq": {
"type": "integer"
},
"members_used": {
"type": "integer"
},
"mode": {
"$ref": "#/$defs/ChannelMode"
},
@@ -331,9 +334,6 @@
"password_required": {
"type": "boolean"
},
"seats_used": {
"type": "integer"
},
"settings": {
"$ref": "#/$defs/ChannelSettings"
}
@@ -345,10 +345,10 @@
"expires_at",
"id",
"last_seq",
"members_used",
"mode",
"oldest_seq",
"password_required",
"seats_used",
"settings"
]
},
@@ -377,6 +377,9 @@
"max_items": {
"type": "integer"
},
"max_members": {
"type": "integer"
},
"members_can_upload": {
"type": "boolean"
},
@@ -390,9 +393,6 @@
}
]
},
"seats": {
"type": "integer"
},
"ttl_seconds": {
"type": "integer"
},
@@ -406,8 +406,8 @@
"file_max_downloads",
"join_policy",
"max_items",
"max_members",
"members_can_upload",
"seats",
"ttl_seconds",
"ui_restrict_copy_download"
]
@@ -475,6 +475,16 @@
}
]
},
"max_members": {
"anyOf": [
{
"type": "integer"
},
{
"type": "null"
}
]
},
"members_can_upload": {
"anyOf": [
{
@@ -495,16 +505,6 @@
}
]
},
"seats": {
"anyOf": [
{
"type": "integer"
},
{
"type": "null"
}
]
},
"ttl_seconds": {
"anyOf": [
{
@@ -841,6 +841,21 @@
"oldest_seq"
]
},
"GuestInfo": {
"type": "object",
"properties": {
"endpoint_hash": {
"type": "string"
},
"joined_at": {
"type": "integer"
}
},
"required": [
"endpoint_hash",
"joined_at"
]
},
"InvitationBody": {
"type": "object",
"properties": {
@@ -963,6 +978,25 @@
},
"required": []
},
"JoinResult": {
"type": "object",
"properties": {
"joined_at": {
"type": "integer"
},
"members_max": {
"type": "integer"
},
"members_used": {
"type": "integer"
}
},
"required": [
"joined_at",
"members_max",
"members_used"
]
},
"KeyPackageRequest": {
"type": "object",
"properties": {
@@ -977,9 +1011,18 @@
"MemberBody": {
"type": "object",
"properties": {
"devices": {
"type": "integer"
},
"joined_at": {
"type": "integer"
},
"online": {
"type": "boolean"
},
"online_devices": {
"type": "integer"
},
"role": {
"type": "string"
},
@@ -988,7 +1031,10 @@
}
},
"required": [
"devices",
"joined_at",
"online",
"online_devices",
"role",
"username"
]
@@ -1270,64 +1316,6 @@
"public_key"
]
},
"SeatInfo": {
"type": "object",
"properties": {
"account_name": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
]
},
"endpoint_hash": {
"type": "string"
},
"joined_at": {
"type": "integer"
},
"label": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
]
}
},
"required": [
"endpoint_hash",
"joined_at"
]
},
"SeatJoined": {
"type": "object",
"properties": {
"endpoint_hash": {
"type": "string"
},
"joined_at": {
"type": "integer"
},
"seats_max": {
"type": "integer"
},
"seats_used": {
"type": "integer"
}
},
"required": [
"endpoint_hash",
"joined_at",
"seats_max",
"seats_used"
]
},
"ServerInfoBody": {
"type": "object",
"properties": {
@@ -1419,10 +1407,10 @@
"max_items": {
"type": "integer"
},
"max_payload_bytes": {
"max_members": {
"type": "integer"
},
"max_seats": {
"max_payload_bytes": {
"type": "integer"
},
"max_ttl_seconds": {
@@ -1445,8 +1433,8 @@
"max_channel_bytes",
"max_frames_per_item",
"max_items",
"max_members",
"max_payload_bytes",
"max_seats",
"max_ttl_seconds",
"max_visitors",
"max_workspace_bytes",
@@ -1463,13 +1451,13 @@
"type": "integer"
}
},
"max_visitors": {
"max_members": {
"type": "array",
"items": {
"type": "integer"
}
},
"seats": {
"max_visitors": {
"type": "array",
"items": {
"type": "integer"
@@ -1484,8 +1472,8 @@
},
"required": [
"file_max_downloads",
"max_members",
"max_visitors",
"seats",
"ttl_seconds"
]
},