Membership
This page states membership in the Developer Preview as trueseal-sync ADR-0027 decides it, with the API names of ADR-0028. Destroy Group is on Destroy Group.
Authority and trust boundary
- Any current member may admit, remove or leave. There are no admins and no creator role.
- Every current member is fully trusted for membership.
- A hostile current member can remove every other member.
- A removed device that does not cooperate can still rewrite membership on devices that have not yet seen its removal.
- TrueSeal defends against neither. The response to a hostile or compromised device is Destroy Group.
- Apps that need roles enforce them above the library, before calling it.
API
accept(request)admits a device. See Pairing. It fails withpairingClosedorgroupFull{max}.remove(memberId)issues a manifest without that member. It takes anidfrommembers. It fails withmemberNotFound.leave()issues a manifest without this device and moves it toleaving.meis this device,Member{id, name}.membersis every other member.membersChanged(members)gives the whole current member list, never a delta.admissionDropped(name, reason)says a re-applied admission was dropped because it would exceedmaxGroupSize.statusChanged(notJoined, removed)says this device was deliberately removed and has wiped.statusChanged(notJoined, left)says this device finished leaving and has wiped.maxGroupSizeis the Maximum Group Size, 32 members, a constant.
Manifest validity and ordering
- Each Group Manifest carries
parent, the hash of the manifest it was built on. The genesis manifest has none.parentis covered by the signature. - An issuer sets
version = base.version + 1with checked arithmetic. - A receiver accepts an incoming manifest only if all of these hold:
- the signature verifies;
- the group ID matches;
- the issuer is a member of the receiver’s current manifest;
(version, hash)is strictly greater than the current manifest’s, where on an equal version the lower hash wins;- the version jump is within a sanity bound.
- A manifest that fails is acked and dropped with a delivery issue (ADR-0026).
Convergence promise
Once every member has received the same set of Group Manifests, all members hold an identical manifest. A membership change from an honest member is never lost, unless that member was itself removed concurrently.
Pending Membership Changes
Admitting, removing or leaving writes the new manifest, its outbox entries and a Pending Membership Change in one Session State transaction. Group Manifest outbox entries never expire. The change records its intent (“D is a member”, “X is not a member”) and the hash of the manifest that carried it.
Each time the issuer accepts a newer manifest W, it checks every pending change:
| Case | Condition | Action |
|---|---|---|
| Satisfied | W already reflects the intent | Cleared |
| Deliberately superseded | W descends from a manifest the issuer knows carried the change | Cleared |
| Concurrently overwritten | W neither descends from the change nor satisfies it | Re-applied on top of W at W.version + 1; the new manifest becomes the carrier |
| Issuer removed | W excludes the issuer | Dropped |
When a concurrently overwritten admission would push the group past maxGroupSize, it is dropped instead, with admissionDropped(name, reason), and the joiner stays pendingJoin.
Pending changes survive restarts. An offline issuer re-applies after it reconnects. The app never redoes a membership change.
Event flicker
membersChanged follows the device’s current view. While a concurrent change is being re-applied, a device can see a member leave and rejoin. Apps render the current members list and never treat one event as a permanent fact.
Maximum Group Size
- The group holds at most
maxGroupSizemembers, 32 members. accept(request)past the limit fails locally withgroupFull{max}.- A manifest at the limit fits in one frame (ADR-0025).
Removal
- A device counts itself removed only when the excluding manifest’s
parentis a manifest it holds or has held that includes it. - On a deliberate removal the library wipes the namespace’s group state, outbox and dedup table, generates a fresh identity, sets
statustonotJoined, and raisesstatusChanged(notJoined, removed). Pending sends are dropped without a delivery issue. - False removal: a manifest that excludes the device but was built concurrently, not on a manifest that included it, is stored as the current view. The device does not wipe, and waits for its admitter to re-apply the admission. The usual case is a new joiner whose admission lost a tie.
- The same
TrueSealobject stays usable and can pair again (ADR-0028).
Leave
leave()issues a manifest excluding the local device and persists theleavingstate.- In
leavingthe device sends noSyncand runs no message handler. It stays connected until the relay has accepted every one of its manifest pushes. - It then wipes and rotates as on removal, and raises
statusChanged(notJoined, left). - A crash or going offline in
leavingresumesleavingon the next start. destroyGroup()inleavingabandons the leave (ADR-0029).
State machine
notJoined → pendingJoin → member → leaving → notJoined, and member → notJoined on a deliberate removal. Destroy Group adds destroying.
| From | Trigger | To |
|---|---|---|
member | leave() | leaving. A manifest without this device is issued. |
leaving | The relay has accepted every manifest push | notJoined. The device wipes, makes a fresh identity and raises statusChanged(notJoined, left). |
member | A manifest that excludes this device, built on one that included it | notJoined. The device wipes, makes a fresh identity and raises statusChanged(notJoined, removed). |
member | A manifest that excludes this device, built concurrently | Unchanged. The device stores it as its current view, does not wipe, and waits for its admitter to re-apply the admission. |
Crash points
- Before the issuing transaction commits. No change. The call fails or never returns.
- After it commits. The manifest is redelivered from the outbox. The pending change is re-checked on the next accepted manifest.
- Receiving. Accepting a manifest, updating pending changes and queuing any re-application commit together. A crash before the commit means the relay redelivers the manifest.
- During
leaving.leavingresumes. The wipe happens only after the relay has persisted the last manifest push. - During the wipe. The wipe restarts.
What stays the app’s job
- Roles and permissions.
- Rendering the current member list.
- Deleting the app’s own data after
statusChanged(notJoined, removed | left), if it should. The wipe covers TrueSeal’s Session State only. - Calling
destroyGroup()instead ofremove(memberId)for a lost, stolen or hostile device.