Skip to main content

Module service

Module service 

Source
Expand description

Concord v2 service — the stateful orchestration binding the pure v2 modules to storage + transport. Free functions, SessionGuard-gated at every write (a swap_session can land at any await — see CLAUDE.md), mirroring the v1 service’s discipline.

First-cut scope (bots): create a community, send + fetch channel messages, accept invites, publish a Guestbook Join. Rotation/refounding/moderation and full roster folding layer on next. Signing is local-keys for now (bots hold their nsec); a NIP-46 bunker create/send path is a documented follow-up (v2’s genesis + chat seals are sign-only ops, so it composes — just needs the async signer threaded through build_seal).

Structs§

AuthorityView
A community’s folded, delegation-authorized authority — the on-demand read view (a paged control-plane fetch + fold, nothing persisted). roles is the owner-seeded authorized roster (shared algebra with v1); banned the enforced banlist. floored/head_entities let a writer detect a WITHHELD entity (floored locally but no head folded) before replacing it blind.
FetchedEvent
A chat event opened from a channel fetch, tagged with the epoch its key decrypted under.
MintedLink
A minted public link: the shareable URL plus the addressable bundle event to publish and the link keypair to retain (in the Invite List) for later refresh or revocation.
RekeyFollow
The outcome of a rekey-follow pass.

Functions§

