Commit Graph
69 Commits
Author SHA1 Message Date
Jannis Braun 3ebdd048bd feat(dm): read-only guard blocks message create/edit/delete in a Deleted-User 1-on-1 (S3) 2026-07-02 15:47:18 +02:00
Jannis Braun 7d8c9c9d8d feat(federation): peer_reset_pending guard during limbo window 2026-07-01 22:32:40 +02:00
Jannis Braun 209aef7e9d fix(uploads): graceful fallback for browser-unplayable video (HEVC .mov)
macOS screen recordings are HEVC inside a .mov container, which Chromium,
Firefox and stock Electron can't decode. The file uploaded fine and a
server-side ffmpeg poster was generated, but inline <video> playback failed
silently — stuck at 0:00 with no error, since AttachmentRenderer had no error
handling. Root cause: the system had no concept of web-playability.

Server detects, client degrades:
- mediaPlayable.ts: classifyVideoPlayable(mimetype, codec) — tri-state
  (false = known-undecodable e.g. HEVC/ProRes, true = web codec in web
  container, null = unknown/optimistic). Never widens `false` beyond codecs
  that fail everywhere, so ffmpeg-less instances keep prior behaviour.
- probeMediaMeta now captures the video codec_name; the upload finish hook
  stores the verdict in the new attachments.playable column (migration 0007).
- Flag propagated through every serializer: space messages, DMs, WS, and
  federation relay (outbound + inbound) — federation-compatible.
- VideoAttachment component: playable===false renders a download card (poster
  + "Can't play here — download" + name/duration/size) with no dead-player
  flash; otherwise plays inline with an onError fallback to the same card.

Specs updated: uploads.md, database.md, federation.md.
2026-06-30 17:38:11 +02:00
Jannis Braun 3c7bb02901 fix(dm): ownership transfer divergence after back-and-forth — canonicalize ownerHomeInstance + normalize authority checks
Manual ownership transfers between two federated instances diverged because
`dm_channels.ownerHomeInstance` was stored as a BARE host (`orbit.ddns.net`)
for federated owners — via `transferGroupDmOwnership` copying `users.homeInstance`
verbatim — while `sourceInstance` always arrives as a full URL on the wire.
`processOwnershipTransferEvent` and `processMemberRemoveEvent` then compared the
two with strict equality and rejected legitimate inbound events as
`unauthorized_source`, keeping ownership permanently divergent across peers.
Live DB inspection on the two test instances confirmed both rows (nova + orbit)
had a BARE `owner_home_instance`, matching the bug report exactly.

Three compounding fixes:

1. Receiver authority checks now compare via `normalizeOriginForCompare` so
   legacy bare-vs-full rows accept legitimate transfers (and kicks).
2. New `canonicalizeHomeInstance` helper in `federationAuth.ts`; every write
   site that persists `ownerHomeInstance` (`transferGroupDmOwnership`, group DM
   creation, lazy federation in member-add, `processMemberAddEvent` bootstrap,
   `processOwnershipTransferEvent` receiver storage) routes through it. Full URL
   is the canonical storage form, matching how `sourceInstance` arrives.
3. `dm_owner_updated` WS event extended with optional `newOwnerHomeUserId` and
   `newOwnerHomeInstance` fields. Client `updateDmOwner` writes them when
   present and leaves existing values untouched otherwise (legacy-server safe).
   Without this, `getOwnerInstanceForDm` returned the previous owner's home
   after a successful WS broadcast, routing the next owner-only op to the wrong
   instance.

Coverage: new `federation.ownershipTransfer.test.ts` (7 receiver tests including
the headline bare-vs-full regression and the dedup replay guard); new bare-vs-full
case in `federation.kick.test.ts`; two new client-side cases in
`groupDm.ownerRouting.test.ts` covering both the extended-payload write path and
the legacy-server passthrough. Tests: 1053 server + 364 web, all green.

Specs updated: `dm-system.md` historical bugs + frontend handler table + WS
state-change events table; `federation.md` `ownership_transfer` receiver flow;
`websocket.md` event-fields table.
2026-05-10 22:38:03 +02:00
Jannis Braun b6842c5590 fix(dm): owner-only group DM ops accept federated target identification
Transferring ownership or kicking a member surfaced "Target user is not a
member of this DM channel" whenever the target was a federated user.

Root cause: the client passed `canonical.id` from `useCanonicalUserView`,
which returns the user's HOME id when the home view is in the userViews
cache. After owner-routing the request to the owner instance, that
instance's `dm_members.userId` (its own local replicated id) never
matched the home id, so `isDmMember` returned false. The same failure
mode applied across any cross-instance scenario where the
channel-serving instance and the owner-serving instance disagree on the
local replicated user id for the same federated user.

Fix: both endpoints now accept federated identification, mirroring the
existing pattern on `POST /api/dm/:id/members`:

