After deleting a federated identity, the server-side user_federation_registry
and users.replicated_instances were not cleaned up, causing "already connected"
errors when trying to re-federate. The deletion endpoint now authoritatively
removes both the registry row and the replicatedInstances entry, and bumps the
LWW timestamp to prevent stale client syncs from re-inserting them.
Also extends the endpoint to accept mode 'leave' (skip S2S, just clean up),
and enables the "Select instances..." scope option in DeleteIdentityDialog.
Introduces a lightweight DmLastMessagePreview interface for the ready
payload and GET /api/dm response, and widens DmChannel.lastMessage to
accept either DmLastMessagePreview or DmMessageWithUser so both the
static preview path and real-time WS events can coexist without casts.
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.
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
- 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
- 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
Adds a `type` column (TEXT NOT NULL DEFAULT 'user') to the dm_messages
table via schema, migration, and type definition. Updates
buildDmMessageWithUser and the inline replyTo builder in the GET
messages handler to include the field in all DM message responses.
- 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
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
When converting a 1-on-1 DM to a group, the existing DM partner was
incorrectly required to be your friend. DMs don't require friendship,
so this check was over-strict. Added fromDmChannelId parameter to
createGroup — existing members of the source DM are exempt from the
friendship validation.
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.
Replace flat userIds array with a structured users array of GroupDmUserIdentity
objects, enabling the group DM endpoint to accept federated identity hints
(homeUserId, homeInstance) alongside the local user ID.
- Add migrateFixOneOnOneOwnerIds migration to NULL-out ownerId on all
existing 1-on-1 DMs (those with exactly 2 members)
- Fix POST /api/dm to create 1-on-1 channels with ownerId=null instead
of the creator's ID
- Guard POST /api/dm/:id/members: reject with 400 if channel has no
owner (i.e. is a 1-on-1), directing callers to POST /api/dm/group
- Guard DELETE /api/dm/:id/members: replace member-count check with
ownerId check; remove now-duplicate dmChannel query in that handler
- Add CreateGroupDmRequest type to shared types
Extend FederationRelayEvent with member_add, member_remove, and
ownership_transfer event types, plus federatedId and the new optional
membership/ownership/group payload fields. Add FederationMembershipPayload,
FederationOwnershipPayload, and FederationGroupPayload interfaces.
Add federatedId to FederationSyncRequest for group DM sync filtering.
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.
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
Task 13: Hook server-to-server peering into connectToRemote (non-fatal)
and add federation API namespace to the client (initiate, peers, revoke).
Task 14: Show a transparency notice in the DM welcome header when the
other user is on a remote instance, informing that messages are stored
on both home instances and are not end-to-end encrypted.
Task 15: Add Federation section to the instance settings General panel
with DM relay toggle, TTL config, and a live peer list with revoke
buttons. Also extends InstanceAdminSettings type and the server settings
route to support federationRelayEnabled / federationRelayTtlDays.
Previously, voice cleanup only happened in finalizeDisconnect after a
5-second grace period, and only when ALL connections closed. If a user
was logged in on multiple devices and closed the one in voice, the
voice state was never cleaned up — creating a permanent ghost in the
channel sidebar.
Now removeConnection checks if the closing socket is the voice-active
one (tracked via voiceWs map) and immediately cleans up voice state,
broadcasts the leave event, and notifies remaining tabs.
- Add Activity, ActivityType, ActivityTimestamps, ActivityAssets types to shared types
- Add activity_update client event and activities field on presence_update server event
- Add userActivities to ready payload and showActivity to User/UpdateUserRequest
- Create shared activities.ts with ACTIVITY_LIMITS, ACTIVITY_PRIORITY, getPrimaryActivity
- Add show_activity column to users table (schema + migration)
- Update sanitizeUser with isSelf parameter; only include showActivity for self
- Fix .map(sanitizeUser) calls to use arrow wrapper to prevent index-as-boolean bug
- Mark auth routes (register/login) as isSelf=true since they return own user data
When a user joins a voice channel while already connected from another
tab, the server now sends a 'displaced' reason on voice_disconnected.
The client tears down LiveKit and shows an informational toast.