Commit Graph
582 Commits
Author SHA1 Message Date
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 53483d6981 polish(server): align sendCallRelay timeout message with codebase convention; use .then on sendTypingRelay fire-and-forget 2026-04-21 13:49:20 +02:00
Jannis Braun 21f220739c feat(server): sendCallRelay auto-peers on demand, typing passes peeringTimeoutMs:0
sendCallRelay now returns CallRelayResult with a typed reason on failure.
When the peer is not already active (or unreachable), runs a racePeering
against CALL_PEERING_TIMEOUT_MS (3s). Background handshake is not aborted
on race loss — next attempt succeeds.

sendTypingRelay passes peeringTimeoutMs:0 so typing never blocks on a
handshake; instead a warm-up ensurePeered runs in the background for any
non-active peer so the NEXT relay (message, call, or typing) benefits.
2026-04-21 13:45:29 +02:00
Jannis Braun 4ddb09edf1 fix(server): racePeering normalizes handshake rejections and only warns on timeout win
Addresses code review on b22a7bd: (1) a rejected handshake now returns
{ status: 'failed', error } instead of throwing, keeping the structured
contract; (2) the "background handshake" warn only fires when the
timeout arm wins — not when the handshake is itself the race winner by
rejection. Timing tests migrated to vi.useFakeTimers for determinism.
Regression test added for the handshake-wins-by-rejection case.
2026-04-21 13:41:44 +02:00
Jannis Braun b22a7bd0c6 feat(server): add racePeering helper with tests
Exports `racePeering(origin, timeoutMs, ensurePeeredFn?)` that races
`ensurePeered` against a deadline. On timeout, the background handshake
continues (warming the peer for the next attempt) and a warn-logged
.catch() prevents unhandledRejection. Injectable `ensurePeeredFn` param
enables full DI in tests without mocking module internals.
2026-04-21 13:37:30 +02:00
Jannis Braun 852e3657f9 fix: dedup federation membership events by (sourceInstance, messageId)
processMemberAddEvent, processMemberRemoveEvent, and processOwnershipTransferEvent inserted system messages unconditionally. Outbox retries and initial-sync replays (triggered whenever an admin re-approves a peering request, which recreates the peer row with lastSyncedAt=0) duplicated the system message on every delivery. Each new snowflake ID exceeded the user's last_read_message_id, flipping the channel back to unread after every deploy.

Processors now short-circuit on a matching (source_instance, source_message_id) row, and persist those fields when inserting. processMemberAddEvent emits the tagged system message in both bootstrap and incremental paths so bootstrap replays don't fall through and insert a second one; the bootstrap's dm_channel_created broadcast carries that message as lastMessage so sidebar previews and unread anchors agree across instances.
2026-04-21 01:06:43 +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 6afad97bd1 fix: revert outgoing peering blocks — autoAcceptPeering only gates incoming
autoAcceptPeering means 'don't accept peering initiated by others', not
'don't initiate peering ourselves'. Two checks were incorrectly blocking
outgoing peering when auto-accept was off:

1. ensurePeered() refused to auto-initiate — reverted. When a local user
   sends a DM, the server should initiate peering. The remote's
   peer/accept decides whether to accept or queue.

2. queueOutboxEvent() refused to create placeholders — reverted. The
   outbox needs placeholders to queue entries. Without them, DM relay
   silently fails.
2026-04-20 18:08:05 +02:00
Jannis Braun 165fda44a3 fix: accept incoming handshake for awaiting_approval peers to break approval ping-pong
When both instances have autoAcceptPeering off, the approval flow
ping-ponged indefinitely. Admin A approves → handshakes to B → B
queues (202) → A's peer becomes awaiting_approval. Admin B approves →
handshakes to A → but A's gate only matched 'pending', not
'awaiting_approval', so it re-queued instead of accepting.

