Summary
concord --invites is documented (crates/concord-cli/src/main.rs:15) as answering
"is a direct invite actually on the network for this account?" — but it false-negatives on
v2 Direct Invites. Run against an inbox holding a real 3313 direct invite, it unwraps the
wrap, tallies the rumor as kind 3313, and then prints:
inner-kind tally (unwrapped): kind 3313 ×N
── community invites on network: 0 ──
NONE. No COMMUNITY_INVITE_BUNDLE gift wrap for this account on these relays in-window.
The verdict contradicts the tally printed two lines above it.
The wrap is not untrusted, either: probe_invites opens it via
UnwrappedGift::from_gift_wrap, which Schnorr-verifies the seal and binds
rumor.pubkey == seal.pubkey (nostr fork pinned at 0f9f3a7, nip59.rs:108/:115),
and the tool prints a signature-verified distinct senders seen: 1. So the invite is
discarded purely on kind/shape — "couldn't authenticate the wrap" is not the explanation.
Reproduced on two independent accounts / inboxes (different keys, different wraps), same result.
Root cause — a v1-only probe path, three layers
main.rs:571 — the classifier is if k == COMMUNITY_INVITE_BUNDLE (3304, the v1 bundle).
A v2 Direct Invite rumor is DIRECT_INVITE = 3313 (crates/vector-core/src/community/v2/mod.rs:65),
so it never enters the invite branch.
crates/vector-core/src/community/invite.rs:184 — even if the filter is widened,
parse_invite_rumor also hard-rejects any kind but 3304, and parses the content with
the v1 CommunityInvite::from_json. A v2 rumor's content is the v2 bundle shape
(CommunityInvite::from_bundle_json), a different deserializer — so a one-line filter fix
moves the failure one call down rather than fixing it.
crates/vector-core/src/community/v2/invite.rs:607 — the correct v2 entrypoint
unwrap_direct_invite(wrap, keys) already exists in the same crate and does the full job:
gift-wrap kind gate → seal decrypt → seal.verify() → rumor decrypt →
rumor.kind == DIRECT_INVITE → rumor.pubkey == seal.pubkey bind → from_bundle_json.
probe_invites never calls it; it re-implements a weaker, v1-only unwrap beside it.
Suggested fix
For each gift wrap whose inner rumor kind is 3313, route it through
vector_core::community::v2::invite::unwrap_direct_invite(wrap, &keys) (which returns
(sender, CommunityInvite) with provenance already checked) instead of the v1
parse_invite_rumor. Keep the v1 3304 path for backward compatibility. This also restores
the sender/community/channel detail the current path drops for v2 invites.
Notes for the maintainer
- Binary is
concord ([[bin]] name = "concord"), not concord-cli.
- The
--invites NONE line and the inner-kind tally are the two artifacts to compare when repro'ing.
Summary
concord --invitesis documented (crates/concord-cli/src/main.rs:15) as answering"is a direct invite actually on the network for this account?" — but it false-negatives on
v2 Direct Invites. Run against an inbox holding a real 3313 direct invite, it unwraps the
wrap, tallies the rumor as
kind 3313, and then prints:The verdict contradicts the tally printed two lines above it.
The wrap is not untrusted, either:
probe_invitesopens it viaUnwrappedGift::from_gift_wrap, which Schnorr-verifies the seal and bindsrumor.pubkey == seal.pubkey(nostrfork pinned at0f9f3a7,nip59.rs:108/:115),and the tool prints a signature-verified
distinct senders seen: 1. So the invite isdiscarded purely on kind/shape — "couldn't authenticate the wrap" is not the explanation.
Reproduced on two independent accounts / inboxes (different keys, different wraps), same result.
Root cause — a v1-only probe path, three layers
main.rs:571— the classifier isif k == COMMUNITY_INVITE_BUNDLE(3304, the v1 bundle).A v2 Direct Invite rumor is
DIRECT_INVITE = 3313(crates/vector-core/src/community/v2/mod.rs:65),so it never enters the invite branch.
crates/vector-core/src/community/invite.rs:184— even if the filter is widened,parse_invite_rumoralso hard-rejects any kind but3304, and parses the content withthe v1
CommunityInvite::from_json. A v2 rumor's content is the v2 bundle shape(
CommunityInvite::from_bundle_json), a different deserializer — so a one-line filter fixmoves the failure one call down rather than fixing it.
crates/vector-core/src/community/v2/invite.rs:607— the correct v2 entrypointunwrap_direct_invite(wrap, keys)already exists in the same crate and does the full job:gift-wrap kind gate → seal decrypt →
seal.verify()→ rumor decrypt →rumor.kind == DIRECT_INVITE→rumor.pubkey == seal.pubkeybind →from_bundle_json.probe_invitesnever calls it; it re-implements a weaker, v1-only unwrap beside it.Suggested fix
For each gift wrap whose inner rumor kind is
3313, route it throughvector_core::community::v2::invite::unwrap_direct_invite(wrap, &keys)(which returns(sender, CommunityInvite)with provenance already checked) instead of the v1parse_invite_rumor. Keep the v13304path for backward compatibility. This also restoresthe sender/community/channel detail the current path drops for v2 invites.
Notes for the maintainer
concord([[bin]] name = "concord"), notconcord-cli.--invitesNONE line and the inner-kind tally are the two artifacts to compare when repro'ing.