Skip to main content

Module invite

Module invite 

Source
Expand description

CORD-05 Invites — how members are handed the keys that make them members.

Two delivery lanes share one payload, the CommunityInvite bundle:

  • Public link — the bundle rides relays as an addressable event (kind::INVITE_BUNDLE, empty d) authored by a throwaway per-link keypair, encrypted under a key nobody on the network holds (derived off the link’s 16-byte unlock token). The link is (naddr, #fragment): the naddr is a bare public locator, the fragment carries the token + bootstrap relays and never reaches a server. Only the creator (holder of the link_signer secret, synced in their InviteList) can refresh or tombstone the coordinate, so a link-holder can join but never squat or kill the link (§2).
  • Direct invite — when the invitee is a known npub the machinery drops away: the bundle giftwraps straight to them as a STANDARD NIP-59 wrap (build_direct_invite, §6), not the reversed stream wrap of CORD-01.

The inviter’s identity is irrelevant to trust: the community_id self-certifies the owner (CORD-02 A.4), so no bundle can smuggle a false owner or a fake key for a real Community. Every bundle passes CommunityInvite::validate before it is trusted, whichever lane carried it.

Structs§

ChannelGrant
One granted Channel inside a bundle (CORD-05 §1). Public Channels derive from the community_root; a private Channel’s independent key travels here.
CommunityInvite
The CommunityInvite bundle (CORD-05 §1). Field names are wire-frozen and shared with Soapbox/Armada; a rename is a silent cross-client join failure.
InviteEntry
One minted link in a creator’s private InviteList (CORD-05 §4). The token is the unlock secret AND the merge key; signer_sk is what refreshing or retiring the bundle needs.
InviteList
A creator’s Invite List — the kind-13303 replaceable, NIP-44-encrypted to self. Two clients can serve one npub, so the round-trip discipline applies.
InviteTombstone
A retired link: a tombstone always beats an entry, terminally (CORD-05 §4).
ParsedInviteLink
A parsed invite link: the bundle coordinate’s author plus the fragment secrets.

Enums§

BundleState
A fetched bundle coordinate resolves to one of these (CORD-05 §2).
InviteError
Errors from the invite layer.

Constants§

FRAGMENT_VERSION
The fragment format byte, which also selects the relay-dictionary generation (CORD-05 §3). Bumping it re-labels the dictionary universe.
MAX_BOOTSTRAP_RELAYS
The fragment carries at most this many bootstrap relays — it only needs to find the bundle, which then carries the authoritative set (CORD-05 §3).
MAX_BUNDLE_CHANNELS
Hostile-bundle bound: a bundle is attacker-crafted input reached by following a link, so reject one carrying more Channels than a Community could sanely hold before allocating on its claims (CORD-05 §1).
MAX_BUNDLE_EPOCH
Sanity ceiling on a bundle’s root_epoch / channel epochs. Not a commitment (epochs aren’t owner-signed), just a bound so attacker-set values can’t push a later epoch + 1 toward overflow. 2^40 is astronomically above any real rotation count.

Functions§

build_bundle_event
Build the addressable bundle event (33301, link_signer, d=""), marked live. The content is the §1 bundle NIP-44-encrypted under bundle_key (derived off the link’s token — super::derive::invite_bundle_key), so relays store it but can never open it.
build_direct_invite
Build a Direct Invite (CORD-05 §6): the §1 bundle handed to a known npub as a STANDARD NIP-59 giftwrap — ephemeral wrap author, the recipient in the p tag, a kind-13 seal signed by the inviter’s REAL key (whose verified npub is what proves who invited), NOT the reversed stream wrap of CORD-01. The wrap carries the outer ["k","3313"] index hint (the deliberate exception to the no-outer-tags rule) plus an optional NIP-40 expiration matching the bundle’s expires_at (ms → seconds). NIP-59 tweaks the wrap and seal timestamps into the past.
build_invite_list_event
Build the creator’s kind-13303 Invite List event (CORD-05 §4): the document NIP-44-encrypted to SELF and signed by the creator’s real key. Replaceable, one per creator. On READ the caller MERGES into the local mirror, never replaces.
build_invite_url
Build a shareable invite URL on base — the base is interchangeable (any deeplink domain works), only the naddr and fragment are protocol (CORD-05 §2).
build_registry_content
Build the vsk-8 Registry entity content (CORD-05 §5): a JSON array of the creator’s live link COORDINATES (link_signer pubkey hex) — never tokens, URLs, or signing secrets, so members see that links exist without gaining the ability to use one. The edition wrapping reuses the Control plane (super::control); this is only the content shape.
build_revocation
Re-post the coordinate as a revocation tombstone (CORD-05 §2) — signer-signed, so only the creator, and exactly as durable as the bundle it replaces (unlike a best-effort relay deletion).
bundle_naddr
Build the bare naddr for a link signer’s bundle coordinate (33301, pk, "") — no identifier bytes, no relay entries (relays travel in the fragment), keeping it as short as an naddr gets.
decode_fragment
Decode a fragment into its token + bootstrap relays. Strict on framing: a wrong version (legacy OR future), a bad count, or any trailing byte after the token is fatal; an unknown dictionary id is skipped, not fatal, so the dictionary can grow without breaking older readers.
encode_fragment
Encode the fragment [version=4][flags][relays?][token:16] as base64url with no padding. The stock set costs zero relay bytes (and is exempt from the 3-relay cap, which applies to explicit entries only); otherwise each relay is a dictionary-id byte, a wss://-implied literal (0,len,host), or a verbatim literal (255,len,url) for ws:// and exotic schemes.
merge_invite_lists
Merge two Invite Lists without coordination (CORD-05 §4): the token is the merge key, an entry is immutable once minted (first-seen wins), tombstones union, and a tombstone always beats an entry — terminally, so a stale device can never resurrect a revoked link.
parse_bundle_event
Verify + open a fetched bundle event. The primary anti-squat guard is the FETCH itself — the coordinate (33301, link_signer, "") means a different author is a different coordinate, so a squatter’s spam never matches the filter — and this re-checks event.pubkey == expected_signer as a belt-and-suspenders against a relay handing back a foreign event. Gates on the signature, the vsk marker, decrypt, and CommunityInvite::validate.
parse_invite_link
Parse a full URL (…/invite/<naddr>#<fragment>) or the domain-agnostic bare form (<naddr>#<fragment>) into its coordinate author + fragment secrets.
parse_invite_list_event
Decrypt + parse a kind-13303 event with the creator’s own keys. A decrypt/parse failure MUST be treated as “no news” by the caller — never a clobber of a populated local list.
parse_registry_content
Parse a Registry entity’s content back into the link-signer coordinates.
stock_relays
The stock relay set (dictionary ids 1..=4, in order) — selected by one flag so the common invite carries zero relay bytes.
unwrap_direct_invite
Unwrap a Direct Invite giftwrap addressed to the recipient, returning the verified inviter + the validated bundle. Peels wrap → seal → rumor and MUST Schnorr-verify the kind-13 seal (its npub is the only proof of who invited), then binds rumor.pubkey == seal.pubkey (anti-spoof) and gates on the rumor kind (the outer k tag was only ever a hint). Nothing joins on unwrap — consent is the caller’s concern (CORD-05 §6).