Skip to content

fix(desktop): land live presence updates for not-yet-cached pubkeys#947

Merged
wesbillman merged 2 commits into
mainfrom
brain/fix-presence-merge
Jun 10, 2026
Merged

fix(desktop): land live presence updates for not-yet-cached pubkeys#947
wesbillman merged 2 commits into
mainfrom
brain/fix-presence-merge

Conversation

@wesbillman

Copy link
Copy Markdown
Collaborator

Problem

Will showed offline for minutes in the desktop app even though the relay had him online the whole time (verified via sprout users presence). It then self-healed without any user action.

Root cause is in the WebSocket presence handler (desktop/src/features/presence/hooks.ts). The live-update merge dropped any presence event for a pubkey not already in the cached lookup:

queryClient.setQueriesData(["presence"], (old) => {
  if (!old || !(pubkey in old)) return old;   // drops the update
  ...
});

The backend get_presence (profile.rs) returns only pubkeys with a presence record — offline/unknown users are omitted entirely, and the UI reads a missing pubkey as offline. So the offline→online transition always targets an absent key:

  1. Will is offline when the client seeds the member list → absent from cache.
  2. Will comes online → relay pushes the event over WS → handler hits !(pubkey in old)dropped.
  3. Stale "offline" persists until the 60s backstop refetch re-seeds him.

The one transition presence exists to show is the one this guard always missed.

Fix

Scope the cache write with a predicate that matches the pubkey against the query key (the key already encodes the requested set: ["presence", ...sortedPubkeys]), then merge the pubkey in even when absent. Live online events now land instantly; the 60s poll stays as the backstop it was meant to be.

  • presence/lib/presence.ts — pure helpers presenceQueryWantsPubkey(key, pubkey) and mergePresenceUpdate(old, pubkey, status).
  • presence/hooks.ts — both setQueriesData sites (WS subscription and self-presence mutation, which had the identical latent guard) use the predicate + helper.
  • presence/lib/presence.test.mjs — 8 unit tests.

Verification

  • New tests 8/8 pass, including the exact failing scenario (absent pubkey + "online" → present and online), scoping, and no-op-when-unchanged. The old code fails the core-bug test; the new code passes.
  • Full desktop suite 619/619 pass.
  • tsc --noEmit clean, biome lint clean.

A unit test is the right proof here: the bug is a deterministic data-merge decision. A live-relay test would ride on timing and the very 60s backstop that masks the bug.

Out of scope (noted, not addressed here)

  • sprout users presence CLI mislabels the subject pubkey with the relay signing key (reads event author instead of the p tag).
  • Staging relay is running on the hardcoded DEV keypair (SPROUT_RELAY_PRIVATE_KEY unset).

The WebSocket presence handler dropped any kind:20001 update for a pubkey
not already present in the cached lookup. Because get_presence omits
offline/unknown pubkeys entirely, a user's offline->online transition
always targeted an absent key and was discarded, leaving the UI stale
until the 60s backstop refetch re-seeded the cache (minutes of lag).

Scope the cache write with a predicate matching the pubkey against the
query key (which encodes the requested set) and merge the pubkey in even
when absent. Same fix applied to the self-presence mutation's onSuccess.
Merge/scope logic extracted to pure helpers in lib/presence.ts with tests.

Co-authored-by: Brain <21994759fc7a6fa6b965551d35cfd7897d262f2495467f2d78694ddcfa6a5c7e@sprout-oss.stage.blox.sqprod.co>
Signed-off-by: Wes <wesbillman@users.noreply.github.com>
@wesbillman wesbillman requested a review from a team as a code owner June 10, 2026 16:36
@wesbillman

Copy link
Copy Markdown
Collaborator Author

@codex please review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 4a9ab982c7

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +126 to +128
presenceQueryWantsPubkey(query.queryKey, pubkey),
},
(old) => mergePresenceUpdate(old, pubkey, status),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Ignore p-tags on live presence events

When any client publishes a live kind:20001 event with a p tag naming a victim, the relay stores presence under the authenticated/event pubkey but fans out the original event unchanged (crates/sprout-relay/src/handlers/event.rs 446-452). With this predicate/merge change, if that victim is requested but absent from the cached lookup (the offline/unknown case this patch now inserts), the attacker-controlled p tag passes the query-key check and mergePresenceUpdate marks the victim online/away/offline until the next refetch. The WS subscription should key live updates by event.pubkey, or only honor p tags on trusted relay-synthesized query results.

Useful? React with 👍 / 👎.


import { mergePresenceUpdate, presenceQueryWantsPubkey } from "./presence.ts";

const WILL = "8e39cba681211b3782d0e4483e9343719b9b7be66515252da5491f26421896b1";

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LOL @wpfleger96 😂

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hey that's me! 🙃

A reviewer flagged that the new absent-pubkey merge widened a spoof
surface. The relay fans out client-authored kind:20001 events unchanged,
and the WS handler read the event's p tag as the subject. Legitimate live
events (sendPresence) carry no p tag — the subject is the author — but a
malicious client could self-sign an event naming a victim in a p tag and
flip that victim's cached presence until the next refetch.

Key live updates off event.pubkey only. The p-tag subject is trusted just
on the relay-signed REST/seed path (get_presence), which is unchanged.
Logic extracted to parseLivePresenceEvent with spoof-prevention tests.

Co-authored-by: Brain <21994759fc7a6fa6b965551d35cfd7897d262f2495467f2d78694ddcfa6a5c7e@sprout-oss.stage.blox.sqprod.co>
Signed-off-by: Wes <wesbillman@users.noreply.github.com>
@wesbillman wesbillman enabled auto-merge (squash) June 10, 2026 16:50
@wesbillman wesbillman merged commit 34c8bda into main Jun 10, 2026
13 checks passed
@wesbillman wesbillman deleted the brain/fix-presence-merge branch June 10, 2026 16:58
wpfleger96 pushed a commit that referenced this pull request Jun 10, 2026
…947)

Signed-off-by: Wes <wesbillman@users.noreply.github.com>
Co-authored-by: Brain <21994759fc7a6fa6b965551d35cfd7897d262f2495467f2d78694ddcfa6a5c7e@sprout-oss.stage.blox.sqprod.co>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants