fix(relay): hide DMs from recipients until the first message arrives - #3790
Open
Maxwellimus wants to merge 1 commit into
Open
fix(relay): hide DMs from recipients until the first message arrives#3790Maxwellimus wants to merge 1 commit into
Maxwellimus wants to merge 1 commit into
Conversation
Clicking a person in search fires kind:41010 (DM open) before anything is typed, and the recipient immediately saw an empty DM window. Non-creator DM members now start hidden_pending; the first kind:9/40002 message (or 48100 huddle ring) atomically reveals them, emits their deferred kind:44100, and invalidates membership caches. Pending memberships are excluded from read scoping and from the REQ/COUNT stale-cache repair path (new is_visible_member), and an explicit kind:41012 hide stays sticky. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: Max Lampert <maxwell@squareup.com>
Author
|
🤖 CI note from Max's AI agent: the Desktop Smoke E2E (3) failure ( |
Contributor
|
solid direction. can you keep the change behind a clear flag until the rollout checklist is done? |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
Clicking a person in search fires kind:41010 (DM open) immediately — before anything is typed. The relay created the DM with all participants visible, so the recipient's client received a kind:44100 member-added notification and NIP-29 discovery events (39000/39002) right away, and an empty DM window appeared on their end. The recipient should not see the DM until the first message arrives.
Fix (relay-side, no client changes)
New
channel_members.hidden_pendingcolumn (migrations/0027_dm_hidden_pending.sql, also inschema/schema.sql):crates/buzz-db/src/dm.rs): non-creator DM members are inserted withhidden_at = NOW(), hidden_pending = TRUE. The creator stays visible.handle_dm_open/handle_dm_add_membernow emit the create-time kind:44100 to the creator/actor only — 44100s are stored globally (channel_id = NULL), so notifying everyone leaked the empty DM. (dm_add_membermints a new channel — DM participant sets are immutable — so the same rule applies.)crates/buzz-relay/src/handlers/side_effects.rs, called from ingest and the workflow sink): the first real message in a DM — kind:9/40002 text, kind:40008 diff, or kind:48100 huddle ring (is_dm_revealing_kind) — runs a single race-freeUPDATE … WHERE hidden_pending RETURNING pubkey, then invalidates membership caches and emits each revealed member's deferred kind:44100 with the message author as actor. Best-effort: a failed reveal never rejects the stored message.crates/buzz-db/src/channel.rs):get_accessible_channel_ids(REQ/COUNT/HTTP-bridge scoping) excludeshidden_pendingmemberships, so pending recipients can't discover the channel via 39000/39002/44100 queries. Deliberately narrow: explicitly hidden DMs remain readable (clients filter those via the NIP-DV 30622 snapshot).hidden_pending = FALSE, so a DM the recipient explicitly hid after the first message stays hidden through later messages.list_hidden_dmsexcludes pending rows so they never leak into the NIP-DV snapshot.unhide_dm(clears both flags) right afteropen_dm.Idempotent re-open of an existing DM early-returns before the member insert, so it never re-hides a recipient who has already seen the DM.
Review hardening (from adversarial Claude review + codex review of the change):
workflow_sink.rsbypasses ingest). The reveal now lives in a sharedreveal_dm_members_on_first_messagehelper called from both ingest and the workflow sink. Scheduled messages deliver client-side through ingest, and huddle kind:48100 is creator-signed through ingest, so those paths were already covered; relay-signed kind:40099 system messages deliberately never reveal.is_visible_membercheck: the REQ/COUNT stale-cache repair path previously confirmed access via rawis_member, which would have let a pending recipient who learned the channel UUID out-of-band read the channel. The repair path now excludeshidden_pendingrows (explicit hides still count as visible, matchingget_accessible_channel_ids). Covered by a#h-probe assertion in the e2e test, verified red without the fix.Known narrow edge, deferred: an ACP agent recipient replays history from its membership timestamp minus 5s of skew when the deferred kind:44100 arrives; a sender clock more than ~5s slow can stamp the first message below that floor and the agent misses it (relay ingest tolerates ±900s drift). Fixing this belongs in the ACP harness (unfloored first fetch for newly discovered DM channels) — follow-up, not this relay change.
Testing (TDD)
test_dm_open_invisible_to_recipient_until_first_message(crates/buzz-test-client/tests/e2e_nostr_interop.rs), written first and failing on the unfixed relay at the exact leak (recipient received the 44100 for a message-less DM). Three phases: (1) after A opens a DM, B sees no 44100/39002/39000 for it and a#hREQ with the known channel id is CLOSED; (2) A's first message delivers the deferred 44100 live to B, the channel becomes discoverable, and the#hREQ is granted; (3) B's explicit kind:41012 hide survives a second message.test_dm_first_diff_message_reveals_recipient, also written first and failing at the missing reveal, covering the kind:40008-first path end to end. The workflow-sink call site is glue on the same helper this test exercises (a workflow-webhook e2e harness doesn't exist yet).e2e_nostr_interopsuite passes (27/27) against the fixed relay, including the pre-existing DM discovery and NIP-DV tests.just testgreen; migrator test updated for migration 0027 (renumbered after0026_replica_heartbeatlanded on main).just cigreen except one pre-existing mobile widget-test failure (ChannelDetailPage keeps follow mode off while a tall newest message stays visible) that reproduces on mobile code identical to main.desktop/src/features/channels/useMembershipNotifications.ts) and relay-provided channel lists, so deferring the relay events is sufficient — no screenshots needed.🤖 Generated with Claude Code