diff --git a/crates/bitwarden-core/README.md b/crates/bitwarden-core/README.md index 9b8b6e01c..7f2f20a91 100644 --- a/crates/bitwarden-core/README.md +++ b/crates/bitwarden-core/README.md @@ -1,17 +1,201 @@ # Bitwarden Core -Contains core functionality used by the feature crates. For an introduction to the Bitwarden SDK and -the `bitwarden-core` create please refer to the +Core infrastructure crate providing the base `Client` type - a container for runtime persistent data +and shared infrastructure that feature crates extend via extension traits. For an introduction to +the SDK architecture, see the [SDK Architecture](https://contributing.bitwarden.com/architecture/sdk/) documentation. -
-Generally you should not find yourself needing to edit this crate! When possible, please use the feature crates instead. -
+> [!WARNING] Do not add business logic or feature-specific functionality to this crate. Use feature +> crates instead. -## Features +## `Client` structure -- `internal` - Internal unstable APIs that should only be consumed by internal Bitwarden clients. -- `no-memory-hardening` - Disables `bitwarden-crypto` memory hardening. -- `secrets` - Secrets Manager specific functionality. -- `uniffi` - Mobile bindings. -- `wasm` - WebAssembly bindings. +The `Client` type serves as a **container for runtime persistent data**, which is intended to +persist for the lifetime of the SDK instance. Think of this as "dependency injection" for the SDK +instance. It should only contain: + +1. **User identity**: + - `UserId` - Ensures each client is immutably associated with one user +2. **Security state**: + - `KeyStore` - Secure in-memory key management +3. **Network state**: + - `ApiClient`/`ApiConfigurations` - HTTP client initialized once and reused + - `Tokens` enum - Includes `ClientManagedTokens` trait and `SdkManagedTokens` struct for access + token management +4. **Storage state**: + - Database/state repository registration + +**Plain data** (tokens, flags, login info, profile data) should be accessed through `Repository` +implementations, not stored directly in `Client`. Historical fields exist due to incremental +migration - they will be moved to repositories over time. + +### `Client` vs `InternalClient` + +- `Client` is a lightweight wrapper around `Arc` +- `Arc` enables cheap cloning for FFI bindings (owned copies point to same underlying instance) +- `InternalClient` originally hid internal APIs from Secrets Manager, but this separation becomes + less important as functionality moves to feature crates + +### Extension pattern + +Feature crates extend `Client` via extension traits. This allows the underlying implementation to be +internal to the crate with only the public API exposed through the `Client` struct. Below is an +example of a generator extension for the `Client` struct. + +> [!IMPORTANT] Do not add feature functionality to `Client` itself. + +```rust,ignore +use bitwarden_core::Client; +#[cfg(feature = "wasm")] +use wasm_bindgen::prelude::*; + +/// Generator extension client that wraps the base Client +#[cfg_attr(feature = "wasm", wasm_bindgen)] +pub struct GeneratorClient { + client: Client, +} + +#[cfg_attr(feature = "wasm", wasm_bindgen)] +impl GeneratorClient { + fn new(client: Client) -> Self { + Self { client } + } + + /// Example method that uses the underlying Client + pub fn password(&self, input: PasswordGeneratorRequest) -> Result { + // Implementation details... + password(input) + } +} + +/// Extension trait which exposes `generator()` method on the `Client` struct +pub trait GeneratorClientsExt { + fn generators(&self) -> GeneratorClient; +} + +impl GeneratorClientsExt for Client { + fn generators(&self) -> GeneratorClient { + GeneratorClient::new(self.clone()) + } +} + +// Usage: +// let password = client.generators().password(request)?; +``` + +## API requests + +One of the responsibilities of the `Client` is managing and exposing the `ApiClient` instances for +our API and Identity back-end services, which should be used to make HTTP requests. + +These `ApiClient`s should be accessed through the `ApiConfigurations` struct that is returned from +the `get_api_configurations()` function. `get_api_configurations()` also refreshes the +authentication token if required. + +```rust +# use bitwarden_core::Client; +# async fn example(client: &Client) -> Result<(), Box> { +// Example API call +let api_config = client.internal.get_api_configurations().await; +let response = api_config.api_client.ciphers_api().get_all().await?; +# Ok(()) +# } +``` + +### Server API bindings + +To make the requests, we use auto-generated bindings whenever possible. We use `openapi-generator` +to generate the Rust bindings from the server OpenAPI specifications. These bindings are regularly +updated to ensure they stay in sync with the server. + +The bindings are exposed as multiple crates, one for each backend service: + +- [`bitwarden-api-api`](../bitwarden-api-api/README.md): For the `Api` service that contains most of + the server side functionality. +- [`bitwarden-api-identity`](../bitwarden-api-identity/README.md): For the `Identity` service that + is used for authentication. + +When performing any API calls the goal is to use the generated bindings as much as possible. This +ensures any changes to the server are accurately reflected in the SDK. The generated bindings are +stateless, and always expects to be provided a Configuration instance. The SDK exposes these under +the get_api_configurations function on the Client struct. + +You should not expose the request and response models of the auto-generated bindings and should +instead define and use your own models. This ensures the server request / response models are +decoupled from the SDK models and allows for easier changes in the future without breaking backwards +compatibility. + +We recommend using either the `From` or `TryFrom` conversion traits depending on if the conversion +requires error handling or not. Below are two examples of how this can be done: + +```rust +# use bitwarden_crypto::EncString; +# use serde::{Serialize, Deserialize}; +# use serde_repr::{Serialize_repr, Deserialize_repr}; +# +# #[derive(Serialize, Deserialize, Debug, Clone)] +# struct LoginUri { +# pub uri: Option, +# pub r#match: Option, +# pub uri_checksum: Option, +# } +# +# #[derive(Clone, Copy, Serialize_repr, Deserialize_repr, Debug, PartialEq)] +# #[repr(u8)] +# pub enum UriMatchType { +# Domain = 0, +# Host = 1, +# StartsWith = 2, +# Exact = 3, +# RegularExpression = 4, +# Never = 5, +# } +# +# #[derive(Debug)] +# struct VaultParseError; +# +impl TryFrom for LoginUri { + type Error = VaultParseError; + + fn try_from(uri: bitwarden_api_api::models::CipherLoginUriModel) -> Result { + Ok(Self { + uri: EncString::try_from_optional(uri.uri) + .map_err(|_| VaultParseError)?, + r#match: uri.r#match.map(|m| m.into()), + uri_checksum: EncString::try_from_optional(uri.uri_checksum) + .map_err(|_| VaultParseError)?, + }) + } +} + +impl From for UriMatchType { + fn from(value: bitwarden_api_api::models::UriMatchType) -> Self { + match value { + bitwarden_api_api::models::UriMatchType::Domain => Self::Domain, + bitwarden_api_api::models::UriMatchType::Host => Self::Host, + bitwarden_api_api::models::UriMatchType::StartsWith => Self::StartsWith, + bitwarden_api_api::models::UriMatchType::Exact => Self::Exact, + bitwarden_api_api::models::UriMatchType::RegularExpression => Self::RegularExpression, + bitwarden_api_api::models::UriMatchType::Never => Self::Never, + } + } +} +``` + +### Updating bindings after a server API change + +When the API exposed by the server changes, new bindings will need to be generated to reflect this +change for consumption in the SDK. This includes adding new fields to server request / response +models, removing fields from models, or changing types of models. + +This can be done the following ways: + +1. Run the `Update API Bindings` workflow in the `sdk-internal` repo. +2. Wait for an automatic binding update to run, which is scheduled every 2 weeks. + +Both of these will generate a PR that will require approval from any teams whose owned code is +affected by the binding updates. + +> [!IMPORTANT] Bindings should **not** be updated manually as part of the changes to consume the new +> server API in the SDK. Doing so manually risks causing conflicts with the auto-generated bindings +> and causing more work in the future to address it.