Commit Graph
1489 Commits
Author SHA1 Message Date
Jannis Braun 40c27ed90f fix(chat): apply loadMessages dedup to force=true callers as well
useWebSocket's ready handler calls loadMessages(channelId, true) from
two adjacent code paths in the same handler invocation (remote-space
refresh + per-origin cache clearing). Both fired in parallel because
force=true bypassed dedup, producing the duplicate /messages requests
visible in playwright network traces even after the parallel-mount
dedup landed.

Coalescing force=true with an in-flight call is safe: the requests hit
the same endpoint with the same params and would return the same data,
and the force caller still gets a fresh result via the shared Promise.
2026-05-05 20:52:05 +02:00
Jannis Braun a189cbfba3 fix(chat): dedup parallel loadMessages/loadMoreMessages per channel
Live network trace via playwright showed every channel mount firing TWO
parallel GET /channels/:id/messages requests (AppLayout + MessageList
both call loadMessages on mount; neither can be removed because
MessageList is also rendered by VoiceChatPanel which doesn't call it),
and every fast-scroll burst firing N parallel loadMoreMessages calls
(handleScroll closure shares !isLoadingMore=true across the burst).

The hasMore-based guard only deduplicates calls AFTER the first
completes — it does not collapse parallel callers. On a NAT hairpin'd
LAN where some federated TCP connections hang, having N hung parallel
fetches keeps the pagination skeleton stuck while any one is still
waiting on the 30 s api-client timeout.

Fix: in-flight Promise dedup at the chatStore level. Module-level Maps
keyed on channelId; concurrent callers receive the same Promise.
Cleared in finally with self-check so a force-reload mid-flight isn't
dropped. force=true bypasses dedup (WS reconnect intent).
2026-05-05 20:47:03 +02:00
Jannis Braun d793ab8f78 fix(message-list): sticky pagination skeleton on channel switch
Two cooperating bugs let the pagination skeleton stick at the top of the
chat across channel switches: (1) the browser's post-clamp scroll event
on channel switch fired the load-more block on the new channel, and
(2) useDelayedLoading's threshold timer refreshed displayStart on every
fire, extending the minDisplay window unboundedly when isLoading cycled.

- useDelayedLoading: stamp displayStart only on the false→true show
  transition (functional setShow form to avoid stale closures).
- MessageList: suppressNextLoadMoreRef armed in Effect 3, consumed by
  the load-more block on the next scroll event, with a 250 ms fallback
  disarm so legitimate user scrolls aren't silently dropped. Suppression
  scoped to the load-more block only.
- Regression test for the displayStart refresh bug.
- Updated docs/systems/message-list.md history.
2026-05-05 19:56:26 +02:00
Jannis Braun 97a8d982d5 Merge feat/s2s-federated-friends-presence-username
S2S federated-friends username + presence relay. Closes the original repro
(youruser on nova saw pbtest3/pbtest4 from orbit as snowflake-ID with stale
offline status when not client-federated to orbit).

Phase A — username:
- Stub creation uses realname@domain when wire snapshot carries a username hint
- profile_update payload extended with username, receiver applies displayName
  ?? username fallback so null-displayName home users don't clobber stubs
- New /api/federation/users/by-home-id reverse-lookup endpoint + helper
- Stub backfill worker heals legacy snowflake-named stubs (per-peer, on
  activation + startup pass)

Phase B — presence:
- New S2S presence_update relay event + sender helper + 5 call sites
- Receiver updates stub status + broadcasts WS to friends/DM/spaces
- Local broadcast scope widened to friends + DM + spaces (matches user_updated)
- Peer-activation snapshot (relationship-scoped) + deactivation mark-stubs-offline
- Outbox-only delivery (no mutation log) — presence is ephemeral

Follow-on fixes after live verification:
- Outbox-to-relay event rebuild was missing presenceUpdate (allowlist gap)
- Profile snapshot now carries status — freshly-friended online remotes show
  online immediately, not stuck at offline until next transition
- spaceStore.updateMemberPresence patches userViews so the avatar green dot
  re-renders on WS presence_update (was reading from a different cache than
  the 'Online' text label)

Docs: federation.md, activity-presence.md, social.md, websocket.md updated.
Verified live on Pi + VM (nova + orbit).
2026-05-05 16:48:19 +02:00
Jannis Braun 1effb1c53f fix: live presence on freshly-friended remotes + green dot in same session
Two follow-on bugs from the initial S2S presence rollout:

(1) New friend stuck offline until they reload: presence_update fires only on
    transitions, so a remote user already online when their stub is created
    locally never receives a relay event seeding their actual status. The
    stub defaulted to 'offline' at creation and stayed there until the next
    transition. Fix: extend FederationRelayProfileSnapshot +
    FederationUserLookupProfile with status. Sender-side buildProfileSnapshot,
    getDmParticipants, and lookup endpoint responses populate it for native
    users only (replicated stubs hold stale status owned elsewhere).
    resolveOrCreateReplicatedUser uses hints.status to seed the new row's
    status column. Threaded through every call site (DM participants, group
    bootstrap, friend events, ownership transfer). Stub backfill worker also
    heals existing rows whose status was stuck at 'offline' from creation.

(2) 'Online' text updates but green avatar dot stays grey on the same page:
    spaceStore.updateMemberPresence patches members[] (which feeds space UIs)
    but never patches userViews — the cache useCanonicalUserView reads from.
    The Avatar in FriendItem reads canonical.status; the text reads
    friend.status (socialStore). Two sources, one stale until full
    user_updated arrives. Fix: updateMemberPresence now mirrors status into
    matching userViews entries, so canonical-view consumers re-render with
    fresh status the moment the WS event lands.
2026-05-05 16:44:22 +02:00
Jannis Braun d2bd7987c1 fix(federation): include presenceUpdate in outbox-to-relay event rebuild
The outbox worker rebuilds FederationRelayEvent objects from stored JSON via
an allowlist of known fields. presenceUpdate was missed when presence_update
events shipped, so peers received events with eventType='presence_update' but
no payload — rejected with missing_presence_update_payload on every tick.
2026-05-05 16:28:07 +02:00
Jannis Braun ba0f8637f5 docs(federation): document username on profile_update + new presence_update relay + stub backfill
- federation.md §10: extend FederationProfileUpdatePayload with username, document
  receiver fallback. Add Presence Sync sub-section: event shape, sender call sites,
  outbox-only (no mutation log) policy, peer-lifecycle hooks, flap recovery
  semantics. Add Stub Username Backfill sub-section + new
  /api/federation/users/by-home-id endpoint.

- activity-presence.md: resolve drift — line 147 previously claimed S2S
  presence_update relays existed but the code didn't ship them. Now points to
  federation.md §10 which describes the actually-implemented mechanism. Connect/
  Disconnect Flow updated to reflect collectProfileBroadcastTargetIds recipient
  set + S2S queueing.

- social.md / websocket.md: presence_update recipient column now reflects
  friends + DM + space co-members (matches user_updated), plus federated stub
  presence sourced via S2S.
2026-05-05 16:19:03 +02:00
Jannis Braun 1cb151d3a9 feat(federation): peer-lifecycle presence hooks (snapshot on activate, mark-offline on deactivate)
onPeerActivated re-emits a presence_update for every relationship-related
online native to the activating peer (relationship = friend with peer-stub /
DM-mate with peer-stub / replicatedInstances opt-in for peer origin). Scope
bounded by relationship count, not native count — flap recovery cost stays
proportional to actual interaction surface.

onPeerDeactivated flips every replicated stub from that peer to offline and
broadcasts a local presence_update so connected friends/DM-mates/space-co-members
see them disappear immediately, instead of seeing stale online until next signal.

Re-snapshot on every activation (incl. health-check unreachable→active flap)
is load-bearing for correctness — markPeerStubsOffline ran on the prior
deactivation and presence is not in the mutation log.
2026-05-05 16:12:22 +02:00
Jannis Braun ad1a0f7164 fix(presence): broadcast presence_update to friends + DM members + space members
Six WS sites that previously broadcast presence_update to spaces only now use
collectProfileBroadcastTargetIds (the same recipient set as user_updated):
  - ws/handler.ts finalizeDisconnect (offline)
  - ws/handler.ts auth path (online)
  - ws/events.ts handlePresenceUpdate (manual idle/dnd/online)
  - ws/events.ts handleActivityUpdate (rich activity changes)
  - routes/users.ts showActivity-toggle clear
  - routes/users.ts status PATCH

Friends with no shared space + DM-only co-members now see each other's
online/offline transitions live, matching user_updated semantics. Federated
stub presence broadcasts (Task B3) use the same helper, so cross-instance
recipients are uniform.

Updates one assertion in social.federated.test.ts that asserted the old
snowflake-style stub username (now realname-based per A1).
2026-05-05 16:06:45 +02:00
Jannis Braun 53fe7d2b53 feat(federation): process inbound presence_update relay events
processPresenceUpdateEvent updates the local stub's status and broadcasts a
WS presence_update to friends + DM members + space co-members of that stub
via collectProfileBroadcastTargetIds. Closes the doc/code drift in
activity-presence.md:147 — federated stubs now have their status projected
by the home instance as documented.

Strict attribution: payload.homeInstance domain must equal source peer
domain. Silently no-ops when no local replica exists (peer broadcast fanout
covers all peers, not all hold a stub).
2026-05-05 16:03:18 +02:00
Jannis Braun 613424e1c7 feat(federation): queue S2S presence_update on auth/disconnect/status/activity changes
New FederationPresenceUpdatePayload + queuePresenceRelay() helper. Five WS
sites now project the native user's status (and optional activities) to all
active peers via the outbox: WS auth-success, finalizeDisconnect,
manual presence_update, activity_update, showActivity-toggle clear.

Outbox-only (no mutation-log entry) — presence is ephemeral; the upcoming
peer-activation hook re-emits a fresh snapshot so peers recovering from
unreachable converge without history replay. No-op for replicated users.
2026-05-05 16:01:28 +02:00
Jannis Braun bdbc90ebd2 feat(federation): backfill snowflake-named replicated-user stubs
Heals existing legacy stubs (created when resolveOrCreateReplicatedUser used
homeUserId@domain) by asking the peer for the canonical username via
lookupRemoteUserByHomeId and rewriting the local row. Idempotent and
collision-safe.

Wired into onPeerActivated (per-origin) so future peer flaps re-attempt for
stubs whose home was unreachable on a prior pass, and into a one-shot pass at
startupBootstrapSync for all currently-active peers (not just first-time
lastSyncedAt=0 peers).
2026-05-05 15:57:37 +02:00
Jannis Braun b2faf5afaa feat(federation): add /users/by-home-id reverse lookup for stub backfill
HMAC-authenticated, rate-limited (60/min/peer) endpoint that resolves a
homeUserId on this instance to its canonical username + profile snapshot.
Native non-deleted users only. Mirrors /users/lookup's auth shape.

Adds lookupRemoteUserByHomeId to federationLookup.ts as the client-side
helper. Used by the upcoming stub-backfill worker on peers that hold legacy
snowflake-named replicas of users now visible by their real handle.
2026-05-05 15:54:06 +02:00
Jannis Braun 097eb9a2ef feat(federation): preserve effective displayName across profile_update relay
FederationProfileUpdatePayload gains `username`: the home user's canonical
handle. Receiver applies displayName ?? username so stubs whose home user has
no displayName show the real handle instead of getting clobbered to null.
Mirrors the existing fallback in hydrateReplicatedUserProfile. Username itself
is immutable on the home instance, so the receiver does not rewrite the stub's
username column on profile_update.
2026-05-05 15:48:56 +02:00
Jannis Braun 1d353c994b fix(federation): create replicated-user stubs with realname@domain when hint provided
When friend_request_create / friend_add / DM relay carries a profile snapshot,
the canonical-username hint is now used as the stub's local-part. Stubs created
purely from S2S (no client-federation) now display the human-readable handle,
not the homeUserId snowflake. Falls back to the snowflake-id scheme only when
no hint is available.
2026-05-05 15:46:26 +02:00
Jannis Braun 702d539e23 fix(register): read home JWT from localStorage so step-2 avatar uploads
RegisterPage step 2 writes the new token to localStorage but defers
initSession (and thus the authStore.token write) until after the avatar
upload, to keep AuthRedirect from yanking the user off /register
mid-upload. The home-origin branch of setTokenForOriginResolver was
reading authStore.token, so getTokenForOrigin('') returned null during
that window — transferStore.startUpload threw "not authenticated" and
the surrounding catch-and-ignore silently dropped the avatar. Align the
resolver with the home api client and read localStorage so both paths
share one source of truth for the home JWT.
2026-05-05 12:53:48 +02:00
Jannis Braun 49e9047005 feat(client-federation): user-view cache for cross-instance DM render
Fixes a render bug where a federated user (e.g. axel@nova) appeared with
the federation globe icon and a broken avatar when viewed on his own home
instance. Root cause: `populateFromReady` is first-wins by federatedId and
discards the entire skipped DM payload — including its `members` array —
so when a sibling instance's ready arrived first, the home instance's view
of every shared user was dropped on the floor.

Adds a render-only `userViews` cache that mirrors the `dmAlternatives`
philosophy: information from skipped ready payloads is preserved for
rendering. Every wire surface that delivers a User upserts into the cache
regardless of dedup outcome; render sites read through a Zustand selector
hook to surface the home view when one is loaded. The DM channel ingestion
race is left untouched — the existing no-flapping invariant on origin
reconnect is intentional and load-bearing for failover.

Layered changes:

- `identity.ts`: `normalizeOriginToHost`, `canonicalUserKey`,
  `isDeliveryFromHome`, `isFederationGlobeApplicable` — single helpers
  for origin/host normalization and the home/stub tier decision.
- `spaceStore.ts`: `userViews` Map, `UserViewEntry` type, `upsertUserView`
  action with the home-wins preference rule, prune by `deliveredBy` in
  `removeInstanceSpaces` (mirrors `dmAlternatives` cleanup), `reset`
  clears.
- `userViewLookup.ts`: `useCanonicalUserView` (Zustand selector hook for
  React) + `getCanonicalUserView` (sync getter for non-React paths).
  Render reactivity is structural via the selector, not coincidence on
  legacy update paths.
- `populateFromReady` upsert pass runs BEFORE the federatedId dedup so
  members of skipped DMs still reach the cache.
- WS handlers (dm_message_*, message_*, user_updated, member_joined,
  friend_request_*, dm_channel_created, dm_member_added) and REST
  hydrators (socialStore, discoverStore, mutuals) feed the cache with
  their delivering origin.
- Render-site routing through `useCanonicalUserView` at every audited
  user-rendering site (sidebar, header, search, message bubble, reply
  chips, profile popout/modal, group settings, voice tiles, mention
  chips, member lists, friends, invites). Self-rendering sites compose
  alongside via existing `isSelf`/`resolveDisplayIdentity`.
- Globe predicate hoisted to `isFederationGlobeApplicable` and applied
  at three sites, gating on `domain !== window.location.host` so we
  never show the globe for users whose home IS our own.

Tests: 31 new unit tests across `identity`, `userViews` store, and
`userViewLookup`. Full suite 276/276.

Docs: `client-federation.md` §3 gains a "User View Cache" section
parallel to "DM Origin Failover"; `dm-system.md` notes the new store
action and WS handler upserts.

Bug 3 (federation profile-sync gap — orbit's stale profile data on
nova-Axel after a clear/color-change on nova never propagated)
remains open. The user-view cache routes around it for the common case
(home instance is connected), but the underlying S2S relay gap is its
own diagnosis and follows in a separate branch.
2026-05-05 01:59:05 +02:00
Jannis Braun fb96b1457a fix(ui): paint avatar fallback gradient before broken-img onError
The fallback <div>'s inline style only carried `background` when src was
falsy, so an <img> that 404'd flipped to display:flex via onError but with
no gradient applied — surfacing a colorless letter for any broken avatar
URL. Always paint the gradient + fontSize; keep `display:none` as the only
src-conditional override.
2026-05-05 01:58:29 +02:00
Jannis Braun 06498836bb fix(federation): gate registry PUT on successful initial GET
Without a sync-ready gate, a transient GET failure during autoConnectAll
left the local registry Map empty/incomplete while `set()` still computed
`registryUpdatedAt = Date.now()`. The trailing `syncRegistry()` would then
PUT the empty payload with a fresh timestamp; the server's LWW guard
accepted it and legitimate registry rows were wiped — including
remote-instance entries the user never explicitly removed.

Add `_registrySyncReady` flag, set true only after a successful initial
GET. `syncRegistry()` short-circuits while false, so the degraded mode
(GET failed) is display-only: the Map is still populated locally from
\`replicatedInstances\` synthesis (status \`auth_expired\`) so the
Connections UI shows the user's known remotes, but mutations don't push.
On the next session where GET succeeds, localStorage cached tokens
reseed the registry and \`syncRegistry()\` pushes the merged authoritative
state — no data lost, sync deferred until we have a complete picture.

\`reset()\` clears the flag alongside the registry. Spec updated with the
sync-ready gate and degraded-mode behavior. Unit tests cover the gate
on initial fail, mutations during degraded mode, recovery on next
successful GET, and the synthesis fallback for empty replicatedInstances.
2026-05-05 00:20:51 +02:00
Jannis Braun c05c04181d fix(chat): keep scroll container mounted during initial-load skeleton
The initial-load skeleton was rendered as an early return that replaced
the JSX containing `containerRef` / `contentRef`. On slow loads, the
200ms threshold flipped the skeleton on before messages arrived, so when
messages did arrive every scroll-affecting effect (Effect A, the
ResizeObserver, the load handler, scrollend) re-fired exactly once
against null refs and bailed — and never re-attached because no dep
changed when the skeleton finally cleared. Net: chat opened scrolled to
the top instead of the bottom; saved-anchor restore was equally broken.

Render the skeleton as an absolutely-positioned overlay alongside the
(always-mounted) scroll container so all refs stay live across the
loading transition. Encode the constraint in the spec as the
"ContainerRef invariant" so future loading/empty/error UI doesn't
reintroduce the early-return pattern.
2026-05-05 00:19:00 +02:00
Jannis Braun b3cbe9403f build(desktop): add electronbuild.sh — multi-platform installer build
Convenience wrapper around `pnpm build:all` in packages/desktop that
builds installers for mac/win/linux × arm64/x64 and collects the
distributable artifacts into packages/desktop/installers/. Excluded
from rsync deploy by deploy.sh.
2026-05-04 00:48:20 +02:00
Jannis Braun 52b5f8e2b1 chore(brand): refresh app icons + add brand source artwork
Replaces desktop and web icon binaries with the new brand mark, ships
PNG raster exports under assets/brand/ alongside the existing SVG
sources, adds Alternative Styles artwork variants, and updates the
icon generation script and System Prompt doc. deploy.sh excludes
the new local-only artifacts (*.rtfd, assets/brand, electronbuild.sh,
multi-platform-roadmap.md) so they don't get rsync'd to the live
boxes. Drops ARCHITECTURE_AUDIT.md (obsolete).
2026-05-04 00:48:11 +02:00
Jannis Braun c7eb14d38e test(federation-identity): #18 unrelated federated user untouched 2026-05-04 00:32:48 +02:00
Jannis Braun dfd3c26922 test(federation-identity): #17 mixed-result fan-out 2026-05-04 00:32:40 +02:00
Jannis Braun ec37b241d9 test(federation-identity): #16 member_left WS broadcast on full delete 2026-05-04 00:29:24 +02:00
Jannis Braun d917ff5354 test(federation-identity): #15 rate limit 5/15min 2026-05-04 00:26:35 +02:00
Jannis Braun 8e3fa1d5aa test(federation-identity): #14 S2S idempotency 2026-05-04 00:23:51 +02:00
Jannis Braun 27b6a3f912 test(federation-identity): #13 attribution guard rejects mismatched origin 2026-05-04 00:23:02 +02:00
Jannis Braun 21b413f9bd test(harness): use DISABLE_RATE_LIMITS in spawned instances + rate-limit-enabled variant
spawnInstance now sets DISABLE_RATE_LIMITS=1 by default so unrelated tests
don't exhaust the shared 127.0.0.1 per-IP bucket. bootHomePlusRemotes/
bootTwoInstances accept an optional { enableRateLimits } that omits the env
for tests that need real enforcement, exposed via the explicit
bootTwoInstancesWithRateLimits() helper for Test #15 (rate-limit assertion).
2026-05-04 00:18:24 +02:00
Jannis Braun 22b01c35f5 feat(server): DISABLE_RATE_LIMITS env replaces NODE_ENV-based bypass
Overloading NODE_ENV='test' to silently disable rate limiting layered a
second meaning onto an env that already gates the test-only seed-peer
route. A dedicated DISABLE_RATE_LIMITS env (envBool semantics, matching
DISABLE_FEDERATION_WORKERS) makes intent explicit, defaults off in
production, and leaves room for tests that need to assert real rate-limit
behaviour to opt back in by simply not setting the var.
2026-05-04 00:17:10 +02:00
Jannis Braun f4c9ed880f test(federation-identity): #9 unreachable / #10 no_active_peer 2026-05-04 00:14:09 +02:00
Jannis Braun b5df3d5075 test(federation-identity): #7 #8 — owned-spaces 409 + transfer-and-retry
Also fixes per-IP rate limit exhaustion in test environments: @fastify/rate-limit
v9 has no skip(); use allowList(() => NODE_ENV==='test') which propagates to
per-route overrides via mergeParams Object.assign merge.
2026-05-04 00:13:14 +02:00
Jannis Braun 22daaafb63 test(federation-identity): #2 #4 #6 — all-remotes fan-out variants 2026-05-04 00:06:44 +02:00
Jannis Braun 3c0e024213 test(harness): document why peerInstances inserts two rows per direction
The original peerInstances comment framed the two-row pattern as a
band-aid for getOurOrigin's https://${DOMAIN} default. After
investigating a clean collapse to one row (PUBLIC_ORIGIN override on
each spawned instance), the deeper coupling surfaces:

  - extractDomain() strips port via new URL().hostname, so unique-port
    localhost instances all share hostname '127.0.0.1' and the
    receiver's attribution guard
        extractDomain(user.homeInstance) === extractDomain(fedHeaders.origin)
    becomes ambiguous in any multi-remote configuration.
  - The homeInstance validator regex /^[a-zA-Z0-9._-]+$/ in auth.ts
    rejects ':', so the port cannot be encoded into homeInstance to
    disambiguate.
  - Eliminating the second row would require a production refactor of
    extractDomain (port-preserving), the attribution check (decoupled
    from URL), or the homeInstance validator (allow ':') — all out of
    scope.

So the harness DELIBERATELY keeps DOMAIN as a per-instance human label
('home.test.local' / 'remoteN.test.local') for stable identity, and the
two peer rows per direction (transport URL + getOurOrigin URL) are
structural to localhost-port test reality, not a band-aid. Comment
rewritten to reflect this. PUBLIC_ORIGIN remains available in
production code for reverse-proxy / dev-without-TLS deployments.
2026-05-04 00:02:14 +02:00
Jannis Braun d55e85d2c5 feat(federation): PUBLIC_ORIGIN env override for getOurOrigin
Adds an explicit override for the federation transport URL returned by
getOurOrigin(). When unset, behaviour is unchanged (https://${DOMAIN} ->
http://localhost:${PORT} dev fallback). Intended for reverse-proxy /
dev-without-TLS deployments where the public origin must be advertised
explicitly (typically http://...) and differs from the bare DOMAIN
value used for federated identity.

Wired via config.publicOrigin (envOptional('PUBLIC_ORIGIN')) so the
override flows through the existing config layer rather than scattering
process.env reads. Trailing slash is stripped for symmetry with
peer.origin storage.

docs/systems/federation.md gets a "Public Origin Override" subsection
under §14 Background Workers documenting the resolution order.
2026-05-04 00:02:14 +02:00
Jannis Braun c5e3d36689 test(federation-identity): #3 soft / #5 full — DB cascade verification + setup fixture
Adds setupFullDeletionFixture (federated user joins remote space, authors 2
messages with reactions, opens 1-on-1 DM with a live observer). Tests #3 (soft
mode: tombstone + dm_members cleared, messages/reactions retained) and #5 (full
mode: tombstone + messages/reactions purged, surviving 1-on-1 DM channel).

Also fixes seedPeer to install both the URL form (outbound lookup on sender)
and the DOMAIN-claim form (inbound auth on receiver) — required because the
test harness's ephemeral http://127.0.0.1 origin and DOMAIN-derived
getOurOrigin() return different strings, while production has them coincide.
This was latent: test #1 (leave mode) skips S2S, so #3 was the first test to
actually exercise the S2S delete path and surfaced the dual-origin gap.
2026-05-03 23:46:20 +02:00
Jannis Braun 957cfd9094 test(federation-identity): #1 leave mode — registry cleaned, remote untouched, no S2S call
Also fix dbInspect.ts UserRow column aliases: SELECT * returns snake_case
columns (is_deleted, display_name, etc.) but UserRow expected camelCase.
Switch to explicit aliased SELECT so all callers get the documented interface.
2026-05-03 23:38:02 +02:00
Jannis Braun b62ffdf03b test(federation-identity): #11 invalid mode / #12 empty origins 2026-05-03 23:35:15 +02:00
Jannis Braun 2df346cf03 test(federation-identity): scaffold + harness boot smoke 2026-05-03 23:34:57 +02:00
Jannis Braun a2fadf7e93 test(harness): wsListener helper for capturing real WS events 2026-05-03 23:33:29 +02:00
Jannis Braun e06b5fec3c test(harness): testUsers helpers — registerLocal + createFederatedUser via production endpoints 2026-05-03 23:26:47 +02:00
Jannis Braun 5fe6e7f225 test(harness): hmacSign + seedSpaceWithStubOwner helpers 2026-05-03 23:21:53 +02:00
Jannis Braun e95d3ef065 test(harness): read-only DB inspector helpers 2026-05-03 23:20:59 +02:00
Jannis Braun db70064a5a test(harness): seedPeer helpers — peer pair + unreachable peer 2026-05-03 23:20:30 +02:00
Jannis Braun cd6c51f893 test(harness): N-remote child-process harness + log-tail helpers 2026-05-03 23:17:42 +02:00
Jannis Braun c25aecc81b chore: add test scripts for federation identity deletion suite 2026-05-03 23:07:37 +02:00
Jannis Braun f661d112fa chore: gitignore tests/.tmp/ and tests/reports/ scratch dirs 2026-05-03 23:07:22 +02:00
Jannis Braun 35072dc11f docs: correct soft-mode purge claim — orphaned DMs always purge regardless of mode 2026-05-03 23:06:34 +02:00
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 3dbf4dab5e feat(server): DISABLE_FEDERATION_WORKERS env gate for test isolation 2026-05-03 22:51:06 +02:00