accept_direct_invite
Accept a Direct Invite: unwrap the 3313 giftwrap (Schnorr-verifying the seal), then run the shared accept path. The recipient’s consent IS this call. No network await precedes the accept, so the guard captured here suffices.
accept_parked_invite
Accept a PARKED Direct Invite from its stored bundle JSON (the wrap was already unwrapped + owner-verified at park time). Re-parses through the same fail-closed bundle validation, then runs the shared accept path (which re-verifies the owner root over the network). inviter_hex is the parked seal signer, for Guestbook Join attribution.
accept_public_link
Accept a public invite link: fetch its bundle (revocation-aware) and join.
bundle_of
Build the §1 invite bundle for this community. Every channel is granted: a Public channel carries the community_root as its “key” (the joiner derives the real secret from the root), a Private one its own key. The bundle self-certifies the owner, so the inviter’s identity is irrelevant to trust.
community_is_public
Whether this community is PUBLIC (CORD-05 §5): fold every creator’s Registry (vsk-8) that its author is authorized for (CREATE_INVITE, bound to their coordinate) into an aggregate live-link set — non-empty ⇒ a live link exists ⇒ Public; empty ⇒ Private. Retiring the last link is what flips it back.
create_community
Create a fresh v2 community owned by the local identity: mint the genesis (self-certifying id + the two owner editions), persist, publish the genesis control editions, and announce the owner’s Guestbook Join. Returns the saved community.
create_private_channel
Create a new PRIVATE channel (CORD-03 §2): mint a fresh id + an independent random key at channel-epoch 1, deliver the key to every current member over the rekey plane (CORD-06 §1), then announce the channel (vsk 2, private). Epoch 0 is the root generation (“the first privatisation is epoch 1”), so the delivery commits its continuity to (0, community_root) — verifiable by every member and bound to THIS community’s root. The key ships BEFORE the announcement: an aborted attempt leaves only an unannounced crate (invisible), and a retry mints a fresh id, so there is no same-coordinate double-mint to fork on. Live public links are refreshed so a joiner’s bundle carries the key; a member who joins through the stale-bundle window keys up at the channel’s next rotation.
create_public_channel
Create a new PUBLIC channel (CORD-03 §2): mint a fresh id, publish its metadata edition (vsk 2), and add it to the held community. A Public channel derives its Chat Plane from the community_root (no per-channel key), so other members fold it in on their next control follow with nothing to distribute. Returns the new channel id. Reader-gated by MANAGE_CHANNELS.
delete_channel
Tombstone a channel (CORD-03 §2, deleted: true) + drop it locally. Reader-gated by MANAGE_CHANNELS; the coordinate stays folded as a grave so peers hide it.
dissolve_community
Owner dissolution / “Delete Community” (CORD-02 §9): publish the terminal tombstone at the dissolved plane (community_id-derived, epoch-free, so every past or present member resolves the same grave and a Refounding can never strand it). The tombstone’s presence IS the state; only the owner’s seal counts. Irreversible — on success the local hold is sealed read-only.
edit_channel_metadata
Add or edit a channel’s metadata (vsk 2, CORD-03 §2). channel_id is the coordinate. Gated on the reader side by MANAGE_CHANNELS.
edit_community_metadata
Edit the community metadata (vsk 0, CORD-02 §6). Gated on the reader side by MANAGE_METADATA.
ensure_admin_role
The community’s @admin role id: the folded Server-scope ADMIN_ALL role when one exists, else (with create_if_missing) a DETERMINISTIC mint — the same id on every device, so concurrent grants converge as editions of ONE entity instead of forking two Admin roles.
fetch_authority
Fetch + fold the community’s current authority (CORD-04), paging older like follow_control while the fold is gapped so a busy control plane can’t push the roster off the newest window. A fetch failure degrades fail-safe: owner-only authority plus the PERSISTED banlist — nobody gains standing from an outage, and a ban never lifts on withheld data.
fetch_channel
Fetch a channel’s newest messages — one page of fetch_channel_history. limit is one relay-side bound across the whole epoch-author OR-set, not per epoch; deeper history pages backwards via the walk.
fetch_channel_history
Walk a channel’s history newest-first (CORD-03 §3 “clients load a Channel newest-first and paginate backwards”), querying every held epoch’s Chat-Plane address one page-sized query at a time until max_pages, a drained relay, or keep_paging returns false for a page (the caller’s “I already hold these” early stop — consulted only on pages that opened something, so junk at the address can’t fake exhaustion). Pages step by INCLUSIVE until with wrap-id dedup, so a page boundary landing mid-second can’t skip siblings; a full page of only-already-seen wraps is a same-second WALL (relay filters are second-granular) and steps past it accepting that unseen same-second siblings beyond the relay cap are unreachable — logged, and a protocol-level limitation (the ms tag can’t be filtered server-side).
fetch_public_bundle
Fetch + decrypt the newest Live bundle at a public link’s coordinate ((33301, link_signer, "")). Revocation is authoritative-if-present: if ANY signer-valid tombstone is among the fetched events, refuse — never trust fetch ordering (a cross-relay union has no global newest-first sort, so a stale Live could otherwise win a partial-propagation race). Otherwise pick the newest valid Live by created_at. Read-only.
follow_control
Re-fold this community’s Control Plane and apply the current metadata + public channel set to the held community, persisting any change. Called when a control-plane wrap arrives in realtime (a rename, a new channel, an edited description) so a long-running bot tracks the community mid-session instead of freezing at its join-time view.
follow_rekeys
Follow rekeys for a held community: advance the base (root) epoch and each Private channel’s epoch as far as authorized rotations allow, adopting the fresh key we’re still a recipient of at each step and dropping a scope we’ve been removed from. Persists the result. Called when a rekey wrap arrives in realtime so a long-running bot keeps decrypting after a rotation instead of going silent.
grant_admin
Grant the @admin role (minting it deterministically when absent), MERGED into the member’s existing grant — a grant entity replaces whole (CORD-04 §2), so a blind push would erase their other roles. Owner-only: the position-1 Admin is manageable only by position 0 (an equal never outranks it), and refusing before any publish keeps an unauthorized edition of the DETERMINISTIC admin entity from advancing this device’s own floor onto a head readers reject.
grant_roles
Grant or revoke a member’s Roles (vsk 3, CORD-04 §2). Empty role_ids is a revoke. Gated on the reader side by MANAGE_ROLES + outrank of every role + the member.
ingest_guestbook_event
Fold ONE live guestbook event into the store (the realtime path — no fetch). Returns whether it was new.
is_dissolved
Whether a valid owner-signed dissolution tombstone exists for this community on its relays (CORD-02 §9). A join refuses a dead community, and a live follow seals on sight. Fail-OPEN on a fetch error (absence of proof is not death), but any owner-verified tombstone found is authoritative.
kick_member
Cooperative Kick (CORD-04 §6, Guestbook plane): name the target; every reader honors it iff the signer holds KICK and strictly outranks them (the coalesce’s can_kick), so publishing without authority is inert. A kicked member may rejoin with a fresh invite — cryptographic severance is the ban/refound path.
leave_community
Leave a community: publish a Guestbook Leave and tear down the local hold.
memberlist
Fold the Complete Memberlist from the Guestbook plane. The proven owner is ALWAYS a member (derived from the self-certifying community_id — no network, so a lost/evicted genesis Join can’t drop them). Observed authors — anyone seen publishing on a channel — are folded in FORWARD-only per CORD-02 §5, so a member whose Join was lost still counts.
mint_public_link
Mint a public invite link for this community: a fresh token + link keypair, the bundle encrypted under the token key and published at (33301, link_signer, ""), and the base/invite/<naddr>#<fragment> URL. base is the deep-link domain (e.g. https://vectorapp.io); the fragment carries the token + bootstrap relays and never reaches a server.
preview_bundle
The fold half of preview_public_link, over an already-fetched bundle. Split out so a caller that only needs the community’s IDENTITY can read it off the bundle (it is self-certifying) and skip the Control-Plane walk entirely — the walk is the join gate, and accept_public_link runs it again regardless.
preview_public_link
Read-only rich preview of a public link: the decrypted bundle plus the LATEST display metadata folded live from the Control Plane (a v2 bundle deliberately carries no icon — the fold is the authority). Owner-root verification rides the fold, so a forged-root link can’t render a convincing preview; on a fold/transport failure the bundle snapshot is the fallback. Nothing persists — the caller hasn’t joined.
refound_community
Owner/admin Refounding (CORD-06 §3): roll the community_root to cryptographically remove removed from a Private community (a Ban’s read-cut). Compacts the Control Plane under the new root (re-wraps each head VERBATIM — the inner owner/actor signatures survive, so no re-authoring), rekeys the base plus every Private channel (each sealed under the PRIOR root, D2, so a base-fork loser can still open them), and seeds the new epoch’s Guestbook snapshot. Requires BAN.
refresh_public_links
Refresh every live public link’s bundle behind its stable URL (CORD-05 §2) — e.g. after a Rekey/Refounding rolled the keys — by re-posting the bundle at the same coordinate with the CURRENT community state, so a link shared once keeps working across rotations. Best-effort.
republish_community_list
Rebuild this account’s 13302 from its held v2 communities, MERGE with the remote copy (preserving tombstones, other-device entries, unknown fields), and publish. just_joined is the community THIS call is recording a create/join for — the ONLY community whose entry is (re)stamped now, so it beats any prior tombstone (a deliberate re-join resurrects). Every OTHER held community that the remote has tombstoned is left tombstoned (a sibling device’s leave is NOT undone just because we joined something else — the W1 resurrection hole). Idempotent; best-effort — a list-publish failure never fails the membership change itself.
revoke_admin
Strip the @admin role from the member’s grant, preserving their other roles. A no-op when they don’t hold it. Owner-only, like grant_admin.
revoke_public_link
Revoke a public link by its token hex (CORD-05 §2/§5): re-post its coordinate as a revocation tombstone (retiring the bundle behind the URL, so a fetcher finds the grave), tombstone the Invite List entry, and refresh the Registry. Retiring the LAST live link empties the Registry → the community reads Private (a Refounding is the owner’s separate read-cut).
send_chat_message
Full chat send: threaded reply (NIP-C7 q, the parent’s (rumor_id, author) hex pair), NIP-30 custom-emoji pairs, and verbatim extra tags (NIP-92 imeta attachments). Returns the message’s rumor id (hex).
send_chat_message_at
send_chat_message with an explicit event time. The rumor id is a pure function of its inputs, so a GUI that picks at_ms can precompute the id for its optimistic pending row — the in-process echo and the finalize then key the SAME id (the exact v1 pending → sent contract).
send_delete
Cooperative in-plane delete (kind 5, NIP-09 semantics): peers stop rendering target_id_hex. The wrap ciphertext on relays is scrubbed separately via the retained per-message stream key (see publish_chat).
send_direct_invite
Gift-wrap a Direct Invite (kind 3313) of this community straight to recipient and publish it to the community relays. expires_at_ms (unix ms) optionally bounds its shelf life; label is echoed in the joiner’s Guestbook Join. The bundle hands over the keys; the recipient consents by accepting (nothing joins on receipt). Returns the wrap.
send_edit
Edit one of your own messages (kind 3302): peers re-render target_id_hex with the replacement text. Author-enforced on the read side — only the original author’s edit folds.
send_message
Send a text message to a channel. Derives the channel’s Chat-Plane group key (community_root for a Public channel, the channel key for a Private one), seals it encrypted, and publishes. Returns the message’s rumor id (hex).
send_reaction
React to a channel message (kind 7, NIP-25 shape). target_id_hex / target_author_hex name the reacted-to message; target_kind is its rumor kind (kind::MESSAGE, or kind::COMMENT for a threaded reply); emoji carries the NIP-30 pair when emoji_content is a custom :shortcode:.
send_typing
Ephemeral typing indicator (kind 23311 in a 21059 wrap — relays never store it).
send_webxdc_signal
WebXDC realtime peer signal (kind 3310) — the v2 twin of v1’s publish_webxdc_signal: the same shared content shape, sealed on the channel’s chat plane, DURABLE (a reopening peer backfills a recent ad). Signed by the member’s real identity — a member can’t forge another player’s presence. Failure is non-fatal to callers (the next re-advertise covers a missed ad).
set_banlist
Replace the Banlist (vsk 4, CORD-04 §4) with banned (lowercase-hex npubs), the whole list on every edit. Gated on the reader side by BAN.
set_role
Create or edit a Role (vsk 1, CORD-04 §2). role.role_id is the coordinate; a rename or permission change is a versioned edit of the same id. Gated on the reader side by MANAGE_ROLES + outrank.
stored_memberlist
The memberlist from LOCAL state only: the persisted Guestbook, plus locally observed authors (the synced events DB), plus roster grantees, minus the banlist. Instant and offline-correct; sync_guestbook (post-join, boot, reconnect, live ingest) keeps the store current. The live memberlist remains the authoritative walk — a refounding’s rekey recipient set must never trust a possibly-stale store.
sync_community_list
Sync memberships from the 13302 across devices: fetch this account’s list from bootstrap_relays (its held communities’ relays plus any caller-supplied set for a fresh device), and JOIN every live entry not already held — reconstructing the community from its join material and re-verifying the owner root. Returns the newly-rehydrated communities (so the caller can subscribe + notify).
sync_guestbook
Catch the persisted Guestbook up from its stored cursor (a fresh hold seeds from zero). The fetch straddles the network, so the session re-checks before the store writes. Returns the events that were NEW to the store — the caller surfaces them (presence lines) and refreshes on non-empty.