Now the gate matches both 'pending' and 'awaiting_approval'. When the
second admin approves and handshakes back, the first instance recognizes
its admin already approved and accepts — completing the peering.
2026-04-20 18:01:47 +02:00
Jannis Braun 072858cbbb fix: multiple federation peering bugs
1. queueOutboxEvent no longer creates pending peer placeholders when
   autoAcceptPeering is disabled — prevents bypassing the admin's
   peering control

2. Approval endpoint checks for 202 before response.ok — when the
   remote also has autoAcceptPeering off, sets peer to awaiting_approval
   instead of incorrectly activating it

3. awaiting_approval status added to Federation panel UI — status label,
   colors, filter options so these peers are visible and manageable
2026-04-20 17:54:00 +02:00
Jannis Braun b40c57f227 fix: call resolvePendingPeers before early return in processOutboxTick
When all peers are pending (no active peers with outbox entries),
processOutboxTick returned early at line 141 before reaching
resolvePendingPeers at line 302. Pending peers were never resolved
because the only code path to resolvePendingPeers was after the
active-peer delivery loop — which never ran.
2026-04-20 17:41:34 +02:00
Jannis Braun 34fe9115b9 fix: compute target origins for 1-on-1 DMs so pending peers are created
getGroupDmTargetOrigins() returned undefined for 1-on-1 DMs, which
queueOutboxEvent() treated as 'broadcast to all existing peers'. When
no peers existed, nothing was queued and no handshake was ever triggered.
Now always computes target origins from DM participants so the pending
placeholder creation path runs, enabling ensurePeered() → peer/accept
→ approval queue flow.
2026-04-20 17:15:22 +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 83b682d501 fix: block auto-peering initiation when autoAcceptPeering is disabled
ensurePeered() now checks the local autoAcceptPeering setting before
initiating new peering. When disabled, only admin-explicit peer/initiate
and approval-request approve bypass this check. Closes the bypass where
client peer/ensure or outbox worker could auto-initiate outward peering
even when the admin intended to control all peering.
2026-04-20 16:51:44 +02:00
Jannis Braun 8be30dc95f fix: check 202 before response.ok so queued approval isn't treated as accepted 2026-04-20 16:41:56 +02:00
Jannis Braun e4e0d0d1f1 fix: handle 403 (inactive peer) alongside 401 for stale peer re-handshake 2026-04-20 16:32:24 +02:00
Jannis Braun ce33ccf69e fix: reset stale peer to pending on 401 so ensurePeered re-handshakes 2026-04-20 16:21:53 +02:00
Jannis Braun 33c45e3184 feat: add awaitingApprovalPeerOrigins and pendingApprovalCount to ready payload 2026-04-20 15:03:29 +02:00
Jannis Braun 5f50ffc5f2 feat: janitor expiry for peer approval requests with signed denial 2026-04-20 15:01:31 +02:00
Jannis Braun 1920324469 feat: add admin approval-request endpoints (list, approve, deny) 2026-04-20 14:59:52 +02:00
Jannis Braun b407e38730 feat: add peer/denied S2S endpoint and export pushPeerRejectedEvent 2026-04-20 14:56:31 +02:00
Jannis Braun 5e48d67cb0 feat: queue auto-peer requests for admin approval when autoAcceptPeering is off 2026-04-20 14:54:35 +02:00
Jannis Braun 9877d78a3a feat: handle 202 queued response and awaiting_approval status in ensurePeered 2026-04-20 14:51:59 +02:00
Jannis Braun 14041e9d11 feat: add peer_approval_requests table and migration 2026-04-20 14:50:08 +02:00
Jannis Braun b434a736a8 fix: address code review findings (C1, I1, I2)
- C1: Include 'unreachable' peers in queueOutboxEvent query to prevent
  UNIQUE constraint violation when creating placeholders
- I1: Add 'rejected' to StatusFilter in FederationPanel so admins can
  see and manage rejected peers with delete/re-initiate actions
