1use crate::{
10 BACKEND_DEFAULT_GROUP_MESSAGE_SECONDS, BACKEND_DEFAULT_KEY_PACKAGE_SECONDS,
11 BACKEND_DEFAULT_MAX_ENVELOPE_BYTES, BACKEND_DEFAULT_MAX_IDENTITY_ENTRIES,
12 BACKEND_DEFAULT_MAX_LOOKUP_IDENTIFIERS, BACKEND_DEFAULT_MAX_NEWEST_FULL_TOPICS,
13 BACKEND_DEFAULT_MAX_NEWEST_METADATA_TOPICS, BACKEND_DEFAULT_MAX_PING_BURST,
14 BACKEND_DEFAULT_MAX_PING_FRAMES_PER_SECOND, BACKEND_DEFAULT_MAX_PUBLISH_TOPICS,
15 BACKEND_DEFAULT_MAX_QUERY_LIMIT, BACKEND_DEFAULT_MAX_QUERY_TOPICS,
16 BACKEND_DEFAULT_MAX_REQUEST_BYTES, BACKEND_DEFAULT_MAX_RESPONSE_BYTES,
17 BACKEND_DEFAULT_MAX_SCW_SIGNATURES, BACKEND_DEFAULT_MAX_STATIC_TOPICS,
18 BACKEND_DEFAULT_MAX_STREAM_TOPICS, BACKEND_DEFAULT_MAX_UPDATE_ADDS,
19 BACKEND_DEFAULT_MAX_UPDATE_BURST, BACKEND_DEFAULT_MAX_UPDATE_FRAMES_PER_SECOND,
20 BACKEND_DEFAULT_MAX_UPDATE_REMOVES, BACKEND_DEFAULT_QUERY_LIMIT,
21 BACKEND_DEFAULT_WELCOME_SECONDS, ENABLE_COMMIT_LOG, MAX_GROUP_SIZE,
22 MAX_INSTALLATIONS_PER_INBOX,
23};
24
25pub const CONFIGURATION_REFRESH_INTERVAL: std::time::Duration =
28 std::time::Duration::from_secs(3600);
29
30pub const CONFIGURATION_REFRESH_JITTER: std::time::Duration = std::time::Duration::from_secs(360);
33
34pub const CONFIGURATION_REFRESH_ATTEMPTS: usize = 3;
37
38pub const CONFIGURATION_REFRESH_BACKOFF: [std::time::Duration; 2] = [
40 std::time::Duration::from_secs(5),
41 std::time::Duration::from_secs(30),
42];
43
44pub const MAX_SERVER_IDENTIFIER_BYTES: usize = 256;
47
48pub const MAX_PUBLISHED_VALUE: u64 = 9_007_199_254_740_991;
55
56#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
58pub enum ServerConfigurationError {
59 #[error(
60 "identifier must be 1 to {MAX_SERVER_IDENTIFIER_BYTES} bytes with no whitespace or control characters"
61 )]
62 Identifier,
63 #[error("min_libxmtp_version {version:?} is not a semantic version")]
64 MinimumVersion { version: String },
65 #[error("{chain:?} is not a CAIP-2 chain identifier")]
66 Chain { chain: String },
67 #[error("{field} is {value}, above the {MAX_PUBLISHED_VALUE} an SDK integer holds exactly")]
68 Magnitude { field: &'static str, value: u64 },
69}
70
71pub fn validate_server_identifier(identifier: &str) -> Result<(), ServerConfigurationError> {
74 if identifier.is_empty()
75 || identifier.len() > MAX_SERVER_IDENTIFIER_BYTES
76 || identifier
77 .chars()
78 .any(|c| c.is_whitespace() || c.is_control())
79 {
80 return Err(ServerConfigurationError::Identifier);
81 }
82 Ok(())
83}
84
85fn within_range(field: &'static str, value: u64) -> Result<(), ServerConfigurationError> {
87 if value > MAX_PUBLISHED_VALUE {
88 return Err(ServerConfigurationError::Magnitude { field, value });
89 }
90 Ok(())
91}
92
93pub fn is_caip2_chain_id(chain: &str) -> bool {
97 let Some((namespace, reference)) = chain.split_once(':') else {
98 return false;
99 };
100 (3..=8).contains(&namespace.len())
101 && !reference.is_empty()
102 && reference.len() <= 32
103 && namespace
104 .chars()
105 .all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == '-')
106 && reference
107 .chars()
108 .all(|c| c.is_ascii_alphanumeric() || c == '-' || c == '_')
109}
110
111pub fn version_is_below(version: &semver::Version, minimum: &semver::Version) -> bool {
116 (version.major, version.minor, version.patch) < (minimum.major, minimum.minor, minimum.patch)
117}
118
119#[derive(Clone, Debug, Default, PartialEq, Eq)]
121pub struct SigningKeyDescription {
122 pub kid: String,
123 pub alg: String,
124}
125
126#[derive(Clone, Debug, Default, PartialEq, Eq)]
129pub struct AuthConfiguration {
130 pub enabled: bool,
131 pub keys: Vec<SigningKeyDescription>,
132 pub audiences: Vec<String>,
133 pub issuers: Vec<String>,
134 pub required_scopes: Vec<String>,
135}
136
137#[derive(Clone, Debug, PartialEq, Eq)]
139pub struct RetentionConfiguration {
140 pub group_message_seconds: u64,
141 pub welcome_seconds: u64,
142 pub key_package_seconds: u64,
143}
144
145impl Default for RetentionConfiguration {
146 fn default() -> Self {
147 Self {
148 group_message_seconds: BACKEND_DEFAULT_GROUP_MESSAGE_SECONDS,
149 welcome_seconds: BACKEND_DEFAULT_WELCOME_SECONDS,
150 key_package_seconds: BACKEND_DEFAULT_KEY_PACKAGE_SECONDS,
151 }
152 }
153}
154
155#[derive(Clone, Debug, PartialEq, Eq)]
158pub struct LimitsConfiguration {
159 pub max_envelope_bytes: usize,
160 pub max_request_bytes: usize,
161 pub max_response_bytes: usize,
162 pub max_publish_topics: usize,
163 pub max_query_topics: usize,
164 pub max_query_limit: usize,
165 pub default_query_limit: usize,
166 pub max_newest_metadata_topics: usize,
167 pub max_newest_full_topics: usize,
168 pub max_update_adds: usize,
169 pub max_update_removes: usize,
170 pub max_stream_topics: usize,
171 pub max_static_topics: usize,
172 pub max_lookup_identifiers: usize,
173 pub max_scw_signatures: usize,
174 pub max_identity_entries: usize,
175 pub max_update_frames_per_second: u32,
176 pub max_update_burst: u32,
177 pub max_ping_frames_per_second: u32,
178 pub max_ping_burst: u32,
179}
180
181impl Default for LimitsConfiguration {
182 fn default() -> Self {
183 Self {
184 max_envelope_bytes: BACKEND_DEFAULT_MAX_ENVELOPE_BYTES,
185 max_request_bytes: BACKEND_DEFAULT_MAX_REQUEST_BYTES,
186 max_response_bytes: BACKEND_DEFAULT_MAX_RESPONSE_BYTES,
187 max_publish_topics: BACKEND_DEFAULT_MAX_PUBLISH_TOPICS,
188 max_query_topics: BACKEND_DEFAULT_MAX_QUERY_TOPICS,
189 max_query_limit: BACKEND_DEFAULT_MAX_QUERY_LIMIT,
190 default_query_limit: BACKEND_DEFAULT_QUERY_LIMIT,
191 max_newest_metadata_topics: BACKEND_DEFAULT_MAX_NEWEST_METADATA_TOPICS,
192 max_newest_full_topics: BACKEND_DEFAULT_MAX_NEWEST_FULL_TOPICS,
193 max_update_adds: BACKEND_DEFAULT_MAX_UPDATE_ADDS,
194 max_update_removes: BACKEND_DEFAULT_MAX_UPDATE_REMOVES,
195 max_stream_topics: BACKEND_DEFAULT_MAX_STREAM_TOPICS,
196 max_static_topics: BACKEND_DEFAULT_MAX_STATIC_TOPICS,
197 max_lookup_identifiers: BACKEND_DEFAULT_MAX_LOOKUP_IDENTIFIERS,
198 max_scw_signatures: BACKEND_DEFAULT_MAX_SCW_SIGNATURES,
199 max_identity_entries: BACKEND_DEFAULT_MAX_IDENTITY_ENTRIES,
200 max_update_frames_per_second: BACKEND_DEFAULT_MAX_UPDATE_FRAMES_PER_SECOND,
201 max_update_burst: BACKEND_DEFAULT_MAX_UPDATE_BURST,
202 max_ping_frames_per_second: BACKEND_DEFAULT_MAX_PING_FRAMES_PER_SECOND,
203 max_ping_burst: BACKEND_DEFAULT_MAX_PING_BURST,
204 }
205 }
206}
207
208impl LimitsConfiguration {
209 pub fn without_zeroes(&self) -> Self {
218 let default = Self::default();
219 macro_rules! or_default {
220 ($($field:ident),+ $(,)?) => {
221 Self {
222 $($field: if self.$field == 0 { default.$field } else { self.$field }),+
223 }
224 };
225 }
226 or_default!(
227 max_envelope_bytes,
228 max_request_bytes,
229 max_response_bytes,
230 max_publish_topics,
231 max_query_topics,
232 max_query_limit,
233 default_query_limit,
234 max_newest_metadata_topics,
235 max_newest_full_topics,
236 max_update_adds,
237 max_update_removes,
238 max_stream_topics,
239 max_static_topics,
240 max_lookup_identifiers,
241 max_scw_signatures,
242 max_identity_entries,
243 max_update_frames_per_second,
244 max_update_burst,
245 max_ping_frames_per_second,
246 max_ping_burst,
247 )
248 }
249}
250
251#[derive(Clone, Debug, PartialEq, Eq)]
253pub struct MlsConfiguration {
254 pub max_group_members: usize,
255 pub max_installations_per_inbox: usize,
256 pub commit_log_enabled: Option<bool>,
259}
260
261impl MlsConfiguration {
262 pub fn commit_log_enabled(&self) -> bool {
264 self.commit_log_enabled.unwrap_or(ENABLE_COMMIT_LOG)
265 }
266}
267
268impl Default for MlsConfiguration {
269 fn default() -> Self {
270 Self {
271 max_group_members: MAX_GROUP_SIZE,
272 max_installations_per_inbox: MAX_INSTALLATIONS_PER_INBOX,
273 commit_log_enabled: None,
274 }
275 }
276}
277
278#[derive(Clone, Debug, Default, PartialEq, Eq)]
283pub struct ServerConfiguration {
284 pub identifier: String,
286 pub server_version: String,
287 pub min_libxmtp_version: String,
289 pub auth: AuthConfiguration,
290 pub retention: RetentionConfiguration,
291 pub limits: LimitsConfiguration,
292 pub mls: MlsConfiguration,
293 pub smart_contract_wallet_chains: Vec<String>,
296}
297
298impl ServerConfiguration {
299 pub fn validate(&self) -> Result<(), ServerConfigurationError> {
303 validate_server_identifier(&self.identifier)?;
304 self.minimum_version()?;
305 for chain in &self.smart_contract_wallet_chains {
306 if !is_caip2_chain_id(chain) {
307 return Err(ServerConfigurationError::Chain {
308 chain: chain.clone(),
309 });
310 }
311 }
312 self.validate_magnitudes()
313 }
314
315 fn validate_magnitudes(&self) -> Result<(), ServerConfigurationError> {
320 within_range(
321 "group_message_seconds",
322 self.retention.group_message_seconds,
323 )?;
324 within_range("welcome_seconds", self.retention.welcome_seconds)?;
325 within_range("key_package_seconds", self.retention.key_package_seconds)?;
326
327 macro_rules! bounded {
328 ($owner:ident: $($field:ident),+ $(,)?) => {
329 $(within_range(stringify!($field), self.$owner.$field as u64)?;)+
330 };
331 }
332 bounded!(
333 limits: max_envelope_bytes,
334 max_request_bytes,
335 max_response_bytes,
336 max_publish_topics,
337 max_query_topics,
338 max_query_limit,
339 default_query_limit,
340 max_newest_metadata_topics,
341 max_newest_full_topics,
342 max_update_adds,
343 max_update_removes,
344 max_stream_topics,
345 max_static_topics,
346 max_lookup_identifiers,
347 max_scw_signatures,
348 max_identity_entries,
349 );
350 bounded!(mls: max_group_members, max_installations_per_inbox);
351 Ok(())
352 }
353
354 pub fn minimum_version(&self) -> Result<Option<semver::Version>, ServerConfigurationError> {
356 if self.min_libxmtp_version.is_empty() {
357 return Ok(None);
358 }
359 semver::Version::parse(&self.min_libxmtp_version)
360 .map(Some)
361 .map_err(|_| ServerConfigurationError::MinimumVersion {
362 version: self.min_libxmtp_version.clone(),
363 })
364 }
365
366 pub fn accepts_chain(&self, chain: &str) -> bool {
369 self.smart_contract_wallet_chains
370 .iter()
371 .any(|accepted| accepted == chain)
372 }
373}
374
375pub trait ConfigProvider:
380 std::fmt::Debug + xmtp_common::MaybeSend + xmtp_common::MaybeSync
381{
382 fn server_configuration(&self) -> &ServerConfiguration;
383}
384
385#[derive(Debug, Clone, Default)]
387pub struct StoredConfigProvider(ServerConfiguration);
388
389impl StoredConfigProvider {
390 pub fn new(configuration: ServerConfiguration) -> Self {
391 Self(configuration)
392 }
393}
394
395impl ConfigProvider for StoredConfigProvider {
396 fn server_configuration(&self) -> &ServerConfiguration {
397 &self.0
398 }
399}
400
401#[derive(Debug, Clone, Default)]
405pub struct StaticConfigProvider(ServerConfiguration);
406
407impl StaticConfigProvider {
408 pub fn new(configuration: ServerConfiguration) -> Self {
409 Self(configuration)
410 }
411
412 pub fn edited(edit: impl FnOnce(&mut ServerConfiguration)) -> Self {
414 let mut configuration = ServerConfiguration {
415 identifier: "org.xmtp.static".to_owned(),
416 ..Default::default()
417 };
418 edit(&mut configuration);
419 Self(configuration)
420 }
421}
422
423impl ConfigProvider for StaticConfigProvider {
424 fn server_configuration(&self) -> &ServerConfiguration {
425 &self.0
426 }
427}
428
429#[cfg(test)]
430mod tests;