Skip to main content

bitwarden_core/client/
client_settings.rs

1use std::{fmt, sync::OnceLock};
2
3use schemars::JsonSchema;
4use serde::{Deserialize, Serialize};
5
6/// Basic client behavior settings. These settings specify the various targets and behavior of the
7/// Bitwarden Client. They are optional and uneditable once the client is initialized.
8///
9/// Defaults to
10///
11/// ```
12/// # use bitwarden_core::{ClientSettings, DeviceType};
13/// let settings = ClientSettings {
14///     identity_url: "https://identity.bitwarden.com".to_string(),
15///     api_url: "https://api.bitwarden.com".to_string(),
16///     user_agent: "Bitwarden Rust-SDK".to_string(),
17///     device_type: DeviceType::SDK,
18///     bitwarden_client_version: None,
19///     bitwarden_package_type: None,
20///     device_identifier: None,
21/// };
22/// let default = ClientSettings::default();
23/// ```
24#[derive(Serialize, Deserialize, Debug, JsonSchema, Clone)]
25#[serde(default, rename_all = "camelCase", deny_unknown_fields)]
26#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
27#[cfg_attr(
28    feature = "wasm",
29    derive(tsify::Tsify),
30    tsify(into_wasm_abi, from_wasm_abi)
31)]
32pub struct ClientSettings {
33    /// The identity url of the targeted Bitwarden instance. Defaults to `https://identity.bitwarden.com`
34    pub identity_url: String,
35    /// The api url of the targeted Bitwarden instance. Defaults to `https://api.bitwarden.com`
36    pub api_url: String,
37    /// The user_agent to sent to Bitwarden. Defaults to `Bitwarden Rust-SDK`
38    pub user_agent: String,
39    /// Device type to send to Bitwarden. Defaults to SDK
40    pub device_type: DeviceType,
41
42    // TODO: PM-29939 - Remove optionality when all clients pass these values
43    /// Device identifier to send to Bitwarden. Optional for now in transition period.
44    pub device_identifier: Option<String>,
45    /// Bitwarden Client Version to send to Bitwarden. Optional for now in transition period.
46    pub bitwarden_client_version: Option<String>,
47    /// Bitwarden Package Type to send to Bitwarden. We should evaluate this field to see if it
48    /// should be optional later.
49    pub bitwarden_package_type: Option<String>,
50}
51
52impl Default for ClientSettings {
53    fn default() -> Self {
54        Self {
55            identity_url: "https://identity.bitwarden.com".into(),
56            api_url: "https://api.bitwarden.com".into(),
57            user_agent: "Bitwarden Rust-SDK".into(),
58            device_type: DeviceType::SDK,
59            device_identifier: None,
60            bitwarden_client_version: None,
61            bitwarden_package_type: None,
62        }
63    }
64}
65
66#[allow(non_camel_case_types, missing_docs)]
67#[derive(Serialize, Deserialize, Copy, Clone, Debug, JsonSchema, PartialEq)]
68#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
69#[cfg_attr(
70    feature = "wasm",
71    derive(tsify::Tsify),
72    tsify(into_wasm_abi, from_wasm_abi)
73)]
74pub enum DeviceType {
75    Android = 0,
76    iOS = 1,
77    ChromeExtension = 2,
78    FirefoxExtension = 3,
79    OperaExtension = 4,
80    EdgeExtension = 5,
81    WindowsDesktop = 6,
82    MacOsDesktop = 7,
83    LinuxDesktop = 8,
84    ChromeBrowser = 9,
85    FirefoxBrowser = 10,
86    OperaBrowser = 11,
87    EdgeBrowser = 12,
88    IEBrowser = 13,
89    UnknownBrowser = 14,
90    AndroidAmazon = 15,
91    UWP = 16,
92    SafariBrowser = 17,
93    VivaldiBrowser = 18,
94    VivaldiExtension = 19,
95    SafariExtension = 20,
96    SDK = 21,
97    Server = 22,
98    WindowsCLI = 23,
99    MacOsCLI = 24,
100    LinuxCLI = 25,
101    DuckDuckGoBrowser = 26,
102}
103
104#[derive(Copy, Clone, Debug)]
105pub enum ClientName {
106    Web,
107    Browser,
108    Desktop,
109    Mobile,
110    Cli,
111}
112
113impl From<DeviceType> for Option<ClientName> {
114    fn from(device_type: DeviceType) -> Self {
115        match device_type {
116            DeviceType::Android | DeviceType::AndroidAmazon | DeviceType::iOS => {
117                Some(ClientName::Mobile)
118            }
119
120            DeviceType::ChromeBrowser
121            | DeviceType::FirefoxBrowser
122            | DeviceType::OperaBrowser
123            | DeviceType::EdgeBrowser
124            | DeviceType::IEBrowser
125            | DeviceType::SafariBrowser
126            | DeviceType::VivaldiBrowser
127            | DeviceType::DuckDuckGoBrowser
128            | DeviceType::UnknownBrowser => Some(ClientName::Web),
129
130            DeviceType::ChromeExtension
131            | DeviceType::FirefoxExtension
132            | DeviceType::OperaExtension
133            | DeviceType::EdgeExtension
134            | DeviceType::VivaldiExtension
135            | DeviceType::SafariExtension => Some(ClientName::Browser),
136
137            DeviceType::LinuxDesktop
138            | DeviceType::MacOsDesktop
139            | DeviceType::WindowsDesktop
140            | DeviceType::UWP => Some(ClientName::Desktop),
141
142            DeviceType::WindowsCLI | DeviceType::MacOsCLI | DeviceType::LinuxCLI => {
143                Some(ClientName::Cli)
144            }
145
146            DeviceType::SDK | DeviceType::Server => None,
147        }
148    }
149}
150
151impl fmt::Display for ClientName {
152    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
153        let s = match self {
154            ClientName::Web => "web",
155            ClientName::Browser => "browser",
156            ClientName::Desktop => "desktop",
157            ClientName::Mobile => "mobile",
158            ClientName::Cli => "cli",
159        };
160        write!(f, "{}", s)
161    }
162}
163
164/// Process-wide, application-provided platform information.
165///
166/// Initialize exactly once at application startup via [`init_host_platform_info`];
167/// read via [`get_host_platform_info`]. Subsequent `init` calls are ignored.
168#[derive(Debug, Clone)]
169pub struct HostPlatformInfo {
170    pub user_agent: String,
171    pub device_type: DeviceType,
172    pub device_identifier: Option<String>,
173    pub bitwarden_client_version: Option<String>,
174    pub bitwarden_package_type: Option<String>,
175}
176
177impl From<&ClientSettings> for HostPlatformInfo {
178    fn from(settings: &ClientSettings) -> Self {
179        Self {
180            user_agent: settings.user_agent.clone(),
181            device_type: settings.device_type,
182            device_identifier: settings.device_identifier.clone(),
183            bitwarden_client_version: settings.bitwarden_client_version.clone(),
184            bitwarden_package_type: settings.bitwarden_package_type.clone(),
185        }
186    }
187}
188
189static HOST_PLATFORM_INFO: OnceLock<HostPlatformInfo> = OnceLock::new();
190
191/// Initialize the global [`HostPlatformInfo`].
192///
193/// Should be called once during application startup, before any
194/// `Client::load_from_state` calls. Subsequent calls are silently ignored.
195pub fn init_host_platform_info(info: HostPlatformInfo) {
196    let _ = HOST_PLATFORM_INFO.set(info);
197}
198
199/// Returns the globally-initialized [`HostPlatformInfo`].
200///
201/// # Panics
202/// Panics if [`init_host_platform_info`] has not yet been called.
203pub fn get_host_platform_info() -> &'static HostPlatformInfo {
204    HOST_PLATFORM_INFO
205        .get()
206        .expect("host platform info to be initialized")
207}
208
209#[cfg(test)]
210mod tests {
211    use super::*;
212
213    // `OnceLock` is process-global. Any test in this binary that calls
214    // `init_host_platform_info` may have run first, so we cannot assert what
215    // value won the first-init race — only that a *subsequent* init does not
216    // change the stored value.
217    #[test]
218    fn second_init_does_not_overwrite_first() {
219        let first = HostPlatformInfo {
220            user_agent: "first".into(),
221            device_type: DeviceType::SDK,
222            device_identifier: Some("dev-1".into()),
223            bitwarden_client_version: Some("1.0.0".into()),
224            bitwarden_package_type: Some("test".into()),
225        };
226        init_host_platform_info(first);
227
228        // Capture whatever value won the first-init race (ours or another
229        // test's).
230        let after_first_init = get_host_platform_info().clone();
231
232        let second = HostPlatformInfo {
233            user_agent: "second".into(),
234            device_type: DeviceType::SDK,
235            device_identifier: Some("dev-2".into()),
236            bitwarden_client_version: Some("2.0.0".into()),
237            bitwarden_package_type: Some("test".into()),
238        };
239        init_host_platform_info(second);
240
241        let after_second_init = get_host_platform_info();
242        assert_eq!(after_second_init.user_agent, after_first_init.user_agent);
243        assert_eq!(after_second_init.device_type, after_first_init.device_type);
244        assert_eq!(
245            after_second_init.device_identifier,
246            after_first_init.device_identifier
247        );
248        assert_eq!(
249            after_second_init.bitwarden_client_version,
250            after_first_init.bitwarden_client_version
251        );
252        assert_eq!(
253            after_second_init.bitwarden_package_type,
254            after_first_init.bitwarden_package_type
255        );
256    }
257}