diff --git a/custom-words.txt b/custom-words.txt index a67fa28fd..a6aee37d0 100644 --- a/custom-words.txt +++ b/custom-words.txt @@ -57,6 +57,7 @@ OTLP owasp passcode passwordless +passthroughs pinentry PNSs POSIX diff --git a/docs/architecture/sdk/adding-functionality.md b/docs/architecture/sdk/adding-functionality.md new file mode 100644 index 000000000..bbd837994 --- /dev/null +++ b/docs/architecture/sdk/adding-functionality.md @@ -0,0 +1,213 @@ +--- +sidebar_position: 1 +--- + +# Adding new functionality + +This guide walks through the steps for adding new functionality to the SDK. For guidance on whether +something belongs in the SDK at all, see +[What belongs in the SDK](index.md#what-belongs-in-the-sdk). + +The examples below use `FoldersClient` from `bitwarden-vault` as a reference. For guidance on how to +organize the files within a client, see [Client patterns](client-patterns.md). + +## 1. Readiness checklist + +Before creating a new crate or adding to an existing one, work through these questions to identify +any blockers or prerequisites. + +- **Does the functionality depend on other code that is not yet in the SDK?** Moving it is not + recommended until those dependencies are available. Consider asking the team that owns the + upstream code to migrate it first. + +- **Does the functionality require authenticated API requests?** The SDK supports authenticated + requests through autogenerated bindings. See [`bitwarden-vault`][vault-crate] as an example of a + crate that makes authenticated calls. + +- **Does the functionality require persistent state?** Review the docs for + [`bitwarden-state`][state-crate] and see [`bitwarden-vault`][vault-crate] for an example of how + state is managed. + +- **Does the functionality need the SDK to produce an observable or reactive value?** Migrate the + business logic to the SDK and build reactivity on top of it in TypeScript. + +## 2. Create the crate + +When the functionality warrants its own crate — typically when it represents a distinct domain — add +a new crate under the `crates/` directory in the +[SDK repository](https://github.com/bitwarden/sdk-internal). + +1. Create the crate with `cargo init` and add it to the workspace `Cargo.toml`. +2. Add `bitwarden-core` as a dependency for the shared runtime. +3. Configure `CODEOWNERS` to ensure the appropriate team is assigned to review changes to the crate. + +## 3. Define the client struct + +Each feature crate exposes one or more client structs that group related operations. Create a struct +that holds the dependencies the client needs and use the `#[derive(FromClient)]` macro to +automatically populate fields from the SDK `Client`. See +[Client patterns — `FromClient` and dependency injection](client-patterns.md#fromclient-and-dependency-injection) +for details on how this works and which dependency types are available. + +```rust +#[derive(FromClient)] +pub struct FoldersClient { + pub(crate) key_store: KeyStore, + pub(crate) api_configurations: Arc, + pub(crate) repository: Option>>, +} +``` + +If the client will be exposed over WASM, annotate it with +`#[cfg_attr(feature = "wasm", wasm_bindgen)]`. + +## 4. Wire into the application interface + +Connect the feature client to the SDK `Client` by defining an +[extension trait](client-patterns.md#extension-traits). This makes the feature accessible without +modifying `Client` itself. + +```rust +pub trait VaultClientExt { + fn vault(&self) -> VaultClient; +} + +impl VaultClientExt for Client { + fn vault(&self) -> VaultClient { + VaultClient::new(self.clone()) + } +} +``` + +:::note + +While there is a team called `vault`, `VaultClient` refers to the vault-domain, not the team. You +should not create team-clients in the SDK. Instead, organize clients by domain or feature area, and +assign ownership to the team that maintains that domain or feature. + +::: + +For larger domains, the application interface client delegates to sub-clients rather than +implementing every method itself. For example, `VaultClient` exposes `FoldersClient`, +`CiphersClient`, and others through accessor methods: + +```rust +#[cfg_attr(feature = "wasm", wasm_bindgen)] +impl VaultClient { + pub fn folders(&self) -> FoldersClient { + FoldersClient::from_client(&self.client) + } +} +``` + +Finally, expose the new client in the application interface entry point so consumers can reach it. +For the Password Manager SDK, this means adding an accessor method to +[`PasswordManagerClient`][pm-lib]: + +```rust +impl PasswordManagerClient { + pub fn vault(&self) -> bitwarden_vault::VaultClient { + self.0.vault() + } +} +``` + +:::tip + +Steps 2–4 (create crate, define client, wire into application interface) should be submitted as a +single pull request. Keep it small and focused on scaffolding — the Platform team reviews additions +to the application interface clients, so a narrow scope helps move that review along quickly. + +::: + +## 5. Implement methods + +With the crate scaffolding merged, add methods in subsequent pull requests. Each method should own +its logic directly on the client struct — avoid thin passthroughs to free functions. For larger +clients, split methods into separate files or subdirectories as described in +[Client patterns](client-patterns.md#per-method-files-or-subdirectories). + +:::important + +Every public method on a client is a contract with consumers and must have test coverage. Treat +client methods as a public API boundary — changes to their behavior can break downstream consumers +across multiple platforms. See [Client patterns — Testing](client-patterns.md#testing) for how to +set up test doubles. + +::: + +```rust +#[cfg_attr(feature = "wasm", wasm_bindgen)] +impl FoldersClient { + pub async fn get(&self, folder_id: FolderId) -> Result { + let folder = self + .repository + .require()? + .get(folder_id) + .await? + .ok_or(ItemNotFoundError)?; + + Ok(self.key_store.decrypt(&folder)?) + } +} +``` + +Consumers access the feature through the application interface: + +```rust +let folders = client.vault().folders().list().await?; +``` + +## 6. Add mobile bindings + +If the new functionality needs to be available on mobile platforms (Android / iOS), add a +[UniFFI](language-bindings.md#mobile-bindings) wrapper in the `bitwarden-uniffi` crate. + +### Expose the client + +Add an accessor method on the appropriate UniFFI client — typically in +[`bitwarden-uniffi/src/lib.rs`][uniffi-lib] or a sub-client — that returns a new wrapper struct: + +```rust +impl Client { + pub fn vault(&self) -> Arc { + Arc::new(VaultClient(self.0.clone())) + } +} +``` + +### Create the wrapper + +Create a wrapper struct that holds the SDK `Client` and delegates to the underlying Rust client. See +[`bitwarden-uniffi/src/tool/sends.rs`][uniffi-sends] for a complete example. + +```rust +use crate::Result; + +pub struct FoldersClient(pub(crate) SharedClient); + +#[uniffi::export] +impl FoldersClient { + pub async fn get(&self, folder_id: FolderId) -> Result { + Ok(self.0.vault().folders().get(folder_id).await?) + } +} +``` + +The wrapper should convert errors into `BitwardenError`. When introducing a new error type, add a +variant for it in [`bitwarden-uniffi/src/error.rs`][uniffi-error] and implement the `From` +conversion. + +## Ownership + +Feature and domain crates are usually owned and maintained by individual teams. When creating a new +crate, coordinate with the Platform team to establish ownership and review expectations. + +[pm-lib]: https://github.com/bitwarden/sdk-internal/blob/main/crates/bitwarden-pm/src/lib.rs +[state-crate]: https://github.com/bitwarden/sdk-internal/tree/main/crates/bitwarden-state +[uniffi-error]: + https://github.com/bitwarden/sdk-internal/blob/main/crates/bitwarden-uniffi/src/error.rs +[uniffi-lib]: https://github.com/bitwarden/sdk-internal/blob/main/crates/bitwarden-uniffi/src/lib.rs +[uniffi-sends]: + https://github.com/bitwarden/sdk-internal/blob/main/crates/bitwarden-uniffi/src/tool/sends.rs +[vault-crate]: https://github.com/bitwarden/sdk-internal/tree/main/crates/bitwarden-vault diff --git a/docs/architecture/sdk/client-patterns.md b/docs/architecture/sdk/client-patterns.md new file mode 100644 index 000000000..3645475a2 --- /dev/null +++ b/docs/architecture/sdk/client-patterns.md @@ -0,0 +1,225 @@ +--- +sidebar_position: 3 +--- + +# Client patterns + +Clients group the SDK API surface into domain-specific bundles. For a step-by-step walkthrough of +creating a client from scratch and wiring it into the application interface, see +[Adding new functionality](adding-functionality.md). + +## `FromClient` and dependency injection + +Every client struct declares its dependencies as fields and derives `FromClient` to have them +automatically populated from the SDK `Client`. The macro generates a `from_client` method that +extracts each field using the `FromClientPart` trait — client structs never call `Client` methods +directly to obtain their dependencies. + +```rust +#[derive(FromClient)] +pub struct FoldersClient { + pub(crate) key_store: KeyStore, + pub(crate) api_configurations: Arc, + pub(crate) repository: Option>>, +} +``` + +Some of the available dependency types that can be extracted are: + +- `KeyStore` — access to the cryptographic key store +- `Arc` — HTTP API client configuration +- `Option>>` — state repository for a given domain type + +This design also makes clients straightforward to test: because dependencies are plain struct +fields, tests can construct clients directly with test doubles instead of spinning up a full SDK +`Client`. See [Testing](#testing) below. + +### WASM support + +If the client will be exposed over WASM, annotate both the struct and its `impl` blocks with: + +```rust +#[cfg_attr(feature = "wasm", wasm_bindgen)] +``` + +### UniFFI wrappers + +Mobile clients access the SDK through thin wrapper structs in the `bitwarden-uniffi` crate. Each +wrapper holds a `SharedClient` (a type alias for `Arc`) and delegates to the underlying Rust +client: + +```rust +pub struct FoldersClient(pub(crate) SharedClient); + +#[uniffi::export] +impl FoldersClient { + pub async fn get(&self, folder_id: FolderId) -> Result { + Ok(self.0.vault().folders().get(folder_id).await?) + } +} +``` + +The wrapper mirrors the structure of the Rust client hierarchy — parent wrappers expose child +wrappers through accessor methods, just like the application interface clients do. For example, a +`VaultClient` wrapper returns `Arc`. + +#### Error conversion + +UniFFI wrappers use a crate-level `Result` type alias that maps errors to `BitwardenError`. This +ensures all errors crossing the FFI boundary are converted into a type that UniFFI can serialize for +Kotlin and Swift consumers. Use the `?` operator in wrapper methods to automatically convert +domain-specific errors through the `From for BitwardenError` implementations. + +When introducing a new error type, add a variant for it in +[`bitwarden-uniffi/src/error.rs`][uniffi-error] and implement the `From` conversion so it can be +propagated with `?`. + +## Extension traits + +Feature crates connect to the SDK `Client` through extension traits. This keeps feature code +decoupled from `Client` — the trait is defined in the feature crate, not in `bitwarden-core`. + +```rust +pub trait VaultClientExt { + fn vault(&self) -> VaultClient; +} + +impl VaultClientExt for Client { + fn vault(&self) -> VaultClient { + VaultClient::new(self.clone()) + } +} +``` + +The application interface (e.g. `PasswordManagerClient`) imports the extension trait and calls it to +expose the feature to consumers. + +## File organization + +Start with everything in a single file. Split when the file grows past ~500 lines (including tests). + +### Single file + +Define the client struct, its initialization, and all method `impl` blocks including tests in one +file. This minimizes indirection and keeps related code easy to discover. Prefer this for smaller +domains. + +``` +domain_client.rs +├── DomainClient struct definition +└── impl DomainClient { methods and tests } +``` + +### Per-method files or subdirectories + +When the single file becomes unwieldy, keep the client struct in its own file and give each method +its own file. Each file contains the `impl DomainClient` block for that method, its DTOs, error +types, and its tests. + +``` +domain/ +├── domain_client.rs # DomainClient struct definition and initialization +├── mod.rs +├── method_name.rs # impl DomainClient { fn method_name() } + tests +└── other_method.rs # impl DomainClient { fn other_method() } + tests +``` + +If a method needs many or large supporting types (request/response structs, error enums), promote it +to a subdirectory: + +``` +domain/ +├── domain_client.rs # DomainClient struct definition and initialization +├── mod.rs +└── method_name/ + ├── mod.rs # impl DomainClient { fn method_name() } + tests + └── request.rs # supporting types +``` + +:::warning Anti-pattern: thin passthroughs + +Do not delegate method bodies to free functions. This splits the implementation away from the API +surface, makes the client harder to navigate, and obscures what the method actually does in +generated documentation. + +```rust +impl LoginClient { + // Bad — the real logic lives somewhere else. + pub async fn login_with_password(&self, data: LoginData) -> Result<()> { + login_with_password(self.client, data).await + } +} +``` + +Instead, implement the logic directly in the method body. + +::: + +## Testing + +Because client structs declare their dependencies as fields, they can be constructed directly in +tests without spinning up a full SDK `Client`. Inject test doubles for each dependency to isolate +the code under test. + +```rust +fn create_test_client() -> FoldersClient { + let key_store = + create_test_crypto_with_user_key(SymmetricCryptoKey::make_aes256_cbc_hmac_key()); + let repository = Arc::new(MemoryRepository::::default()); + + FoldersClient { + key_store, + api_configurations: Arc::new(ApiConfigurations::from_api_client( + ApiClient::new_mocked(|_| {}), + )), + repository: Some(repository), + } +} +``` + +Key points: + +- **Key store** — use `create_test_crypto_with_user_key` to set up a key store with a test key. +- **API configurations** — use `ApiClient::new_mocked` to create a mock HTTP client. The closure + receives requests and can return custom responses. +- **Repositories** — use `MemoryRepository` as an in-memory test double for state repositories. + Populate it with test data before exercising the client method. + +### Writing test cases + +Each public client method should have tests that cover the expected behavior and important error +paths. Construct the client, set up any required state, call the method, and assert the result. + +```rust +#[tokio::test] +async fn test_get_folder() { + let client = create_test_client(); + let folder_id = FolderId::new(uuid!("25afb11c-9c95-4db5-8bac-c21cb204a3f1")); + + // Populate the repository with test data + let folder = client.key_store.encrypt(FolderView { + id: Some(folder_id), + name: "Test Folder".to_string(), + revision_date: "2025-01-01T00:00:00Z".parse().unwrap(), + }).unwrap(); + + client.repository.as_ref().unwrap() + .set(folder_id, folder).await.unwrap(); + + // Exercise the method and verify + let result = client.get(folder_id).await.unwrap(); + assert_eq!(result.name, "Test Folder"); +} + +#[tokio::test] +async fn test_get_folder_not_found() { + let client = create_test_client(); + let folder_id = FolderId::new(uuid!("25afb11c-9c95-4db5-8bac-c21cb204a3f1")); + + let result = client.get(folder_id).await; + assert!(matches!(result.unwrap_err(), GetFolderError::ItemNotFound(_))); +} +``` + +[uniffi-error]: + https://github.com/bitwarden/sdk-internal/blob/main/crates/bitwarden-uniffi/src/error.rs diff --git a/docs/architecture/sdk/crate-structure.md b/docs/architecture/sdk/crate-structure.md new file mode 100644 index 000000000..bce061765 --- /dev/null +++ b/docs/architecture/sdk/crate-structure.md @@ -0,0 +1,109 @@ +--- +sidebar_position: 2 +--- + +# Crate structure + +The internal SDK is organized as a mono-repository containing multiple Rust crates, each with a +focused responsibility. The crates form a layered architecture where higher-level crates depend on +lower-level ones, but never the reverse. This layering keeps the codebase modular and ensures that +each crate can be developed, tested, and compiled independently. + +The [SDK repository](https://github.com/bitwarden/sdk-internal) groups its crates into four +categories: + +- **Bindings** — compile the SDK for consumption by non-Rust platforms (WASM, iOS, Android) +- **Application Interfaces** — assemble features into product-specific clients +- **Features** — implement individual business-logic domains +- **Core and Utility** — provide the shared runtime, cryptographic primitives, and common types + +From top to bottom, the dependency flow looks like this: + +```kroki type=plantuml +@startuml +skinparam componentStyle rectangle + +component "Bindings (WASM & UniFFI)" as bindings #e1f5ff + +package "Application Interfaces" #fff3e0 { + component "Password Manager" as passwordMgr + component "Secrets Manager" as secretsMgr +} + +package "Features" #f3e5f5 { + component "Auth" as auth + component "Vault" as vault + component "Send" as send + component "Generators" as generators + component "Exporters" as export +} + +component "Core" as core #e8f5e9 + +bindings --> passwordMgr +bindings --> secretsMgr +bindings --> core + +passwordMgr --> auth +passwordMgr --> vault +passwordMgr --> send +passwordMgr --> generators +passwordMgr --> export +passwordMgr --> core + +secretsMgr --> core + +auth --> core +vault --> core +send --> core +generators --> core +export --> core + +@enduml +``` + +## Bindings + +Binding crates compile the SDK into platform-specific artifacts. They translate Rust types and +functions into interfaces that non-Rust consumers can call. The two binding strategies are: + +- **WASM** — targets web clients via `wasm-bindgen`, producing a WebAssembly module consumed as an + NPM package +- **UniFFI** — targets mobile clients, generating Kotlin and Swift bindings from Rust definitions + +Bindings depend on the application interface crates and selectively re-export their public API. See +[Language bindings](language-bindings.md) for details on each strategy. + +## Application interfaces + +An application interface crate collects the features relevant to a Bitwarden product and presents +them through a single, cohesive client. For example, the Password Manager client exposes auth, +vault, send, generators, and exporters, while the Secrets Manager client exposes a narrower surface +focused on secret storage and retrieval. + +These clients are the primary entry points for SDK consumers. The binding layer wraps them so that +callers on each platform interact with a consistent, product-scoped API rather than reaching into +individual feature crates directly. + +## Core and utility + +The `bitwarden-core` crate provides the shared runtime that every other crate builds on. This +includes client initialization, authentication state management, HTTP request handling, and common +error types. The companion `bitwarden-crypto` crate owns all cryptographic primitives — encryption, +decryption, key derivation, and key management. + +Feature crates depend on core for their runtime but should not embed functionality that could be +shared. See the +[crate documentation](https://github.com/bitwarden/sdk-internal/tree/main/crates/bitwarden-core) for +specifics. + +## Features and domains + +Feature crates contain the business logic for a specific domain such as vault management, +authentication, or password generation. Each crate extends the `Client` struct with domain-specific +methods, keeping feature code isolated from unrelated domains. + +Extracting features into their own crates keeps `bitwarden-core` lean and brings practical benefits: +faster incremental compile times, clearer code ownership, and the ability to evolve a domain without +touching shared infrastructure. These crates are usually owned and maintained by +individual teams. diff --git a/docs/architecture/sdk/data-models.md b/docs/architecture/sdk/data-models.md index d3661353c..3df46a018 100644 --- a/docs/architecture/sdk/data-models.md +++ b/docs/architecture/sdk/data-models.md @@ -1,13 +1,18 @@ +--- +sidebar_position: 4 +--- + # Data models The SDK is expected to be used in a variety of projects, and to that end it defines a stable public -interface. Care must be taken to avoid exposing the inner workings on the SDK including its data -model. +interface. Care must be taken to avoid exposing the inner workings of the SDK including its data +model. For general Rust coding conventions, see the +[Rust code style guide](/contributing/code-style/rust). ## Public interface The public interface of the SDK is anything that is exposed externally, be it function names, types, -or exposed models. The SDK generally exposes request and response models that contains the expected +or exposed models. The SDK generally exposes request and response models that contain the expected data for each API call. We can generally group the public models into the following categories: @@ -54,7 +59,7 @@ variants populated simultaneously — and forces every consumer to reason about should be impossible. The server wire format uses numeric discriminants and optional fields, but that transformation -belongs at the API→domain mapping boundary, not in the domain or view models. +belongs at the `API → domain` mapping boundary, not in the domain or view models. ## Internal models diff --git a/docs/architecture/sdk/dependencies.md b/docs/architecture/sdk/dependencies.md index d1993c28e..e8fe69792 100644 --- a/docs/architecture/sdk/dependencies.md +++ b/docs/architecture/sdk/dependencies.md @@ -1,3 +1,7 @@ +--- +sidebar_position: 6 +--- + # Dependencies The SDK is structured as several standalone crates. Each crate can have their own dependencies and @@ -14,7 +18,7 @@ keep up with security patches and new features. To that effort we **must** be conservative with adding dependencies. And we ask that you consider the following when evaluating a new dependency: -- Do we already have a dependency or transient dependency that does the same thing? If so we +- Do we already have a dependency or transitive dependency that does the same thing? If so we _should_ use that instead. - How complex is the desired functionality? - How much time would it take to implement the functionality yourself? @@ -34,14 +38,14 @@ ourselves. ## Dependency ranges -For any library we always use ranges for dependencies. This allows other crates to depend on the the -SDK without requiring the exact same versions. The Cargo lockfile ensures dependencies are -consistent between builds. +For any library we always use ranges for dependencies. This allows other crates to depend on the SDK +without requiring the exact same versions. The Cargo lockfile ensures dependencies are consistent +between builds. ### Explicit ranges We use explicit ranges for dependencies, i.e. we specify the minimum and maximum version of a -dependency. This helps avoiding confusion around how Cargo expands implicit versions. +dependency. This helps avoid confusion around how Cargo expands implicit versions. ```toml # Bad @@ -56,7 +60,7 @@ serde = ">1.0, <2.0" ## Workspace dependencies To provide a more developer-friendly experience for managing dependencies we define commonly used -dependencies in our workspace `Cargo.toml`. This allow us to update dependencies across the +dependencies in our workspace `Cargo.toml`. This allows us to update dependencies across the workspace by modifying a single file. Internal dependencies i.e. dependencies that are part of the same workspace **must** be defined diff --git a/docs/architecture/sdk/index.md b/docs/architecture/sdk/index.md index 4647c9bac..963959077 100644 --- a/docs/architecture/sdk/index.md +++ b/docs/architecture/sdk/index.md @@ -4,10 +4,12 @@ sidebar_position: 3 # SDK Architecture -Bitwarden provides a public Software Development Kit (SDK) for [Secrets Manager][sm] and an internal -SDK for the Bitwarden [Password Manager][pm]. The SDK is written in Rust and provides bindings for -multiple languages. The general end goal of the SDK is to own everything up to the presentational -layers. This includes but is not limited to: API calls, data models, encryption, and business logic. +The Bitwarden SDK is designed for internal use within Bitwarden and provides shared functionality +across all Bitwarden clients. It serves as the single source of truth for core business logic. +Written in Rust, the SDK is versatile and provides bindings for a variety of platforms, including +mobile clients (Kotlin and Swift) and web clients (JavaScript/TypeScript). The general aspiration is +to write as much code as possible once in the SDK and have it consumed by all clients, ensuring +feature parity and reducing duplication. We have compiled a list of resources for learning Rust in a [Confluence page](https://bitwarden.atlassian.net/wiki/spaces/DEV/pages/517898288/Rust+Learning+Resources). @@ -15,5 +17,127 @@ For API documentation view the latest [API documentation](https://sdk-api-docs.bitwarden.com/bitwarden/index.html) that also includes internal private items. -[sm]: https://bitwarden.com/products/secrets-manager/ -[pm]: https://bitwarden.com/ +## Architecture overview + +```kroki type=plantuml +@startuml +skinparam packageStyle rectangle +skinparam componentStyle rectangle +skinparam linetype ortho +skinparam shadowing false +skinparam defaultTextAlignment center + +skinparam package { + BackgroundColor<> #E3F2FD + BorderColor<> #1976D2 + BackgroundColor<> #FFF3E0 + BorderColor<> #F57C00 + BackgroundColor<> #E8F5E9 + BorderColor<> #388E3C +} + +skinparam component { + BackgroundColor<> #E8F5E9 + BorderColor<> #388E3C +} + +skinparam component { + BackgroundColor #FFFFFF + BorderColor #424242 + FontSize 12 +} + +package "Client Applications" <> { + package "TypeScript" { + [Web vault] as web + [Desktop] as desktop + [Browser extension] as browser + [CLI] as cli + } + package "Mobile" { + [iOS\n(Swift)] as ios + [Android\n(Kotlin)] as android + } + package "Rust" { + [Rust CLI] as rustcli + } +} + +package "Binding Layer" <> as bindings { + [WASM] as wasm + [UniFFI] as uniffi +} + +package "Rust SDK" <> as sdk { + package "Application Interfaces" { + [Password Manager] as pm + [Secrets Manager] as sm + } + folder "Features" { + [Vault] + [Generators] + } + folder "Utils" { + [Crypto] + [Logging] + } + [Core Runtime] as core +} + +TypeScript --> wasm +web -[hidden]> wasm +desktop -[hidden]> wasm +browser -[hidden]> wasm +cli -[hidden]> wasm +Mobile --> uniffi +ios -[hidden]> uniffi +android -[hidden]> uniffi + +bindings --> pm +bindings --> sm +uniffi --[hidden]-> pm +uniffi --[hidden]-> sm + +rustcli --> pm + +[Application Interfaces] -> [Features] +[Application Interfaces] ---> core +[Features] --> core +[Features] ---> Utils + +@enduml +``` + +## What belongs in the SDK + +**The guiding principle: everything except presentational logic belongs in the SDK.** + +The SDK should own all business logic that would otherwise be duplicated across clients. Client code +should be limited to UI rendering, platform-specific integrations, and calling SDK methods. + +### SDK responsibility + +| Layer | Owned By | Examples | +| ----------------- | -------- | -------------------------------------------- | +| Presentation | Client | UI components, navigation, platform gestures | +| Business Logic | **SDK** | Validation, transformations, calculations | +| State Management | **SDK** | User state, vault data, sync coordination | +| API Communication | **SDK** | Request/response handling, serialization | +| Cryptography | **SDK** | Encryption, decryption, key derivation | +| Data Models | **SDK** | Domain objects, view models | + +### Decision checklist + +When implementing a feature, ask: + +**Put it in the SDK if:** + +- The logic will be used across multiple clients (web, mobile, desktop) +- It involves cryptographic operations or sensitive data handling +- It's business logic that should behave identically everywhere +- It doesn't depend on platform-specific UI frameworks + +**Keep it in application code if:** + +- It's purely presentational (rendering, animations, gestures) +- It requires platform-specific APIs with no cross-platform abstraction diff --git a/docs/architecture/sdk/internal/index.md b/docs/architecture/sdk/internal/index.md deleted file mode 100644 index 170ed86ac..000000000 --- a/docs/architecture/sdk/internal/index.md +++ /dev/null @@ -1,280 +0,0 @@ -# Internal SDK - -The Internal SDK is designed for internal use within Bitwarden and supports key functionality for -managing encrypted data, vault access, and user authentication. Written in Rust, the SDK is -versatile and provides bindings for a variety of platforms, including mobile clients (Kotlin and -Swift) and web clients (JavaScript/TypeScript). - -This section will provide guidance on developing with the SDK in a way that ensures compatibility -across both mobile and web platforms. It will cover best practices for structuring code, addressing -platform-specific challenges, and ensuring that your implementation works seamlessly across -Bitwarden’s mobile and web applications. - -## Crate structure - -The internal SDK is structured as a single -[Git repository](https://github.com/bitwarden/sdk-internal) with multiple internal crates. This -document describes the general structure of the project. Please review the `README` in the -repository for information about the specific crates or implementation details. - -Crates in the project fall into one of these categories. - -- Bindings -- Application Interfaces -- Features -- Core and Utility - -We generally strive towards extracting features into separate crates to keep the `bitwarden-core` -crate as lean as possible. This has multiple benefits such as faster compile-time and clear -ownership of features. - -This hierarchy winds up producing a structure that looks like: - -```kroki type=plantuml -@startuml -skinparam componentStyle rectangle - -component "Bindings (WASM & UniFFI)" as bindings #e1f5ff - -package "Application Interfaces" #fff3e0 { - component "Password Manager" as passwordMgr - component "Secrets Manager" as secretsMgr -} - -package "Features" #f3e5f5 { - component "Auth" as auth - component "Vault" as vault - component "Send" as send - component "Generators" as generators - component "Exporters" as export -} - -component "Core" as core #e8f5e9 - -bindings --> passwordMgr -bindings --> secretsMgr -bindings --> core - -passwordMgr --> auth -passwordMgr --> vault -passwordMgr --> send -passwordMgr --> generators -passwordMgr --> export -passwordMgr --> core - -secretsMgr --> core - -auth --> core -vault --> core -send --> core -generators --> core -export --> core - -@enduml -``` - -
-Prior to [bitwarden/sdk-internal#468][sdk-internal-468], the application interfaces had not been explicitly created. - -```kroki type=plantuml -@startuml -skinparam componentStyle rectangle -skinparam defaultTextAlignment center - -component "Bindings (WASM & UniFFI)" as bindings #e1f5ff - -package "Features" #f3e5f5 { - component "Auth" as auth - component "Vault" as vault - component "Exporters" as export - component "Generators" as generators - component "Send" as send - component "Crypto" as crypto -} - -component "Core" as core #e8f5e9 - -bindings --> core -bindings --> auth -bindings --> vault -bindings --> export -bindings --> generators -bindings --> send - -auth --> core -vault --> core -export --> core -generators --> core -send --> core -crypto --> core - -@enduml -``` - -
- -### Bindings - -Bindings are those crates whose purpose is to provide bindings for other projects by targeting -`wasm`, iOS, and Android. The two mobile targets are built using UniFFI. See -[below](#language-bindings) for more information. - -### Application Interfaces - -An application interface collects the various features relevant for a given Bitwarden product, e.g. -Password Manager, or Secrets Manager, into a single easy-to-use client for that particular product. - -These clients, exposed through an external binding layer, are how consumers of the SDK will interact -with it. - -### Core and Utility - -The `bitwarden-core` crate contains the core runtime of the SDK. See the -[crate documentation](https://github.com/bitwarden/sdk-internal/tree/main/crates/bitwarden-core) for -more details. - -### Features and Domains - -Feature and domain crates constitute the application business logic. Feature crates depend on -`bitwarden-core` for their runtime and provide extensions to the `Client` struct to implement -specific domains. These crates are usually owned and maintained by individual -teams. - -The each feature or domain crate exposes its extended `Client` struct(s), which can be further -grouped into application interfaces for consumption. See the -[`VaultClient`](https://github.com/bitwarden/sdk-internal/blob/main/crates/bitwarden-vault/src/vault_client.rs) -as as example. - -## Client structure - -One of the core concepts of our SDK is the "client". The client groups the SDK API surface into -domain-specific bundles for easier instantiation and use by the consuming application. - -There are two recommended approaches for structuring a client, depending on the size of the domain. - -### Single file - -Define the client struct, its initialization, and all method `impl` blocks in one file. This -minimizes indirection and keeps related code easy to discover. Prefer this structure when the file -is manageable in size (~500 lines, including tests). - -``` -domain_client.rs -├── DomainClient struct definition and initialization -└── impl DomainClient with full method implementations and tests -``` - -### Per-method files or subdirectories - -When the single file would otherwise become unwieldy (~500 lines, including tests), the client -definition should be split from individual method implementations. - -Define the client struct in one file and each method in either its own file or its own subdirectory, -depending on the implementation complexity. - -When each method is self-contained and does not require supporting types alongside it, individual -methods can be split into separate files. - -``` -domain/ -├── domain_client.rs # DomainClient struct definition and initialization -├── mod.rs -├── method_name.rs # impl DomainClient { fn method_name() } and tests -└── other_method.rs # impl DomainClient { fn other_method() } and tests -``` - -For more complex clients, subdirectories can be used to contain the `impl DomainClient` block for -that method, its tests, and any supporting types. - -``` -domain/ -├── domain_client.rs # DomainClient struct definition and initialization -├── mod.rs -└── method_name/ - ├── mod.rs - ├── method_name.rs # impl DomainClient { fn method_name() } and tests - └── request.rs # supporting types (errors, etc.) -``` - -:::warning - -Avoid the thin passthrough pattern, where the client delegates to free functions defined elsewhere. -This creates unnecessary indirection and splits documentation away from the API surface. - -```rust - impl LoginClient { - // Avoid delegating the entire implementation to another function like this. - pub async fn login_with_password(&self, data: LoginData) -> Result<()> { - login_with_password(self.client, data).await - } - } -``` - -::: - -## Language bindings - -The internal SDK supports mobile and web platforms and uses UniFFI and `wasm-bindgen` to generate -bindings for those targets. - -### Mobile bindings - -We use [UniFFI](https://github.com/mozilla/uniffi-rs/) to generate bindings for the mobile -platforms, more specifically we publish Android and iOS libraries with Kotlin and Swift bindings, -respectively. While UniFFI supports additional languages they typically lag a few releases behind -the UniFFI core library. - -The Android bindings are currently published on -[GitHub Packages](https://github.com/bitwarden/sdk/packages/) in the `sdk_internal` repository. The -Swift package is published in the [`sdk-swift` repository](https://github.com/bitwarden/sdk-swift). - -### Web bindings - -For the web bindings we use [`wasm-bindgen`](https://github.com/rustwasm/wasm-bindgen) to generate a -WebAssembly module that can be used in JavaScript / TypeScript. To ensure compatibility with -browsers that do not support WebAssembly, we also generate a JavaScript module from the WebAssembly -that can be used as a fallback. - -The WebAssembly module is published on [npm](https://www.npmjs.com/package/@bitwarden/sdk-internal). - - -## Adding New Functionality - -### Adding new feature and domain crates - -Teams are encouraged to add their own feature or domain crates as they build additional -functionality in the SDK. - -Adding the crate and referencing it from the appropriate application interface clients should be -done as a separate pull request from adding any functionality in the crate itself. - -This will ensure that the scope of the initial setup pull request is small and focused, as our -Platform team will be responsible for reviewing additions to the application clients. - -### How do I decide what to move to the internal SDK? - -Considering adding to or moving code into the SDK? Review these questions to help come to a -decision. - -- Does the functionality or service depend on other functionality that is not currently part of the - SDK? - - Moving that functionality to the SDK is not recommended at this time, but asking the team - responsible for the other functionality to migrate that to the SDK _is_ recommended. -- Does the functionality require authenticated requests? - - The autogenerated bindings can help with that. See [`bitwarden-vault`][vault-crate] as an - example. -- Does this functionality require persistent state? - - Review the docs for [`bitwarden-state`][state-crate] and see [`bitwarden-vault`][vault-crate] as - an example. -- Is the functionality only relevant for a single client? - - There is likely not much chance of reusing that functionality, but it may still be added to the - SDK. -- Does the functionality need the SDK to produce an observable or reactive value? - - The SDK does not support reactivity at this time. However we still encourage migrating the - relevant business logic to the SDK and then building reactivity with that logic in TypeScript. - - - -[sdk-internal-468]: https://github.com/bitwarden/sdk-internal/pull/468 -[state-crate]: https://github.com/bitwarden/sdk-internal/tree/main/crates/bitwarden-state -[vault-crate]: https://github.com/bitwarden/sdk-internal/tree/main/crates/bitwarden-vault diff --git a/docs/architecture/sdk/language-bindings.md b/docs/architecture/sdk/language-bindings.md new file mode 100644 index 000000000..ac50c9d40 --- /dev/null +++ b/docs/architecture/sdk/language-bindings.md @@ -0,0 +1,31 @@ +--- +sidebar_position: 5 +--- + +# Language bindings + +The internal SDK supports mobile and web platforms and uses UniFFI and `wasm-bindgen` to generate +bindings for those targets. + +## Mobile bindings + +We use [UniFFI](https://github.com/mozilla/uniffi-rs/) to generate bindings for the mobile +platforms, more specifically we publish Android and iOS libraries with Kotlin and Swift bindings, +respectively. While UniFFI supports additional languages they typically lag a few releases behind +the UniFFI core library. + +The Android bindings are currently published on +[GitHub Packages](https://github.com/bitwarden/sdk/packages/) in the `sdk_internal` repository. The +Swift package is published in the [`sdk-swift` repository](https://github.com/bitwarden/sdk-swift). + +## Web bindings + +For the web bindings we use [`wasm-bindgen`](https://github.com/rustwasm/wasm-bindgen) to generate a +WebAssembly module that can be used in JavaScript / TypeScript. To ensure compatibility with +browsers that do not support WebAssembly, we also generate a JavaScript module from the WebAssembly +that can be used as a fallback. + +The WebAssembly module is published on [npm](https://www.npmjs.com/package/@bitwarden/sdk-internal). + +For detailed guidance on choosing between `wasm-bindgen` and `tsify` for web interoperability, see +[Web interoperability](web/interoperability.md). diff --git a/docs/architecture/sdk/secrets-manager.md b/docs/architecture/sdk/secrets-manager.md index d829749ac..185cb45ed 100644 --- a/docs/architecture/sdk/secrets-manager.md +++ b/docs/architecture/sdk/secrets-manager.md @@ -1,13 +1,88 @@ -# Secrets Manager +--- +sidebar_position: 8 +--- -The Secrets Manager SDK is designed for external use. The SDK is written in Rust and provides -bindings for multiple languages. +# Secrets Manager SDK + +The [Secrets Manager SDK](https://github.com/bitwarden/sdk-sm) encompasses a sub-set of the +Bitwarden SDK designed for external use. The SDK is written in Rust and provides bindings for +multiple languages. For general binding concepts, see [Language bindings](language-bindings.md). For +getting started with the Secrets Manager SDK, see the +[setup guide](/getting-started/sdk/secrets-manager/). + +The primary goal of the Secrets Manager SDK is to provide a **stable public API** that shields +external consumers from the churn of internal SDK APIs. While the internal SDK evolves rapidly to +support all Bitwarden clients, the SM SDK exposes a curated, backward-compatible surface. + +## Architecture overview + +```kroki type=plantuml +@startuml +skinparam packageStyle rectangle +skinparam componentStyle rectangle +skinparam shadowing false +skinparam defaultTextAlignment center + +skinparam package { + BackgroundColor<> #E3F2FD + BorderColor<> #1976D2 + BackgroundColor<> #FFF3E0 + BorderColor<> #F57C00 + BackgroundColor<> #E8F5E9 + BorderColor<> #388E3C +} + +skinparam component { + BackgroundColor<> #E8F5E9 + BorderColor<> #388E3C +} + +skinparam component { + BackgroundColor #FFFFFF + BorderColor #424242 + FontSize 12 +} + +package "Language Bindings" <> as bindings { + [Python] as py + [Golang] as go + [C#] as csharp + [JS] as wasm +} + +package "Secrets Manager SDK" <> as sm { + [bws] <> + [bitwarden] + [bitwarden-c] + [bitwarden-json] + [bitwarden-napi] + [bitwarden-py] +} + +["Rust SDK"] <> as sdk + +[bws] --> [bitwarden] +[bitwarden-json] --> [bitwarden] +[bitwarden-c] --> [bitwarden-json] +[bitwarden-napi] --> [bitwarden-json] +[bitwarden-py] --> [bitwarden-json] + +[bitwarden] --> sdk + +py --> [bitwarden-py] +go --> [bitwarden-c] +csharp --> [bitwarden-c] +wasm --> [bitwarden-napi] + +@enduml +``` ## `bitwarden` crate -The `bitwarden` crate represents the entry point for consumers of the SDK and is responsible for -providing a cohesive interface. The crate re-exports functionality of the internal crates and -contains very little logic itself. +The [`bitwarden`](https://github.com/bitwarden/sdk-sm/tree/main/crates/bitwarden) crate is the entry +point for consumers and acts as a thin wrapper around the internal crates. It re-exports a curated +subset of functionality while containing very little logic itself. This indirection allows internal +APIs to change without breaking external consumers. ## Bindings diff --git a/docs/architecture/sdk/versioning.md b/docs/architecture/sdk/versioning.md index 0c34b466c..e11100a9d 100644 --- a/docs/architecture/sdk/versioning.md +++ b/docs/architecture/sdk/versioning.md @@ -1,3 +1,7 @@ +--- +sidebar_position: 7 +--- + # Versioning and breaking changes The SDK strives towards maintaining backward compatibility for a reasonable time. This ensures that @@ -7,7 +11,7 @@ see the [SemVer Compatibility section](https://doc.rust-lang.org/cargo/reference [the Cargo book](https://doc.rust-lang.org/cargo/index.html). There may be certain functionality that is actively being developed and is not yet stable. In these -cases, they should be marked as such and gated behind a `unstable` feature flag. Consuming client +cases, they should be marked as such and gated behind an `unstable` feature flag. Consuming client applications should avoid merging changes that depend on these features into `main`. ## Public APIs diff --git a/docs/architecture/sdk/internal/web/index.md b/docs/architecture/sdk/web/index.md similarity index 93% rename from docs/architecture/sdk/internal/web/index.md rename to docs/architecture/sdk/web/index.md index 9331a31b5..dc996102e 100644 --- a/docs/architecture/sdk/internal/web/index.md +++ b/docs/architecture/sdk/web/index.md @@ -1,3 +1,7 @@ +--- +sidebar_position: 9 +--- + # Web The Password Manager SDK supports web-based clients, allowing developers to integrate core password diff --git a/docs/architecture/sdk/internal/web/interoperability.md b/docs/architecture/sdk/web/interoperability.md similarity index 97% rename from docs/architecture/sdk/internal/web/interoperability.md rename to docs/architecture/sdk/web/interoperability.md index 349f051a7..070d72513 100644 --- a/docs/architecture/sdk/internal/web/interoperability.md +++ b/docs/architecture/sdk/web/interoperability.md @@ -1,3 +1,7 @@ +--- +sidebar_position: 1 +--- + # Interoperability When working with the SDK, one of the key challenges is managing Rust/JavaScript bindings to enable @@ -28,7 +32,7 @@ need additional work to be represented correctly in JavaScript. On the other hand, `tsify` leverages Rust's `serde` ecosystem to expose Rust data models to JavaScript and automatically provide TypeScript bindings. It accomplishes this by serializing Rust -data structures into JavaScript objects, allowing them to be treated as native, typed TypesScript +data structures into JavaScript objects, allowing them to be treated as native, typed TypeScript objects. This differs from `wasm-bindgen`, which allows JavaScript to interact directly with Rust-owned structures in memory. `tsify` is generally better for handling complex data types, especially when working with Rust crates that already support `serde`. @@ -130,7 +134,7 @@ export class User { This ensures that values like `String` are properly cloned before being passed to JavaScript. -### Structs with Functions +### Structs with functions You can also return types with methods: @@ -160,7 +164,7 @@ export class User { } ``` -### About the `free` Method +### About the `free` method When working with `wasm-bindgen`, you may notice that all generated classes have a `free` method. This method is used to forcibly free the memory allocated for the Rust object when it is no longer @@ -170,7 +174,7 @@ up automatically. For more information see ## How to use `tsify` -### Basic Types +### Basic types To return a struct from Rust to JavaScript using `tsify`, derive `Serialize`, `Deserialize`, and `Tsify`: @@ -214,7 +218,7 @@ export interface Address { Note how `tsify` creates TypeScript interfaces, whereas `wasm-bindgen` creates classes that reference Rust objects stored in WebAssembly memory. -### External Types +### External types With `tsify`, fields can be any type that `serde` can serialize or deserialize, including external types from other crates. For example: @@ -267,4 +271,4 @@ export type Utc = unknown; ``` -This ensures the TypesScript definitions match the serialized Rust structure. +This ensures the TypeScript definitions match the serialized Rust structure.