Nordhavn / specs / convene / core / Membership

convene / corecontext

Membership

Purpose

The group's life — founding it, bringing a member in, taking a member out, and picking up the key a membership change delivered. Membership is never edited: every change mints the next GroupRevision under a fresh key and the old epoch stands untouched. One ceremony underlies every change: gate on the founder, mint the next epoch, seal the record, deliver to each member's own inbox — the record, and the key wrapped to them alone — and the actor's ring adopts the key it minted. Founding is the same ceremony at the lineage's beginning: no parent, the founder its sole member, and nothing fans out because there is no one else yet. Nothing is posted to a shared feed; each member learns of a new epoch only in their own dead-drop. Only the founder tends the group — the founder is the one member who can read the whole chain, so the check is the founder's to make from what they hold. Removal takes nothing back: the future is sealed under a key the removed member never receives, their exclusion arithmetic, not authority.

The delivered key admits itself on arrival: adopting is one motion the delivery drives — unwrap the key, unseal the membership record that lies beside it with that same key, and confirm the epoch actually names the collector before the key is kept. A delivery whose epoch, once unsealed, does not name its recipient means something upstream lied, and the adoption refuses with an impossible-delivery Alarm rather than keeping a key the collector was never meant to hold.

The full backbone surrounds these acts with its trust ceremony — inviter signatures, passphrase commitments, vouching — deferred in this scoped example; what is kept is the part the keys enforce.

Role Players

Props

Role Methods

group

ring

relay

Functionalities

found

Given a revision id Text and the key GroupKey, composes the founding GroupRevision through the group's mint — the revision id as its own lineage, no parent, the actor its sole member and creator, under that key's id — seals it under that key, records it to the actor's own inbox, and yields the actor's next KeyRing, now holding the first epoch's key. It fails when the ring is sealed — founding happens with the passkey present, like everything else that touches a key. Result: the actor's next KeyRing; fails when the ring is sealed

add

Given the joiner Member, a revision id Text, and the key GroupKey, confirms through the group that the actor is the founder and that the joiner is not already among the current revision's members. Then it mints the next GroupRevision — the current revision's members plus the joiner, under that key's id — seals it under that key, and delivers to each member of the new epoch: to the joiner the record dropped with an invitation WrappedKey, to each continuing member the record dropped with a rotation WrappedKey, and to the actor the record alone. It yields the actor's next KeyRing, holding the new epoch's key. It fails when the actor is not the founder, and when the joiner is already a member. Result: the actor's next KeyRing; fails when the actor is not the founder or the joiner is already a member

remove

Given the leaver Member, a revision id Text, and the key GroupKey, confirms through the group that the actor is the founder and that the leaver is among the current revision's members. Then it mints the next GroupRevision — the current revision's members without the leaver, under that key's id — seals it under that key, and delivers to each remaining member the record dropped with a rotation WrappedKey, the actor receiving the record alone — and to the leaver nothing, ever: no key they could open the future with, no record naming the epoch that stands without them. It yields the actor's next KeyRing, holding the new epoch's key. It fails when the actor is not the founder, and when the leaver is not a member. Result: the actor's next KeyRing; fails when the actor is not the founder or the leaver is not a member

adopt

Given a key id Text, collects the actor's own inbox through the relay, takes the wrapped key carrying that key id, unwraps it, unseals with the unwrapped key the sealed revision that opens under it, confirms that revision names the actor among its members, and yields the actor's next KeyRing, holding the key. It fails when nothing in the actor's inbox carries that key id, and it fails raising an impossible-delivery Alarm when the unsealed revision does not name the actor — in that case the key is not kept. Result: the actor's next KeyRing; fails when nothing in the inbox carries the key id, and when the unsealed revision does not name the actor

Scenarios

Anne founds the group for case 7421

Given: a Membership with actor anne.bruun, a relay, an empty group, and anne's opened ring holding no keys When: found is performed with revision id "g-7421" and the GroupKey "k-0142" Then: a SealedRevision under key id "k-0142", sealing the founding revision "g-7421" with "anne.bruun" its sole member, is dropped into anne.bruun's inbox — the membership readable by no one without the key — and the yielded ring holds "k-0142"

Founding with a sealed ring fails

Given: a Membership with actor anne.bruun, a relay, an empty group, and anne's ring sealed When: found is performed with revision id "g-7421" and the GroupKey "k-0142" Then: the founding fails because the ring is sealed, and nothing is dropped

The actor's own record survives the ceremony

Given: a Membership with actor anne.bruun, a relay, a group holding the founding "g-7421" created by "anne.bruun" with member "anne.bruun" under key "k-0142", and anne's opened ring holding "k-0142" When: add is performed with joiner karin.dahl, revision id "g-7421.2", and the GroupKey "k-0150" Then: the relay delivered the new SealedRevision to anne.bruun's own inbox as well as to karin.dahl's — the record the actor minted is her record too, and no WrappedKey travelled to anne.bruun

