mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2026-08-22 16:39:47 +00:00
wip
This commit is contained in:
@@ -32,23 +32,30 @@ Only the creator can do 2, 4, 5 today; 3 works for one owner at a time. Each is
|
||||
|
||||
The invitee must already be a channel member — its key is then known, and its relay connections exist (a subscriber creates `GRRelay`/`GCHostMember` rows, so `getGroupRelayMembers` returns them for a promoted subscriber unchanged). **Its key must also be verified out of band (§8).**
|
||||
|
||||
Two events, both `requiresSignature`, over the existing relay connections. Named for promotion, not ownership: a later release promoting to any role reuses them; only the link fields are owner-specific.
|
||||
Three events, all `requiresSignature`, over the existing relay connections, delivered in M's support scope (the owner→member private channel, `MSMember`). Named for promotion, not ownership: a later release promoting to any role reuses them; only the link fields are owner-specific.
|
||||
|
||||
```
|
||||
x.grp.promote.inv { invitationId, memberRole, linkRcvId? } -- O1 -> M
|
||||
x.grp.promote.acpt { invitationId, memberKey, linkRcvKey? } -- M -> O1
|
||||
x.grp.promote.inv { invitationId, memberRole, linkRcvId? } -- O1 -> M
|
||||
x.grp.promote.acpt { invitationId, memberKey, linkRcvKey? } -- M -> O1
|
||||
x.grp.promote.reject { invitationId } -- M -> O1
|
||||
```
|
||||
|
||||
- `memberRole` — carried though acceptance is automatic, so a later release can add accept/reject without a wire change.
|
||||
- `invitationId` — binds the acceptance to one invitation; blocks relay replay.
|
||||
- `memberRole` — the role M is offered, shown in M's confirmation prompt.
|
||||
- `invitationId` — binds the acceptance/rejection to one invitation; blocks relay replay.
|
||||
- `memberKey` — M's existing member key. O1 already holds it and **must** check they match and abort otherwise, so a promotion can never introduce a key.
|
||||
- `linkRcvId` / `linkRcvKey` — the link queue's recipient ID, and a fresh recipient auth **public** key M generates. Owner promotions only.
|
||||
|
||||
Everything else M needs to write link data is public or derivable: `shortLinkKey`/`shortLinkId` from the channel link; fixed-data plaintext (hence `rootPubKey`) via `LGET`, re-encrypted under a fresh nonce — readers check `sha3_256(fd) == linkKey` and the root signature over the *plaintext*.
|
||||
|
||||
No third credentials message: `linkRcvId` authorises nothing on its own (`Server.hs:1249` verifies every recipient command against `recipientKeys`), which is equally why the relay carrying these learns nothing. A decliner is never RKEY'd and never enters the link.
|
||||
`linkRcvId` authorises nothing on its own (`Server.hs:1249` verifies every recipient command against `recipientKeys`), which is equally why the relay carrying these learns nothing.
|
||||
|
||||
O1 on acceptance, via a durable worker (mirror `runRelayRequestWorker` — multi-step, cross-network, must survive a crash):
|
||||
**Acceptance is not automatic.** On `x.grp.promote.inv`, M stores a pending promotion and surfaces it (§7); it does not act. Only when the user confirms does M generate `linkRcvKey`, send `x.grp.promote.acpt`, and move the record to accepted-pending-completion. Decline sends `x.grp.promote.reject` and clears it; O1 marks the invitation rejected and notifies its UI, mirroring the relay-rejection path (`x.grp.relay.reject`). An ignored invitation stays pending until it expires on the `relayRequestExpiry` pattern, or is superseded by a fresh invitation.
|
||||
|
||||
This is consent, and — combined with §8 — a second lock. §8 gives O1 M's out-of-band-verified key; the acceptance is signed by M's real key, which O1 checks equals the verified one, so a relay that never delivered the invitation cannot forge an acceptance. It does **not** by itself stop the §8 relay-substitution attack (a malicious relay short-circuits M entirely) — that remains the verified key's job.
|
||||
|
||||
The wire already anticipated this: nothing in inv/acpt changes. The additions are one reject event, a stored pending record on each side (§5), and the accept/reject UI (§7).
|
||||
|
||||
O1 on receiving `x.grp.promote.acpt`, via a durable worker (mirror `runRelayRequestWorker` — multi-step, cross-network, must survive a crash):
|
||||
|
||||
1. Add `linkRcvKey` to the link queue's recipient keys (see below — `RKEY` as shipped cannot do this).
|
||||
2. `LSET` — publish `owners = [oa1, …, oaN, mkOwnerAuth memberId2 memberKey signingKey]`.
|
||||
@@ -191,7 +198,11 @@ Two owners genuinely disagreeing — one adding a relay, the other removing the
|
||||
- `XGrpMemNew` + departure counterparts gain an optional relay-stated count, filled from the number the relay's own `updatePublicGroupData` branch already computes. Owners store it per relay, use the max.
|
||||
- `Internal.hs:1458`, `Commands.hs:4011` `runUpdateGroupProfile`, `Commands.hs:2713` `APIAddGroupRelays` — gate on `memberRole' membership == GROwner` but need "can write link data" (an owner mid-handover cannot). One predicate, all three.
|
||||
- `Commands.hs:3268` `APIAddGroupShortLink` — asserts no role at all; same gate.
|
||||
- `Commands.hs:2872` `APIMembersRole` — route `newRole == GROwner` into §3; require `memberVerifiedCode` on the invitee (§8). The invitation carries the verified key; the acceptance's `memberKey` must equal it or abort, or verification is worthless.
|
||||
- `Commands.hs:2872` `APIMembersRole` — route `newRole == GROwner` into §3 (send `x.grp.promote.inv`, do not change the role); require `memberVerifiedCode` on the invitee (§8). The invitation carries the verified key; the acceptance's `memberKey` must equal it or abort, or verification is worthless.
|
||||
- Pending-promotion record on each side, stored on the member row (columns, mirroring `relay_request_*`): offered role, `invitationId`, status (invited / accepted-pending / rejected), and `linkRcvId`/`linkRcvKey`. On O1 the invitee's row; on M its own membership row. A `promotionPending`-style predicate mirroring `memberPending`/`gmRequiresAttention`, to drive the affordance and its badge.
|
||||
- New commands: `APIAcceptOwnerPromotion groupId` (M generates `linkRcvKey`, sends `x.grp.promote.acpt`, marks accepted-pending), `APIRejectOwnerPromotion groupId` (sends `x.grp.promote.reject`, clears). Handlers in `Commands.hs`; wrappers + strings in both apps.
|
||||
- New events `x.grp.promote.inv` / `.acpt` / `.reject` (`Protocol.hs`, all `requiresSignature`). `x.grp.promote.reject` mirrors `XGrpRelayReject`.
|
||||
- Inbound handlers in `Subscriber.hs`: `x.grp.promote.inv` on M creates the pending record and emits a UI event; `.acpt` on O1 starts the durable worker; `.reject` on O1 marks and notifies.
|
||||
- `Commands.hs:4016` — reorder: write link, then commit and broadcast (§4.2).
|
||||
- `Subscriber.hs:3345` `allowCreate` — widen so an owner-signed `x.grp.mem.role` with a key can TOFU-create a `GROwner`; `isRosterRole GROwner == False` blocks it today, so subscribers can never materialise a new owner.
|
||||
- `Commands.hs:2936` `mKey` — send the key on owner promotion, not only when a roster version is present.
|
||||
@@ -225,9 +236,20 @@ Already ahead in places: owners are pluralised (`ownersContributorsCountStr`, `C
|
||||
|
||||
Replace both with a backend-supplied `canManageLink` on `GroupInfo` — not equivalent to `isOwner`: an owner mid-handover cannot write. Gate link, relay and domain-claim UI on it.
|
||||
|
||||
- `canChangeRoleTo` (`ChatTypes.swift:3060`, `ChatModel.kt:2652`) hard-codes `[.observer, .member]`. Add `.owner`; selecting it starts an async two-phase flow (pending → owner, or failed).
|
||||
- Promoting to owner warns prominently when the invitee's key is unverified and routes to the existing verification screen. Unlike contact verification the consequence is not one conversation but the channel's trust chain (§8).
|
||||
- Owners section in channel info; pending-owner state.
|
||||
**Promoter side (O1):**
|
||||
- `canChangeRoleTo` (`ChatTypes.swift:3060`, `ChatModel.kt:2652`) hard-codes `[.observer, .member]`. Add `.owner`; selecting it sends an invitation and shows an "owner invitation sent — pending" state, not an immediate role change.
|
||||
- Warn prominently when the invitee's key is unverified and route to the existing verification screen. Unlike contact verification the consequence is not one conversation but the channel's trust chain (§8).
|
||||
|
||||
**Invitee side (M): surfacing the pending promotion.** The invitation lands in M's support scope, so that is its native home; the question is the entry point, since a subscriber rarely opens support chat. Variants (not exclusive — an entry point plus a consent surface):
|
||||
|
||||
- **A — accept/reject bar in M's support chat.** Directly mirrors `ContextPendingMemberActionsView` (`ComposeView.swift:432`) / `ComposeContextPendingMemberActionsView.kt` (`ComposeView.kt:1594`): an above-compose bar gated on `scope == memberSupport(self) && promotionPending`. Reuses the exact mechanism; native to where the message arrives. Weak discovery alone — needs a badge to pull M in.
|
||||
- **B — bar above compose in the main channel**, like the relay bar (`ComposeView.swift:399` / `ComposeView.kt:1563`), driven by `promotionPending`. Most visible — M sees it on opening the channel. Note an observer's composer is replaced by "you can't send messages" (`userCantSendReason`), so this bar must take precedence over that.
|
||||
- **C — chat-list row affordance**, like a contact request or group invitation (`ChatListNavLink.swift:495/320`, `ChatListNavLinkView.kt:125/198`): the channel row shows an "Owner invitation" badge/CTA that deep-links to the consent surface. Visible without opening; pairs with A.
|
||||
- **D — a consent screen** reached from any of the above, stating who invited (with verify option), the role, and that ownership grants publishing, moderation, link management, **and that any owner can delete the channel** (§8). Accept/Decline live here; the bars/rows are entry points, not one-tap commits — ownership is heavier than member admission, which does one-tap with a confirmation alert.
|
||||
|
||||
Recommendation: **B as the primary entry (a channel bar, since that is where M's attention is) + C for discovery when the channel is unopened, both opening D.** A is a low-cost addition if support chat is kept as the private owner↔M record. Decline is one tap plus a confirm; accept always goes through D.
|
||||
|
||||
- Owners section in channel info; pending-owner and pending-invitation state.
|
||||
- A role change can revert when it loses a tie-break (§4.1) — surface it.
|
||||
- `ChatView.swift:771` / `Commands.hs:1918` — owners are excluded from the on-open link reconcile; remove the exclusion (§4.2).
|
||||
- **iOS-only:** `GroupChatInfoView.swift:303` skips `apiGetGroupLink` unless `isOwner`; Kotlin fetches unconditionally (`ChatView.kt:403/428`). Reconcile.
|
||||
|
||||
Reference in New Issue
Block a user