- `POST /api/dm/:id/transfer` body: `{ newOwnerId? } | { homeUserId, homeInstance }`.
  Federated args win when both are supplied (strictly more specific).
- `DELETE /api/dm/:id/members/:targetUserId` reads optional
  `?homeInstance=<origin>` query; when present, the URL segment is
  treated as a homeUserId and resolved via `resolveOrCreateReplicatedUser`.

Client `api.dm.kickMember` and `api.dm.transferOwnership` gain an
optional `federated` argument; `DmRosterPanel` and `MobileGroupDmInfo`
pass it whenever the target has `homeUserId` + `homeInstance` populated.

Adds 5 server tests (3 transfer + 2 kick) covering federated targets,
the federated-wins-over-local precedence rule, and federated-non-member
rejection. Updates 2 client routing tests and 2 DmRosterPanel test
assertions for the new signature. Updates `docs/systems/dm-system.md`
and `docs/systems/api.md`.

Server: 965 tests pass (was 960). Web: 362 tests pass (was 360).
2026-05-10 22:09:14 +02:00
Jannis Braun 24240f24f4 fix(janitor): protect dm_channels.icon files from cleanup (owner + receiver)
getProfileReferencedFilenames() didn't include dm_channels.icon, so the
storage janitor deleted group DM icons within ~1 hour:

- Owner instance: PATCH /api/dm/:id leaves an attachments row with
  messageId=null and dmMessageId=null. After 1h, getUnlinkedAttachments
  flags it and cleanupStorage phase 2 deletes the file because the
  filename isn't in profileReferenced.

- Receiver instance: downloadProfileAsset writes the icon directly to
  uploadDir with no attachments row. cleanupStorage phase 1 treats it
  as orphaned and deletes it.

