- Fix race window: store pendingHmacSecret AFTER remote peer confirms,
not before (admin endpoint + auto-rotation worker)
- Add hex validation on newSecret at /peer/rotate endpoint
- Use pending-secret-aware signing in initial sync worker
- Add test for corrupt state (pendingHmacSecret set, secretRotationAt null)
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.
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.
- 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
Replace the hard-delete in the leave handler with a soft-delete (sets
deleted_at timestamp) when the last member leaves a group DM. A new
janitor sweep in the federation worker runs hourly and purges channels
whose grace period has expired, cascading through reactions, embeds,
attachments, messages, members, outbox/mutation-log/file-queue entries,
and finally the channel itself.
All client-facing dm_channels queries now filter on deleted_at IS NULL
to hide soft-deleted channels from the REST API and WebSocket ready
payload.
Consolidates 4 inline constructions of the instance origin URL into a
single shared function. Removes the private copy in federationWorker
and two ad-hoc domainOrigin variables in federationOutbox.
After the file worker downloads a federated attachment, generate a
thumbnail using the same sharp pipeline as local uploads. This ensures
federated images use bandwidth-efficient thumbnails in chat view
instead of serving the full-size image.
Also fixes processCreateEvent to not copy the source instance's
thumbnailFilename (which doesn't exist locally).
The file worker now UPDATEs the attachment row created by processCreateEvent
(swapping sourceUrl interim filename to local path) instead of INSERTing a
duplicate. Falls back to INSERT for legacy queue entries.
After download, broadcasts dm_message_updated so clients see the attachment
transition from remote hotlink to local file in real-time.
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