Commit Graph
282 Commits
Author SHA1 Message Date
Jannis Braun 03165a3b7d feat(server): test-only seed-peer route gated by NODE_ENV+ENABLE_TEST_ROUTES 2026-05-03 22:59:46 +02:00
Jannis Braun c0e71b1ded fix(federation): hydrate downloads replicated avatars locally + backfill stale URL rows
hydrateReplicatedUserProfile now calls downloadProfileAsset and stores bare local filenames, falling back to absolute URLs only on download failure. It also fills empty fields only — no longer clobbering local files written by processProfileUpdateEvent. Adds an idempotent startup backfill that converts existing http-prefixed avatar/banner rows on replicated users into local files, so federated profile pictures keep rendering when the home instance is offline.
2026-05-02 22:45:27 +02:00
Jannis Braun 2f0940c30b feat(admin): manual cleanup of stale tus upload sessions + visibility
Adds an admin-driven sweep on top of the existing 24h auto-expire so
operators can see and reap abandoned `.tus/` sessions without waiting.

- storageJanitor: extract `walkTusDir(predicate)` helper, add
  `getStaleTusInfo` + `cleanupStaleTusSessions(thresholdMs, dryRun)`;
  refactor `cleanupTusStragglers` to delegate while preserving its
  janitor-tick `{ removed }` contract.
- StorageStats gains `staleTusSessions` + `staleTusSize` (fixed 1h
  display threshold).
- New `POST /api/admin/storage/cleanup-tus` route with
  `maxAgeHours` validation (positive finite number, default 1) and
  `dryRun` support; admin-gated.
- StoragePanel: 6th overview card "Stale Uploads" + new cleanup
  subsection mirroring the media-cleanup pattern (preview-then-clean
  with shared result panel styling).
- Tests: 8 new janitor tests covering empty dir, threshold filtering,
  dry-run vs live, oldest-mtime tracking, subdir skipping, and the
  override path on the existing straggler sweep. New
  `routes/admin.test.ts` covers auth/admin gates, validation (zero,
  negative, NaN), default `maxAgeHours`, dry-run vs live unlink.
- Docs: `uploads.md` §Janitor expanded to the full lifecycle (cancel
  DELETE, discard DELETE, auto-expire, straggler sweep, admin route);
  `admin.md` Storage Management updated with the new endpoint and
  StorageStats fields.
2026-05-02 18:44:19 +02:00
Jannis Braun 2be243336b feat: all profile uploads through transferStore; delete legacy POST /api/uploads
Migrates the remaining 5 profile/space upload sites (CreateSpace, AccountPanel
avatar+banner, OverviewPanel icon+banner) to transferStore.startUpload with
tray:false. Space sites pass _instanceOrigin so uploads route to the space's
home instance.

Removes upload/uploadWithProgress from api.uploads (and their private
uploadFile/uploadFileWithProgress helpers); api.uploads.url is preserved for
GET-path URL building. Deletes the server-side POST /api/uploads handler and
the now-unused @fastify/multipart plugin registration. GET /api/uploads/:filename
remains intact.
2026-05-02 16:46:29 +02:00
Jannis Braun 974fbf759e feat(server): tus upload endpoint at /api/files with auth, ownership, size, finalize hooks 2026-04-30 01:50:03 +02:00
Jannis Braun 1ed70a90b1 fix(dm): render system messages in sidebar preview instead of raw JSON
DmLastMessagePreview lacked a `type` field, so the sidebar rendered
`lastMessage.content` verbatim — surfacing JSON like
`{"event":"space_invite",...}` for space invites and member-add events.

Adds `type` to the preview payload (populated server-side from
`dm_messages.type`) and routes all sidebar call sites through a single
`formatDmSidebarPreview` helper that renders human-readable text for
each system event and skips the group `Sender:` prefix on system rows.
2026-04-29 23:13:09 +02:00
Jannis Braun bc66ddc633 fix(server): canonicalize spaceInstanceOrigin before storing payload
Empty-string (local) origin is now stored as the absolute home origin
so relayed DM space-invite cards carry the correct value to remote
recipients instead of resolving against the wrong instance.
2026-04-29 22:35:34 +02:00
Jannis Braun 44e9a4234c fix(server): local-fast-path for invite snapshot — skip HTTP self-reach
POST /api/dm/space-invite was hanging 5s and returning invite_invalid
for any local-space invite. fetchSpaceInviteSnapshot was being called
against our own public domain from inside the backspace container, which
fails (Docker NAT loopback) and aborts on timeout.

