Commit Graph
42 Commits
Author SHA1 Message Date
Jannis Braun d2073efdd7 feat(federation): add in-memory nonce store with TTL eviction (FED-008) 2026-03-31 18:08:53 +02:00
Jannis Braun 75504e07c0 fix(federation): add per-peer rate limiting to relay endpoint (FED-007)
Sliding-window rate limiter (30 req/min per peer origin) on POST
/api/federation/relay, matching the existing accept endpoint pattern.
Returns 429 when exceeded — outbox workers retry with backoff. Check
runs before HMAC verification to avoid wasted computation on floods.
2026-03-31 17:33:57 +02:00
Jannis Braun 2cac39a460 fix(federation): replace DNS hairpin self-POST with direct function call (FED-005)
Extract processRelayEvents() from the relay HTTP handler and call it
directly in runInitialSyncForNewPeers(), eliminating the HTTP round-trip
through public DNS that failed on networks without hairpin NAT.
2026-03-31 05:20:16 +02:00
Jannis Braun f5a937eff7 feat(federation): pass profile username hints to all resolveOrCreateReplicatedUser call sites 2026-03-31 04:46:41 +02:00
Jannis Braun f8e41e3cc5 feat(federation): resolveOrCreateReplicatedUser uses findFederatedUser + normalizes homeInstance 2026-03-31 04:42:12 +02:00
Jannis Braun 8167540afc feat(federation): add findFederatedUser lookup + backfillHomeUserId helper 2026-03-31 04:41:51 +02:00
Jannis Braun 77f02c805e refactor(federation): extract extractDomain helper from resolveOrCreateReplicatedUser 2026-03-31 04:41:14 +02:00
Jannis Braun 70678ff198 fix(federation): batch A production readiness — normalization, logging, security
FED-001: normalize homeInstance in processCreateEvent member skip
FED-002: normalize homeInstance in getFriendEventTargets
FED-003: normalize homeInstance in handleSizeRejection
FED-004: add warning log when queueOutboxEvent drops events (zero peer match)
FED-012: remove unused challenge from peer handshake
FED-013: reject non-HTTPS origins in validateOrigin (except localhost)
2026-03-31 03:33:22 +02:00
Jannis Braun 41658ec2ef fix: repair corrupted group DMs, prevent ownerId nulling, show empty groups
Three fixes for group DM data integrity and display:

1. processOwnershipTransferEvent: use resolveOrCreateReplicatedUser instead
   of resolveLocalUser to guarantee a valid ownerId. The previous ?? null
   fallback converted group DMs into 1-on-1s when resolution failed.

2. Self-healing migration: detect group DMs with UUID-format federated_id
   but NULL owner_id (corrupted by the old fallback) and restore owner from
   the first remaining member. Found and repaired 7 across both instances.

3. Sidebar: group DMs with 0 other members (last person standing) now show
   as "Empty Group" instead of being hidden. 1-on-1 DMs with 0 others are
   still correctly filtered out.
2026-03-27 17:38:09 +01:00
Jannis Braun 44b6317c16 fix: federation relay for group DMs — origin normalization, missing federatedId, local-only broadcasts
Three stacked bugs prevented federation relay from working for group DMs:

1. Origin format mismatch: users.home_instance stores bare domains
   ("nova.ddns.net") but federation_peers.origin stores full URLs
   ("https://nova.ddns.net"). getGroupDmTargetOrigins() built target
   lists from bare domains, so queueOutboxEvent() never matched any
   peers — events were never queued into the outbox.

2. Missing federatedId in outbox reconstruction: the outbox worker
   rebuilt relay events from stored payloads but never copied the
   federatedId field. Receiving instances check this field and rejected
   all member_add/remove/ownership_transfer events with
   "missing_membership_payload".

3. Duplicate channels from cross-instance broadcasts: dm_channel_created
   was sent to ALL members including remote replicas. Users connected to
   multiple instances received the event twice (once per instance),
   creating duplicate group DMs in their sidebar. Fixed by only
   broadcasting to members whose home instance matches the current
   server — remote members receive the channel via federation bootstrap
   on their home instance.
