Skip to main content

xmtp_configuration/common/
server.rs

1//! What one backend deployment publishes about itself, and the provider every
2//! consumer reads it through.
3//!
4//! The backend answers `ConfigurationService.GetConfiguration` with these
5//! values. A client fetches them once, stores them, and holds one immutable
6//! snapshot for the life of the client. Consumers never read the database for
7//! a configuration value; they read the provider.
8//!
9//! The snapshot also carries the operator's application component catalogue
10//! ([`ApplicationComponentDefinition`]). It is a sending catalogue: a client
11//! copies an entry into the registry of a conversation it creates, and the
12//! committed group registry, never this snapshot, governs what it receives.
13//! [`validate_application_components`] holds the structural rules that the
14//! backend applies at startup and a client applies before it stores an answer.
15
16use 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
32/// How long after one refresh run ends before the next one starts, before
33/// jitter. The first run starts this long after build.
34pub const CONFIGURATION_REFRESH_INTERVAL: std::time::Duration =
35    std::time::Duration::from_secs(3600);
36
37/// Random spread added to each wait, so a fleet of clients does not refresh
38/// at the same time.
39pub const CONFIGURATION_REFRESH_JITTER: std::time::Duration = std::time::Duration::from_secs(360);
40
41/// Attempts in one refresh run. After the last failure the run ends and the
42/// stored copy is left alone.
43pub const CONFIGURATION_REFRESH_ATTEMPTS: usize = 3;
44
45/// Waits between the attempts of one run.
46pub const CONFIGURATION_REFRESH_BACKOFF: [std::time::Duration; 2] = [
47    std::time::Duration::from_secs(5),
48    std::time::Duration::from_secs(30),
49];
50
51/// Longest operator identifier, in bytes. Shared with the backend so one rule
52/// governs what it accepts and what a client will store.
53pub const MAX_SERVER_IDENTIFIER_BYTES: usize = 256;
54
55/// Largest published numeric value, `2^53 - 1`.
56///
57/// The SDK maps every `uint64` to a JavaScript `number`, which holds
58/// integers exactly only up to this value. A deployment that publishes more
59/// would reach a JavaScript app rounded, so the client refuses it
60/// rather than reading a number the deployment never published.
61pub const MAX_PUBLISHED_VALUE: u64 = 9_007_199_254_740_991;
62
63/// Component IDs an operator may define, `0xC000`–`0xFEFF`. The group
64/// metadata ID ranges derive from this, so both sides agree on one range.
65pub const APPLICATION_COMPONENT_IDS: std::ops::RangeInclusive<u16> = 0xC000..=0xFEFF;
66
67/// Longest application component name, in UTF-8 bytes.
68pub const MAX_APPLICATION_COMPONENT_NAME_BYTES: usize = 100;
69
70/// Every well-known group metadata component, as its ID and its META
71/// section 2 name, in ID order.
72///
73/// The client names well-known components from this table, and the backend
74/// refuses an application component that takes one of these names.
75pub 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/// Why a published configuration cannot be used.
98#[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/// Why one application component definition cannot be used. Carries no
118/// value from the definition, so the backend can report it for a document
119/// whose contents came from the environment.
120#[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
138/// Check each definition alone, then the catalogue for repeated IDs and
139/// names. The error carries the position of the first failing definition.
140///
141/// A type tag or base policy this build does not know is accepted: a newer
142/// backend may publish one, and the entry's group registry, not this check,
143/// decides what a conversation accepts. A clash with a well-known component
144/// name is the backend's check alone, because a later release may add a
145/// well-known name that a deployment already uses.
146// implements: CONF-071, CONF-078
147pub 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
171/// Check the shape of an operator identifier. The backend refuses to start
172/// with one that fails, and a client refuses to store one.
173pub 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
185/// Refuse one published number an SDK integer could not carry exactly.
186fn 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
193/// A CAIP-2 identifier is `namespace:reference`, where the namespace is three
194/// to eight lowercase characters and the reference is one to thirty-two. The
195/// client only needs the shape; the verifier owns the routes.
196pub 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
211/// Compare two versions on major, minor, and patch only. A prerelease tag
212/// never makes a client too old for a minimum it otherwise satisfies.
213///
214/// Returns `true` when `version` is below `minimum`.
215// implements: CONF-050
216pub 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/// Public identity of one accepted signing key. Never the key itself.
221#[derive(Clone, Debug, Default, PartialEq, Eq)]
222pub struct SigningKeyDescription {
223    pub kid: String,
224    pub alg: String,
225}
226
227/// What a client must present to be admitted. The client acts only on
228/// `enabled` and `required_scopes`; the rest exists for operator tooling.
229#[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/// How long the deployment keeps each payload kind, in seconds.
239#[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/// Request shapes the deployment accepts. A client chunks its work to these
257/// values rather than to its compiled constants.
258#[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    /// The same snapshot with every zero replaced by the compiled default.
311    ///
312    /// Wire conversion already applies this rule, so no
313    /// published configuration can carry a zero. A snapshot built in Rust and
314    /// handed in through a `ConfigProvider` skips that conversion,
315    /// and a zero chunk dimension would panic the transport that slices its
316    /// work into chunks of it. Applying the wire rule once more,
317    /// where the transport reads the value, means it never can.
318    // implements: CONF-025
319    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/// A group metadata permission policy, mirroring the wire `MetadataPolicy`.
354///
355/// Tags stay raw integers so a policy this build does not know survives the
356/// trip from the backend to a group registry unchanged.
357#[derive(Clone, Debug, PartialEq, Eq)]
358pub enum MetadataPolicy {
359    /// A `MetadataBasePolicy` tag. `0`, unspecified, also stands for a wire
360    /// policy with no kind set; validation refuses both.
361    Base(i32),
362    /// Every policy must allow.
363    And(Vec<MetadataPolicy>),
364    /// At least one policy must allow.
365    Any(Vec<MetadataPolicy>),
366}
367
368impl MetadataPolicy {
369    /// False when some branch is unspecified or an empty condition, which no
370    /// evaluator can apply. An unknown positive tag passes: a newer client may
371    /// know it.
372    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/// The member policies of one component. `None` is a policy the definition
383/// left out, which a valid definition never does.
384#[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/// One operator-defined group metadata field. A client registers it in a new
392/// group when `in_groups` is set and in a new DM when `in_dms` is set.
393#[derive(Clone, Debug, PartialEq, Eq)]
394pub struct ApplicationComponentDefinition {
395    /// In [`APPLICATION_COMPONENT_IDS`]. A published ID that does not fit in
396    /// a `u16` saturates to `u16::MAX`, which is outside that range.
397    pub component_id: u16,
398    /// A stable label and lookup key, 1 to
399    /// [`MAX_APPLICATION_COMPONENT_NAME_BYTES`] UTF-8 bytes.
400    pub name: String,
401    /// A `ComponentType` tag, never `0` (unspecified).
402    pub component_type: i32,
403    pub permissions: ComponentPermissions,
404    pub in_groups: bool,
405    pub in_dms: bool,
406}
407
408impl ApplicationComponentDefinition {
409    /// The rules one definition must meet on its own.
410    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/// Advisory group shapes. The backend publishes them and does not enforce them.
436#[derive(Clone, Debug, PartialEq, Eq)]
437pub struct MlsConfiguration {
438    pub max_group_members: usize,
439    pub max_installations_per_inbox: usize,
440    /// Absent means the client keeps its compiled default. `false` is distinct
441    /// from absent, so an operator can switch the commit log off explicitly.
442    pub commit_log_enabled: Option<bool>,
443}
444
445impl MlsConfiguration {
446    /// Whether this deployment wants clients to write and read the commit log.
447    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/// Handle settings the deployment applies when it admits a claim. They are
463/// published for apps, for example to hint at a name a user types. The client
464/// never applies them: it neither fails an operation nor hides a handle because
465/// of them.
466#[derive(Clone, Debug, PartialEq, Eq)]
467pub struct HandlesConfiguration {
468    /// The shortest name part, the text before `#`, the backend admits in a
469    /// new claim.
470    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/// Attachment storage offered by this deployment.
482#[derive(Clone, Debug, PartialEq, Eq)]
483pub struct AttachmentsConfiguration {
484    /// The validated value exactly as the backend published it.
485    pub base_url: String,
486    pub max_upload_bytes: u64,
487    pub retention_seconds: u64,
488}
489
490/// One immutable snapshot of what a deployment published.
491///
492/// `Default` is the compiled fallback used before any fetch succeeds, and for
493/// any field the deployment left at zero.
494#[derive(Clone, Debug, Default, PartialEq, Eq)]
495pub struct ServerConfiguration {
496    /// Stable operator-chosen name. Empty only before a first fetch succeeds.
497    pub identifier: String,
498    pub server_version: String,
499    /// Empty when the operator published no minimum.
500    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    /// Absent when attachment storage is not offered or its message is unusable.
507    pub attachments: Option<AttachmentsConfiguration>,
508    /// CAIP-2 chain ids this deployment verifies smart contract wallet
509    /// signatures on. Empty rejects every app-supplied signature.
510    pub smart_contract_wallet_chains: Vec<String>,
511    /// Group metadata fields a client registers in the conversations it
512    /// creates, in published order.
513    pub application_components: Vec<ApplicationComponentDefinition>,
514}
515
516impl ServerConfiguration {
517    /// The identifier must be well formed, any
518    /// minimum version must parse, every chain must be CAIP-2, every
519    /// numeric value must survive the trip to an SDK integer intact, and the
520    /// application catalogue must pass [`validate_application_components`].
521    // implements: CONF-071
522    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    /// §7: every `uint64` reaches JavaScript as a `number`, so a value above
539    /// `MAX_PUBLISHED_VALUE` would arrive rounded. Refusing it here is what
540    /// makes that mapping exact for every SDK. The four `uint32` rates cannot
541    /// reach the bound, so they need no check.
542    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    /// The published minimum, parsed. `None` admits every client version.
578    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    /// Whether this deployment accepts a smart contract wallet signature on
590    /// `chain`. An empty list accepts none.
591    pub fn accepts_chain(&self, chain: &str) -> bool {
592        self.smart_contract_wallet_chains
593            .iter()
594            .any(|accepted| accepted == chain)
595    }
596}
597
598/// One immutable configuration, read by every consumer in section 6.4.
599///
600/// A provider never touches the database. The snapshot is resolved once at
601/// build and handed to the client whole.
602pub trait ConfigProvider: std::fmt::Debug + Send + Sync {
603    fn server_configuration(&self) -> &ServerConfiguration;
604}
605
606/// The provider backed by the copy stored in the client database.
607#[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/// A provider a test constructs from any values, with no database and no
623/// fetch. Passing one to the builder disables fetch, store, refresh, and the
624/// identifier check.
625#[derive(Debug, Clone, Default)]
626pub struct StaticConfigProvider(ServerConfiguration);
627
628impl StaticConfigProvider {
629    pub fn new(configuration: ServerConfiguration) -> Self {
630        Self(configuration)
631    }
632
633    /// Start from the compiled defaults and change only what a test needs.
634    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;