Add getLocalInviteSnapshot — reads the snapshot directly from the DB —
and branch in dm.ts so local invites bypass the HTTP roundtrip entirely.
Cross-instance invites still go through fetchSpaceInviteSnapshot with
its existing SSRF guard.

Also refactor the GET /api/spaces/invite/:code/preview handler to use
the same helper, keeping the snapshot shape in one place.

Tests assert fetchSpaceInviteSnapshot is NOT called for the local case
(critical regression guard) and that the cross-instance path still hits
the HTTP fetch.
2026-04-29 22:29:44 +02:00
Jannis Braun 7303c23b20 test(server): integration tests for POST /api/dm/space-invite 2026-04-29 21:43:25 +02:00
Jannis Braun 5cb0aa9d66 feat(server): POST /api/dm/space-invite endpoint with rate-limited per-friend invite
Adds POST /api/dm/space-invite which fetches a space-invite snapshot
server-to-server from the space's home instance, ensures a 1-on-1 DM
between the caller and a friend, and posts a type='system' message
carrying SpaceInviteSystemPayload. Snapshot is never trusted from the
client. Rate-limited 30/60s per caller. Federation relay queued when the
recipient is on a remote instance (system message type forwarded by
Tasks 1-3).

Adds an `ensureOneOnOneDmChannel` helper that mirrors the dedup-or-create
behavior of the existing POST /api/dm handler — including federatedId
computation and the dm_channel_created notification payload — without
modifying that handler. Duplication is intentional; consolidation is a
separate follow-up.
2026-04-29 21:37:44 +02:00
Jannis Braun a995bd4148 feat(federation): processCreateEvent inserts system-typed messages from relay 2026-04-29 21:30:51 +02:00
Jannis Braun dbcc399ebb test(settings): tighten 'preserves field when omitted' to actually prove preservation
The original test seeded the DB with the schema default (1) and asserted
the response was true after a partial PATCH. That passes both for
'untouched' and 'reset to default' — doesn't distinguish them. Now the
test toggles the DB column to false BEFORE the PATCH, then asserts the
false value survives both in the response AND in the DB row directly.
2026-04-28 21:06:05 +02:00
Jannis Braun 8d7ba33c21 feat(settings): expose federatedRegistrationOpen in /settings/instance + /instance/info
Surfaces the federatedRegistrationOpen flag (Task 1 schema column) on the
admin settings GET/PATCH endpoints and the public /api/instance/info
endpoint. Closes the 3 deferred TypeScript errors from Task 2 by
populating the now-required InstanceAdminSettings/InstanceInfoResponse
field.

Adds smoke tests (routes/instance.test.ts, routes/settings.test.ts) that
lock in the JSON contract the Connections UI (Task 21) and admin
RegistrationPanel (Task 15) consume, plus boolean-validation coverage
for the PATCH path. Updates docs/systems/admin.md with the new field in
both InstanceAdminSettings and the public info response schema.
2026-04-28 21:01:50 +02:00
Jannis Braun 87301bd4d2 test(auth): polish register handler — comment, test isolation, +1 coverage
Quality-review polish on Task 11:

1. One-line comment near the federatedRegistrationOpen default behavior
   noting that the missing-row case is unreachable post-migration but
   falls federation-closed defensively (asymmetric with registrationOpen
   which falls back to env config — by design).

2. The "federated gate blocks token registration" test now sets ONLY
   federatedRegistrationOpen=0, isolating the federated-gate-alone
   effect rather than a both-gates-closed compound.

3. New test: open registration + revoked token → 201 (silently ignored).
   Locks the spec §5.7 invariant "no validation when registration is open"
   against future "let's just validate it for safety" regressions.