2026-03-27 16:53:35 +01:00
Jannis Braun 04c665e0f7 fix: three code review issues — leaveGroup duplicate, as any casts, federation ownership fallback
- Remove redundant `leaveGroup` API method from client.ts (duplicated `leave`); update MessageList.tsx WelcomeHeader to call `api.dm.leave` directly
- Add optional `type` field to shared `Message` interface so `MessageWithUser` carries it; remove `(msg as any).type` casts in `isSameGroup` and the render branch in MessageList.tsx
- Fix `processOwnershipTransferEvent` in federation.ts: replace `channel.ownerId` fallbacks (pre-update, old owner) with `event.ownership.newOwner.homeUserId` in the db update, dm_owner_updated broadcast, and both system message content payloads
2026-03-27 05:50:38 +01:00
Jannis Braun c3191be4e7 feat: system messages, group DM welcome header, and dm_owner_updated event
- Render system messages (member_added, member_removed, owner_changed) inline
  in MessageList with icon + human-readable text; system messages never group
  with adjacent user messages
- Rewrite WelcomeHeader to branch on ownerId: group DMs show overlapping avatars,
  group name, creator attribution, federated privacy note, and a Leave Group button
- Add dm_owner_updated ServerEvent; broadcast from dm.ts leave handler and
  federation processOwnershipTransferEvent so all clients update ownerId in real-time
- Add updateDmOwner action to spaceStore and handle dm_owner_updated in useWebSocket
- Add leaveGroup alias to API client dm namespace
2026-03-27 05:43:30 +01:00
Jannis Braun 2eeca93268 feat: add system messages for federated member add/remove/ownership events
On the receiving instance, federation event processors now write
dm_messages with type='system' for member_added, member_removed, and
owner_changed events and broadcast them via dm_message_created to all
connected local WebSocket clients, matching the behaviour of local
group DM operations.
2026-03-27 05:37:27 +01:00
Jannis Braun 8e589f41fd fix: send dm_channel_created to local users when bootstrapping federated group DM
When processMemberAddEvent bootstrapped a new group DM channel for the
first time on a receiving instance, local users were never notified via
WebSocket — only dm_member_added was broadcast, which requires the client
to already know about the channel. Now, after bootstrap, dm_channel_created
is sent directly to each local connected member, and the redundant
dm_member_added broadcast is skipped for the bootstrap path.
2026-03-27 05:35:29 +01:00
Jannis Braun ceb0c9812d fix(federation): resolve code review issues in upload size mismatch
- Fix critical: outbox worker now copies file_rejected payload fields
  (attachmentId, sourceFilename, rejectionReason, rejectionLimit,
  affectedUserIds) so the reverse relay actually delivers them
- Fix: add sourceFilename to file_rejected event for reliable
  multi-attachment matching on the sender side
- Fix: change text-accent-warning to text-accent-amber (valid class)
- Add Array.isArray guard on federationMeta parse
2026-03-27 04:51:30 +01:00
Jannis Braun b99cbea268 feat(federation): process file_rejected relay events on sender instance 2026-03-27 04:39:48 +01:00
Jannis Braun 0d6bb9dbd3 fix(federation): auto-create and hydrate user profiles during DM relay
Federated 1-on-1 DMs showed the raw snowflake ID as the display name
and no avatar when the remote user had no pre-existing local record.
processCreateEvent used resolveLocalUser (find-only) instead of
resolveOrCreateReplicatedUser, and relay events carried no profile data
for participants.

- Add profile snapshot (displayName, avatar, avatarColor) to
  FederationRelayParticipant and populate it in getDmParticipants
- Change processCreateEvent to auto-create replicated user stubs and
  hydrate them with profile data from the relay event
- Fix hydrateReplicatedUserProfile URL resolution for homeInstance
  values without protocol prefix
- Fix WelcomeHeader: return null while DM data is loading (eliminates
  "unknown" flash on reload), use displayName for @mention text
