Commit Graph
325 Commits
Author SHA1 Message Date
Jannis Braun 2c09953864 fix: scope zombie guard by homeInstance to prevent cross-instance false matches
homeUserId snowflakes aren't globally unique — must also match
homeInstance to avoid blocking stub creation for unrelated users.
2026-04-03 02:35:06 +02:00
Jannis Braun 4da373c970 fix: prevent resolveOrCreateReplicatedUser from recreating deleted user stubs
When a federated user's identity has been tombstoned (isDeleted=1),
findFederatedUser filters them out, causing resolveOrCreateReplicatedUser
to silently create a new stub — a "zombie" resurrection. This guard checks
for a deleted row before creating a stub and returns null instead.

All 11 call sites across federation.ts and dm.ts have been updated with
appropriate null guards: federation relay handlers reject or skip the event
(participant_not_found / accepted no-op), while dm.ts routes convert null
to undefined so the existing 404 path handles it.
2026-04-03 02:32:36 +02:00
Jannis Braun afa583df1d feat(server): include attachment metadata in GET /api/dm lastMessage 2026-04-02 17:53:14 +02:00
Jannis Braun 7200ccd7e4 feat(server): centralize DM auth via plugin-scoped hooks, add federation gating 2026-04-02 10:48:45 +02:00
Jannis Braun b8ab162570 fix(server): add registry size/duplicate validation; update database and API docs 2026-04-01 18:25:24 +02:00
Jannis Braun 7db9459f11 feat(server): add GET & PUT federation registry API endpoints
Adds persistent federation registry storage endpoints under
/api/users/@me/federation-registry. GET returns all registry entries
with the stored updatedAt timestamp. PUT replaces the full registry via
LWW (409 on stale write) in an atomic transaction.
2026-04-01 17:54:46 +02:00
Jannis Braun e1ece8a5b6 feat(federation): inbound typing relay processors + implicit clear on message relay
processDmTypingStartEvent and processDmTypingStopEvent handle
typing indicator relay from peers. Uses federatedId for channel
lookup, resolveLocalUser for ephemeral identity (no stub creation).
Also clears typing indicator in processCreateEvent when a relayed
message arrives — belt-and-suspenders for dropped relay packets.
2026-04-01 12:56:44 +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 4d7a777ae4 feat(federation): POST /api/dm/:id/members accepts homeUserId+homeInstance
Extend the add-member endpoint to resolve federated identity via
resolveOrCreateReplicatedUser() when homeUserId+homeInstance are provided,
falling back to the existing local userId lookup.
2026-04-01 12:40:57 +02:00
Jannis Braun 5c02bff2d2 feat(federation): POST /api/dm accepts homeUserId+homeInstance for federated DM creation 2026-04-01 12:38:50 +02:00
Jannis Braun e40a63ba9b fix(federation): assign deterministic federatedId on 1-on-1 DM creation
POST /api/dm created channels with federatedId=NULL, so when the S2S
reply arrived, processCreateEvent couldn't find the channel and created
a duplicate. Now computes the deterministic SHA256 hash at creation time
when either participant is federated.
2026-04-01 03:57:42 +02:00
Jannis Braun 4b596afae5 fix(federation): allow homeward relay in attribution check
Client-federation users (e.g., youruser@nova logged into orbit)
send DMs on the remote server. The S2S relay forwards these back to the
author's home instance, but verifyAttribution rejected them because the
author's homeInstance didn't match the sourceInstance.

Now also accepts when the author's home matches the receiving instance
(getOurOrigin()), covering the homeward relay case.
2026-04-01 03:56:12 +02:00
Jannis Braun 42cf8afddd fix(federation): raise relay rate limit from 30 to 90 req/min per peer
The outbox worker interval was reduced from 10s to 1s in FED-009,
so a busy sender can now hit 60 req/min during sustained traffic.
90 gives 50% headroom.
2026-04-01 02:00:19 +02:00
Jannis Braun 3265670047 feat(server): add PATCH peer and permanent delete endpoints for federation admin 2026-04-01 01:22:41 +02:00
Jannis Braun 83074e40ef feat(server): add defaultAutoRotateIntervalDays to instance settings schema and API 2026-04-01 01:22:41 +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 09916917f1 fix(federation): include livekitUrl/livekitToken in dm_call_incoming relay event (FED-009) 2026-03-31 23:37:24 +02:00
Jannis Braun 1f4c2bbeed feat(federation): add relay processors for dm_call_start/accept/reject/end (FED-009) 2026-03-31 23:35:43 +02:00
Jannis Braun 8de27fd39c feat(federation): add generateFederatedCallToken and federated room naming (FED-009) 2026-03-31 23:20:08 +02:00
Jannis Braun abdaf99bb4 fix(federation): address code review findings for FED-011
- 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)
2026-03-31 21:01:02 +02:00
Jannis Braun d529ff20c2 feat(federation): add admin rotation endpoint and expose rotation state (FED-011) 2026-03-31 20:46:41 +02:00
Jannis Braun bd9a598b36 feat(federation): add /peer/rotate endpoint for secret rotation (FED-011) 2026-03-31 20:44:17 +02:00
Jannis Braun 2ceea5e474 feat(federation): switch relay+sync handlers to verifyPeerSignature (FED-011) 2026-03-31 20:43:41 +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 5c5d41e462 feat(federation): wire nonce verification into relay and sync handlers (FED-008)
Pass nonce through to verifySignature and enforce replay protection in
both /api/federation/relay and /api/federation/sync: reject duplicate
nonces (409), reject nonce-less requests from peers that previously sent
nonces (401), warn for legacy peers, and auto-ratchet nonceSupported flag.
2026-03-31 18:12:16 +02:00
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 3eb46ede67 feat(auth): upgrade existing federation stubs during registration instead of creating duplicates 2026-03-31 04:49:19 +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 a3a7527c9e chore: add system docs, specs, and misc updates from other sessions
- Add complete docs/systems/ reference (18 system docs)
- Add federation relay status doc and prior spec/plan docs
- Remove superseded docs/federation-dm-s2s.md (replaced by docs/systems/federation.md)
- CLAUDE.md updates
- Minor fixes in social.ts, types.ts, AddDmMemberModal, NewDmModal, UserSettings
2026-03-31 03:40:34 +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 72aa318928 feat: insert and broadcast system messages for group DM membership events
On group DM creation, emit a system message per added member (event:
member_added). On POST /api/dm/:id/members, emit a system message for
the newly added member. On DELETE /api/dm/:id/members, emit a
member_removed system message before the row is deleted (so the leaver
is still a member at broadcast time), and emit an owner_changed system
message when ownership transfers. The newOwnerUser query is moved
outside the federation-only block so it is available unconditionally.
2026-03-27 05:33:45 +01:00
Jannis Braun 56e8fc8795 feat: add type column to dm_messages for system message support
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.
2026-03-27 05:30:42 +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 bfb358c530 feat(federation): include federation status columns in attachment queries
Both buildDmMessageWithUser and buildMessageWithUser manually map
attachment fields — add federationStatus and federationMeta so they
reach the frontend.
2026-03-27 04:40:48 +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 becd5c8ac8 fix: exempt existing DM members from friend check in createGroup
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.
2026-03-27 02:48:26 +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