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/// Why a published configuration cannot be used.
71#[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/// Why one application component definition cannot be used. Carries no
91/// value from the definition, so the backend can report it for a document
92/// whose contents came from the environment.
93#[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
111/// Check each definition alone, then the catalogue for repeated IDs and
112/// names. The error carries the position of the first failing definition.
113///
114/// A type tag or base policy this build does not know is accepted: a newer
115/// backend may publish one, and the entry's group registry, not this check,
116/// decides what a conversation accepts. A clash with a well-known component
117/// name is the backend's check alone, because a later release may add a
118/// well-known name that a deployment already uses.
119// implements: CONF-071, CONF-078
120pub 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
144/// Check the shape of an operator identifier. The backend refuses to start
145/// with one that fails, and a client refuses to store one.
146pub 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
158/// Refuse one published number an SDK integer could not carry exactly.
159fn 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
166/// A CAIP-2 identifier is `namespace:reference`, where the namespace is three
167/// to eight lowercase characters and the reference is one to thirty-two. The
168/// client only needs the shape; the verifier owns the routes.
169pub 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
184/// Compare two versions on major, minor, and patch only. A prerelease tag
185/// never makes a client too old for a minimum it otherwise satisfies.
186///
187/// Returns `true` when `version` is below `minimum`.
188// implements: CONF-050
189pub 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/// Public identity of one accepted signing key. Never the key itself.
194#[derive(Clone, Debug, Default, PartialEq, Eq)]
195pub struct SigningKeyDescription {
196    pub kid: String,
197    pub alg: String,
198}
199
200/// What a client must present to be admitted. The client acts only on
201/// `enabled` and `required_scopes`; the rest exists for operator tooling.
202#[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/// How long the deployment keeps each payload kind, in seconds.
212#[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/// Request shapes the deployment accepts. A client chunks its work to these
230/// values rather than to its compiled constants.
231#[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    /// The same snapshot with every zero replaced by the compiled default.
284    ///
285    /// Wire conversion already applies this rule, so no
286    /// published configuration can carry a zero. A snapshot built in Rust and
287    /// handed in through a `ConfigProvider` skips that conversion,
288    /// and a zero chunk dimension would panic the transport that slices its
289    /// work into chunks of it. Applying the wire rule once more,
290    /// where the transport reads the value, means it never can.
291    // implements: CONF-025
292    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/// A group metadata permission policy, mirroring the wire `MetadataPolicy`.
327///
328/// Tags stay raw integers so a policy this build does not know survives the
329/// trip from the backend to a group registry unchanged.
330#[derive(Clone, Debug, PartialEq, Eq)]
331pub enum MetadataPolicy {
332    /// A `MetadataBasePolicy` tag. `0`, unspecified, also stands for a wire
333    /// policy with no kind set; validation refuses both.
334    Base(i32),
335    /// Every policy must allow.
336    And(Vec<MetadataPolicy>),
337    /// At least one policy must allow.
338    Any(Vec<MetadataPolicy>),
339}
340
341impl MetadataPolicy {
342    /// False when some branch is unspecified or an empty condition, which no
343    /// evaluator can apply. An unknown positive tag passes: a newer client may
344    /// know it.
345    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/// The member policies of one component. `None` is a policy the definition
356/// left out, which a valid definition never does.
357#[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/// One operator-defined group metadata field. A client registers it in a new
365/// group when `in_groups` is set and in a new DM when `in_dms` is set.
366#[derive(Clone, Debug, PartialEq, Eq)]
367pub struct ApplicationComponentDefinition {
368    /// In [`APPLICATION_COMPONENT_IDS`]. A published ID that does not fit in
369    /// a `u16` saturates to `u16::MAX`, which is outside that range.
370    pub component_id: u16,
371    /// A stable label and lookup key, 1 to
372    /// [`MAX_APPLICATION_COMPONENT_NAME_BYTES`] UTF-8 bytes.
373    pub name: String,
374    /// A `ComponentType` tag, never `0` (unspecified).
375    pub component_type: i32,
376    pub permissions: ComponentPermissions,
377    pub in_groups: bool,
378    pub in_dms: bool,
379}
380
381impl ApplicationComponentDefinition {
382    /// The rules one definition must meet on its own.
383    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/// Advisory group shapes. The backend publishes them and does not enforce them.
409#[derive(Clone, Debug, PartialEq, Eq)]
410pub struct MlsConfiguration {
411    pub max_group_members: usize,
412    pub max_installations_per_inbox: usize,
413    /// Absent means the client keeps its compiled default. `false` is distinct
414    /// from absent, so an operator can switch the commit log off explicitly.
415    pub commit_log_enabled: Option<bool>,
416}
417
418impl MlsConfiguration {
419    /// Whether this deployment wants clients to write and read the commit log.
420    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/// Handle settings the deployment applies when it admits a claim. They are
436/// published for apps, for example to hint at a name a user types. The client
437/// never applies them: it neither fails an operation nor hides a handle because
438/// of them.
439#[derive(Clone, Debug, PartialEq, Eq)]
440pub struct HandlesConfiguration {
441    /// The shortest name part, the text before `#`, the backend admits in a
442    /// new claim.
443    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/// Attachment storage offered by this deployment.
455#[derive(Clone, Debug, PartialEq, Eq)]
456pub struct AttachmentsConfiguration {
457    /// The validated value exactly as the backend published it.
458    pub base_url: String,
459    pub max_upload_bytes: u64,
460    pub retention_seconds: u64,
461}
462
463/// One immutable snapshot of what a deployment published.
464///
465/// `Default` is the compiled fallback used before any fetch succeeds, and for
466/// any field the deployment left at zero.
467#[derive(Clone, Debug, Default, PartialEq, Eq)]
468pub struct ServerConfiguration {
469    /// Stable operator-chosen name. Empty only before a first fetch succeeds.
470    pub identifier: String,
471    pub server_version: String,
472    /// Empty when the operator published no minimum.
473    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    /// Absent when attachment storage is not offered or its message is unusable.
480    pub attachments: Option<AttachmentsConfiguration>,
481    /// CAIP-2 chain ids this deployment verifies smart contract wallet
482    /// signatures on. Empty rejects every app-supplied signature.
483    pub smart_contract_wallet_chains: Vec<String>,
484    /// Group metadata fields a client registers in the conversations it
485    /// creates, in published order.
486    pub application_components: Vec<ApplicationComponentDefinition>,
487}
488
489impl ServerConfiguration {
490    /// The identifier must be well formed, any
491    /// minimum version must parse, every chain must be CAIP-2, every
492    /// numeric value must survive the trip to an SDK integer intact, and the
493    /// application catalogue must pass [`validate_application_components`].
494    // implements: CONF-071
495    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    /// §7: every `uint64` reaches JavaScript as a `number`, so a value above
512    /// `MAX_PUBLISHED_VALUE` would arrive rounded. Refusing it here is what
513    /// makes that mapping exact for every SDK. The four `uint32` rates cannot
514    /// reach the bound, so they need no check.
515    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    /// The published minimum, parsed. `None` admits every client version.
551    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    /// Whether this deployment accepts a smart contract wallet signature on
563    /// `chain`. An empty list accepts none.
564    pub fn accepts_chain(&self, chain: &str) -> bool {
565        self.smart_contract_wallet_chains
566            .iter()
567            .any(|accepted| accepted == chain)
568    }
569}
570
571/// One immutable configuration, read by every consumer in section 6.4.
572///
573/// A provider never touches the database. The snapshot is resolved once at
574/// build and handed to the client whole.
575pub trait ConfigProvider:
576    std::fmt::Debug + xmtp_common::MaybeSend + xmtp_common::MaybeSync
577{
578    fn server_configuration(&self) -> &ServerConfiguration;
579}
580
581/// The provider backed by the copy stored in the client database.
582#[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/// A provider a test constructs from any values, with no database and no
598/// fetch. Passing one to the builder disables fetch, store, refresh, and the
599/// identifier check.
600#[derive(Debug, Clone, Default)]
601pub struct StaticConfigProvider(ServerConfiguration);
602
603impl StaticConfigProvider {
604    pub fn new(configuration: ServerConfiguration) -> Self {
605        Self(configuration)
606    }
607
608    /// Start from the compiled defaults and change only what a test needs.
609    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;