4. auth.md prose explicitly notes that federated stub upgrade and new-
   account paths do NOT enter redeemInvite — surfacing the structural
   enforcement of spec §1.3 "tokens never unlock federated creation".
2026-04-28 20:58:16 +02:00
Jannis Braun 0559ea369b feat(auth): split registration gate by homeInstance + atomic invite redemption
The /api/auth/register handler now branches on homeInstance:

- Local path (no homeInstance): gated by registrationOpen. When closed, a
  valid inviteToken bypasses the gate and is consumed atomically inside
  redeemInvite()'s transaction (user insert + usedCount bump + redemption
  row all commit together, or all roll back). When open, inviteToken is
  silently ignored.

- Federated path (homeInstance set): gated by federatedRegistrationOpen.
  Token is ignored entirely on this path -- tokens never unlock federated
  creation. Closed → 403 with "Federated registration is closed".

InviteUnavailableError thrown by redeemInvite() (concurrent revoke,
last-slot race, expiry between check-invite and submit) is mapped to 403
"Invalid or expired invite". The in-txn re-derive closes the TOCTOU window.

9 new tests cover the toggle matrix from spec §5.6 + invite consumption
semantics + federated-gate independence + last-slot race rejection.

Updates docs/systems/auth.md: rewrites the Registration Gate section to
describe the three-path model (open / invite / federated), adds the toggle
matrix, adds an Invite Tokens subsection with the atomic-redemption shape,
notes that the federated stub upgrade is always gated by
federatedRegistrationOpen, never by an invite token.
2026-04-28 20:51:49 +02:00
Jannis Braun 1b76b8bf2a test(invites): tighten check-invite assertions to byte-identical responses
Per quality review: replace per-property assertions with toEqual()
object-equality on the invalid-response bodies. Locks the enumeration-
shield contract — revoked/unknown/malformed/missing must all return the
SAME body, not just bodies that happen to satisfy individual assertions.
2026-04-28 20:46:24 +02:00
Jannis Braun effb36c7f4 feat(auth): GET /api/auth/check-invite with collapsed enumeration shield 2026-04-28 20:42:46 +02:00
Jannis Braun ade5c0d998 test(invites): tighten requireAdmin mock to match real Fastify contract 2026-04-28 20:40:24 +02:00
Jannis Braun 0c9865ad90 feat(invites): /api/admin/invites CRUD endpoints 2026-04-28 20:36:09 +02:00
Jannis Braun 76b6621d62 feat(storage): remove upload-size cap, add MB/GB unit toggle
Storage panel's max-upload-size input now accepts any positive integer
(bounded only by JS safe-integer ceiling) and offers an MB/GB unit
toggle. Multipart limit relaxed to MAX_SAFE_INTEGER — actual cap is
enforced per-request from the DB setting, not at the framework layer.
2026-04-28 18:37:16 +02:00
Jannis Braun 4fa2414933 fix(presence): drop status='online' from registration insert
Mirrors the REST-login change in f17c46c on the registration insert path.
A successful POST /api/auth/register does not by itself imply a live
WebSocket — the client may never connect (transient network, mobile
background, error path between the 201 and /ws), leaving a permanently
stuck-online row that no disconnect timer can clean up. The schema
default 'offline' is correct; ws/handler.ts flips it to 'online' on real
WS auth.

The federated-stub upgrade path in the same handler is unaffected: it
only updates passwordHash/username/homeUserId/displayName/avatarColor,
leaving the stub's pre-existing 'offline' status (set when the stub was
created via replication) untouched.

Updates docs/systems/auth.md step 7 to reflect the new behavior.
2026-04-27 09:35:34 +02:00
Jannis Braun f17c46c77f fix(presence): reset stale users.status on boot; drop REST-login online write
users.status was only flipped back to offline by the WebSocket disconnect
path (5s grace timer in ConnectionManager). Process exits (deploy/crash/OOM)
lose those in-memory timers, freezing any non-offline row at its last value
and making the user appear permanently online to friends and space co-members.
Confirmed in production on the Pi instance: a user appeared online for ~3
days with no live socket.

