Expand description
Orchestration that ties the Community send/delete primitives to persistence, with multi-account safety. This is the layer Tauri commands are thin wrappers over: it publishes a message AND retains its ephemeral key (so the sender can later delete it), and deletes by loading that retained key back.
Every method is SessionGuard-gated: a swap_session can happen at any await
point, and persisting an ephemeral secret (or reading one) must never cross into
the wrong account’s DB.
Structs§
- Base
Catchup - Outcome of
catch_up_server_root: the base epoch reached, and whether an AUTHORIZED base rotation EXCLUDED us (a read-cut / private ban).removedis the catch-all “you’ve been removed” signal for a cryptographically cut member who can no longer read the banlist to learn it the normal way. - Community
Capabilities - The local user’s effective management capabilities in a community, resolved purely by the role engine (positions + permission bits; the owner is just the role at position 0 — NOTHING is owner-hardcoded). The frontend gates each management affordance on the matching bit, so an admin whose role carries a permission gets the exact same affordance as the owner.
Enums§
- Rekey
Outcome - The result of applying a received channel Rekey (3303).
Constants§
- MAX_
COMMUNITIES - The active signer for authority actions (bunker support): the live client’s signer — which covers a NIP-46 bunker — falling back to the local vault keys when there is no client OR the client has no signer attached (local accounts, headless/CLI paths, and tests). Every keyless control edition + moderation hide signs through this, so a bunker account can create AND administer a community. (The REKEY path is the one exception — its blob locator needs a raw ECDH the signer can’t expose, so it still requires a local key; the ban/privatize flows fail-fast for bunker accounts.) Max communities a device may hold locally. The synced Community List is a single NIP-44 event (65 KB plaintext); past the cap even the slimmed list can’t encrypt, so a NEW join/create is rejected above it (the user leaves one to make room).
Functions§
- accept_
invite - Accept a parked invite and persist the member-view Community (the user-consented half of the carrier — the inbound handler only parks invites; this is reached from an explicit accept command). Guards against id-collision overwrites:
- accept_
public_ invite - Accept a fetched public-invite bundle: reject if expired, join via the guarded member-save (caps + id-collision checks), then patch in the preview’s display metadata (description/icon) so the new member sees them immediately.
- am_
i_ banned - Is the local user in this community’s (folded, cached) banlist? Drives BAN self-removal: a
banned member tears down locally (drop the community keys + wipe local chat data) exactly like a kick,
but CANNOT rejoin — re-detecting the ban on any later sync re-removes them, and admins can’t invite a
banned npub. Reads the cached banlist, so refresh it via
fetch_and_apply_banlistfirst for an authoritative (realtime or boot) check. - apply_
channel_ rekey - Apply a received, already-opened channel Rekey (
super::rekey::open_rekey_event) forcommunity. - apply_
server_ root_ rekey - Apply a received, already-opened SERVER-ROOT (base) Rekey for
community— the base counterpart toapply_channel_rekey. Verifies the rotator’s server-wide rotation authority (BAN, “role-based, not owner-only”), checks continuity against the held prior ROOT (when held), finds + opens MY ServerRoot-scope blob, and commits the new root via the atomic base head+archive write. The new root reaches me ONLY through my ECDH blob — if I was removed in this rotation I find no blob (NotARecipient) and recover nothing.SessionGuard-gated; synchronous (one guard + the write re-check suffice, same asapply_channel_rekey). - build_
presence - Announce presence (join/leave) into a channel: a kind-3306 inner signed by the active identity,
published under a fresh ephemeral outer. Content is
"leave", plain"join", or — for a join via a public invite — a small JSON{"by":"<inviter npub>","l":"<label>"}carrying attribution (which link/source brought this member; members-only). Client best-practice (not enforced); no deletion key retained. Callers treat failure as non-fatal.attribution=Some((inviter_npub, label))on an invite-join, elseNone. Build + sign a presence (3306) inner event WITHOUT publishing. Lets the caller record the local system event first (memory→DB, like an outgoing message) and publish in the background — the relay echo then dedups by this inner’s id.inner.idis the system-event dedup key. - caller_
can_ manage_ role_ id - Can the local caller grant/revoke
role_id— i.e. do they holdMANAGE_ROLESAND outrank that role’s position? The crown’s gate, expressed as the POSITION rule (NOT an owner check): the owner is just position 0, so in the single-@admin-role MVP this resolves to “owner only” because the @admin role sits directly below position 0 — but it generalizes to any role hierarchy.falseif the role is unknown. - caller_
can_ manage_ roles - True iff the local user may manage roles — i.e. holds the
MANAGE_ROLESpermission. Permission-based, NOT a hardcoded owner check: the owner is simply the uppermost role and holds every permission; any member granted a role carryingMANAGE_ROLESqualifies just the same. - caller_
capabilities - caller_
has_ permission - Does the local user hold
permissionin this community? The generalizedcaller_can_manage_roles: owner = supreme (every bit), otherwise the union of their granted roles’ bits (the role engine). Drives both the capability report and the producer-side authority gates — no hardcoded owner check. - can_
moderation_ hide - Can
actor_hexmoderation-hide a message authored byauthor_hexin this community? True iff the actor holds MANAGE_MESSAGES and strictly outranks the author (the owner is unhideable). This is the SINGLE source of truth for moderation authority — both the publish gate (publish_owner_hide) and the UI affordance (get_message_delete_options) call it, so the button shown can never disagree with what the publish will actually allow. - catch_
up_ channel_ rekeys - Catch a channel up to the latest epoch it is still a recipient of (windowed scan): fetch every
rekey published since our held epoch and apply the chain. Returns the channel’s new current epoch.
Idempotent + cheap on the steady state (no new rotations → one empty-window fetch → returns the
held epoch). 3303s are addressed by the server-root-derived
rekey_pseudonym, so this is a SEPARATE fetch from the channel message plane (the exception). - catch_
up_ server_ root - create_
community - Create a brand-new Community end-to-end: mint keys + the default channel, persist it locally, and publish its GroupRoot + ChannelMetadata to the Community’s relays. Returns the created Community. (The caller then runs the subscription refresh so it starts receiving.)
- create_
public_ invite - delete_
message - Delete a message the local user previously sent, by its INNER message id (what the UI holds). Loads the retained ephemeral key + the outer event id it points at, then NIP-09-deletes that outer event. Errors if no key is retained (not ours, or already deleted).
- dissolve_
community - fetch_
and_ apply_ banlist - fetch_
and_ apply_ control - Fetch the control plane ONCE and apply every slice — banlist, roles, invite links, metadata — from a
single REQ + single fold. Sync/join/boot call THIS instead of the four
fetch_and_apply_*in sequence (which was four identical REQs). Banlist is applied first so a caller’s subsequentam_i_bannedsees the freshest list. Each slice is best-effort; one failing doesn’t abort the rest. (Solo callers that need a single slice — e.g. revoke refreshing invite links — still use the individualfetch_and_apply_*.) - fetch_
and_ apply_ control_ full fetch_and_apply_controlat Full evidence — for callers whose folded view gates a DESTRUCTIVE decision (the pre-admin-write sync: itsis_publicread routes a ban through the member-severing read-cut path, so it must see the completest control plane the reachable relays allow).- fetch_
and_ apply_ invite_ links - Fetch the control plane and apply the folded invite-link AGGREGATE locally: UNION the locators of
every per-creator vsk=8 edition whose
creatorheldCREATE_INVITEin the AUTHORIZED roster (the keyless gate, same shape as the banlist’s BAN check), advancing each authorized creator’s head (refuse-downgrade). The union is the source of truth for the Public/Private mode (is_public) + the metrics — NOT join-gating (joining is envelope-only). Returns the aggregate set (empty = Private). - fetch_
and_ apply_ metadata - Fetch the Community’s control plane and apply folded METADATA edits locally: the GroupRoot
(vsk=0 — community name/description/icon/banner) and each ChannelMetadata (vsk=2 — channel name). An
edition applies only if its signer held
MANAGE_METADATAin the AUTHORIZED roster (the keyless gate, same as the producer) AND is strictly newer than the head we hold (refuse-downgrade by version). Identity/transport fields (server_root_key,relays,owner_attestation) are NEVER taken from a metadata edit — a manage-metadata admin edits DISPLAY, not the community’s identity. Best-effort: returnsOkeven when nothing applied. This is what makes an owner/admin’s edit sync to every member. - fetch_
and_ apply_ roles - Fetch the Community’s role graph (real-npub control editions, kind 3308) and fold it into the local roster. Fetches by the server-root pseudonym (not by author — the outer is ephemeral), opens each edition under the server-root key, and folds: verify authorship, bind entity↔content, version-fold, quarantine gaps. Advances each entity’s monotonic head (the per-entity refuse-downgrade floor) and refreshes the roster cache. Returns the folded roster.
- fetch_
public_ invite - Fetch + decrypt the bundle for a public-invite token from the given bootstrap relays.
Queries the addressable coordinate (
d= token locator, author = token signer) and verifies the signer, so an impostor squatting the locator is rejected. - grant_
role - Grant
membera role (requires theMANAGE_ROLESpermission). Publishes the per-member Grant event. The member already holds read keys from membership; the roster entry adds write authority, exercised by signing their own control actions, which peers verify against the roster. - is_
proven_ owner - True iff the local user is the PROVEN owner of this community — derived by verifying the owner
attestation against
my_public_key()(keyless: the owner is the npub that signed the attestation binding this community_id). The check honest clients use to gate owner-only actions (mint invites, set images) and to render the owner crown. - is_
public - The computed Public/Private mode: a community is PUBLIC iff the folded per-creator invite-link
aggregate has ≥1 active locator, else PRIVATE. Every member computes the same value from the folded
editions, which is what lets it drive rekey-on-removal consistently (Private removals rekey the base
to the roster; Public ones don’t — anti-memberlist). Reads the cached aggregate, which is only as
fresh as the last successful latest-page sync (
fetch_and_apply_invite_links, wired best-effort into the sync path) — a member who only scrolled back, or whose sync failed, can hold a stale mode (which is whyrevoke_public_inviterefreshes the aggregate before deciding to privatize). - latest_
invite_ preview - Read-only freshen for an invite preview: build the bundle’s ephemeral community, fold the live control plane, and return the LATEST authorized display metadata — never the bundle’s mint-time snapshot (which goes stale the moment metadata is edited; mirrors the website preview). No DB floors and no persistence: the previewer isn’t a member, so there is no local state to anchor. Any failure falls back to the snapshot so a flaky relay can’t blank the preview.
- persist_
webxdc_ signal - Persist an inbound WebXDC peer signal as a kind-30078 event row — the SAME shape the DM
peer-advertisement handler writes (content
peer-advertisement/peer-left,reference_id= topic,webxdc-topic/webxdc-node-addrtags) — so the miniapp layer’sget_active_peer_advertisements(latest-per-npub, left-tombstone-aware) reads both transports identically. This is what lets a member who closed Vector mid-session rediscover the active players on reopen. Idempotent viaevent_exists. - preload_
community - Warm a community’s primary-channel first page into the RAM preload cache BEFORE the user joins,
so accepting opens a populated chat instead of paying the join sync. RAM-only and side-effect-
free: builds the member view from the bundle WITHOUT persisting (nothing is stored for a
community the user may decline), fetches one page, and stashes it keyed by community id (the
fetch also warms the relay connection). Best-effort — any failure just leaves Join to sync
normally. Spawn this behind a
SessionGuard; promotion on Join re-validates freshness. - publish_
banlist - Replace the Community banlist and publish it as a real-npub-signed 3308 EDITION (vsk=4) at the
community-scoped banlist locator (keyless; foldable + re-anchorable).
banned_hexis the full new list (latest-wins). The actor’s inner signature IS the authority proof; every member re-verifies it heldBANagainst the authorized roster on receipt. Publish FIRST, then persist locally on success — a failed publish must not leave us enforcing a ban no one else sees. - publish_
kick - publish_
migration_ carrier - Publish the v1→v2 migration CARRIER: an owner-signed GroupDissolved tombstone whose
content carries the migration payload (§migration). One event seals v1 AND delivers the
v2 keys to every member. Sealed at BOTH coordinates like an ordinary dissolution
(rotation-stable + current-epoch fast path) with BYTE-IDENTICAL inner content, so the
member’s fold and probe extract the same payload. NO link-retire/rekey — dissolution
moots every link. Does NOT seal locally: the wizard’s own flip handles the owner’s
transition to v2. Bunker-safe (signs through the active
VectorSigner). - publish_
my_ invite_ links - Publish the LOCAL user’s OWN invite-link set as a
CREATE_INVITE-gated vsk=8 control edition at their per-creator coordinate — one of the per-creator lists members fold into the aggregate active-set.my_locatorsis the FULL new set of THIS creator’s active link locators (hex; the token in the URL is the secret, never listed). Publish FIRST, then advance the head + merge into the cached aggregate on success (relay-authoritative + phantom-head rule). A creator manages only their own list — noMANAGE_INVITES. Carries the actor’svaccitation so a non-owner creator’s authority is verifiable. - publish_
owner_ hide - Moderation-hide: publish a 3305 delete for another member’s message, signed by the actor’s REAL npub (keyless). Authority is the inner signature, re-verified by every member against the owner-rooted roster (MANAGE_MESSAGES + a strict outrank of the target’s author). Permanent (the tombstone can’t be un-published).
- publish_
presence - publish_
presence_ event - Publish a pre-built presence inner (from
build_presence) to the channel’s recipient set. - publish_
typing_ signal - Publish a typing indicator (3311) into a channel: an inner “typing” event signed by the member,
sealed under the channel epoch key like presence. The Community-transport twin of the NIP-17
typing rumor. Ephemeral — never persisted/folded; the latency-sensitive single-attempt path
(
durable = false), and callers treat failure as non-fatal (a dropped keystroke ping is harmless; the next one ~every few seconds covers it). - publish_
webxdc_ signal - Publish a WebXDC realtime peer signal (3310) into a channel: an advertisement of the local
Iroh node for a Mini App session (
node_addr= Some) or a peer-left (node_addr= None). The Community-transport twin of the NIP-17 peer-advertisement/peer-left DM rumors — signed by the member’s real identity (a member can’t forge another player’s presence), sealed under the channel epoch key like presence. Callers treat failure as non-fatal (a missed ad only delays discovery; the next re-advertise covers it). - republish_
channel_ metadata - Rename a channel and republish its ChannelMetadata as a real-npub 3308 edition (vsk=2) so
members fold it via
fetch_and_apply_metadata. Keyless authority: the actor must holdMANAGE_CHANNELS(channel edits are a channel-management action; the owner holds every permission).channel_idmust be one ofcommunity’s channels. Publish-FIRST then persist on success (relay- authoritative, phantom-head-safe — same contract as the community GroupRoot). - republish_
community_ metadata - Persist edited Community display metadata and republish the GroupRoot as a real-npub 3308 edition
(vsk=0) so other members + re-anchoring pick it up. Keyless authority: the actor must hold
MANAGE_METADATA(the owner holds every permission). The caller mutatescommunity(name / description / icon / banner) first; this gates, saves it, then publishes the next edition version. - retry_
pending_ read_ cut - Retry an outstanding PRIVATE-community read-cut re-seal, if one is pending. Called from the sync
path so a re-seal that failed during a ban (e.g. a relay outage) AUTO-RECOVERS on the owner’s next
community sync — no manual re-ban needed. No-op if nothing is pending. If the community has since gone
PUBLIC the read-cut is moot (anti-memberlist: a Public ban doesn’t rotate the base), so the stale flag
is cleared. Best-effort + idempotent; the re-seal authority (BAN) is enforced by
rotate_server_root. - revoke_
public_ invite - Revoke a public invite: NIP-09-delete the bundle event (by its addressable coordinate, signed by the
token-derived key we re-derive from the retained token), forget the token locally, and republish the
invite-link registry so the mode tracks reality. If this was the LAST link, the community goes
Private → it is re-founded (privatize): the base key is rotated to the observed-participants set,
sealing out link-joined lurkers who never spoke. Creator-only: you can only retire YOUR OWN
links (the token is held only by its creator); the privatize rekey is
BAN-gated + needs a local key. - revoke_
role - Revoke a role from
member(owner/admin authority) — instant logical (the role record is dropped, so the grant-set check stops honoring their actions). The physical lockout (channel rekey per) is a later step; this only edits the grant. In the MVP a role is permission bits, NOT a channel read key (channels aren’t role-gated), so a revoke needs NO rekey and a bunker account can do it freely. WHEN role-gated channels ship, the rekey-on-revoke path must adopt the same bunker fail-fast guard aspublish_banlist/revoke_public_invite(a rekey needs a raw local key). - rotate_
channel - Rotate a channel’s key (a channel rekey): mint a fresh-random key for
current_epoch + 1, deliver it torecipientsas one self-proving 3303 event (epoch + every recipient blob + the prior-epoch commitment + my real-npub authority sig, all in one — the design tenet), publish it, then advance MY local epoch. Returns the new epoch. - send_
message - Publish a Community message and retain its ephemeral key in the account DB so the sender can delete it later. Returns the published outer event.
- send_
signed_ message - Publish a message whose inner authorship event was signed externally (via the active
signer — local OR bunker) and retain its ephemeral key. Use this from the command
layer where
client.signer()is available; it gives bunker accounts send parity with DMs. (Local-only callers/tests can usesend_message.) - set_
member_ grant - Set a member’s complete role set (owner/admin authority) and publish their per-member
Grant event (vsk=3). Empty
role_idsrevokes all of that member’s roles. Persists the updated local graph BEFORE the publish await (so our own client reflects it immediately and the write lands in the captured account); the relay echo dedups. - sync_
before_ admin_ write - FRESHEN-BEFORE-WRITE guard for an administrative write (rekey / ban / kick / grant / revoke / metadata): hop any base rotation + fold the LATEST control plane from ALL relays + (for a rekey) ingest channel activity, so the write acts on the freshest reachable truth — not just a stale local view. The demonstrated bug this fixes: privatizing before observing a member’s activity wrongly cut them.