Anne brings Karin in, and Sofie's key rotates

Given: a Membership with actor anne.bruun, a relay, a group holding the founding "g-7421" created by "anne.bruun" and the current "g-7421.2" with members "anne.bruun" and "sofie.lind" under key "k-0150", and anne's opened ring holding "k-0142" and "k-0150" When: add is performed with joiner karin.dahl, revision id "g-7421.3", and the GroupKey "k-0163" Then: karin.dahl's inbox gains an invitation WrappedKey of "k-0163" and the SealedRevision of "g-7421.3"; sofie.lind's inbox gains a rotation WrappedKey of "k-0163" and the same SealedRevision; anne.bruun's inbox gains the same SealedRevision and no WrappedKey — hers is the record too, the key already on her ring; and the yielded ring holds "k-0163"

Only the founder invites

Given: a Membership with actor sofie.lind, a relay, and a group whose founding "g-7421" was created by "anne.bruun" with a current revision "g-7421.3" When: add is performed with joiner mette.krog, revision id "g-7421.4", and the GroupKey "k-0180" Then: the adding fails because the actor is not the founder, and nothing is dropped

A member already in the group is not added again

Given: a Membership with actor anne.bruun, a relay, and a group whose founding "g-7421" was created by "anne.bruun" and whose current "g-7421.2" has members "anne.bruun" and "sofie.lind" When: add is performed with joiner sofie.lind, revision id "g-7421.3", and the GroupKey "k-0163" Then: the adding fails because the joiner is already a member, and nothing is dropped

The remover's own record survives the ceremony

Given: a Membership with actor anne.bruun, a relay, a group holding the founding "g-7421" created by "anne.bruun" and the current "g-7421.2" with members "anne.bruun" and "sofie.lind" under key "k-0150", and anne's opened ring holding "k-0142" and "k-0150" When: remove is performed with leaver sofie.lind, revision id "g-7421.3", and the GroupKey "k-0163" Then: the relay delivered the new SealedRevision to anne.bruun's own inbox — the record the actor minted is her record too — and nothing at all travelled to sofie.lind

Karin is removed, and the future is sealed away from her

Given: a Membership with actor anne.bruun, a relay, a group whose founding "g-7421" was created by "anne.bruun" and whose current "g-7421.3" has members "anne.bruun", "sofie.lind", and "karin.dahl" under key "k-0163", and anne's opened ring When: remove is performed with leaver karin.dahl, revision id "g-7421.4", and the GroupKey "k-0264" Then: sofie.lind's inbox gains a rotation WrappedKey of "k-0264" and the SealedRevision of "g-7421.4" with members "anne.bruun" and "sofie.lind"; karin.dahl's inbox gains nothing; and the yielded ring holds "k-0264"

Removing someone who is not a member is refused

Given: a Membership with actor anne.bruun, a relay, and a group whose founding "g-7421" was created by "anne.bruun" and whose current "g-7421.3" has members "anne.bruun", "sofie.lind", and "karin.dahl" When: remove is performed with leaver mette.krog, revision id "g-7421.4", and the GroupKey "k-0264" Then: the removal fails because the leaver is not a member, and nothing is dropped

Only the founder removes

Given: a Membership with actor sofie.lind, a relay, and a group whose founding "g-7421" was created by "anne.bruun" with a current revision "g-7421.3" When: remove is performed with leaver karin.dahl, revision id "g-7421.4", and the GroupKey "k-0264" Then: the removal fails because the actor is not the founder, and nothing is dropped

Sofie adopts the rotated key, which opens the record beside it

Given: a Membership with actor sofie.lind, a relay whose feed for sofie.lind's inbox holds a rotation WrappedKey of "k-0264" and a SealedRevision under key id "k-0264" sealing "g-7421.4" with members "anne.bruun" and "sofie.lind", an empty group, and sofie's opened ring holding "k-0150" and "k-0163" When: adopt is performed with key id "k-0264" Then: the yielded ring holds "k-0264" alongside her earlier keys — the delivered key unsealed the record beside it and confirmed her in the epoch

A delivery whose epoch does not name its recipient is refused

Given: a Membership with actor karin.dahl, a relay whose feed for karin.dahl's inbox holds a rotation WrappedKey of "k-0264" and a SealedRevision under key id "k-0264" sealing "g-7421.4" with members "anne.bruun" and "sofie.lind", an empty group, and karin's opened ring holding "k-0163" When: adopt is performed with key id "k-0264" Then: the adoption fails raising an impossible-delivery Alarm — the unsealed revision does not name her — and her ring does not gain "k-0264"

Nothing waiting means nothing to adopt

Given: a Membership with actor sofie.lind, a relay whose feed for sofie.lind's inbox holds no deliveries, an empty group, and sofie's opened ring When: adopt is performed with key id "k-0264" Then: the adoption fails because nothing in her inbox carries "k-0264"