Add resetStalePresenceOnBoot() in utils/presenceBoot.ts and call it from
index.ts after getDb()/seedDatabase() and before WebSocket route registration.
The reset is federation-safe: it only updates rows where home_instance IS
NULL (replicated stubs are projections of remote presence and must not be
stomped) and is_deleted = 0 (tombstoned users are excluded from broadcasts).

Also remove the redundant status='online' write from POST /api/auth/login.
A successful REST login does not imply a live socket; the WS auth handshake
is the single source of truth. Login alone could otherwise produce the same
stuck-online row when a client logs in and never establishes a WS.

Tests cover: locally-homed online/idle/dnd reset, replicated rows untouched,
tombstoned rows untouched, idempotence, mixed populations.

Updates docs/systems/activity-presence.md (Connect/Disconnect Flow, new Boot
Reset section) and docs/systems/auth.md (login no longer mutates status).
2026-04-27 00:35:36 +02:00
Jannis Braun b698ded47d fix(federation): harden processFriendRequestCreateEvent receiver-side
Two correctness/defense fixes plus regression tests in the existing
in-memory drizzle test file.

1. Reverse-direction idempotency. The sender-side path in social.ts
   checks BOTH directions of friend_requests and returns 409
   incoming_request_exists when an opposite-direction row exists. The
   receiver only matched from->to, so cross-fire (alice@A and bob@B both
   click "add friend" near-simultaneously) produced two opposite
   pending rows on each instance. The receiver now silent-accepts when
   either direction matches a pending row, mirroring the sender's
   both-direction check.

2. Self-target guard (defense-in-depth). Reject events whose
   from-identity equals to-identity (after normalizeOriginForCompare)
   with a new receiver-acknowledged 4xx code self_target_invalid.
   Sender's local cannot_friend_self should catch this, but the
   receiver does not trust upstream validation. Added to
   TERMINAL_REJECTION_REASONS so the standard rollback fires
   (mapped client-side to peer_rejected). Logged at console.warn.

Spec updates: social.md inbound contract now documents both-direction
idempotency and the self-target guard; federation.md and the
s2s-friend-add design spec list the new terminal rejection reason.
2026-04-27 00:07:52 +02:00
Jannis Braun 8cad964809 chore(test): cover processFriendRequestCreateEvent branches
Extends the existing happy-path test (one case) with 14 more covering
every branch of the receiver-side handler: missing payload, attribution
mismatch, tombstoned sender (silent accept), recipient_not_found
(including soft-deleted recipient), already-friends idempotency in both
column orderings, same-direction pending re-delivery idempotency,
declined-then-resent (status filter), reused replicated stub, profile
hydration with URL resolution, explicit and fallback createdAt, and
mixed-batch accept/reject isolation through processRelayEvents.
2026-04-26 23:58:45 +02:00
Jannis Braun 6ba8b9256b fix(federation): broadcast peering_subscription_changed on terminal state
Live verification caught a stale-UI bug: when admin denial / approval
fanout / janitor expiry cascaded subscriber rows away, only the
peering_notification_received WS event fired (which only refetches
the notification list). The user's pending-subscriptions section
stayed stale until manual refresh.

Each terminal-state path now also fires peering_subscription_changed
to affected users so their pending list refreshes alongside the new
notification. Fixed in:
- onPeerActivated.fanoutOutboundSubscribers (approval path)
- handleOutboundDeny (admin denial)
- cleanupExpiredApprovalRequests (janitor expiry; also adds the
  notification-received broadcast that was previously deferred to
  next-page-load only)
2026-04-26 23:20:31 +02:00
Jannis Braun e82ccbde62 fix(federation): tighten peering-notifications GET response to spec shape
db.select() returned every column including userId; spec §4.9
defined the response row WITHOUT userId. The leak is harmless today
(user queries their own rows) but expands the public API surface
beyond the spec, and would become part of the contract once Task 10
generates client types. Switching to explicit column projection.
2026-04-26 22:27:43 +02:00
Jannis Braun 86ac54b913 feat(federation): peering-notifications endpoints + janitor expiry fanout
- GET /api/federation/peering-notifications (?unread=1 filter)
- POST /api/federation/peering-notifications/:id/read (per-row mark-read)
- POST /api/federation/peering-notifications/read-all (bulk mark-read,
  preserves already-read readAt; returns affected count)