Fix: include dm_channels.icon (non-null, not soft-deleted, not http://)
in the profile-referenced set. Soft-deleted DMs are excluded so their
files still get reaped by cleanupSoftDeletedDmChannels. Absolute URLs
are skipped because they live on a remote instance.

Also mirror the avatar precedent at the PATCH endpoint by deleting the
new icon's standalone attachment row — the file is now protected via
dm_channels.icon, matching users.ts:473.
2026-05-10 21:18:58 +02:00
Jannis Braun 063ed2dd64 refactor(federation): extract normalizeIconForWire helper 2026-05-10 19:31:06 +02:00
Jannis Braun 04d7910077 feat(federation): bootstrap carries group name + icon + metadataUpdatedAt 2026-05-10 19:23:47 +02:00
Jannis Braun 7f8d650cab refactor(server): extract transferGroupDmOwnership helper + wrap in transaction 2026-05-10 18:56:01 +02:00
Jannis Braun 35c720429e feat(server): POST /api/dm/:id/transfer — manual ownership transfer 2026-05-10 18:46:41 +02:00
Jannis Braun ce11fd12b5 refactor(server): extract evictUserFromDmVoiceRoom helper + drop redundant optional chains 2026-05-10 18:42:25 +02:00
Jannis Braun 78035bfc64 feat(server): DELETE /api/dm/:id/members/:targetUserId — owner kick
Refactors the leave-DM destructive core into a shared `removeDmMember`
helper and adds a kick endpoint that reuses it. Both endpoints write the
`member_removed` system message, delete the dm_members + read_states
rows, broadcast `dm_member_removed`, and queue a federation
`member_remove` event with the appropriate `reason` ('leave' | 'kick').

Branching invariants preserved by the helper:
- Ownership transfer fires only on self-leave when the leaver was the
  owner. Kicks cannot orphan a group (the owner is still present), so
  the transfer block is skipped.
- Soft-delete on last-member-empty fires only on self-leave. Kicks are
  guaranteed to leave the owner behind, so the channel can never be
  empty after a kick.

Endpoint validation:
- 1-on-1 DM → 400 'Cannot kick from a 1-on-1 DM'
- Caller not the owner → 403 'Only the group owner can remove members'
- Self-target → 400 'Owners cannot kick themselves; use leave instead'
- Target not a member → 404
- Channel missing or soft-deleted → 404

The kicked user is also evicted from the DM voice room (mirroring the
self-leave path) and receives `dm_channel_closed` so the client closes
the channel locally.
2026-05-10 18:35:29 +02:00
Jannis Braun a06776fd86 feat(server): PATCH /api/dm/:id — group name + icon update 2026-05-10 18:23:48 +02:00
Jannis Braun 2afe230de0 feat(shared): group_metadata_update event + extended FederationGroupPayload (with safe defaults at producers) 2026-05-10 18:00:07 +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 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 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 ce0b2d0e15 feat: allow any group DM member to add friends, not just owner
Remove the owner-only gate on POST /api/dm/:id/members. The S2S relay
already accepts member_add from any HMAC-verified peer, and the UI
already shows the add button to all group DM members. Only the
server-side check was blocking non-owners.
2026-04-09 02:15:44 +02:00
Jannis Braun 213325edaa feat: relay dm_close and dm_reopen to federated peers
Queue dm_close relay in DELETE /api/dm/:id after broadcasting locally,
and queue dm_reopen relay inside the myDm.closed === 1 branch of POST
/api/dm so peers are notified only on actual reopen, not new DM creation.
2026-04-07 22:26:14 +02:00
Jannis Braun 658eba556e feat: include federatedId in dm_channel_created WS events
Ensures the client receives federatedId for dedup when DM channels
are created, reopened, or bootstrapped via REST handlers.
2026-04-07 19:56:23 +02:00
Jannis Braun 662143bf08 feat: lift DM gates for federated users
Remove requireLocalUser from DM routes, include DMs in federated
ready payload, replace blanket dm_* WS gate with call-only blocklist.
DM calls remain gated (separate scope).
2026-04-07 19:48:07 +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 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 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 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 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 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 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 4150e32b84 feat(federation): resolve federated identities in POST /api/dm/group
Rewrite the group DM creation endpoint to accept identity objects
(GroupDmUserIdentity) instead of raw user ID strings. Each identity
is resolved to a local database user via resolveOrCreateReplicatedUser
for federated users or direct ID lookup with resolveLocalUser fallback
for local users. Dedup and caller-exclusion checks now operate on
resolved local IDs rather than input IDs.
2026-03-26 23:16:53 +01:00
Jannis Braun 4422d97ba7 fix(dm): deduplicate callerUser fetch and align federation guard pattern
- Hoist callerUser DB query above the federation block so it's fetched
  once and reused for response building, federation ID assignment, and
  relay payload construction (was fetched 3 times).
- Add homeInstance !== domainOrigin guard to finalTargets augmentation,
  matching the existing pattern in POST /api/dm/:id/members.
2026-03-26 22:30:07 +01:00
Jannis Braun 342eb8cca3 feat(dm): add POST /api/dm/group endpoint for creating group DMs
Adds a dedicated endpoint for creating group DMs with 3-10 members.
Validates friendship, deduplication, and member caps. Includes federation
relay support for remote instance members.
2026-03-26 22:24:05 +01:00
Jannis Braun bec4c2446f fix(dm): restore ownerId=NULL semantics for 1-on-1 DM channels
- 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
2026-03-26 22:16:55 +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 6a83b6c6a5 fix(federation): compute relay targets before member deletion in leave handler
getGroupDmTargetOrigins() was called after the leaving user's dm_members row
was deleted, causing their instance to be excluded from member_remove and
ownership_transfer relay fan-out if they were the only member from that peer.
Move the target origins and leaving user lookups to before the deletion.
2026-03-26 20:19:55 +01:00
Jannis Braun f072568b8a feat(federation): relay membership mutations from dm.ts to federation outbox
Hook POST /api/dm/:id/members and DELETE /api/dm/:id/members to the
federation outbox so membership changes propagate to peer instances:

- Assign federatedId when a group DM first gains remote members
- Relay member_add events with group metadata and participant-aware routing
- Relay member_remove (leave) events to peer instances
- Relay ownership_transfer when the departing user was the group owner,
  updating the federated owner columns accordingly
2026-03-26 20:14:52 +01:00
Jannis Braun 62f1d637ac feat(federation): surface sourceMessageId/sourceInstance in DM message payloads
Both REST and WebSocket DM message payloads now include the relay
identity fields. This enables client-side deduplication of relay copies.
2026-03-26 17:54:24 +01:00
Jannis Braun b49be65dcd refactor(federation): replace outbox hooks in dm.ts with queueDmRelay() 2026-03-26 06:15:13 +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 32a0c2e618 feat(federation): hook outbox and mutation log into DM message and reaction handlers
Wire appendMutationLog + queueOutboxEvent + buildRelayPayload into all
DM mutation paths so federation peers receive relay events:

- REST: POST /api/dm/:id/messages, PATCH /api/dm/messages/:id,
  DELETE /api/dm/messages/:id
- WebSocket: dm_message_create, dm_message_edit, dm_message_delete,
  reaction_add (DM path), reaction_remove (DM path)
- Fix buildRelayPayload parameter types to accept optional replyToId
  and editedAt (matching DmMessageWithUser's optional fields)
2026-03-25 21:14:34 +01:00
Jannis Braun 561c9bc4f3 feat: include width, height, duration in all attachment serialization 2026-03-21 17:20:04 +01:00
Jannis Braun bc408235c9 feat: add Activity type system, DB migration, and self-only showActivity in sanitizeUser
- 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
2026-03-21 01:38:36 +01:00