Skip to main content

xmtp_mls/groups/
intents.rs

1use super::{
2    group_membership::GroupMembership,
3    group_permissions::{MembershipPolicies, MetadataPolicies, PermissionsPolicies},
4};
5use crate::groups::mls_ext::WelcomePointersExtension;
6use openmls::key_packages::KeyPackage;
7use openmls::prelude::{
8    MlsMessageOut,
9    tls_codec::{Error as TlsCodecError, Serialize},
10};
11use prost::{Message, bytes::Bytes};
12use std::collections::{HashMap, HashSet};
13use thiserror::Error;
14use xmtp_common::types::Address;
15use xmtp_configuration::GROUP_KEY_ROTATION_INTERVAL_NS;
16use xmtp_cryptography::secret::{Zeroize, Zeroizing};
17use xmtp_id::key_package::{KeyPackageVerificationError, VerifiedKeyPackageV2, WrapperAlgorithm};
18use xmtp_mls_group_state::{
19    app_data::{
20        component_id::ComponentId,
21        fields::{FieldWrite, WriteOperation},
22    },
23    group_mutable_metadata::MetadataField,
24};
25use xmtp_proto::{
26    ConversionError,
27    xmtp::mls::database::{
28        AccountAddresses, AddressesOrInstallationIds as AddressesOrInstallationIdsProtoWrapper,
29        CommitPendingProposalsData, InstallationIds, PostCommitAction as PostCommitActionProto,
30        ProposeGroupContextExtensionData, ProposeMemberUpdateData, ReaddInstallationsData,
31        SendMessageData, UpdateAdminListsData, UpdateGroupMembershipData, UpdateMetadataData,
32        UpdatePermissionData,
33        addresses_or_installation_ids::AddressesOrInstallationIds as AddressesOrInstallationIdsProto,
34        commit_pending_proposals_data,
35        post_commit_action::{
36            Installation as InstallationProto, Kind as PostCommitActionKind,
37            SendWelcomes as SendWelcomesProto,
38        },
39        propose_group_context_extension_data, propose_member_update_data,
40        readd_installations_data::{
41            V1 as ReaddInstallationsV1, Version as ReaddInstallationsVersion,
42        },
43        send_message_data::{V1 as SendMessageV1, Version as SendMessageVersion},
44        update_admin_lists_data::{V1 as UpdateAdminListsV1, Version as UpdateAdminListsVersion},
45        update_group_membership_data::{
46            V1 as UpdateGroupMembershipV1, Version as UpdateGroupMembershipVersion,
47        },
48        update_metadata_data::{V1 as UpdateMetadataV1, Version as UpdateMetadataVersion},
49        update_permission_data::{
50            self, V1 as UpdatePermissionV1, Version as UpdatePermissionVersion,
51        },
52    },
53    xmtp::mls::message_contents::{ComponentType, WelcomePointerWrapperAlgorithm},
54};
55
56mod queue;
57pub use queue::*;
58
59#[derive(Debug, Error)]
60pub enum IntentError {
61    #[error("conversion error: {0}")]
62    Conversion(#[from] xmtp_proto::ConversionError),
63    #[error("key package verification: {0}")]
64    KeyPackageVerification(#[from] KeyPackageVerificationError),
65    #[error("TLS Codec error: {0}")]
66    TlsError(#[from] TlsCodecError),
67    #[error(transparent)]
68    Storage(#[from] xmtp_db::StorageError),
69    #[error("missing update permission")]
70    MissingUpdatePermissionVersion,
71    #[error("missing payload")]
72    MissingPayload,
73    #[error("missing update admin version")]
74    MissingUpdateAdminVersion,
75    #[error("missing post commit action")]
76    MissingPostCommit,
77    #[error("unsupported permission version")]
78    UnsupportedPermissionVersion,
79    #[error("unknown permission update type")]
80    UnknownPermissionUpdateType,
81    #[error("unknown value for PermissionPolicyOption")]
82    UnknownPermissionPolicyOption,
83    #[error("unknown value for AdminListActionType")]
84    UnknownAdminListAction,
85    #[error("intent payload error: {0}")]
86    Generic(String),
87}
88
89impl From<prost::DecodeError> for IntentError {
90    fn from(error: prost::DecodeError) -> Self {
91        IntentError::Conversion(xmtp_proto::ConversionError::Decode(error))
92    }
93}
94
95/// The encoded envelope of a message to send. It can hold a device-sync
96/// HMAC root key, so it is wiped on drop and `Debug` shows only its length.
97#[derive(Debug, Clone)]
98pub struct SendMessageIntentData {
99    pub message: xmtp_cryptography::Secret,
100}
101
102impl SendMessageIntentData {
103    pub fn new(message: Vec<u8>) -> Self {
104        Self {
105            message: message.into(),
106        }
107    }
108
109    /// The returned bytes are the stored intent. The proto temporary is wiped
110    /// after encoding.
111    pub(crate) fn to_bytes(&self) -> Vec<u8> {
112        let mut data = SendMessageData {
113            version: Some(SendMessageVersion::V1(SendMessageV1 {
114                payload_bytes: self.message.as_slice().to_vec(),
115            })),
116        };
117        let bytes = data.encode_to_vec();
118        if let Some(SendMessageVersion::V1(v1)) = &mut data.version {
119            v1.payload_bytes.zeroize();
120        }
121        bytes
122    }
123
124    pub(crate) fn from_bytes(data: &[u8]) -> Result<Self, IntentError> {
125        let msg = SendMessageData::decode(data)?;
126        let payload_bytes = match msg.version {
127            Some(SendMessageVersion::V1(mut v1)) => std::mem::take(&mut v1.payload_bytes),
128            None => return Err(IntentError::MissingPayload),
129        };
130
131        Ok(Self::new(payload_bytes))
132    }
133}
134
135impl From<SendMessageIntentData> for Vec<u8> {
136    fn from(intent: SendMessageIntentData) -> Self {
137        intent.to_bytes()
138    }
139}
140
141#[derive(Debug, PartialEq, Eq, Clone)]
142pub enum AddressesOrInstallationIds {
143    AccountAddresses(Vec<String>),
144    InstallationIds(Vec<Vec<u8>>),
145}
146
147impl From<AddressesOrInstallationIds> for AddressesOrInstallationIdsProtoWrapper {
148    fn from(address_or_id: AddressesOrInstallationIds) -> Self {
149        match address_or_id {
150            AddressesOrInstallationIds::AccountAddresses(account_addresses) => {
151                AddressesOrInstallationIdsProtoWrapper {
152                    addresses_or_installation_ids: Some(
153                        AddressesOrInstallationIdsProto::AccountAddresses(AccountAddresses {
154                            account_addresses,
155                        }),
156                    ),
157                }
158            }
159            AddressesOrInstallationIds::InstallationIds(installation_ids) => {
160                AddressesOrInstallationIdsProtoWrapper {
161                    addresses_or_installation_ids: Some(
162                        AddressesOrInstallationIdsProto::InstallationIds(InstallationIds {
163                            installation_ids,
164                        }),
165                    ),
166                }
167            }
168        }
169    }
170}
171
172impl TryFrom<AddressesOrInstallationIdsProtoWrapper> for AddressesOrInstallationIds {
173    type Error = IntentError;
174
175    fn try_from(wrapper: AddressesOrInstallationIdsProtoWrapper) -> Result<Self, Self::Error> {
176        match wrapper.addresses_or_installation_ids {
177            Some(AddressesOrInstallationIdsProto::AccountAddresses(addrs)) => Ok(
178                AddressesOrInstallationIds::AccountAddresses(addrs.account_addresses),
179            ),
180            Some(AddressesOrInstallationIdsProto::InstallationIds(ids)) => Ok(
181                AddressesOrInstallationIds::InstallationIds(ids.installation_ids),
182            ),
183            _ => Err(IntentError::MissingPayload),
184        }
185    }
186}
187
188impl From<Vec<Address>> for AddressesOrInstallationIds {
189    fn from(addrs: Vec<Address>) -> Self {
190        AddressesOrInstallationIds::AccountAddresses(addrs)
191    }
192}
193
194impl From<Vec<Vec<u8>>> for AddressesOrInstallationIds {
195    fn from(installation_ids: Vec<Vec<u8>>) -> Self {
196        AddressesOrInstallationIds::InstallationIds(installation_ids)
197    }
198}
199
200/// A metadata update. Its values can hold the commit-log signer's private key
201/// as hex, so they are wiped on drop and `Debug` shows only the field name.
202#[derive(Clone)]
203pub struct UpdateMetadataIntentData {
204    pub field_name: String,
205    pub field_value: String,
206    /// Compare-and-swap guard. When set, publishing abandons this intent
207    /// unless the field's currently committed value still equals this.
208    /// `None` keeps the historical last-writer-wins behavior.
209    pub expected_field_value: Option<String>,
210}
211
212impl std::fmt::Debug for UpdateMetadataIntentData {
213    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
214        f.debug_struct("UpdateMetadataIntentData")
215            .field("field_name", &self.field_name)
216            .field("field_value", &"<redacted>")
217            .field(
218                "expected_field_value",
219                &self.expected_field_value.as_ref().map(|_| "<redacted>"),
220            )
221            .finish()
222    }
223}
224
225impl Drop for UpdateMetadataIntentData {
226    fn drop(&mut self) {
227        self.field_value.zeroize();
228        self.expected_field_value.zeroize();
229    }
230}
231
232impl UpdateMetadataIntentData {
233    pub fn new(field_name: String, field_value: String) -> Self {
234        Self {
235            field_name,
236            field_value,
237            expected_field_value: None,
238        }
239    }
240
241    /// A guarded update: publishing abandons the intent (marking it
242    /// [`xmtp_db::group_intent::IntentState::Superseded`]) unless the
243    /// committed value still equals `expected_field_value`.
244    pub fn new_guarded(
245        field_name: String,
246        field_value: String,
247        expected_field_value: String,
248    ) -> Self {
249        Self {
250            field_name,
251            field_value,
252            expected_field_value: Some(expected_field_value),
253        }
254    }
255
256    pub fn new_update_group_name(group_name: String) -> Self {
257        Self::new(MetadataField::GroupName.to_string(), group_name)
258    }
259
260    pub fn new_update_group_image_url_square(group_image_url_square: String) -> Self {
261        Self::new(
262            MetadataField::GroupImageUrlSquare.to_string(),
263            group_image_url_square,
264        )
265    }
266
267    pub fn new_update_group_description(group_description: String) -> Self {
268        Self::new(MetadataField::Description.to_string(), group_description)
269    }
270
271    pub fn new_update_app_data(app_data: String, expected_app_data: Option<String>) -> Self {
272        match expected_app_data {
273            Some(expected) => {
274                Self::new_guarded(MetadataField::AppData.to_string(), app_data, expected)
275            }
276            None => Self::new(MetadataField::AppData.to_string(), app_data),
277        }
278    }
279
280    pub fn new_update_conversation_message_disappear_from_ns(from_ns: i64) -> Self {
281        Self::new(
282            MetadataField::MessageDisappearFromNS.to_string(),
283            from_ns.to_string(),
284        )
285    }
286    pub fn new_update_conversation_message_disappear_in_ns(in_ns: i64) -> Self {
287        Self::new(
288            MetadataField::MessageDisappearInNS.to_string(),
289            in_ns.to_string(),
290        )
291    }
292
293    pub fn new_update_group_min_version_to_match_self(min_version: String) -> Self {
294        Self::new(
295            MetadataField::MinimumSupportedProtocolVersion.to_string(),
296            min_version,
297        )
298    }
299
300    pub fn new_update_commit_log_signer(commit_log_signer: xmtp_cryptography::Secret) -> Self {
301        Self::new(
302            MetadataField::CommitLogSigner.to_string(),
303            hex::encode(commit_log_signer.as_slice()),
304        )
305    }
306}
307
308/// The returned bytes are the stored intent. The values move into a proto
309/// temporary that is wiped after encoding.
310impl From<UpdateMetadataIntentData> for Vec<u8> {
311    fn from(mut intent: UpdateMetadataIntentData) -> Self {
312        let data = UpdateMetadataData {
313            version: Some(UpdateMetadataVersion::V1(UpdateMetadataV1 {
314                field_name: std::mem::take(&mut intent.field_name),
315                field_value: std::mem::take(&mut intent.field_value),
316                expected_field_value: intent.expected_field_value.take(),
317            })),
318        };
319        // Sized up front, so encoding never frees a partial copy. The proto
320        // wipes its values when it drops.
321        data.encode_to_vec()
322    }
323}
324
325impl TryFrom<Vec<u8>> for UpdateMetadataIntentData {
326    type Error = IntentError;
327
328    /// Decodes a stored intent. The input is wiped, and the values move out of
329    /// the decoded proto.
330    fn try_from(data: Vec<u8>) -> Result<Self, Self::Error> {
331        let data = Zeroizing::new(data);
332        let msg = UpdateMetadataData::decode(data.as_slice())?;
333        let Some(UpdateMetadataVersion::V1(mut v1)) = msg.version else {
334            return Err(IntentError::MissingPayload);
335        };
336        // Absent on intents queued before the guard existed, and on payloads
337        // written by a client that predates it — both mean last-writer-wins.
338        Ok(Self {
339            field_name: std::mem::take(&mut v1.field_name),
340            field_value: std::mem::take(&mut v1.field_value),
341            expected_field_value: v1.expected_field_value.take(),
342        })
343    }
344}
345
346#[derive(Debug, Default, Clone)]
347pub struct UpdateGroupMembershipResult {
348    pub added_members: HashMap<String, u64>,
349    pub removed_members: Vec<String>,
350    pub failed_installations: Vec<Vec<u8>>,
351}
352
353impl UpdateGroupMembershipResult {
354    pub fn new(
355        added_members: HashMap<String, u64>,
356        removed_members: Vec<String>,
357        failed_installations: Vec<Vec<u8>>,
358    ) -> Self {
359        Self {
360            added_members,
361            removed_members,
362            failed_installations,
363        }
364    }
365}
366
367impl From<UpdateGroupMembershipIntentData> for UpdateGroupMembershipResult {
368    fn from(value: UpdateGroupMembershipIntentData) -> Self {
369        UpdateGroupMembershipResult::new(
370            value.membership_updates,
371            value.removed_members,
372            value.failed_installations,
373        )
374    }
375}
376
377#[derive(Debug, Clone)]
378pub(crate) struct UpdateGroupMembershipIntentData {
379    pub membership_updates: HashMap<String, u64>,
380    pub removed_members: Vec<String>,
381    pub failed_installations: Vec<Vec<u8>>,
382    /// Inboxes in `membership_updates` that were members when the intent was
383    /// built and that the app did not ask to add. Empty on intents that
384    /// earlier builds stored: each of their entries can insert its inbox.
385    pub refreshed_members: Vec<String>,
386    /// Add no leaf; record each installation the raise would add as failed.
387    pub removal_only: bool,
388    /// Removals made for leave requests. Empty on older intents.
389    pub leave_removals: Vec<String>,
390}
391
392impl UpdateGroupMembershipIntentData {
393    pub fn new(
394        membership_updates: HashMap<String, u64>,
395        removed_members: Vec<String>,
396        failed_installations: Vec<Vec<u8>>,
397    ) -> Self {
398        Self {
399            membership_updates,
400            removed_members,
401            failed_installations,
402            refreshed_members: Vec::new(),
403            removal_only: false,
404            leave_removals: Vec::new(),
405        }
406    }
407
408    /// Mark these inboxes' entries as refreshes of existing members.
409    pub fn with_refreshed_members(mut self, mut refreshed_members: Vec<String>) -> Self {
410        // Encode the same intent to the same bytes.
411        refreshed_members.sort_unstable();
412        refreshed_members.dedup();
413        self.refreshed_members = refreshed_members;
414        self
415    }
416
417    pub fn with_leave_removals(mut self, leave_removals: Vec<String>) -> Self {
418        self.leave_removals = leave_removals;
419        self
420    }
421
422    // implements: GMOD-033
423    /// Drop each leave removal whose request is no longer pending.
424    pub fn settle_leave_removals(&mut self, pending: &HashSet<String>) {
425        let leaves = &self.leave_removals;
426        self.removed_members
427            .retain(|i| !leaves.contains(i) || pending.contains(i));
428        self.leave_removals.retain(|i| pending.contains(i));
429    }
430
431    pub fn is_empty(&self) -> bool {
432        self.membership_updates.is_empty()
433            && self.removed_members.is_empty()
434            && self.failed_installations.is_empty()
435    }
436
437    pub fn apply_to_group_membership(&self, group_membership: &GroupMembership) -> GroupMembership {
438        tracing::info!("old group membership: {:?}", group_membership.members);
439        let mut new_membership = group_membership.clone();
440        for (inbox_id, &sequence_id) in self.membership_updates.iter() {
441            let sequence_id = match group_membership.get(inbox_id) {
442                // A queued refresh can be older than a commit that raised this
443                // entry. Keep the higher sequence id; never lower it.
444                Some(&current) => current.max(sequence_id),
445                // implements: GMOD-051
446                // A refresh raises an existing entry. A commit may have removed
447                // the inbox after the refresh was built; do not insert it again.
448                None if self.refreshed_members.contains(inbox_id) => continue,
449                None => sequence_id,
450            };
451            new_membership.add(inbox_id.clone(), sequence_id);
452        }
453
454        for inbox_id in self.removed_members.iter() {
455            new_membership.remove(inbox_id)
456        }
457
458        new_membership.failed_installations = new_membership
459            .failed_installations
460            .into_iter()
461            .chain(self.failed_installations.iter().cloned())
462            .collect::<HashSet<_>>()
463            .into_iter()
464            .collect();
465
466        tracing::info!("updated group membership: {:?}", new_membership.members);
467        new_membership
468    }
469}
470
471impl From<UpdateGroupMembershipIntentData> for Vec<u8> {
472    fn from(intent: UpdateGroupMembershipIntentData) -> Self {
473        let mut buf = Vec::new();
474
475        UpdateGroupMembershipData {
476            version: Some(UpdateGroupMembershipVersion::V1(UpdateGroupMembershipV1 {
477                membership_updates: intent.membership_updates,
478                removed_members: intent.removed_members,
479                failed_installations: intent.failed_installations,
480                refreshed_members: intent.refreshed_members,
481                removal_only: intent.removal_only,
482                leave_removals: intent.leave_removals,
483            })),
484        }
485        .encode(&mut buf)
486        .expect("encode error");
487
488        buf
489    }
490}
491
492impl TryFrom<Vec<u8>> for UpdateGroupMembershipIntentData {
493    type Error = IntentError;
494
495    fn try_from(data: Vec<u8>) -> Result<Self, Self::Error> {
496        if let UpdateGroupMembershipData {
497            version: Some(UpdateGroupMembershipVersion::V1(v1)),
498        } = UpdateGroupMembershipData::decode(data.as_slice())?
499        {
500            Ok(Self {
501                membership_updates: v1.membership_updates,
502                removed_members: v1.removed_members,
503                failed_installations: v1.failed_installations,
504                refreshed_members: v1.refreshed_members,
505                removal_only: v1.removal_only,
506                leave_removals: v1.leave_removals,
507            })
508        } else {
509            Err(IntentError::MissingPayload)
510        }
511    }
512}
513
514impl<'a> TryFrom<&'a [u8]> for UpdateGroupMembershipIntentData {
515    type Error = IntentError;
516
517    fn try_from(data: &'a [u8]) -> Result<Self, Self::Error> {
518        if let UpdateGroupMembershipData {
519            version: Some(UpdateGroupMembershipVersion::V1(v1)),
520        } = UpdateGroupMembershipData::decode(data)?
521        {
522            Ok(Self {
523                membership_updates: v1.membership_updates,
524                removed_members: v1.removed_members,
525                failed_installations: v1.failed_installations,
526                refreshed_members: v1.refreshed_members,
527                removal_only: v1.removal_only,
528                leave_removals: v1.leave_removals,
529            })
530        } else {
531            Err(IntentError::MissingPayload)
532        }
533    }
534}
535#[repr(i32)]
536#[derive(Debug, Clone, PartialEq)]
537pub enum AdminListActionType {
538    Add = 1,         // Matches ADD_ADMIN in Protobuf
539    Remove = 2,      // Matches REMOVE_ADMIN in Protobuf
540    AddSuper = 3,    // Matches ADD_SUPER_ADMIN in Protobuf
541    RemoveSuper = 4, // Matches REMOVE_SUPER_ADMIN in Protobuf
542}
543
544impl TryFrom<i32> for AdminListActionType {
545    type Error = IntentError;
546
547    fn try_from(value: i32) -> Result<Self, Self::Error> {
548        match value {
549            1 => Ok(AdminListActionType::Add),
550            2 => Ok(AdminListActionType::Remove),
551            3 => Ok(AdminListActionType::AddSuper),
552            4 => Ok(AdminListActionType::RemoveSuper),
553            _ => Err(IntentError::UnknownAdminListAction),
554        }
555    }
556}
557
558#[derive(Debug, Clone)]
559pub struct UpdateAdminListIntentData {
560    pub action_type: AdminListActionType,
561    pub inbox_id: String,
562}
563
564impl UpdateAdminListIntentData {
565    pub fn new(action_type: AdminListActionType, inbox_id: String) -> Self {
566        Self {
567            action_type,
568            inbox_id,
569        }
570    }
571}
572
573impl From<UpdateAdminListIntentData> for Vec<u8> {
574    fn from(intent: UpdateAdminListIntentData) -> Self {
575        let mut buf = Vec::new();
576        let action_type = intent.action_type as i32;
577
578        UpdateAdminListsData {
579            version: Some(UpdateAdminListsVersion::V1(UpdateAdminListsV1 {
580                admin_list_update_type: action_type,
581                inbox_id: intent.inbox_id,
582            })),
583        }
584        .encode(&mut buf)
585        .expect("encode error");
586
587        buf
588    }
589}
590
591impl TryFrom<Vec<u8>> for UpdateAdminListIntentData {
592    type Error = IntentError;
593
594    fn try_from(data: Vec<u8>) -> Result<Self, Self::Error> {
595        let msg = UpdateAdminListsData::decode(Bytes::from(data))?;
596
597        let action_type: AdminListActionType = match msg.version {
598            Some(UpdateAdminListsVersion::V1(ref v1)) => {
599                AdminListActionType::try_from(v1.admin_list_update_type)?
600            }
601            None => return Err(IntentError::MissingUpdateAdminVersion),
602        };
603        let inbox_id = match msg.version {
604            Some(UpdateAdminListsVersion::V1(ref v1)) => v1.inbox_id.clone(),
605            None => return Err(IntentError::MissingUpdateAdminVersion),
606        };
607
608        Ok(Self::new(action_type, inbox_id))
609    }
610}
611
612#[repr(i32)]
613#[derive(Debug, Clone, PartialEq)]
614pub enum PermissionUpdateType {
615    AddMember = 1,      // Matches ADD_MEMBER in Protobuf
616    RemoveMember = 2,   // Matches REMOVE_MEMBER in Protobuf
617    AddAdmin = 3,       // Matches ADD_ADMIN in Protobuf
618    RemoveAdmin = 4,    // Matches REMOVE_ADMIN in Protobuf
619    UpdateMetadata = 5, // Matches UPDATE_METADATA in Protobuf
620}
621
622impl TryFrom<i32> for PermissionUpdateType {
623    type Error = IntentError;
624
625    fn try_from(value: i32) -> Result<Self, Self::Error> {
626        match value {
627            1 => Ok(PermissionUpdateType::AddMember),
628            2 => Ok(PermissionUpdateType::RemoveMember),
629            3 => Ok(PermissionUpdateType::AddAdmin),
630            4 => Ok(PermissionUpdateType::RemoveAdmin),
631            5 => Ok(PermissionUpdateType::UpdateMetadata),
632            _ => Err(IntentError::UnknownPermissionUpdateType),
633        }
634    }
635}
636
637#[repr(i32)]
638#[derive(Debug, Clone, PartialEq)]
639pub enum PermissionPolicyOption {
640    Allow = 1,          // Matches ADD_MEMBER in Protobuf
641    Deny = 2,           // Matches REMOVE_MEMBER in Protobuf
642    AdminOnly = 3,      // Matches ADD_ADMIN in Protobuf
643    SuperAdminOnly = 4, // Matches REMOVE_ADMIN in Protobuf
644}
645
646impl TryFrom<i32> for PermissionPolicyOption {
647    type Error = IntentError;
648
649    fn try_from(value: i32) -> Result<Self, Self::Error> {
650        match value {
651            1 => Ok(PermissionPolicyOption::Allow),
652            2 => Ok(PermissionPolicyOption::Deny),
653            3 => Ok(PermissionPolicyOption::AdminOnly),
654            4 => Ok(PermissionPolicyOption::SuperAdminOnly),
655            _ => Err(IntentError::UnknownPermissionPolicyOption),
656        }
657    }
658}
659
660impl From<PermissionPolicyOption> for MembershipPolicies {
661    fn from(value: PermissionPolicyOption) -> Self {
662        match value {
663            PermissionPolicyOption::Allow => MembershipPolicies::allow(),
664            PermissionPolicyOption::Deny => MembershipPolicies::deny(),
665            PermissionPolicyOption::AdminOnly => MembershipPolicies::allow_if_actor_admin(),
666            PermissionPolicyOption::SuperAdminOnly => {
667                MembershipPolicies::allow_if_actor_super_admin()
668            }
669        }
670    }
671}
672
673impl From<PermissionPolicyOption> for MetadataPolicies {
674    fn from(value: PermissionPolicyOption) -> Self {
675        match value {
676            PermissionPolicyOption::Allow => MetadataPolicies::allow(),
677            PermissionPolicyOption::Deny => MetadataPolicies::deny(),
678            PermissionPolicyOption::AdminOnly => MetadataPolicies::allow_if_actor_admin(),
679            PermissionPolicyOption::SuperAdminOnly => {
680                MetadataPolicies::allow_if_actor_super_admin()
681            }
682        }
683    }
684}
685
686impl From<PermissionPolicyOption> for PermissionsPolicies {
687    fn from(value: PermissionPolicyOption) -> Self {
688        match value {
689            PermissionPolicyOption::Allow => {
690                tracing::error!(
691                    "PermissionPolicyOption::Allow is not allowed for PermissionsPolicies, set to super_admin only instead"
692                );
693                PermissionsPolicies::allow_if_actor_super_admin()
694            }
695            PermissionPolicyOption::Deny => PermissionsPolicies::deny(),
696            PermissionPolicyOption::AdminOnly => PermissionsPolicies::allow_if_actor_admin(),
697            PermissionPolicyOption::SuperAdminOnly => {
698                PermissionsPolicies::allow_if_actor_super_admin()
699            }
700        }
701    }
702}
703
704#[derive(Debug, Clone)]
705pub struct UpdatePermissionIntentData {
706    pub update_type: PermissionUpdateType,
707    pub policy_option: PermissionPolicyOption,
708    pub metadata_field_name: Option<String>,
709}
710
711impl UpdatePermissionIntentData {
712    pub fn new(
713        update_type: PermissionUpdateType,
714        policy_option: PermissionPolicyOption,
715        metadata_field_name: Option<String>,
716    ) -> Self {
717        Self {
718            update_type,
719            policy_option,
720            metadata_field_name,
721        }
722    }
723}
724
725impl From<UpdatePermissionIntentData> for Vec<u8> {
726    fn from(intent: UpdatePermissionIntentData) -> Self {
727        let mut buf = Vec::new();
728        let update_type = intent.update_type as i32;
729        let policy_option = intent.policy_option as i32;
730
731        UpdatePermissionData {
732            version: Some(UpdatePermissionVersion::V1(UpdatePermissionV1 {
733                permission_update_type: update_type,
734                permission_policy_option: policy_option,
735                metadata_field_name: intent.metadata_field_name,
736            })),
737        }
738        .encode(&mut buf)
739        .expect("encode error");
740
741        buf
742    }
743}
744
745impl TryFrom<Vec<u8>> for UpdatePermissionIntentData {
746    type Error = IntentError;
747
748    fn try_from(data: Vec<u8>) -> Result<Self, Self::Error> {
749        let msg = UpdatePermissionData::decode(Bytes::from(data))?;
750        let Some(UpdatePermissionVersion::V1(update_permission_data::V1 {
751            permission_update_type,
752            permission_policy_option,
753            metadata_field_name,
754        })) = msg.version
755        else {
756            return Err(IntentError::UnsupportedPermissionVersion);
757        };
758        let update_type: PermissionUpdateType = permission_update_type.try_into()?;
759        let policy_option: PermissionPolicyOption = permission_policy_option.try_into()?;
760        Ok(Self::new(update_type, policy_option, metadata_field_name))
761    }
762}
763
764pub(crate) struct ReaddInstallationsIntentData {
765    pub readded_installations: Vec<Vec<u8>>,
766}
767
768impl ReaddInstallationsIntentData {
769    pub fn new(readded_installations: Vec<Vec<u8>>) -> Self {
770        Self {
771            readded_installations,
772        }
773    }
774}
775
776impl From<ReaddInstallationsIntentData> for Vec<u8> {
777    fn from(intent: ReaddInstallationsIntentData) -> Self {
778        let mut buf = Vec::new();
779        ReaddInstallationsData {
780            version: Some(ReaddInstallationsVersion::V1(ReaddInstallationsV1 {
781                readded_installations: intent.readded_installations,
782            })),
783        }
784        .encode(&mut buf)
785        .expect("encode error");
786
787        buf
788    }
789}
790
791impl TryFrom<Vec<u8>> for ReaddInstallationsIntentData {
792    type Error = IntentError;
793
794    fn try_from(data: Vec<u8>) -> Result<Self, Self::Error> {
795        if let ReaddInstallationsData {
796            version: Some(ReaddInstallationsVersion::V1(v1)),
797        } = ReaddInstallationsData::decode(data.as_slice())?
798        {
799            Ok(Self::new(v1.readded_installations))
800        } else {
801            Err(IntentError::MissingPayload)
802        }
803    }
804}
805
806impl TryFrom<&[u8]> for ReaddInstallationsIntentData {
807    type Error = IntentError;
808
809    fn try_from(data: &[u8]) -> Result<Self, Self::Error> {
810        if let ReaddInstallationsData {
811            version: Some(ReaddInstallationsVersion::V1(v1)),
812        } = ReaddInstallationsData::decode(data)?
813        {
814            Ok(Self::new(v1.readded_installations))
815        } else {
816            Err(IntentError::MissingPayload)
817        }
818    }
819}
820
821/// Intent data for proposing member updates (adds and/or removes) to a group
822#[derive(Debug, Clone)]
823pub(crate) struct ProposeMemberUpdateIntentData {
824    pub add_inbox_ids: Vec<String>,
825    pub remove_inbox_ids: Vec<String>,
826}
827
828impl ProposeMemberUpdateIntentData {
829    pub fn new(add_inbox_ids: Vec<String>, remove_inbox_ids: Vec<String>) -> Self {
830        Self {
831            add_inbox_ids,
832            remove_inbox_ids,
833        }
834    }
835}
836
837impl TryFrom<ProposeMemberUpdateIntentData> for Vec<u8> {
838    type Error = IntentError;
839    fn try_from(intent: ProposeMemberUpdateIntentData) -> Result<Self, Self::Error> {
840        let decode_inbox_ids =
841            |inbox_ids: Vec<String>, item: &'static str| -> Result<Vec<Vec<u8>>, IntentError> {
842                inbox_ids
843                    .into_iter()
844                    .map(|s| {
845                        hex::decode(&s)
846                            .map_err(|_| xmtp_proto::ConversionError::InvalidValue {
847                                item,
848                                expected: "hex encoded string",
849                                got: s,
850                            })
851                            .map_err(Into::into)
852                    })
853                    .collect::<Result<Vec<_>, _>>()
854            };
855        let proposal = ProposeMemberUpdateData {
856            version: Some(propose_member_update_data::Version::V1(
857                propose_member_update_data::V1 {
858                    add_inbox_ids: decode_inbox_ids(intent.add_inbox_ids, "add_inbox_ids")?,
859                    remove_inbox_ids: decode_inbox_ids(
860                        intent.remove_inbox_ids,
861                        "remove_inbox_ids",
862                    )?,
863                },
864            )),
865        }
866        .encode_to_vec();
867        Ok(proposal)
868    }
869}
870
871impl TryFrom<&[u8]> for ProposeMemberUpdateIntentData {
872    type Error = IntentError;
873
874    fn try_from(data: &[u8]) -> Result<Self, Self::Error> {
875        let proto = ProposeMemberUpdateData::decode(data)?;
876        let decode_inbox_ids = |inbox_ids: Vec<Vec<u8>>| -> Vec<String> {
877            inbox_ids
878                .into_iter()
879                .map(|b| hex::encode(&b))
880                .collect::<Vec<_>>()
881        };
882        match proto.version {
883            Some(propose_member_update_data::Version::V1(v1)) => {
884                let add_inbox_ids = decode_inbox_ids(v1.add_inbox_ids);
885                let remove_inbox_ids = decode_inbox_ids(v1.remove_inbox_ids);
886                Ok(Self::new(add_inbox_ids, remove_inbox_ids))
887            }
888            None => Err(IntentError::MissingPayload),
889        }
890    }
891}
892
893/// Intent data for proposing group context extension updates (proposal-by-reference flow)
894#[derive(Debug, Clone)]
895pub(crate) struct ProposeGroupContextExtensionsIntentData {
896    /// The serialized extensions bytes
897    pub extensions_bytes: Vec<u8>,
898}
899
900impl ProposeGroupContextExtensionsIntentData {
901    pub fn new(extensions_bytes: Vec<u8>) -> Self {
902        Self { extensions_bytes }
903    }
904}
905
906impl From<ProposeGroupContextExtensionsIntentData> for Vec<u8> {
907    fn from(intent: ProposeGroupContextExtensionsIntentData) -> Self {
908        ProposeGroupContextExtensionData {
909            version: Some(propose_group_context_extension_data::Version::V1(
910                propose_group_context_extension_data::V1 {
911                    group_context_extension: intent.extensions_bytes,
912                },
913            )),
914        }
915        .encode_to_vec()
916    }
917}
918
919impl TryFrom<&[u8]> for ProposeGroupContextExtensionsIntentData {
920    type Error = IntentError;
921
922    fn try_from(data: &[u8]) -> Result<Self, Self::Error> {
923        let proto = ProposeGroupContextExtensionData::decode(data)?;
924        match proto.version {
925            Some(propose_group_context_extension_data::Version::V1(v1)) => {
926                Ok(Self::new(v1.group_context_extension))
927            }
928            None => Err(IntentError::MissingPayload),
929        }
930    }
931}
932
933/// Intent data for committing pending proposals (proposal-by-reference flow)
934#[derive(Debug, Clone, Default)]
935pub(crate) struct CommitPendingProposalsIntentData {
936    // Empty for now - commits all pending proposals in the proposal store
937}
938
939impl CommitPendingProposalsIntentData {
940    pub fn new() -> Self {
941        Self {}
942    }
943}
944
945impl From<CommitPendingProposalsIntentData> for Vec<u8> {
946    fn from(_intent: CommitPendingProposalsIntentData) -> Self {
947        CommitPendingProposalsData {
948            version: Some(commit_pending_proposals_data::Version::V1(
949                commit_pending_proposals_data::V1 {
950                    proposal_hashes: vec![],
951                },
952            )),
953        }
954        .encode_to_vec()
955    }
956}
957
958impl TryFrom<&[u8]> for CommitPendingProposalsIntentData {
959    type Error = IntentError;
960
961    fn try_from(data: &[u8]) -> Result<Self, Self::Error> {
962        // Handle empty data for backwards compatibility and default case
963        if data.is_empty() {
964            return Ok(Self::new());
965        }
966        let proto = CommitPendingProposalsData::decode(data)?;
967        match proto.version {
968            Some(commit_pending_proposals_data::Version::V1(_)) => Ok(Self::new()),
969            None => Err(IntentError::MissingPayload),
970        }
971    }
972}
973
974#[derive(Debug, Clone)]
975pub enum PostCommitAction {
976    SendWelcomes(SendWelcomesAction),
977}
978
979#[derive(Debug)]
980pub struct MembershipDiffWithKeyPackages {
981    pub new_installations: Vec<Installation>,
982    pub new_key_packages: Vec<KeyPackage>,
983    pub removed_installations: HashSet<Vec<u8>>,
984    pub failed_installations: Vec<Vec<u8>>,
985}
986
987impl MembershipDiffWithKeyPackages {
988    pub fn new(
989        new_installations: Vec<Installation>,
990        new_key_packages: Vec<KeyPackage>,
991        removed_installations: HashSet<Vec<u8>>,
992        failed_installations: Vec<Vec<u8>>,
993    ) -> MembershipDiffWithKeyPackages {
994        MembershipDiffWithKeyPackages {
995            new_installations,
996            new_key_packages,
997            removed_installations,
998            failed_installations,
999        }
1000    }
1001}
1002
1003#[derive(Debug, Clone)]
1004pub struct Installation {
1005    pub(crate) installation_key: Vec<u8>,
1006    pub(crate) hpke_public_key: Vec<u8>,
1007    pub(crate) welcome_wrapper_algorithm: WrapperAlgorithm,
1008    pub(crate) welcome_pointee_encryption_aead_types: WelcomePointersExtension,
1009}
1010
1011impl Installation {
1012    /// The addressing fields verification already decoded and checked, so a
1013    /// sender can always wrap a Welcome for the result.
1014    pub fn from_verified_key_package(key_package: &VerifiedKeyPackageV2) -> Self {
1015        Self {
1016            installation_key: key_package.installation_id(),
1017            hpke_public_key: key_package.wrapper.pub_key_bytes.clone(),
1018            welcome_wrapper_algorithm: key_package.wrapper.algorithm,
1019            welcome_pointee_encryption_aead_types: key_package.pointee_aead.clone(),
1020        }
1021    }
1022
1023    /// The installation can open a welcome pointer: it advertises a
1024    /// ChaCha20-Poly1305 pointee, and its wrapper is one the pointer wire format
1025    /// can name. Pointers are always wrapped post-quantum.
1026    // implements: JOIN-091
1027    pub(crate) fn pointer_capable(&self) -> bool {
1028        self.welcome_pointee_encryption_aead_types.compatible()
1029            && WelcomePointerWrapperAlgorithm::try_from(self.welcome_wrapper_algorithm).is_ok()
1030    }
1031}
1032
1033impl From<Installation> for InstallationProto {
1034    fn from(installation: Installation) -> Self {
1035        Self {
1036            installation_key: installation.installation_key,
1037            hpke_public_key: installation.hpke_public_key,
1038            welcome_wrapper_algorithm: installation.welcome_wrapper_algorithm.into(),
1039            welcome_pointee_encryption_aead_types: Some(
1040                installation.welcome_pointee_encryption_aead_types.into(),
1041            ),
1042        }
1043    }
1044}
1045
1046impl TryFrom<InstallationProto> for Installation {
1047    type Error = ConversionError;
1048    fn try_from(installation: InstallationProto) -> Result<Self, Self::Error> {
1049        Ok(Self {
1050            installation_key: installation.installation_key,
1051            hpke_public_key: installation.hpke_public_key,
1052            welcome_wrapper_algorithm: installation.welcome_wrapper_algorithm.try_into()?,
1053            welcome_pointee_encryption_aead_types: installation
1054                .welcome_pointee_encryption_aead_types
1055                .map(Into::into)
1056                .unwrap_or_else(WelcomePointersExtension::empty),
1057        })
1058    }
1059}
1060
1061#[derive(Debug, Clone)]
1062pub struct SendWelcomesAction {
1063    pub installations: Vec<Installation>,
1064    pub welcome_message: Vec<u8>,
1065}
1066
1067impl SendWelcomesAction {
1068    pub fn new(installations: Vec<Installation>, welcome_message: Vec<u8>) -> Self {
1069        Self {
1070            installations,
1071            welcome_message,
1072        }
1073    }
1074
1075    pub(crate) fn to_bytes(&self) -> Vec<u8> {
1076        PostCommitActionProto {
1077            kind: Some(PostCommitActionKind::SendWelcomes(SendWelcomesProto {
1078                installations: self
1079                    .installations
1080                    .clone()
1081                    .into_iter()
1082                    .map(|i| i.into())
1083                    .collect(),
1084                welcome_message: self.welcome_message.clone(),
1085            })),
1086        }
1087        .encode_to_vec()
1088    }
1089}
1090
1091impl PostCommitAction {
1092    pub(crate) fn to_bytes(&self) -> Vec<u8> {
1093        match self {
1094            PostCommitAction::SendWelcomes(action) => action.to_bytes(),
1095        }
1096    }
1097
1098    pub(crate) fn from_bytes(data: &[u8]) -> Result<Self, IntentError> {
1099        let decoded = PostCommitActionProto::decode(data)?
1100            .kind
1101            .ok_or(IntentError::MissingPostCommit)?;
1102        match decoded {
1103            PostCommitActionKind::SendWelcomes(proto) => {
1104                Ok(Self::SendWelcomes(SendWelcomesAction::new(
1105                    proto
1106                        .installations
1107                        .into_iter()
1108                        .map(|i| i.try_into())
1109                        .collect::<Result<_, _>>()?,
1110                    proto.welcome_message,
1111                )))
1112            }
1113        }
1114    }
1115
1116    pub(crate) fn from_welcome(
1117        welcome: MlsMessageOut,
1118        installations: Vec<Installation>,
1119    ) -> Result<Self, IntentError> {
1120        let welcome_bytes = welcome.tls_serialize_detached()?;
1121
1122        Ok(Self::SendWelcomes(SendWelcomesAction::new(
1123            installations,
1124            welcome_bytes,
1125        )))
1126    }
1127}
1128
1129impl TryFrom<Vec<u8>> for PostCommitAction {
1130    type Error = IntentError;
1131
1132    fn try_from(data: Vec<u8>) -> Result<Self, Self::Error> {
1133        PostCommitAction::from_bytes(data.as_slice())
1134    }
1135}
1136
1137/// Payload of [`crate::groups::intents::queue::QueueIntent::app_data_update`].
1138///
1139/// Serialized as the local-only `xmtp.mls.database.AppDataUpdateData`
1140/// proto (`proto/mls/database/intents.proto`); it never crosses the MLS
1141/// wire. Component IDs are `u32` in the proto and narrowed back here;
1142/// a value above `u16::MAX` or an unknown version variant fails closed.
1143#[derive(Debug, Clone, PartialEq, Eq)]
1144pub enum AppDataUpdateIntentData {
1145    /// One verbatim `AppDataUpdate` payload (V1). Its meaning follows the
1146    /// target component's registered `ComponentType`: a scalar's new
1147    /// value, or a TLS-encoded map or set delta.
1148    Payload { component_id: u16, payload: Vec<u8> },
1149    /// Typed metadata field writes, committed together (V2). The
1150    /// publisher resolves them with
1151    /// [`xmtp_mls_group_state::app_data::fields::FieldSnapshot::resolve_writes`]
1152    /// against the committed registry.
1153    Fields(Vec<FieldWrite>),
1154}
1155
1156impl AppDataUpdateIntentData {
1157    /// Write `payload` to `component_id`.
1158    pub fn new(component_id: u16, payload: Vec<u8>) -> Self {
1159        Self::Payload {
1160            component_id,
1161            payload,
1162        }
1163    }
1164}
1165
1166impl From<AppDataUpdateIntentData> for Vec<u8> {
1167    fn from(intent: AppDataUpdateIntentData) -> Self {
1168        use xmtp_proto::xmtp::mls::database::{
1169            AppDataUpdateData,
1170            app_data_update_data::{
1171                FieldWrite as FieldWriteProto, V1, V2, Version, field_write::Operation,
1172            },
1173        };
1174        let version = match intent {
1175            AppDataUpdateIntentData::Payload {
1176                component_id,
1177                payload,
1178            } => Version::V1(V1 {
1179                component_id: component_id.into(),
1180                payload,
1181            }),
1182            AppDataUpdateIntentData::Fields(writes) => Version::V2(V2 {
1183                writes: writes
1184                    .into_iter()
1185                    .map(|write| FieldWriteProto {
1186                        component_id: write.component_id.as_u16().into(),
1187                        component_type: write.component_type.into(),
1188                        operation: Some(match write.operation {
1189                            WriteOperation::Update(payload) => Operation::Update(payload),
1190                            WriteOperation::Remove => Operation::Remove(true),
1191                            WriteOperation::SetOwn(value) => Operation::SetOwn(value),
1192                            WriteOperation::ClearOwn => Operation::ClearOwn(true),
1193                        }),
1194                    })
1195                    .collect(),
1196            }),
1197        };
1198        AppDataUpdateData {
1199            version: Some(version),
1200        }
1201        .encode_to_vec()
1202    }
1203}
1204
1205impl TryFrom<Vec<u8>> for AppDataUpdateIntentData {
1206    type Error = IntentError;
1207
1208    fn try_from(data: Vec<u8>) -> Result<Self, Self::Error> {
1209        Self::try_from(data.as_slice())
1210    }
1211}
1212
1213impl TryFrom<&[u8]> for AppDataUpdateIntentData {
1214    type Error = IntentError;
1215
1216    fn try_from(data: &[u8]) -> Result<Self, Self::Error> {
1217        use xmtp_proto::xmtp::mls::database::{
1218            AppDataUpdateData,
1219            app_data_update_data::{Version, field_write::Operation},
1220        };
1221        let invalid =
1222            |reason: String| IntentError::Generic(format!("AppDataUpdateIntentData {reason}"));
1223        let component_id = |id: u32| {
1224            u16::try_from(id).map_err(|_| invalid(format!("component_id {id} exceeds u16 range")))
1225        };
1226        match AppDataUpdateData::decode(data)?.version {
1227            Some(Version::V1(v1)) => Ok(Self::Payload {
1228                component_id: component_id(v1.component_id)?,
1229                payload: v1.payload,
1230            }),
1231            Some(Version::V2(v2)) => v2
1232                .writes
1233                .into_iter()
1234                .map(|write| {
1235                    Ok(FieldWrite {
1236                        component_id: ComponentId::new(component_id(write.component_id)?),
1237                        component_type: ComponentType::try_from(write.component_type).map_err(
1238                            |_| {
1239                                invalid(format!(
1240                                    "component_type {} is unknown",
1241                                    write.component_type
1242                                ))
1243                            },
1244                        )?,
1245                        operation: match write.operation {
1246                            Some(Operation::Update(payload)) => WriteOperation::Update(payload),
1247                            Some(Operation::Remove(_)) => WriteOperation::Remove,
1248                            Some(Operation::SetOwn(value)) => WriteOperation::SetOwn(value),
1249                            Some(Operation::ClearOwn(_)) => WriteOperation::ClearOwn,
1250                            None => return Err(invalid("field write has no operation".into())),
1251                        },
1252                    })
1253                })
1254                .collect::<Result<_, _>>()
1255                .map(Self::Fields),
1256            None => Err(invalid("missing version oneof variant".into())),
1257        }
1258    }
1259}
1260
1261#[cfg(test)]
1262mod app_data_update_intent_tests {
1263    use super::*;
1264
1265    #[test]
1266    fn round_trip_basic() {
1267        let intent = AppDataUpdateIntentData::new(0x800C, vec![0xAA, 0xBB, 0xCC]);
1268        let bytes: Vec<u8> = intent.clone().into();
1269        let restored = AppDataUpdateIntentData::try_from(bytes).unwrap();
1270        assert_eq!(restored, intent);
1271    }
1272
1273    #[test]
1274    fn empty_payload_round_trips() {
1275        // Empty payload bytes (a "no-op" intent shape) must round-trip
1276        // cleanly — useful for components that legitimately write
1277        // empty values (e.g., revoke clears EXTERNAL_COMMIT_POLICY's
1278        // symmetric_key to zero-length).
1279        let intent = AppDataUpdateIntentData::new(0x800C, vec![]);
1280        let bytes: Vec<u8> = intent.clone().into();
1281        let restored = AppDataUpdateIntentData::try_from(bytes).unwrap();
1282        assert_eq!(restored, intent);
1283    }
1284
1285    #[test]
1286    fn missing_version_variant_surfaces_error() {
1287        // Proto with no `version` oneof set decodes to `version: None`
1288        // on unknown-variant tolerance. Reject explicitly — an intent
1289        // payload without a recognised version variant is malformed.
1290        use prost::Message;
1291        use xmtp_proto::xmtp::mls::database::AppDataUpdateData;
1292        let bytes = AppDataUpdateData { version: None }.encode_to_vec();
1293        let err = AppDataUpdateIntentData::try_from(bytes).unwrap_err();
1294        match err {
1295            IntentError::Generic(msg) => assert!(msg.contains("missing version")),
1296            _ => panic!("expected Generic error, got {err:?}"),
1297        }
1298    }
1299
1300    #[test]
1301    fn component_id_overflow_surfaces_error() {
1302        // u16 component_id is widened to u32 on the wire. A payload that
1303        // somehow carries a value above u16::MAX (e.g., a malformed
1304        // intent or one written by a hypothetical future client widening
1305        // the id space) must be rejected — the dispatcher narrows to
1306        // u16 and we'd silently lose information without the explicit
1307        // check.
1308        use prost::Message;
1309        use xmtp_proto::xmtp::mls::database::{
1310            AppDataUpdateData, app_data_update_data::V1 as AppDataUpdateDataV1,
1311            app_data_update_data::Version as AppDataUpdateVersion,
1312        };
1313        let bytes = AppDataUpdateData {
1314            version: Some(AppDataUpdateVersion::V1(AppDataUpdateDataV1 {
1315                component_id: u16::MAX as u32 + 1,
1316                payload: vec![],
1317            })),
1318        }
1319        .encode_to_vec();
1320        let err = AppDataUpdateIntentData::try_from(bytes).unwrap_err();
1321        match err {
1322            IntentError::Generic(msg) => assert!(msg.contains("exceeds u16")),
1323            _ => panic!("expected Generic error, got {err:?}"),
1324        }
1325    }
1326
1327    /// A queued field write must reach the publisher exactly as encoded:
1328    /// its ID, the type it was encoded under (the publisher's stale-type
1329    /// check compares against it), and each operation kind.
1330    #[xmtp_common::test]
1331    fn field_writes_round_trip() {
1332        let write = |id, component_type, operation| FieldWrite {
1333            component_id: ComponentId::new(id),
1334            component_type,
1335            operation,
1336        };
1337        let intent = AppDataUpdateIntentData::Fields(vec![
1338            write(
1339                0x8001,
1340                ComponentType::String,
1341                WriteOperation::Update(b"a".to_vec()),
1342            ),
1343            write(0x8002, ComponentType::Bytes, WriteOperation::Remove),
1344            write(
1345                0x8003,
1346                ComponentType::TlsMapInboxIdString,
1347                WriteOperation::SetOwn(b"b".to_vec()),
1348            ),
1349            write(
1350                0x8004,
1351                ComponentType::TlsMapInboxIdBytes,
1352                WriteOperation::ClearOwn,
1353            ),
1354        ]);
1355        let bytes: Vec<u8> = intent.clone().into();
1356        assert_eq!(AppDataUpdateIntentData::try_from(bytes).unwrap(), intent);
1357    }
1358
1359    /// A field write whose encoding type or operation this build cannot
1360    /// read fails closed rather than publishing a guess.
1361    #[xmtp_common::test]
1362    fn unreadable_field_writes_fail_closed() {
1363        use xmtp_proto::xmtp::mls::database::{
1364            AppDataUpdateData,
1365            app_data_update_data::{
1366                FieldWrite as FieldWriteProto, V2, Version, field_write::Operation,
1367            },
1368        };
1369        let decode = |component_type, operation| {
1370            AppDataUpdateIntentData::try_from(
1371                AppDataUpdateData {
1372                    version: Some(Version::V2(V2 {
1373                        writes: vec![FieldWriteProto {
1374                            component_id: 0x8001,
1375                            component_type,
1376                            operation,
1377                        }],
1378                    })),
1379                }
1380                .encode_to_vec(),
1381            )
1382        };
1383        assert!(decode(ComponentType::Bytes.into(), Some(Operation::Remove(true))).is_ok());
1384        assert!(decode(99, Some(Operation::Remove(true))).is_err());
1385        assert!(decode(ComponentType::Bytes.into(), None).is_err());
1386    }
1387
1388    #[test]
1389    fn malformed_proto_bytes_surface_decode_error() {
1390        // Random bytes that aren't a valid proto must surface a
1391        // prost::DecodeError via the IntentError::Conversion path.
1392        let bytes = vec![0xFF; 16];
1393        assert!(AppDataUpdateIntentData::try_from(bytes).is_err());
1394    }
1395}
1396
1397#[cfg(test)]
1398pub(crate) mod tests {
1399    use crate::context::CoreContext;
1400    use crate::groups::send_message_opts::SendMessageOpts;
1401    use openmls::prelude::{ProcessedMessageContent, ProtocolMessage};
1402    use xmtp_cryptography::utils::generate_local_wallet;
1403    use xmtp_db::XmtpOpenMlsProviderRef;
1404
1405    use crate::{builder::ClientBuilder, utils::TestMlsGroup};
1406
1407    use super::*;
1408
1409    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1410    #[cfg_attr(not(target_arch = "wasm32"), test)]
1411    fn test_serialize_send_message() {
1412        let message = vec![1, 2, 3];
1413        let intent = SendMessageIntentData::new(message.clone());
1414        let as_bytes: Vec<u8> = intent.into();
1415        let restored_intent = SendMessageIntentData::from_bytes(as_bytes.as_slice()).unwrap();
1416
1417        assert_eq!(restored_intent.message.as_slice(), message.as_slice());
1418    }
1419
1420    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1421    #[cfg_attr(not(target_arch = "wasm32"), tokio::test)]
1422    async fn test_serialize_update_membership() {
1423        let mut membership_updates = HashMap::new();
1424        membership_updates.insert("foo".to_string(), 123);
1425
1426        let intent = UpdateGroupMembershipIntentData::new(
1427            membership_updates,
1428            vec!["bar".to_string()],
1429            vec![vec![1, 2, 3]],
1430        );
1431
1432        let as_bytes: Vec<u8> = intent.clone().into();
1433        let restored_intent: UpdateGroupMembershipIntentData = as_bytes.try_into().unwrap();
1434
1435        assert_eq!(
1436            intent.membership_updates,
1437            restored_intent.membership_updates
1438        );
1439
1440        assert_eq!(intent.removed_members, restored_intent.removed_members);
1441
1442        assert_eq!(
1443            intent.failed_installations,
1444            restored_intent.failed_installations
1445        );
1446    }
1447
1448    #[xmtp_common::test(unwrap_try = true)]
1449    async fn membership_refresh_never_lowers_committed_sequence() {
1450        let mut committed = GroupMembership::new();
1451        committed.add("stale".to_string(), 5);
1452        committed.add("raised".to_string(), 3);
1453        committed.add("removed".to_string(), 2);
1454        let intent = UpdateGroupMembershipIntentData::new(
1455            HashMap::from([
1456                ("stale".to_string(), 4),
1457                ("raised".to_string(), 7),
1458                ("added".to_string(), 1),
1459                ("removed".to_string(), 9),
1460            ]),
1461            vec!["removed".to_string()],
1462            vec![],
1463        );
1464        let updated = intent.apply_to_group_membership(&committed);
1465        assert_eq!(updated.get("stale"), Some(&5));
1466        assert_eq!(updated.get("raised"), Some(&7));
1467        assert_eq!(updated.get("added"), Some(&1));
1468        assert_eq!(updated.get("removed"), None);
1469        assert_eq!(updated.members.len(), 3);
1470    }
1471
1472    #[xmtp_common::test]
1473    async fn test_serialize_update_metadata() {
1474        let intent = UpdateMetadataIntentData::new_update_group_name("group name".to_string());
1475        let as_bytes: Vec<u8> = intent.clone().into();
1476        let restored_intent: UpdateMetadataIntentData =
1477            UpdateMetadataIntentData::try_from(as_bytes).unwrap();
1478
1479        assert_eq!(intent.field_value, restored_intent.field_value);
1480    }
1481
1482    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1483    #[cfg_attr(not(target_arch = "wasm32"), tokio::test)]
1484    async fn test_serialize_readd_installations() {
1485        let readded_installations = vec![vec![1, 2, 3], vec![4, 5, 6], vec![7, 8, 9]];
1486
1487        let intent = ReaddInstallationsIntentData::new(readded_installations.clone());
1488
1489        let as_bytes: Vec<u8> = intent.into();
1490        let restored_intent: ReaddInstallationsIntentData = as_bytes.try_into().unwrap();
1491
1492        assert_eq!(readded_installations, restored_intent.readded_installations);
1493    }
1494
1495    #[xmtp_common::test]
1496    async fn test_key_rotation_before_first_message() {
1497        let client_a = ClientBuilder::new_test_client_vanilla(&generate_local_wallet()).await;
1498        let client_b = ClientBuilder::new_test_client_vanilla(&generate_local_wallet()).await;
1499
1500        // client A makes a group with client B, and then sends a message to client B.
1501        let group_a = client_a.create_group(None, None).expect("create group");
1502        group_a.add_members(&[client_b.inbox_id()]).await.unwrap();
1503        group_a
1504            .send_message(b"First message from A", SendMessageOpts::default())
1505            .await
1506            .unwrap();
1507
1508        // No key rotation is needed because A's commit to add B rotates the key.
1509        // The Add proposal, membership AppDataUpdate proposal, commit, and message are published.
1510        verify_num_payloads_in_group(&group_a, 4).await;
1511
1512        // Client B sends a message to Client A
1513        let groups_b = client_b.sync_welcomes().await.unwrap();
1514        assert_eq!(groups_b.len(), 1);
1515        let group_b = groups_b[0].clone();
1516        group_b
1517            .send_message(b"First message from B", SendMessageOpts::default())
1518            .await
1519            .expect("send message");
1520
1521        // B must perform a key rotation before sending their first message.
1522        // The initial four envelopes are followed by B's key rotation and message.
1523        let payloads_a = verify_num_payloads_in_group(&group_a, 6).await;
1524        let payloads_b = verify_num_payloads_in_group(&group_b, 6).await;
1525
1526        // Verify key rotation payload
1527        for i in 0..payloads_a.len() {
1528            assert_eq!(payloads_a[i].payload_hash, payloads_b[i].payload_hash);
1529        }
1530        verify_commit_updates_leaf_node(&group_a, &payloads_a[4]);
1531
1532        // Client B sends another message to Client A, and Client A sends another message to Client B.
1533        group_b
1534            .send_message(b"Second message from B", SendMessageOpts::default())
1535            .await
1536            .expect("send message");
1537        group_a
1538            .send_message(b"Second message from A", SendMessageOpts::default())
1539            .await
1540            .expect("send message");
1541
1542        // Group should only have two additional messages. No more key rotations are needed.
1543        verify_num_payloads_in_group(&group_a, 8).await;
1544        verify_num_payloads_in_group(&group_b, 8).await;
1545    }
1546
1547    async fn verify_num_payloads_in_group(
1548        group: &TestMlsGroup,
1549        num_messages: usize,
1550    ) -> Vec<xmtp_proto::types::GroupMessage> {
1551        let messages = group
1552            .context
1553            .api()
1554            .query_group_messages(group.group_id)
1555            .await
1556            .unwrap();
1557        assert_eq!(messages.len(), num_messages);
1558        messages
1559    }
1560
1561    fn verify_commit_updates_leaf_node(
1562        group: &TestMlsGroup,
1563        message: &xmtp_proto::types::GroupMessage,
1564    ) {
1565        let mls_message = message.message.clone();
1566        let mls_message = match mls_message {
1567            ProtocolMessage::PrivateMessage(mls_message) => mls_message,
1568            _ => panic!("error mls_message"),
1569        };
1570
1571        let storage = group.context.mls_storage();
1572        let decrypted_message = group
1573            .load_mls_group_with_lock(storage, |mut mls_group| {
1574                Ok(mls_group
1575                    .process_message(&XmtpOpenMlsProviderRef::new(storage), mls_message.clone())
1576                    .unwrap())
1577            })
1578            .unwrap();
1579
1580        let staged_commit = match decrypted_message.into_content() {
1581            ProcessedMessageContent::StagedCommitMessage(staged_commit) => *staged_commit,
1582            _ => panic!("error staged_commit"),
1583        };
1584
1585        // Check there is indeed some updated leaf node, which means the key update works.
1586        let path_update_leaf_node = staged_commit.update_path_leaf_node();
1587        assert!(path_update_leaf_node.is_some());
1588    }
1589}