Commit Graph
34 Commits
Author SHA1 Message Date
Jannis Braun 02a1ed73f4 refactor(federation): replace runInitialSyncForNewPeers with startupBootstrapSync
The per-peer sync body is now syncPeerMutationLog (in the new
peer-activation module), invoked via onPeerActivated. The startup
path scans for status='active' AND lastSyncedAt=0 and calls the
unified handler for each — same trigger condition as before, unified
code path with runtime transitions.
2026-04-22 00:28:18 +02:00
Jannis Braun 0d74d1d112 perf(federation-worker): tighten health-check cadence to 15 min
HEALTH_CHECK_INTERVAL_MS was 1 h, but ROTATION_GRACE_PERIOD_MS is 15 min.
Phase skew between two peers' health-check ticks could stretch rotation
finalization desync up to ~1 h, during which signatures from the already-
finalized side verify against the other side's primary-only secret (grace
has expired; verifyPeerSignature stops trying the pending secret). With
AUTH_FAILURE_THRESHOLD = 5 and the existing backoff schedule, this
occasionally tripped legitimate rotations into needs_attention.

Setting the interval to 15 min (= ROTATION_GRACE_PERIOD_MS) guarantees a
finalization tick fires within one grace window on each side, so the
cross-verification window where one peer signs with NEW while the other
still treats NEW as pending cannot outlast the grace period.

Per-tick cost is negligible for the worker's steady state: the only
network fetches are per-active-peer /peer/rotate calls when the 90-day
rotation interval hits (rare) and per-unreachable-peer /instance/info
health pings (bounded by outage count). Going lower than 15 min would
reduce the residual desync but increase tick overhead with diminishing
returns; 15 min is the grace-period-aligned value that the original spec
("runs hourly") deviated from without justification.

Follow-up #20 from S2S DM unification backlog; reduces #19 false-positive
rate (outbox auth-failure transition) on legitimate rotations.
2026-04-21 22:16:09 +02:00
Jannis Braun 48dbe32a69 fix(federation-worker): reset consecutive_auth_failures on successful delivery
Pairs with the new 401/403 handler — a 2xx relay confirms HMAC trust is
healthy so the counter should clear. Mirrors the existing
consecutive_failures reset for network-layer health.
2026-04-21 20:46:52 +02:00
Jannis Braun 012e489bc7 fix(federation-worker): auth failures must not increment consecutive_failures
Code review of the previous commit found that the backoff branch of the
new 401/403 handler delegated to handleOutboxDeliveryFailure, which
double-dips by also incrementing consecutive_failures (the network-layer
counter that drives the 'unreachable' transition at threshold 10). Per the
design spec §State Machine Changes → Reset logic, auth failures must
increment consecutive_auth_failures ONLY.

Split handleOutboxDeliveryFailure into:
- applyOutboxEntryBackoff: just the per-entry backoff update (safe to call
  from the auth-failure path)
- handleOutboxDeliveryFailure: entry backoff + peer's consecutive_failures
  bump (network-error path only)

Also adds a console.warn to the backoff branch so operators can diagnose
clock-skew and rotation-grace incidents before the peer hits the terminal
threshold.

Part of backlog #19.
2026-04-21 20:45:33 +02:00
Jannis Braun e5afd376d2 fix(federation-worker): replace 401/403 wipe-and-rehandshake with bounded retry
The previous handler (commit ce33ccf + its 403 extension) wiped hmac_secret
and reset peer status to 'pending' on any 401/403 from an active peer. This
collapsed three distinct failure modes — transient clock skew, legitimate
split-brain, active MITM attempt — into "silently establish new trust
immediately." The remote's /peer/accept idempotent-200-no-update safeguard
then prevented the re-handshake from actually working, producing a 1-req/sec
loop observed during backlog #16 verification.

New behavior: increment consecutive_auth_failures, apply backoff to outbox
entries. At AUTH_FAILURE_THRESHOLD (5) transition to needs_attention,
preserve hmac_secret, surface delivery-impossible to affected users,
notify admins. Secret is NEVER wiped in response to a network-observed
401/403.

Part of backlog #19.
2026-04-21 20:39:37 +02:00
Jannis Braun 695ea0849d refactor(federation-worker): extract buildContextMapForPeer helper
Pure refactor — will be reused by the needs_attention transition handler.
No behavior change.
2026-04-21 20:35:51 +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 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 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 b407e38730 feat: add peer/denied S2S endpoint and export pushPeerRejectedEvent 2026-04-20 14:56:31 +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 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 f51e427493 fix: pass readState and dmCloseReopen through outbox worker relay reconstruction
The outbox worker's event reconstruction whitelist was missing these two
fields, causing read_state_update and dm_close/dm_reopen relay events to
arrive at remote instances with empty payloads and get rejected.
2026-04-08 02:01:41 +02:00
Jannis Braun 43900576b2 fix: update contextType casts to include 'profile', remove stale profileSync comments 2026-04-07 14:05:19 +02:00
Jannis Braun ec6cb236a2 fix: ensure profile_update payload is included in outbox event reconstruction 2026-04-07 13:58:11 +02:00
Jannis Braun aff2a403d7 fix(federation): reduce outbox worker interval from 10s to 1s for lower relay latency 2026-03-31 23:42:31 +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 0c09a58d61 feat(federation): add grace period finalization and auto-rotation to health worker (FED-011) 2026-03-31 20:49:15 +02:00
Jannis Braun 1f6605b801 feat(federation): outbox worker signs with pending secret during rotation (FED-011) 2026-03-31 20:48:00 +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 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 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 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 1990b90022 feat(federation): send file_rejected reverse relay on size limit exceeded 2026-03-27 04:37:27 +01:00
Jannis Braun 7125c62a1f feat(federation): store remote peer upload size limit from relay response 2026-03-27 04:34:57 +01:00
Jannis Braun 2a741a0dc7 refactor(federation): update worker and janitor for generalized outbox columns
Replace dmChannelId/messageId column references with contextId/entityId/contextType in
federationWorker outbox delivery, spread all payload fields (membership, ownership, group,
friendship), add friend-context initial sync pass, and fix storageJanitor DM purge queries.
2026-03-27 00:46:16 +01:00
Jannis Braun 4efa35f311 feat(federation): add soft-delete GC for empty group DMs with 24h grace period
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.
2026-03-26 20:35:55 +01:00
Jannis Braun 22664a6a14 refactor(federation): extract getOurOrigin() into shared federationAuth export
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.
2026-03-26 18:39:42 +01:00
Jannis Braun 086195aa7c fix(federation): generate thumbnails for downloaded federated attachments
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).
2026-03-26 06:46:14 +01:00
Jannis Braun ee8abc1022 fix(federation): file worker UPDATEs existing attachment rows + broadcasts
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.
2026-03-26 06:23:12 +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 ae004b72e1 feat(federation): trigger checkpoint sync on startup for peers that have never been synced 2026-03-26 04:30:54 +01:00
Jannis Braun 3898e4940a feat(federation): add outbox delivery, file queue download, and health check workers 2026-03-25 21:39:32 +01:00