2026-03-27 03:43:45 +01:00
Jannis Braun 843a2ce727 fix(federation): store absolute URLs for replicated user avatars
Bare filenames stored on replicated user stubs can't be resolved by
the home WS (normalizeUserAssets only runs for remote origins).
Now resolves avatar/banner to absolute URLs pointing to the user's
home instance so they render correctly without page refresh.
Also overwrites stale bare filenames from the prior deploy.
2026-03-27 01:36:23 +01:00
Jannis Braun 699c5a4365 fix(federation): include profile snapshots in friend relay events
Replicated user stubs created by resolveOrCreateReplicatedUser had
null avatar/displayName, causing blank profiles in the UI until
page refresh. Friend relay events now carry profile snapshots
(displayName, avatar, avatarColor, banner, bio) so the receiving
instance can hydrate stubs with real data.
2026-03-27 01:26:46 +01:00
Jannis Braun 97f83b8d1e chore: remove unused imports from federation.ts 2026-03-27 01:07:09 +01:00
Jannis Braun 8918c43597 feat(federation): update sync endpoint for friend event context type 2026-03-27 00:55:07 +01:00
Jannis Braun a485bf3098 feat(federation): add friend event processors for relay receiving side 2026-03-27 00:51:35 +01:00
Jannis Braun 4b181b925d feat(federation): export resolveLocalUser and resolveOrCreateReplicatedUser
Make both identity resolution helpers module-level exports so the group DM
endpoint can import and use them when resolving federated user identities
during group DM creation.
2026-03-26 23:13:17 +01:00
Jannis Braun d8563ec505 fix(federation): validate source authority on membership mutation receivers 2026-03-26 20:48:39 +01:00
Jannis Braun 31729f360e fix(federation): create replicated users during group DM bootstrap for cross-instance members 2026-03-26 20:46:53 +01:00
Jannis Braun 62a16e884a feat(federation): extend catch-up sync to include group DMs and membership mutations 2026-03-26 20:28:19 +01:00
Jannis Braun f7ba391678 feat(federation): handle group DM messages via federatedId lookup in relay processor
In processCreateEvent, branch on event.federatedId: group DM messages now
look up the pre-bootstrapped local channel by federatedId instead of
computing a pair hash from two participants. In queueDmRelay, fetch the
channel's federatedId and ownerId and include federatedId in the outgoing
relay payload for group DMs so receiving instances can route correctly.
2026-03-26 20:26:23 +01:00
Jannis Braun 242004e8ee feat(federation): add membership mutation receiver processors
Add three new event processors to the relay endpoint for handling
inbound group DM membership changes from peer instances:

- processMemberAddEvent: handles member additions with channel bootstrap
  (creates the local channel from group metadata on first contact)
- processMemberRemoveEvent: handles member removals with read state
  cleanup and soft-delete GC when no local members remain
- processOwnershipTransferEvent: updates local channel owner tracking

All three are wired into the relay endpoint's event dispatch switch.
2026-03-26 20:23:40 +01:00
Jannis Braun 0742c37ca1 refactor(federation): rename canonicalDmPairId to computeFederatedId 2026-03-26 20:08:37 +01:00
Jannis Braun 742d851aae fix(federation): branching message lookup in reaction relay processors
Uses messageHomeInstance to determine whether the target message
originated locally (lookup by ID) or was replicated from another
instance (lookup by sourceInstance + sourceMessageId). Supports
N-instance federation and falls back to relay-sender lookup for
backward compatibility with in-flight outbox entries.
2026-03-26 18:49:28 +01:00
Jannis Braun 10d4ca0110 chore: remove unused broadcastDmMessage import from federation.ts 2026-03-26 17:58:24 +01:00
Jannis Braun 6bf2a2621c fix(federation): skip broadcasting relay messages to source-instance users
When Instance 2 receives a relay from Instance 1, it no longer
broadcasts dm_message_created to members whose home instance is Instance
1. Those users already have the original message via their home WS
connection. This prevents duplicate messages from appearing in the
sender's chat.
2026-03-26 17:57:07 +01:00
Jannis Braun a21243256e fix(federation): fix reaction relay using wrong ID for message lookup
Reaction outbox events used reactionId (add) or a composite dedup key
(remove) as the event messageId. The receiver looked up dm_messages by
sourceMessageId = event.messageId, which never matched because it was
searching for a reactionId, not the actual message ID.