- janitor: outbound expired rows fan out kind='expired' notifications
  to each subscriber before cascade-deleting parent (replaces Task 1's
  scaffolding 'continue' guard); inbound expired rows are deleted outright
  (both sides expire independently — no cross-instance network call)
- janitor: 30-day cleanup pass for read notifications
  (cleanupReadPeeringNotifications); unread rows persist indefinitely
- cleanupExpiredApprovalRequests is now sync (no async network IO)

Notes:
- No WS broadcast on janitor expiry — offline users see notifications on
  next GET, matching the persistence guarantee of the notifications table.
- The previous /peer/denied network call on inbound expiry is removed;
  symmetric per-side expiry now handles termination on both sides.

Tests: 12 new (3 + 4 endpoint cases on 3 endpoints; 2 outbound/inbound
janitor expiry cases + 1 not-yet-expired guard + 1 retention sweep);
247 server tests passing total.
2026-04-26 22:18:44 +02:00
Jannis Braun 45ab0ea2c8 feat(federation): peering-subscriptions GET and DELETE endpoints
User-facing endpoints for the pending-peering-approvals UI surface.
GET lists the user's own pending subscriber rows joined to parent
peering requests. DELETE cancels a single subscription; if it was
the last subscriber, cascade-deletes the parent (admin queue updates
via federation_peers_changed). Authorization: subscriber.userId must
match request.userId.
2026-04-26 22:09:41 +02:00
Jannis Braun 42355ee889 feat(federation): direction-branched approve and deny for outbound queue
- /approve on outbound: generates HMAC, sends /peer/accept to remote.
  200 -> activate peer + onPeerActivated cleanup. 202 -> awaiting_approval,
  capture token, queue row + subscribers REMAIN. 4xx/5xx/network -> clean
  up peer row, leave queue for admin retry.
- /deny on outbound: fans out kind='denied' notifications, cascade-deletes
  parent + subscribers, broadcasts admin event. No remote network call.
- /approve and /deny on inbound: existing behavior preserved verbatim.
- GET /approval-requests: response includes direction; outbound rows
  carry subscribers[] (joined with users.username, possibly empty).
- Removes Task 1's temporary /deny scaffolding guard now that the
  direction-branched dispatcher handles outbound rows correctly.
- Updates docs/systems/federation.md to describe direction-branched flow.
2026-04-26 22:01:14 +02:00
Jannis Braun 4d4dc383d7 feat(federation): pass explicit intent at every ensurePeered call site
- social.ts friend-add: user_action, with 409 peer_pending_local_admin
  when gate fires
- /peer/ensure: user_action, surfaces peeringStatus: 'admin_required'
- sendCallRelay (typing warm-up + call relay): system intent
- federationWorker resolvePendingPeers: system intent (defensive — gate
  is unreachable from here since pending rows already exist)
- CallRelayFailureReason: peer_admin_required added (mapped to
  peer_transient_failure on the user-facing event surface, since system
  intent should never legitimately surface admin_required)
- Test files: thread intent arg through racePeering and ensurePeered
  calls (positional shift from racePeering signature change)
- outboundGate.test.ts: tighten noUncheckedIndexedAccess access via
  non-null assertions after toHaveLength()
- docs/systems/social.md: peer_pending_local_admin error code documented
2026-04-26 21:37:19 +02:00
Jannis Braun b687bfe721 feat(federation): schema for outbound peering gate (direction column, subscribers, notifications) 2026-04-26 21:02:27 +02:00
Jannis Braun fa7a5f831d feat(federation): /approve forwards token + /approve & /peer/initiate capture on 202 + 200 clear
Three changes to keep the outbound /peer/accept call sites consistent
with the new approval-token mechanism:

