fix(dm): keep hidden DMs hidden across refetch via relay-signed visibility snapshot (NIP-DV)#857
Merged
Conversation
…ility snapshot (NIP-DV) Hiding a DM removed it from the list optimistically, but the next `get_channels` refetch rebuilt the DM list purely from `kind:39002` membership and brought it back. Per-viewer hide state lives only in the relay DB (`channel_members.hidden_at`) and was never exposed as a queryable Nostr event, so the thin client had no way to learn it. Add a relay-signed, per-viewer parameterized-replaceable snapshot (`kind:30622`, NIP-DV): emitted on hide (41012) and re-open (41010 that clears `hidden_at`), recompute-and-replace so the latest event is the authoritative hidden set. Both client read paths filter on it. Privacy: snapshots are owner-scoped. A filter-level `#p` gate rejects queries for another viewer's snapshot, and a result-level owner check (`reader_authorized_for_event`) is applied at every delivery surface — HTTP query, WebSocket historical, live fan-out, and NIP-50 search — to close the kindless-`ids` bypass. 30622 is also excluded from the search index as defense in depth. Docs: new docs/nips/NIP-DV.md describes the event, client behavior, and privacy/write-protection model. Tests: relay unit suite green (273), incl. per-viewer independence, ids/kindless-ids gating, and search-surface owner checks. Five live e2e (hide->reopen, two-viewer independence, WS/REST/search third-party rejection) are written and compile but are NOT yet run against a live relay due to a local Postgres PG17/PG18 mismatch — running them is a must-do before merge. Co-authored-by: Brain <21994759fc7a6fa6b965551d35cfd7897d262f2495467f2d78694ddcfa6a5c7e@sprout-oss.stage.blox.sqprod.co> Signed-off-by: Wes <wesbillman@users.noreply.github.com>
…d updates publish_dm_visibility_snapshot signed the replacement event with the default current-second created_at. replace_parameterized_event rejects a same-second replacement whose random event id sorts higher, so a hide -> re-open within one wall-clock second could strand the stale hide snapshot ~50% of the time -- reintroducing the "hidden DMs come back" symptom under double-action timing. Force created_at = max(now, prior + 1) for the viewer's (kind=30622, relay pubkey, d=viewer) snapshot, mirroring the existing emit_addressable_discovery_event guard. Add a same-second hide->reopen regression test (existing e2e tests sleep multi-second and miss this window). Co-authored-by: Brain <21994759fc7a6fa6b965551d35cfd7897d262f2495467f2d78694ddcfa6a5c7e@sprout-oss.stage.blox.sqprod.co> Signed-off-by: Wes <wesbillman@users.noreply.github.com>
d178b1c to
be09981
Compare
Two harness-only fixes; zero relay code changes: 1. Fix REST paths: helpers referenced /api/events, /api/query, /api/channels/... but the router serves /events, /query, /channels/... — and /api/dms does not exist at all (DM creation is kind:41010 only). Every NIP-DV e2e test 404'd before reaching any visibility logic. create_dm now posts a signed kind:41010 and parses the channel_id from the relay's response message. 2. Backdate create_dm's initial 41010 by 10s: a later re-open with identical tags in the same wall-clock second mints a byte-identical event id, which the relay correctly dedupes — making the hide->reopen tests fail spuriously. Real clients cannot hit this. With these fixes all 8 live NIP-DV e2e tests pass against a real relay (PG18 + Typesense), including the same-second hide->reopen regression test for the monotonic snapshot fix. Signed-off-by: npub1qyvc0c5kl4gqv2fd97fsk46tu378sqgy35vc83rvgfwne90sel7s0ed67d <011987e296fd5006292d2f930b574be47c7801048d1983c46c425d3c95f0cffd@sprout-oss.stage.blox.sqprod.co>
tlongwell-block
approved these changes
Jun 9, 2026
tlongwell-block
pushed a commit
that referenced
this pull request
Jun 10, 2026
* origin/main: Fix post-compact handoff context for OpenAI providers (#931) chore(release): release version 0.3.15 (#936) fix: persona is source of truth at spawn + thread-depth conventions (#930) fix: skip avatar reconciliation for legacy agent records (#933) feat(desktop): add nest commit identity guidance with human sign-off (#929) feat: provider/model selection for personas and runtime-aware env injection (#794) fix: reconcile agent profile on startup when relay publish was missed (#921) Revamp first-run onboarding (#924) Update setup loading screen (#926) fix(dm): keep hidden DMs hidden across refetch via relay-signed visibility snapshot (NIP-DV) (#857) Maximize desktop window on launch (#925) feat: preview features (experiments settings UI) (#888) fix(updater): send no-cache header on update check to avoid stale manifest (#922) fix(desktop): refresh channel state after unarchive (#923) Add channel visibility & ephemeral TTL controls to manage sidebar (#911) ci(release): add Intel macOS (x86_64) DMG as a release target (#748) Signed-off-by: npub1mprnacetjua2xx3p5eddmhxyk6wv929ymm5py8kd2xfxurxahspqqlgyta <d8473ee32b973aa31a21a65adddcc4b69cc2a8a4dee8121ecd51926e0cddbc02@sprout-oss.stage.blox.sqprod.co> # Conflicts: # desktop/src/features/sidebar/ui/AppSidebar.tsx
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
Hiding a DM removed it from the list optimistically, but the next
get_channelsrefetch rebuilt the DM list purely fromkind:39002membership and brought it back. Per-viewer hide state lives only in the relay DB (channel_members.hidden_at) and was never exposed as a queryable Nostr event, so the thin client had no way to learn it. Re-open (41010) carries no#h, so a pure-client filter couldn't be correct either.Fix
A relay-signed, per-viewer parameterized-replaceable snapshot (
kind:30622, NIP-DV):hidden_at), recompute-and-replace so the latest event is the authoritative hidden set (d= viewer → unique per viewer).get_channelsRust + e2e bridge) filter on it.Privacy (owner-scoped, every surface)
#pgate rejects queries for another viewer's snapshot (explicitkinds:[30622]→ 403).reader_authorized_for_event) at every delivery surface — HTTP/query, WebSocket historical, live fan-out, and NIP-50 search — closes the kindless-idsbypass (kindlessids:[…]→ 200 + empty).Review
Independently reviewed by @Pinky across three adversarial passes — three real CHANGE-level issues caught and fixed:
replace_parameterized_event.idsprivacy bypass →#p=self enforced for explicit-kind filters.Code/privacy shape is blessed.
Tests
#[ignore]d NIP-DV e2e, run them locally before merging. Tests:test_nipdv_hide_then_reopen_updates_snapshot,test_nipdv_two_viewers_independent_snapshots,test_nipdv_ws_req_rejects_third_party,test_nipdv_ids_query_rejects_third_party+test_nipdv_explicit_kind_query_forbidden_for_third_party,test_nipdv_search_rejects_third_party.Docs
New
docs/nips/NIP-DV.md— event format, client behavior, privacy + write-protection model. NIP says untrusted/multi-relay clients MUST verify NIP-11self; notes that Sprout Desktop currently trusts its configured relay like NIP-IA (document-the-gap, not broadened scope).