Phase 0 of the collaboration plan: - Remove BroadcastToAll; task, task list and workspace folder events now only reach users who can see the resource. Task list events carry only the list ID, clients reload with their own permissions. - Reject unauthenticated WebSocket/SSE connections (401). Every room join (URL room, JOIN_ROOM, SSE rooms) is checked via a room authorizer; /events/rooms requires login and only accepts the caller's own sseId. - Reorder rejects tasks the caller cannot access or edit (IDOR). - Lift the server WriteTimeout for SSE streams so they are not cut after 30s. - Fix folder share removal sending DOC_DELETED to the share ID. - Tests for all of the above; docs and flaw list updated. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
4.2 KiB
Echtzeit: WebSocket und SSE
Der Server verteilt Änderungen als JSON-Ereignisse an verbundene Clients. Die Ereignisse entstehen nach einer REST-Änderung; es gibt keinen Schreibkanal über die Verbindung. Implementierung: hub.go, Client: api.ts.
Keine CRDTs, keine Sperren, keine Cursor-Presence. Frühere Fassungen dieser Doku beschrieben DocRooms,
clientId-Echo-Schutz und Yjs; das existiert nicht (mehr).
Transporte
| Transport | Endpunkt | Hinweise |
|---|---|---|
| WebSocket | GET /api/v1/ws (auch /ws/lists/{id}, /ws/spaces/{id}) |
Ping alle 30 s, Lesefrist 60 s, Nachrichten bis 4 MB |
| SSE | GET /api/v1/events |
Kommentar-Ping alle 15 s, Header X-Accel-Buffering: no |
Der Client versucht zuerst WebSocket. Gelingt der Aufbau nicht innerhalb von 2,5 s oder bricht die Verbindung unsauber ab, wechselt er auf SSE und bleibt dabei (Firmen-Proxys). Dort liefert der Server SSE_CONNECTED mit einer sseId; Räume werden über POST /api/v1/events/rooms (sseId, room, action: join/leave) abonniert.
Authentifizierung: Token als ?token=<JWT|API-Key> oder Authorization-Header. Ohne gültiges Token antworten /ws und /events mit 401. Das Token wird nur beim Verbinden geprüft (FLAW-22).
Räume: Jeder Beitritt (Raum in der URL, JOIN_ROOM, SSE-Startparameter rooms/docId, POST /events/rooms) wird geprüft: doc:<id> nur mit Lesezugriff auf die Notiz, sonst muss die ID eine lesbare geteilte Liste sein. Nicht erlaubte Räume werden ignoriert, POST /events/rooms antwortet dann mit 403. SSE-Räume kann nur der Besitzer der jeweiligen sseId ändern.
Nachrichtenformat
{ "event": "TASK_UPDATED", "listId": "", "payload": { … }, "timestamp": "2026-10-27T12:00:00Z" }
Client → Server versteht der Server nur: {"event":"JOIN_ROOM","room":"<id>"} und {"event":"LEAVE_ROOM","room":"<id>"}. Räume heißen doc:<docId> für Notizen oder <listId> für geteilte Listen.
Zustellung
| Kanal | Wer erhält es |
|---|---|
Benutzer-Kanal (BroadcastToUser) |
alle Verbindungen eines Benutzers |
Raum (BroadcastRoom, Broadcast) |
Verbindungen im Raum |
Mehrere Benutzer (BroadcastToUsers) |
alle Verbindungen einer berechneten Benutzergruppe, z. B. Eigentümer und Mitglieder einer Aufgabenliste |
Ereignisse
| Ereignis | Auslöser | Kanal |
|---|---|---|
TASK_CREATED |
POST /inbox, Webhook |
Eigentümer und Benutzer der Liste |
TASK_UPDATED |
PATCH /thoughts/{id}, Triage |
Eigentümer und Benutzer der alten und neuen Liste |
TASK_DELETED |
DELETE /thoughts/{id} |
Eigentümer und Benutzer der Liste |
TASK_REORDERED |
Aufgaben umsortieren | Benutzer aller betroffenen Listen |
TASK_LIST_UPDATED / TASK_LIST_DELETED |
Liste ändern, (un)teilen, löschen; Payload nur {id}, Clients laden ihre Listen neu |
Benutzer vor und nach der Änderung |
DOC_CREATED, DOC_UPDATED, DOC_DELETED |
Notiz anlegen/ändern/löschen, Freigaben | berechtigte Benutzer |
DOC_BLOCKS_PATCHED |
PATCH /documents/{id}/blocks (docId, updatedBy, upserted, deletedIds, blockOrder) |
Raum doc:<id> und berechtigte Benutzer |
WORKSPACE_FOLDERS_UPDATED |
Ordner/Mounts ändern | Benutzer selbst; bei geteilten Ordnern Eigentümer und alle Beteiligten |
LIST_CREATED, LIST_UPDATED, LIST_DELETED, LIST_SHARED, LIST_UNSHARED, LIST_CLEARED |
Listenverwaltung | Benutzer bzw. Raum der Liste |
ITEM_CREATED, ITEM_UPDATED, ITEM_TOGGLED, ITEM_DELETED, ITEM_REORDERED |
Listeneinträge | Raum der Liste |
USER_PRESENCE |
Verbindung auf-/abgebaut (onlineCount) |
Raum |
SSE_CONNECTED |
Nur SSE, direkt nach Verbindung | Verbindung |
Verhalten im Frontend
- Die globale Verbindung (ein Singleton) leitet jede Nachricht als Browser-Ereignis
mindsift:wsweiter; Ansichten (TasksView,WorkspaceView,SharedListsView) hören darauf und laden bzw. patchen ihren Zustand. - Nach einem Abbruch verbindet sich der Client mit Verzögerung neu.
- Weil Notizen nur vom Eigentümer geschrieben werden, dient
DOC_BLOCKS_PATCHEDvor allem dazu, Lesenden und weiteren Geräten des Eigentümers Änderungen zu zeigen. Es gibt keine Konfliktauflösung (letzter Schreiber gewinnt).