1. /approve outbound body now forwards approvalToken from the queued
   peer_approval_requests row when present. Receiver's awaiting_approval
   branch verifies it and promotes mutual approval. Legacy null-token
   rows omit the field; receiver falls through autoAccept gate. Spec §3.7.

2. /approve and /peer/initiate 202 paths now capture the approvalToken
   returned by the remote and store it on the local federation_peers row.
   Without this, the symmetric autoAccept=0 mutual-approval flow could
   not verify on the eventual return /peer/accept. Spec §3.7.

3. /approve and /peer/initiate 200 paths now include approvalToken=null
   in the activation UPDATE — single-use lifecycle hygiene per §3.2.

Test coverage: +6 tests (4 in approveOutbound, 2 in peerInitiateOutbound).
Total: 301 → 307. Web tsc clean.
2026-04-26 11:52:04 +02:00
Jannis Braun 058eb992e4 feat(federation): verify approval token before awaiting_approval → active
Receiver-side defense — closes the trust-bypass class the cheap fix
(4533e36) cannot cover. The /peer/accept handler's awaiting_approval
branch now requires an approvalToken matching the one stored on the
local peer row before promoting to active. Without a match:
- autoAccept=0 falls through to queueApprovalRequest (no bypass; new
  approval-request queued, existing awaiting_approval row untouched).
- autoAccept=1 falls back to permissive promotion (no regression vs
  prior behavior, since autoAccept=1 would accept any inbound regardless).

Successful match also deletes any stale approval-request row for the
origin to prevent debris accumulation from prior bypass attempts.

Refactor: queueing path extracted to a top-level queueApprovalRequest()
helper so both the no-existing-peer case and the awaiting_approval
mismatch fallback share one implementation. Helper generates a fresh
single-use token on every call.

Adds 'accept_awaiting_approval_fallback' variant to PeerActivationReason
to distinguish the autoAccept=1 fallback path from the verified path
in onPeerActivated audit logs.

Spec §3.5, §3.6.

Test counts: 289 → 301 (+12).
2026-04-26 11:48:33 +02:00
Jannis Braun bd70950de7 test(federation): receiver accepts home-queued friend_request_create 2026-04-25 22:23:04 +02:00
Jannis Braun d88d369a82 fix(social): friend_request_sent local broadcast must carry target profile
The local branch was reusing friendRequestPayload (built for
friend_request_received with user=sender) for the sender-side
friend_request_sent broadcast. Tab B on the sender would render
the sender's own avatar where the target's should appear.
Match the federated branch — sent broadcast carries target.
2026-04-25 22:18:47 +02:00
Jannis Braun 1ad1fd1b0c feat(social): broadcast friend_request_sent on local request creation 2026-04-25 22:15:57 +02:00
Jannis Braun bf13e2a223 feat(social): federated branch — authority + self-friend + idempotency
Adds direction-aware idempotency and already-friends checks to
handleFederatedFriendRequest (after stub hydration, before transaction),
plus 6 tests covering authority defense, bare-host normalization, cannot_friend_self,
already_friends, same-direction idempotent 200, and opposite-direction 409.
2026-04-25 22:12:08 +02:00
Jannis Braun 72f4b170f6 test(social): federated branch peer-status + lookup-failure mappings 2026-04-25 22:09:42 +02:00
Jannis Braun 0574a10ff3 feat(social): federated branch for POST /api/social/requests (happy path)
Refactors the POST handler into handleLocalFriendRequest + handleFederatedFriendRequest helpers. The federated branch resolves the target domain, ensures peering, looks up the remote user, creates/hydrates a replicated stub, and writes a transactional (friend_requests + mutation_log + outbox) event with relayMessageId set. Also exports hydrateReplicatedUserProfile from federation.ts and adds the T11 happy-path test.
2026-04-25 22:04:36 +02:00
Jannis Braun 03cef0e6b3 feat(federation): POST /api/federation/users/lookup endpoint 2026-04-25 21:32:49 +02:00
Jannis Braun 069a1525ea feat(federation): per-peer rate limiter for user lookup (60/min) 2026-04-25 21:28:44 +02:00
Jannis Braun b5b48e407e test(social): note the shared-state harness contract
Per code-review suggestion: add a brief comment explaining why
module-level sqlite/testDb/app reassignment works (mock getter
closes over the current binding) and what would break it
(top-level it, .concurrent describe). Prevents a future foot-gun.
2026-04-25 19:17:50 +02:00
Jannis Braun 5f5590a3c9 fix(social): normalize username lookup on POST /api/social/requests
Registration canonicalizes usernames to lowercase (auth.ts:32),
but the friend-request endpoint compared with strict eq() against
raw user input, so 'Bob' returned 404 even when 'bob' existed.
Trim and lowercase before lookup, matching the rest of the auth
boundary. Empty-after-trim now returns 400 (was: 404).

