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, so a single sequential test covers the
214    // not-set, happy-path, and already-set cases without cross-test leakage.
215    #[test]
216    fn init_then_get_preserves_first_value() {
217        // Not-set case: `get` must panic before any `init` call.
218        let prev_hook = std::panic::take_hook();
219        std::panic::set_hook(Box::new(|_| {}));
220        let not_set = std::panic::catch_unwind(get_host_platform_info);
221        std::panic::set_hook(prev_hook);
222        assert!(not_set.is_err(), "expected panic before init");
223
224        let first = HostPlatformInfo {
225            user_agent: "first".into(),
226            device_type: DeviceType::SDK,
227            device_identifier: Some("dev-1".into()),
228            bitwarden_client_version: Some("1.0.0".into()),
229            bitwarden_package_type: Some("test".into()),
230        };
231        init_host_platform_info(first.clone());
232
233        let got = get_host_platform_info();
234        assert_eq!(got.user_agent, first.user_agent);
235        assert_eq!(got.device_type, first.device_type);
236        assert_eq!(got.device_identifier, first.device_identifier);
237
238        let second = HostPlatformInfo {
239            user_agent: "second".into(),
240            ..first.clone()
241        };
242        init_host_platform_info(second);
243
244        let after = get_host_platform_info();
245        assert_eq!(after.user_agent, first.user_agent);
246    }
247}