@@ -186,6 +186,87 @@ impl WelcomeWrapperAlgorithm {
186186 }
187187 }
188188}
189+ /// ExternalInvitePayload is the shareable invite blob for QR-code or link-based
190+ /// joining of an XMTP group via an MLS external commit. The application embeds
191+ /// the serialized bytes in whatever transport it prefers (hex, base64, raw QR,
192+ /// NFC, etc.) and stores the encrypted GroupInfo blob on an external service
193+ /// keyed by `group_id_hash`.
194+ #[ derive( Clone , PartialEq , Eq , Hash , :: prost:: Message ) ]
195+ pub struct ExternalInvitePayload {
196+ #[ prost( uint32, tag = "1" ) ]
197+ pub version : u32 ,
198+ /// Application-defined opaque bytes identifying the service location.
199+ #[ prost( bytes = "vec" , tag = "2" ) ]
200+ pub service_pointer : :: prost:: alloc:: vec:: Vec < u8 > ,
201+ /// sha256(group_id); service lookup key for the encrypted blob.
202+ #[ prost( bytes = "vec" , tag = "3" ) ]
203+ pub group_id_hash : :: prost:: alloc:: vec:: Vec < u8 > ,
204+ /// 32 bytes; ChaCha20Poly1305 key used to wrap the GroupInfo.
205+ #[ prost( bytes = "vec" , tag = "4" ) ]
206+ pub symmetric_key : :: prost:: alloc:: vec:: Vec < u8 > ,
207+ /// Nanoseconds since UNIX epoch; 0 means no expiry.
208+ #[ prost( uint64, tag = "5" ) ]
209+ pub expires_at_ns : u64 ,
210+ /// MLS ciphersuite identifier of the target group.
211+ #[ prost( uint32, tag = "6" ) ]
212+ pub ciphersuite : u32 ,
213+ }
214+ impl :: prost:: Name for ExternalInvitePayload {
215+ const NAME : & ' static str = "ExternalInvitePayload" ;
216+ const PACKAGE : & ' static str = "xmtp.mls.message_contents" ;
217+ fn full_name ( ) -> :: prost:: alloc:: string:: String {
218+ "xmtp.mls.message_contents.ExternalInvitePayload" . into ( )
219+ }
220+ fn type_url ( ) -> :: prost:: alloc:: string:: String {
221+ "/xmtp.mls.message_contents.ExternalInvitePayload" . into ( )
222+ }
223+ }
224+ /// EncryptedGroupInfoBlob wraps a single GroupInfo TLS-serialized bytes under
225+ /// ChaCha20Poly1305 with a fresh nonce per re-encryption. Stored on the
226+ /// external service and replaced by joiners (with a fresh nonce) after each
227+ /// successful join.
228+ ///
229+ /// `epoch` and `group_state_hash` are plaintext metadata used by the
230+ /// external service for ordering and fork detection. The service should
231+ /// accept an uploaded blob iff its `(epoch, group_state_hash)` is strictly
232+ /// newer than the currently-stored blob's — concurrent uploads at the same
233+ /// epoch with different state hashes signal a fork and should be rejected.
234+ /// The joiner verifies on download that the blob's `epoch` and
235+ /// `group_state_hash` match the decrypted GroupInfo before attempting to
236+ /// join — closes the "malicious service swapped ciphertext" gap.
237+ #[ derive( Clone , PartialEq , Eq , Hash , :: prost:: Message ) ]
238+ pub struct EncryptedGroupInfoBlob {
239+ #[ prost( uint32, tag = "1" ) ]
240+ pub version : u32 ,
241+ /// 12 bytes; ChaCha20Poly1305 nonce specific to this ciphertext.
242+ #[ prost( bytes = "vec" , tag = "2" ) ]
243+ pub nonce : :: prost:: alloc:: vec:: Vec < u8 > ,
244+ /// wrap_payload_symmetric output: AEAD ciphertext over the serialized
245+ /// MlsMessageOut(GroupInfo).
246+ #[ prost( bytes = "vec" , tag = "3" ) ]
247+ pub ciphertext : :: prost:: alloc:: vec:: Vec < u8 > ,
248+ /// MLS group epoch of the wrapped GroupInfo. Plaintext; service uses
249+ /// for monotonic-ordering. Joiner verifies against the decrypted
250+ /// GroupInfo before joining.
251+ #[ prost( uint64, tag = "4" ) ]
252+ pub epoch : u64 ,
253+ /// Tree-hash (or equivalent group-state digest) of the wrapped
254+ /// GroupInfo. Plaintext; service uses alongside `epoch` for fork
255+ /// detection. Joiner verifies against the decrypted GroupInfo before
256+ /// joining.
257+ #[ prost( bytes = "vec" , tag = "5" ) ]
258+ pub group_state_hash : :: prost:: alloc:: vec:: Vec < u8 > ,
259+ }
260+ impl :: prost:: Name for EncryptedGroupInfoBlob {
261+ const NAME : & ' static str = "EncryptedGroupInfoBlob" ;
262+ const PACKAGE : & ' static str = "xmtp.mls.message_contents" ;
263+ fn full_name ( ) -> :: prost:: alloc:: string:: String {
264+ "xmtp.mls.message_contents.EncryptedGroupInfoBlob" . into ( )
265+ }
266+ fn type_url ( ) -> :: prost:: alloc:: string:: String {
267+ "/xmtp.mls.message_contents.EncryptedGroupInfoBlob" . into ( )
268+ }
269+ }
189270/// Message for group mutable metadata
190271#[ derive( Clone , PartialEq , :: prost:: Message ) ]
191272pub struct GroupMutableMetadataV1 {
@@ -520,6 +601,66 @@ impl Compression {
520601 }
521602 }
522603}
604+ /// v1 external-commit-policy payload.
605+ #[ derive( Clone , Copy , PartialEq , Eq , Hash , :: prost:: Message ) ]
606+ pub struct ExternalCommitPolicyV1 {
607+ /// Master switch for MLS External Commits adding new members.
608+ /// Required for the QR-invite flow. Defaults to false; admins
609+ /// (super-admin by default) opt in via
610+ /// AppDataUpdate(EXTERNAL_COMMIT_POLICY).
611+ #[ prost( bool , tag = "1" ) ]
612+ pub allow_external_commit : bool ,
613+ /// Wall-clock auto-disable timestamp (ns since UNIX epoch).
614+ /// 0 = no automatic expiry. After this timestamp the validator
615+ /// rejects all external commits regardless of `allow_external_commit`.
616+ /// Lets admins issue time-bounded invite campaigns without having to
617+ /// come back and flip the bit manually.
618+ #[ prost( uint64, tag = "2" ) ]
619+ pub expires_at_ns : u64 ,
620+ /// Maximum staleness of the GroupInfo referenced by an external
621+ /// commit, in nanoseconds since GroupInfo export. 0 = no staleness
622+ /// limit. External commits whose referenced GroupInfo was exported
623+ /// more than `expire_in_ns` ago are rejected. Narrows the replay
624+ /// window for stolen-blob attacks and forces re-export frequency.
625+ #[ prost( uint64, tag = "3" ) ]
626+ pub expire_in_ns : u64 ,
627+ }
628+ impl :: prost:: Name for ExternalCommitPolicyV1 {
629+ const NAME : & ' static str = "ExternalCommitPolicyV1" ;
630+ const PACKAGE : & ' static str = "xmtp.mls.message_contents" ;
631+ fn full_name ( ) -> :: prost:: alloc:: string:: String {
632+ "xmtp.mls.message_contents.ExternalCommitPolicyV1" . into ( )
633+ }
634+ fn type_url ( ) -> :: prost:: alloc:: string:: String {
635+ "/xmtp.mls.message_contents.ExternalCommitPolicyV1" . into ( )
636+ }
637+ }
638+ /// Versioned envelope. New variants are added as new oneof variants;
639+ /// readers that don't recognize a variant treat the policy as default
640+ /// (all fields zero) per the standard unknown-variant tolerance rules.
641+ #[ derive( Clone , Copy , PartialEq , Eq , Hash , :: prost:: Message ) ]
642+ pub struct ExternalCommitPolicyEntry {
643+ #[ prost( oneof = "external_commit_policy_entry::Version" , tags = "1" ) ]
644+ pub version : :: core:: option:: Option < external_commit_policy_entry:: Version > ,
645+ }
646+ /// Nested message and enum types in `ExternalCommitPolicyEntry`.
647+ pub mod external_commit_policy_entry {
648+ #[ derive( Clone , Copy , PartialEq , Eq , Hash , :: prost:: Oneof ) ]
649+ pub enum Version {
650+ #[ prost( message, tag = "1" ) ]
651+ V1 ( super :: ExternalCommitPolicyV1 ) ,
652+ }
653+ }
654+ impl :: prost:: Name for ExternalCommitPolicyEntry {
655+ const NAME : & ' static str = "ExternalCommitPolicyEntry" ;
656+ const PACKAGE : & ' static str = "xmtp.mls.message_contents" ;
657+ fn full_name ( ) -> :: prost:: alloc:: string:: String {
658+ "xmtp.mls.message_contents.ExternalCommitPolicyEntry" . into ( )
659+ }
660+ fn type_url ( ) -> :: prost:: alloc:: string:: String {
661+ "/xmtp.mls.message_contents.ExternalCommitPolicyEntry" . into ( )
662+ }
663+ }
523664/// Message for group mutable metadata
524665#[ derive( Clone , PartialEq , :: prost:: Message ) ]
525666pub struct GroupMutablePermissionsV1 {
@@ -927,9 +1068,18 @@ pub struct ComponentMetadata {
9271068 /// The data structure type of the component's value
9281069 #[ prost( enumeration = "ComponentType" , tag = "1" ) ]
9291070 pub component_type : i32 ,
930- /// Permission policies for this component
1071+ /// Permission policies for this component, evaluated against regular
1072+ /// (member-issued) commits.
9311073 #[ prost( message, optional, tag = "2" ) ]
9321074 pub permissions : :: core:: option:: Option < ComponentPermissions > ,
1075+ /// Permission policies for this component, evaluated against MLS External
1076+ /// Commits (RFC 9420 §12.4.3.2). Absent / unset is equivalent to all-Deny:
1077+ /// external committers cannot touch this component. Each component opts in
1078+ /// explicitly by setting this field. Combined with the JOIN_POLICY master
1079+ /// switch (`allow_external_commit`), this is the per-component declarative
1080+ /// authorization for external-commit-driven joins.
1081+ #[ prost( message, optional, tag = "3" ) ]
1082+ pub external_committer_permissions : :: core:: option:: Option < ComponentPermissions > ,
9331083}
9341084impl :: prost:: Name for ComponentMetadata {
9351085 const NAME : & ' static str = "ComponentMetadata" ;
0 commit comments