|
| 1 | +--- |
| 2 | +sidebar_position: 1 |
| 3 | +--- |
| 4 | + |
| 5 | +# Adding new functionality |
| 6 | + |
| 7 | +This guide walks through the steps for adding new functionality to the SDK. For guidance on whether |
| 8 | +something belongs in the SDK at all, see |
| 9 | +[What belongs in the SDK](index.md#what-belongs-in-the-sdk). |
| 10 | + |
| 11 | +The examples below use `FoldersClient` from `bitwarden-vault` as a reference. For guidance on how to |
| 12 | +organize the files within a client, see [Client patterns](client-patterns.md). |
| 13 | + |
| 14 | +## 1. Readiness checklist |
| 15 | + |
| 16 | +Before creating a new crate or adding to an existing one, work through these questions to identify |
| 17 | +any blockers or prerequisites. |
| 18 | + |
| 19 | +- **Does the functionality depend on other code that is not yet in the SDK?** Moving it is not |
| 20 | + recommended until those dependencies are available. Consider asking the team that owns the |
| 21 | + upstream code to migrate it first. |
| 22 | + |
| 23 | +- **Does the functionality require authenticated API requests?** The SDK supports authenticated |
| 24 | + requests through autogenerated bindings. See [`bitwarden-vault`][vault-crate] as an example of a |
| 25 | + crate that makes authenticated calls. |
| 26 | + |
| 27 | +- **Does the functionality require persistent state?** Review the docs for |
| 28 | + [`bitwarden-state`][state-crate] and see [`bitwarden-vault`][vault-crate] for an example of how |
| 29 | + state is managed. |
| 30 | + |
| 31 | +- **Does the functionality need the SDK to produce an observable or reactive value?** Migrate the |
| 32 | + business logic to the SDK and build reactivity on top of it in TypeScript. |
| 33 | + |
| 34 | +## 2. Create the crate |
| 35 | + |
| 36 | +When the functionality warrants its own crate — typically when it represents a distinct domain — add |
| 37 | +a new crate under the `crates/` directory in the |
| 38 | +[SDK repository](https://github.com/bitwarden/sdk-internal). |
| 39 | + |
| 40 | +1. Create the crate with `cargo init` and add it to the workspace `Cargo.toml`. |
| 41 | +2. Add `bitwarden-core` as a dependency for the shared runtime. |
| 42 | +3. Configure `CODEOWNERS` to ensure the appropriate team is assigned to review changes to the crate. |
| 43 | + |
| 44 | +## 3. Define the client struct |
| 45 | + |
| 46 | +Each feature crate exposes one or more client structs that group related operations. Create a struct |
| 47 | +that holds the dependencies the client needs and use the `#[derive(FromClient)]` macro to |
| 48 | +automatically populate fields from the SDK `Client`. See |
| 49 | +[Client patterns — `FromClient` and dependency injection](client-patterns.md#fromclient-and-dependency-injection) |
| 50 | +for details on how this works and which dependency types are available. |
| 51 | + |
| 52 | +```rust |
| 53 | +#[derive(FromClient)] |
| 54 | +pub struct FoldersClient { |
| 55 | + pub(crate) key_store: KeyStore<KeyIds>, |
| 56 | + pub(crate) api_configurations: Arc<ApiConfigurations>, |
| 57 | + pub(crate) repository: Option<Arc<dyn Repository<Folder>>>, |
| 58 | +} |
| 59 | +``` |
| 60 | + |
| 61 | +If the client will be exposed over WASM, annotate it with |
| 62 | +`#[cfg_attr(feature = "wasm", wasm_bindgen)]`. |
| 63 | + |
| 64 | +## 4. Wire into the application interface |
| 65 | + |
| 66 | +Connect the feature client to the SDK `Client` by defining an |
| 67 | +[extension trait](client-patterns.md#extension-traits). This makes the feature accessible without |
| 68 | +modifying `Client` itself. |
| 69 | + |
| 70 | +```rust |
| 71 | +pub trait VaultClientExt { |
| 72 | + fn vault(&self) -> VaultClient; |
| 73 | +} |
| 74 | + |
| 75 | +impl VaultClientExt for Client { |
| 76 | + fn vault(&self) -> VaultClient { |
| 77 | + VaultClient::new(self.clone()) |
| 78 | + } |
| 79 | +} |
| 80 | +``` |
| 81 | + |
| 82 | +:::note |
| 83 | + |
| 84 | +While there is a team called `vault`, `VaultClient` refers to the vault-domain, not the team. You |
| 85 | +should not create team-clients in the SDK. Instead, organize clients by domain or feature area, and |
| 86 | +assign ownership to the team that maintains that domain or feature. |
| 87 | + |
| 88 | +::: |
| 89 | + |
| 90 | +For larger domains, the application interface client delegates to sub-clients rather than |
| 91 | +implementing every method itself. For example, `VaultClient` exposes `FoldersClient`, |
| 92 | +`CiphersClient`, and others through accessor methods: |
| 93 | + |
| 94 | +```rust |
| 95 | +#[cfg_attr(feature = "wasm", wasm_bindgen)] |
| 96 | +impl VaultClient { |
| 97 | + pub fn folders(&self) -> FoldersClient { |
| 98 | + FoldersClient::from_client(&self.client) |
| 99 | + } |
| 100 | +} |
| 101 | +``` |
| 102 | + |
| 103 | +Finally, expose the new client in the application interface entry point so consumers can reach it. |
| 104 | +For the Password Manager SDK, this means adding an accessor method to |
| 105 | +[`PasswordManagerClient`][pm-lib]: |
| 106 | + |
| 107 | +```rust |
| 108 | +impl PasswordManagerClient { |
| 109 | + pub fn vault(&self) -> bitwarden_vault::VaultClient { |
| 110 | + self.0.vault() |
| 111 | + } |
| 112 | +} |
| 113 | +``` |
| 114 | + |
| 115 | +:::tip |
| 116 | + |
| 117 | +Steps 2–4 (create crate, define client, wire into application interface) should be submitted as a |
| 118 | +single pull request. Keep it small and focused on scaffolding — the Platform team reviews additions |
| 119 | +to the application interface clients, so a narrow scope helps move that review along quickly. |
| 120 | + |
| 121 | +::: |
| 122 | + |
| 123 | +## 5. Implement methods |
| 124 | + |
| 125 | +With the crate scaffolding merged, add methods in subsequent pull requests. Each method should own |
| 126 | +its logic directly on the client struct — avoid thin passthroughs to free functions. For larger |
| 127 | +clients, split methods into separate files or subdirectories as described in |
| 128 | +[Client patterns](client-patterns.md#per-method-files-or-subdirectories). |
| 129 | + |
| 130 | +:::important |
| 131 | + |
| 132 | +Every public method on a client is a contract with consumers and must have test coverage. Treat |
| 133 | +client methods as a public API boundary — changes to their behavior can break downstream consumers |
| 134 | +across multiple platforms. See [Client patterns — Testing](client-patterns.md#testing) for how to |
| 135 | +set up test doubles. |
| 136 | + |
| 137 | +::: |
| 138 | + |
| 139 | +```rust |
| 140 | +#[cfg_attr(feature = "wasm", wasm_bindgen)] |
| 141 | +impl FoldersClient { |
| 142 | + pub async fn get(&self, folder_id: FolderId) -> Result<FolderView, GetFolderError> { |
| 143 | + let folder = self |
| 144 | + .repository |
| 145 | + .require()? |
| 146 | + .get(folder_id) |
| 147 | + .await? |
| 148 | + .ok_or(ItemNotFoundError)?; |
| 149 | + |
| 150 | + Ok(self.key_store.decrypt(&folder)?) |
| 151 | + } |
| 152 | +} |
| 153 | +``` |
| 154 | + |
| 155 | +Consumers access the feature through the application interface: |
| 156 | + |
| 157 | +```rust |
| 158 | +let folders = client.vault().folders().list().await?; |
| 159 | +``` |
| 160 | + |
| 161 | +## 6. Add mobile bindings |
| 162 | + |
| 163 | +If the new functionality needs to be available on mobile platforms (Android / iOS), add a |
| 164 | +[UniFFI](language-bindings.md#mobile-bindings) wrapper in the `bitwarden-uniffi` crate. |
| 165 | + |
| 166 | +### Expose the client |
| 167 | + |
| 168 | +Add an accessor method on the appropriate UniFFI client — typically in |
| 169 | +[`bitwarden-uniffi/src/lib.rs`][uniffi-lib] or a sub-client — that returns a new wrapper struct: |
| 170 | + |
| 171 | +```rust |
| 172 | +impl Client { |
| 173 | + pub fn vault(&self) -> Arc<VaultClient> { |
| 174 | + Arc::new(VaultClient(self.0.clone())) |
| 175 | + } |
| 176 | +} |
| 177 | +``` |
| 178 | + |
| 179 | +### Create the wrapper |
| 180 | + |
| 181 | +Create a wrapper struct that holds the SDK `Client` and delegates to the underlying Rust client. See |
| 182 | +[`bitwarden-uniffi/src/tool/sends.rs`][uniffi-sends] for a complete example. |
| 183 | + |
| 184 | +```rust |
| 185 | +use crate::Result; |
| 186 | + |
| 187 | +pub struct FoldersClient(pub(crate) SharedClient); |
| 188 | + |
| 189 | +#[uniffi::export] |
| 190 | +impl FoldersClient { |
| 191 | + pub async fn get(&self, folder_id: FolderId) -> Result<FolderView> { |
| 192 | + Ok(self.0.vault().folders().get(folder_id).await?) |
| 193 | + } |
| 194 | +} |
| 195 | +``` |
| 196 | + |
| 197 | +The wrapper should convert errors into `BitwardenError`. When introducing a new error type, add a |
| 198 | +variant for it in [`bitwarden-uniffi/src/error.rs`][uniffi-error] and implement the `From` |
| 199 | +conversion. |
| 200 | + |
| 201 | +## Ownership |
| 202 | + |
| 203 | +Feature and domain crates are usually owned and maintained by individual teams. When creating a new |
| 204 | +crate, coordinate with the Platform team to establish ownership and review expectations. |
| 205 | + |
| 206 | +[pm-lib]: https://github.com/bitwarden/sdk-internal/blob/main/crates/bitwarden-pm/src/lib.rs |
| 207 | +[state-crate]: https://github.com/bitwarden/sdk-internal/tree/main/crates/bitwarden-state |
| 208 | +[uniffi-error]: |
| 209 | + https://github.com/bitwarden/sdk-internal/blob/main/crates/bitwarden-uniffi/src/error.rs |
| 210 | +[uniffi-lib]: https://github.com/bitwarden/sdk-internal/blob/main/crates/bitwarden-uniffi/src/lib.rs |
| 211 | +[uniffi-sends]: |
| 212 | + https://github.com/bitwarden/sdk-internal/blob/main/crates/bitwarden-uniffi/src/tool/sends.rs |
| 213 | +[vault-crate]: https://github.com/bitwarden/sdk-internal/tree/main/crates/bitwarden-vault |
0 commit comments