- I2: Map ensurePeered 'failed' to 'pending' in peer/ensure response
  to match spec and client expectations
2026-04-09 14:08:54 +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 83771b863c feat: outbox worker resolves pending peers and pushes rejection events 2026-04-09 13:51:37 +02:00
Jannis Braun d5a6d470f9 feat: queueOutboxEvent creates pending peer placeholders, skips rejected/revoked 2026-04-09 13:49:04 +02:00
Jannis Braun 63a7f0c922 fix: use proper HTTP status codes on peer/ensure (400 for validation, 429 for rate limit) 2026-04-09 13:47:30 +02:00
Jannis Braun 051646763a feat: add autoAcceptPeering gate on peer/accept and POST /api/federation/peer/ensure endpoint 2026-04-09 13:46:15 +02:00
Jannis Braun f802528688 feat: add ensurePeered() core function with race deduplication 2026-04-09 13:40:49 +02:00
Jannis Braun b5dffba7ef feat: add autoAcceptPeering instance setting with drizzle migration
Adds `auto_accept_peering` integer column (default 1/true) to the
`instance_settings` singleton table, controlling whether this instance
auto-accepts incoming peering requests. Includes generated migration
`0001_clear_earthquake.sql` applied automatically on server boot.
2026-04-09 13:37:49 +02:00
Jannis Braun ce0b2d0e15 feat: allow any group DM member to add friends, not just owner
Remove the owner-only gate on POST /api/dm/:id/members. The S2S relay
already accepts member_add from any HMAC-verified peer, and the UI
already shows the add button to all group DM members. Only the
server-side check was blocking non-owners.
2026-04-09 02:15:44 +02:00
Jannis Braun 3789ece0ca chore: add db:generate script for drizzle-kit migrations 2026-04-09 00:37:10 +02:00
Jannis Braun e467a878d0 chore: remove completed backfill calls (thumbnails, media dimensions)
Both backfills are flagged complete on all instances.
The upload pipeline generates thumbnails at upload time.
2026-04-09 00:33:15 +02:00
Jannis Braun fec0502a2f refactor: replace createTables() with Drizzle migrate()
Delete ~270 lines of hand-written CREATE TABLE SQL.
Schema is now managed entirely by drizzle-kit migrations.
initDatabase() flow: baseline check -> migrate() -> ensureDefaults().
2026-04-09 00:32:01 +02:00
Jannis Braun 3acaea2e45 refactor: replace 2100-line migration system with ensureDefaults + baseline
Remove all manual migrations — Drizzle-kit now manages schema DDL.
Data-fix migrations have all completed on both instances.
Startup initialization (settings row, worker ID, first admin)
moves to idempotent ensureDefaults().
2026-04-09 00:30:00 +02:00
Jannis Braun 450d96407f feat: add drizzle-kit config and generate initial migration 2026-04-09 00:26:53 +02:00
Jannis Braun be2c16efa4 fix: use bare column names in partial index WHERE clauses
SQLite rejects table-qualified column names in partial index
WHERE clauses. Use literal column names instead of Drizzle
column interpolation.
2026-04-09 00:25:36 +02:00
Jannis Braun ac03151b61 feat: add database index definitions to Drizzle schema 2026-04-09 00:19:10 +02:00
Jannis Braun 0db5e4e453 feat: download profile images locally in processProfileUpdateEvent
When a profile_update relay arrives with avatar/banner URLs, download
the files to local storage instead of storing remote absolute URLs.
Falls back to absolute URL on any download failure. Cleans up old
local files when replaced.
2026-04-08 16:57:06 +02:00
Jannis Braun fd2254c9c4 feat: add downloadProfileAsset helper for profile image replication 2026-04-08 16:54:07 +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 94461f8967 debug: add token endpoint logging 2026-04-08 13:39:09 +02:00
Jannis Braun f6af6bf579 debug: more diagnostic logging in processDmCallAcceptEvent 2026-04-08 13:32:41 +02:00