Fix: include the actual DM messageId in the reaction payload JSON.
The receiver now uses event.reaction.messageId for the lookup, with
fallback to event.messageId for backward compatibility.
2026-03-26 14:27:48 +01:00
Jannis Braun 4ed9b30ce9 fix(federation): don't copy source thumbnail filename to relay attachments
The source instance's thumbnail file doesn't exist locally, so copying
its thumbnailFilename causes a 404 in the chat view. Set to null so
AttachmentRenderer falls back to the full image URL.
2026-03-26 06:37:26 +01:00
Jannis Braun 50f7a398dc fix(federation): create attachment rows immediately in processCreateEvent
Insert attachment rows with sourceUrl as interim filename so the initial
WebSocket broadcast includes working remote URLs. AttachmentRenderer
already handles http:// filenames as direct URLs. The file worker will
UPDATE the filename to the local path after download completes.

This fixes the invisible bubble bug where attachment-only messages
(content: null, attachments: []) appeared as empty messages.
2026-03-26 06:21:01 +01:00
Jannis Braun cf9fcb78ed fix(federation): add participants array to relay events and fix recipient resolution
The relay was failing because processCreateEvent relied on the friends
table to discover the DM recipient, but friendships aren't federated
across instances. Also, resolveLocalUser matched deleted replicated
users before active ones.

- Add participants[] to FederationRelayEvent with homeUserId/homeInstance
  for all DM channel members
- Add getDmParticipants() helper to look up member identities
- Include participants in outbox payloads (create/update) and sync events
- Rewrite processCreateEvent to resolve participants directly, compute
  canonicalDmPairId, and findOrCreateDmChannel — removing the entire
  friends-list fallback (60+ lines)
- Fix resolveLocalUser to filter out deleted users (is_deleted = 0)
  and prefer the replicated user match when multiple candidates exist
2026-03-26 05:36:41 +01:00
Jannis Braun cb9a70d7a6 fix(federation): use canonical_pair_id for sync scope instead of home_instance matching
The old query missed channels where both members appear as 'local'
(e.g., native Nova + native youruser on orbit). Using
canonical_pair_id is simpler and correct — any channel with a pair
ID is a federated DM that should be synced.
2026-03-26 04:41:00 +01:00
Jannis Braun 49f88c2552 fix(federation): actually remove source_instance IS NULL filter from sync SQL queries 2026-03-26 04:37:47 +01:00
Jannis Braun 277ff0e50c fix(federation): sync ALL locally-created messages, not just local-home-user messages
Messages by replicated users (e.g., Jannis browsing orbit) have
source_instance IS NULL because they were created on that instance.
The sync endpoint was incorrectly filtering these out, causing most
DM history to be missing from the sync response.
2026-03-26 04:34:13 +01:00
Jannis Braun 7f20a4517c feat(federation): implement checkpoint sync endpoint using mutation log
Add POST /api/federation/sync for peer instances to catch up on missed
DM mutations after downtime. Uses HMAC-SHA256 authentication (same as
relay), determines shared channels between peers via raw SQL join on
dm_members/users, queries the federation_mutation_log with timestamp
pagination, and returns FederationRelayEvent payloads for each mutation
type (create, update, delete, reaction_add, reaction_remove).
2026-03-25 21:33:47 +01:00
Jannis Braun 5b9272e22b feat(federation): implement relay receive endpoint with dedup, channel mapping, SSRF validation 2026-03-25 21:27:28 +01:00
Jannis Braun fbdec82172 feat(federation): add peering handshake and admin peer management endpoints
Add federation routes module with four endpoints:
- POST /api/federation/peer/initiate — admin-only, starts peering handshake
  with a remote instance (validates origin, generates HMAC secret, sends
  server-to-server accept request with 10s timeout)
- POST /api/federation/peer/accept — server-to-server first contact,
  rate-limited to 10 req/min per IP, creates/activates peer record
- GET /api/federation/peers — admin-only, lists all peers (hmacSecret excluded)
- DELETE /api/federation/peers/:id — admin-only, revokes peer and cleans outbox
2026-03-25 21:19:47 +01:00