1use crate::{
17 BACKEND_DEFAULT_GROUP_MESSAGE_SECONDS, BACKEND_DEFAULT_HANDLE_MIN_NAME_LENGTH,
18 BACKEND_DEFAULT_KEY_PACKAGE_SECONDS, BACKEND_DEFAULT_MAX_ENVELOPE_BYTES,
19 BACKEND_DEFAULT_MAX_IDENTITY_ENTRIES, BACKEND_DEFAULT_MAX_LOOKUP_IDENTIFIERS,
20 BACKEND_DEFAULT_MAX_NEWEST_FULL_TOPICS, BACKEND_DEFAULT_MAX_NEWEST_METADATA_TOPICS,
21 BACKEND_DEFAULT_MAX_PING_BURST, BACKEND_DEFAULT_MAX_PING_FRAMES_PER_SECOND,
22 BACKEND_DEFAULT_MAX_PUBLISH_TOPICS, BACKEND_DEFAULT_MAX_QUERY_LIMIT,
23 BACKEND_DEFAULT_MAX_QUERY_TOPICS, BACKEND_DEFAULT_MAX_REQUEST_BYTES,
24 BACKEND_DEFAULT_MAX_RESPONSE_BYTES, BACKEND_DEFAULT_MAX_SCW_SIGNATURES,
25 BACKEND_DEFAULT_MAX_STATIC_TOPICS, BACKEND_DEFAULT_MAX_STREAM_TOPICS,
26 BACKEND_DEFAULT_MAX_UPDATE_ADDS, BACKEND_DEFAULT_MAX_UPDATE_BURST,
27 BACKEND_DEFAULT_MAX_UPDATE_FRAMES_PER_SECOND, BACKEND_DEFAULT_MAX_UPDATE_REMOVES,
28 BACKEND_DEFAULT_QUERY_LIMIT, BACKEND_DEFAULT_WELCOME_SECONDS, ENABLE_COMMIT_LOG,
29 MAX_GROUP_SIZE, MAX_INSTALLATIONS_PER_INBOX,
30};
31
32pub const CONFIGURATION_REFRESH_INTERVAL: std::time::Duration =
35 std::time::Duration::from_secs(3600);
36
37pub const CONFIGURATION_REFRESH_JITTER: std::time::Duration = std::time::Duration::from_secs(360);
40
41pub const CONFIGURATION_REFRESH_ATTEMPTS: usize = 3;
44
45pub const CONFIGURATION_REFRESH_BACKOFF: [std::time::Duration; 2] = [
47 std::time::Duration::from_secs(5),
48 std::time::Duration::from_secs(30),
49];
50
51pub const MAX_SERVER_IDENTIFIER_BYTES: usize = 256;
54
55pub const MAX_PUBLISHED_VALUE: u64 = 9_007_199_254_740_991;
62
63pub const APPLICATION_COMPONENT_IDS: std::ops::RangeInclusive<u16> = 0xC000..=0xFEFF;
66
67pub const MAX_APPLICATION_COMPONENT_NAME_BYTES: usize = 100;
69
70pub const WELL_KNOWN_COMPONENT_NAMES: [(u16, &str); 19] = [
76 (0x8000, "COMPONENT_REGISTRY"),
77 (0x8001, "SUPER_ADMIN_LIST"),
78 (0x8002, "ADMIN_LIST"),
79 (0x8003, "GROUP_MEMBERSHIP"),
80 (0x8004, "GROUP_NAME"),
81 (0x8005, "GROUP_DESCRIPTION"),
82 (0x8006, "GROUP_IMAGE_URL"),
83 (0x8007, "MESSAGE_DISAPPEAR_FROM_NS"),
84 (0x8008, "MESSAGE_DISAPPEAR_IN_NS"),
85 (0x8009, "APP_DATA"),
86 (0x800A, "MIN_SUPPORTED_PROTOCOL_VERSION"),
87 (0x800B, "COMMIT_LOG_SIGNER"),
88 (0x800C, "USER_DISPLAY_NAME"),
89 (0x800D, "GROUP_IMAGE"),
90 (0xBFFB, "GROUP_ROOT_KEY"),
91 (0xBFFC, "ONESHOT_MESSAGE"),
92 (0xBFFD, "DM_MEMBERS"),
93 (0xBFFE, "CREATOR_INBOX_ID"),
94 (0xBFFF, "CONVERSATION_TYPE"),
95];
96
97#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
99pub enum ServerConfigurationError {
100 #[error(
101 "identifier must be 1 to {MAX_SERVER_IDENTIFIER_BYTES} bytes with no whitespace or control characters"
102 )]
103 Identifier,
104 #[error("min_libxmtp_version {version:?} is not a semantic version")]
105 MinimumVersion { version: String },
106 #[error("{chain:?} is not a CAIP-2 chain identifier")]
107 Chain { chain: String },
108 #[error("{field} is {value}, above the {MAX_PUBLISHED_VALUE} an SDK integer holds exactly")]
109 Magnitude { field: &'static str, value: u64 },
110 #[error("application_components[{index}]: {reason}")]
111 ApplicationComponent {
112 index: usize,
113 reason: ApplicationComponentError,
114 },
115}
116
117#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
121pub enum ApplicationComponentError {
122 #[error("component_id must be in 0xC000-0xFEFF")]
123 ComponentId,
124 #[error("name must be 1 to {MAX_APPLICATION_COMPONENT_NAME_BYTES} UTF-8 bytes")]
125 Name,
126 #[error("component_type must be set")]
127 ComponentType,
128 #[error("insert, update, and delete policies must all be set and non-empty")]
129 Permissions,
130 #[error("in_groups or in_dms must be true")]
131 Conversations,
132 #[error("component_id repeats an earlier definition")]
133 DuplicateId,
134 #[error("name repeats an earlier definition")]
135 DuplicateName,
136}
137
138pub fn validate_application_components(
148 definitions: &[ApplicationComponentDefinition],
149) -> Result<(), (usize, ApplicationComponentError)> {
150 let mut ids = std::collections::HashSet::new();
151 let mut names = std::collections::HashSet::new();
152 definitions
153 .iter()
154 .enumerate()
155 .try_for_each(|(index, definition)| {
156 definition
157 .validate()
158 .and_then(|()| {
159 if !ids.insert(definition.component_id) {
160 return Err(ApplicationComponentError::DuplicateId);
161 }
162 if !names.insert(definition.name.as_str()) {
163 return Err(ApplicationComponentError::DuplicateName);
164 }
165 Ok(())
166 })
167 .map_err(|reason| (index, reason))
168 })
169}
170
171pub fn validate_server_identifier(identifier: &str) -> Result<(), ServerConfigurationError> {
174 if identifier.is_empty()
175 || identifier.len() > MAX_SERVER_IDENTIFIER_BYTES
176 || identifier
177 .chars()
178 .any(|c| c.is_whitespace() || c.is_control())
179 {
180 return Err(ServerConfigurationError::Identifier);
181 }
182 Ok(())
183}
184
185fn within_range(field: &'static str, value: u64) -> Result<(), ServerConfigurationError> {
187 if value > MAX_PUBLISHED_VALUE {
188 return Err(ServerConfigurationError::Magnitude { field, value });
189 }
190 Ok(())
191}
192
193pub fn is_caip2_chain_id(chain: &str) -> bool {
197 let Some((namespace, reference)) = chain.split_once(':') else {
198 return false;
199 };
200 (3..=8).contains(&namespace.len())
201 && !reference.is_empty()
202 && reference.len() <= 32
203 && namespace
204 .chars()
205 .all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == '-')
206 && reference
207 .chars()
208 .all(|c| c.is_ascii_alphanumeric() || c == '-' || c == '_')
209}
210
211pub fn version_is_below(version: &semver::Version, minimum: &semver::Version) -> bool {
217 (version.major, version.minor, version.patch) < (minimum.major, minimum.minor, minimum.patch)
218}
219
220#[derive(Clone, Debug, Default, PartialEq, Eq)]
222pub struct SigningKeyDescription {
223 pub kid: String,
224 pub alg: String,
225}
226
227#[derive(Clone, Debug, Default, PartialEq, Eq)]
230pub struct AuthConfiguration {
231 pub enabled: bool,
232 pub keys: Vec<SigningKeyDescription>,
233 pub audiences: Vec<String>,
234 pub issuers: Vec<String>,
235 pub required_scopes: Vec<String>,
236}
237
238#[derive(Clone, Debug, PartialEq, Eq)]
240pub struct RetentionConfiguration {
241 pub group_message_seconds: u64,
242 pub welcome_seconds: u64,
243 pub key_package_seconds: u64,
244}
245
246impl Default for RetentionConfiguration {
247 fn default() -> Self {
248 Self {
249 group_message_seconds: BACKEND_DEFAULT_GROUP_MESSAGE_SECONDS,
250 welcome_seconds: BACKEND_DEFAULT_WELCOME_SECONDS,
251 key_package_seconds: BACKEND_DEFAULT_KEY_PACKAGE_SECONDS,
252 }
253 }
254}
255
256#[derive(Clone, Debug, PartialEq, Eq)]
259pub struct LimitsConfiguration {
260 pub max_envelope_bytes: usize,
261 pub max_request_bytes: usize,
262 pub max_response_bytes: usize,
263 pub max_publish_topics: usize,
264 pub max_query_topics: usize,
265 pub max_query_limit: usize,
266 pub default_query_limit: usize,
267 pub max_newest_metadata_topics: usize,
268 pub max_newest_full_topics: usize,
269 pub max_update_adds: usize,
270 pub max_update_removes: usize,
271 pub max_stream_topics: usize,
272 pub max_static_topics: usize,
273 pub max_lookup_identifiers: usize,
274 pub max_scw_signatures: usize,
275 pub max_identity_entries: usize,
276 pub max_update_frames_per_second: u32,
277 pub max_update_burst: u32,
278 pub max_ping_frames_per_second: u32,
279 pub max_ping_burst: u32,
280}
281
282impl Default for LimitsConfiguration {
283 fn default() -> Self {
284 Self {
285 max_envelope_bytes: BACKEND_DEFAULT_MAX_ENVELOPE_BYTES,
286 max_request_bytes: BACKEND_DEFAULT_MAX_REQUEST_BYTES,
287 max_response_bytes: BACKEND_DEFAULT_MAX_RESPONSE_BYTES,
288 max_publish_topics: BACKEND_DEFAULT_MAX_PUBLISH_TOPICS,
289 max_query_topics: BACKEND_DEFAULT_MAX_QUERY_TOPICS,
290 max_query_limit: BACKEND_DEFAULT_MAX_QUERY_LIMIT,
291 default_query_limit: BACKEND_DEFAULT_QUERY_LIMIT,
292 max_newest_metadata_topics: BACKEND_DEFAULT_MAX_NEWEST_METADATA_TOPICS,
293 max_newest_full_topics: BACKEND_DEFAULT_MAX_NEWEST_FULL_TOPICS,
294 max_update_adds: BACKEND_DEFAULT_MAX_UPDATE_ADDS,
295 max_update_removes: BACKEND_DEFAULT_MAX_UPDATE_REMOVES,
296 max_stream_topics: BACKEND_DEFAULT_MAX_STREAM_TOPICS,
297 max_static_topics: BACKEND_DEFAULT_MAX_STATIC_TOPICS,
298 max_lookup_identifiers: BACKEND_DEFAULT_MAX_LOOKUP_IDENTIFIERS,
299 max_scw_signatures: BACKEND_DEFAULT_MAX_SCW_SIGNATURES,
300 max_identity_entries: BACKEND_DEFAULT_MAX_IDENTITY_ENTRIES,
301 max_update_frames_per_second: BACKEND_DEFAULT_MAX_UPDATE_FRAMES_PER_SECOND,
302 max_update_burst: BACKEND_DEFAULT_MAX_UPDATE_BURST,
303 max_ping_frames_per_second: BACKEND_DEFAULT_MAX_PING_FRAMES_PER_SECOND,
304 max_ping_burst: BACKEND_DEFAULT_MAX_PING_BURST,
305 }
306 }
307}
308
309impl LimitsConfiguration {
310 pub fn without_zeroes(&self) -> Self {
320 let default = Self::default();
321 macro_rules! or_default {
322 ($($field:ident),+ $(,)?) => {
323 Self {
324 $($field: if self.$field == 0 { default.$field } else { self.$field }),+
325 }
326 };
327 }
328 or_default!(
329 max_envelope_bytes,
330 max_request_bytes,
331 max_response_bytes,
332 max_publish_topics,
333 max_query_topics,
334 max_query_limit,
335 default_query_limit,
336 max_newest_metadata_topics,
337 max_newest_full_topics,
338 max_update_adds,
339 max_update_removes,
340 max_stream_topics,
341 max_static_topics,
342 max_lookup_identifiers,
343 max_scw_signatures,
344 max_identity_entries,
345 max_update_frames_per_second,
346 max_update_burst,
347 max_ping_frames_per_second,
348 max_ping_burst,
349 )
350 }
351}
352
353#[derive(Clone, Debug, PartialEq, Eq)]
358pub enum MetadataPolicy {
359 Base(i32),
362 And(Vec<MetadataPolicy>),
364 Any(Vec<MetadataPolicy>),
366}
367
368impl MetadataPolicy {
369 fn is_specified(&self) -> bool {
373 match self {
374 Self::Base(base) => *base > 0,
375 Self::And(policies) | Self::Any(policies) => {
376 !policies.is_empty() && policies.iter().all(Self::is_specified)
377 }
378 }
379 }
380}
381
382#[derive(Clone, Debug, Default, PartialEq, Eq)]
385pub struct ComponentPermissions {
386 pub insert: Option<MetadataPolicy>,
387 pub update: Option<MetadataPolicy>,
388 pub delete: Option<MetadataPolicy>,
389}
390
391#[derive(Clone, Debug, PartialEq, Eq)]
394pub struct ApplicationComponentDefinition {
395 pub component_id: u16,
398 pub name: String,
401 pub component_type: i32,
403 pub permissions: ComponentPermissions,
404 pub in_groups: bool,
405 pub in_dms: bool,
406}
407
408impl ApplicationComponentDefinition {
409 pub fn validate(&self) -> Result<(), ApplicationComponentError> {
411 let ComponentPermissions {
412 insert,
413 update,
414 delete,
415 } = &self.permissions;
416 if !APPLICATION_COMPONENT_IDS.contains(&self.component_id) {
417 Err(ApplicationComponentError::ComponentId)
418 } else if self.name.is_empty() || self.name.len() > MAX_APPLICATION_COMPONENT_NAME_BYTES {
419 Err(ApplicationComponentError::Name)
420 } else if self.component_type <= 0 {
421 Err(ApplicationComponentError::ComponentType)
422 } else if ![insert, update, delete]
423 .iter()
424 .all(|policy| policy.as_ref().is_some_and(MetadataPolicy::is_specified))
425 {
426 Err(ApplicationComponentError::Permissions)
427 } else if !self.in_groups && !self.in_dms {
428 Err(ApplicationComponentError::Conversations)
429 } else {
430 Ok(())
431 }
432 }
433}
434
435#[derive(Clone, Debug, PartialEq, Eq)]
437pub struct MlsConfiguration {
438 pub max_group_members: usize,
439 pub max_installations_per_inbox: usize,
440 pub commit_log_enabled: Option<bool>,
443}
444
445impl MlsConfiguration {
446 pub fn commit_log_enabled(&self) -> bool {
448 self.commit_log_enabled.unwrap_or(ENABLE_COMMIT_LOG)
449 }
450}
451
452impl Default for MlsConfiguration {
453 fn default() -> Self {
454 Self {
455 max_group_members: MAX_GROUP_SIZE,
456 max_installations_per_inbox: MAX_INSTALLATIONS_PER_INBOX,
457 commit_log_enabled: None,
458 }
459 }
460}
461
462#[derive(Clone, Debug, PartialEq, Eq)]
467pub struct HandlesConfiguration {
468 pub min_name_length: u32,
471}
472
473impl Default for HandlesConfiguration {
474 fn default() -> Self {
475 Self {
476 min_name_length: BACKEND_DEFAULT_HANDLE_MIN_NAME_LENGTH,
477 }
478 }
479}
480
481#[derive(Clone, Debug, PartialEq, Eq)]
483pub struct AttachmentsConfiguration {
484 pub base_url: String,
486 pub max_upload_bytes: u64,
487 pub retention_seconds: u64,
488}
489
490#[derive(Clone, Debug, Default, PartialEq, Eq)]
495pub struct ServerConfiguration {
496 pub identifier: String,
498 pub server_version: String,
499 pub min_libxmtp_version: String,
501 pub auth: AuthConfiguration,
502 pub retention: RetentionConfiguration,
503 pub limits: LimitsConfiguration,
504 pub mls: MlsConfiguration,
505 pub handles: HandlesConfiguration,
506 pub attachments: Option<AttachmentsConfiguration>,
508 pub smart_contract_wallet_chains: Vec<String>,
511 pub application_components: Vec<ApplicationComponentDefinition>,
514}
515
516impl ServerConfiguration {
517 pub fn validate(&self) -> Result<(), ServerConfigurationError> {
523 validate_server_identifier(&self.identifier)?;
524 self.minimum_version()?;
525 for chain in &self.smart_contract_wallet_chains {
526 if !is_caip2_chain_id(chain) {
527 return Err(ServerConfigurationError::Chain {
528 chain: chain.clone(),
529 });
530 }
531 }
532 self.validate_magnitudes()?;
533 validate_application_components(&self.application_components).map_err(|(index, reason)| {
534 ServerConfigurationError::ApplicationComponent { index, reason }
535 })
536 }
537
538 fn validate_magnitudes(&self) -> Result<(), ServerConfigurationError> {
543 within_range(
544 "group_message_seconds",
545 self.retention.group_message_seconds,
546 )?;
547 within_range("welcome_seconds", self.retention.welcome_seconds)?;
548 within_range("key_package_seconds", self.retention.key_package_seconds)?;
549
550 macro_rules! bounded {
551 ($owner:ident: $($field:ident),+ $(,)?) => {
552 $(within_range(stringify!($field), self.$owner.$field as u64)?;)+
553 };
554 }
555 bounded!(
556 limits: max_envelope_bytes,
557 max_request_bytes,
558 max_response_bytes,
559 max_publish_topics,
560 max_query_topics,
561 max_query_limit,
562 default_query_limit,
563 max_newest_metadata_topics,
564 max_newest_full_topics,
565 max_update_adds,
566 max_update_removes,
567 max_stream_topics,
568 max_static_topics,
569 max_lookup_identifiers,
570 max_scw_signatures,
571 max_identity_entries,
572 );
573 bounded!(mls: max_group_members, max_installations_per_inbox);
574 Ok(())
575 }
576
577 pub fn minimum_version(&self) -> Result<Option<semver::Version>, ServerConfigurationError> {
579 if self.min_libxmtp_version.is_empty() {
580 return Ok(None);
581 }
582 semver::Version::parse(&self.min_libxmtp_version)
583 .map(Some)
584 .map_err(|_| ServerConfigurationError::MinimumVersion {
585 version: self.min_libxmtp_version.clone(),
586 })
587 }
588
589 pub fn accepts_chain(&self, chain: &str) -> bool {
592 self.smart_contract_wallet_chains
593 .iter()
594 .any(|accepted| accepted == chain)
595 }
596}
597
598pub trait ConfigProvider: std::fmt::Debug + Send + Sync {
603 fn server_configuration(&self) -> &ServerConfiguration;
604}
605
606#[derive(Debug, Clone, Default)]
608pub struct StoredConfigProvider(ServerConfiguration);
609
610impl StoredConfigProvider {
611 pub fn new(configuration: ServerConfiguration) -> Self {
612 Self(configuration)
613 }
614}
615
616impl ConfigProvider for StoredConfigProvider {
617 fn server_configuration(&self) -> &ServerConfiguration {
618 &self.0
619 }
620}
621
622#[derive(Debug, Clone, Default)]
626pub struct StaticConfigProvider(ServerConfiguration);
627
628impl StaticConfigProvider {
629 pub fn new(configuration: ServerConfiguration) -> Self {
630 Self(configuration)
631 }
632
633 pub fn edited(edit: impl FnOnce(&mut ServerConfiguration)) -> Self {
635 let mut configuration = ServerConfiguration {
636 identifier: "org.xmtp.static".to_owned(),
637 ..Default::default()
638 };
639 edit(&mut configuration);
640 Self(configuration)
641 }
642}
643
644impl ConfigProvider for StaticConfigProvider {
645 fn server_configuration(&self) -> &ServerConfiguration {
646 &self.0
647 }
648}
649
650#[cfg(test)]
651mod tests;