diff --git a/Cargo.lock b/Cargo.lock index 03b9794a11..60be302dc8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -14831,6 +14831,7 @@ dependencies = [ "serde", "thiserror 2.0.18", "tls_codec", + "url", "wasm-bindgen-test", "xmtp-workspace-hack", "xmtp_common", diff --git a/crates/xmtp_mls/src/groups/app_data/bootstrap_validator.rs b/crates/xmtp_mls/src/groups/app_data/bootstrap_validator.rs index 5c13af2545..d8f8390c8b 100644 --- a/crates/xmtp_mls/src/groups/app_data/bootstrap_validator.rs +++ b/crates/xmtp_mls/src/groups/app_data/bootstrap_validator.rs @@ -840,6 +840,7 @@ mod tests { version: Some(GroupMembershipEntryVersion::V1(GroupMembershipEntryV1 { sequence_id: seq, failed_installations: failed, + admitted_via_external_group_id: vec![], })), } } diff --git a/crates/xmtp_mls/src/groups/app_data/component_source.rs b/crates/xmtp_mls/src/groups/app_data/component_source.rs index 43427348e4..f0f76b6876 100644 --- a/crates/xmtp_mls/src/groups/app_data/component_source.rs +++ b/crates/xmtp_mls/src/groups/app_data/component_source.rs @@ -2375,6 +2375,7 @@ mod tests { version: Some(Version::V1(GroupMembershipEntryV1 { sequence_id: 1, failed_installations: vec![], + admitted_via_external_group_id: vec![], })), }, ); @@ -2399,6 +2400,7 @@ mod tests { version: Some(Version::V1(GroupMembershipEntryV1 { sequence_id: 7, failed_installations: vec![vec![0xA1; 16]], + admitted_via_external_group_id: vec![], })), }, ); @@ -2408,6 +2410,7 @@ mod tests { version: Some(Version::V1(GroupMembershipEntryV1 { sequence_id: 42, failed_installations: vec![vec![0xB1; 16]], + admitted_via_external_group_id: vec![], })), }, ); diff --git a/crates/xmtp_mls/src/groups/app_data/migration.rs b/crates/xmtp_mls/src/groups/app_data/migration.rs index deda78d55d..e5c810e8cd 100644 --- a/crates/xmtp_mls/src/groups/app_data/migration.rs +++ b/crates/xmtp_mls/src/groups/app_data/migration.rs @@ -285,6 +285,10 @@ async fn build_partitioned_group_membership( version: Some(GroupMembershipEntryVersion::V1(GroupMembershipEntryV1 { sequence_id: *seq, failed_installations: failed, + // Every pre-migration member was added via Welcome / + // legacy flows, never an external commit, so the + // admitted-via tag is correctly absent for all of them. + admitted_via_external_group_id: vec![], })), }, ); diff --git a/crates/xmtp_mls/src/groups/intents.rs b/crates/xmtp_mls/src/groups/intents.rs index 685a9fc5a8..cd1f5ae2ad 100644 --- a/crates/xmtp_mls/src/groups/intents.rs +++ b/crates/xmtp_mls/src/groups/intents.rs @@ -1070,6 +1070,10 @@ impl From for Vec { version: Some(AppDataUpdateVersion::V1(AppDataUpdateDataV1 { component_id: intent.component_id as u32, payload: intent.payload, + // Multi-component atomic writes (XIP-82 enable atomicity) + // populate this in the external-commit stack; the generic + // single-component intent carries none. + additional_updates: vec![], })), } .encode_to_vec() @@ -1170,6 +1174,7 @@ mod app_data_update_intent_tests { version: Some(AppDataUpdateVersion::V1(AppDataUpdateDataV1 { component_id: u16::MAX as u32 + 1, payload: vec![], + additional_updates: vec![], })), } .encode_to_vec(); diff --git a/crates/xmtp_mls/src/groups/mls_sync/update_group_membership.rs b/crates/xmtp_mls/src/groups/mls_sync/update_group_membership.rs index 604cc278f3..de9066c483 100644 --- a/crates/xmtp_mls/src/groups/mls_sync/update_group_membership.rs +++ b/crates/xmtp_mls/src/groups/mls_sync/update_group_membership.rs @@ -91,12 +91,22 @@ pub(crate) fn build_group_membership_app_data_payload( /// Encode a per-inbox `GroupMembershipEntry::V1` value with the given /// `sequence_id` and an empty `failed_installations` list. +/// +/// `admitted_via_external_group_id` is left absent here. That is only +/// correct while nothing can set it: the field is written exclusively by +/// the external-commit join path (XIP-82), which does not exist yet on +/// this branch. The Update arm of [`build_group_membership_app_data_payload`] +/// rewrites entries from scratch, so once joins can tag entries it MUST +/// carry the existing tag through (the tag is write-once and validators +/// will reject a member commit that clears it); that preservation lands +/// together with the validator enforcement in the external-commit stack. fn encode_membership_entry(sequence_id: u64) -> Result, GroupError> { let entry = GroupMembershipEntry { version: Some(group_membership_entry::Version::V1( group_membership_entry::V1 { sequence_id, failed_installations: vec![], + admitted_via_external_group_id: vec![], }, )), }; diff --git a/crates/xmtp_mls_common/Cargo.toml b/crates/xmtp_mls_common/Cargo.toml index 1d9c6ee27a..c613d70e1c 100644 --- a/crates/xmtp_mls_common/Cargo.toml +++ b/crates/xmtp_mls_common/Cargo.toml @@ -22,6 +22,7 @@ prost.workspace = true serde.workspace = true thiserror.workspace = true tls_codec.workspace = true +url.workspace = true xmtp-workspace-hack.workspace = true xmtp_common.workspace = true xmtp_configuration.workspace = true diff --git a/crates/xmtp_mls_common/src/app_data/migration.rs b/crates/xmtp_mls_common/src/app_data/migration.rs index 21c4a9c172..9e9e803e68 100644 --- a/crates/xmtp_mls_common/src/app_data/migration.rs +++ b/crates/xmtp_mls_common/src/app_data/migration.rs @@ -1169,6 +1169,7 @@ mod tests { GroupMembershipEntryV1 { sequence_id: 42, failed_installations: vec![vec![0xAA; 16]], + admitted_via_external_group_id: vec![], }, ); entries.insert( @@ -1176,6 +1177,7 @@ mod tests { GroupMembershipEntryV1 { sequence_id: 99, failed_installations: vec![], + admitted_via_external_group_id: vec![], }, ); let entries = entries @@ -1257,6 +1259,7 @@ mod tests { version: Some(GroupMembershipEntryVersion::V1(GroupMembershipEntryV1 { sequence_id: 7, failed_installations: vec![], + admitted_via_external_group_id: vec![], })), }; snapshot diff --git a/crates/xmtp_mls_common/src/invite/payload.rs b/crates/xmtp_mls_common/src/invite/payload.rs index 0faf1cc147..11b1799942 100644 --- a/crates/xmtp_mls_common/src/invite/payload.rs +++ b/crates/xmtp_mls_common/src/invite/payload.rs @@ -5,6 +5,8 @@ //! //! * fresh symmetric keys / nonces / external-group-ids from the workspace CSPRNG //! * recognising / unwrapping the `oneof version { V1 v1 }` envelope +//! * validating the typed fields ([`SymmetricKey`] length, [`ServicePointer`] +//! shape and `https` scheme) //! * a [`build_payload`] convenience constructor //! //! The actual encryption of the [`GroupInfo`] blob is performed by the @@ -18,12 +20,15 @@ use thiserror::Error; use xmtp_proto::xmtp::mls::message_contents::{ - ExternalInvitePayload, ExternalInvitePayloadV1, + ExternalInvitePayload, ExternalInvitePayloadV1, ServicePointer, SymmetricKey, external_invite_payload::Version as ExternalInvitePayloadVersion, + service_pointer::Location as ServiceLocation, }; /// Length in bytes of the ChaCha20Poly1305 key used to wrap the encrypted -/// `GroupInfo` blob referenced by an [`ExternalInvitePayload`]. +/// `GroupInfo` blob referenced by an [`ExternalInvitePayload`]. The +/// [`SymmetricKey`] submessage does not constrain length, so validators and +/// setters enforce it here. pub const SYMMETRIC_KEY_LEN: usize = 32; /// Length in bytes of the ChaCha20Poly1305 nonce used alongside @@ -54,16 +59,33 @@ pub enum InvitePayloadError { /// Observed length. len: usize, }, - /// `symmetric_key` was not exactly [`SYMMETRIC_KEY_LEN`] bytes. - #[error("symmetric_key must be exactly {SYMMETRIC_KEY_LEN} bytes (got {0})")] + /// `symmetric_key` was absent. A payload without the key cannot decrypt + /// anything; absence is only a legal encoding on the *policy* component + /// (where it means "no active invite"), never on the payload. + #[error("symmetric_key is required on an external-invite payload")] + MissingSymmetricKey, + /// `symmetric_key.material` was not exactly [`SYMMETRIC_KEY_LEN`] bytes. + #[error("symmetric_key.material must be exactly {SYMMETRIC_KEY_LEN} bytes (got {0})")] InvalidSymmetricKeyLength(usize), + /// `service_pointer` was present but its `location` oneof was unset. + /// A pointer with no location gives the joiner no fetch target: fail + /// closed, like an unrecognized version. (A payload with the field + /// entirely ABSENT is fine — that means application-resolved.) + #[error("service_pointer is present but carries no location (fail closed)")] + EmptyServicePointer, + /// `service_pointer.https_url` failed to parse as a URL, or its scheme + /// was not `https`. + #[error("service_pointer.https_url is invalid: {0}")] + InvalidHttpsUrl(String), } /// Generate a fresh 32-byte symmetric key from the workspace CSPRNG. /// /// The key is intended for use with ChaCha20Poly1305 when wrapping the /// encrypted GroupInfo blob referenced by the resulting -/// [`ExternalInvitePayload`]. +/// [`ExternalInvitePayload`]. Uniform randomness is also what guarantees a +/// re-enabled invite never revives a previously-used key — there is no +/// key-history tracking anywhere. pub fn generate_symmetric_key() -> [u8; SYMMETRIC_KEY_LEN] { xmtp_common::rand_array::() } @@ -73,6 +95,10 @@ pub fn generate_symmetric_key() -> [u8; SYMMETRIC_KEY_LEN] { /// Intended for use with ChaCha20Poly1305 alongside a key produced by /// [`generate_symmetric_key`]. The nonce is *not* stored in the payload /// itself — it lives next to the ciphertext in the encrypted GroupInfo blob. +/// Nonces MUST come from this (or an equivalent CSPRNG) source on every +/// encryption: many independent writers encrypt under the same long-lived +/// key, so deterministic (counter) schemes would collide across writers and +/// reuse a nonce. pub fn generate_nonce() -> [u8; NONCE_LEN] { xmtp_common::rand_array::() } @@ -87,8 +113,58 @@ pub fn generate_external_group_id() -> [u8; RECOMMENDED_EXTERNAL_GROUP_ID_LEN] { xmtp_common::rand_array::() } +/// Build a [`ServicePointer`] from an `https` URL, validating it parses and +/// carries the `https` scheme. +pub fn https_service_pointer(url: &str) -> Result { + validate_https_url(url)?; + Ok(ServicePointer { + location: Some(ServiceLocation::HttpsUrl(url.to_string())), + }) +} + +/// Build a [`ServicePointer`] from application-defined opaque bytes (NFC +/// tags, custom resolver schemes, …). Opaque to libxmtp; no validation +/// beyond carrying *a* location. +pub fn opaque_service_pointer(bytes: Vec) -> ServicePointer { + ServicePointer { + location: Some(ServiceLocation::Opaque(bytes)), + } +} + +fn validate_https_url(raw: &str) -> Result<(), InvitePayloadError> { + let parsed = + url::Url::parse(raw).map_err(|e| InvitePayloadError::InvalidHttpsUrl(e.to_string()))?; + if parsed.scheme() != "https" { + return Err(InvitePayloadError::InvalidHttpsUrl(format!( + "scheme must be https (got {})", + parsed.scheme() + ))); + } + Ok(()) +} + +/// Validate a [`ServicePointer`]: exactly one `location` variant must be +/// set, and an `https_url` location must parse with the `https` scheme. +/// +/// Note the asymmetry with the *field* being absent on a payload: an absent +/// `service_pointer` means the application resolves the service out-of-band +/// and is legal; a present-but-empty pointer is a parse failure. +pub fn validate_service_pointer(pointer: &ServicePointer) -> Result<(), InvitePayloadError> { + match &pointer.location { + None => Err(InvitePayloadError::EmptyServicePointer), + Some(ServiceLocation::HttpsUrl(raw)) => validate_https_url(raw), + Some(ServiceLocation::Opaque(_)) => Ok(()), + } +} + /// Validate that `payload.version` carries a recognised variant and that -/// the V1 fields meet their length requirements. +/// the V1 fields meet their requirements: +/// +/// * `service_pointer` — absent is legal (application-resolved); present +/// requires a location variant ([`validate_service_pointer`]). +/// * `external_group_id` — at least [`MIN_EXTERNAL_GROUP_ID_LEN`] bytes. +/// * `symmetric_key` — present with exactly [`SYMMETRIC_KEY_LEN`] bytes of +/// `material`. /// /// Currently the only recognised variant is V1. Future versions extend the /// oneof; unknown variants are rejected (fail closed). @@ -99,14 +175,21 @@ pub fn validate( Some(ExternalInvitePayloadVersion::V1(v1)) => v1, None => return Err(InvitePayloadError::UnsupportedVersion), }; + if let Some(pointer) = &v1.service_pointer { + validate_service_pointer(pointer)?; + } if v1.external_group_id.len() < MIN_EXTERNAL_GROUP_ID_LEN { return Err(InvitePayloadError::InvalidExternalGroupIdLength { len: v1.external_group_id.len(), }); } - if v1.symmetric_key.len() != SYMMETRIC_KEY_LEN { + let key = v1 + .symmetric_key + .as_ref() + .ok_or(InvitePayloadError::MissingSymmetricKey)?; + if key.material.len() != SYMMETRIC_KEY_LEN { return Err(InvitePayloadError::InvalidSymmetricKeyLength( - v1.symmetric_key.len(), + key.material.len(), )); } Ok(v1) @@ -115,8 +198,10 @@ pub fn validate( /// Build an [`ExternalInvitePayload`] wrapping a [`ExternalInvitePayloadV1`] /// with the supplied fields. /// -/// * `service_pointer` — application-defined opaque bytes describing where -/// the encrypted GroupInfo blob can be fetched. +/// * `service_pointer` — where the encrypted GroupInfo blob can be fetched +/// ([`https_service_pointer`] / [`opaque_service_pointer`]). `None` means +/// application-resolved: the scanning app already knows how to reach its +/// service, and the QR carries no fetch target at all. /// * `external_group_id` — service-slot identifier carried on the wire and /// verified by the joiner against the group's /// `EXTERNAL_COMMIT_POLICY.external_group_id` after joining. MUST be at @@ -126,10 +211,13 @@ pub fn validate( /// * `symmetric_key` — typically the output of [`generate_symmetric_key`]. /// Length is type-enforced. pub fn build_payload( - service_pointer: Vec, + service_pointer: Option, external_group_id: Vec, symmetric_key: [u8; SYMMETRIC_KEY_LEN], ) -> Result { + if let Some(pointer) = &service_pointer { + validate_service_pointer(pointer)?; + } if external_group_id.len() < MIN_EXTERNAL_GROUP_ID_LEN { return Err(InvitePayloadError::InvalidExternalGroupIdLength { len: external_group_id.len(), @@ -139,7 +227,9 @@ pub fn build_payload( version: Some(ExternalInvitePayloadVersion::V1(ExternalInvitePayloadV1 { service_pointer, external_group_id, - symmetric_key: symmetric_key.to_vec(), + symmetric_key: Some(SymmetricKey { + material: symmetric_key.to_vec(), + }), })), }) } @@ -150,7 +240,7 @@ mod tests { fn well_formed_payload() -> ExternalInvitePayload { build_payload( - b"https://invites.example/abc".to_vec(), + Some(https_service_pointer("https://invites.example/abc").expect("valid https url")), generate_external_group_id().to_vec(), [0x42u8; SYMMETRIC_KEY_LEN], ) @@ -185,10 +275,53 @@ mod tests { fn validate_accepts_well_formed_v1() { let payload = well_formed_payload(); let v1 = validate(&payload)?; - assert_eq!(v1.symmetric_key.len(), SYMMETRIC_KEY_LEN); + assert_eq!( + v1.symmetric_key.as_ref().unwrap().material.len(), + SYMMETRIC_KEY_LEN + ); assert!(v1.external_group_id.len() >= MIN_EXTERNAL_GROUP_ID_LEN); } + #[xmtp_common::test(unwrap_try = true)] + fn validate_accepts_absent_service_pointer() { + // Absent pointer = application-resolved service: the scanning app + // knows its own endpoint and the QR carries no fetch target. + let payload = build_payload( + None, + generate_external_group_id().to_vec(), + [0x42u8; SYMMETRIC_KEY_LEN], + )?; + let v1 = validate(&payload)?; + assert!(v1.service_pointer.is_none()); + } + + #[xmtp_common::test(unwrap_try = true)] + fn validate_rejects_present_but_empty_service_pointer() { + // Present-but-empty is distinguishable from absent on the wire and + // gives the joiner no fetch target: fail closed. + let mut payload = well_formed_payload(); + if let Some(ExternalInvitePayloadVersion::V1(ref mut v1)) = payload.version { + v1.service_pointer = Some(ServicePointer { location: None }); + } + assert_eq!( + validate(&payload), + Err(InvitePayloadError::EmptyServicePointer) + ); + } + + #[xmtp_common::test(unwrap_try = true)] + fn https_pointer_rejects_non_https_and_garbage() { + assert!(matches!( + https_service_pointer("http://invites.example/abc"), + Err(InvitePayloadError::InvalidHttpsUrl(_)) + )); + assert!(matches!( + https_service_pointer("not a url"), + Err(InvitePayloadError::InvalidHttpsUrl(_)) + )); + assert!(https_service_pointer("https://invites.example/abc").is_ok()); + } + #[xmtp_common::test(unwrap_try = true)] fn validate_rejects_missing_version() { let payload = ExternalInvitePayload { version: None }; @@ -201,7 +334,7 @@ mod tests { #[xmtp_common::test(unwrap_try = true)] fn build_payload_rejects_short_external_group_id() { let result = build_payload( - b"svc".to_vec(), + None, vec![0u8; MIN_EXTERNAL_GROUP_ID_LEN - 1], [0x42u8; SYMMETRIC_KEY_LEN], ); @@ -221,9 +354,11 @@ mod tests { // untrusted peers. let payload = ExternalInvitePayload { version: Some(ExternalInvitePayloadVersion::V1(ExternalInvitePayloadV1 { - service_pointer: b"svc".to_vec(), + service_pointer: Some(opaque_service_pointer(b"svc".to_vec())), external_group_id: vec![0u8; MIN_EXTERNAL_GROUP_ID_LEN - 1], - symmetric_key: vec![0x42u8; SYMMETRIC_KEY_LEN], + symmetric_key: Some(SymmetricKey { + material: vec![0x42u8; SYMMETRIC_KEY_LEN], + }), })), }; assert_eq!( @@ -234,11 +369,25 @@ mod tests { ); } + #[xmtp_common::test(unwrap_try = true)] + fn validate_rejects_missing_symmetric_key() { + let mut payload = well_formed_payload(); + if let Some(ExternalInvitePayloadVersion::V1(ref mut v1)) = payload.version { + v1.symmetric_key = None; + } + assert_eq!( + validate(&payload), + Err(InvitePayloadError::MissingSymmetricKey) + ); + } + #[xmtp_common::test(unwrap_try = true)] fn validate_rejects_wrong_symmetric_key_length() { let mut payload = well_formed_payload(); if let Some(ExternalInvitePayloadVersion::V1(ref mut v1)) = payload.version { - v1.symmetric_key = vec![0u8; SYMMETRIC_KEY_LEN - 1]; + v1.symmetric_key = Some(SymmetricKey { + material: vec![0u8; SYMMETRIC_KEY_LEN - 1], + }); } assert_eq!( validate(&payload), @@ -250,14 +399,19 @@ mod tests { #[xmtp_common::test(unwrap_try = true)] fn build_payload_round_trip() { - let service_pointer = b"https://invites.example/abc".to_vec(); + let pointer = https_service_pointer("https://invites.example/abc")?; let external_group_id = generate_external_group_id().to_vec(); let key = [0x42u8; SYMMETRIC_KEY_LEN]; - let payload = build_payload(service_pointer.clone(), external_group_id.clone(), key)?; + let payload = build_payload(Some(pointer.clone()), external_group_id.clone(), key)?; let v1 = validate(&payload)?; - assert_eq!(v1.service_pointer, service_pointer); + assert_eq!(v1.service_pointer, Some(pointer)); assert_eq!(v1.external_group_id, external_group_id); - assert_eq!(v1.symmetric_key, key.to_vec()); + assert_eq!( + v1.symmetric_key, + Some(SymmetricKey { + material: key.to_vec() + }) + ); } } diff --git a/crates/xmtp_proto/proto_version b/crates/xmtp_proto/proto_version index aba460fee9..39ee867457 100644 --- a/crates/xmtp_proto/proto_version +++ b/crates/xmtp_proto/proto_version @@ -1 +1 @@ -e6f640a8994dad1779da0280b15be482c0bb4bb8 +c78e3795bfcad611c1da025b0ad40bd3b625c39c diff --git a/crates/xmtp_proto/src/gen/proto_descriptor.bin b/crates/xmtp_proto/src/gen/proto_descriptor.bin index b2d195784b..b17dd0e333 100644 Binary files a/crates/xmtp_proto/src/gen/proto_descriptor.bin and b/crates/xmtp_proto/src/gen/proto_descriptor.bin differ diff --git a/crates/xmtp_proto/src/gen/xmtp.message_api.v1.rs b/crates/xmtp_proto/src/gen/xmtp.message_api.v1.rs index d3fb9f6be9..6882242c90 100644 --- a/crates/xmtp_proto/src/gen/xmtp.message_api.v1.rs +++ b/crates/xmtp_proto/src/gen/xmtp.message_api.v1.rs @@ -1,54 +1,4 @@ // This file is @generated by prost-build. -/// Token is used by clients to prove to the nodes -/// that they are serving a specific wallet. -#[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] -pub struct Token { - /// identity key signed by a wallet - #[prost(message, optional, tag = "1")] - pub identity_key: ::core::option::Option, - /// encoded bytes of AuthData - #[prost(bytes = "vec", tag = "2")] - pub auth_data_bytes: ::prost::alloc::vec::Vec, - /// identity key signature of AuthData bytes - #[prost(message, optional, tag = "3")] - pub auth_data_signature: ::core::option::Option< - super::super::message_contents::Signature, - >, -} -impl ::prost::Name for Token { - const NAME: &'static str = "Token"; - const PACKAGE: &'static str = "xmtp.message_api.v1"; - fn full_name() -> ::prost::alloc::string::String { - "xmtp.message_api.v1.Token".into() - } - fn type_url() -> ::prost::alloc::string::String { - "/xmtp.message_api.v1.Token".into() - } -} -/// AuthData carries token parameters that are authenticated -/// by the identity key signature. -/// It is embedded in the Token structure as bytes -/// so that the bytes don't need to be reconstructed -/// to verify the token signature. -#[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] -pub struct AuthData { - /// address of the wallet - #[prost(string, tag = "1")] - pub wallet_addr: ::prost::alloc::string::String, - /// time when the token was generated/signed - #[prost(uint64, tag = "2")] - pub created_ns: u64, -} -impl ::prost::Name for AuthData { - const NAME: &'static str = "AuthData"; - const PACKAGE: &'static str = "xmtp.message_api.v1"; - fn full_name() -> ::prost::alloc::string::String { - "xmtp.message_api.v1.AuthData".into() - } - fn type_url() -> ::prost::alloc::string::String { - "/xmtp.message_api.v1.AuthData".into() - } -} /// This is based off of the go-waku Index type, but with the /// receiverTime and pubsubTopic removed for simplicity. /// Both removed fields are optional @@ -768,3 +718,53 @@ pub mod message_api_server { const NAME: &'static str = SERVICE_NAME; } } +/// Token is used by clients to prove to the nodes +/// that they are serving a specific wallet. +#[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] +pub struct Token { + /// identity key signed by a wallet + #[prost(message, optional, tag = "1")] + pub identity_key: ::core::option::Option, + /// encoded bytes of AuthData + #[prost(bytes = "vec", tag = "2")] + pub auth_data_bytes: ::prost::alloc::vec::Vec, + /// identity key signature of AuthData bytes + #[prost(message, optional, tag = "3")] + pub auth_data_signature: ::core::option::Option< + super::super::message_contents::Signature, + >, +} +impl ::prost::Name for Token { + const NAME: &'static str = "Token"; + const PACKAGE: &'static str = "xmtp.message_api.v1"; + fn full_name() -> ::prost::alloc::string::String { + "xmtp.message_api.v1.Token".into() + } + fn type_url() -> ::prost::alloc::string::String { + "/xmtp.message_api.v1.Token".into() + } +} +/// AuthData carries token parameters that are authenticated +/// by the identity key signature. +/// It is embedded in the Token structure as bytes +/// so that the bytes don't need to be reconstructed +/// to verify the token signature. +#[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] +pub struct AuthData { + /// address of the wallet + #[prost(string, tag = "1")] + pub wallet_addr: ::prost::alloc::string::String, + /// time when the token was generated/signed + #[prost(uint64, tag = "2")] + pub created_ns: u64, +} +impl ::prost::Name for AuthData { + const NAME: &'static str = "AuthData"; + const PACKAGE: &'static str = "xmtp.message_api.v1"; + fn full_name() -> ::prost::alloc::string::String { + "xmtp.message_api.v1.AuthData".into() + } + fn type_url() -> ::prost::alloc::string::String { + "/xmtp.message_api.v1.AuthData".into() + } +} diff --git a/crates/xmtp_proto/src/gen/xmtp.message_contents.rs b/crates/xmtp_proto/src/gen/xmtp.message_contents.rs index 1362df0280..5e4f142c79 100644 --- a/crates/xmtp_proto/src/gen/xmtp.message_contents.rs +++ b/crates/xmtp_proto/src/gen/xmtp.message_contents.rs @@ -922,6 +922,87 @@ impl ::prost::Name for PrivatePreferencesPayload { "/xmtp.message_contents.PrivatePreferencesPayload".into() } } +/// The message that will be signed by the Client and returned inside the +/// `action_body` field of the FrameAction message +#[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] +pub struct FrameActionBody { + /// The URL of the frame that was clicked + /// May be different from `post_url` + #[prost(string, tag = "1")] + pub frame_url: ::prost::alloc::string::String, + /// The 1-indexed button that was clicked + #[prost(int32, tag = "2")] + pub button_index: i32, + /// Timestamp of the click in milliseconds since the epoch + #[deprecated] + #[prost(uint64, tag = "3")] + pub timestamp: u64, + /// A unique identifier for the conversation, not tied to anything on the + /// network. Will not match the topic or conversation_id + #[prost(string, tag = "4")] + pub opaque_conversation_identifier: ::prost::alloc::string::String, + /// Unix timestamp + #[prost(uint32, tag = "5")] + pub unix_timestamp: u32, + /// Input text from a text input field + #[prost(string, tag = "6")] + pub input_text: ::prost::alloc::string::String, + /// A state serialized to a string (for example via JSON.stringify()). Maximum 4096 bytes. + #[prost(string, tag = "7")] + pub state: ::prost::alloc::string::String, + /// A 0x wallet address + #[prost(string, tag = "8")] + pub address: ::prost::alloc::string::String, + /// A hash from a transaction + #[prost(string, tag = "9")] + pub transaction_id: ::prost::alloc::string::String, +} +impl ::prost::Name for FrameActionBody { + const NAME: &'static str = "FrameActionBody"; + const PACKAGE: &'static str = "xmtp.message_contents"; + fn full_name() -> ::prost::alloc::string::String { + "xmtp.message_contents.FrameActionBody".into() + } + fn type_url() -> ::prost::alloc::string::String { + "/xmtp.message_contents.FrameActionBody".into() + } +} +/// The outer payload that will be sent as the `messageBytes` in the +/// `trusted_data` part of the Frames message +#[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] +pub struct FrameAction { + #[deprecated] + #[prost(message, optional, tag = "1")] + pub signature: ::core::option::Option, + /// The SignedPublicKeyBundle of the signer, used to link the XMTP signature + /// with a blockchain account through a chain of signatures. + #[deprecated] + #[prost(message, optional, tag = "2")] + pub signed_public_key_bundle: ::core::option::Option, + /// Serialized FrameActionBody message, so that the signature verification can + /// happen on a byte-perfect representation of the message + #[prost(bytes = "vec", tag = "3")] + pub action_body: ::prost::alloc::vec::Vec, + /// The installation signature + #[prost(bytes = "vec", tag = "4")] + pub installation_signature: ::prost::alloc::vec::Vec, + /// The public installation id used to sign. + #[prost(bytes = "vec", tag = "5")] + pub installation_id: ::prost::alloc::vec::Vec, + /// The inbox id of the installation used to sign. + #[prost(string, tag = "6")] + pub inbox_id: ::prost::alloc::string::String, +} +impl ::prost::Name for FrameAction { + const NAME: &'static str = "FrameAction"; + const PACKAGE: &'static str = "xmtp.message_contents"; + fn full_name() -> ::prost::alloc::string::String { + "xmtp.message_contents.FrameAction".into() + } + fn type_url() -> ::prost::alloc::string::String { + "/xmtp.message_contents.FrameAction".into() + } +} /// ContentTypeId is used to identify the type of content stored in a Message. #[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] pub struct ContentTypeId { @@ -1036,51 +1117,6 @@ impl Compression { } } } -/// Composite is used to implement xmtp.org/composite content type -#[derive(Clone, PartialEq, ::prost::Message)] -pub struct Composite { - #[prost(message, repeated, tag = "1")] - pub parts: ::prost::alloc::vec::Vec, -} -/// Nested message and enum types in `Composite`. -pub mod composite { - /// Part represents one section of a composite message - #[derive(Clone, PartialEq, ::prost::Message)] - pub struct Part { - #[prost(oneof = "part::Element", tags = "1, 2")] - pub element: ::core::option::Option, - } - /// Nested message and enum types in `Part`. - pub mod part { - #[derive(Clone, PartialEq, ::prost::Oneof)] - pub enum Element { - #[prost(message, tag = "1")] - Part(super::super::EncodedContent), - #[prost(message, tag = "2")] - Composite(super::super::Composite), - } - } - impl ::prost::Name for Part { - const NAME: &'static str = "Part"; - const PACKAGE: &'static str = "xmtp.message_contents"; - fn full_name() -> ::prost::alloc::string::String { - "xmtp.message_contents.Composite.Part".into() - } - fn type_url() -> ::prost::alloc::string::String { - "/xmtp.message_contents.Composite.Part".into() - } - } -} -impl ::prost::Name for Composite { - const NAME: &'static str = "Composite"; - const PACKAGE: &'static str = "xmtp.message_contents"; - fn full_name() -> ::prost::alloc::string::String { - "xmtp.message_contents.Composite".into() - } - fn type_url() -> ::prost::alloc::string::String { - "/xmtp.message_contents.Composite".into() - } -} /// LEGACY: User key bundle V1 using PublicKeys. /// The PublicKeys MUST be signed. #[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] @@ -1140,110 +1176,67 @@ impl ::prost::Name for ContactBundle { "/xmtp.message_contents.ContactBundle".into() } } -/// EciesMessage is a wrapper for ECIES encrypted payloads +/// SignedPayload is a wrapper for a signature and a payload #[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] -pub struct EciesMessage { - #[prost(oneof = "ecies_message::Version", tags = "1")] - pub version: ::core::option::Option, -} -/// Nested message and enum types in `EciesMessage`. -pub mod ecies_message { - #[derive(Clone, PartialEq, Eq, Hash, ::prost::Oneof)] - pub enum Version { - /// Expected to be an ECIES encrypted SignedPayload - #[prost(bytes, tag = "1")] - V1(::prost::alloc::vec::Vec), - } +pub struct SignedPayload { + #[prost(bytes = "vec", tag = "1")] + pub payload: ::prost::alloc::vec::Vec, + #[prost(message, optional, tag = "2")] + pub signature: ::core::option::Option, } -impl ::prost::Name for EciesMessage { - const NAME: &'static str = "EciesMessage"; +impl ::prost::Name for SignedPayload { + const NAME: &'static str = "SignedPayload"; const PACKAGE: &'static str = "xmtp.message_contents"; fn full_name() -> ::prost::alloc::string::String { - "xmtp.message_contents.EciesMessage".into() + "xmtp.message_contents.SignedPayload".into() } fn type_url() -> ::prost::alloc::string::String { - "/xmtp.message_contents.EciesMessage".into() + "/xmtp.message_contents.SignedPayload".into() } } -/// The message that will be signed by the Client and returned inside the -/// `action_body` field of the FrameAction message -#[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] -pub struct FrameActionBody { - /// The URL of the frame that was clicked - /// May be different from `post_url` - #[prost(string, tag = "1")] - pub frame_url: ::prost::alloc::string::String, - /// The 1-indexed button that was clicked - #[prost(int32, tag = "2")] - pub button_index: i32, - /// Timestamp of the click in milliseconds since the epoch - #[deprecated] - #[prost(uint64, tag = "3")] - pub timestamp: u64, - /// A unique identifier for the conversation, not tied to anything on the - /// network. Will not match the topic or conversation_id - #[prost(string, tag = "4")] - pub opaque_conversation_identifier: ::prost::alloc::string::String, - /// Unix timestamp - #[prost(uint32, tag = "5")] - pub unix_timestamp: u32, - /// Input text from a text input field - #[prost(string, tag = "6")] - pub input_text: ::prost::alloc::string::String, - /// A state serialized to a string (for example via JSON.stringify()). Maximum 4096 bytes. - #[prost(string, tag = "7")] - pub state: ::prost::alloc::string::String, - /// A 0x wallet address - #[prost(string, tag = "8")] - pub address: ::prost::alloc::string::String, - /// A hash from a transaction - #[prost(string, tag = "9")] - pub transaction_id: ::prost::alloc::string::String, +/// Composite is used to implement xmtp.org/composite content type +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct Composite { + #[prost(message, repeated, tag = "1")] + pub parts: ::prost::alloc::vec::Vec, } -impl ::prost::Name for FrameActionBody { - const NAME: &'static str = "FrameActionBody"; - const PACKAGE: &'static str = "xmtp.message_contents"; - fn full_name() -> ::prost::alloc::string::String { - "xmtp.message_contents.FrameActionBody".into() +/// Nested message and enum types in `Composite`. +pub mod composite { + /// Part represents one section of a composite message + #[derive(Clone, PartialEq, ::prost::Message)] + pub struct Part { + #[prost(oneof = "part::Element", tags = "1, 2")] + pub element: ::core::option::Option, } - fn type_url() -> ::prost::alloc::string::String { - "/xmtp.message_contents.FrameActionBody".into() + /// Nested message and enum types in `Part`. + pub mod part { + #[derive(Clone, PartialEq, ::prost::Oneof)] + pub enum Element { + #[prost(message, tag = "1")] + Part(super::super::EncodedContent), + #[prost(message, tag = "2")] + Composite(super::super::Composite), + } + } + impl ::prost::Name for Part { + const NAME: &'static str = "Part"; + const PACKAGE: &'static str = "xmtp.message_contents"; + fn full_name() -> ::prost::alloc::string::String { + "xmtp.message_contents.Composite.Part".into() + } + fn type_url() -> ::prost::alloc::string::String { + "/xmtp.message_contents.Composite.Part".into() + } } } -/// The outer payload that will be sent as the `messageBytes` in the -/// `trusted_data` part of the Frames message -#[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] -pub struct FrameAction { - #[deprecated] - #[prost(message, optional, tag = "1")] - pub signature: ::core::option::Option, - /// The SignedPublicKeyBundle of the signer, used to link the XMTP signature - /// with a blockchain account through a chain of signatures. - #[deprecated] - #[prost(message, optional, tag = "2")] - pub signed_public_key_bundle: ::core::option::Option, - /// Serialized FrameActionBody message, so that the signature verification can - /// happen on a byte-perfect representation of the message - #[prost(bytes = "vec", tag = "3")] - pub action_body: ::prost::alloc::vec::Vec, - /// The installation signature - #[prost(bytes = "vec", tag = "4")] - pub installation_signature: ::prost::alloc::vec::Vec, - /// The public installation id used to sign. - #[prost(bytes = "vec", tag = "5")] - pub installation_id: ::prost::alloc::vec::Vec, - /// The inbox id of the installation used to sign. - #[prost(string, tag = "6")] - pub inbox_id: ::prost::alloc::string::String, -} -impl ::prost::Name for FrameAction { - const NAME: &'static str = "FrameAction"; +impl ::prost::Name for Composite { + const NAME: &'static str = "Composite"; const PACKAGE: &'static str = "xmtp.message_contents"; fn full_name() -> ::prost::alloc::string::String { - "xmtp.message_contents.FrameAction".into() + "xmtp.message_contents.Composite".into() } fn type_url() -> ::prost::alloc::string::String { - "/xmtp.message_contents.FrameAction".into() + "/xmtp.message_contents.Composite".into() } } /// Message header is encoded separately as the bytes are also used @@ -1397,21 +1390,28 @@ impl ::prost::Name for DecodedMessage { "/xmtp.message_contents.DecodedMessage".into() } } -/// SignedPayload is a wrapper for a signature and a payload +/// EciesMessage is a wrapper for ECIES encrypted payloads #[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] -pub struct SignedPayload { - #[prost(bytes = "vec", tag = "1")] - pub payload: ::prost::alloc::vec::Vec, - #[prost(message, optional, tag = "2")] - pub signature: ::core::option::Option, +pub struct EciesMessage { + #[prost(oneof = "ecies_message::Version", tags = "1")] + pub version: ::core::option::Option, } -impl ::prost::Name for SignedPayload { - const NAME: &'static str = "SignedPayload"; +/// Nested message and enum types in `EciesMessage`. +pub mod ecies_message { + #[derive(Clone, PartialEq, Eq, Hash, ::prost::Oneof)] + pub enum Version { + /// Expected to be an ECIES encrypted SignedPayload + #[prost(bytes, tag = "1")] + V1(::prost::alloc::vec::Vec), + } +} +impl ::prost::Name for EciesMessage { + const NAME: &'static str = "EciesMessage"; const PACKAGE: &'static str = "xmtp.message_contents"; fn full_name() -> ::prost::alloc::string::String { - "xmtp.message_contents.SignedPayload".into() + "xmtp.message_contents.EciesMessage".into() } fn type_url() -> ::prost::alloc::string::String { - "/xmtp.message_contents.SignedPayload".into() + "/xmtp.message_contents.EciesMessage".into() } } diff --git a/crates/xmtp_proto/src/gen/xmtp.mls.database.rs b/crates/xmtp_proto/src/gen/xmtp.mls.database.rs index 7b3795c241..d364ca0e4d 100644 --- a/crates/xmtp_proto/src/gen/xmtp.mls.database.rs +++ b/crates/xmtp_proto/src/gen/xmtp.mls.database.rs @@ -1,4 +1,72 @@ // This file is @generated by prost-build. +#[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] +pub struct Task { + #[prost(oneof = "task::Task", tags = "1, 2, 3")] + pub task: ::core::option::Option, +} +/// Nested message and enum types in `Task`. +pub mod task { + #[derive(Clone, PartialEq, Eq, Hash, ::prost::Oneof)] + pub enum Task { + #[prost(message, tag = "1")] + ProcessWelcomePointer(super::super::message_contents::WelcomePointer), + #[prost(message, tag = "2")] + SendSyncArchive(super::SendSyncArchive), + #[prost(message, tag = "3")] + ProcessPendingSelfRemove(super::ProcessPendingSelfRemove), + } +} +impl ::prost::Name for Task { + const NAME: &'static str = "Task"; + const PACKAGE: &'static str = "xmtp.mls.database"; + fn full_name() -> ::prost::alloc::string::String { + "xmtp.mls.database.Task".into() + } + fn type_url() -> ::prost::alloc::string::String { + "/xmtp.mls.database.Task".into() + } +} +#[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] +pub struct SendSyncArchive { + #[prost(message, optional, tag = "1")] + pub options: ::core::option::Option, + #[prost(bytes = "vec", tag = "2")] + pub sync_group_id: ::prost::alloc::vec::Vec, + #[prost(string, optional, tag = "3")] + pub pin: ::core::option::Option<::prost::alloc::string::String>, + #[prost(string, tag = "4")] + pub server_url: ::prost::alloc::string::String, +} +impl ::prost::Name for SendSyncArchive { + const NAME: &'static str = "SendSyncArchive"; + const PACKAGE: &'static str = "xmtp.mls.database"; + fn full_name() -> ::prost::alloc::string::String { + "xmtp.mls.database.SendSyncArchive".into() + } + fn type_url() -> ::prost::alloc::string::String { + "/xmtp.mls.database.SendSyncArchive".into() + } +} +/// Durable TaskRunner intent: process a group's pending self-remove requests +/// (build the MLS RemoveProposal/Commit to evict members who sent a LeaveRequest, +/// then clean up the pending-remove list). Enqueued in the same DB transaction as +/// the pending_remove row so it survives restart; runs on the TaskRunner with +/// retry/backoff. group_id is the target conversation. +#[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] +pub struct ProcessPendingSelfRemove { + #[prost(bytes = "vec", tag = "1")] + pub group_id: ::prost::alloc::vec::Vec, +} +impl ::prost::Name for ProcessPendingSelfRemove { + const NAME: &'static str = "ProcessPendingSelfRemove"; + const PACKAGE: &'static str = "xmtp.mls.database"; + fn full_name() -> ::prost::alloc::string::String { + "xmtp.mls.database.ProcessPendingSelfRemove".into() + } + fn type_url() -> ::prost::alloc::string::String { + "/xmtp.mls.database.ProcessPendingSelfRemove".into() + } +} /// The data required to publish a message #[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] pub struct SendMessageData { @@ -781,71 +849,3 @@ impl PermissionPolicyOption { } } } -#[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] -pub struct Task { - #[prost(oneof = "task::Task", tags = "1, 2, 3")] - pub task: ::core::option::Option, -} -/// Nested message and enum types in `Task`. -pub mod task { - #[derive(Clone, PartialEq, Eq, Hash, ::prost::Oneof)] - pub enum Task { - #[prost(message, tag = "1")] - ProcessWelcomePointer(super::super::message_contents::WelcomePointer), - #[prost(message, tag = "2")] - SendSyncArchive(super::SendSyncArchive), - #[prost(message, tag = "3")] - ProcessPendingSelfRemove(super::ProcessPendingSelfRemove), - } -} -impl ::prost::Name for Task { - const NAME: &'static str = "Task"; - const PACKAGE: &'static str = "xmtp.mls.database"; - fn full_name() -> ::prost::alloc::string::String { - "xmtp.mls.database.Task".into() - } - fn type_url() -> ::prost::alloc::string::String { - "/xmtp.mls.database.Task".into() - } -} -#[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] -pub struct SendSyncArchive { - #[prost(message, optional, tag = "1")] - pub options: ::core::option::Option, - #[prost(bytes = "vec", tag = "2")] - pub sync_group_id: ::prost::alloc::vec::Vec, - #[prost(string, optional, tag = "3")] - pub pin: ::core::option::Option<::prost::alloc::string::String>, - #[prost(string, tag = "4")] - pub server_url: ::prost::alloc::string::String, -} -impl ::prost::Name for SendSyncArchive { - const NAME: &'static str = "SendSyncArchive"; - const PACKAGE: &'static str = "xmtp.mls.database"; - fn full_name() -> ::prost::alloc::string::String { - "xmtp.mls.database.SendSyncArchive".into() - } - fn type_url() -> ::prost::alloc::string::String { - "/xmtp.mls.database.SendSyncArchive".into() - } -} -/// Durable TaskRunner intent: process a group's pending self-remove requests -/// (build the MLS RemoveProposal/Commit to evict members who sent a LeaveRequest, -/// then clean up the pending-remove list). Enqueued in the same DB transaction as -/// the pending_remove row so it survives restart; runs on the TaskRunner with -/// retry/backoff. group_id is the target conversation. -#[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] -pub struct ProcessPendingSelfRemove { - #[prost(bytes = "vec", tag = "1")] - pub group_id: ::prost::alloc::vec::Vec, -} -impl ::prost::Name for ProcessPendingSelfRemove { - const NAME: &'static str = "ProcessPendingSelfRemove"; - const PACKAGE: &'static str = "xmtp.mls.database"; - fn full_name() -> ::prost::alloc::string::String { - "xmtp.mls.database.ProcessPendingSelfRemove".into() - } - fn type_url() -> ::prost::alloc::string::String { - "/xmtp.mls.database.ProcessPendingSelfRemove".into() - } -} diff --git a/crates/xmtp_proto/src/gen/xmtp.mls.message_contents.content_types.rs b/crates/xmtp_proto/src/gen/xmtp.mls.message_contents.content_types.rs index 375080f5d2..de3b0d73b7 100644 --- a/crates/xmtp_proto/src/gen/xmtp.mls.message_contents.content_types.rs +++ b/crates/xmtp_proto/src/gen/xmtp.mls.message_contents.content_types.rs @@ -1,112 +1,4 @@ // This file is @generated by prost-build. -/// DeleteMessage message type -#[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] -pub struct DeleteMessage { - /// ID of the message to delete - #[prost(string, tag = "1")] - pub message_id: ::prost::alloc::string::String, -} -impl ::prost::Name for DeleteMessage { - const NAME: &'static str = "DeleteMessage"; - const PACKAGE: &'static str = "xmtp.mls.message_contents.content_types"; - fn full_name() -> ::prost::alloc::string::String { - "xmtp.mls.message_contents.content_types.DeleteMessage".into() - } - fn type_url() -> ::prost::alloc::string::String { - "/xmtp.mls.message_contents.content_types.DeleteMessage".into() - } -} -/// EditMessage message type -#[derive(Clone, PartialEq, ::prost::Message)] -pub struct EditMessage { - /// ID of the message to edit - #[prost(string, tag = "1")] - pub message_id: ::prost::alloc::string::String, - /// The new content for the message - #[prost(message, optional, tag = "2")] - pub edited_content: ::core::option::Option, -} -impl ::prost::Name for EditMessage { - const NAME: &'static str = "EditMessage"; - const PACKAGE: &'static str = "xmtp.mls.message_contents.content_types"; - fn full_name() -> ::prost::alloc::string::String { - "xmtp.mls.message_contents.content_types.EditMessage".into() - } - fn type_url() -> ::prost::alloc::string::String { - "/xmtp.mls.message_contents.content_types.EditMessage".into() - } -} -/// LeaveRequest message type -#[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] -pub struct LeaveRequest { - /// A serialized AuthenticatedNote containing the sender's signed, member-only verifiable statement - #[prost(bytes = "vec", optional, tag = "1")] - pub authenticated_note: ::core::option::Option<::prost::alloc::vec::Vec>, -} -impl ::prost::Name for LeaveRequest { - const NAME: &'static str = "LeaveRequest"; - const PACKAGE: &'static str = "xmtp.mls.message_contents.content_types"; - fn full_name() -> ::prost::alloc::string::String { - "xmtp.mls.message_contents.content_types.LeaveRequest".into() - } - fn type_url() -> ::prost::alloc::string::String { - "/xmtp.mls.message_contents.content_types.LeaveRequest".into() - } -} -/// MultiRemoteAttachment message type -#[derive(Clone, PartialEq, ::prost::Message)] -pub struct MultiRemoteAttachment { - /// Array of attachment information - #[prost(message, repeated, tag = "1")] - pub attachments: ::prost::alloc::vec::Vec, -} -impl ::prost::Name for MultiRemoteAttachment { - const NAME: &'static str = "MultiRemoteAttachment"; - const PACKAGE: &'static str = "xmtp.mls.message_contents.content_types"; - fn full_name() -> ::prost::alloc::string::String { - "xmtp.mls.message_contents.content_types.MultiRemoteAttachment".into() - } - fn type_url() -> ::prost::alloc::string::String { - "/xmtp.mls.message_contents.content_types.MultiRemoteAttachment".into() - } -} -#[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] -pub struct RemoteAttachmentInfo { - /// The SHA256 hash of the remote content - #[prost(string, tag = "1")] - pub content_digest: ::prost::alloc::string::String, - /// A 32 byte array for decrypting the remote content payload - #[prost(bytes = "vec", tag = "2")] - pub secret: ::prost::alloc::vec::Vec, - /// A byte array for the nonce used to encrypt the remote content payload - #[prost(bytes = "vec", tag = "3")] - pub nonce: ::prost::alloc::vec::Vec, - /// A byte array for the salt used to encrypt the remote content payload - #[prost(bytes = "vec", tag = "4")] - pub salt: ::prost::alloc::vec::Vec, - /// The scheme of the URL. Must be " - #[prost(string, tag = "5")] - pub scheme: ::prost::alloc::string::String, - /// The URL of the remote content - #[prost(string, tag = "6")] - pub url: ::prost::alloc::string::String, - /// The size of the encrypted content in bytes (max size of 4GB) - #[prost(uint32, optional, tag = "7")] - pub content_length: ::core::option::Option, - /// The filename of the remote content - #[prost(string, optional, tag = "8")] - pub filename: ::core::option::Option<::prost::alloc::string::String>, -} -impl ::prost::Name for RemoteAttachmentInfo { - const NAME: &'static str = "RemoteAttachmentInfo"; - const PACKAGE: &'static str = "xmtp.mls.message_contents.content_types"; - fn full_name() -> ::prost::alloc::string::String { - "xmtp.mls.message_contents.content_types.RemoteAttachmentInfo".into() - } - fn type_url() -> ::prost::alloc::string::String { - "/xmtp.mls.message_contents.content_types.RemoteAttachmentInfo".into() - } -} /// Reaction message type #[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] pub struct ReactionV2 { @@ -200,6 +92,94 @@ impl ReactionSchema { } } } +/// DeleteMessage message type +#[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] +pub struct DeleteMessage { + /// ID of the message to delete + #[prost(string, tag = "1")] + pub message_id: ::prost::alloc::string::String, +} +impl ::prost::Name for DeleteMessage { + const NAME: &'static str = "DeleteMessage"; + const PACKAGE: &'static str = "xmtp.mls.message_contents.content_types"; + fn full_name() -> ::prost::alloc::string::String { + "xmtp.mls.message_contents.content_types.DeleteMessage".into() + } + fn type_url() -> ::prost::alloc::string::String { + "/xmtp.mls.message_contents.content_types.DeleteMessage".into() + } +} +/// MultiRemoteAttachment message type +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct MultiRemoteAttachment { + /// Array of attachment information + #[prost(message, repeated, tag = "1")] + pub attachments: ::prost::alloc::vec::Vec, +} +impl ::prost::Name for MultiRemoteAttachment { + const NAME: &'static str = "MultiRemoteAttachment"; + const PACKAGE: &'static str = "xmtp.mls.message_contents.content_types"; + fn full_name() -> ::prost::alloc::string::String { + "xmtp.mls.message_contents.content_types.MultiRemoteAttachment".into() + } + fn type_url() -> ::prost::alloc::string::String { + "/xmtp.mls.message_contents.content_types.MultiRemoteAttachment".into() + } +} +#[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] +pub struct RemoteAttachmentInfo { + /// The SHA256 hash of the remote content + #[prost(string, tag = "1")] + pub content_digest: ::prost::alloc::string::String, + /// A 32 byte array for decrypting the remote content payload + #[prost(bytes = "vec", tag = "2")] + pub secret: ::prost::alloc::vec::Vec, + /// A byte array for the nonce used to encrypt the remote content payload + #[prost(bytes = "vec", tag = "3")] + pub nonce: ::prost::alloc::vec::Vec, + /// A byte array for the salt used to encrypt the remote content payload + #[prost(bytes = "vec", tag = "4")] + pub salt: ::prost::alloc::vec::Vec, + /// The scheme of the URL. Must be " + #[prost(string, tag = "5")] + pub scheme: ::prost::alloc::string::String, + /// The URL of the remote content + #[prost(string, tag = "6")] + pub url: ::prost::alloc::string::String, + /// The size of the encrypted content in bytes (max size of 4GB) + #[prost(uint32, optional, tag = "7")] + pub content_length: ::core::option::Option, + /// The filename of the remote content + #[prost(string, optional, tag = "8")] + pub filename: ::core::option::Option<::prost::alloc::string::String>, +} +impl ::prost::Name for RemoteAttachmentInfo { + const NAME: &'static str = "RemoteAttachmentInfo"; + const PACKAGE: &'static str = "xmtp.mls.message_contents.content_types"; + fn full_name() -> ::prost::alloc::string::String { + "xmtp.mls.message_contents.content_types.RemoteAttachmentInfo".into() + } + fn type_url() -> ::prost::alloc::string::String { + "/xmtp.mls.message_contents.content_types.RemoteAttachmentInfo".into() + } +} +/// LeaveRequest message type +#[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] +pub struct LeaveRequest { + /// A serialized AuthenticatedNote containing the sender's signed, member-only verifiable statement + #[prost(bytes = "vec", optional, tag = "1")] + pub authenticated_note: ::core::option::Option<::prost::alloc::vec::Vec>, +} +impl ::prost::Name for LeaveRequest { + const NAME: &'static str = "LeaveRequest"; + const PACKAGE: &'static str = "xmtp.mls.message_contents.content_types"; + fn full_name() -> ::prost::alloc::string::String { + "xmtp.mls.message_contents.content_types.LeaveRequest".into() + } + fn type_url() -> ::prost::alloc::string::String { + "/xmtp.mls.message_contents.content_types.LeaveRequest".into() + } +} /// WalletSendCalls represents parameters for sending wallet calls #[derive(Clone, PartialEq, ::prost::Message)] pub struct WalletSendCalls { @@ -262,3 +242,23 @@ impl ::prost::Name for Call { "/xmtp.mls.message_contents.content_types.Call".into() } } +/// EditMessage message type +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct EditMessage { + /// ID of the message to edit + #[prost(string, tag = "1")] + pub message_id: ::prost::alloc::string::String, + /// The new content for the message + #[prost(message, optional, tag = "2")] + pub edited_content: ::core::option::Option, +} +impl ::prost::Name for EditMessage { + const NAME: &'static str = "EditMessage"; + const PACKAGE: &'static str = "xmtp.mls.message_contents.content_types"; + fn full_name() -> ::prost::alloc::string::String { + "xmtp.mls.message_contents.content_types.EditMessage".into() + } + fn type_url() -> ::prost::alloc::string::String { + "/xmtp.mls.message_contents.content_types.EditMessage".into() + } +} diff --git a/crates/xmtp_proto/src/gen/xmtp.mls.message_contents.rs b/crates/xmtp_proto/src/gen/xmtp.mls.message_contents.rs index 01bb177c28..587a9c4bb6 100644 --- a/crates/xmtp_proto/src/gen/xmtp.mls.message_contents.rs +++ b/crates/xmtp_proto/src/gen/xmtp.mls.message_contents.rs @@ -1,93 +1,4 @@ // This file is @generated by prost-build. -/// PlaintextCommitLogEntry indicates whether a commit was successful or not, -/// when applied on top of the indicated `last_epoch_authenticator`. -#[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] -pub struct PlaintextCommitLogEntry { - /// The group_id of the group that the commit belongs to. - #[prost(bytes = "vec", tag = "1")] - pub group_id: ::prost::alloc::vec::Vec, - /// The sequence ID of the commit payload being validated. - #[prost(uint64, tag = "2")] - pub commit_sequence_id: u64, - /// The encryption state before the commit was applied. - #[prost(bytes = "vec", tag = "3")] - pub last_epoch_authenticator: ::prost::alloc::vec::Vec, - /// Indicates whether the commit was successful, or why it failed. - #[prost(enumeration = "CommitResult", tag = "4")] - pub commit_result: i32, - /// The epoch number after the commit was applied, if successful. - #[prost(uint64, tag = "5")] - pub applied_epoch_number: u64, - /// The encryption state after the commit was applied, if successful. - #[prost(bytes = "vec", tag = "6")] - pub applied_epoch_authenticator: ::prost::alloc::vec::Vec, -} -impl ::prost::Name for PlaintextCommitLogEntry { - const NAME: &'static str = "PlaintextCommitLogEntry"; - const PACKAGE: &'static str = "xmtp.mls.message_contents"; - fn full_name() -> ::prost::alloc::string::String { - "xmtp.mls.message_contents.PlaintextCommitLogEntry".into() - } - fn type_url() -> ::prost::alloc::string::String { - "/xmtp.mls.message_contents.PlaintextCommitLogEntry".into() - } -} -#[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] -pub struct CommitLogEntry { - #[prost(uint64, tag = "1")] - pub sequence_id: u64, - #[prost(bytes = "vec", tag = "2")] - pub serialized_commit_log_entry: ::prost::alloc::vec::Vec, - #[prost(message, optional, tag = "3")] - pub signature: ::core::option::Option< - super::super::identity::associations::RecoverableEd25519Signature, - >, -} -impl ::prost::Name for CommitLogEntry { - const NAME: &'static str = "CommitLogEntry"; - const PACKAGE: &'static str = "xmtp.mls.message_contents"; - fn full_name() -> ::prost::alloc::string::String { - "xmtp.mls.message_contents.CommitLogEntry".into() - } - fn type_url() -> ::prost::alloc::string::String { - "/xmtp.mls.message_contents.CommitLogEntry".into() - } -} -#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, ::prost::Enumeration)] -#[repr(i32)] -pub enum CommitResult { - Unspecified = 0, - Applied = 1, - WrongEpoch = 2, - Undecryptable = 3, - Invalid = 4, -} -impl CommitResult { - /// String value of the enum field names used in the ProtoBuf definition. - /// - /// The values are not transformed in any way and thus are considered stable - /// (if the ProtoBuf definition does not change) and safe for programmatic use. - pub fn as_str_name(&self) -> &'static str { - match self { - Self::Unspecified => "COMMIT_RESULT_UNSPECIFIED", - Self::Applied => "COMMIT_RESULT_APPLIED", - Self::WrongEpoch => "COMMIT_RESULT_WRONG_EPOCH", - Self::Undecryptable => "COMMIT_RESULT_UNDECRYPTABLE", - Self::Invalid => "COMMIT_RESULT_INVALID", - } - } - /// Creates an enum from field names used in the ProtoBuf definition. - pub fn from_str_name(value: &str) -> ::core::option::Option { - match value { - "COMMIT_RESULT_UNSPECIFIED" => Some(Self::Unspecified), - "COMMIT_RESULT_APPLIED" => Some(Self::Applied), - "COMMIT_RESULT_WRONG_EPOCH" => Some(Self::WrongEpoch), - "COMMIT_RESULT_UNDECRYPTABLE" => Some(Self::Undecryptable), - "COMMIT_RESULT_INVALID" => Some(Self::Invalid), - _ => None, - } - } -} /// A WelcomePointer is used to point to the welcome message for several installations at once to save overhead #[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] pub struct WelcomePointer { @@ -275,1047 +186,1502 @@ impl WelcomeWrapperAlgorithm { } } } -/// Message for group mutable metadata -#[derive(Clone, PartialEq, ::prost::Message)] -pub struct GroupMutablePermissionsV1 { - #[prost(message, optional, tag = "1")] - pub policies: ::core::option::Option, +/// A symmetric AEAD key. In every v1 envelope this is a 32-byte +/// ChaCha20Poly1305 key. The protobuf submessage does not itself constrain +/// length, so validators and setters MUST check that `material` is exactly +/// 32 bytes for v1; empty or wrong-length `material` is invalid. The +/// algorithm is fixed by the enclosing envelope version, not carried here — +/// a new algorithm is a new envelope version, never an in-message tag that +/// could disagree with the version. +#[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] +pub struct SymmetricKey { + #[prost(bytes = "vec", tag = "1")] + pub material: ::prost::alloc::vec::Vec, } -impl ::prost::Name for GroupMutablePermissionsV1 { - const NAME: &'static str = "GroupMutablePermissionsV1"; +impl ::prost::Name for SymmetricKey { + const NAME: &'static str = "SymmetricKey"; const PACKAGE: &'static str = "xmtp.mls.message_contents"; fn full_name() -> ::prost::alloc::string::String { - "xmtp.mls.message_contents.GroupMutablePermissionsV1".into() + "xmtp.mls.message_contents.SymmetricKey".into() } fn type_url() -> ::prost::alloc::string::String { - "/xmtp.mls.message_contents.GroupMutablePermissionsV1".into() - } -} -/// The set of policies that govern the group -#[derive(Clone, PartialEq, ::prost::Message)] -pub struct PolicySet { - #[prost(message, optional, tag = "1")] - pub add_member_policy: ::core::option::Option, - #[prost(message, optional, tag = "2")] - pub remove_member_policy: ::core::option::Option, - #[prost(map = "string, message", tag = "3")] - pub update_metadata_policy: ::std::collections::HashMap< - ::prost::alloc::string::String, - MetadataPolicy, - >, - #[prost(message, optional, tag = "4")] - pub add_admin_policy: ::core::option::Option, - #[prost(message, optional, tag = "5")] - pub remove_admin_policy: ::core::option::Option, - #[prost(message, optional, tag = "6")] - pub update_permissions_policy: ::core::option::Option, + "/xmtp.mls.message_contents.SymmetricKey".into() + } +} +/// A digest of MLS group state at a single epoch. The hash function is the +/// one bound to the group's MLS ciphersuite (one ciphersuite per group, so +/// the algorithm is never ambiguous and is not carried on the wire). v1 +/// pins the preimage precisely: the TLS-serialized `GroupContext` of the +/// referenced epoch — which itself binds `epoch`, `tree_hash`, and +/// `confirmed_transcript_hash`. MLS is deterministic, so every member at an +/// epoch derives an identical `GroupContext` and therefore an identical +/// digest; equal digests mean identical group state. Like SymmetricKey, the +/// submessage does not constrain length: validators and setters MUST check +/// that `digest` is exactly the ciphersuite's hash output length (32 bytes +/// under XMTP's current ciphersuite); any other length is invalid. +#[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] +pub struct GroupStateHash { + #[prost(bytes = "vec", tag = "1")] + pub digest: ::prost::alloc::vec::Vec, } -impl ::prost::Name for PolicySet { - const NAME: &'static str = "PolicySet"; +impl ::prost::Name for GroupStateHash { + const NAME: &'static str = "GroupStateHash"; const PACKAGE: &'static str = "xmtp.mls.message_contents"; fn full_name() -> ::prost::alloc::string::String { - "xmtp.mls.message_contents.PolicySet".into() + "xmtp.mls.message_contents.GroupStateHash".into() } fn type_url() -> ::prost::alloc::string::String { - "/xmtp.mls.message_contents.PolicySet".into() + "/xmtp.mls.message_contents.GroupStateHash".into() } } -/// A policy that governs adding/removing members or installations -#[derive(Clone, PartialEq, ::prost::Message)] -pub struct MembershipPolicy { - #[prost(oneof = "membership_policy::Kind", tags = "1, 2, 3")] - pub kind: ::core::option::Option, +/// Where the EncryptedGroupInfoBlob is hosted. A typed transport with a +/// validated happy path plus an application-defined escape hatch. Lives in +/// the invite payload (the QR) and, optionally, in +/// EXTERNAL_COMMIT_POLICY.refresh_pointers so members can keep slots fresh. +/// Exactly one `location` variant MUST be set; a ServicePointer with no +/// variant set (the empty oneof) gives the joiner no fetch target and MUST +/// be treated as a parse failure (fail-closed), like an unrecognized +/// version. +#[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] +pub struct ServicePointer { + #[prost(oneof = "service_pointer::Location", tags = "1, 2")] + pub location: ::core::option::Option, } -/// Nested message and enum types in `MembershipPolicy`. -pub mod membership_policy { - /// Combine multiple policies. All must evaluate to true - #[derive(Clone, PartialEq, ::prost::Message)] - pub struct AndCondition { - #[prost(message, repeated, tag = "1")] - pub policies: ::prost::alloc::vec::Vec, - } - impl ::prost::Name for AndCondition { - const NAME: &'static str = "AndCondition"; - const PACKAGE: &'static str = "xmtp.mls.message_contents"; - fn full_name() -> ::prost::alloc::string::String { - "xmtp.mls.message_contents.MembershipPolicy.AndCondition".into() - } - fn type_url() -> ::prost::alloc::string::String { - "/xmtp.mls.message_contents.MembershipPolicy.AndCondition".into() - } - } - /// Combine multiple policies. Any must evaluate to true - #[derive(Clone, PartialEq, ::prost::Message)] - pub struct AnyCondition { - #[prost(message, repeated, tag = "1")] - pub policies: ::prost::alloc::vec::Vec, - } - impl ::prost::Name for AnyCondition { - const NAME: &'static str = "AnyCondition"; - const PACKAGE: &'static str = "xmtp.mls.message_contents"; - fn full_name() -> ::prost::alloc::string::String { - "xmtp.mls.message_contents.MembershipPolicy.AnyCondition".into() - } - fn type_url() -> ::prost::alloc::string::String { - "/xmtp.mls.message_contents.MembershipPolicy.AnyCondition".into() - } - } - /// Base policy - #[derive( - Clone, - Copy, - Debug, - PartialEq, - Eq, - Hash, - PartialOrd, - Ord, - ::prost::Enumeration - )] - #[repr(i32)] - pub enum BasePolicy { - Unspecified = 0, - Allow = 1, - Deny = 2, - AllowIfAdminOrSuperAdmin = 3, - AllowIfSuperAdmin = 4, - } - impl BasePolicy { - /// String value of the enum field names used in the ProtoBuf definition. - /// - /// The values are not transformed in any way and thus are considered stable - /// (if the ProtoBuf definition does not change) and safe for programmatic use. - pub fn as_str_name(&self) -> &'static str { - match self { - Self::Unspecified => "BASE_POLICY_UNSPECIFIED", - Self::Allow => "BASE_POLICY_ALLOW", - Self::Deny => "BASE_POLICY_DENY", - Self::AllowIfAdminOrSuperAdmin => { - "BASE_POLICY_ALLOW_IF_ADMIN_OR_SUPER_ADMIN" - } - Self::AllowIfSuperAdmin => "BASE_POLICY_ALLOW_IF_SUPER_ADMIN", - } - } - /// Creates an enum from field names used in the ProtoBuf definition. - pub fn from_str_name(value: &str) -> ::core::option::Option { - match value { - "BASE_POLICY_UNSPECIFIED" => Some(Self::Unspecified), - "BASE_POLICY_ALLOW" => Some(Self::Allow), - "BASE_POLICY_DENY" => Some(Self::Deny), - "BASE_POLICY_ALLOW_IF_ADMIN_OR_SUPER_ADMIN" => { - Some(Self::AllowIfAdminOrSuperAdmin) - } - "BASE_POLICY_ALLOW_IF_SUPER_ADMIN" => Some(Self::AllowIfSuperAdmin), - _ => None, - } - } - } - #[derive(Clone, PartialEq, ::prost::Oneof)] - pub enum Kind { - #[prost(enumeration = "BasePolicy", tag = "1")] - Base(i32), - #[prost(message, tag = "2")] - AndCondition(AndCondition), - #[prost(message, tag = "3")] - AnyCondition(AnyCondition), +/// Nested message and enum types in `ServicePointer`. +pub mod service_pointer { + #[derive(Clone, PartialEq, Eq, Hash, ::prost::Oneof)] + pub enum Location { + /// RFC 3986 https URI. libxmtp parses and syntactically validates it. + /// Clients MUST require the https scheme, MUST NOT follow redirects to + /// other schemes, and MUST ignore credentials embedded in the URL. + #[prost(string, tag = "1")] + HttpsUrl(::prost::alloc::string::String), + /// Application-defined opaque bytes for non-URL transports (NFC tags, + /// custom resolver schemes, etc.). Opaque to libxmtp. + #[prost(bytes, tag = "2")] + Opaque(::prost::alloc::vec::Vec), } } -impl ::prost::Name for MembershipPolicy { - const NAME: &'static str = "MembershipPolicy"; +impl ::prost::Name for ServicePointer { + const NAME: &'static str = "ServicePointer"; const PACKAGE: &'static str = "xmtp.mls.message_contents"; fn full_name() -> ::prost::alloc::string::String { - "xmtp.mls.message_contents.MembershipPolicy".into() + "xmtp.mls.message_contents.ServicePointer".into() } fn type_url() -> ::prost::alloc::string::String { - "/xmtp.mls.message_contents.MembershipPolicy".into() + "/xmtp.mls.message_contents.ServicePointer".into() } } -/// A policy that governs updating metadata -#[derive(Clone, PartialEq, ::prost::Message)] -pub struct MetadataPolicy { - #[prost(oneof = "metadata_policy::Kind", tags = "1, 2, 3")] - pub kind: ::core::option::Option, -} -/// Nested message and enum types in `MetadataPolicy`. -pub mod metadata_policy { - /// Combine multiple policies. All must evaluate to true - #[derive(Clone, PartialEq, ::prost::Message)] - pub struct AndCondition { - #[prost(message, repeated, tag = "1")] - pub policies: ::prost::alloc::vec::Vec, - } - impl ::prost::Name for AndCondition { - const NAME: &'static str = "AndCondition"; - const PACKAGE: &'static str = "xmtp.mls.message_contents"; - fn full_name() -> ::prost::alloc::string::String { - "xmtp.mls.message_contents.MetadataPolicy.AndCondition".into() - } - fn type_url() -> ::prost::alloc::string::String { - "/xmtp.mls.message_contents.MetadataPolicy.AndCondition".into() - } - } - /// Combine multiple policies. Any must evaluate to true - #[derive(Clone, PartialEq, ::prost::Message)] - pub struct AnyCondition { - #[prost(message, repeated, tag = "1")] - pub policies: ::prost::alloc::vec::Vec, +/// v1 shape of the shareable invite payload for QR-code or link-based +/// joining of an XMTP group via an MLS external commit. +#[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] +pub struct ExternalInvitePayloadV1 { + /// Typed pointer to where the encrypted blob is hosted (HTTPS URL or + /// application-defined opaque bytes). Per-QR; not stored in group state. + /// + /// MAY be ABSENT: an application whose scanner already knows how to + /// reach its service (a first-party reader with a baked-in endpoint) + /// omits the pointer entirely and resolves the service out-of-band. + /// This keeps the fetch target out of the QR — a leaked payload then + /// reveals no service location. When the field IS present, exactly one + /// `location` variant MUST be set (see ServicePointer): present-but- + /// empty is a parse failure; absent means application-resolved. + #[prost(message, optional, tag = "1")] + pub service_pointer: ::core::option::Option, + /// Identifier for the service slot holding the encrypted blob. Format + /// is application-defined (UUID, snowflake, short slot key, etc.) and + /// opaque to libxmtp; the only constraint is that the value is unique + /// within the chosen service. Decoupled from the MLS group_id — + /// rotation may keep this stable (overwrite the same slot) or change + /// it (new slot on the service); the admin chooses per invite. + /// + /// MUST be at least 4 bytes (collision-avoidance floor for tiny + /// services). RECOMMENDED: 16 random bytes when no application- + /// specific scheme is in use. Maximum length is not capped by the + /// protocol; applications should bound it to fit their QR / link + /// transport. + /// + /// After joining, the joiner verifies this matches + /// `EXTERNAL_COMMIT_POLICY.external_group_id` in the group state as + /// defense-in-depth against a stale or swapped QR. + #[prost(bytes = "vec", tag = "2")] + pub external_group_id: ::prost::alloc::vec::Vec, + /// ChaCha20Poly1305 key (32 bytes in v1) used to wrap the GroupInfo. + /// Matches `EXTERNAL_COMMIT_POLICY.symmetric_key` in the group state. + #[prost(message, optional, tag = "3")] + pub symmetric_key: ::core::option::Option, +} +impl ::prost::Name for ExternalInvitePayloadV1 { + const NAME: &'static str = "ExternalInvitePayloadV1"; + const PACKAGE: &'static str = "xmtp.mls.message_contents"; + fn full_name() -> ::prost::alloc::string::String { + "xmtp.mls.message_contents.ExternalInvitePayloadV1".into() + } + fn type_url() -> ::prost::alloc::string::String { + "/xmtp.mls.message_contents.ExternalInvitePayloadV1".into() + } +} +/// Versioned envelope for the shareable invite payload. The application +/// embeds the serialized bytes in whatever transport it prefers (hex, +/// base64, raw QR, NFC, etc.) and stores the corresponding +/// EncryptedGroupInfoBlob on an external service keyed by the v1 payload's +/// `external_group_id`. +/// +/// New wire-format variants are added as new oneof entries; readers that +/// don't recognize a variant treat the invite as unparseable and fail +/// closed (no implicit downgrade). +#[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] +pub struct ExternalInvitePayload { + #[prost(oneof = "external_invite_payload::Version", tags = "1")] + pub version: ::core::option::Option, +} +/// Nested message and enum types in `ExternalInvitePayload`. +pub mod external_invite_payload { + #[derive(Clone, PartialEq, Eq, Hash, ::prost::Oneof)] + pub enum Version { + #[prost(message, tag = "1")] + V1(super::ExternalInvitePayloadV1), + } +} +impl ::prost::Name for ExternalInvitePayload { + const NAME: &'static str = "ExternalInvitePayload"; + const PACKAGE: &'static str = "xmtp.mls.message_contents"; + fn full_name() -> ::prost::alloc::string::String { + "xmtp.mls.message_contents.ExternalInvitePayload".into() + } + fn type_url() -> ::prost::alloc::string::String { + "/xmtp.mls.message_contents.ExternalInvitePayload".into() + } +} +/// v1 shape of the encrypted-GroupInfo envelope. +/// +/// `epoch` and `group_state_hash` are plaintext metadata. They are NOT a +/// trust anchor for the service — both are uploader-asserted, so a +/// conformant service MUST NOT use them to evict entries or "pick a +/// winner" (doing so turns a forged high `epoch` into a permanent slot +/// wedge). A conformant service instead retains the most recent N uploads +/// (RECOMMENDED N >= 4), evicting by arrival order (FIFO), and coalesces +/// only byte-identical re-uploads (equal across ALL fields — the plaintext +/// metadata alone is attacker-copyable). The roles of the metadata are +/// joiner-side: +/// +/// * `epoch` lets the joiner sort the retained candidates and prefer +/// the freshest, and lets the joiner reject a blob whose claimed +/// epoch disagrees with the GroupInfo it decrypts. +/// +/// * `group_state_hash` lets the joiner confirm, after decrypting, that +/// the blob's metadata matches the wrapped GroupInfo, and lets a +/// member recognize an idempotent re-upload. +/// +/// The trust anchor is the joiner's key + MLS validation, not the +/// metadata: the joiner AEAD-decrypts each candidate and parses it to a +/// GroupInfo whose internal epoch and `digest(GroupContext)` MUST equal +/// the blob's `epoch` and `group_state_hash` before it will join. A blob +/// that fails either check is discarded. +/// +/// All plaintext metadata is additionally bound into the AEAD as +/// associated data. v1 AAD = `epoch` || `expires_at_ns` (each 8-byte +/// big-endian) || `group_state_hash.digest`. A writer without the key +/// therefore cannot mutate any metadata field of a genuine ciphertext — +/// re-uploading a captured blob with, say, an extended `expires_at_ns` +/// fails the unwrap. A key-holder can still re-mint a blob with arbitrary +/// metadata; the post-decrypt consistency check and the validator-side +/// policy bounds cover that case. +#[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] +pub struct EncryptedGroupInfoBlobV1 { + /// 12 bytes; ChaCha20Poly1305 nonce specific to this ciphertext. MUST + /// be generated uniformly at random from a cryptographically secure + /// source for every encryption; deterministic (counter-based) schemes + /// are forbidden. Many independent writers encrypt under the same + /// long-lived key (every joiner refreshes the blob), and two writers' + /// counters colliding would reuse a nonce — which in ChaCha20Poly1305 + /// reuses the cipher stream and leaks the Poly1305 forgery key for + /// that nonce. + #[prost(bytes = "vec", tag = "1")] + pub nonce: ::prost::alloc::vec::Vec, + /// wrap_payload_symmetric output: AEAD ciphertext over the serialized + /// MlsMessageOut(GroupInfo), with the v1 AAD (see the message comment). + #[prost(bytes = "vec", tag = "2")] + pub ciphertext: ::prost::alloc::vec::Vec, + /// MLS group epoch of the wrapped GroupInfo. Plaintext, uploader- + /// asserted. Used by the joiner to prefer the freshest candidate and to + /// consistency-check the decrypted GroupInfo; NOT used by the service + /// to order or evict. Joiner verifies against the decrypted GroupInfo + /// before joining. + #[prost(uint64, tag = "3")] + pub epoch: u64, + /// Digest of the wrapped GroupInfo's epoch state (see GroupStateHash: a + /// digest over the canonical `GroupContext` under the group's MLS + /// ciphersuite). Plaintext. The joiner verifies it equals + /// `digest(GroupContext)` of the decrypted GroupInfo; a member uses it + /// to recognize an idempotent re-upload. Not used for service ordering. + #[prost(message, optional, tag = "4")] + pub group_state_hash: ::core::option::Option, + /// Wall-clock expiry of this blob, in nanoseconds since UNIX epoch. + /// 0 means no expiry. The service uses this as a TTL hint and MAY + /// garbage-collect blobs past their `expires_at_ns` autonomously. + /// The joining client also enforces this — refuses to join from an + /// expired blob even if the service is still serving it. + /// + /// This is the blob's EFFECTIVE expiry, computed by the uploader as the + /// earlier of the two policy bounds that apply at wrap time (saturating; + /// a bound of 0 means "no bound" and drops out of the min): + /// + /// min(EXTERNAL_COMMIT_POLICY.expires_at_ns, // campaign end + /// epoch_start_ns + EXTERNAL_COMMIT_POLICY.expire_in_ns) + /// + /// where epoch_start_ns is the delivery-service envelope timestamp of + /// the commit that began the wrapped GroupInfo's epoch (known to every + /// uploader). Folding the staleness bound in here means the joiner's + /// single expiry check also skips candidates that validators would + /// reject as stale — avoiding a "zombie join" where the joiner + /// publishes a commit every member rejects and believes it joined a + /// group that never accepted it — and the service's TTL-based GC + /// naturally collects staleness-dead blobs. 0 only when neither policy + /// bound is set. + /// + /// AAD-bound: a writer without the key cannot alter a genuine blob's + /// expiry (see the message comment). A key-holder can re-mint with a + /// later value, so this is a TTL / UX bound, not a security bound + /// against key-holders — the authoritative bounds are the policy + /// fields, enforced by validators against envelope timestamps. + #[prost(uint64, tag = "5")] + pub expires_at_ns: u64, +} +impl ::prost::Name for EncryptedGroupInfoBlobV1 { + const NAME: &'static str = "EncryptedGroupInfoBlobV1"; + const PACKAGE: &'static str = "xmtp.mls.message_contents"; + fn full_name() -> ::prost::alloc::string::String { + "xmtp.mls.message_contents.EncryptedGroupInfoBlobV1".into() + } + fn type_url() -> ::prost::alloc::string::String { + "/xmtp.mls.message_contents.EncryptedGroupInfoBlobV1".into() + } +} +/// Versioned envelope wrapping a single GroupInfo TLS-serialized bytes +/// under an AEAD scheme (ChaCha20Poly1305 in v1) with a fresh random nonce +/// per re-encryption and all plaintext metadata bound as associated data +/// (see EncryptedGroupInfoBlobV1). Stored on the external service; joiners +/// upload a refreshed blob (fresh nonce) after each successful join, and +/// the slot retains the most recent uploads. +/// +/// New variants represent breaking wire-format changes (different AEAD, +/// different metadata layout). Readers that don't recognize a variant +/// fail closed — the joiner cannot attempt MLS state transitions against +/// a blob it can't validate. +#[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] +pub struct EncryptedGroupInfoBlob { + #[prost(oneof = "encrypted_group_info_blob::Version", tags = "1")] + pub version: ::core::option::Option, +} +/// Nested message and enum types in `EncryptedGroupInfoBlob`. +pub mod encrypted_group_info_blob { + #[derive(Clone, PartialEq, Eq, Hash, ::prost::Oneof)] + pub enum Version { + #[prost(message, tag = "1")] + V1(super::EncryptedGroupInfoBlobV1), + } +} +impl ::prost::Name for EncryptedGroupInfoBlob { + const NAME: &'static str = "EncryptedGroupInfoBlob"; + const PACKAGE: &'static str = "xmtp.mls.message_contents"; + fn full_name() -> ::prost::alloc::string::String { + "xmtp.mls.message_contents.EncryptedGroupInfoBlob".into() + } + fn type_url() -> ::prost::alloc::string::String { + "/xmtp.mls.message_contents.EncryptedGroupInfoBlob".into() + } +} +/// Message for group mutable metadata +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct GroupMutableMetadataV1 { + /// Map to store various metadata attributes (Group name, etc.) + #[prost(map = "string, string", tag = "1")] + pub attributes: ::std::collections::HashMap< + ::prost::alloc::string::String, + ::prost::alloc::string::String, + >, + #[prost(message, optional, tag = "2")] + pub admin_list: ::core::option::Option, + /// Creator starts as only super_admin + /// Only super_admin can add/remove other super_admin + #[prost(message, optional, tag = "3")] + pub super_admin_list: ::core::option::Option, +} +impl ::prost::Name for GroupMutableMetadataV1 { + const NAME: &'static str = "GroupMutableMetadataV1"; + const PACKAGE: &'static str = "xmtp.mls.message_contents"; + fn full_name() -> ::prost::alloc::string::String { + "xmtp.mls.message_contents.GroupMutableMetadataV1".into() + } + fn type_url() -> ::prost::alloc::string::String { + "/xmtp.mls.message_contents.GroupMutableMetadataV1".into() + } +} +/// Wrapper around a list of repeated Inbox Ids +#[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] +pub struct Inboxes { + #[prost(string, repeated, tag = "1")] + pub inbox_ids: ::prost::alloc::vec::Vec<::prost::alloc::string::String>, +} +impl ::prost::Name for Inboxes { + const NAME: &'static str = "Inboxes"; + const PACKAGE: &'static str = "xmtp.mls.message_contents"; + fn full_name() -> ::prost::alloc::string::String { + "xmtp.mls.message_contents.Inboxes".into() + } + fn type_url() -> ::prost::alloc::string::String { + "/xmtp.mls.message_contents.Inboxes".into() + } +} +/// PlaintextCommitLogEntry indicates whether a commit was successful or not, +/// when applied on top of the indicated `last_epoch_authenticator`. +#[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] +pub struct PlaintextCommitLogEntry { + /// The group_id of the group that the commit belongs to. + #[prost(bytes = "vec", tag = "1")] + pub group_id: ::prost::alloc::vec::Vec, + /// The sequence ID of the commit payload being validated. + #[prost(uint64, tag = "2")] + pub commit_sequence_id: u64, + /// The encryption state before the commit was applied. + #[prost(bytes = "vec", tag = "3")] + pub last_epoch_authenticator: ::prost::alloc::vec::Vec, + /// Indicates whether the commit was successful, or why it failed. + #[prost(enumeration = "CommitResult", tag = "4")] + pub commit_result: i32, + /// The epoch number after the commit was applied, if successful. + #[prost(uint64, tag = "5")] + pub applied_epoch_number: u64, + /// The encryption state after the commit was applied, if successful. + #[prost(bytes = "vec", tag = "6")] + pub applied_epoch_authenticator: ::prost::alloc::vec::Vec, +} +impl ::prost::Name for PlaintextCommitLogEntry { + const NAME: &'static str = "PlaintextCommitLogEntry"; + const PACKAGE: &'static str = "xmtp.mls.message_contents"; + fn full_name() -> ::prost::alloc::string::String { + "xmtp.mls.message_contents.PlaintextCommitLogEntry".into() + } + fn type_url() -> ::prost::alloc::string::String { + "/xmtp.mls.message_contents.PlaintextCommitLogEntry".into() + } +} +#[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] +pub struct CommitLogEntry { + #[prost(uint64, tag = "1")] + pub sequence_id: u64, + #[prost(bytes = "vec", tag = "2")] + pub serialized_commit_log_entry: ::prost::alloc::vec::Vec, + #[prost(message, optional, tag = "3")] + pub signature: ::core::option::Option< + super::super::identity::associations::RecoverableEd25519Signature, + >, +} +impl ::prost::Name for CommitLogEntry { + const NAME: &'static str = "CommitLogEntry"; + const PACKAGE: &'static str = "xmtp.mls.message_contents"; + fn full_name() -> ::prost::alloc::string::String { + "xmtp.mls.message_contents.CommitLogEntry".into() + } + fn type_url() -> ::prost::alloc::string::String { + "/xmtp.mls.message_contents.CommitLogEntry".into() + } +} +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, ::prost::Enumeration)] +#[repr(i32)] +pub enum CommitResult { + Unspecified = 0, + Applied = 1, + WrongEpoch = 2, + Undecryptable = 3, + Invalid = 4, +} +impl CommitResult { + /// String value of the enum field names used in the ProtoBuf definition. + /// + /// The values are not transformed in any way and thus are considered stable + /// (if the ProtoBuf definition does not change) and safe for programmatic use. + pub fn as_str_name(&self) -> &'static str { + match self { + Self::Unspecified => "COMMIT_RESULT_UNSPECIFIED", + Self::Applied => "COMMIT_RESULT_APPLIED", + Self::WrongEpoch => "COMMIT_RESULT_WRONG_EPOCH", + Self::Undecryptable => "COMMIT_RESULT_UNDECRYPTABLE", + Self::Invalid => "COMMIT_RESULT_INVALID", + } + } + /// Creates an enum from field names used in the ProtoBuf definition. + pub fn from_str_name(value: &str) -> ::core::option::Option { + match value { + "COMMIT_RESULT_UNSPECIFIED" => Some(Self::Unspecified), + "COMMIT_RESULT_APPLIED" => Some(Self::Applied), + "COMMIT_RESULT_WRONG_EPOCH" => Some(Self::WrongEpoch), + "COMMIT_RESULT_UNDECRYPTABLE" => Some(Self::Undecryptable), + "COMMIT_RESULT_INVALID" => Some(Self::Invalid), + _ => None, + } + } +} +/// Extension data for proposal support in group context. +/// When present in the group context extensions, indicates the group +/// uses proposal-by-reference flow. +#[derive(Clone, Copy, PartialEq, Eq, Hash, ::prost::Message)] +pub struct ProposalSupport { + #[prost(uint32, tag = "1")] + pub version: u32, +} +impl ::prost::Name for ProposalSupport { + const NAME: &'static str = "ProposalSupport"; + const PACKAGE: &'static str = "xmtp.mls.message_contents"; + fn full_name() -> ::prost::alloc::string::String { + "xmtp.mls.message_contents.ProposalSupport".into() + } + fn type_url() -> ::prost::alloc::string::String { + "/xmtp.mls.message_contents.ProposalSupport".into() + } +} +/// ContentTypeId is used to identify the type of content stored in a Message. +#[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] +pub struct ContentTypeId { + /// authority governing this content type + #[prost(string, tag = "1")] + pub authority_id: ::prost::alloc::string::String, + /// type identifier + #[prost(string, tag = "2")] + pub type_id: ::prost::alloc::string::String, + /// major version of the type + #[prost(uint32, tag = "3")] + pub version_major: u32, + /// minor version of the type + #[prost(uint32, tag = "4")] + pub version_minor: u32, +} +impl ::prost::Name for ContentTypeId { + const NAME: &'static str = "ContentTypeId"; + const PACKAGE: &'static str = "xmtp.mls.message_contents"; + fn full_name() -> ::prost::alloc::string::String { + "xmtp.mls.message_contents.ContentTypeId".into() + } + fn type_url() -> ::prost::alloc::string::String { + "/xmtp.mls.message_contents.ContentTypeId".into() + } +} +/// EncodedContent bundles the content with metadata identifying its type +/// and parameters required for correct decoding and presentation of the content. +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct EncodedContent { + /// content type identifier used to match the payload with + /// the correct decoding machinery + #[prost(message, optional, tag = "1")] + pub r#type: ::core::option::Option, + /// optional encoding parameters required to correctly decode the content + #[prost(map = "string, string", tag = "2")] + pub parameters: ::std::collections::HashMap< + ::prost::alloc::string::String, + ::prost::alloc::string::String, + >, + /// optional fallback description of the content that can be used in case + /// the client cannot decode or render the content + #[prost(string, optional, tag = "3")] + pub fallback: ::core::option::Option<::prost::alloc::string::String>, + /// optional compression; the value indicates algorithm used to + /// compress the encoded content bytes + #[prost(enumeration = "Compression", optional, tag = "5")] + pub compression: ::core::option::Option, + /// encoded content itself + #[prost(bytes = "vec", tag = "4")] + pub content: ::prost::alloc::vec::Vec, +} +impl ::prost::Name for EncodedContent { + const NAME: &'static str = "EncodedContent"; + const PACKAGE: &'static str = "xmtp.mls.message_contents"; + fn full_name() -> ::prost::alloc::string::String { + "xmtp.mls.message_contents.EncodedContent".into() } - impl ::prost::Name for AnyCondition { - const NAME: &'static str = "AnyCondition"; + fn type_url() -> ::prost::alloc::string::String { + "/xmtp.mls.message_contents.EncodedContent".into() + } +} +/// A PlaintextEnvelope is the outermost payload that gets encrypted by MLS +#[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] +pub struct PlaintextEnvelope { + /// Selector which declares which version of the EncodedContent this + /// PlaintextEnvelope is + #[prost(oneof = "plaintext_envelope::Content", tags = "1, 2")] + pub content: ::core::option::Option, +} +/// Nested message and enum types in `PlaintextEnvelope`. +pub mod plaintext_envelope { + /// Version 1 of the encrypted envelope + #[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] + pub struct V1 { + /// Expected to be EncodedContent + #[prost(bytes = "vec", tag = "1")] + pub content: ::prost::alloc::vec::Vec, + /// A unique value that can be used to ensure that the same content can + /// produce different hashes. May be the sender timestamp. + #[prost(string, tag = "2")] + pub idempotency_key: ::prost::alloc::string::String, + } + impl ::prost::Name for V1 { + const NAME: &'static str = "V1"; const PACKAGE: &'static str = "xmtp.mls.message_contents"; fn full_name() -> ::prost::alloc::string::String { - "xmtp.mls.message_contents.MetadataPolicy.AnyCondition".into() + "xmtp.mls.message_contents.PlaintextEnvelope.V1".into() } fn type_url() -> ::prost::alloc::string::String { - "/xmtp.mls.message_contents.MetadataPolicy.AnyCondition".into() + "/xmtp.mls.message_contents.PlaintextEnvelope.V1".into() } } - /// Base policy - #[derive( - Clone, - Copy, - Debug, - PartialEq, - Eq, - Hash, - PartialOrd, - Ord, - ::prost::Enumeration - )] - #[repr(i32)] - pub enum MetadataBasePolicy { - Unspecified = 0, - Allow = 1, - Deny = 2, - AllowIfAdmin = 3, - AllowIfSuperAdmin = 4, + /// Version 2 of the encrypted envelope + #[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] + pub struct V2 { + /// A unique value that can be used to ensure that the same content can + /// produce different hashes. May be the sender timestamp. + #[prost(string, tag = "1")] + pub idempotency_key: ::prost::alloc::string::String, + #[prost(oneof = "v2::MessageType", tags = "2, 3, 4, 5")] + pub message_type: ::core::option::Option, } - impl MetadataBasePolicy { - /// String value of the enum field names used in the ProtoBuf definition. - /// - /// The values are not transformed in any way and thus are considered stable - /// (if the ProtoBuf definition does not change) and safe for programmatic use. - pub fn as_str_name(&self) -> &'static str { - match self { - Self::Unspecified => "METADATA_BASE_POLICY_UNSPECIFIED", - Self::Allow => "METADATA_BASE_POLICY_ALLOW", - Self::Deny => "METADATA_BASE_POLICY_DENY", - Self::AllowIfAdmin => "METADATA_BASE_POLICY_ALLOW_IF_ADMIN", - Self::AllowIfSuperAdmin => "METADATA_BASE_POLICY_ALLOW_IF_SUPER_ADMIN", - } + /// Nested message and enum types in `V2`. + pub mod v2 { + #[derive(Clone, PartialEq, Eq, Hash, ::prost::Oneof)] + pub enum MessageType { + /// Expected to be EncodedContent + #[prost(bytes, tag = "2")] + Content(::prost::alloc::vec::Vec), + /// Initiator sends a request to receive sync payload + #[prost(message, tag = "3")] + DeviceSyncRequest( + super::super::super::super::device_sync::content::DeviceSyncRequest, + ), + /// Some other authorized installation sends a reply with a link to payload + #[prost(message, tag = "4")] + DeviceSyncReply( + super::super::super::super::device_sync::content::DeviceSyncReply, + ), + /// A serialized user preference update + #[prost(message, tag = "5")] + UserPreferenceUpdate( + super::super::super::super::device_sync::content::V1UserPreferenceUpdate, + ), } - /// Creates an enum from field names used in the ProtoBuf definition. - pub fn from_str_name(value: &str) -> ::core::option::Option { - match value { - "METADATA_BASE_POLICY_UNSPECIFIED" => Some(Self::Unspecified), - "METADATA_BASE_POLICY_ALLOW" => Some(Self::Allow), - "METADATA_BASE_POLICY_DENY" => Some(Self::Deny), - "METADATA_BASE_POLICY_ALLOW_IF_ADMIN" => Some(Self::AllowIfAdmin), - "METADATA_BASE_POLICY_ALLOW_IF_SUPER_ADMIN" => { - Some(Self::AllowIfSuperAdmin) - } - _ => None, - } + } + impl ::prost::Name for V2 { + const NAME: &'static str = "V2"; + const PACKAGE: &'static str = "xmtp.mls.message_contents"; + fn full_name() -> ::prost::alloc::string::String { + "xmtp.mls.message_contents.PlaintextEnvelope.V2".into() + } + fn type_url() -> ::prost::alloc::string::String { + "/xmtp.mls.message_contents.PlaintextEnvelope.V2".into() } } - #[derive(Clone, PartialEq, ::prost::Oneof)] - pub enum Kind { - #[prost(enumeration = "MetadataBasePolicy", tag = "1")] - Base(i32), + /// Selector which declares which version of the EncodedContent this + /// PlaintextEnvelope is + #[derive(Clone, PartialEq, Eq, Hash, ::prost::Oneof)] + pub enum Content { + #[prost(message, tag = "1")] + V1(V1), #[prost(message, tag = "2")] - AndCondition(AndCondition), - #[prost(message, tag = "3")] - AnyCondition(AnyCondition), + V2(V2), } } -impl ::prost::Name for MetadataPolicy { - const NAME: &'static str = "MetadataPolicy"; +impl ::prost::Name for PlaintextEnvelope { + const NAME: &'static str = "PlaintextEnvelope"; const PACKAGE: &'static str = "xmtp.mls.message_contents"; fn full_name() -> ::prost::alloc::string::String { - "xmtp.mls.message_contents.MetadataPolicy".into() + "xmtp.mls.message_contents.PlaintextEnvelope".into() + } + fn type_url() -> ::prost::alloc::string::String { + "/xmtp.mls.message_contents.PlaintextEnvelope".into() + } +} +/// Recognized compression algorithms +/// protolint:disable ENUM_FIELD_NAMES_ZERO_VALUE_END_WITH +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, ::prost::Enumeration)] +#[repr(i32)] +pub enum Compression { + Deflate = 0, + Gzip = 1, +} +impl Compression { + /// String value of the enum field names used in the ProtoBuf definition. + /// + /// The values are not transformed in any way and thus are considered stable + /// (if the ProtoBuf definition does not change) and safe for programmatic use. + pub fn as_str_name(&self) -> &'static str { + match self { + Self::Deflate => "COMPRESSION_DEFLATE", + Self::Gzip => "COMPRESSION_GZIP", + } + } + /// Creates an enum from field names used in the ProtoBuf definition. + pub fn from_str_name(value: &str) -> ::core::option::Option { + match value { + "COMPRESSION_DEFLATE" => Some(Self::Deflate), + "COMPRESSION_GZIP" => Some(Self::Gzip), + _ => None, + } + } +} +/// v1 external-commit-policy payload. +/// +/// The fields split into two classes. DURABLE SETTINGS (`expire_in_ns`, +/// `max_uses`) describe the group's posture toward ANY invite and survive +/// a revoke. PER-INVITE fields (`symmetric_key`, `external_group_id`, +/// `expires_at_ns`, `refresh_pointers`) describe the currently-active +/// invite and are cleared by a revoke. +/// +/// Field-coupling invariants enforced by libxmtp when applying an +/// AppDataUpdate(EXTERNAL_COMMIT_POLICY) proposal: +/// +/// * When `allow_external_commit` transitions to true: `symmetric_key` +/// and `external_group_id` MUST be populated (meeting their length +/// requirements) in the same proposal. The transitions are atomic — +/// there is no window where the bit is on but the invite coordinates +/// are unset. `allow_external_commit = true` itself authorizes the +/// one write the atomic external-commit shape requires — the joiner +/// inserting its OWN GROUP_MEMBERSHIP entry. No permissions grant +/// exists for it: in every state validators accept, a grant's value +/// would be forced by this switch (enabled => must admit, disabled +/// => never consulted), so the enable stays a single-component +/// write with no cross-component coupling. +/// +/// * When `allow_external_commit` transitions to false (revoke): every +/// per-invite field MUST be ABSENT from the resulting policy — not +/// serialized at all. For the proto3 scalar and repeated fields this +/// is the only cleared state there is (defaults — empty bytes, 0, +/// empty list — are never serialized; "empty" and "absent" are the +/// same wire state). The message-typed `symmetric_key` is the one +/// field with explicit presence and therefore a second representable +/// state, which is forbidden: an empty SymmetricKey submessage (or +/// empty `material`) is invalid — absence is the only cleared +/// encoding. Net effect: a revoked policy serializes to nothing but +/// the durable settings, byte-identical to a policy that never had +/// an invite. Validators enforce this as a state invariant — +/// `allow_external_commit == false` implies all four absent — so +/// stale state cannot linger (a lingering key could be revived by a +/// careless re-enable; lingering pointers re-adopted; a lingering +/// absolute `expires_at_ns` would silently mis-bound the next +/// campaign) and a re-enable mints everything fresh. +/// +/// * On every enable (first or re-enable): the new `symmetric_key` +/// MUST be freshly generated from a cryptographically secure random +/// source. Uniform 256-bit randomness guarantees — up to negligible +/// probability — that it differs from every previously-used key, +/// with no key-history tracking; validators do not (and cannot) +/// audit this rule, so it binds the setter. Reusing a revoked key +/// would re-validate every QR ever printed under that key, defeating +/// the revocation. The new `external_group_id` SHOULD also differ. +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct ExternalCommitPolicyV1 { + /// Master switch for MLS External Commits adding new members. + /// Required for the QR-invite flow. Defaults to false; admins + /// (super-admin by default) opt in via + /// AppDataUpdate(EXTERNAL_COMMIT_POLICY). + /// + /// See the field-coupling invariants in the message-level comment + /// above: enabling MUST populate symmetric_key + external_group_id; + /// revoking (true → false) MUST leave every per-invite field absent. + #[prost(bool, tag = "1")] + pub allow_external_commit: bool, + /// Wall-clock auto-disable timestamp (ns since UNIX epoch). + /// 0 = no automatic expiry. After this timestamp the validator + /// rejects all external commits regardless of `allow_external_commit`. + /// Lets admins issue time-bounded invite campaigns without having to + /// come back and flip the bit manually. Validators evaluate this + /// against the external commit's delivery-service envelope timestamp — + /// never the validator's wall clock at processing time — so members + /// that sync at different times reach the same verdict. + /// + /// Per-invite, not a durable setting: revoking clears it to 0 — i.e. + /// absent on the wire; proto3 never serializes defaults (see the + /// field-coupling invariants — an absolute campaign end left behind + /// would silently mis-bound the next campaign). While enabled, 0 + /// remains a legal value (no automatic expiry). + #[prost(uint64, tag = "2")] + pub expires_at_ns: u64, + /// Maximum staleness of the GroupInfo referenced by an external + /// commit, in nanoseconds. 0 = no staleness limit. Validators reject + /// an external commit when its envelope timestamp minus the current + /// epoch's start timestamp exceeds this value (the epoch-start is the + /// envelope timestamp of the message by which the validator entered or + /// observed the current epoch — the epoch's commit, or the Welcome + /// published together with it). Narrows the replay window for + /// stolen-blob attacks and forces re-export frequency. A coarse bound: + /// SHOULD be set to values (minutes or more) for which publish-latency + /// skew across members is immaterial. + /// + /// Durable setting: survives a revoke (it describes the group's + /// staleness posture for any invite, not the current campaign). + #[prost(uint64, tag = "3")] + pub expire_in_ns: u64, + /// ChaCha20Poly1305 key (32 bytes in v1) used to wrap the + /// EncryptedGroupInfoBlob for the currently-active invite. Carried in + /// the group state so any member (especially a just-joined external + /// committer) can re-export GroupInfo and re-upload a refreshed blob + /// under the same key after a join — without this, a printed QR / link + /// would die the moment the issuing admin went offline. + /// + /// The QR carries the same key. Rotation = admin sets a new value here + /// in a single AppDataUpdate(EXTERNAL_COMMIT_POLICY) proposal AND + /// issues a new QR carrying the matching key; old QR holders' keys no + /// longer decrypt blobs wrapped after the rotation. + /// + /// `material` MUST be exactly 32 bytes when populated (v1); the + /// SymmetricKey submessage does not enforce this, so validators and + /// setters MUST check it. The field being ABSENT is the canonical "no + /// active invite" encoding (an empty submessage or empty `material` is + /// invalid) — and MUST coincide with `allow_external_commit == false` + /// (see the field-coupling invariants at the top of this message). + /// Revoking the invite MUST clear this field; re-enabling MUST + /// populate it with a value freshly generated from a cryptographically + /// secure random source — uniform randomness guarantees distinctness + /// from every prior key without any key-history tracking. + /// + /// Note: the per-QR service_pointer is application-defined and travels + /// in the QR — joiners use the pointer from the QR they scanned (a + /// scanner cannot read group state before decrypting the blob). + /// Members keep slots fresh via the optional `refresh_pointers` list + /// below. + #[prost(message, optional, tag = "4")] + pub symmetric_key: ::core::option::Option, + /// Identifier for the service slot holding the active invite's + /// encrypted blob. Application-defined opaque bytes (UUID, snowflake, + /// short slot key, etc.); decoupled from the MLS group_id. Admins + /// MAY rotate the symmetric_key while keeping this stable (overwrite + /// the same slot on the service) or change both together (new slot, + /// leaves the old slot orphaned for application-side GC). + /// + /// The QR carries the same value. The joiner verifies that the QR's + /// `external_group_id` equals this field after joining, as + /// defense-in-depth against a stale or swapped QR. Mismatch indicates + /// the admin rotated to a new slot after the QR was minted; the + /// joining client MUST NOT upload a refreshed blob on mismatch (it + /// would land on an orphaned slot, or be encrypted under a rotated + /// key). + /// + /// MUST be at least 4 bytes when populated (collision-avoidance floor + /// for tiny services). RECOMMENDED: 16 random bytes when no + /// application-specific scheme is in use. Absent means no active + /// invite (proto3 bytes: an empty value is never serialized, so + /// "empty" and "absent" are the same wire state) — and MUST coincide + /// with `allow_external_commit == false` (see the field-coupling + /// invariants at the top of this message). Revoking the invite MUST + /// clear this field; re-enabling SHOULD use a freshly-generated value + /// (reusing a prior `external_group_id` is permitted only when the + /// admin intends to overwrite the old service slot — typically the + /// admin generates a new value to leave the prior slot orphaned). + #[prost(bytes = "vec", tag = "5")] + pub external_group_id: ::prost::alloc::vec::Vec, + /// Maximum number of members concurrently admitted to the group via + /// the currently-active invite. 0 = unlimited; 1 = a single active + /// invited member at a time. + /// + /// Enforced by every validating member, NOT by the service. Each + /// external committer tags its own GROUP_MEMBERSHIP entry with the + /// `external_group_id` it joined under (see + /// GroupMembershipEntry.V1.admitted_via_external_group_id); the live + /// use-count is the number of current GROUP_MEMBERSHIP entries + /// carrying the active `external_group_id`. A member rejects an + /// external commit when that count is already >= max_uses. Because the + /// count is read from GROUP_MEMBERSHIP in the shared group state — not + /// replayed from commit history — every member, INCLUDING one that + /// joined after the invite was issued, computes the same value and + /// converges. No change to the atomic external-commit shape: the + /// joiner writes only its own GROUP_MEMBERSHIP entry. + /// + /// Semantics are CONCURRENT, not total-ever: removing an invited + /// member drops its entry and frees a slot. The count is scoped to + /// `external_group_id`, so rotating to a new slot id starts a fresh + /// budget (entries under the old id no longer count); a same-slot + /// `symmetric_key`-only rotation does NOT reset it. To make an invite + /// truly one-shot, revoke it (allow_external_commit = false) or rotate + /// the `external_group_id` after the join. + /// + /// max_uses is intentionally scoped per-invite (per- + /// `external_group_id`): it throttles a single invite/slot, by design. + /// It is NOT a cap on the total inboxes in the group; a cap on how + /// many inboxes may join a group is a SEPARATE setting, not max_uses. + /// Durable setting: survives a revoke. + #[prost(uint32, tag = "6")] + pub max_uses: u32, + /// Service locations members use to keep the active invite's blob + /// fresh. Optional. When populated, every member re-wraps and + /// re-uploads the blob after epoch advances (jittered, check-before- + /// write — see XIP-82 "Keeping the blob fresh") — the poster on the + /// wall stays joinable while any one member is online. When empty, + /// only the issuing admin and past scanners (who know a pointer from + /// their own QR) can refresh, or the application drives refresh itself + /// against application-resolved pointers; that mode preserves pointer + /// secrecy from group state at a liveness cost. + /// + /// Listing pointers here places fetch locations in group state: every + /// member — including every future removed member — learns them. The + /// per-QR `service_pointer` is unaffected: scanners cannot read group + /// state before decrypting the blob, so the QR (or the app's own + /// knowledge) always carries the fetch target for joining. + /// + /// Per-invite, not a durable setting: revoking clears the list — i.e. + /// absent on the wire; repeated fields have no separate presence, an + /// empty list is simply not serialized (see the field-coupling + /// invariants). Re-enabling SHOULD populate fresh locations. While + /// enabled, an empty list remains the legal opt-out of member-driven + /// refresh. + #[prost(message, repeated, tag = "7")] + pub refresh_pointers: ::prost::alloc::vec::Vec, +} +impl ::prost::Name for ExternalCommitPolicyV1 { + const NAME: &'static str = "ExternalCommitPolicyV1"; + const PACKAGE: &'static str = "xmtp.mls.message_contents"; + fn full_name() -> ::prost::alloc::string::String { + "xmtp.mls.message_contents.ExternalCommitPolicyV1".into() } fn type_url() -> ::prost::alloc::string::String { - "/xmtp.mls.message_contents.MetadataPolicy".into() + "/xmtp.mls.message_contents.ExternalCommitPolicyV1".into() } } -/// A policy that governs updating permissions +/// Versioned envelope. New variants are added as new oneof variants; +/// readers that don't recognize a variant treat the policy as default +/// (all fields zero) per the standard unknown-variant tolerance rules. #[derive(Clone, PartialEq, ::prost::Message)] -pub struct PermissionsUpdatePolicy { - #[prost(oneof = "permissions_update_policy::Kind", tags = "1, 2, 3")] - pub kind: ::core::option::Option, +pub struct ExternalCommitPolicyEntry { + #[prost(oneof = "external_commit_policy_entry::Version", tags = "1")] + pub version: ::core::option::Option, } -/// Nested message and enum types in `PermissionsUpdatePolicy`. -pub mod permissions_update_policy { - /// Combine multiple policies. All must evaluate to true - #[derive(Clone, PartialEq, ::prost::Message)] - pub struct AndCondition { - #[prost(message, repeated, tag = "1")] - pub policies: ::prost::alloc::vec::Vec, - } - impl ::prost::Name for AndCondition { - const NAME: &'static str = "AndCondition"; - const PACKAGE: &'static str = "xmtp.mls.message_contents"; - fn full_name() -> ::prost::alloc::string::String { - "xmtp.mls.message_contents.PermissionsUpdatePolicy.AndCondition".into() - } - fn type_url() -> ::prost::alloc::string::String { - "/xmtp.mls.message_contents.PermissionsUpdatePolicy.AndCondition".into() - } - } - /// Combine multiple policies. Any must evaluate to true - #[derive(Clone, PartialEq, ::prost::Message)] - pub struct AnyCondition { - #[prost(message, repeated, tag = "1")] - pub policies: ::prost::alloc::vec::Vec, - } - impl ::prost::Name for AnyCondition { - const NAME: &'static str = "AnyCondition"; - const PACKAGE: &'static str = "xmtp.mls.message_contents"; - fn full_name() -> ::prost::alloc::string::String { - "xmtp.mls.message_contents.PermissionsUpdatePolicy.AnyCondition".into() - } - fn type_url() -> ::prost::alloc::string::String { - "/xmtp.mls.message_contents.PermissionsUpdatePolicy.AnyCondition".into() - } - } - /// Base policy - #[derive( - Clone, - Copy, - Debug, - PartialEq, - Eq, - Hash, - PartialOrd, - Ord, - ::prost::Enumeration - )] - #[repr(i32)] - pub enum PermissionsBasePolicy { - Unspecified = 0, - Deny = 1, - AllowIfAdmin = 2, - AllowIfSuperAdmin = 3, - } - impl PermissionsBasePolicy { - /// String value of the enum field names used in the ProtoBuf definition. - /// - /// The values are not transformed in any way and thus are considered stable - /// (if the ProtoBuf definition does not change) and safe for programmatic use. - pub fn as_str_name(&self) -> &'static str { - match self { - Self::Unspecified => "PERMISSIONS_BASE_POLICY_UNSPECIFIED", - Self::Deny => "PERMISSIONS_BASE_POLICY_DENY", - Self::AllowIfAdmin => "PERMISSIONS_BASE_POLICY_ALLOW_IF_ADMIN", - Self::AllowIfSuperAdmin => "PERMISSIONS_BASE_POLICY_ALLOW_IF_SUPER_ADMIN", - } - } - /// Creates an enum from field names used in the ProtoBuf definition. - pub fn from_str_name(value: &str) -> ::core::option::Option { - match value { - "PERMISSIONS_BASE_POLICY_UNSPECIFIED" => Some(Self::Unspecified), - "PERMISSIONS_BASE_POLICY_DENY" => Some(Self::Deny), - "PERMISSIONS_BASE_POLICY_ALLOW_IF_ADMIN" => Some(Self::AllowIfAdmin), - "PERMISSIONS_BASE_POLICY_ALLOW_IF_SUPER_ADMIN" => { - Some(Self::AllowIfSuperAdmin) - } - _ => None, - } - } - } +/// Nested message and enum types in `ExternalCommitPolicyEntry`. +pub mod external_commit_policy_entry { #[derive(Clone, PartialEq, ::prost::Oneof)] - pub enum Kind { - #[prost(enumeration = "PermissionsBasePolicy", tag = "1")] - Base(i32), - #[prost(message, tag = "2")] - AndCondition(AndCondition), - #[prost(message, tag = "3")] - AnyCondition(AnyCondition), + pub enum Version { + #[prost(message, tag = "1")] + V1(super::ExternalCommitPolicyV1), } } -impl ::prost::Name for PermissionsUpdatePolicy { - const NAME: &'static str = "PermissionsUpdatePolicy"; +impl ::prost::Name for ExternalCommitPolicyEntry { + const NAME: &'static str = "ExternalCommitPolicyEntry"; const PACKAGE: &'static str = "xmtp.mls.message_contents"; fn full_name() -> ::prost::alloc::string::String { - "xmtp.mls.message_contents.PermissionsUpdatePolicy".into() + "xmtp.mls.message_contents.ExternalCommitPolicyEntry".into() } fn type_url() -> ::prost::alloc::string::String { - "/xmtp.mls.message_contents.PermissionsUpdatePolicy".into() + "/xmtp.mls.message_contents.ExternalCommitPolicyEntry".into() } } -/// Per-component permission policy with separate rules for insert, update, -/// and delete operations. -/// -/// Insert and update are separate because some components need different -/// permission levels for creating vs modifying entries. For example, group -/// membership allows any member to update (installations/sequence ID) but -/// only admins to insert (add a new member). +/// Message for group mutable metadata #[derive(Clone, PartialEq, ::prost::Message)] -pub struct ComponentPermissions { - /// Policy for inserting a new value (component does not yet exist) +pub struct GroupMutablePermissionsV1 { #[prost(message, optional, tag = "1")] - pub insert_policy: ::core::option::Option, - /// Policy for updating an existing value - #[prost(message, optional, tag = "2")] - pub update_policy: ::core::option::Option, - /// Policy for deleting a value - #[prost(message, optional, tag = "3")] - pub delete_policy: ::core::option::Option, + pub policies: ::core::option::Option, } -impl ::prost::Name for ComponentPermissions { - const NAME: &'static str = "ComponentPermissions"; +impl ::prost::Name for GroupMutablePermissionsV1 { + const NAME: &'static str = "GroupMutablePermissionsV1"; const PACKAGE: &'static str = "xmtp.mls.message_contents"; fn full_name() -> ::prost::alloc::string::String { - "xmtp.mls.message_contents.ComponentPermissions".into() + "xmtp.mls.message_contents.GroupMutablePermissionsV1".into() } fn type_url() -> ::prost::alloc::string::String { - "/xmtp.mls.message_contents.ComponentPermissions".into() + "/xmtp.mls.message_contents.GroupMutablePermissionsV1".into() } } -/// Metadata describing a component: its data type and permission policies. -/// -/// Stored as the value in the component registry (ComponentId 0x8000). -/// Each registered component has one of these describing what kind of data -/// it holds and who can insert, update, or delete it. +/// The set of policies that govern the group #[derive(Clone, PartialEq, ::prost::Message)] -pub struct ComponentMetadata { - /// The data structure type of the component's value - #[prost(enumeration = "ComponentType", tag = "1")] - pub component_type: i32, - /// Permission policies for this component, evaluated against regular - /// (member-issued) commits. +pub struct PolicySet { + #[prost(message, optional, tag = "1")] + pub add_member_policy: ::core::option::Option, #[prost(message, optional, tag = "2")] - pub permissions: ::core::option::Option, - /// Permission policies for this component, evaluated against MLS External - /// Commits (RFC 9420 §12.4.3.2). Absent / unset is equivalent to all-Deny: - /// external committers cannot touch this component. Each component opts in - /// explicitly by setting this field. Combined with the EXTERNAL_COMMIT_POLICY - /// master switch (`allow_external_commit`), this is the per-component declarative - /// authorization for external-commit-driven joins. - #[prost(message, optional, tag = "3")] - pub external_committer_permissions: ::core::option::Option, + pub remove_member_policy: ::core::option::Option, + #[prost(map = "string, message", tag = "3")] + pub update_metadata_policy: ::std::collections::HashMap< + ::prost::alloc::string::String, + MetadataPolicy, + >, + #[prost(message, optional, tag = "4")] + pub add_admin_policy: ::core::option::Option, + #[prost(message, optional, tag = "5")] + pub remove_admin_policy: ::core::option::Option, + #[prost(message, optional, tag = "6")] + pub update_permissions_policy: ::core::option::Option, } -impl ::prost::Name for ComponentMetadata { - const NAME: &'static str = "ComponentMetadata"; +impl ::prost::Name for PolicySet { + const NAME: &'static str = "PolicySet"; const PACKAGE: &'static str = "xmtp.mls.message_contents"; fn full_name() -> ::prost::alloc::string::String { - "xmtp.mls.message_contents.ComponentMetadata".into() + "xmtp.mls.message_contents.PolicySet".into() } fn type_url() -> ::prost::alloc::string::String { - "/xmtp.mls.message_contents.ComponentMetadata".into() + "/xmtp.mls.message_contents.PolicySet".into() } } -/// The data structure type of a component's value -#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, ::prost::Enumeration)] -#[repr(i32)] -pub enum ComponentType { - Unspecified = 0, - /// Opaque bytes, replaced atomically - Bytes = 1, - /// A utf-8 encoded string, replaced atomically - String = 2, - /// A TlsMap\ supporting key-level insert/update/delete via deltas - TlsMapBytesBytes = 3, - /// A TlsMap\ supporting key-level insert/update/delete via deltas - TlsMapInboxIdBytes = 4, - /// A TlsSet supporting insert/remove/remove-by-hash via deltas - TlsSetBytes = 5, - /// A TlsSet supporting insert/remove/remove-by-hash via deltas - TlsSetInboxId = 6, +/// A policy that governs adding/removing members or installations +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct MembershipPolicy { + #[prost(oneof = "membership_policy::Kind", tags = "1, 2, 3")] + pub kind: ::core::option::Option, } -impl ComponentType { - /// String value of the enum field names used in the ProtoBuf definition. - /// - /// The values are not transformed in any way and thus are considered stable - /// (if the ProtoBuf definition does not change) and safe for programmatic use. - pub fn as_str_name(&self) -> &'static str { - match self { - Self::Unspecified => "COMPONENT_TYPE_UNSPECIFIED", - Self::Bytes => "COMPONENT_TYPE_BYTES", - Self::String => "COMPONENT_TYPE_STRING", - Self::TlsMapBytesBytes => "COMPONENT_TYPE_TLS_MAP_BYTES_BYTES", - Self::TlsMapInboxIdBytes => "COMPONENT_TYPE_TLS_MAP_INBOX_ID_BYTES", - Self::TlsSetBytes => "COMPONENT_TYPE_TLS_SET_BYTES", - Self::TlsSetInboxId => "COMPONENT_TYPE_TLS_SET_INBOX_ID", +/// Nested message and enum types in `MembershipPolicy`. +pub mod membership_policy { + /// Combine multiple policies. All must evaluate to true + #[derive(Clone, PartialEq, ::prost::Message)] + pub struct AndCondition { + #[prost(message, repeated, tag = "1")] + pub policies: ::prost::alloc::vec::Vec, + } + impl ::prost::Name for AndCondition { + const NAME: &'static str = "AndCondition"; + const PACKAGE: &'static str = "xmtp.mls.message_contents"; + fn full_name() -> ::prost::alloc::string::String { + "xmtp.mls.message_contents.MembershipPolicy.AndCondition".into() + } + fn type_url() -> ::prost::alloc::string::String { + "/xmtp.mls.message_contents.MembershipPolicy.AndCondition".into() } } - /// Creates an enum from field names used in the ProtoBuf definition. - pub fn from_str_name(value: &str) -> ::core::option::Option { - match value { - "COMPONENT_TYPE_UNSPECIFIED" => Some(Self::Unspecified), - "COMPONENT_TYPE_BYTES" => Some(Self::Bytes), - "COMPONENT_TYPE_STRING" => Some(Self::String), - "COMPONENT_TYPE_TLS_MAP_BYTES_BYTES" => Some(Self::TlsMapBytesBytes), - "COMPONENT_TYPE_TLS_MAP_INBOX_ID_BYTES" => Some(Self::TlsMapInboxIdBytes), - "COMPONENT_TYPE_TLS_SET_BYTES" => Some(Self::TlsSetBytes), - "COMPONENT_TYPE_TLS_SET_INBOX_ID" => Some(Self::TlsSetInboxId), - _ => None, + /// Combine multiple policies. Any must evaluate to true + #[derive(Clone, PartialEq, ::prost::Message)] + pub struct AnyCondition { + #[prost(message, repeated, tag = "1")] + pub policies: ::prost::alloc::vec::Vec, + } + impl ::prost::Name for AnyCondition { + const NAME: &'static str = "AnyCondition"; + const PACKAGE: &'static str = "xmtp.mls.message_contents"; + fn full_name() -> ::prost::alloc::string::String { + "xmtp.mls.message_contents.MembershipPolicy.AnyCondition".into() + } + fn type_url() -> ::prost::alloc::string::String { + "/xmtp.mls.message_contents.MembershipPolicy.AnyCondition".into() } } -} -/// ContentTypeId is used to identify the type of content stored in a Message. -#[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] -pub struct ContentTypeId { - /// authority governing this content type - #[prost(string, tag = "1")] - pub authority_id: ::prost::alloc::string::String, - /// type identifier - #[prost(string, tag = "2")] - pub type_id: ::prost::alloc::string::String, - /// major version of the type - #[prost(uint32, tag = "3")] - pub version_major: u32, - /// minor version of the type - #[prost(uint32, tag = "4")] - pub version_minor: u32, -} -impl ::prost::Name for ContentTypeId { - const NAME: &'static str = "ContentTypeId"; - const PACKAGE: &'static str = "xmtp.mls.message_contents"; - fn full_name() -> ::prost::alloc::string::String { - "xmtp.mls.message_contents.ContentTypeId".into() + /// Base policy + #[derive( + Clone, + Copy, + Debug, + PartialEq, + Eq, + Hash, + PartialOrd, + Ord, + ::prost::Enumeration + )] + #[repr(i32)] + pub enum BasePolicy { + Unspecified = 0, + Allow = 1, + Deny = 2, + AllowIfAdminOrSuperAdmin = 3, + AllowIfSuperAdmin = 4, } - fn type_url() -> ::prost::alloc::string::String { - "/xmtp.mls.message_contents.ContentTypeId".into() + impl BasePolicy { + /// String value of the enum field names used in the ProtoBuf definition. + /// + /// The values are not transformed in any way and thus are considered stable + /// (if the ProtoBuf definition does not change) and safe for programmatic use. + pub fn as_str_name(&self) -> &'static str { + match self { + Self::Unspecified => "BASE_POLICY_UNSPECIFIED", + Self::Allow => "BASE_POLICY_ALLOW", + Self::Deny => "BASE_POLICY_DENY", + Self::AllowIfAdminOrSuperAdmin => { + "BASE_POLICY_ALLOW_IF_ADMIN_OR_SUPER_ADMIN" + } + Self::AllowIfSuperAdmin => "BASE_POLICY_ALLOW_IF_SUPER_ADMIN", + } + } + /// Creates an enum from field names used in the ProtoBuf definition. + pub fn from_str_name(value: &str) -> ::core::option::Option { + match value { + "BASE_POLICY_UNSPECIFIED" => Some(Self::Unspecified), + "BASE_POLICY_ALLOW" => Some(Self::Allow), + "BASE_POLICY_DENY" => Some(Self::Deny), + "BASE_POLICY_ALLOW_IF_ADMIN_OR_SUPER_ADMIN" => { + Some(Self::AllowIfAdminOrSuperAdmin) + } + "BASE_POLICY_ALLOW_IF_SUPER_ADMIN" => Some(Self::AllowIfSuperAdmin), + _ => None, + } + } + } + #[derive(Clone, PartialEq, ::prost::Oneof)] + pub enum Kind { + #[prost(enumeration = "BasePolicy", tag = "1")] + Base(i32), + #[prost(message, tag = "2")] + AndCondition(AndCondition), + #[prost(message, tag = "3")] + AnyCondition(AnyCondition), } } -/// EncodedContent bundles the content with metadata identifying its type -/// and parameters required for correct decoding and presentation of the content. -#[derive(Clone, PartialEq, ::prost::Message)] -pub struct EncodedContent { - /// content type identifier used to match the payload with - /// the correct decoding machinery - #[prost(message, optional, tag = "1")] - pub r#type: ::core::option::Option, - /// optional encoding parameters required to correctly decode the content - #[prost(map = "string, string", tag = "2")] - pub parameters: ::std::collections::HashMap< - ::prost::alloc::string::String, - ::prost::alloc::string::String, - >, - /// optional fallback description of the content that can be used in case - /// the client cannot decode or render the content - #[prost(string, optional, tag = "3")] - pub fallback: ::core::option::Option<::prost::alloc::string::String>, - /// optional compression; the value indicates algorithm used to - /// compress the encoded content bytes - #[prost(enumeration = "Compression", optional, tag = "5")] - pub compression: ::core::option::Option, - /// encoded content itself - #[prost(bytes = "vec", tag = "4")] - pub content: ::prost::alloc::vec::Vec, -} -impl ::prost::Name for EncodedContent { - const NAME: &'static str = "EncodedContent"; +impl ::prost::Name for MembershipPolicy { + const NAME: &'static str = "MembershipPolicy"; const PACKAGE: &'static str = "xmtp.mls.message_contents"; fn full_name() -> ::prost::alloc::string::String { - "xmtp.mls.message_contents.EncodedContent".into() + "xmtp.mls.message_contents.MembershipPolicy".into() } fn type_url() -> ::prost::alloc::string::String { - "/xmtp.mls.message_contents.EncodedContent".into() + "/xmtp.mls.message_contents.MembershipPolicy".into() } } -/// A PlaintextEnvelope is the outermost payload that gets encrypted by MLS -#[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] -pub struct PlaintextEnvelope { - /// Selector which declares which version of the EncodedContent this - /// PlaintextEnvelope is - #[prost(oneof = "plaintext_envelope::Content", tags = "1, 2")] - pub content: ::core::option::Option, +/// A policy that governs updating metadata +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct MetadataPolicy { + #[prost(oneof = "metadata_policy::Kind", tags = "1, 2, 3")] + pub kind: ::core::option::Option, } -/// Nested message and enum types in `PlaintextEnvelope`. -pub mod plaintext_envelope { - /// Version 1 of the encrypted envelope - #[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] - pub struct V1 { - /// Expected to be EncodedContent - #[prost(bytes = "vec", tag = "1")] - pub content: ::prost::alloc::vec::Vec, - /// A unique value that can be used to ensure that the same content can - /// produce different hashes. May be the sender timestamp. - #[prost(string, tag = "2")] - pub idempotency_key: ::prost::alloc::string::String, +/// Nested message and enum types in `MetadataPolicy`. +pub mod metadata_policy { + /// Combine multiple policies. All must evaluate to true + #[derive(Clone, PartialEq, ::prost::Message)] + pub struct AndCondition { + #[prost(message, repeated, tag = "1")] + pub policies: ::prost::alloc::vec::Vec, } - impl ::prost::Name for V1 { - const NAME: &'static str = "V1"; + impl ::prost::Name for AndCondition { + const NAME: &'static str = "AndCondition"; const PACKAGE: &'static str = "xmtp.mls.message_contents"; fn full_name() -> ::prost::alloc::string::String { - "xmtp.mls.message_contents.PlaintextEnvelope.V1".into() + "xmtp.mls.message_contents.MetadataPolicy.AndCondition".into() } fn type_url() -> ::prost::alloc::string::String { - "/xmtp.mls.message_contents.PlaintextEnvelope.V1".into() + "/xmtp.mls.message_contents.MetadataPolicy.AndCondition".into() } } - /// Version 2 of the encrypted envelope - #[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] - pub struct V2 { - /// A unique value that can be used to ensure that the same content can - /// produce different hashes. May be the sender timestamp. - #[prost(string, tag = "1")] - pub idempotency_key: ::prost::alloc::string::String, - #[prost(oneof = "v2::MessageType", tags = "2, 3, 4, 5")] - pub message_type: ::core::option::Option, - } - /// Nested message and enum types in `V2`. - pub mod v2 { - #[derive(Clone, PartialEq, Eq, Hash, ::prost::Oneof)] - pub enum MessageType { - /// Expected to be EncodedContent - #[prost(bytes, tag = "2")] - Content(::prost::alloc::vec::Vec), - /// Initiator sends a request to receive sync payload - #[prost(message, tag = "3")] - DeviceSyncRequest( - super::super::super::super::device_sync::content::DeviceSyncRequest, - ), - /// Some other authorized installation sends a reply with a link to payload - #[prost(message, tag = "4")] - DeviceSyncReply( - super::super::super::super::device_sync::content::DeviceSyncReply, - ), - /// A serialized user preference update - #[prost(message, tag = "5")] - UserPreferenceUpdate( - super::super::super::super::device_sync::content::V1UserPreferenceUpdate, - ), - } + /// Combine multiple policies. Any must evaluate to true + #[derive(Clone, PartialEq, ::prost::Message)] + pub struct AnyCondition { + #[prost(message, repeated, tag = "1")] + pub policies: ::prost::alloc::vec::Vec, } - impl ::prost::Name for V2 { - const NAME: &'static str = "V2"; + impl ::prost::Name for AnyCondition { + const NAME: &'static str = "AnyCondition"; const PACKAGE: &'static str = "xmtp.mls.message_contents"; fn full_name() -> ::prost::alloc::string::String { - "xmtp.mls.message_contents.PlaintextEnvelope.V2".into() + "xmtp.mls.message_contents.MetadataPolicy.AnyCondition".into() } fn type_url() -> ::prost::alloc::string::String { - "/xmtp.mls.message_contents.PlaintextEnvelope.V2".into() + "/xmtp.mls.message_contents.MetadataPolicy.AnyCondition".into() } } - /// Selector which declares which version of the EncodedContent this - /// PlaintextEnvelope is - #[derive(Clone, PartialEq, Eq, Hash, ::prost::Oneof)] - pub enum Content { - #[prost(message, tag = "1")] - V1(V1), + /// Base policy + #[derive( + Clone, + Copy, + Debug, + PartialEq, + Eq, + Hash, + PartialOrd, + Ord, + ::prost::Enumeration + )] + #[repr(i32)] + pub enum MetadataBasePolicy { + Unspecified = 0, + Allow = 1, + Deny = 2, + AllowIfAdmin = 3, + AllowIfSuperAdmin = 4, + } + impl MetadataBasePolicy { + /// String value of the enum field names used in the ProtoBuf definition. + /// + /// The values are not transformed in any way and thus are considered stable + /// (if the ProtoBuf definition does not change) and safe for programmatic use. + pub fn as_str_name(&self) -> &'static str { + match self { + Self::Unspecified => "METADATA_BASE_POLICY_UNSPECIFIED", + Self::Allow => "METADATA_BASE_POLICY_ALLOW", + Self::Deny => "METADATA_BASE_POLICY_DENY", + Self::AllowIfAdmin => "METADATA_BASE_POLICY_ALLOW_IF_ADMIN", + Self::AllowIfSuperAdmin => "METADATA_BASE_POLICY_ALLOW_IF_SUPER_ADMIN", + } + } + /// Creates an enum from field names used in the ProtoBuf definition. + pub fn from_str_name(value: &str) -> ::core::option::Option { + match value { + "METADATA_BASE_POLICY_UNSPECIFIED" => Some(Self::Unspecified), + "METADATA_BASE_POLICY_ALLOW" => Some(Self::Allow), + "METADATA_BASE_POLICY_DENY" => Some(Self::Deny), + "METADATA_BASE_POLICY_ALLOW_IF_ADMIN" => Some(Self::AllowIfAdmin), + "METADATA_BASE_POLICY_ALLOW_IF_SUPER_ADMIN" => { + Some(Self::AllowIfSuperAdmin) + } + _ => None, + } + } + } + #[derive(Clone, PartialEq, ::prost::Oneof)] + pub enum Kind { + #[prost(enumeration = "MetadataBasePolicy", tag = "1")] + Base(i32), #[prost(message, tag = "2")] - V2(V2), + AndCondition(AndCondition), + #[prost(message, tag = "3")] + AnyCondition(AnyCondition), } } -impl ::prost::Name for PlaintextEnvelope { - const NAME: &'static str = "PlaintextEnvelope"; +impl ::prost::Name for MetadataPolicy { + const NAME: &'static str = "MetadataPolicy"; const PACKAGE: &'static str = "xmtp.mls.message_contents"; fn full_name() -> ::prost::alloc::string::String { - "xmtp.mls.message_contents.PlaintextEnvelope".into() + "xmtp.mls.message_contents.MetadataPolicy".into() } fn type_url() -> ::prost::alloc::string::String { - "/xmtp.mls.message_contents.PlaintextEnvelope".into() + "/xmtp.mls.message_contents.MetadataPolicy".into() } } -/// Recognized compression algorithms -/// protolint:disable ENUM_FIELD_NAMES_ZERO_VALUE_END_WITH -#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, ::prost::Enumeration)] -#[repr(i32)] -pub enum Compression { - Deflate = 0, - Gzip = 1, +/// A policy that governs updating permissions +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct PermissionsUpdatePolicy { + #[prost(oneof = "permissions_update_policy::Kind", tags = "1, 2, 3")] + pub kind: ::core::option::Option, } -impl Compression { - /// String value of the enum field names used in the ProtoBuf definition. - /// - /// The values are not transformed in any way and thus are considered stable - /// (if the ProtoBuf definition does not change) and safe for programmatic use. - pub fn as_str_name(&self) -> &'static str { - match self { - Self::Deflate => "COMPRESSION_DEFLATE", - Self::Gzip => "COMPRESSION_GZIP", - } +/// Nested message and enum types in `PermissionsUpdatePolicy`. +pub mod permissions_update_policy { + /// Combine multiple policies. All must evaluate to true + #[derive(Clone, PartialEq, ::prost::Message)] + pub struct AndCondition { + #[prost(message, repeated, tag = "1")] + pub policies: ::prost::alloc::vec::Vec, } - /// Creates an enum from field names used in the ProtoBuf definition. - pub fn from_str_name(value: &str) -> ::core::option::Option { - match value { - "COMPRESSION_DEFLATE" => Some(Self::Deflate), - "COMPRESSION_GZIP" => Some(Self::Gzip), - _ => None, + impl ::prost::Name for AndCondition { + const NAME: &'static str = "AndCondition"; + const PACKAGE: &'static str = "xmtp.mls.message_contents"; + fn full_name() -> ::prost::alloc::string::String { + "xmtp.mls.message_contents.PermissionsUpdatePolicy.AndCondition".into() + } + fn type_url() -> ::prost::alloc::string::String { + "/xmtp.mls.message_contents.PermissionsUpdatePolicy.AndCondition".into() } } -} -/// v1 external-commit-policy payload. -/// Field-coupling invariants enforced by libxmtp when applying an -/// AppDataUpdate(EXTERNAL_COMMIT_POLICY) proposal: -/// -/// * When `allow_external_commit` transitions to true: `symmetric_key` -/// and `external_group_id` MUST be populated (non-empty, meeting -/// their length requirements) in the same proposal. The two -/// transitions are atomic — there is no window where the bit is on -/// but the invite coordinates are unset. -/// -/// * When `allow_external_commit` transitions to false (revoke): -/// `symmetric_key` and `external_group_id` MUST be cleared (set to -/// empty bytes) in the same proposal. Leaving stale coordinates in -/// the group state after revoke would let a future re-enable -/// accidentally revive a previously-distributed key. -/// -/// * On re-enable (false → true after a prior revoke): the new -/// `symmetric_key` MUST differ from every previously-used value for -/// this group, and the new `external_group_id` SHOULD differ as -/// well. Reusing a revoked key would re-validate every QR ever -/// printed under that key, defeating the revocation. Admin clients -/// are responsible for generating fresh material on each enable. -#[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] -pub struct ExternalCommitPolicyV1 { - /// Master switch for MLS External Commits adding new members. - /// Required for the QR-invite flow. Defaults to false; admins - /// (super-admin by default) opt in via - /// AppDataUpdate(EXTERNAL_COMMIT_POLICY). - /// - /// See the field-coupling invariants in the message-level comment - /// above: enabling MUST populate symmetric_key + external_group_id; - /// revoking (true → false) MUST clear them. - #[prost(bool, tag = "1")] - pub allow_external_commit: bool, - /// Wall-clock auto-disable timestamp (ns since UNIX epoch). - /// 0 = no automatic expiry. After this timestamp the validator - /// rejects all external commits regardless of `allow_external_commit`. - /// Lets admins issue time-bounded invite campaigns without having to - /// come back and flip the bit manually. - #[prost(uint64, tag = "2")] - pub expires_at_ns: u64, - /// Maximum staleness of the GroupInfo referenced by an external - /// commit, in nanoseconds since GroupInfo export. 0 = no staleness - /// limit. External commits whose referenced GroupInfo was exported - /// more than `expire_in_ns` ago are rejected. Narrows the replay - /// window for stolen-blob attacks and forces re-export frequency. - #[prost(uint64, tag = "3")] - pub expire_in_ns: u64, - /// 32-byte ChaCha20Poly1305 key used to wrap the EncryptedGroupInfoBlob - /// for the currently-active invite. Carried in the group state so any - /// member (especially a just-joined external committer) can re-export - /// GroupInfo and re-upload a refreshed blob under the same key after a - /// join — without this, a printed QR / link would die the moment the - /// issuing admin went offline. - /// - /// The QR carries the same key bytes. Rotation = admin sets a new value - /// here in a single AppDataUpdate(EXTERNAL_COMMIT_POLICY) proposal AND - /// issues a new QR carrying the matching key; old QR holders' keys no - /// longer decrypt blobs the service serves under the rotated slot. - /// - /// Length MUST be exactly 32 bytes when populated. Empty (zero-length) - /// means no active invite — and MUST coincide with - /// `allow_external_commit == false` (see the field-coupling invariants - /// at the top of this message). Revoking the invite MUST clear this - /// field; re-enabling MUST populate it with a freshly-generated value - /// distinct from any previously-used key for this group. - /// - /// Note: the service_pointer (where the blob lives) is intentionally - /// NOT stored in the group. It is per-QR application-defined opaque - /// bytes; different invites for the same group may point at different - /// services. Joiners use the service_pointer from the QR they scanned. - #[prost(bytes = "vec", tag = "4")] - pub symmetric_key: ::prost::alloc::vec::Vec, - /// Identifier for the service slot holding the active invite's - /// encrypted blob. Application-defined opaque bytes (UUID, snowflake, - /// short slot key, etc.); decoupled from the MLS group_id. Admins - /// MAY rotate the symmetric_key while keeping this stable (overwrite - /// the same slot on the service) or change both together (new slot, - /// leaves the old slot orphaned for application-side GC). - /// - /// The QR carries the same value. The joiner verifies that the QR's - /// `external_group_id` equals this field after joining, as - /// defense-in-depth against a stale or swapped QR. Mismatch indicates - /// the admin rotated to a new slot after the QR was minted; the - /// joining client SHOULD treat the just-published commit as orphaned - /// (it validates fine, but the refreshed blob the joiner would upload - /// to the old slot will not be reachable by holders of the new QR). - /// - /// MUST be at least 4 bytes when populated (collision-avoidance floor - /// for tiny services). RECOMMENDED: 16 random bytes when no - /// application-specific scheme is in use. Empty (zero-length) means - /// no active invite — and MUST coincide with - /// `allow_external_commit == false` (see the field-coupling - /// invariants at the top of this message). Revoking the invite MUST - /// clear this field; re-enabling SHOULD use a freshly-generated value - /// (reusing a prior `external_group_id` is permitted only when the - /// admin intends to overwrite the old service slot — typically the - /// admin generates a new value to leave the prior slot orphaned). - #[prost(bytes = "vec", tag = "5")] - pub external_group_id: ::prost::alloc::vec::Vec, -} -impl ::prost::Name for ExternalCommitPolicyV1 { - const NAME: &'static str = "ExternalCommitPolicyV1"; - const PACKAGE: &'static str = "xmtp.mls.message_contents"; - fn full_name() -> ::prost::alloc::string::String { - "xmtp.mls.message_contents.ExternalCommitPolicyV1".into() + /// Combine multiple policies. Any must evaluate to true + #[derive(Clone, PartialEq, ::prost::Message)] + pub struct AnyCondition { + #[prost(message, repeated, tag = "1")] + pub policies: ::prost::alloc::vec::Vec, } - fn type_url() -> ::prost::alloc::string::String { - "/xmtp.mls.message_contents.ExternalCommitPolicyV1".into() + impl ::prost::Name for AnyCondition { + const NAME: &'static str = "AnyCondition"; + const PACKAGE: &'static str = "xmtp.mls.message_contents"; + fn full_name() -> ::prost::alloc::string::String { + "xmtp.mls.message_contents.PermissionsUpdatePolicy.AnyCondition".into() + } + fn type_url() -> ::prost::alloc::string::String { + "/xmtp.mls.message_contents.PermissionsUpdatePolicy.AnyCondition".into() + } } -} -/// Versioned envelope. New variants are added as new oneof variants; -/// readers that don't recognize a variant treat the policy as default -/// (all fields zero) per the standard unknown-variant tolerance rules. -#[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] -pub struct ExternalCommitPolicyEntry { - #[prost(oneof = "external_commit_policy_entry::Version", tags = "1")] - pub version: ::core::option::Option, -} -/// Nested message and enum types in `ExternalCommitPolicyEntry`. -pub mod external_commit_policy_entry { - #[derive(Clone, PartialEq, Eq, Hash, ::prost::Oneof)] - pub enum Version { - #[prost(message, tag = "1")] - V1(super::ExternalCommitPolicyV1), + /// Base policy + #[derive( + Clone, + Copy, + Debug, + PartialEq, + Eq, + Hash, + PartialOrd, + Ord, + ::prost::Enumeration + )] + #[repr(i32)] + pub enum PermissionsBasePolicy { + Unspecified = 0, + Deny = 1, + AllowIfAdmin = 2, + AllowIfSuperAdmin = 3, } -} -impl ::prost::Name for ExternalCommitPolicyEntry { - const NAME: &'static str = "ExternalCommitPolicyEntry"; - const PACKAGE: &'static str = "xmtp.mls.message_contents"; - fn full_name() -> ::prost::alloc::string::String { - "xmtp.mls.message_contents.ExternalCommitPolicyEntry".into() + impl PermissionsBasePolicy { + /// String value of the enum field names used in the ProtoBuf definition. + /// + /// The values are not transformed in any way and thus are considered stable + /// (if the ProtoBuf definition does not change) and safe for programmatic use. + pub fn as_str_name(&self) -> &'static str { + match self { + Self::Unspecified => "PERMISSIONS_BASE_POLICY_UNSPECIFIED", + Self::Deny => "PERMISSIONS_BASE_POLICY_DENY", + Self::AllowIfAdmin => "PERMISSIONS_BASE_POLICY_ALLOW_IF_ADMIN", + Self::AllowIfSuperAdmin => "PERMISSIONS_BASE_POLICY_ALLOW_IF_SUPER_ADMIN", + } + } + /// Creates an enum from field names used in the ProtoBuf definition. + pub fn from_str_name(value: &str) -> ::core::option::Option { + match value { + "PERMISSIONS_BASE_POLICY_UNSPECIFIED" => Some(Self::Unspecified), + "PERMISSIONS_BASE_POLICY_DENY" => Some(Self::Deny), + "PERMISSIONS_BASE_POLICY_ALLOW_IF_ADMIN" => Some(Self::AllowIfAdmin), + "PERMISSIONS_BASE_POLICY_ALLOW_IF_SUPER_ADMIN" => { + Some(Self::AllowIfSuperAdmin) + } + _ => None, + } + } } - fn type_url() -> ::prost::alloc::string::String { - "/xmtp.mls.message_contents.ExternalCommitPolicyEntry".into() + #[derive(Clone, PartialEq, ::prost::Oneof)] + pub enum Kind { + #[prost(enumeration = "PermissionsBasePolicy", tag = "1")] + Base(i32), + #[prost(message, tag = "2")] + AndCondition(AndCondition), + #[prost(message, tag = "3")] + AnyCondition(AnyCondition), } } -/// v1 shape of the shareable invite blob for QR-code or link-based joining -/// of an XMTP group via an MLS external commit. -#[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] -pub struct ExternalInvitePayloadV1 { - /// Application-defined opaque bytes identifying the service location. - #[prost(bytes = "vec", tag = "1")] - pub service_pointer: ::prost::alloc::vec::Vec, - /// Identifier for the service slot holding the encrypted blob. Format - /// is application-defined (UUID, snowflake, short slot key, etc.) and - /// opaque to libxmtp; the only constraint is that the value is unique - /// within the chosen service. Decoupled from the MLS group_id — - /// rotation may keep this stable (overwrite the same slot) or change - /// it (new slot on the service); the admin chooses per invite. - /// - /// MUST be at least 4 bytes (collision-avoidance floor for tiny - /// services). RECOMMENDED: 16 random bytes when no application- - /// specific scheme is in use. Maximum length is not capped by the - /// protocol; applications should bound it to fit their QR / link - /// transport. - /// - /// After joining, the joiner verifies this matches - /// `EXTERNAL_COMMIT_POLICY.external_group_id` in the group state as - /// defense-in-depth against a stale or swapped QR. - #[prost(bytes = "vec", tag = "2")] - pub external_group_id: ::prost::alloc::vec::Vec, - /// 32 bytes; ChaCha20Poly1305 key used to wrap the GroupInfo. Matches - /// `EXTERNAL_COMMIT_POLICY.symmetric_key` in the group state. - #[prost(bytes = "vec", tag = "3")] - pub symmetric_key: ::prost::alloc::vec::Vec, -} -impl ::prost::Name for ExternalInvitePayloadV1 { - const NAME: &'static str = "ExternalInvitePayloadV1"; +impl ::prost::Name for PermissionsUpdatePolicy { + const NAME: &'static str = "PermissionsUpdatePolicy"; const PACKAGE: &'static str = "xmtp.mls.message_contents"; fn full_name() -> ::prost::alloc::string::String { - "xmtp.mls.message_contents.ExternalInvitePayloadV1".into() + "xmtp.mls.message_contents.PermissionsUpdatePolicy".into() } fn type_url() -> ::prost::alloc::string::String { - "/xmtp.mls.message_contents.ExternalInvitePayloadV1".into() + "/xmtp.mls.message_contents.PermissionsUpdatePolicy".into() } } -/// Versioned envelope for the shareable invite blob. The application embeds -/// the serialized bytes in whatever transport it prefers (hex, base64, raw -/// QR, NFC, etc.) and stores the corresponding EncryptedGroupInfoBlob on an -/// external service keyed by the v1 payload's `external_group_id`. +/// Per-component permission policy with separate rules for insert, update, +/// and delete operations. /// -/// New wire-format variants are added as new oneof entries; readers that -/// don't recognize a variant treat the invite as unparseable and fail -/// closed (no implicit downgrade). -#[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] -pub struct ExternalInvitePayload { - #[prost(oneof = "external_invite_payload::Version", tags = "1")] - pub version: ::core::option::Option, -} -/// Nested message and enum types in `ExternalInvitePayload`. -pub mod external_invite_payload { - #[derive(Clone, PartialEq, Eq, Hash, ::prost::Oneof)] - pub enum Version { - #[prost(message, tag = "1")] - V1(super::ExternalInvitePayloadV1), - } +/// Insert and update are separate because some components need different +/// permission levels for creating vs modifying entries. For example, group +/// membership allows any member to update (installations/sequence ID) but +/// only admins to insert (add a new member). +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct ComponentPermissions { + /// Policy for inserting a new value (component does not yet exist) + #[prost(message, optional, tag = "1")] + pub insert_policy: ::core::option::Option, + /// Policy for updating an existing value + #[prost(message, optional, tag = "2")] + pub update_policy: ::core::option::Option, + /// Policy for deleting a value + #[prost(message, optional, tag = "3")] + pub delete_policy: ::core::option::Option, } -impl ::prost::Name for ExternalInvitePayload { - const NAME: &'static str = "ExternalInvitePayload"; +impl ::prost::Name for ComponentPermissions { + const NAME: &'static str = "ComponentPermissions"; const PACKAGE: &'static str = "xmtp.mls.message_contents"; fn full_name() -> ::prost::alloc::string::String { - "xmtp.mls.message_contents.ExternalInvitePayload".into() + "xmtp.mls.message_contents.ComponentPermissions".into() } fn type_url() -> ::prost::alloc::string::String { - "/xmtp.mls.message_contents.ExternalInvitePayload".into() + "/xmtp.mls.message_contents.ComponentPermissions".into() } } -/// v1 shape of the encrypted-GroupInfo envelope. -/// -/// `epoch` and `group_state_hash` are plaintext metadata serving two -/// distinct purposes: -/// -/// * `epoch` provides a total ordering on uploads. The service accepts -/// an upload iff `upload.epoch > current.epoch` (strictly newer); -/// lower-epoch uploads are stale and rejected outright. -/// -/// * `group_state_hash` is a consistency check at a single epoch. MLS -/// is deterministic — every member that applies the same commit -/// derives identical group state — so two correct uploads at the -/// same epoch MUST carry the same hash. When `upload.epoch == current.epoch`: equal hashes mean an idempotent re-upload (no-op -/// or duplicate-reject); different hashes mean the uploaders are on -/// forked views of the group and the service must refuse to pick a -/// winner. +/// Metadata describing a component: its data type and permission policies. /// -/// The joiner additionally verifies on download that the blob's `epoch` -/// and `group_state_hash` match the decrypted GroupInfo before -/// attempting to join — closes the "malicious service swapped -/// ciphertext" gap. -#[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] -pub struct EncryptedGroupInfoBlobV1 { - /// 12 bytes; ChaCha20Poly1305 nonce specific to this ciphertext. - #[prost(bytes = "vec", tag = "1")] - pub nonce: ::prost::alloc::vec::Vec, - /// wrap_payload_symmetric output: AEAD ciphertext over the serialized - /// MlsMessageOut(GroupInfo). - #[prost(bytes = "vec", tag = "2")] - pub ciphertext: ::prost::alloc::vec::Vec, - /// MLS group epoch of the wrapped GroupInfo. Plaintext; the service - /// totally orders uploads by this value — strictly-newer wins, stale - /// is rejected. Joiner verifies against the decrypted GroupInfo - /// before joining. - #[prost(uint64, tag = "3")] - pub epoch: u64, - /// Tree-hash (or equivalent group-state digest) of the wrapped - /// GroupInfo. Plaintext; the service uses this only at equal epochs - /// to detect forks (same epoch + differing hash = forked uploaders). - /// Not used for ordering. Joiner verifies against the decrypted - /// GroupInfo before joining. - #[prost(bytes = "vec", tag = "4")] - pub group_state_hash: ::prost::alloc::vec::Vec, - /// Wall-clock expiry of this blob, in nanoseconds since UNIX epoch. - /// 0 means no expiry. The service uses this as a TTL hint and MAY - /// garbage-collect blobs past their `expires_at_ns` autonomously. - /// The joining client also enforces this — refuses to join from an - /// expired blob even if the service is still serving it. Admin - /// bounds the campaign by setting this at upload time; extending an - /// invite is a re-upload with a later value. - #[prost(uint64, tag = "5")] - pub expires_at_ns: u64, +/// Stored as the value in the component registry (ComponentId 0x8000). +/// Each registered component has one of these describing what kind of data +/// it holds and who can insert, update, or delete it. +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct ComponentMetadata { + /// The data structure type of the component's value + #[prost(enumeration = "ComponentType", tag = "1")] + pub component_type: i32, + /// Permission policies for this component, evaluated against regular + /// (member-issued) commits. + #[prost(message, optional, tag = "2")] + pub permissions: ::core::option::Option, + /// Permission policies for this component, evaluated against MLS External + /// Commits (RFC 9420 §12.4.3.2). Absent / unset is equivalent to all-Deny: + /// external committers cannot touch this component. Each component opts in + /// explicitly by setting this field. + /// + /// Reserved surface in XIP-82 v1: the atomic external-commit shape permits + /// no AppDataUpdate beyond the joiner's own GROUP_MEMBERSHIP entry, and that + /// one mandatory write is authorized by `allow_external_commit` itself — not + /// by this field. Future flavors that let an external committer write other + /// components consult the touched component's block here (deny-by-default). + #[prost(message, optional, tag = "3")] + pub external_committer_permissions: ::core::option::Option, } -impl ::prost::Name for EncryptedGroupInfoBlobV1 { - const NAME: &'static str = "EncryptedGroupInfoBlobV1"; +impl ::prost::Name for ComponentMetadata { + const NAME: &'static str = "ComponentMetadata"; const PACKAGE: &'static str = "xmtp.mls.message_contents"; fn full_name() -> ::prost::alloc::string::String { - "xmtp.mls.message_contents.EncryptedGroupInfoBlobV1".into() + "xmtp.mls.message_contents.ComponentMetadata".into() } fn type_url() -> ::prost::alloc::string::String { - "/xmtp.mls.message_contents.EncryptedGroupInfoBlobV1".into() + "/xmtp.mls.message_contents.ComponentMetadata".into() } } -/// Versioned envelope wrapping a single GroupInfo TLS-serialized bytes -/// under an AEAD scheme (ChaCha20Poly1305 in v1) with a fresh nonce per -/// re-encryption. Stored on the external service and replaced by joiners -/// (with a fresh nonce) after each successful join. -/// -/// New variants represent breaking wire-format changes (different AEAD, -/// different metadata layout). Readers that don't recognize a variant -/// fail closed — the joiner cannot attempt MLS state transitions against -/// a blob it can't validate. -#[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] -pub struct EncryptedGroupInfoBlob { - #[prost(oneof = "encrypted_group_info_blob::Version", tags = "1")] - pub version: ::core::option::Option, +/// The data structure type of a component's value +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, ::prost::Enumeration)] +#[repr(i32)] +pub enum ComponentType { + Unspecified = 0, + /// Opaque bytes, replaced atomically + Bytes = 1, + /// A utf-8 encoded string, replaced atomically + String = 2, + /// A TlsMap\ supporting key-level insert/update/delete via deltas + TlsMapBytesBytes = 3, + /// A TlsMap\ supporting key-level insert/update/delete via deltas + TlsMapInboxIdBytes = 4, + /// A TlsSet supporting insert/remove/remove-by-hash via deltas + TlsSetBytes = 5, + /// A TlsSet supporting insert/remove/remove-by-hash via deltas + TlsSetInboxId = 6, } -/// Nested message and enum types in `EncryptedGroupInfoBlob`. -pub mod encrypted_group_info_blob { - #[derive(Clone, PartialEq, Eq, Hash, ::prost::Oneof)] - pub enum Version { - #[prost(message, tag = "1")] - V1(super::EncryptedGroupInfoBlobV1), +impl ComponentType { + /// String value of the enum field names used in the ProtoBuf definition. + /// + /// The values are not transformed in any way and thus are considered stable + /// (if the ProtoBuf definition does not change) and safe for programmatic use. + pub fn as_str_name(&self) -> &'static str { + match self { + Self::Unspecified => "COMPONENT_TYPE_UNSPECIFIED", + Self::Bytes => "COMPONENT_TYPE_BYTES", + Self::String => "COMPONENT_TYPE_STRING", + Self::TlsMapBytesBytes => "COMPONENT_TYPE_TLS_MAP_BYTES_BYTES", + Self::TlsMapInboxIdBytes => "COMPONENT_TYPE_TLS_MAP_INBOX_ID_BYTES", + Self::TlsSetBytes => "COMPONENT_TYPE_TLS_SET_BYTES", + Self::TlsSetInboxId => "COMPONENT_TYPE_TLS_SET_INBOX_ID", + } + } + /// Creates an enum from field names used in the ProtoBuf definition. + pub fn from_str_name(value: &str) -> ::core::option::Option { + match value { + "COMPONENT_TYPE_UNSPECIFIED" => Some(Self::Unspecified), + "COMPONENT_TYPE_BYTES" => Some(Self::Bytes), + "COMPONENT_TYPE_STRING" => Some(Self::String), + "COMPONENT_TYPE_TLS_MAP_BYTES_BYTES" => Some(Self::TlsMapBytesBytes), + "COMPONENT_TYPE_TLS_MAP_INBOX_ID_BYTES" => Some(Self::TlsMapInboxIdBytes), + "COMPONENT_TYPE_TLS_SET_BYTES" => Some(Self::TlsSetBytes), + "COMPONENT_TYPE_TLS_SET_INBOX_ID" => Some(Self::TlsSetInboxId), + _ => None, + } } } -impl ::prost::Name for EncryptedGroupInfoBlob { - const NAME: &'static str = "EncryptedGroupInfoBlob"; +/// A group member and affected installation IDs +#[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] +pub struct MembershipChange { + #[prost(bytes = "vec", repeated, tag = "1")] + pub installation_ids: ::prost::alloc::vec::Vec<::prost::alloc::vec::Vec>, + #[prost(string, tag = "2")] + pub account_address: ::prost::alloc::string::String, + #[prost(string, tag = "3")] + pub initiated_by_account_address: ::prost::alloc::string::String, +} +impl ::prost::Name for MembershipChange { + const NAME: &'static str = "MembershipChange"; const PACKAGE: &'static str = "xmtp.mls.message_contents"; fn full_name() -> ::prost::alloc::string::String { - "xmtp.mls.message_contents.EncryptedGroupInfoBlob".into() + "xmtp.mls.message_contents.MembershipChange".into() } fn type_url() -> ::prost::alloc::string::String { - "/xmtp.mls.message_contents.EncryptedGroupInfoBlob".into() + "/xmtp.mls.message_contents.MembershipChange".into() } } -/// Contains a mapping of `inbox_id` -> `sequence_id` for all members of a group. -/// Designed to be stored in the group context extension of the MLS group +/// The group membership change proto +/// +/// protolint:disable REPEATED_FIELD_NAMES_PLURALIZED #[derive(Clone, PartialEq, ::prost::Message)] -pub struct GroupMembership { - #[prost(map = "string, uint64", tag = "1")] - pub members: ::std::collections::HashMap<::prost::alloc::string::String, u64>, - /// List of installations that failed to be added due to errors encountered during the evaluation process. - #[prost(bytes = "vec", repeated, tag = "2")] - pub failed_installations: ::prost::alloc::vec::Vec<::prost::alloc::vec::Vec>, +pub struct GroupMembershipChanges { + /// Members that have been added in the commit + #[prost(message, repeated, tag = "1")] + pub members_added: ::prost::alloc::vec::Vec, + /// Members that have been removed in the commit + #[prost(message, repeated, tag = "2")] + pub members_removed: ::prost::alloc::vec::Vec, + /// Installations that have been added in the commit, grouped by member + #[prost(message, repeated, tag = "3")] + pub installations_added: ::prost::alloc::vec::Vec, + /// Installations removed in the commit, grouped by member + #[prost(message, repeated, tag = "4")] + pub installations_removed: ::prost::alloc::vec::Vec, } -impl ::prost::Name for GroupMembership { - const NAME: &'static str = "GroupMembership"; +impl ::prost::Name for GroupMembershipChanges { + const NAME: &'static str = "GroupMembershipChanges"; const PACKAGE: &'static str = "xmtp.mls.message_contents"; fn full_name() -> ::prost::alloc::string::String { - "xmtp.mls.message_contents.GroupMembership".into() + "xmtp.mls.message_contents.GroupMembershipChanges".into() } fn type_url() -> ::prost::alloc::string::String { - "/xmtp.mls.message_contents.GroupMembership".into() + "/xmtp.mls.message_contents.GroupMembershipChanges".into() } } -/// Per-member membership state stored inside the GROUP_MEMBERSHIP component -/// as a TlsMap\. Keys are 32-byte inbox ids, values are the -/// encoded bytes of this message. -#[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] -pub struct GroupMembershipEntry { - #[prost(oneof = "group_membership_entry::Version", tags = "1")] - pub version: ::core::option::Option, +/// A summary of the changes in a commit. +/// Includes added/removed inboxes and changes to metadata +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct GroupUpdated { + #[prost(string, tag = "1")] + pub initiated_by_inbox_id: ::prost::alloc::string::String, + /// The inboxes added in the commit + #[prost(message, repeated, tag = "2")] + pub added_inboxes: ::prost::alloc::vec::Vec, + /// The inboxes removed in the commit + #[prost(message, repeated, tag = "3")] + pub removed_inboxes: ::prost::alloc::vec::Vec, + /// The metadata changes in the commit + #[prost(message, repeated, tag = "4")] + pub metadata_field_changes: ::prost::alloc::vec::Vec< + group_updated::MetadataFieldChange, + >, + /// / The inboxes that were removed from the group in response to pending-remove/self-remove requests + #[prost(message, repeated, tag = "5")] + pub left_inboxes: ::prost::alloc::vec::Vec, + /// The inboxes that were added to admin list in the commit + #[prost(message, repeated, tag = "6")] + pub added_admin_inboxes: ::prost::alloc::vec::Vec, + /// The inboxes that were removed from admin list in the commit + #[prost(message, repeated, tag = "7")] + pub removed_admin_inboxes: ::prost::alloc::vec::Vec, + /// The inboxes that were added to super admin list in the commit + #[prost(message, repeated, tag = "8")] + pub added_super_admin_inboxes: ::prost::alloc::vec::Vec, + /// The inboxes that were removed from super admin list in the commit + #[prost(message, repeated, tag = "9")] + pub removed_super_admin_inboxes: ::prost::alloc::vec::Vec, } -/// Nested message and enum types in `GroupMembershipEntry`. -pub mod group_membership_entry { - /// V1 of the per-member membership state. +/// Nested message and enum types in `GroupUpdated`. +pub mod group_updated { + /// An inbox that was added or removed in this commit #[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] - pub struct V1 { - /// Latest identity-update sequence id this client has applied for this - /// member. Validator-checked at bootstrap against the pre-flip - /// `GroupMembership.members\[inbox_id\]` value. - #[prost(uint64, tag = "1")] - pub sequence_id: u64, - /// Installation ids belonging to this member that we previously failed - /// to add (expired key package, validation failure, etc.). Used to - /// suppress retries on later membership updates. - /// - /// Sender-authoritative at migration: the migrator partitions the - /// global `failed_installations` per inbox by walking identity-update - /// history. Receivers accept these bytes as-is — the validator only - /// checks `sequence_id`, so the blast radius of a bad partition is - /// bounded to extra or silenced retries. Installations whose owning - /// inbox can't be determined are dropped. - #[prost(bytes = "vec", repeated, tag = "2")] - pub failed_installations: ::prost::alloc::vec::Vec<::prost::alloc::vec::Vec>, + pub struct Inbox { + #[prost(string, tag = "1")] + pub inbox_id: ::prost::alloc::string::String, + } + impl ::prost::Name for Inbox { + const NAME: &'static str = "Inbox"; + const PACKAGE: &'static str = "xmtp.mls.message_contents"; + fn full_name() -> ::prost::alloc::string::String { + "xmtp.mls.message_contents.GroupUpdated.Inbox".into() + } + fn type_url() -> ::prost::alloc::string::String { + "/xmtp.mls.message_contents.GroupUpdated.Inbox".into() + } } - impl ::prost::Name for V1 { - const NAME: &'static str = "V1"; + /// A summary of a change to the mutable metadata + #[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] + pub struct MetadataFieldChange { + /// The field that was changed + #[prost(string, tag = "1")] + pub field_name: ::prost::alloc::string::String, + /// The previous value + #[prost(string, optional, tag = "2")] + pub old_value: ::core::option::Option<::prost::alloc::string::String>, + /// The updated value + #[prost(string, optional, tag = "3")] + pub new_value: ::core::option::Option<::prost::alloc::string::String>, + } + impl ::prost::Name for MetadataFieldChange { + const NAME: &'static str = "MetadataFieldChange"; const PACKAGE: &'static str = "xmtp.mls.message_contents"; fn full_name() -> ::prost::alloc::string::String { - "xmtp.mls.message_contents.GroupMembershipEntry.V1".into() + "xmtp.mls.message_contents.GroupUpdated.MetadataFieldChange".into() } fn type_url() -> ::prost::alloc::string::String { - "/xmtp.mls.message_contents.GroupMembershipEntry.V1".into() + "/xmtp.mls.message_contents.GroupUpdated.MetadataFieldChange".into() } } - #[derive(Clone, PartialEq, Eq, Hash, ::prost::Oneof)] - pub enum Version { - #[prost(message, tag = "1")] - V1(V1), - } } -impl ::prost::Name for GroupMembershipEntry { - const NAME: &'static str = "GroupMembershipEntry"; +impl ::prost::Name for GroupUpdated { + const NAME: &'static str = "GroupUpdated"; const PACKAGE: &'static str = "xmtp.mls.message_contents"; fn full_name() -> ::prost::alloc::string::String { - "xmtp.mls.message_contents.GroupMembershipEntry".into() + "xmtp.mls.message_contents.GroupUpdated".into() } fn type_url() -> ::prost::alloc::string::String { - "/xmtp.mls.message_contents.GroupMembershipEntry".into() + "/xmtp.mls.message_contents.GroupUpdated".into() } } #[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] @@ -1461,196 +1827,99 @@ impl ConversationType { } } } -/// Message for group mutable metadata +/// Contains a mapping of `inbox_id` -> `sequence_id` for all members of a group. +/// Designed to be stored in the group context extension of the MLS group #[derive(Clone, PartialEq, ::prost::Message)] -pub struct GroupMutableMetadataV1 { - /// Map to store various metadata attributes (Group name, etc.) - #[prost(map = "string, string", tag = "1")] - pub attributes: ::std::collections::HashMap< - ::prost::alloc::string::String, - ::prost::alloc::string::String, - >, - #[prost(message, optional, tag = "2")] - pub admin_list: ::core::option::Option, - /// Creator starts as only super_admin - /// Only super_admin can add/remove other super_admin - #[prost(message, optional, tag = "3")] - pub super_admin_list: ::core::option::Option, -} -impl ::prost::Name for GroupMutableMetadataV1 { - const NAME: &'static str = "GroupMutableMetadataV1"; - const PACKAGE: &'static str = "xmtp.mls.message_contents"; - fn full_name() -> ::prost::alloc::string::String { - "xmtp.mls.message_contents.GroupMutableMetadataV1".into() - } - fn type_url() -> ::prost::alloc::string::String { - "/xmtp.mls.message_contents.GroupMutableMetadataV1".into() - } -} -/// Wrapper around a list of repeated Inbox Ids -#[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] -pub struct Inboxes { - #[prost(string, repeated, tag = "1")] - pub inbox_ids: ::prost::alloc::vec::Vec<::prost::alloc::string::String>, -} -impl ::prost::Name for Inboxes { - const NAME: &'static str = "Inboxes"; - const PACKAGE: &'static str = "xmtp.mls.message_contents"; - fn full_name() -> ::prost::alloc::string::String { - "xmtp.mls.message_contents.Inboxes".into() - } - fn type_url() -> ::prost::alloc::string::String { - "/xmtp.mls.message_contents.Inboxes".into() - } -} -/// Extension data for proposal support in group context. -/// When present in the group context extensions, indicates the group -/// uses proposal-by-reference flow. -#[derive(Clone, Copy, PartialEq, Eq, Hash, ::prost::Message)] -pub struct ProposalSupport { - #[prost(uint32, tag = "1")] - pub version: u32, +pub struct GroupMembership { + #[prost(map = "string, uint64", tag = "1")] + pub members: ::std::collections::HashMap<::prost::alloc::string::String, u64>, + /// List of installations that failed to be added due to errors encountered during the evaluation process. + #[prost(bytes = "vec", repeated, tag = "2")] + pub failed_installations: ::prost::alloc::vec::Vec<::prost::alloc::vec::Vec>, } -impl ::prost::Name for ProposalSupport { - const NAME: &'static str = "ProposalSupport"; +impl ::prost::Name for GroupMembership { + const NAME: &'static str = "GroupMembership"; const PACKAGE: &'static str = "xmtp.mls.message_contents"; fn full_name() -> ::prost::alloc::string::String { - "xmtp.mls.message_contents.ProposalSupport".into() + "xmtp.mls.message_contents.GroupMembership".into() } fn type_url() -> ::prost::alloc::string::String { - "/xmtp.mls.message_contents.ProposalSupport".into() + "/xmtp.mls.message_contents.GroupMembership".into() } } -/// A group member and affected installation IDs +/// Per-member membership state stored inside the GROUP_MEMBERSHIP component +/// as a TlsMap\. Keys are 32-byte inbox ids, values are the +/// encoded bytes of this message. #[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] -pub struct MembershipChange { - #[prost(bytes = "vec", repeated, tag = "1")] - pub installation_ids: ::prost::alloc::vec::Vec<::prost::alloc::vec::Vec>, - #[prost(string, tag = "2")] - pub account_address: ::prost::alloc::string::String, - #[prost(string, tag = "3")] - pub initiated_by_account_address: ::prost::alloc::string::String, -} -impl ::prost::Name for MembershipChange { - const NAME: &'static str = "MembershipChange"; - const PACKAGE: &'static str = "xmtp.mls.message_contents"; - fn full_name() -> ::prost::alloc::string::String { - "xmtp.mls.message_contents.MembershipChange".into() - } - fn type_url() -> ::prost::alloc::string::String { - "/xmtp.mls.message_contents.MembershipChange".into() - } -} -/// The group membership change proto -/// -/// protolint:disable REPEATED_FIELD_NAMES_PLURALIZED -#[derive(Clone, PartialEq, ::prost::Message)] -pub struct GroupMembershipChanges { - /// Members that have been added in the commit - #[prost(message, repeated, tag = "1")] - pub members_added: ::prost::alloc::vec::Vec, - /// Members that have been removed in the commit - #[prost(message, repeated, tag = "2")] - pub members_removed: ::prost::alloc::vec::Vec, - /// Installations that have been added in the commit, grouped by member - #[prost(message, repeated, tag = "3")] - pub installations_added: ::prost::alloc::vec::Vec, - /// Installations removed in the commit, grouped by member - #[prost(message, repeated, tag = "4")] - pub installations_removed: ::prost::alloc::vec::Vec, -} -impl ::prost::Name for GroupMembershipChanges { - const NAME: &'static str = "GroupMembershipChanges"; - const PACKAGE: &'static str = "xmtp.mls.message_contents"; - fn full_name() -> ::prost::alloc::string::String { - "xmtp.mls.message_contents.GroupMembershipChanges".into() - } - fn type_url() -> ::prost::alloc::string::String { - "/xmtp.mls.message_contents.GroupMembershipChanges".into() - } -} -/// A summary of the changes in a commit. -/// Includes added/removed inboxes and changes to metadata -#[derive(Clone, PartialEq, ::prost::Message)] -pub struct GroupUpdated { - #[prost(string, tag = "1")] - pub initiated_by_inbox_id: ::prost::alloc::string::String, - /// The inboxes added in the commit - #[prost(message, repeated, tag = "2")] - pub added_inboxes: ::prost::alloc::vec::Vec, - /// The inboxes removed in the commit - #[prost(message, repeated, tag = "3")] - pub removed_inboxes: ::prost::alloc::vec::Vec, - /// The metadata changes in the commit - #[prost(message, repeated, tag = "4")] - pub metadata_field_changes: ::prost::alloc::vec::Vec< - group_updated::MetadataFieldChange, - >, - /// / The inboxes that were removed from the group in response to pending-remove/self-remove requests - #[prost(message, repeated, tag = "5")] - pub left_inboxes: ::prost::alloc::vec::Vec, - /// The inboxes that were added to admin list in the commit - #[prost(message, repeated, tag = "6")] - pub added_admin_inboxes: ::prost::alloc::vec::Vec, - /// The inboxes that were removed from admin list in the commit - #[prost(message, repeated, tag = "7")] - pub removed_admin_inboxes: ::prost::alloc::vec::Vec, - /// The inboxes that were added to super admin list in the commit - #[prost(message, repeated, tag = "8")] - pub added_super_admin_inboxes: ::prost::alloc::vec::Vec, - /// The inboxes that were removed from super admin list in the commit - #[prost(message, repeated, tag = "9")] - pub removed_super_admin_inboxes: ::prost::alloc::vec::Vec, +pub struct GroupMembershipEntry { + #[prost(oneof = "group_membership_entry::Version", tags = "1")] + pub version: ::core::option::Option, } -/// Nested message and enum types in `GroupUpdated`. -pub mod group_updated { - /// An inbox that was added or removed in this commit +/// Nested message and enum types in `GroupMembershipEntry`. +pub mod group_membership_entry { + /// V1 of the per-member membership state. #[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] - pub struct Inbox { - #[prost(string, tag = "1")] - pub inbox_id: ::prost::alloc::string::String, + pub struct V1 { + /// Latest identity-update sequence id this client has applied for this + /// member. Validator-checked at bootstrap against the pre-flip + /// `GroupMembership.members\[inbox_id\]` value. + #[prost(uint64, tag = "1")] + pub sequence_id: u64, + /// Installation ids belonging to this member that we previously failed + /// to add (expired key package, validation failure, etc.). Used to + /// suppress retries on later membership updates. + /// + /// Sender-authoritative at migration: the migrator partitions the + /// global `failed_installations` per inbox by walking identity-update + /// history. Receivers accept these bytes as-is — the validator only + /// checks `sequence_id`, so the blast radius of a bad partition is + /// bounded to extra or silenced retries. Installations whose owning + /// inbox can't be determined are dropped. + #[prost(bytes = "vec", repeated, tag = "2")] + pub failed_installations: ::prost::alloc::vec::Vec<::prost::alloc::vec::Vec>, + /// The `external_group_id` of the invite this member was admitted + /// under (XIP-82). Set if and only if the member joined via an MLS + /// external commit; absent for members added by `Welcome`. This is + /// what `EXTERNAL_COMMIT_POLICY.max_uses` accounting counts: the + /// live use-count for an invite is the number of entries whose value + /// here equals the policy's active `external_group_id`. + /// + /// Recorded on EVERY external commit (even when `max_uses` is 0, so + /// a later policy change to a finite cap starts from accurate data) + /// and WRITE-ONCE: set exactly once by the admitting external commit + /// and immutable for the life of the entry. Validators reject a + /// member-sender commit that sets, clears, or alters it — its own + /// entry included; otherwise an invited member could untag itself + /// and free a `max_uses` slot at will — and any rewrite of a + /// member's entry for unrelated reasons (an installation change, + /// say) MUST carry the field through unchanged. The field disappears + /// only when the entry itself does (the member is removed). + #[prost(bytes = "vec", tag = "3")] + pub admitted_via_external_group_id: ::prost::alloc::vec::Vec, } - impl ::prost::Name for Inbox { - const NAME: &'static str = "Inbox"; + impl ::prost::Name for V1 { + const NAME: &'static str = "V1"; const PACKAGE: &'static str = "xmtp.mls.message_contents"; fn full_name() -> ::prost::alloc::string::String { - "xmtp.mls.message_contents.GroupUpdated.Inbox".into() + "xmtp.mls.message_contents.GroupMembershipEntry.V1".into() } fn type_url() -> ::prost::alloc::string::String { - "/xmtp.mls.message_contents.GroupUpdated.Inbox".into() + "/xmtp.mls.message_contents.GroupMembershipEntry.V1".into() } } - /// A summary of a change to the mutable metadata - #[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] - pub struct MetadataFieldChange { - /// The field that was changed - #[prost(string, tag = "1")] - pub field_name: ::prost::alloc::string::String, - /// The previous value - #[prost(string, optional, tag = "2")] - pub old_value: ::core::option::Option<::prost::alloc::string::String>, - /// The updated value - #[prost(string, optional, tag = "3")] - pub new_value: ::core::option::Option<::prost::alloc::string::String>, - } - impl ::prost::Name for MetadataFieldChange { - const NAME: &'static str = "MetadataFieldChange"; - const PACKAGE: &'static str = "xmtp.mls.message_contents"; - fn full_name() -> ::prost::alloc::string::String { - "xmtp.mls.message_contents.GroupUpdated.MetadataFieldChange".into() - } - fn type_url() -> ::prost::alloc::string::String { - "/xmtp.mls.message_contents.GroupUpdated.MetadataFieldChange".into() - } + #[derive(Clone, PartialEq, Eq, Hash, ::prost::Oneof)] + pub enum Version { + #[prost(message, tag = "1")] + V1(V1), } } -impl ::prost::Name for GroupUpdated { - const NAME: &'static str = "GroupUpdated"; +impl ::prost::Name for GroupMembershipEntry { + const NAME: &'static str = "GroupMembershipEntry"; const PACKAGE: &'static str = "xmtp.mls.message_contents"; fn full_name() -> ::prost::alloc::string::String { - "xmtp.mls.message_contents.GroupUpdated".into() + "xmtp.mls.message_contents.GroupMembershipEntry".into() } fn type_url() -> ::prost::alloc::string::String { - "/xmtp.mls.message_contents.GroupUpdated".into() + "/xmtp.mls.message_contents.GroupMembershipEntry".into() } } diff --git a/crates/xmtp_proto/src/gen/xmtp.mls.message_contents.serde.rs b/crates/xmtp_proto/src/gen/xmtp.mls.message_contents.serde.rs index edac2dc005..dc86104170 100644 --- a/crates/xmtp_proto/src/gen/xmtp.mls.message_contents.serde.rs +++ b/crates/xmtp_proto/src/gen/xmtp.mls.message_contents.serde.rs @@ -1275,7 +1275,7 @@ impl serde::Serialize for EncryptedGroupInfoBlobV1 { if self.epoch != 0 { len += 1; } - if !self.group_state_hash.is_empty() { + if self.group_state_hash.is_some() { len += 1; } if self.expires_at_ns != 0 { @@ -1297,10 +1297,8 @@ impl serde::Serialize for EncryptedGroupInfoBlobV1 { #[allow(clippy::needless_borrows_for_generic_args)] struct_ser.serialize_field("epoch", ToString::to_string(&self.epoch).as_str())?; } - if !self.group_state_hash.is_empty() { - #[allow(clippy::needless_borrow)] - #[allow(clippy::needless_borrows_for_generic_args)] - struct_ser.serialize_field("group_state_hash", pbjson::private::base64::encode(&self.group_state_hash).as_str())?; + if let Some(v) = self.group_state_hash.as_ref() { + struct_ser.serialize_field("group_state_hash", v)?; } if self.expires_at_ns != 0 { #[allow(clippy::needless_borrow)] @@ -1414,9 +1412,7 @@ impl<'de> serde::Deserialize<'de> for EncryptedGroupInfoBlobV1 { if group_state_hash__.is_some() { return Err(serde::de::Error::duplicate_field("groupStateHash")); } - group_state_hash__ = - Some(map_.next_value::<::pbjson::private::BytesDeserialize<_>>()?.0) - ; + group_state_hash__ = map_.next_value()?; } GeneratedField::ExpiresAtNs => { if expires_at_ns__.is_some() { @@ -1435,7 +1431,7 @@ impl<'de> serde::Deserialize<'de> for EncryptedGroupInfoBlobV1 { nonce: nonce__.unwrap_or_default(), ciphertext: ciphertext__.unwrap_or_default(), epoch: epoch__.unwrap_or_default(), - group_state_hash: group_state_hash__.unwrap_or_default(), + group_state_hash: group_state_hash__, expires_at_ns: expires_at_ns__.unwrap_or_default(), }) } @@ -1560,12 +1556,18 @@ impl serde::Serialize for ExternalCommitPolicyV1 { if self.expire_in_ns != 0 { len += 1; } - if !self.symmetric_key.is_empty() { + if self.symmetric_key.is_some() { len += 1; } if !self.external_group_id.is_empty() { len += 1; } + if self.max_uses != 0 { + len += 1; + } + if !self.refresh_pointers.is_empty() { + len += 1; + } let mut struct_ser = serializer.serialize_struct("xmtp.mls.message_contents.ExternalCommitPolicyV1", len)?; if self.allow_external_commit { struct_ser.serialize_field("allow_external_commit", &self.allow_external_commit)?; @@ -1580,16 +1582,20 @@ impl serde::Serialize for ExternalCommitPolicyV1 { #[allow(clippy::needless_borrows_for_generic_args)] struct_ser.serialize_field("expire_in_ns", ToString::to_string(&self.expire_in_ns).as_str())?; } - if !self.symmetric_key.is_empty() { - #[allow(clippy::needless_borrow)] - #[allow(clippy::needless_borrows_for_generic_args)] - struct_ser.serialize_field("symmetric_key", pbjson::private::base64::encode(&self.symmetric_key).as_str())?; + if let Some(v) = self.symmetric_key.as_ref() { + struct_ser.serialize_field("symmetric_key", v)?; } if !self.external_group_id.is_empty() { #[allow(clippy::needless_borrow)] #[allow(clippy::needless_borrows_for_generic_args)] struct_ser.serialize_field("external_group_id", pbjson::private::base64::encode(&self.external_group_id).as_str())?; } + if self.max_uses != 0 { + struct_ser.serialize_field("max_uses", &self.max_uses)?; + } + if !self.refresh_pointers.is_empty() { + struct_ser.serialize_field("refresh_pointers", &self.refresh_pointers)?; + } struct_ser.end() } } @@ -1610,6 +1616,10 @@ impl<'de> serde::Deserialize<'de> for ExternalCommitPolicyV1 { "symmetricKey", "external_group_id", "externalGroupId", + "max_uses", + "maxUses", + "refresh_pointers", + "refreshPointers", ]; #[allow(clippy::enum_variant_names)] @@ -1619,6 +1629,8 @@ impl<'de> serde::Deserialize<'de> for ExternalCommitPolicyV1 { ExpireInNs, SymmetricKey, ExternalGroupId, + MaxUses, + RefreshPointers, __SkipField__, } impl<'de> serde::Deserialize<'de> for GeneratedField { @@ -1646,6 +1658,8 @@ impl<'de> serde::Deserialize<'de> for ExternalCommitPolicyV1 { "expireInNs" | "expire_in_ns" => Ok(GeneratedField::ExpireInNs), "symmetricKey" | "symmetric_key" => Ok(GeneratedField::SymmetricKey), "externalGroupId" | "external_group_id" => Ok(GeneratedField::ExternalGroupId), + "maxUses" | "max_uses" => Ok(GeneratedField::MaxUses), + "refreshPointers" | "refresh_pointers" => Ok(GeneratedField::RefreshPointers), _ => Ok(GeneratedField::__SkipField__), } } @@ -1670,6 +1684,8 @@ impl<'de> serde::Deserialize<'de> for ExternalCommitPolicyV1 { let mut expire_in_ns__ = None; let mut symmetric_key__ = None; let mut external_group_id__ = None; + let mut max_uses__ = None; + let mut refresh_pointers__ = None; while let Some(k) = map_.next_key()? { match k { GeneratedField::AllowExternalCommit => { @@ -1698,9 +1714,7 @@ impl<'de> serde::Deserialize<'de> for ExternalCommitPolicyV1 { if symmetric_key__.is_some() { return Err(serde::de::Error::duplicate_field("symmetricKey")); } - symmetric_key__ = - Some(map_.next_value::<::pbjson::private::BytesDeserialize<_>>()?.0) - ; + symmetric_key__ = map_.next_value()?; } GeneratedField::ExternalGroupId => { if external_group_id__.is_some() { @@ -1710,6 +1724,20 @@ impl<'de> serde::Deserialize<'de> for ExternalCommitPolicyV1 { Some(map_.next_value::<::pbjson::private::BytesDeserialize<_>>()?.0) ; } + GeneratedField::MaxUses => { + if max_uses__.is_some() { + return Err(serde::de::Error::duplicate_field("maxUses")); + } + max_uses__ = + Some(map_.next_value::<::pbjson::private::NumberDeserialize<_>>()?.0) + ; + } + GeneratedField::RefreshPointers => { + if refresh_pointers__.is_some() { + return Err(serde::de::Error::duplicate_field("refreshPointers")); + } + refresh_pointers__ = Some(map_.next_value()?); + } GeneratedField::__SkipField__ => { let _ = map_.next_value::()?; } @@ -1719,8 +1747,10 @@ impl<'de> serde::Deserialize<'de> for ExternalCommitPolicyV1 { allow_external_commit: allow_external_commit__.unwrap_or_default(), expires_at_ns: expires_at_ns__.unwrap_or_default(), expire_in_ns: expire_in_ns__.unwrap_or_default(), - symmetric_key: symmetric_key__.unwrap_or_default(), + symmetric_key: symmetric_key__, external_group_id: external_group_id__.unwrap_or_default(), + max_uses: max_uses__.unwrap_or_default(), + refresh_pointers: refresh_pointers__.unwrap_or_default(), }) } } @@ -1835,30 +1865,26 @@ impl serde::Serialize for ExternalInvitePayloadV1 { { use serde::ser::SerializeStruct; let mut len = 0; - if !self.service_pointer.is_empty() { + if self.service_pointer.is_some() { len += 1; } if !self.external_group_id.is_empty() { len += 1; } - if !self.symmetric_key.is_empty() { + if self.symmetric_key.is_some() { len += 1; } let mut struct_ser = serializer.serialize_struct("xmtp.mls.message_contents.ExternalInvitePayloadV1", len)?; - if !self.service_pointer.is_empty() { - #[allow(clippy::needless_borrow)] - #[allow(clippy::needless_borrows_for_generic_args)] - struct_ser.serialize_field("service_pointer", pbjson::private::base64::encode(&self.service_pointer).as_str())?; + if let Some(v) = self.service_pointer.as_ref() { + struct_ser.serialize_field("service_pointer", v)?; } if !self.external_group_id.is_empty() { #[allow(clippy::needless_borrow)] #[allow(clippy::needless_borrows_for_generic_args)] struct_ser.serialize_field("external_group_id", pbjson::private::base64::encode(&self.external_group_id).as_str())?; } - if !self.symmetric_key.is_empty() { - #[allow(clippy::needless_borrow)] - #[allow(clippy::needless_borrows_for_generic_args)] - struct_ser.serialize_field("symmetric_key", pbjson::private::base64::encode(&self.symmetric_key).as_str())?; + if let Some(v) = self.symmetric_key.as_ref() { + struct_ser.serialize_field("symmetric_key", v)?; } struct_ser.end() } @@ -1936,9 +1962,7 @@ impl<'de> serde::Deserialize<'de> for ExternalInvitePayloadV1 { if service_pointer__.is_some() { return Err(serde::de::Error::duplicate_field("servicePointer")); } - service_pointer__ = - Some(map_.next_value::<::pbjson::private::BytesDeserialize<_>>()?.0) - ; + service_pointer__ = map_.next_value()?; } GeneratedField::ExternalGroupId => { if external_group_id__.is_some() { @@ -1952,9 +1976,7 @@ impl<'de> serde::Deserialize<'de> for ExternalInvitePayloadV1 { if symmetric_key__.is_some() { return Err(serde::de::Error::duplicate_field("symmetricKey")); } - symmetric_key__ = - Some(map_.next_value::<::pbjson::private::BytesDeserialize<_>>()?.0) - ; + symmetric_key__ = map_.next_value()?; } GeneratedField::__SkipField__ => { let _ = map_.next_value::()?; @@ -1962,9 +1984,9 @@ impl<'de> serde::Deserialize<'de> for ExternalInvitePayloadV1 { } } Ok(ExternalInvitePayloadV1 { - service_pointer: service_pointer__.unwrap_or_default(), + service_pointer: service_pointer__, external_group_id: external_group_id__.unwrap_or_default(), - symmetric_key: symmetric_key__.unwrap_or_default(), + symmetric_key: symmetric_key__, }) } } @@ -2356,6 +2378,9 @@ impl serde::Serialize for group_membership_entry::V1 { if !self.failed_installations.is_empty() { len += 1; } + if !self.admitted_via_external_group_id.is_empty() { + len += 1; + } let mut struct_ser = serializer.serialize_struct("xmtp.mls.message_contents.GroupMembershipEntry.V1", len)?; if self.sequence_id != 0 { #[allow(clippy::needless_borrow)] @@ -2365,6 +2390,11 @@ impl serde::Serialize for group_membership_entry::V1 { if !self.failed_installations.is_empty() { struct_ser.serialize_field("failed_installations", &self.failed_installations.iter().map(pbjson::private::base64::encode).collect::>())?; } + if !self.admitted_via_external_group_id.is_empty() { + #[allow(clippy::needless_borrow)] + #[allow(clippy::needless_borrows_for_generic_args)] + struct_ser.serialize_field("admitted_via_external_group_id", pbjson::private::base64::encode(&self.admitted_via_external_group_id).as_str())?; + } struct_ser.end() } } @@ -2379,12 +2409,15 @@ impl<'de> serde::Deserialize<'de> for group_membership_entry::V1 { "sequenceId", "failed_installations", "failedInstallations", + "admitted_via_external_group_id", + "admittedViaExternalGroupId", ]; #[allow(clippy::enum_variant_names)] enum GeneratedField { SequenceId, FailedInstallations, + AdmittedViaExternalGroupId, __SkipField__, } impl<'de> serde::Deserialize<'de> for GeneratedField { @@ -2409,6 +2442,7 @@ impl<'de> serde::Deserialize<'de> for group_membership_entry::V1 { match value { "sequenceId" | "sequence_id" => Ok(GeneratedField::SequenceId), "failedInstallations" | "failed_installations" => Ok(GeneratedField::FailedInstallations), + "admittedViaExternalGroupId" | "admitted_via_external_group_id" => Ok(GeneratedField::AdmittedViaExternalGroupId), _ => Ok(GeneratedField::__SkipField__), } } @@ -2430,6 +2464,7 @@ impl<'de> serde::Deserialize<'de> for group_membership_entry::V1 { { let mut sequence_id__ = None; let mut failed_installations__ = None; + let mut admitted_via_external_group_id__ = None; while let Some(k) = map_.next_key()? { match k { GeneratedField::SequenceId => { @@ -2449,6 +2484,14 @@ impl<'de> serde::Deserialize<'de> for group_membership_entry::V1 { .into_iter().map(|x| x.0).collect()) ; } + GeneratedField::AdmittedViaExternalGroupId => { + if admitted_via_external_group_id__.is_some() { + return Err(serde::de::Error::duplicate_field("admittedViaExternalGroupId")); + } + admitted_via_external_group_id__ = + Some(map_.next_value::<::pbjson::private::BytesDeserialize<_>>()?.0) + ; + } GeneratedField::__SkipField__ => { let _ = map_.next_value::()?; } @@ -2457,6 +2500,7 @@ impl<'de> serde::Deserialize<'de> for group_membership_entry::V1 { Ok(group_membership_entry::V1 { sequence_id: sequence_id__.unwrap_or_default(), failed_installations: failed_installations__.unwrap_or_default(), + admitted_via_external_group_id: admitted_via_external_group_id__.unwrap_or_default(), }) } } @@ -2861,6 +2905,105 @@ impl<'de> serde::Deserialize<'de> for GroupMutablePermissionsV1 { deserializer.deserialize_struct("xmtp.mls.message_contents.GroupMutablePermissionsV1", FIELDS, GeneratedVisitor) } } +impl serde::Serialize for GroupStateHash { + #[allow(deprecated)] + fn serialize(&self, serializer: S) -> std::result::Result + where + S: serde::Serializer, + { + use serde::ser::SerializeStruct; + let mut len = 0; + if !self.digest.is_empty() { + len += 1; + } + let mut struct_ser = serializer.serialize_struct("xmtp.mls.message_contents.GroupStateHash", len)?; + if !self.digest.is_empty() { + #[allow(clippy::needless_borrow)] + #[allow(clippy::needless_borrows_for_generic_args)] + struct_ser.serialize_field("digest", pbjson::private::base64::encode(&self.digest).as_str())?; + } + struct_ser.end() + } +} +impl<'de> serde::Deserialize<'de> for GroupStateHash { + #[allow(deprecated)] + fn deserialize(deserializer: D) -> std::result::Result + where + D: serde::Deserializer<'de>, + { + const FIELDS: &[&str] = &[ + "digest", + ]; + + #[allow(clippy::enum_variant_names)] + enum GeneratedField { + Digest, + __SkipField__, + } + impl<'de> serde::Deserialize<'de> for GeneratedField { + fn deserialize(deserializer: D) -> std::result::Result + where + D: serde::Deserializer<'de>, + { + struct GeneratedVisitor; + + impl serde::de::Visitor<'_> for GeneratedVisitor { + type Value = GeneratedField; + + fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(formatter, "expected one of: {:?}", &FIELDS) + } + + #[allow(unused_variables)] + fn visit_str(self, value: &str) -> std::result::Result + where + E: serde::de::Error, + { + match value { + "digest" => Ok(GeneratedField::Digest), + _ => Ok(GeneratedField::__SkipField__), + } + } + } + deserializer.deserialize_identifier(GeneratedVisitor) + } + } + struct GeneratedVisitor; + impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { + type Value = GroupStateHash; + + fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + formatter.write_str("struct xmtp.mls.message_contents.GroupStateHash") + } + + fn visit_map(self, mut map_: V) -> std::result::Result + where + V: serde::de::MapAccess<'de>, + { + let mut digest__ = None; + while let Some(k) = map_.next_key()? { + match k { + GeneratedField::Digest => { + if digest__.is_some() { + return Err(serde::de::Error::duplicate_field("digest")); + } + digest__ = + Some(map_.next_value::<::pbjson::private::BytesDeserialize<_>>()?.0) + ; + } + GeneratedField::__SkipField__ => { + let _ = map_.next_value::()?; + } + } + } + Ok(GroupStateHash { + digest: digest__.unwrap_or_default(), + }) + } + } + deserializer.deserialize_struct("xmtp.mls.message_contents.GroupStateHash", FIELDS, GeneratedVisitor) + } +} impl serde::Serialize for GroupUpdated { #[allow(deprecated)] fn serialize(&self, serializer: S) -> std::result::Result @@ -5957,6 +6100,219 @@ impl<'de> serde::Deserialize<'de> for ReaddRequest { deserializer.deserialize_struct("xmtp.mls.message_contents.ReaddRequest", FIELDS, GeneratedVisitor) } } +impl serde::Serialize for ServicePointer { + #[allow(deprecated)] + fn serialize(&self, serializer: S) -> std::result::Result + where + S: serde::Serializer, + { + use serde::ser::SerializeStruct; + let mut len = 0; + if self.location.is_some() { + len += 1; + } + let mut struct_ser = serializer.serialize_struct("xmtp.mls.message_contents.ServicePointer", len)?; + if let Some(v) = self.location.as_ref() { + match v { + service_pointer::Location::HttpsUrl(v) => { + struct_ser.serialize_field("https_url", v)?; + } + service_pointer::Location::Opaque(v) => { + #[allow(clippy::needless_borrow)] + #[allow(clippy::needless_borrows_for_generic_args)] + struct_ser.serialize_field("opaque", pbjson::private::base64::encode(&v).as_str())?; + } + } + } + struct_ser.end() + } +} +impl<'de> serde::Deserialize<'de> for ServicePointer { + #[allow(deprecated)] + fn deserialize(deserializer: D) -> std::result::Result + where + D: serde::Deserializer<'de>, + { + const FIELDS: &[&str] = &[ + "https_url", + "httpsUrl", + "opaque", + ]; + + #[allow(clippy::enum_variant_names)] + enum GeneratedField { + HttpsUrl, + Opaque, + __SkipField__, + } + impl<'de> serde::Deserialize<'de> for GeneratedField { + fn deserialize(deserializer: D) -> std::result::Result + where + D: serde::Deserializer<'de>, + { + struct GeneratedVisitor; + + impl serde::de::Visitor<'_> for GeneratedVisitor { + type Value = GeneratedField; + + fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(formatter, "expected one of: {:?}", &FIELDS) + } + + #[allow(unused_variables)] + fn visit_str(self, value: &str) -> std::result::Result + where + E: serde::de::Error, + { + match value { + "httpsUrl" | "https_url" => Ok(GeneratedField::HttpsUrl), + "opaque" => Ok(GeneratedField::Opaque), + _ => Ok(GeneratedField::__SkipField__), + } + } + } + deserializer.deserialize_identifier(GeneratedVisitor) + } + } + struct GeneratedVisitor; + impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { + type Value = ServicePointer; + + fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + formatter.write_str("struct xmtp.mls.message_contents.ServicePointer") + } + + fn visit_map(self, mut map_: V) -> std::result::Result + where + V: serde::de::MapAccess<'de>, + { + let mut location__ = None; + while let Some(k) = map_.next_key()? { + match k { + GeneratedField::HttpsUrl => { + if location__.is_some() { + return Err(serde::de::Error::duplicate_field("httpsUrl")); + } + location__ = map_.next_value::<::std::option::Option<_>>()?.map(service_pointer::Location::HttpsUrl); + } + GeneratedField::Opaque => { + if location__.is_some() { + return Err(serde::de::Error::duplicate_field("opaque")); + } + location__ = map_.next_value::<::std::option::Option<::pbjson::private::BytesDeserialize<_>>>()?.map(|x| service_pointer::Location::Opaque(x.0)); + } + GeneratedField::__SkipField__ => { + let _ = map_.next_value::()?; + } + } + } + Ok(ServicePointer { + location: location__, + }) + } + } + deserializer.deserialize_struct("xmtp.mls.message_contents.ServicePointer", FIELDS, GeneratedVisitor) + } +} +impl serde::Serialize for SymmetricKey { + #[allow(deprecated)] + fn serialize(&self, serializer: S) -> std::result::Result + where + S: serde::Serializer, + { + use serde::ser::SerializeStruct; + let mut len = 0; + if !self.material.is_empty() { + len += 1; + } + let mut struct_ser = serializer.serialize_struct("xmtp.mls.message_contents.SymmetricKey", len)?; + if !self.material.is_empty() { + #[allow(clippy::needless_borrow)] + #[allow(clippy::needless_borrows_for_generic_args)] + struct_ser.serialize_field("material", pbjson::private::base64::encode(&self.material).as_str())?; + } + struct_ser.end() + } +} +impl<'de> serde::Deserialize<'de> for SymmetricKey { + #[allow(deprecated)] + fn deserialize(deserializer: D) -> std::result::Result + where + D: serde::Deserializer<'de>, + { + const FIELDS: &[&str] = &[ + "material", + ]; + + #[allow(clippy::enum_variant_names)] + enum GeneratedField { + Material, + __SkipField__, + } + impl<'de> serde::Deserialize<'de> for GeneratedField { + fn deserialize(deserializer: D) -> std::result::Result + where + D: serde::Deserializer<'de>, + { + struct GeneratedVisitor; + + impl serde::de::Visitor<'_> for GeneratedVisitor { + type Value = GeneratedField; + + fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(formatter, "expected one of: {:?}", &FIELDS) + } + + #[allow(unused_variables)] + fn visit_str(self, value: &str) -> std::result::Result + where + E: serde::de::Error, + { + match value { + "material" => Ok(GeneratedField::Material), + _ => Ok(GeneratedField::__SkipField__), + } + } + } + deserializer.deserialize_identifier(GeneratedVisitor) + } + } + struct GeneratedVisitor; + impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { + type Value = SymmetricKey; + + fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + formatter.write_str("struct xmtp.mls.message_contents.SymmetricKey") + } + + fn visit_map(self, mut map_: V) -> std::result::Result + where + V: serde::de::MapAccess<'de>, + { + let mut material__ = None; + while let Some(k) = map_.next_key()? { + match k { + GeneratedField::Material => { + if material__.is_some() { + return Err(serde::de::Error::duplicate_field("material")); + } + material__ = + Some(map_.next_value::<::pbjson::private::BytesDeserialize<_>>()?.0) + ; + } + GeneratedField::__SkipField__ => { + let _ = map_.next_value::()?; + } + } + } + Ok(SymmetricKey { + material: material__.unwrap_or_default(), + }) + } + } + deserializer.deserialize_struct("xmtp.mls.message_contents.SymmetricKey", FIELDS, GeneratedVisitor) + } +} impl serde::Serialize for WelcomePointeeEncryptionAeadType { #[allow(deprecated)] fn serialize(&self, serializer: S) -> std::result::Result diff --git a/crates/xmtp_proto/src/gen/xmtp.xmtpv4.message_api.rs b/crates/xmtp_proto/src/gen/xmtp.xmtpv4.message_api.rs index e443d13bcc..0740fb53c3 100644 --- a/crates/xmtp_proto/src/gen/xmtp.xmtpv4.message_api.rs +++ b/crates/xmtp_proto/src/gen/xmtp.xmtpv4.message_api.rs @@ -1027,215 +1027,9 @@ pub mod replication_api_server { const NAME: &'static str = SERVICE_NAME; } } -#[derive(Clone, PartialEq, ::prost::Message)] -pub struct LivenessFailure { - #[prost(uint32, tag = "1")] - pub response_time_ns: u32, - #[prost(oneof = "liveness_failure::Request", tags = "2, 3, 4")] - pub request: ::core::option::Option, -} -/// Nested message and enum types in `LivenessFailure`. -pub mod liveness_failure { - #[derive(Clone, PartialEq, ::prost::Oneof)] - pub enum Request { - #[prost(message, tag = "2")] - Subscribe(super::SubscribeEnvelopesRequest), - #[prost(message, tag = "3")] - Query(super::QueryEnvelopesRequest), - #[prost(message, tag = "4")] - Publish(super::PublishPayerEnvelopesRequest), - } -} -impl ::prost::Name for LivenessFailure { - const NAME: &'static str = "LivenessFailure"; - const PACKAGE: &'static str = "xmtp.xmtpv4.message_api"; - fn full_name() -> ::prost::alloc::string::String { - "xmtp.xmtpv4.message_api.LivenessFailure".into() - } - fn type_url() -> ::prost::alloc::string::String { - "/xmtp.xmtpv4.message_api.LivenessFailure".into() - } -} -#[derive(Clone, PartialEq, ::prost::Message)] -pub struct SafetyFailure { - #[prost(message, repeated, tag = "1")] - pub envelopes: ::prost::alloc::vec::Vec, -} -impl ::prost::Name for SafetyFailure { - const NAME: &'static str = "SafetyFailure"; - const PACKAGE: &'static str = "xmtp.xmtpv4.message_api"; - fn full_name() -> ::prost::alloc::string::String { - "xmtp.xmtpv4.message_api.SafetyFailure".into() - } - fn type_url() -> ::prost::alloc::string::String { - "/xmtp.xmtpv4.message_api.SafetyFailure".into() - } -} -#[derive(Clone, PartialEq, ::prost::Message)] -pub struct UnsignedMisbehaviorReport { - #[prost(uint64, tag = "1")] - pub reporter_time_ns: u64, - #[prost(uint32, tag = "2")] - pub misbehaving_node_id: u32, - #[prost(enumeration = "Misbehavior", tag = "3")] - pub r#type: i32, - /// Nodes must verify this field is false for client-submitted reports - #[prost(bool, tag = "6")] - pub submitted_by_node: bool, - #[prost(oneof = "unsigned_misbehavior_report::Failure", tags = "4, 5")] - pub failure: ::core::option::Option, -} -/// Nested message and enum types in `UnsignedMisbehaviorReport`. -pub mod unsigned_misbehavior_report { - #[derive(Clone, PartialEq, ::prost::Oneof)] - pub enum Failure { - #[prost(message, tag = "4")] - Liveness(super::LivenessFailure), - #[prost(message, tag = "5")] - Safety(super::SafetyFailure), - } -} -impl ::prost::Name for UnsignedMisbehaviorReport { - const NAME: &'static str = "UnsignedMisbehaviorReport"; - const PACKAGE: &'static str = "xmtp.xmtpv4.message_api"; - fn full_name() -> ::prost::alloc::string::String { - "xmtp.xmtpv4.message_api.UnsignedMisbehaviorReport".into() - } - fn type_url() -> ::prost::alloc::string::String { - "/xmtp.xmtpv4.message_api.UnsignedMisbehaviorReport".into() - } -} -#[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] -pub struct MisbehaviorReport { - /// Server time when the report was stored. Used only for querying reports. - /// This field is not signed. - #[prost(uint64, tag = "1")] - pub server_time_ns: u64, - #[prost(bytes = "vec", tag = "2")] - pub unsigned_misbehavior_report: ::prost::alloc::vec::Vec, - /// Signed by the node hosting the report - #[prost(message, optional, tag = "3")] - pub signature: ::core::option::Option< - super::super::identity::associations::RecoverableEcdsaSignature, - >, -} -impl ::prost::Name for MisbehaviorReport { - const NAME: &'static str = "MisbehaviorReport"; - const PACKAGE: &'static str = "xmtp.xmtpv4.message_api"; - fn full_name() -> ::prost::alloc::string::String { - "xmtp.xmtpv4.message_api.MisbehaviorReport".into() - } - fn type_url() -> ::prost::alloc::string::String { - "/xmtp.xmtpv4.message_api.MisbehaviorReport".into() - } -} -#[derive(Clone, PartialEq, ::prost::Message)] -pub struct SubmitMisbehaviorReportRequest { - #[prost(message, optional, tag = "1")] - pub report: ::core::option::Option, -} -impl ::prost::Name for SubmitMisbehaviorReportRequest { - const NAME: &'static str = "SubmitMisbehaviorReportRequest"; - const PACKAGE: &'static str = "xmtp.xmtpv4.message_api"; - fn full_name() -> ::prost::alloc::string::String { - "xmtp.xmtpv4.message_api.SubmitMisbehaviorReportRequest".into() - } - fn type_url() -> ::prost::alloc::string::String { - "/xmtp.xmtpv4.message_api.SubmitMisbehaviorReportRequest".into() - } -} -#[derive(Clone, Copy, PartialEq, Eq, Hash, ::prost::Message)] -pub struct SubmitMisbehaviorReportResponse {} -impl ::prost::Name for SubmitMisbehaviorReportResponse { - const NAME: &'static str = "SubmitMisbehaviorReportResponse"; - const PACKAGE: &'static str = "xmtp.xmtpv4.message_api"; - fn full_name() -> ::prost::alloc::string::String { - "xmtp.xmtpv4.message_api.SubmitMisbehaviorReportResponse".into() - } - fn type_url() -> ::prost::alloc::string::String { - "/xmtp.xmtpv4.message_api.SubmitMisbehaviorReportResponse".into() - } -} -#[derive(Clone, Copy, PartialEq, Eq, Hash, ::prost::Message)] -pub struct QueryMisbehaviorReportsRequest { - #[prost(uint64, tag = "1")] - pub after_ns: u64, -} -impl ::prost::Name for QueryMisbehaviorReportsRequest { - const NAME: &'static str = "QueryMisbehaviorReportsRequest"; - const PACKAGE: &'static str = "xmtp.xmtpv4.message_api"; - fn full_name() -> ::prost::alloc::string::String { - "xmtp.xmtpv4.message_api.QueryMisbehaviorReportsRequest".into() - } - fn type_url() -> ::prost::alloc::string::String { - "/xmtp.xmtpv4.message_api.QueryMisbehaviorReportsRequest".into() - } -} -#[derive(Clone, PartialEq, ::prost::Message)] -pub struct QueryMisbehaviorReportsResponse { - #[prost(message, repeated, tag = "1")] - pub reports: ::prost::alloc::vec::Vec, -} -impl ::prost::Name for QueryMisbehaviorReportsResponse { - const NAME: &'static str = "QueryMisbehaviorReportsResponse"; - const PACKAGE: &'static str = "xmtp.xmtpv4.message_api"; - fn full_name() -> ::prost::alloc::string::String { - "xmtp.xmtpv4.message_api.QueryMisbehaviorReportsResponse".into() - } - fn type_url() -> ::prost::alloc::string::String { - "/xmtp.xmtpv4.message_api.QueryMisbehaviorReportsResponse".into() - } -} -#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, ::prost::Enumeration)] -#[repr(i32)] -pub enum Misbehavior { - Unspecified = 0, - UnresponsiveNode = 1, - SlowNode = 2, - FailedRequest = 3, - OutOfOrder = 4, - DuplicateSequenceId = 5, - CausalOrdering = 6, - InvalidPayload = 7, - BlockchainInconsistency = 8, -} -impl Misbehavior { - /// String value of the enum field names used in the ProtoBuf definition. - /// - /// The values are not transformed in any way and thus are considered stable - /// (if the ProtoBuf definition does not change) and safe for programmatic use. - pub fn as_str_name(&self) -> &'static str { - match self { - Self::Unspecified => "MISBEHAVIOR_UNSPECIFIED", - Self::UnresponsiveNode => "MISBEHAVIOR_UNRESPONSIVE_NODE", - Self::SlowNode => "MISBEHAVIOR_SLOW_NODE", - Self::FailedRequest => "MISBEHAVIOR_FAILED_REQUEST", - Self::OutOfOrder => "MISBEHAVIOR_OUT_OF_ORDER", - Self::DuplicateSequenceId => "MISBEHAVIOR_DUPLICATE_SEQUENCE_ID", - Self::CausalOrdering => "MISBEHAVIOR_CAUSAL_ORDERING", - Self::InvalidPayload => "MISBEHAVIOR_INVALID_PAYLOAD", - Self::BlockchainInconsistency => "MISBEHAVIOR_BLOCKCHAIN_INCONSISTENCY", - } - } - /// Creates an enum from field names used in the ProtoBuf definition. - pub fn from_str_name(value: &str) -> ::core::option::Option { - match value { - "MISBEHAVIOR_UNSPECIFIED" => Some(Self::Unspecified), - "MISBEHAVIOR_UNRESPONSIVE_NODE" => Some(Self::UnresponsiveNode), - "MISBEHAVIOR_SLOW_NODE" => Some(Self::SlowNode), - "MISBEHAVIOR_FAILED_REQUEST" => Some(Self::FailedRequest), - "MISBEHAVIOR_OUT_OF_ORDER" => Some(Self::OutOfOrder), - "MISBEHAVIOR_DUPLICATE_SEQUENCE_ID" => Some(Self::DuplicateSequenceId), - "MISBEHAVIOR_CAUSAL_ORDERING" => Some(Self::CausalOrdering), - "MISBEHAVIOR_INVALID_PAYLOAD" => Some(Self::InvalidPayload), - "MISBEHAVIOR_BLOCKCHAIN_INCONSISTENCY" => Some(Self::BlockchainInconsistency), - _ => None, - } - } -} /// Generated server implementations. #[cfg(any(not(target_arch = "wasm32"), feature = "grpc_server_impls"))] -pub mod misbehavior_api_server { +pub mod notification_api_server { #![allow( unused_variables, dead_code, @@ -1244,33 +1038,36 @@ pub mod misbehavior_api_server { clippy::let_unit_value, )] use tonic::codegen::*; - /// Generated trait containing gRPC methods that should be implemented for use with MisbehaviorApiServer. + /// Generated trait containing gRPC methods that should be implemented for use with NotificationApiServer. #[async_trait] - pub trait MisbehaviorApi: std::marker::Send + std::marker::Sync + 'static { - async fn submit_misbehavior_report( - &self, - request: tonic::Request, - ) -> std::result::Result< - tonic::Response, - tonic::Status, - >; - async fn query_misbehavior_reports( + pub trait NotificationApi: std::marker::Send + std::marker::Sync + 'static { + /// Server streaming response type for the SubscribeAllEnvelopes method. + type SubscribeAllEnvelopesStream: tonic::codegen::tokio_stream::Stream< + Item = std::result::Result< + super::SubscribeEnvelopesResponse, + tonic::Status, + >, + > + + std::marker::Send + + 'static; + async fn subscribe_all_envelopes( &self, - request: tonic::Request, + request: tonic::Request, ) -> std::result::Result< - tonic::Response, + tonic::Response, tonic::Status, >; } + /// Full envelope stream for notification services. #[derive(Debug)] - pub struct MisbehaviorApiServer { + pub struct NotificationApiServer { inner: Arc, accept_compression_encodings: EnabledCompressionEncodings, send_compression_encodings: EnabledCompressionEncodings, max_decoding_message_size: Option, max_encoding_message_size: Option, } - impl MisbehaviorApiServer { + impl NotificationApiServer { pub fn new(inner: T) -> Self { Self::from_arc(Arc::new(inner)) } @@ -1321,9 +1118,9 @@ pub mod misbehavior_api_server { self } } - impl tonic::codegen::Service> for MisbehaviorApiServer + impl tonic::codegen::Service> for NotificationApiServer where - T: MisbehaviorApi, + T: NotificationApi, B: Body + std::marker::Send + 'static, B::Error: Into + std::marker::Send + 'static, { @@ -1338,27 +1135,27 @@ pub mod misbehavior_api_server { } fn call(&mut self, req: http::Request) -> Self::Future { match req.uri().path() { - "/xmtp.xmtpv4.message_api.MisbehaviorApi/SubmitMisbehaviorReport" => { + "/xmtp.xmtpv4.message_api.NotificationApi/SubscribeAllEnvelopes" => { #[allow(non_camel_case_types)] - struct SubmitMisbehaviorReportSvc(pub Arc); + struct SubscribeAllEnvelopesSvc(pub Arc); impl< - T: MisbehaviorApi, - > tonic::server::UnaryService - for SubmitMisbehaviorReportSvc { - type Response = super::SubmitMisbehaviorReportResponse; - type Future = BoxFuture< - tonic::Response, - tonic::Status, - >; - fn call( + T: NotificationApi, + > tonic::server::ServerStreamingService< + super::SubscribeAllEnvelopesRequest, + > for SubscribeAllEnvelopesSvc { + type Response = super::SubscribeEnvelopesResponse; + type ResponseStream = T::SubscribeAllEnvelopesStream; + type Future = BoxFuture< + tonic::Response, + tonic::Status, + >; + fn call( &mut self, - request: tonic::Request< - super::SubmitMisbehaviorReportRequest, - >, + request: tonic::Request, ) -> Self::Future { let inner = Arc::clone(&self.0); let fut = async move { - ::submit_misbehavior_report( + ::subscribe_all_envelopes( &inner, request, ) @@ -1373,7 +1170,7 @@ pub mod misbehavior_api_server { let max_encoding_message_size = self.max_encoding_message_size; let inner = self.inner.clone(); let fut = async move { - let method = SubmitMisbehaviorReportSvc(inner); + let method = SubscribeAllEnvelopesSvc(inner); let codec = tonic_prost::ProstCodec::default(); let mut grpc = tonic::server::Grpc::new(codec) .apply_compression_config( @@ -1384,35 +1181,169 @@ pub mod misbehavior_api_server { max_decoding_message_size, max_encoding_message_size, ); - let res = grpc.unary(method, req).await; + let res = grpc.server_streaming(method, req).await; Ok(res) }; Box::pin(fut) } - "/xmtp.xmtpv4.message_api.MisbehaviorApi/QueryMisbehaviorReports" => { + _ => { + Box::pin(async move { + let mut response = http::Response::new( + tonic::body::Body::default(), + ); + let headers = response.headers_mut(); + headers + .insert( + tonic::Status::GRPC_STATUS, + (tonic::Code::Unimplemented as i32).into(), + ); + headers + .insert( + http::header::CONTENT_TYPE, + tonic::metadata::GRPC_CONTENT_TYPE, + ); + Ok(response) + }) + } + } + } + } + impl Clone for NotificationApiServer { + fn clone(&self) -> Self { + let inner = self.inner.clone(); + Self { + inner, + accept_compression_encodings: self.accept_compression_encodings, + send_compression_encodings: self.send_compression_encodings, + max_decoding_message_size: self.max_decoding_message_size, + max_encoding_message_size: self.max_encoding_message_size, + } + } + } + /// Generated gRPC service name + pub const SERVICE_NAME: &str = "xmtp.xmtpv4.message_api.NotificationApi"; + impl tonic::server::NamedService for NotificationApiServer { + const NAME: &'static str = SERVICE_NAME; + } +} +/// Generated server implementations. +#[cfg(any(not(target_arch = "wasm32"), feature = "grpc_server_impls"))] +pub mod publish_api_server { + #![allow( + unused_variables, + dead_code, + missing_docs, + clippy::wildcard_imports, + clippy::let_unit_value, + )] + use tonic::codegen::*; + /// Generated trait containing gRPC methods that should be implemented for use with PublishApiServer. + #[async_trait] + pub trait PublishApi: std::marker::Send + std::marker::Sync + 'static { + async fn publish_payer_envelopes( + &self, + request: tonic::Request, + ) -> std::result::Result< + tonic::Response, + tonic::Status, + >; + } + /// Gateway -> Node. + #[derive(Debug)] + pub struct PublishApiServer { + inner: Arc, + accept_compression_encodings: EnabledCompressionEncodings, + send_compression_encodings: EnabledCompressionEncodings, + max_decoding_message_size: Option, + max_encoding_message_size: Option, + } + impl PublishApiServer { + pub fn new(inner: T) -> Self { + Self::from_arc(Arc::new(inner)) + } + pub fn from_arc(inner: Arc) -> Self { + Self { + inner, + accept_compression_encodings: Default::default(), + send_compression_encodings: Default::default(), + max_decoding_message_size: None, + max_encoding_message_size: None, + } + } + pub fn with_interceptor( + inner: T, + interceptor: F, + ) -> InterceptedService + where + F: tonic::service::Interceptor, + { + InterceptedService::new(Self::new(inner), interceptor) + } + /// Enable decompressing requests with the given encoding. + #[must_use] + pub fn accept_compressed(mut self, encoding: CompressionEncoding) -> Self { + self.accept_compression_encodings.enable(encoding); + self + } + /// Compress responses with the given encoding, if the client supports it. + #[must_use] + pub fn send_compressed(mut self, encoding: CompressionEncoding) -> Self { + self.send_compression_encodings.enable(encoding); + self + } + /// Limits the maximum size of a decoded message. + /// + /// Default: `4MB` + #[must_use] + pub fn max_decoding_message_size(mut self, limit: usize) -> Self { + self.max_decoding_message_size = Some(limit); + self + } + /// Limits the maximum size of an encoded message. + /// + /// Default: `usize::MAX` + #[must_use] + pub fn max_encoding_message_size(mut self, limit: usize) -> Self { + self.max_encoding_message_size = Some(limit); + self + } + } + impl tonic::codegen::Service> for PublishApiServer + where + T: PublishApi, + B: Body + std::marker::Send + 'static, + B::Error: Into + std::marker::Send + 'static, + { + type Response = http::Response; + type Error = std::convert::Infallible; + type Future = BoxFuture; + fn poll_ready( + &mut self, + _cx: &mut Context<'_>, + ) -> Poll> { + Poll::Ready(Ok(())) + } + fn call(&mut self, req: http::Request) -> Self::Future { + match req.uri().path() { + "/xmtp.xmtpv4.message_api.PublishApi/PublishPayerEnvelopes" => { #[allow(non_camel_case_types)] - struct QueryMisbehaviorReportsSvc(pub Arc); + struct PublishPayerEnvelopesSvc(pub Arc); impl< - T: MisbehaviorApi, - > tonic::server::UnaryService - for QueryMisbehaviorReportsSvc { - type Response = super::QueryMisbehaviorReportsResponse; + T: PublishApi, + > tonic::server::UnaryService + for PublishPayerEnvelopesSvc { + type Response = super::PublishPayerEnvelopesResponse; type Future = BoxFuture< tonic::Response, tonic::Status, >; fn call( &mut self, - request: tonic::Request< - super::QueryMisbehaviorReportsRequest, - >, + request: tonic::Request, ) -> Self::Future { let inner = Arc::clone(&self.0); let fut = async move { - ::query_misbehavior_reports( - &inner, - request, - ) + ::publish_payer_envelopes(&inner, request) .await }; Box::pin(fut) @@ -1424,7 +1355,7 @@ pub mod misbehavior_api_server { let max_encoding_message_size = self.max_encoding_message_size; let inner = self.inner.clone(); let fut = async move { - let method = QueryMisbehaviorReportsSvc(inner); + let method = PublishPayerEnvelopesSvc(inner); let codec = tonic_prost::ProstCodec::default(); let mut grpc = tonic::server::Grpc::new(codec) .apply_compression_config( @@ -1462,7 +1393,7 @@ pub mod misbehavior_api_server { } } } - impl Clone for MisbehaviorApiServer { + impl Clone for PublishApiServer { fn clone(&self) -> Self { let inner = self.inner.clone(); Self { @@ -1474,15 +1405,221 @@ pub mod misbehavior_api_server { } } } - /// Generated gRPC service name - pub const SERVICE_NAME: &str = "xmtp.xmtpv4.message_api.MisbehaviorApi"; - impl tonic::server::NamedService for MisbehaviorApiServer { - const NAME: &'static str = SERVICE_NAME; + /// Generated gRPC service name + pub const SERVICE_NAME: &str = "xmtp.xmtpv4.message_api.PublishApi"; + impl tonic::server::NamedService for PublishApiServer { + const NAME: &'static str = SERVICE_NAME; + } +} +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct LivenessFailure { + #[prost(uint32, tag = "1")] + pub response_time_ns: u32, + #[prost(oneof = "liveness_failure::Request", tags = "2, 3, 4")] + pub request: ::core::option::Option, +} +/// Nested message and enum types in `LivenessFailure`. +pub mod liveness_failure { + #[derive(Clone, PartialEq, ::prost::Oneof)] + pub enum Request { + #[prost(message, tag = "2")] + Subscribe(super::SubscribeEnvelopesRequest), + #[prost(message, tag = "3")] + Query(super::QueryEnvelopesRequest), + #[prost(message, tag = "4")] + Publish(super::PublishPayerEnvelopesRequest), + } +} +impl ::prost::Name for LivenessFailure { + const NAME: &'static str = "LivenessFailure"; + const PACKAGE: &'static str = "xmtp.xmtpv4.message_api"; + fn full_name() -> ::prost::alloc::string::String { + "xmtp.xmtpv4.message_api.LivenessFailure".into() + } + fn type_url() -> ::prost::alloc::string::String { + "/xmtp.xmtpv4.message_api.LivenessFailure".into() + } +} +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct SafetyFailure { + #[prost(message, repeated, tag = "1")] + pub envelopes: ::prost::alloc::vec::Vec, +} +impl ::prost::Name for SafetyFailure { + const NAME: &'static str = "SafetyFailure"; + const PACKAGE: &'static str = "xmtp.xmtpv4.message_api"; + fn full_name() -> ::prost::alloc::string::String { + "xmtp.xmtpv4.message_api.SafetyFailure".into() + } + fn type_url() -> ::prost::alloc::string::String { + "/xmtp.xmtpv4.message_api.SafetyFailure".into() + } +} +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct UnsignedMisbehaviorReport { + #[prost(uint64, tag = "1")] + pub reporter_time_ns: u64, + #[prost(uint32, tag = "2")] + pub misbehaving_node_id: u32, + #[prost(enumeration = "Misbehavior", tag = "3")] + pub r#type: i32, + /// Nodes must verify this field is false for client-submitted reports + #[prost(bool, tag = "6")] + pub submitted_by_node: bool, + #[prost(oneof = "unsigned_misbehavior_report::Failure", tags = "4, 5")] + pub failure: ::core::option::Option, +} +/// Nested message and enum types in `UnsignedMisbehaviorReport`. +pub mod unsigned_misbehavior_report { + #[derive(Clone, PartialEq, ::prost::Oneof)] + pub enum Failure { + #[prost(message, tag = "4")] + Liveness(super::LivenessFailure), + #[prost(message, tag = "5")] + Safety(super::SafetyFailure), + } +} +impl ::prost::Name for UnsignedMisbehaviorReport { + const NAME: &'static str = "UnsignedMisbehaviorReport"; + const PACKAGE: &'static str = "xmtp.xmtpv4.message_api"; + fn full_name() -> ::prost::alloc::string::String { + "xmtp.xmtpv4.message_api.UnsignedMisbehaviorReport".into() + } + fn type_url() -> ::prost::alloc::string::String { + "/xmtp.xmtpv4.message_api.UnsignedMisbehaviorReport".into() + } +} +#[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] +pub struct MisbehaviorReport { + /// Server time when the report was stored. Used only for querying reports. + /// This field is not signed. + #[prost(uint64, tag = "1")] + pub server_time_ns: u64, + #[prost(bytes = "vec", tag = "2")] + pub unsigned_misbehavior_report: ::prost::alloc::vec::Vec, + /// Signed by the node hosting the report + #[prost(message, optional, tag = "3")] + pub signature: ::core::option::Option< + super::super::identity::associations::RecoverableEcdsaSignature, + >, +} +impl ::prost::Name for MisbehaviorReport { + const NAME: &'static str = "MisbehaviorReport"; + const PACKAGE: &'static str = "xmtp.xmtpv4.message_api"; + fn full_name() -> ::prost::alloc::string::String { + "xmtp.xmtpv4.message_api.MisbehaviorReport".into() + } + fn type_url() -> ::prost::alloc::string::String { + "/xmtp.xmtpv4.message_api.MisbehaviorReport".into() + } +} +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct SubmitMisbehaviorReportRequest { + #[prost(message, optional, tag = "1")] + pub report: ::core::option::Option, +} +impl ::prost::Name for SubmitMisbehaviorReportRequest { + const NAME: &'static str = "SubmitMisbehaviorReportRequest"; + const PACKAGE: &'static str = "xmtp.xmtpv4.message_api"; + fn full_name() -> ::prost::alloc::string::String { + "xmtp.xmtpv4.message_api.SubmitMisbehaviorReportRequest".into() + } + fn type_url() -> ::prost::alloc::string::String { + "/xmtp.xmtpv4.message_api.SubmitMisbehaviorReportRequest".into() + } +} +#[derive(Clone, Copy, PartialEq, Eq, Hash, ::prost::Message)] +pub struct SubmitMisbehaviorReportResponse {} +impl ::prost::Name for SubmitMisbehaviorReportResponse { + const NAME: &'static str = "SubmitMisbehaviorReportResponse"; + const PACKAGE: &'static str = "xmtp.xmtpv4.message_api"; + fn full_name() -> ::prost::alloc::string::String { + "xmtp.xmtpv4.message_api.SubmitMisbehaviorReportResponse".into() + } + fn type_url() -> ::prost::alloc::string::String { + "/xmtp.xmtpv4.message_api.SubmitMisbehaviorReportResponse".into() + } +} +#[derive(Clone, Copy, PartialEq, Eq, Hash, ::prost::Message)] +pub struct QueryMisbehaviorReportsRequest { + #[prost(uint64, tag = "1")] + pub after_ns: u64, +} +impl ::prost::Name for QueryMisbehaviorReportsRequest { + const NAME: &'static str = "QueryMisbehaviorReportsRequest"; + const PACKAGE: &'static str = "xmtp.xmtpv4.message_api"; + fn full_name() -> ::prost::alloc::string::String { + "xmtp.xmtpv4.message_api.QueryMisbehaviorReportsRequest".into() + } + fn type_url() -> ::prost::alloc::string::String { + "/xmtp.xmtpv4.message_api.QueryMisbehaviorReportsRequest".into() + } +} +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct QueryMisbehaviorReportsResponse { + #[prost(message, repeated, tag = "1")] + pub reports: ::prost::alloc::vec::Vec, +} +impl ::prost::Name for QueryMisbehaviorReportsResponse { + const NAME: &'static str = "QueryMisbehaviorReportsResponse"; + const PACKAGE: &'static str = "xmtp.xmtpv4.message_api"; + fn full_name() -> ::prost::alloc::string::String { + "xmtp.xmtpv4.message_api.QueryMisbehaviorReportsResponse".into() + } + fn type_url() -> ::prost::alloc::string::String { + "/xmtp.xmtpv4.message_api.QueryMisbehaviorReportsResponse".into() + } +} +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, ::prost::Enumeration)] +#[repr(i32)] +pub enum Misbehavior { + Unspecified = 0, + UnresponsiveNode = 1, + SlowNode = 2, + FailedRequest = 3, + OutOfOrder = 4, + DuplicateSequenceId = 5, + CausalOrdering = 6, + InvalidPayload = 7, + BlockchainInconsistency = 8, +} +impl Misbehavior { + /// String value of the enum field names used in the ProtoBuf definition. + /// + /// The values are not transformed in any way and thus are considered stable + /// (if the ProtoBuf definition does not change) and safe for programmatic use. + pub fn as_str_name(&self) -> &'static str { + match self { + Self::Unspecified => "MISBEHAVIOR_UNSPECIFIED", + Self::UnresponsiveNode => "MISBEHAVIOR_UNRESPONSIVE_NODE", + Self::SlowNode => "MISBEHAVIOR_SLOW_NODE", + Self::FailedRequest => "MISBEHAVIOR_FAILED_REQUEST", + Self::OutOfOrder => "MISBEHAVIOR_OUT_OF_ORDER", + Self::DuplicateSequenceId => "MISBEHAVIOR_DUPLICATE_SEQUENCE_ID", + Self::CausalOrdering => "MISBEHAVIOR_CAUSAL_ORDERING", + Self::InvalidPayload => "MISBEHAVIOR_INVALID_PAYLOAD", + Self::BlockchainInconsistency => "MISBEHAVIOR_BLOCKCHAIN_INCONSISTENCY", + } + } + /// Creates an enum from field names used in the ProtoBuf definition. + pub fn from_str_name(value: &str) -> ::core::option::Option { + match value { + "MISBEHAVIOR_UNSPECIFIED" => Some(Self::Unspecified), + "MISBEHAVIOR_UNRESPONSIVE_NODE" => Some(Self::UnresponsiveNode), + "MISBEHAVIOR_SLOW_NODE" => Some(Self::SlowNode), + "MISBEHAVIOR_FAILED_REQUEST" => Some(Self::FailedRequest), + "MISBEHAVIOR_OUT_OF_ORDER" => Some(Self::OutOfOrder), + "MISBEHAVIOR_DUPLICATE_SEQUENCE_ID" => Some(Self::DuplicateSequenceId), + "MISBEHAVIOR_CAUSAL_ORDERING" => Some(Self::CausalOrdering), + "MISBEHAVIOR_INVALID_PAYLOAD" => Some(Self::InvalidPayload), + "MISBEHAVIOR_BLOCKCHAIN_INCONSISTENCY" => Some(Self::BlockchainInconsistency), + _ => None, + } } } /// Generated server implementations. #[cfg(any(not(target_arch = "wasm32"), feature = "grpc_server_impls"))] -pub mod notification_api_server { +pub mod misbehavior_api_server { #![allow( unused_variables, dead_code, @@ -1491,36 +1628,33 @@ pub mod notification_api_server { clippy::let_unit_value, )] use tonic::codegen::*; - /// Generated trait containing gRPC methods that should be implemented for use with NotificationApiServer. + /// Generated trait containing gRPC methods that should be implemented for use with MisbehaviorApiServer. #[async_trait] - pub trait NotificationApi: std::marker::Send + std::marker::Sync + 'static { - /// Server streaming response type for the SubscribeAllEnvelopes method. - type SubscribeAllEnvelopesStream: tonic::codegen::tokio_stream::Stream< - Item = std::result::Result< - super::SubscribeEnvelopesResponse, - tonic::Status, - >, - > - + std::marker::Send - + 'static; - async fn subscribe_all_envelopes( + pub trait MisbehaviorApi: std::marker::Send + std::marker::Sync + 'static { + async fn submit_misbehavior_report( &self, - request: tonic::Request, + request: tonic::Request, ) -> std::result::Result< - tonic::Response, + tonic::Response, + tonic::Status, + >; + async fn query_misbehavior_reports( + &self, + request: tonic::Request, + ) -> std::result::Result< + tonic::Response, tonic::Status, >; } - /// Full envelope stream for notification services. #[derive(Debug)] - pub struct NotificationApiServer { + pub struct MisbehaviorApiServer { inner: Arc, accept_compression_encodings: EnabledCompressionEncodings, send_compression_encodings: EnabledCompressionEncodings, max_decoding_message_size: Option, max_encoding_message_size: Option, } - impl NotificationApiServer { + impl MisbehaviorApiServer { pub fn new(inner: T) -> Self { Self::from_arc(Arc::new(inner)) } @@ -1571,9 +1705,9 @@ pub mod notification_api_server { self } } - impl tonic::codegen::Service> for NotificationApiServer + impl tonic::codegen::Service> for MisbehaviorApiServer where - T: NotificationApi, + T: MisbehaviorApi, B: Body + std::marker::Send + 'static, B::Error: Into + std::marker::Send + 'static, { @@ -1588,27 +1722,27 @@ pub mod notification_api_server { } fn call(&mut self, req: http::Request) -> Self::Future { match req.uri().path() { - "/xmtp.xmtpv4.message_api.NotificationApi/SubscribeAllEnvelopes" => { + "/xmtp.xmtpv4.message_api.MisbehaviorApi/SubmitMisbehaviorReport" => { #[allow(non_camel_case_types)] - struct SubscribeAllEnvelopesSvc(pub Arc); + struct SubmitMisbehaviorReportSvc(pub Arc); impl< - T: NotificationApi, - > tonic::server::ServerStreamingService< - super::SubscribeAllEnvelopesRequest, - > for SubscribeAllEnvelopesSvc { - type Response = super::SubscribeEnvelopesResponse; - type ResponseStream = T::SubscribeAllEnvelopesStream; + T: MisbehaviorApi, + > tonic::server::UnaryService + for SubmitMisbehaviorReportSvc { + type Response = super::SubmitMisbehaviorReportResponse; type Future = BoxFuture< - tonic::Response, + tonic::Response, tonic::Status, >; fn call( &mut self, - request: tonic::Request, + request: tonic::Request< + super::SubmitMisbehaviorReportRequest, + >, ) -> Self::Future { let inner = Arc::clone(&self.0); let fut = async move { - ::subscribe_all_envelopes( + ::submit_misbehavior_report( &inner, request, ) @@ -1623,7 +1757,7 @@ pub mod notification_api_server { let max_encoding_message_size = self.max_encoding_message_size; let inner = self.inner.clone(); let fut = async move { - let method = SubscribeAllEnvelopesSvc(inner); + let method = SubmitMisbehaviorReportSvc(inner); let codec = tonic_prost::ProstCodec::default(); let mut grpc = tonic::server::Grpc::new(codec) .apply_compression_config( @@ -1634,169 +1768,35 @@ pub mod notification_api_server { max_decoding_message_size, max_encoding_message_size, ); - let res = grpc.server_streaming(method, req).await; + let res = grpc.unary(method, req).await; Ok(res) }; Box::pin(fut) } - _ => { - Box::pin(async move { - let mut response = http::Response::new( - tonic::body::Body::default(), - ); - let headers = response.headers_mut(); - headers - .insert( - tonic::Status::GRPC_STATUS, - (tonic::Code::Unimplemented as i32).into(), - ); - headers - .insert( - http::header::CONTENT_TYPE, - tonic::metadata::GRPC_CONTENT_TYPE, - ); - Ok(response) - }) - } - } - } - } - impl Clone for NotificationApiServer { - fn clone(&self) -> Self { - let inner = self.inner.clone(); - Self { - inner, - accept_compression_encodings: self.accept_compression_encodings, - send_compression_encodings: self.send_compression_encodings, - max_decoding_message_size: self.max_decoding_message_size, - max_encoding_message_size: self.max_encoding_message_size, - } - } - } - /// Generated gRPC service name - pub const SERVICE_NAME: &str = "xmtp.xmtpv4.message_api.NotificationApi"; - impl tonic::server::NamedService for NotificationApiServer { - const NAME: &'static str = SERVICE_NAME; - } -} -/// Generated server implementations. -#[cfg(any(not(target_arch = "wasm32"), feature = "grpc_server_impls"))] -pub mod publish_api_server { - #![allow( - unused_variables, - dead_code, - missing_docs, - clippy::wildcard_imports, - clippy::let_unit_value, - )] - use tonic::codegen::*; - /// Generated trait containing gRPC methods that should be implemented for use with PublishApiServer. - #[async_trait] - pub trait PublishApi: std::marker::Send + std::marker::Sync + 'static { - async fn publish_payer_envelopes( - &self, - request: tonic::Request, - ) -> std::result::Result< - tonic::Response, - tonic::Status, - >; - } - /// Gateway -> Node. - #[derive(Debug)] - pub struct PublishApiServer { - inner: Arc, - accept_compression_encodings: EnabledCompressionEncodings, - send_compression_encodings: EnabledCompressionEncodings, - max_decoding_message_size: Option, - max_encoding_message_size: Option, - } - impl PublishApiServer { - pub fn new(inner: T) -> Self { - Self::from_arc(Arc::new(inner)) - } - pub fn from_arc(inner: Arc) -> Self { - Self { - inner, - accept_compression_encodings: Default::default(), - send_compression_encodings: Default::default(), - max_decoding_message_size: None, - max_encoding_message_size: None, - } - } - pub fn with_interceptor( - inner: T, - interceptor: F, - ) -> InterceptedService - where - F: tonic::service::Interceptor, - { - InterceptedService::new(Self::new(inner), interceptor) - } - /// Enable decompressing requests with the given encoding. - #[must_use] - pub fn accept_compressed(mut self, encoding: CompressionEncoding) -> Self { - self.accept_compression_encodings.enable(encoding); - self - } - /// Compress responses with the given encoding, if the client supports it. - #[must_use] - pub fn send_compressed(mut self, encoding: CompressionEncoding) -> Self { - self.send_compression_encodings.enable(encoding); - self - } - /// Limits the maximum size of a decoded message. - /// - /// Default: `4MB` - #[must_use] - pub fn max_decoding_message_size(mut self, limit: usize) -> Self { - self.max_decoding_message_size = Some(limit); - self - } - /// Limits the maximum size of an encoded message. - /// - /// Default: `usize::MAX` - #[must_use] - pub fn max_encoding_message_size(mut self, limit: usize) -> Self { - self.max_encoding_message_size = Some(limit); - self - } - } - impl tonic::codegen::Service> for PublishApiServer - where - T: PublishApi, - B: Body + std::marker::Send + 'static, - B::Error: Into + std::marker::Send + 'static, - { - type Response = http::Response; - type Error = std::convert::Infallible; - type Future = BoxFuture; - fn poll_ready( - &mut self, - _cx: &mut Context<'_>, - ) -> Poll> { - Poll::Ready(Ok(())) - } - fn call(&mut self, req: http::Request) -> Self::Future { - match req.uri().path() { - "/xmtp.xmtpv4.message_api.PublishApi/PublishPayerEnvelopes" => { + "/xmtp.xmtpv4.message_api.MisbehaviorApi/QueryMisbehaviorReports" => { #[allow(non_camel_case_types)] - struct PublishPayerEnvelopesSvc(pub Arc); + struct QueryMisbehaviorReportsSvc(pub Arc); impl< - T: PublishApi, - > tonic::server::UnaryService - for PublishPayerEnvelopesSvc { - type Response = super::PublishPayerEnvelopesResponse; + T: MisbehaviorApi, + > tonic::server::UnaryService + for QueryMisbehaviorReportsSvc { + type Response = super::QueryMisbehaviorReportsResponse; type Future = BoxFuture< tonic::Response, tonic::Status, >; fn call( &mut self, - request: tonic::Request, + request: tonic::Request< + super::QueryMisbehaviorReportsRequest, + >, ) -> Self::Future { let inner = Arc::clone(&self.0); let fut = async move { - ::publish_payer_envelopes(&inner, request) + ::query_misbehavior_reports( + &inner, + request, + ) .await }; Box::pin(fut) @@ -1808,7 +1808,7 @@ pub mod publish_api_server { let max_encoding_message_size = self.max_encoding_message_size; let inner = self.inner.clone(); let fut = async move { - let method = PublishPayerEnvelopesSvc(inner); + let method = QueryMisbehaviorReportsSvc(inner); let codec = tonic_prost::ProstCodec::default(); let mut grpc = tonic::server::Grpc::new(codec) .apply_compression_config( @@ -1846,7 +1846,7 @@ pub mod publish_api_server { } } } - impl Clone for PublishApiServer { + impl Clone for MisbehaviorApiServer { fn clone(&self) -> Self { let inner = self.inner.clone(); Self { @@ -1859,8 +1859,8 @@ pub mod publish_api_server { } } /// Generated gRPC service name - pub const SERVICE_NAME: &str = "xmtp.xmtpv4.message_api.PublishApi"; - impl tonic::server::NamedService for PublishApiServer { + pub const SERVICE_NAME: &str = "xmtp.xmtpv4.message_api.MisbehaviorApi"; + impl tonic::server::NamedService for MisbehaviorApiServer { const NAME: &'static str = SERVICE_NAME; } }