Commit Graph
123 Commits
Author SHA1 Message Date
Jannis Braun 26a4925032 feat(server): reclassify undeliverable targeted-peer as no_recipient failure (#18)
sendFederatedCallStart now treats a 200-with-undeliverable-messageId as
a peer-failure instead of unconditional success. Feeds the existing
failures[] array and terminal-determination machinery from #16.
New sendFederatedCallStartForTest export mirrors the existing
handleDm*ForTest pattern. TDD — three tests cover single-peer terminal
no_recipient, group-DM mixed delivered+undeliverable non-terminal, and
the happy-path (empty undeliverable → no event).

Also hardens sendCallRelay's response parse: validates undeliverable
is an Array and entries are well-shaped, logs protocol drift at warn/debug
rather than silently falling back to old-peer semantics.
2026-04-24 21:11:11 +02:00
Jannis Braun 7d2137b6d4 feat(server): sendCallRelay surfaces undeliverable messageIds (#18)
CallRelayResult success arm gains undeliverable: string[]. sendCallRelay
parses FederationRelayResponse.undeliverable (when present) and returns
the messageIds so sendFederatedCallStart can reclassify per-peer results.
Old peers that omit the field → empty array → today's behavior.
TDD — three tests cover old-peer, new-peer-with-undeliverable, and 5xx paths.
2026-04-24 21:05:28 +02:00
Jannis Braun 3b61380a1e feat(server): ConnectionManager.evictFederatedCallsForHost (TDD) 2026-04-24 00:37:44 +02:00
Jannis Braun 1e59e7012c fix(server): scope accept-rollback terminal to the acceptor only
Code-review catch: the Path-2 accept-rollback previously emitted
dm_call_undeliverable { terminal: true } via sendToFederatedCallUsers,
which broadcasts to every ringedUserIds entry. In a group DM this
would prematurely tear down non-accepting ringees whose own accept /
reject / timeout paths should govern their state. Switch to
sendToUser(acceptorId) so only the acting user gets the terminal
signal. Reorder the clearFederatedCall to happen before the emit so a
concurrent end-handler sees a cleared entry (clearFederatedCall is
idempotent). Spec updated, test extended to assert the scoping with a
two-ringee group-DM fixture.
2026-04-23 23:38:31 +02:00
Jannis Braun f6252b8ce1 feat(server): fan dm_call_end out on host ring timeout 2026-04-23 23:19:42 +02:00
Jannis Braun 6cd7728f3e feat(server): fanOutCallEvent returns failures; surface to host-side caller 2026-04-23 23:14:50 +02:00
Jannis Braun 3cb80d110d feat(server): aggregate Path-1 call fan-out failures and surface to originator 2026-04-23 23:12:55 +02:00
Jannis Braun 13241345de feat(server): surface dm_call_end relay failure to originator (TDD) 2026-04-23 23:11:20 +02:00
Jannis Braun 5170d316ba feat(server): surface dm_call_reject relay failure to rejector (TDD) 2026-04-23 23:10:18 +02:00
Jannis Braun 07c5b0e7de feat(server): surface dm_call_accept relay failure to acceptor (TDD) 2026-04-23 23:09:18 +02:00
Jannis Braun 06e1ed92e7 refactor(server): add buildFailureFromResult + CallFanoutFailure types 2026-04-23 23:07:07 +02:00
Jannis Braun 44a44163af polish(server): consolidate federation_peers queries, narrow targetedPeers map, use return values from Promise.all to drop non-null assertion 2026-04-21 14:00:29 +02:00
Jannis Braun f83c2af357 feat(server): aggregate call-start failures into dm_call_undeliverable
sendFederatedCallStart now collects per-targeted-peer results and emits
a single dm_call_undeliverable event to the caller when any targeted
peer relay fails. Destroys the local ring room when no plausible
recipient remains (no targeted success + no connected local ringee).

LiveKit pre-flight also emits via this path with reason
'livekit_unavailable' instead of a silent console.warn, closing the
60s hang for unconfigured instances.

Guards against phantom toasts when the caller cancels mid-race by
checking getRoom() before emitting.
2026-04-21 13:53:34 +02:00
Jannis Braun 3d8709d20a feat: real-time Federation panel updates via WS events
Added federation_peers_changed (no-payload signal) broadcast from every
peer state mutation, and federation_approval_request_received when a new
approval request is queued. Client subscribes via onFederationPeersChanged
callback registry. FederationPanel and PendingApprovals debounce-refetch
on any event. sendToAdmins helper broadcasts only to admin users.
2026-04-20 18:28:10 +02:00
Jannis Braun 0aec716d4c fix: gate all client DM events on active S2S peer status
The client's direct WS connection to remote instances (via Connections)
delivered DM events independently of S2S peering. Added activePeerOrigins
allowlist to ready payload — all DM event handlers now silently drop
events from non-home origins without an active peer. This prevents
notifications, sounds, previews, typing indicators, calls, and channel
updates from instances where peering was revoked or never established.
2026-04-20 17:05:57 +02:00
Jannis Braun 33c45e3184 feat: add awaitingApprovalPeerOrigins and pendingApprovalCount to ready payload 2026-04-20 15:03:29 +02:00
Jannis Braun 5f700c1a51 feat: expose autoAcceptPeering in admin settings and add rejectedPeerOrigins to ready payload 2026-04-09 13:53:26 +02:00
Jannis Braun 5b1c847e57 fix: exclude acting user from federated call end/reject broadcast
sendToFederatedCallUsers sent dm_call_ended/rejected back to the user
who initiated the action. They already disconnected in their click
handler — the redundant event triggered disconnectFn() again, causing
connect and disconnect sounds to play simultaneously.

Added excludeUserId parameter to sendToFederatedCallUsers, used in
handleDmCallEnd and handleDmCallReject Path 2.
2026-04-08 14:49:29 +02:00
Jannis Braun 9e7e068df7 chore: remove federated call diagnostic logging 2026-04-08 14:44:27 +02:00
Jannis Braun 782f5d280d debug: log federated call relay params 2026-04-08 14:38:05 +02:00
Jannis Braun 27514e1596 fix: use wss:// LiveKit URL in federated call relay, not https://
sendFederatedCallStart was sending `https://${domain}/livekit` as the
LiveKit URL. The LiveKit SDK requires `wss://` for WebSocket connections.
The caller (local) worked because it gets the URL from config.livekit.url
(wss://). The federated acceptor failed because it used the relay URL
(https://) — the SDK can't connect over HTTPS.

Now uses config.livekit.url directly, falling back to wss:// if unset.
2026-04-08 14:30:48 +02:00
Jannis Braun aae0b1a74e fix: comprehensive client-side session management for federated DM calls
Four fixes addressing the full state management problem:

1. Passive ready handler: no longer auto-connects to LiveKit on
   page refresh. Prevents identity conflicts when the same user
   has multiple sessions fighting for one LiveKit identity slot.
   The user must re-accept to join; state is shown but not acted on.

2. SoundController sync guard: incomingCallLoading/outgoingCallLoading
   refs prevent multiple playSound calls during async audio load.
   If call is cancelled while sound loads, stops it immediately on
   completion. Eliminates the "5 ringtones at once" bug.

3. Host dm_call_accepted broadcasts now include federatedCallId so
   all clients (including remote instances) can match the event.

4. Removed all diagnostic console.log statements.
2026-04-08 14:12:45 +02:00
Jannis Braun 86fe713a7c fix: cross-instance event contamination in federated DM calls
Root cause: sendToFederatedCallUsers used sendToDmMembers when dmChannelId
was set, which broadcast to ALL DM members including the caller's replicated
stub. The caller's multi-instance WS received dm_call_accepted with the
REMOTE instance's dmChannelId, causing token request for a non-existent
channel (403) and preventing the caller from connecting.

Fix 1: sendToFederatedCallUsers always uses ringedUserIds (exact recipients)
instead of sendToDmMembers (all members including caller stub).

Fix 2: dm_call_accepted handler only sets activeDmCall if the client is the
caller (wasOutgoingCall) or already connected to LiveKit. Other instances of
the same user just clear ringing without entering stuck "Connecting..." state.
2026-04-08 13:50:25 +02:00
Jannis Braun 70dbe04e6d debug: add diagnostic logging to DM call accept flow 2026-04-08 13:29:45 +02:00
Jannis Braun 5265117d29 fix: three root causes for federated DM call failures
1. Accept/reject/end from remote instance now resolves federatedId
   to local dmChannelId via DB lookup, so the host can find its
   VoiceRoom when the event arrives with only a federatedCallId.
   Previously silently failed with "No active call" error.

2. Batch all dm_call_incoming state updates into a single
   useVoiceStore.setState() call. Prevents SoundController from
   starting multiple ringtone instances (async playSound guard
   race when 4 separate set() calls each triggered the subscription).

3. Always overwrite callOrigin/federatedCallId (with null if absent)
   on dm_call_incoming. Prevents stale values from a previous
   federated call routing local accepts to the wrong instance.
2026-04-08 12:53:58 +02:00
Jannis Braun 07edb25d12 feat: update DM call handlers and processors for federatedCallId lookup 2026-04-08 03:18:25 +02:00
Jannis Braun 1690295db5 feat: universal call relay — broadcast to all active federation peers 2026-04-08 03:11:56 +02:00
Jannis Braun 2538bb5e4c fix: remove federated user gate for DM call events
Federated users can now send dm_call_start/accept/reject/end.
Handlers already branch correctly for local vs federated call state.
2026-04-08 03:04:51 +02:00
Jannis Braun 365c64794d refactor: re-key FederatedCallEntry by federatedId, add ringedUserIds 2026-04-08 03:03:44 +02:00
Jannis Braun 23f2863bdc fix: seed DM read states for federated users on first access
When a federated user first accesses DMs on a remote instance,
all channels appear unread because no read_states rows exist.
Seed missing read states to the latest message during the ready
payload build. The S2S relay keeps things in sync going forward.
2026-04-07 21:38:11 +02:00
Jannis Braun 1ada46baff fix: relay mark_unread to peers, fix docs and timestamp consistency
- Add queueReadStateRelay call in handleMarkUnread (skip '0' sentinel)
- Fix double Date.now() in queueReadStateRelay (use single const)
- Fix federation.md: read state relay uses outbox (not fire-and-forget),
  correct payload schema to match implementation
2026-04-07 20:08:31 +02:00
Jannis Braun 32ccd9c410 feat: outbound S2S read state relay
Queue read_state_update events when users ack DM messages on
channels with a federatedId. Translates local message IDs to
federation coordinates using sourceInstance/sourceMessageId.
2026-04-07 19:49:52 +02:00
Jannis Braun 662143bf08 feat: lift DM gates for federated users
Remove requireLocalUser from DM routes, include DMs in federated
ready payload, replace blanket dm_* WS gate with call-only blocklist.
DM calls remain gated (separate scope).
2026-04-07 19:48:07 +02:00
Jannis Braun 5e778f400b fix(server): use two-step MAX(created_at) for DM last message in ready payload
Aligns the ready payload query with the GET /api/dm approach:
get MAX(created_at) per channel first, then fetch the actual
message rows. Avoids issues with federated relay messages whose
local snowflake IDs don't match chronological order.
2026-04-03 00:04:40 +02:00
Jannis Braun 6e5792ff72 feat(server): include attachment metadata in ready payload DM lastMessage 2026-04-02 17:51:37 +02:00
Jannis Braun 1d81838868 feat(server): filter DM read states from ready payload for federated users 2026-04-02 11:33:59 +02:00
Jannis Braun b1ba7b35d2 feat(server): skip DM channel queries in ready payload for federated users 2026-04-02 11:33:51 +02:00
Jannis Braun 54bce681f9 feat(server): add isFederated flag and visibleChannelIdSet to ready payload builder 2026-04-02 11:33:43 +02:00
Jannis Braun c94e1644a8 feat(server): add WS federation gating for DM events and shared handlers 2026-04-02 10:51:20 +02:00
Jannis Braun 32d9f16a15 feat(server): cache isFederated flag at WS auth, pass to event handler 2026-04-02 10:47:52 +02:00
Jannis Braun b3011fb3da feat(federation): add fire-and-forget S2S typing relay
sendTypingRelay() mirrors sendCallRelay() — direct POST to peers,
no outbox, no retry. Uses federatedId for cross-instance channel
identification. Wired into handleDmTypingStart() for typing_start
and broadcastDmMessage() for typing_stop.
2026-04-01 12:53:55 +02:00
Jannis Braun 187f71e68e feat: add dm_typing_stop event to clear typing indicator on message send
Broadcasts dm_typing_stop to DM members before dm_message_created,
so the typing indicator clears immediately when a message arrives
instead of lingering for up to 3 seconds after delivery.
2026-04-01 12:50:03 +02:00
Jannis Braun 626fbfdba8 fix(federation): address code review findings for FED-009
- Resolve homeUserId from DB in sendFederatedCallStart/End (not raw userId)
- Clear existing timeout in createFederatedCall before overwriting
- Clear federatedCallToken/Url in leaveVoice and handleForceDisconnect
- Remove unnecessary `as any` cast in relay processor
2026-03-31 23:58:27 +02:00
Jannis Braun 2f47d32136 feat(federation): handleDmCallAccept/Reject/End with federated call support (FED-009) 2026-03-31 23:30:27 +02:00
Jannis Braun f2affb0f1d feat(federation): handleDmCallStart sends S2S to remote instances (FED-009) 2026-03-31 23:27:42 +02:00
Jannis Braun cbcb324e24 feat(federation): add federated call registry with ringing timeout (FED-009) 2026-03-31 23:24:45 +02:00
Jannis Braun 720a5de945 fix(federation): add strict origin enforcement for user attribution (FED-010)
Prevent malicious peers from forging events attributed to users on other
instances. Every relay event processor now verifies the acting user's
homeInstance (from payload) matches X-Federation-Origin (from HMAC-verified
header) via verifyAttribution(), normalized to bare domain.

- Add verifyAttribution() helper using extractDomain normalization
- Guard all 13 event processors before any user resolution or DB writes
- Add homeInstance to FederationRelayReaction type + outbound payloads
- Replace unnormalized string equality in friend handlers
- Log mismatched values on rejection for debugging
2026-03-31 19:16:48 +02:00
Jannis Braun d4d160ff10 fix: filter closed DMs from WebSocket ready payload
The buildReadyPayload query fetched all dm_members rows without
checking the closed flag, causing closed DMs to reappear on every
page reload. The REST endpoint GET /api/dm already filtered correctly.
2026-03-27 03:49:20 +01:00
Jannis Braun 6df80aa1f0 fix(federation): resolve persistent unread indicator on federated DMs
Federated relay messages can have local snowflake IDs that don't match
chronological (createdAt) order — a message sent earlier on a remote
instance can arrive later and get a higher local ID. This caused a
permanent mismatch between the ready event's lastMessage (MAX id) and
the acked message (last in createdAt display order), making federated
DM channels appear unread after every server restart.

- Server: change ready event DM lastMessage query from MAX(id) to
  ORDER BY created_at DESC (matching the DM REST API)
- Frontend: change ackChannel to ack MAX(id) among loaded messages
  instead of last in display order (consistent with server comparison)
- Fix federated username display fallback in UserDiscoverCard
2026-03-27 03:05:26 +01:00
Jannis Braun fc8b1ab16e fix(federation): use participant-aware routing for reactions and deletes in group DMs 2026-03-26 20:48:43 +01:00