Tests appended to social.test.ts as a second describe block
sharing the harness from Task 1.
2026-04-25 19:14:31 +02:00
Jannis Braun e99e0e7862 fix(social): tighten comment on federated-stub filter
Drop the file-path reference per code-review suggestion (paths in
comments rot); keep the load-bearing why — substring of the domain
matches every stub because stubs are stored as <homeUserId>@<domain>.
2026-04-25 19:12:40 +02:00
Jannis Braun b37d3bfa29 fix(social): apply discover-equivalent filters to /api/social/search
Tombstoned users, replicated federated stubs, and users with
discoverable=0 were all surfacing in Add Friend search results.
Add the three WHERE filters that /api/social/discover already
applies. Federated users continue to be surfaced via the
client-side cross-instance fan-out in socialStore.searchUsers.

New tests: social.test.ts covers all five filter cases plus
existing self-exclusion and displayName-match behaviours.
2026-04-25 18:42:05 +02:00
Jannis Braun f0d6bf2ed9 fix(federation): persist remote instance_name from approval-requests/:id/approve handshake response
Third initiator path that calls remote /peer/accept. Mirrors performHandshake (auto-peer) and /peer/initiate (admin-initiate) — same try/catch parse, same null-or-non-empty-string guard. Caught in final review of #33; same root cause as Bug #1, bundled rather than fragmented to a new backlog item.
2026-04-25 11:18:12 +02:00
Jannis Braun a9bf5aeb53 fix(dm): include federatedId in POST /api/dm idempotent existing-DM response
Fresh-create returned {id, ownerId, federatedId, createdAt, members, lastMessage}; the existing-DM path returned the same shape minus federatedId. Inconsistency was a footgun for any future feature reading federatedId from this response — fresh-create tests would pass while idempotent path would break. One-line addition to the result builder.
2026-04-25 00:59:16 +02:00
Jannis Braun 618056659e fix(federation): persist remote instance_name from /peer/initiate handshake response
Mirrors the previous performHandshake fix for the admin-initiated path. /peer/initiate now parses the remote's instanceName from the /peer/accept response body and writes it alongside status='active'.
2026-04-25 00:48:42 +02:00
Jannis Braun eba16e16a3 fix(federation): include own instanceName in /peer/accept response body
Bidirectional handshake exchange. Today the responder learns the initiator's instance name from request body but the initiator never learns the responder's. Adding {instanceName} to the response body lets the initiator persist it on its side (next commit). Field is optional so older peers omitting it cause no ill effect.
2026-04-25 00:42:11 +02:00
Jannis Braun 7fe9476e57 fix(federation): persist peer instance_name on /peer/accept activation paths
Previously /peer/accept read body.instanceName only when queueing for admin approval. The four paths that mutate federation_peers (rejected→active override, awaiting_approval→active, pending→active, new-peer create) all wrote status='active' without persisting instance_name. Result: every peer established via direct handshake had instance_name = NULL forever. Anywhere peerLabel was rendered fell back to origin hostname.

Active/needs_attention idempotent early-return path deliberately left alone — same security posture that already refuses to overwrite hmac_secret from unauthenticated requests on already-active peers.

Existing live NULL rows are repaired post-deploy via manual UPDATE statements (see plan).
2026-04-25 00:35:42 +02:00