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
70#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
72pub enum ServerConfigurationError {
73 #[error(
74 "identifier must be 1 to {MAX_SERVER_IDENTIFIER_BYTES} bytes with no whitespace or control characters"
75 )]
76 Identifier,
77 #[error("min_libxmtp_version {version:?} is not a semantic version")]
78 MinimumVersion { version: String },
79 #[error("{chain:?} is not a CAIP-2 chain identifier")]
80 Chain { chain: String },
81 #[error("{field} is {value}, above the {MAX_PUBLISHED_VALUE} an SDK integer holds exactly")]
82 Magnitude { field: &'static str, value: u64 },
83 #[error("application_components[{index}]: {reason}")]
84 ApplicationComponent {
85 index: usize,
86 reason: ApplicationComponentError,
87 },
88}
89
90#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
94pub enum ApplicationComponentError {
95 #[error("component_id must be in 0xC000-0xFEFF")]
96 ComponentId,
97 #[error("name must be 1 to {MAX_APPLICATION_COMPONENT_NAME_BYTES} UTF-8 bytes")]
98 Name,
99 #[error("component_type must be set")]
100 ComponentType,
101 #[error("insert, update, and delete policies must all be set and non-empty")]
102 Permissions,
103 #[error("in_groups or in_dms must be true")]
104 Conversations,
105 #[error("component_id repeats an earlier definition")]
106 DuplicateId,
107 #[error("name repeats an earlier definition")]
108 DuplicateName,
109}
110
111pub fn validate_application_components(
121 definitions: &[ApplicationComponentDefinition],
122) -> Result<(), (usize, ApplicationComponentError)> {
123 let mut ids = std::collections::HashSet::new();
124 let mut names = std::collections::HashSet::new();
125 definitions
126 .iter()
127 .enumerate()
128 .try_for_each(|(index, definition)| {
129 definition
130 .validate()
131 .and_then(|()| {
132 if !ids.insert(definition.component_id) {
133 return Err(ApplicationComponentError::DuplicateId);
134 }
135 if !names.insert(definition.name.as_str()) {
136 return Err(ApplicationComponentError::DuplicateName);
137 }
138 Ok(())
139 })
140 .map_err(|reason| (index, reason))
141 })
142}
143
144pub fn validate_server_identifier(identifier: &str) -> Result<(), ServerConfigurationError> {
147 if identifier.is_empty()
148 || identifier.len() > MAX_SERVER_IDENTIFIER_BYTES
149 || identifier
150 .chars()
151 .any(|c| c.is_whitespace() || c.is_control())
152 {
153 return Err(ServerConfigurationError::Identifier);
154 }
155 Ok(())
156}
157
158fn within_range(field: &'static str, value: u64) -> Result<(), ServerConfigurationError> {
160 if value > MAX_PUBLISHED_VALUE {
161 return Err(ServerConfigurationError::Magnitude { field, value });
162 }
163 Ok(())
164}
165
166pub fn is_caip2_chain_id(chain: &str) -> bool {
170 let Some((namespace, reference)) = chain.split_once(':') else {
171 return false;
172 };
173 (3..=8).contains(&namespace.len())
174 && !reference.is_empty()
175 && reference.len() <= 32
176 && namespace
177 .chars()
178 .all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == '-')
179 && reference
180 .chars()
181 .all(|c| c.is_ascii_alphanumeric() || c == '-' || c == '_')
182}
183
184pub fn version_is_below(version: &semver::Version, minimum: &semver::Version) -> bool {
190 (version.major, version.minor, version.patch) < (minimum.major, minimum.minor, minimum.patch)
191}
192
193#[derive(Clone, Debug, Default, PartialEq, Eq)]
195pub struct SigningKeyDescription {
196 pub kid: String,
197 pub alg: String,
198}
199
200#[derive(Clone, Debug, Default, PartialEq, Eq)]
203pub struct AuthConfiguration {
204 pub enabled: bool,
205 pub keys: Vec<SigningKeyDescription>,
206 pub audiences: Vec<String>,
207 pub issuers: Vec<String>,
208 pub required_scopes: Vec<String>,
209}
210
211#[derive(Clone, Debug, PartialEq, Eq)]
213pub struct RetentionConfiguration {
214 pub group_message_seconds: u64,
215 pub welcome_seconds: u64,
216 pub key_package_seconds: u64,
217}
218
219impl Default for RetentionConfiguration {
220 fn default() -> Self {
221 Self {
222 group_message_seconds: BACKEND_DEFAULT_GROUP_MESSAGE_SECONDS,
223 welcome_seconds: BACKEND_DEFAULT_WELCOME_SECONDS,
224 key_package_seconds: BACKEND_DEFAULT_KEY_PACKAGE_SECONDS,
225 }
226 }
227}
228
229#[derive(Clone, Debug, PartialEq, Eq)]
232pub struct LimitsConfiguration {
233 pub max_envelope_bytes: usize,
234 pub max_request_bytes: usize,
235 pub max_response_bytes: usize,
236 pub max_publish_topics: usize,
237 pub max_query_topics: usize,
238 pub max_query_limit: usize,
239 pub default_query_limit: usize,
240 pub max_newest_metadata_topics: usize,
241 pub max_newest_full_topics: usize,
242 pub max_update_adds: usize,
243 pub max_update_removes: usize,
244 pub max_stream_topics: usize,
245 pub max_static_topics: usize,
246 pub max_lookup_identifiers: usize,
247 pub max_scw_signatures: usize,
248 pub max_identity_entries: usize,
249 pub max_update_frames_per_second: u32,
250 pub max_update_burst: u32,
251 pub max_ping_frames_per_second: u32,
252 pub max_ping_burst: u32,
253}
254
255impl Default for LimitsConfiguration {
256 fn default() -> Self {
257 Self {
258 max_envelope_bytes: BACKEND_DEFAULT_MAX_ENVELOPE_BYTES,
259 max_request_bytes: BACKEND_DEFAULT_MAX_REQUEST_BYTES,
260 max_response_bytes: BACKEND_DEFAULT_MAX_RESPONSE_BYTES,
261 max_publish_topics: BACKEND_DEFAULT_MAX_PUBLISH_TOPICS,
262 max_query_topics: BACKEND_DEFAULT_MAX_QUERY_TOPICS,
263 max_query_limit: BACKEND_DEFAULT_MAX_QUERY_LIMIT,
264 default_query_limit: BACKEND_DEFAULT_QUERY_LIMIT,
265 max_newest_metadata_topics: BACKEND_DEFAULT_MAX_NEWEST_METADATA_TOPICS,
266 max_newest_full_topics: BACKEND_DEFAULT_MAX_NEWEST_FULL_TOPICS,
267 max_update_adds: BACKEND_DEFAULT_MAX_UPDATE_ADDS,
268 max_update_removes: BACKEND_DEFAULT_MAX_UPDATE_REMOVES,
269 max_stream_topics: BACKEND_DEFAULT_MAX_STREAM_TOPICS,
270 max_static_topics: BACKEND_DEFAULT_MAX_STATIC_TOPICS,
271 max_lookup_identifiers: BACKEND_DEFAULT_MAX_LOOKUP_IDENTIFIERS,
272 max_scw_signatures: BACKEND_DEFAULT_MAX_SCW_SIGNATURES,
273 max_identity_entries: BACKEND_DEFAULT_MAX_IDENTITY_ENTRIES,
274 max_update_frames_per_second: BACKEND_DEFAULT_MAX_UPDATE_FRAMES_PER_SECOND,
275 max_update_burst: BACKEND_DEFAULT_MAX_UPDATE_BURST,
276 max_ping_frames_per_second: BACKEND_DEFAULT_MAX_PING_FRAMES_PER_SECOND,
277 max_ping_burst: BACKEND_DEFAULT_MAX_PING_BURST,
278 }
279 }
280}
281
282impl LimitsConfiguration {
283 pub fn without_zeroes(&self) -> Self {
293 let default = Self::default();
294 macro_rules! or_default {
295 ($($field:ident),+ $(,)?) => {
296 Self {
297 $($field: if self.$field == 0 { default.$field } else { self.$field }),+
298 }
299 };
300 }
301 or_default!(
302 max_envelope_bytes,
303 max_request_bytes,
304 max_response_bytes,
305 max_publish_topics,
306 max_query_topics,
307 max_query_limit,
308 default_query_limit,
309 max_newest_metadata_topics,
310 max_newest_full_topics,
311 max_update_adds,
312 max_update_removes,
313 max_stream_topics,
314 max_static_topics,
315 max_lookup_identifiers,
316 max_scw_signatures,
317 max_identity_entries,
318 max_update_frames_per_second,
319 max_update_burst,
320 max_ping_frames_per_second,
321 max_ping_burst,
322 )
323 }
324}
325
326#[derive(Clone, Debug, PartialEq, Eq)]
331pub enum MetadataPolicy {
332 Base(i32),
335 And(Vec<MetadataPolicy>),
337 Any(Vec<MetadataPolicy>),
339}
340
341impl MetadataPolicy {
342 fn is_specified(&self) -> bool {
346 match self {
347 Self::Base(base) => *base > 0,
348 Self::And(policies) | Self::Any(policies) => {
349 !policies.is_empty() && policies.iter().all(Self::is_specified)
350 }
351 }
352 }
353}
354
355#[derive(Clone, Debug, Default, PartialEq, Eq)]
358pub struct ComponentPermissions {
359 pub insert: Option<MetadataPolicy>,
360 pub update: Option<MetadataPolicy>,
361 pub delete: Option<MetadataPolicy>,
362}
363
364#[derive(Clone, Debug, PartialEq, Eq)]
367pub struct ApplicationComponentDefinition {
368 pub component_id: u16,
371 pub name: String,
374 pub component_type: i32,
376 pub permissions: ComponentPermissions,
377 pub in_groups: bool,
378 pub in_dms: bool,
379}
380
381impl ApplicationComponentDefinition {
382 pub fn validate(&self) -> Result<(), ApplicationComponentError> {
384 let ComponentPermissions {
385 insert,
386 update,
387 delete,
388 } = &self.permissions;
389 if !APPLICATION_COMPONENT_IDS.contains(&self.component_id) {
390 Err(ApplicationComponentError::ComponentId)
391 } else if self.name.is_empty() || self.name.len() > MAX_APPLICATION_COMPONENT_NAME_BYTES {
392 Err(ApplicationComponentError::Name)
393 } else if self.component_type <= 0 {
394 Err(ApplicationComponentError::ComponentType)
395 } else if ![insert, update, delete]
396 .iter()
397 .all(|policy| policy.as_ref().is_some_and(MetadataPolicy::is_specified))
398 {
399 Err(ApplicationComponentError::Permissions)
400 } else if !self.in_groups && !self.in_dms {
401 Err(ApplicationComponentError::Conversations)
402 } else {
403 Ok(())
404 }
405 }
406}
407
408#[derive(Clone, Debug, PartialEq, Eq)]
410pub struct MlsConfiguration {
411 pub max_group_members: usize,
412 pub max_installations_per_inbox: usize,
413 pub commit_log_enabled: Option<bool>,
416}
417
418impl MlsConfiguration {
419 pub fn commit_log_enabled(&self) -> bool {
421 self.commit_log_enabled.unwrap_or(ENABLE_COMMIT_LOG)
422 }
423}
424
425impl Default for MlsConfiguration {
426 fn default() -> Self {
427 Self {
428 max_group_members: MAX_GROUP_SIZE,
429 max_installations_per_inbox: MAX_INSTALLATIONS_PER_INBOX,
430 commit_log_enabled: None,
431 }
432 }
433}
434
435#[derive(Clone, Debug, PartialEq, Eq)]
440pub struct HandlesConfiguration {
441 pub min_name_length: u32,
444}
445
446impl Default for HandlesConfiguration {
447 fn default() -> Self {
448 Self {
449 min_name_length: BACKEND_DEFAULT_HANDLE_MIN_NAME_LENGTH,
450 }
451 }
452}
453
454#[derive(Clone, Debug, PartialEq, Eq)]
456pub struct AttachmentsConfiguration {
457 pub base_url: String,
459 pub max_upload_bytes: u64,
460 pub retention_seconds: u64,
461}
462
463#[derive(Clone, Debug, Default, PartialEq, Eq)]
468pub struct ServerConfiguration {
469 pub identifier: String,
471 pub server_version: String,
472 pub min_libxmtp_version: String,
474 pub auth: AuthConfiguration,
475 pub retention: RetentionConfiguration,
476 pub limits: LimitsConfiguration,
477 pub mls: MlsConfiguration,
478 pub handles: HandlesConfiguration,
479 pub attachments: Option<AttachmentsConfiguration>,
481 pub smart_contract_wallet_chains: Vec<String>,
484 pub application_components: Vec<ApplicationComponentDefinition>,
487}
488
489impl ServerConfiguration {
490 pub fn validate(&self) -> Result<(), ServerConfigurationError> {
496 validate_server_identifier(&self.identifier)?;
497 self.minimum_version()?;
498 for chain in &self.smart_contract_wallet_chains {
499 if !is_caip2_chain_id(chain) {
500 return Err(ServerConfigurationError::Chain {
501 chain: chain.clone(),
502 });
503 }
504 }
505 self.validate_magnitudes()?;
506 validate_application_components(&self.application_components).map_err(|(index, reason)| {
507 ServerConfigurationError::ApplicationComponent { index, reason }
508 })
509 }
510
511 fn validate_magnitudes(&self) -> Result<(), ServerConfigurationError> {
516 within_range(
517 "group_message_seconds",
518 self.retention.group_message_seconds,
519 )?;
520 within_range("welcome_seconds", self.retention.welcome_seconds)?;
521 within_range("key_package_seconds", self.retention.key_package_seconds)?;
522
523 macro_rules! bounded {
524 ($owner:ident: $($field:ident),+ $(,)?) => {
525 $(within_range(stringify!($field), self.$owner.$field as u64)?;)+
526 };
527 }
528 bounded!(
529 limits: max_envelope_bytes,
530 max_request_bytes,
531 max_response_bytes,
532 max_publish_topics,
533 max_query_topics,
534 max_query_limit,
535 default_query_limit,
536 max_newest_metadata_topics,
537 max_newest_full_topics,
538 max_update_adds,
539 max_update_removes,
540 max_stream_topics,
541 max_static_topics,
542 max_lookup_identifiers,
543 max_scw_signatures,
544 max_identity_entries,
545 );
546 bounded!(mls: max_group_members, max_installations_per_inbox);
547 Ok(())
548 }
549
550 pub fn minimum_version(&self) -> Result<Option<semver::Version>, ServerConfigurationError> {
552 if self.min_libxmtp_version.is_empty() {
553 return Ok(None);
554 }
555 semver::Version::parse(&self.min_libxmtp_version)
556 .map(Some)
557 .map_err(|_| ServerConfigurationError::MinimumVersion {
558 version: self.min_libxmtp_version.clone(),
559 })
560 }
561
562 pub fn accepts_chain(&self, chain: &str) -> bool {
565 self.smart_contract_wallet_chains
566 .iter()
567 .any(|accepted| accepted == chain)
568 }
569}
570
571pub trait ConfigProvider:
576 std::fmt::Debug + xmtp_common::MaybeSend + xmtp_common::MaybeSync
577{
578 fn server_configuration(&self) -> &ServerConfiguration;
579}
580
581#[derive(Debug, Clone, Default)]
583pub struct StoredConfigProvider(ServerConfiguration);
584
585impl StoredConfigProvider {
586 pub fn new(configuration: ServerConfiguration) -> Self {
587 Self(configuration)
588 }
589}
590
591impl ConfigProvider for StoredConfigProvider {
592 fn server_configuration(&self) -> &ServerConfiguration {
593 &self.0
594 }
595}
596
597#[derive(Debug, Clone, Default)]
601pub struct StaticConfigProvider(ServerConfiguration);
602
603impl StaticConfigProvider {
604 pub fn new(configuration: ServerConfiguration) -> Self {
605 Self(configuration)
606 }
607
608 pub fn edited(edit: impl FnOnce(&mut ServerConfiguration)) -> Self {
610 let mut configuration = ServerConfiguration {
611 identifier: "org.xmtp.static".to_owned(),
612 ..Default::default()
613 };
614 edit(&mut configuration);
615 Self(configuration)
616 }
617}
618
619impl ConfigProvider for StaticConfigProvider {
620 fn server_configuration(&self) -> &ServerConfiguration {
621 &self.0
622 }
623}
624
625#[cfg(test)]
626mod tests;