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, emptyd) 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 theirInviteList) 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§
- Channel
Grant - One granted Channel inside a bundle (CORD-05 §1). Public Channels derive from
the
community_root; a private Channel’s independentkeytravels here. - Community
Invite - The
CommunityInvitebundle (CORD-05 §1). Field names are wire-frozen and shared with Soapbox/Armada; a rename is a silent cross-client join failure. - Invite
Entry - One minted link in a creator’s private
InviteList(CORD-05 §4). Thetokenis the unlock secret AND the merge key;signer_skis what refreshing or retiring the bundle needs. - Invite
List - 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.
- Invite
Tombstone - A retired link: a tombstone always beats an entry, terminally (CORD-05 §4).
- Parsed
Invite Link - A parsed invite link: the bundle coordinate’s author plus the fragment secrets.
Enums§
- Bundle
State - A fetched bundle coordinate resolves to one of these (CORD-05 §2).
- Invite
Error - 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 laterepoch + 1toward overflow.2^40is 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 underbundle_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
ptag, 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-40expirationmatching the bundle’sexpires_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, awss://-implied literal (0,len,host), or a verbatim literal (255,len,url) forws://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-checksevent.pubkey == expected_signeras a belt-and-suspenders against a relay handing back a foreign event. Gates on the signature, thevskmarker, decrypt, andCommunityInvite::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 outerktag was only ever a hint). Nothing joins on unwrap — consent is the caller’s concern (CORD-05 §6).