From eb7dc301bfaddf28d26e5e3e6f09f8af9f4af384 Mon Sep 17 00:00:00 2001 From: Hinton Date: Mon, 30 Jun 2025 11:23:16 +0200 Subject: [PATCH 01/16] Add ClientManagedTokens trait --- crates/bitwarden-core/src/auth/renew.rs | 18 +++++--- crates/bitwarden-core/src/client/client.rs | 18 ++++++-- crates/bitwarden-core/src/client/internal.rs | 42 ++++++++----------- .../src/platform/get_user_api_key.rs | 12 ++---- 4 files changed, 49 insertions(+), 41 deletions(-) diff --git a/crates/bitwarden-core/src/auth/renew.rs b/crates/bitwarden-core/src/auth/renew.rs index 7b3e6f02c..a4fdb030f 100644 --- a/crates/bitwarden-core/src/auth/renew.rs +++ b/crates/bitwarden-core/src/auth/renew.rs @@ -10,24 +10,30 @@ use crate::{ }; use crate::{ auth::api::{request::ApiTokenRequest, response::IdentityTokenResponse}, - client::{internal::InternalClient, LoginMethod, UserLoginMethod}, + client::{ + internal::{InternalClient, Tokens}, + LoginMethod, UserLoginMethod, + }, NotAuthenticatedError, }; pub(crate) async fn renew_token(client: &InternalClient) -> Result<(), LoginError> { const TOKEN_RENEW_MARGIN_SECONDS: i64 = 5 * 60; - let tokens = client - .tokens - .read() - .expect("RwLock is not poisoned") - .clone(); let login_method = client .login_method .read() .expect("RwLock is not poisoned") .clone(); + let tokens = { + let tokens_guard = client.tokens.read().expect("RwLock is not poisoned"); + match &*tokens_guard { + Tokens::SdkManaged(tokens) => tokens.clone(), + _ => return Err(NotAuthenticatedError.into()), + } + }; + if let (Some(expires), Some(login_method)) = (tokens.expires_on, login_method) { if Utc::now().timestamp() < expires - TOKEN_RENEW_MARGIN_SECONDS { return Ok(()); diff --git a/crates/bitwarden-core/src/client/client.rs b/crates/bitwarden-core/src/client/client.rs index cf22ffef4..a9bb28bff 100644 --- a/crates/bitwarden-core/src/client/client.rs +++ b/crates/bitwarden-core/src/client/client.rs @@ -10,7 +10,7 @@ use super::internal::InternalClient; use crate::client::flags::Flags; use crate::client::{ client_settings::ClientSettings, - internal::{ApiConfigurations, Tokens}, + internal::{ApiConfigurations, ClientManagedTokens, SdkManagedTokens, Tokens}, }; /// The main struct to interact with the Bitwarden SDK. @@ -26,7 +26,19 @@ pub struct Client { impl Client { #[allow(missing_docs)] - pub fn new(settings_input: Option) -> Self { + pub fn new(settings: Option) -> Self { + Self::new_tokens(settings, Tokens::SdkManaged(SdkManagedTokens::default())) + } + + #[allow(missing_docs)] + pub fn new_with_client_tokens( + settings: Option, + tokens: Box, + ) -> Self { + Self::new_tokens(settings, Tokens::ClientManaged(tokens)) + } + + fn new_tokens(settings_input: Option, tokens: Tokens) -> Self { let settings = settings_input.unwrap_or_default(); fn new_client_builder() -> reqwest::ClientBuilder { @@ -81,7 +93,7 @@ impl Client { Self { internal: Arc::new(InternalClient { user_id: OnceLock::new(), - tokens: RwLock::new(Tokens::default()), + tokens: RwLock::new(tokens), login_method: RwLock::new(None), #[cfg(feature = "internal")] flags: RwLock::new(Flags::default()), diff --git a/crates/bitwarden-core/src/client/internal.rs b/crates/bitwarden-core/src/client/internal.rs index 9cc39bd08..12fcbae5e 100644 --- a/crates/bitwarden-core/src/client/internal.rs +++ b/crates/bitwarden-core/src/client/internal.rs @@ -33,8 +33,19 @@ pub struct ApiConfigurations { pub device_type: DeviceType, } +#[derive(Debug)] +pub(crate) enum Tokens { + SdkManaged(SdkManagedTokens), + ClientManaged(Box), +} + +pub trait ClientManagedTokens: std::fmt::Debug + Send + Sync { + /// Returns the access token, if available. + fn get_access_token(&self) -> Option; +} + #[derive(Debug, Default, Clone)] -pub(crate) struct Tokens { +pub(crate) struct SdkManagedTokens { // These two fields are always written to, but they are not read // from the secrets manager SDK. #[cfg_attr(not(feature = "internal"), allow(dead_code))] @@ -117,11 +128,12 @@ impl InternalClient { } pub(crate) fn set_tokens(&self, token: String, refresh_token: Option, expires_in: u64) { - *self.tokens.write().expect("RwLock is not poisoned") = Tokens { - access_token: Some(token.clone()), - expires_on: Some(Utc::now().timestamp() + expires_in as i64), - refresh_token, - }; + *self.tokens.write().expect("RwLock is not poisoned") = + Tokens::SdkManaged(SdkManagedTokens { + access_token: Some(token.clone()), + expires_on: Some(Utc::now().timestamp() + expires_in as i64), + refresh_token, + }); let mut guard = self .__api_configurations .write() @@ -132,24 +144,6 @@ impl InternalClient { inner.api.oauth_access_token = Some(token); } - #[allow(missing_docs)] - #[cfg(feature = "internal")] - pub fn is_authed(&self) -> bool { - let is_token_set = self - .tokens - .read() - .expect("RwLock is not poisoned") - .access_token - .is_some(); - let is_login_method_set = self - .login_method - .read() - .expect("RwLock is not poisoned") - .is_some(); - - is_token_set || is_login_method_set - } - #[allow(missing_docs)] #[cfg(feature = "internal")] pub fn get_kdf(&self) -> Result { diff --git a/crates/bitwarden-core/src/platform/get_user_api_key.rs b/crates/bitwarden-core/src/platform/get_user_api_key.rs index a127abab6..24c89c0d5 100644 --- a/crates/bitwarden-core/src/platform/get_user_api_key.rs +++ b/crates/bitwarden-core/src/platform/get_user_api_key.rs @@ -63,14 +63,10 @@ pub(crate) async fn get_user_api_key( } fn get_login_method(client: &Client) -> Result, NotAuthenticatedError> { - if client.internal.is_authed() { - client - .internal - .get_login_method() - .ok_or(NotAuthenticatedError) - } else { - Err(NotAuthenticatedError) - } + client + .internal + .get_login_method() + .ok_or(NotAuthenticatedError) } /// Build the secret verification request. From a58679cf1609ce9d38dac0bdc1be4799b14c9c6f Mon Sep 17 00:00:00 2001 From: Hinton Date: Mon, 30 Jun 2025 11:58:55 +0200 Subject: [PATCH 02/16] wip --- crates/bitwarden-core/Cargo.toml | 1 + crates/bitwarden-core/src/auth/renew.rs | 37 ++++-- crates/bitwarden-core/src/client/client.rs | 2 +- crates/bitwarden-core/src/client/internal.rs | 12 +- crates/bitwarden-wasm-internal/Cargo.toml | 4 + crates/bitwarden-wasm-internal/src/client.rs | 129 ++++++++++++++++++- 6 files changed, 169 insertions(+), 16 deletions(-) diff --git a/crates/bitwarden-core/Cargo.toml b/crates/bitwarden-core/Cargo.toml index c99b4918d..170fa1dd1 100644 --- a/crates/bitwarden-core/Cargo.toml +++ b/crates/bitwarden-core/Cargo.toml @@ -29,6 +29,7 @@ wasm = [ ] # WASM support [dependencies] +async-trait = { workspace = true } base64 = ">=0.22.1, <0.23" bitwarden-api-api = { workspace = true } bitwarden-api-identity = { workspace = true } diff --git a/crates/bitwarden-core/src/auth/renew.rs b/crates/bitwarden-core/src/auth/renew.rs index a4fdb030f..1143580c0 100644 --- a/crates/bitwarden-core/src/auth/renew.rs +++ b/crates/bitwarden-core/src/auth/renew.rs @@ -11,13 +11,40 @@ use crate::{ use crate::{ auth::api::{request::ApiTokenRequest, response::IdentityTokenResponse}, client::{ - internal::{InternalClient, Tokens}, + internal::{InternalClient, SdkManagedTokens, Tokens}, LoginMethod, UserLoginMethod, }, NotAuthenticatedError, }; +// TODO: Clean up, the match is ugly pub(crate) async fn renew_token(client: &InternalClient) -> Result<(), LoginError> { + let tokens = { + let tokens_guard = client.tokens.read().expect("RwLock is not poisoned"); + match &*tokens_guard { + Tokens::SdkManaged(tokens) => (Some(tokens.clone()), None), + Tokens::ClientManaged(tokens) => (None, Some(tokens.clone())), + } + }; + + match tokens { + (Some(tokens), None) => renew_token_sdk_managed(client, tokens).await, + (None, Some(tokens)) => { + let token = tokens + .get_access_token() + .await + .ok_or(NotAuthenticatedError)?; + client.set_tokens_internal(token); + Ok(()) + } + _ => Err(NotAuthenticatedError.into()), + } +} + +pub(crate) async fn renew_token_sdk_managed( + client: &InternalClient, + tokens: SdkManagedTokens, +) -> Result<(), LoginError> { const TOKEN_RENEW_MARGIN_SECONDS: i64 = 5 * 60; let login_method = client @@ -26,14 +53,6 @@ pub(crate) async fn renew_token(client: &InternalClient) -> Result<(), LoginErro .expect("RwLock is not poisoned") .clone(); - let tokens = { - let tokens_guard = client.tokens.read().expect("RwLock is not poisoned"); - match &*tokens_guard { - Tokens::SdkManaged(tokens) => tokens.clone(), - _ => return Err(NotAuthenticatedError.into()), - } - }; - if let (Some(expires), Some(login_method)) = (tokens.expires_on, login_method) { if Utc::now().timestamp() < expires - TOKEN_RENEW_MARGIN_SECONDS { return Ok(()); diff --git a/crates/bitwarden-core/src/client/client.rs b/crates/bitwarden-core/src/client/client.rs index a9bb28bff..d1f5ccf1a 100644 --- a/crates/bitwarden-core/src/client/client.rs +++ b/crates/bitwarden-core/src/client/client.rs @@ -33,7 +33,7 @@ impl Client { #[allow(missing_docs)] pub fn new_with_client_tokens( settings: Option, - tokens: Box, + tokens: Arc, ) -> Self { Self::new_tokens(settings, Tokens::ClientManaged(tokens)) } diff --git a/crates/bitwarden-core/src/client/internal.rs b/crates/bitwarden-core/src/client/internal.rs index 12fcbae5e..a12eecd85 100644 --- a/crates/bitwarden-core/src/client/internal.rs +++ b/crates/bitwarden-core/src/client/internal.rs @@ -36,19 +36,20 @@ pub struct ApiConfigurations { #[derive(Debug)] pub(crate) enum Tokens { SdkManaged(SdkManagedTokens), - ClientManaged(Box), + ClientManaged(Arc), } +#[async_trait::async_trait] pub trait ClientManagedTokens: std::fmt::Debug + Send + Sync { /// Returns the access token, if available. - fn get_access_token(&self) -> Option; + async fn get_access_token(&self) -> Option; } #[derive(Debug, Default, Clone)] pub(crate) struct SdkManagedTokens { // These two fields are always written to, but they are not read // from the secrets manager SDK. - #[cfg_attr(not(feature = "internal"), allow(dead_code))] + #[allow(dead_code)] access_token: Option, pub(crate) expires_on: Option, @@ -134,6 +135,11 @@ impl InternalClient { expires_on: Some(Utc::now().timestamp() + expires_in as i64), refresh_token, }); + self.set_tokens_internal(token); + } + + /// Used to set tokens for internal API clients, use [set_tokens] for SdkManagedTokens. + pub(crate) fn set_tokens_internal(&self, token: String) { let mut guard = self .__api_configurations .write() diff --git a/crates/bitwarden-wasm-internal/Cargo.toml b/crates/bitwarden-wasm-internal/Cargo.toml index db4d673ea..32c11787b 100644 --- a/crates/bitwarden-wasm-internal/Cargo.toml +++ b/crates/bitwarden-wasm-internal/Cargo.toml @@ -30,6 +30,7 @@ bitwarden-threading = { workspace = true } bitwarden-vault = { workspace = true, features = ["wasm"] } console_error_panic_hook = "0.1.7" console_log = { version = "1.0.0", features = ["color"] } +js-sys = "0.3" log = "0.4.20" serde = { workspace = true } tsify-next = { workspace = true } @@ -37,5 +38,8 @@ tsify-next = { workspace = true } wasm-bindgen = { version = "=0.2.100", features = ["serde-serialize"] } wasm-bindgen-futures = "0.4.41" +[dev-dependencies] +wasm-bindgen-test = "0.3" + [lints] workspace = true diff --git a/crates/bitwarden-wasm-internal/src/client.rs b/crates/bitwarden-wasm-internal/src/client.rs index 200ff69ff..2ded5bbac 100644 --- a/crates/bitwarden-wasm-internal/src/client.rs +++ b/crates/bitwarden-wasm-internal/src/client.rs @@ -1,5 +1,5 @@ extern crate console_error_panic_hook; -use std::fmt::Display; +use std::{fmt::Display, sync::Arc}; use bitwarden_core::{key_management::CryptoClient, Client, ClientSettings}; use bitwarden_error::bitwarden_error; @@ -18,8 +18,9 @@ pub struct BitwardenClient(pub(crate) Client); impl BitwardenClient { #[allow(missing_docs)] #[wasm_bindgen(constructor)] - pub fn new(settings: Option) -> Self { - Self(Client::new(settings)) + pub fn new(settings: Option, token_provider: JsTokenProvider) -> Self { + let tokens = Arc::new(WasmClientManagedTokens::new(token_provider)); + Self(Client::new_with_client_tokens(settings, tokens)) } /// Test method, echoes back the input @@ -79,3 +80,125 @@ impl Display for TestError { write!(f, "{}", self.0) } } + +/// JavaScript-compatible token provider using function closure +#[wasm_bindgen] +pub struct JsTokenProvider { + get_access_token_fn: js_sys::Function, +} + +impl std::fmt::Debug for JsTokenProvider { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("JsTokenProvider") + .field("get_access_token_fn", &"") + .finish() + } +} + +#[wasm_bindgen] +impl JsTokenProvider { + #[wasm_bindgen(constructor)] + pub fn new(get_access_token_fn: js_sys::Function) -> Self { + Self { + get_access_token_fn, + } + } +} + +/// Wrapper to make JsTokenProvider compatible with ClientManagedTokens +#[derive(Debug)] +struct WasmClientManagedTokens { + js_provider: JsTokenProvider, +} + +impl WasmClientManagedTokens { + fn new(js_provider: JsTokenProvider) -> Self { + Self { js_provider } + } +} + +impl bitwarden_core::client::internal::ClientManagedTokens for WasmClientManagedTokens { + fn get_access_token(&self) -> Option { + match self + .js_provider + .get_access_token_fn + .call0(&wasm_bindgen::JsValue::UNDEFINED) + { + Ok(result) => { + if result.is_null() || result.is_undefined() { + None + } else { + result.as_string() + } + } + Err(_) => None, + } + } +} + +// SAFETY: JsTokenProvider is only used in WASM context where there's no real threading +unsafe impl Send for WasmClientManagedTokens {} +unsafe impl Sync for WasmClientManagedTokens {} + +#[cfg(test)] +#[allow(dead_code)] // Not actually dead, but rust-analyzer doesn't understand `wasm_bindgen_test` +mod tests { + use super::*; + use bitwarden_core::client::internal::ClientManagedTokens; + use wasm_bindgen_test::*; + + // Note: These tests are designed to run in a WASM environment + // Run with: wasm-pack test --node + + #[wasm_bindgen_test] + fn test_js_token_provider_creation() { + // Create a simple function that returns a test token + let js_fn = js_sys::Function::new_no_args("return 'test-token-123';"); + let provider = JsTokenProvider::new(js_fn); + + // Verify the provider was created successfully + // This mainly tests the constructor works without panicking + assert!(format!("{:?}", provider).contains("JsTokenProvider")); + } + + #[wasm_bindgen_test] + fn test_wasm_client_managed_tokens_with_valid_token() { + let js_fn = js_sys::Function::new_no_args("return 'valid-access-token';"); + let provider = JsTokenProvider::new(js_fn); + let tokens = WasmClientManagedTokens::new(provider); + + let result = tokens.get_access_token(); + assert_eq!(result, Some("valid-access-token".to_string())); + } + + #[wasm_bindgen_test] + fn test_wasm_client_managed_tokens_with_null_token() { + let js_fn = js_sys::Function::new_no_args("return null;"); + let provider = JsTokenProvider::new(js_fn); + let tokens = WasmClientManagedTokens::new(provider); + + let result = tokens.get_access_token(); + assert_eq!(result, None); + } + + #[wasm_bindgen_test] + fn test_wasm_client_managed_tokens_with_undefined_token() { + let js_fn = js_sys::Function::new_no_args("return undefined;"); + let provider = JsTokenProvider::new(js_fn); + let tokens = WasmClientManagedTokens::new(provider); + + let result = tokens.get_access_token(); + assert_eq!(result, None); + } + + #[wasm_bindgen_test] + fn test_wasm_client_managed_tokens_with_error() { + let js_fn = js_sys::Function::new_no_args("throw new Error('Token error');"); + let provider = JsTokenProvider::new(js_fn); + let tokens = WasmClientManagedTokens::new(provider); + + let result = tokens.get_access_token(); + // Should return None when the JS function throws an error + assert_eq!(result, None); + } +} From 425aaa07fec38425a3ba72407913e50ad993f769 Mon Sep 17 00:00:00 2001 From: Hinton Date: Mon, 30 Jun 2025 13:11:36 +0200 Subject: [PATCH 03/16] Fix --- Cargo.lock | 3 + crates/bitwarden-wasm-internal/src/client.rs | 81 +++++++++----------- 2 files changed, 38 insertions(+), 46 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index d42405358..c74a57a51 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -357,6 +357,7 @@ dependencies = [ name = "bitwarden-core" version = "1.0.0" dependencies = [ + "async-trait", "base64", "bitwarden-api-api", "bitwarden-api-identity", @@ -726,11 +727,13 @@ dependencies = [ "bitwarden-vault", "console_error_panic_hook", "console_log", + "js-sys", "log", "serde", "tsify-next", "wasm-bindgen", "wasm-bindgen-futures", + "wasm-bindgen-test", ] [[package]] diff --git a/crates/bitwarden-wasm-internal/src/client.rs b/crates/bitwarden-wasm-internal/src/client.rs index 2ded5bbac..d60a4720b 100644 --- a/crates/bitwarden-wasm-internal/src/client.rs +++ b/crates/bitwarden-wasm-internal/src/client.rs @@ -1,10 +1,13 @@ extern crate console_error_panic_hook; use std::{fmt::Display, sync::Arc}; -use bitwarden_core::{key_management::CryptoClient, Client, ClientSettings}; +use bitwarden_core::{ + client::internal::ClientManagedTokens, key_management::CryptoClient, Client, ClientSettings, +}; use bitwarden_error::bitwarden_error; use bitwarden_exporters::ExporterClientExt; use bitwarden_generators::GeneratorClientsExt; +use bitwarden_threading::ThreadBoundRunner; use bitwarden_vault::{VaultClient, VaultClientExt}; use wasm_bindgen::prelude::*; @@ -81,65 +84,50 @@ impl Display for TestError { } } -/// JavaScript-compatible token provider using function closure -#[wasm_bindgen] -pub struct JsTokenProvider { - get_access_token_fn: js_sys::Function, -} - -impl std::fmt::Debug for JsTokenProvider { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.debug_struct("JsTokenProvider") - .field("get_access_token_fn", &"") - .finish() - } +#[wasm_bindgen(typescript_custom_section)] +const TOKEN_CUSTOM_TS_TYPE: &'static str = r#" +export interface TokenProvider { + get_access_token(): Promise; } +"#; #[wasm_bindgen] -impl JsTokenProvider { - #[wasm_bindgen(constructor)] - pub fn new(get_access_token_fn: js_sys::Function) -> Self { - Self { - get_access_token_fn, - } - } -} +extern "C" { + #[wasm_bindgen(js_name = TokenProvider)] + pub type JsTokenProvider; -/// Wrapper to make JsTokenProvider compatible with ClientManagedTokens -#[derive(Debug)] -struct WasmClientManagedTokens { - js_provider: JsTokenProvider, + #[wasm_bindgen(method)] + pub async fn get_access_token(this: &JsTokenProvider) -> JsValue; } +struct WasmClientManagedTokens(ThreadBoundRunner); + impl WasmClientManagedTokens { - fn new(js_provider: JsTokenProvider) -> Self { - Self { js_provider } + pub fn new(js_provider: JsTokenProvider) -> Self { + Self(ThreadBoundRunner::new(js_provider)) } } -impl bitwarden_core::client::internal::ClientManagedTokens for WasmClientManagedTokens { - fn get_access_token(&self) -> Option { - match self - .js_provider - .get_access_token_fn - .call0(&wasm_bindgen::JsValue::UNDEFINED) - { - Ok(result) => { - if result.is_null() || result.is_undefined() { - None - } else { - result.as_string() - } - } - Err(_) => None, - } +impl std::fmt::Debug for WasmClientManagedTokens { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("WasmClientManagedTokens").finish() } } -// SAFETY: JsTokenProvider is only used in WASM context where there's no real threading -unsafe impl Send for WasmClientManagedTokens {} -unsafe impl Sync for WasmClientManagedTokens {} +#[async_trait::async_trait] +impl ClientManagedTokens for WasmClientManagedTokens { + async fn get_access_token(&self) -> Option { + let t = self + .0 + .run_in_thread(async move |c| c.get_access_token().await.as_string()) + .await + .unwrap(); + + t + } +} +/* #[cfg(test)] #[allow(dead_code)] // Not actually dead, but rust-analyzer doesn't understand `wasm_bindgen_test` mod tests { @@ -202,3 +190,4 @@ mod tests { assert_eq!(result, None); } } +*/ From 890450d08547c5368d524c1b1d2dd856f9cb3a1b Mon Sep 17 00:00:00 2001 From: Hinton Date: Mon, 30 Jun 2025 13:12:13 +0200 Subject: [PATCH 04/16] Migrate folder --- Cargo.lock | 1 + crates/bitwarden-vault/Cargo.toml | 4 +- crates/bitwarden-vault/src/folder.rs | 8 ++- crates/bitwarden-vault/src/folder_client.rs | 70 ++++++++++++++++++++- 4 files changed, 77 insertions(+), 6 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index c74a57a51..a296e11e6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -707,6 +707,7 @@ dependencies = [ "uniffi", "uuid", "wasm-bindgen", + "wasm-bindgen-futures", ] [[package]] diff --git a/crates/bitwarden-vault/Cargo.toml b/crates/bitwarden-vault/Cargo.toml index e06ce1460..d49b95274 100644 --- a/crates/bitwarden-vault/Cargo.toml +++ b/crates/bitwarden-vault/Cargo.toml @@ -23,7 +23,8 @@ uniffi = [ wasm = [ "bitwarden-core/wasm", "dep:tsify-next", - "dep:wasm-bindgen" + "dep:wasm-bindgen", + "dep:wasm-bindgen-futures" ] # WASM support [dependencies] @@ -48,6 +49,7 @@ tsify-next = { workspace = true, optional = true } uniffi = { workspace = true, optional = true } uuid = { workspace = true } wasm-bindgen = { workspace = true, optional = true } +wasm-bindgen-futures = { workspace = true, optional = true } [dev-dependencies] tokio = { workspace = true, features = ["rt"] } diff --git a/crates/bitwarden-vault/src/folder.rs b/crates/bitwarden-vault/src/folder.rs index 18083a5f5..bc3e5ca0b 100644 --- a/crates/bitwarden-vault/src/folder.rs +++ b/crates/bitwarden-vault/src/folder.rs @@ -20,11 +20,13 @@ use crate::VaultParseError; #[cfg_attr(feature = "uniffi", derive(uniffi::Record))] #[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi, from_wasm_abi))] pub struct Folder { - id: Option, - name: EncString, - revision_date: DateTime, + pub id: Option, + pub name: EncString, + pub revision_date: DateTime, } +bitwarden_state::register_repository_item!(Folder, "Folder"); + #[allow(missing_docs)] #[derive(Serialize, Deserialize, Debug)] #[serde(rename_all = "camelCase")] diff --git a/crates/bitwarden-vault/src/folder_client.rs b/crates/bitwarden-vault/src/folder_client.rs index 90f62bd0b..259ce025e 100644 --- a/crates/bitwarden-vault/src/folder_client.rs +++ b/crates/bitwarden-vault/src/folder_client.rs @@ -1,18 +1,46 @@ -use bitwarden_core::Client; +use bitwarden_api_api::{apis::folders_api, models::FolderRequestModel}; +use bitwarden_core::{ApiError, Client}; +use bitwarden_error::bitwarden_error; +use chrono::Utc; +use serde::{Deserialize, Serialize}; +use thiserror::Error; +#[cfg(feature = "wasm")] +use tsify_next::Tsify; #[cfg(feature = "wasm")] use wasm_bindgen::prelude::*; use crate::{ error::{DecryptError, EncryptError}, - Folder, FolderView, + Folder, FolderView, VaultParseError, }; +/// Request to add or edit a folder. +#[derive(Serialize, Deserialize, Debug)] +#[serde(rename_all = "camelCase")] +#[cfg_attr(feature = "uniffi", derive(uniffi::Record))] +#[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi, from_wasm_abi))] +pub struct FolderAddEditRequest { + pub name: String, +} + #[allow(missing_docs)] #[cfg_attr(feature = "wasm", wasm_bindgen)] pub struct FoldersClient { pub(crate) client: Client, } +#[allow(missing_docs)] +#[bitwarden_error(flat)] +#[derive(Debug, Error)] +pub enum CreateFolderError { + #[error(transparent)] + Encrypt(#[from] EncryptError), + #[error(transparent)] + Api(#[from] ApiError), + #[error(transparent)] + VaultParse(#[from] VaultParseError), +} + #[cfg_attr(feature = "wasm", wasm_bindgen)] impl FoldersClient { #[allow(missing_docs)] @@ -35,4 +63,42 @@ impl FoldersClient { let views = key_store.decrypt_list(&folders)?; Ok(views) } + + /// Create a new folder and save it to the server. + pub async fn create(&self, request: FolderAddEditRequest) -> Result { + // TODO: We should probably not use a Folder model here, but rather create FolderRequestModel directly? + let folder = self.encrypt(FolderView { + id: None, + name: request.name, + revision_date: Utc::now(), + })?; + + let config = self.client.internal.get_api_configurations().await; + let req = folders_api::folders_post( + &config.api, + Some(FolderRequestModel { + name: folder.name.to_string(), + }), + ) + .await + .map_err(ApiError::from)?; + + Ok(req.try_into()?) + } + + /// Edit the folder. + /// + /// TODO: Replace `old_folder` with `FolderId` and load the old folder from state. + /// TODO: Save the folder to the server and state. + pub fn edit_without_state( + &self, + old_folder: Folder, + folder: FolderAddEditRequest, + ) -> Result { + self.encrypt(FolderView { + id: old_folder.id, + name: folder.name, + revision_date: old_folder.revision_date, + }) + } } From 3813a28c261c7454a13707a8d21e8f4f1b717b68 Mon Sep 17 00:00:00 2001 From: Hinton Date: Mon, 30 Jun 2025 13:24:41 +0200 Subject: [PATCH 05/16] Add repository --- crates/bitwarden-vault/src/folder.rs | 2 +- crates/bitwarden-vault/src/folder_client.rs | 21 ++++++++++++++++++--- 2 files changed, 19 insertions(+), 4 deletions(-) diff --git a/crates/bitwarden-vault/src/folder.rs b/crates/bitwarden-vault/src/folder.rs index bc3e5ca0b..eb342cfa3 100644 --- a/crates/bitwarden-vault/src/folder.rs +++ b/crates/bitwarden-vault/src/folder.rs @@ -15,7 +15,7 @@ use {tsify_next::Tsify, wasm_bindgen::prelude::*}; use crate::VaultParseError; #[allow(missing_docs)] -#[derive(Serialize, Deserialize, Debug)] +#[derive(Serialize, Deserialize, Debug, Clone)] #[serde(rename_all = "camelCase")] #[cfg_attr(feature = "uniffi", derive(uniffi::Record))] #[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi, from_wasm_abi))] diff --git a/crates/bitwarden-vault/src/folder_client.rs b/crates/bitwarden-vault/src/folder_client.rs index 259ce025e..7e54589e8 100644 --- a/crates/bitwarden-vault/src/folder_client.rs +++ b/crates/bitwarden-vault/src/folder_client.rs @@ -1,6 +1,7 @@ use bitwarden_api_api::{apis::folders_api, models::FolderRequestModel}; -use bitwarden_core::{ApiError, Client}; +use bitwarden_core::{require, ApiError, Client, MissingFieldError}; use bitwarden_error::bitwarden_error; +use bitwarden_state::repository::RepositoryError; use chrono::Utc; use serde::{Deserialize, Serialize}; use thiserror::Error; @@ -39,6 +40,10 @@ pub enum CreateFolderError { Api(#[from] ApiError), #[error(transparent)] VaultParse(#[from] VaultParseError), + #[error(transparent)] + MissingField(#[from] MissingFieldError), + #[error(transparent)] + RepositoryError(#[from] RepositoryError), } #[cfg_attr(feature = "wasm", wasm_bindgen)] @@ -74,7 +79,7 @@ impl FoldersClient { })?; let config = self.client.internal.get_api_configurations().await; - let req = folders_api::folders_post( + let resp = folders_api::folders_post( &config.api, Some(FolderRequestModel { name: folder.name.to_string(), @@ -83,7 +88,17 @@ impl FoldersClient { .await .map_err(ApiError::from)?; - Ok(req.try_into()?) + let folder: Folder = resp.try_into()?; + + self.client + .platform() + .state() + .get_client_managed::() + .ok_or(MissingFieldError("Folder not found in state"))? + .set(require!(folder.id).to_string(), folder.clone()) + .await?; + + Ok(folder) } /// Edit the folder. From 665a5a2267410ff8f6414f9da04b16b407730ea0 Mon Sep 17 00:00:00 2001 From: Hinton Date: Mon, 30 Jun 2025 13:59:08 +0200 Subject: [PATCH 06/16] Cleanup --- Cargo.lock | 2 - crates/bitwarden-vault/src/folder_client.rs | 3 +- crates/bitwarden-wasm-internal/Cargo.toml | 4 - crates/bitwarden-wasm-internal/src/client.rs | 118 +----------------- .../src/platform/mod.rs | 1 + .../src/platform/token_provider.rs | 43 +++++++ 6 files changed, 51 insertions(+), 120 deletions(-) create mode 100644 crates/bitwarden-wasm-internal/src/platform/token_provider.rs diff --git a/Cargo.lock b/Cargo.lock index a296e11e6..d09378f89 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -728,13 +728,11 @@ dependencies = [ "bitwarden-vault", "console_error_panic_hook", "console_log", - "js-sys", "log", "serde", "tsify-next", "wasm-bindgen", "wasm-bindgen-futures", - "wasm-bindgen-test", ] [[package]] diff --git a/crates/bitwarden-vault/src/folder_client.rs b/crates/bitwarden-vault/src/folder_client.rs index 7e54589e8..da7c5fd05 100644 --- a/crates/bitwarden-vault/src/folder_client.rs +++ b/crates/bitwarden-vault/src/folder_client.rs @@ -71,7 +71,8 @@ impl FoldersClient { /// Create a new folder and save it to the server. pub async fn create(&self, request: FolderAddEditRequest) -> Result { - // TODO: We should probably not use a Folder model here, but rather create FolderRequestModel directly? + // TODO: We should probably not use a Folder model here, but rather create + // FolderRequestModel directly? let folder = self.encrypt(FolderView { id: None, name: request.name, diff --git a/crates/bitwarden-wasm-internal/Cargo.toml b/crates/bitwarden-wasm-internal/Cargo.toml index 32c11787b..db4d673ea 100644 --- a/crates/bitwarden-wasm-internal/Cargo.toml +++ b/crates/bitwarden-wasm-internal/Cargo.toml @@ -30,7 +30,6 @@ bitwarden-threading = { workspace = true } bitwarden-vault = { workspace = true, features = ["wasm"] } console_error_panic_hook = "0.1.7" console_log = { version = "1.0.0", features = ["color"] } -js-sys = "0.3" log = "0.4.20" serde = { workspace = true } tsify-next = { workspace = true } @@ -38,8 +37,5 @@ tsify-next = { workspace = true } wasm-bindgen = { version = "=0.2.100", features = ["serde-serialize"] } wasm-bindgen-futures = "0.4.41" -[dev-dependencies] -wasm-bindgen-test = "0.3" - [lints] workspace = true diff --git a/crates/bitwarden-wasm-internal/src/client.rs b/crates/bitwarden-wasm-internal/src/client.rs index d60a4720b..bdd601bcc 100644 --- a/crates/bitwarden-wasm-internal/src/client.rs +++ b/crates/bitwarden-wasm-internal/src/client.rs @@ -1,17 +1,17 @@ extern crate console_error_panic_hook; use std::{fmt::Display, sync::Arc}; -use bitwarden_core::{ - client::internal::ClientManagedTokens, key_management::CryptoClient, Client, ClientSettings, -}; +use bitwarden_core::{key_management::CryptoClient, Client, ClientSettings}; use bitwarden_error::bitwarden_error; use bitwarden_exporters::ExporterClientExt; use bitwarden_generators::GeneratorClientsExt; -use bitwarden_threading::ThreadBoundRunner; use bitwarden_vault::{VaultClient, VaultClientExt}; use wasm_bindgen::prelude::*; -use crate::platform::PlatformClient; +use crate::platform::{ + token_provider::{JsTokenProvider, WasmClientManagedTokens}, + PlatformClient, +}; #[allow(missing_docs)] #[wasm_bindgen] @@ -83,111 +83,3 @@ impl Display for TestError { write!(f, "{}", self.0) } } - -#[wasm_bindgen(typescript_custom_section)] -const TOKEN_CUSTOM_TS_TYPE: &'static str = r#" -export interface TokenProvider { - get_access_token(): Promise; -} -"#; - -#[wasm_bindgen] -extern "C" { - #[wasm_bindgen(js_name = TokenProvider)] - pub type JsTokenProvider; - - #[wasm_bindgen(method)] - pub async fn get_access_token(this: &JsTokenProvider) -> JsValue; -} - -struct WasmClientManagedTokens(ThreadBoundRunner); - -impl WasmClientManagedTokens { - pub fn new(js_provider: JsTokenProvider) -> Self { - Self(ThreadBoundRunner::new(js_provider)) - } -} - -impl std::fmt::Debug for WasmClientManagedTokens { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.debug_struct("WasmClientManagedTokens").finish() - } -} - -#[async_trait::async_trait] -impl ClientManagedTokens for WasmClientManagedTokens { - async fn get_access_token(&self) -> Option { - let t = self - .0 - .run_in_thread(async move |c| c.get_access_token().await.as_string()) - .await - .unwrap(); - - t - } -} - -/* -#[cfg(test)] -#[allow(dead_code)] // Not actually dead, but rust-analyzer doesn't understand `wasm_bindgen_test` -mod tests { - use super::*; - use bitwarden_core::client::internal::ClientManagedTokens; - use wasm_bindgen_test::*; - - // Note: These tests are designed to run in a WASM environment - // Run with: wasm-pack test --node - - #[wasm_bindgen_test] - fn test_js_token_provider_creation() { - // Create a simple function that returns a test token - let js_fn = js_sys::Function::new_no_args("return 'test-token-123';"); - let provider = JsTokenProvider::new(js_fn); - - // Verify the provider was created successfully - // This mainly tests the constructor works without panicking - assert!(format!("{:?}", provider).contains("JsTokenProvider")); - } - - #[wasm_bindgen_test] - fn test_wasm_client_managed_tokens_with_valid_token() { - let js_fn = js_sys::Function::new_no_args("return 'valid-access-token';"); - let provider = JsTokenProvider::new(js_fn); - let tokens = WasmClientManagedTokens::new(provider); - - let result = tokens.get_access_token(); - assert_eq!(result, Some("valid-access-token".to_string())); - } - - #[wasm_bindgen_test] - fn test_wasm_client_managed_tokens_with_null_token() { - let js_fn = js_sys::Function::new_no_args("return null;"); - let provider = JsTokenProvider::new(js_fn); - let tokens = WasmClientManagedTokens::new(provider); - - let result = tokens.get_access_token(); - assert_eq!(result, None); - } - - #[wasm_bindgen_test] - fn test_wasm_client_managed_tokens_with_undefined_token() { - let js_fn = js_sys::Function::new_no_args("return undefined;"); - let provider = JsTokenProvider::new(js_fn); - let tokens = WasmClientManagedTokens::new(provider); - - let result = tokens.get_access_token(); - assert_eq!(result, None); - } - - #[wasm_bindgen_test] - fn test_wasm_client_managed_tokens_with_error() { - let js_fn = js_sys::Function::new_no_args("throw new Error('Token error');"); - let provider = JsTokenProvider::new(js_fn); - let tokens = WasmClientManagedTokens::new(provider); - - let result = tokens.get_access_token(); - // Should return None when the JS function throws an error - assert_eq!(result, None); - } -} -*/ diff --git a/crates/bitwarden-wasm-internal/src/platform/mod.rs b/crates/bitwarden-wasm-internal/src/platform/mod.rs index b2b977659..1bb5b551b 100644 --- a/crates/bitwarden-wasm-internal/src/platform/mod.rs +++ b/crates/bitwarden-wasm-internal/src/platform/mod.rs @@ -3,6 +3,7 @@ use bitwarden_vault::Cipher; use wasm_bindgen::prelude::wasm_bindgen; mod repository; +pub mod token_provider; #[wasm_bindgen] pub struct PlatformClient(Client); diff --git a/crates/bitwarden-wasm-internal/src/platform/token_provider.rs b/crates/bitwarden-wasm-internal/src/platform/token_provider.rs new file mode 100644 index 000000000..8693512f5 --- /dev/null +++ b/crates/bitwarden-wasm-internal/src/platform/token_provider.rs @@ -0,0 +1,43 @@ +use bitwarden_core::client::internal::ClientManagedTokens; +use bitwarden_threading::ThreadBoundRunner; +use wasm_bindgen::{prelude::wasm_bindgen, JsValue}; + +#[wasm_bindgen(typescript_custom_section)] +const TOKEN_CUSTOM_TS_TYPE: &'static str = r#" +export interface TokenProvider { + get_access_token(): Promise; +} +"#; + +#[wasm_bindgen] +extern "C" { + #[wasm_bindgen(js_name = TokenProvider)] + pub type JsTokenProvider; + + #[wasm_bindgen(method)] + pub async fn get_access_token(this: &JsTokenProvider) -> JsValue; +} + +pub(crate) struct WasmClientManagedTokens(ThreadBoundRunner); + +impl WasmClientManagedTokens { + pub fn new(js_provider: JsTokenProvider) -> Self { + Self(ThreadBoundRunner::new(js_provider)) + } +} + +impl std::fmt::Debug for WasmClientManagedTokens { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("WasmClientManagedTokens").finish() + } +} + +#[async_trait::async_trait] +impl ClientManagedTokens for WasmClientManagedTokens { + async fn get_access_token(&self) -> Option { + self.0 + .run_in_thread(async move |c| c.get_access_token().await.as_string()) + .await + .unwrap_or_default() + } +} From 4bcf32add352f36c1fd4e2b7a2645cd1f3e3b064 Mon Sep 17 00:00:00 2001 From: Hinton Date: Mon, 30 Jun 2025 14:07:19 +0200 Subject: [PATCH 07/16] Cleanup --- crates/bitwarden-core/src/auth/renew.rs | 45 +++++++++++--------- crates/bitwarden-core/src/client/internal.rs | 5 ++- 2 files changed, 28 insertions(+), 22 deletions(-) diff --git a/crates/bitwarden-core/src/auth/renew.rs b/crates/bitwarden-core/src/auth/renew.rs index 1143580c0..c5b5093af 100644 --- a/crates/bitwarden-core/src/auth/renew.rs +++ b/crates/bitwarden-core/src/auth/renew.rs @@ -1,3 +1,5 @@ +use std::sync::Arc; + use chrono::Utc; use super::login::LoginError; @@ -11,37 +13,38 @@ use crate::{ use crate::{ auth::api::{request::ApiTokenRequest, response::IdentityTokenResponse}, client::{ - internal::{InternalClient, SdkManagedTokens, Tokens}, + internal::{ClientManagedTokens, InternalClient, SdkManagedTokens, Tokens}, LoginMethod, UserLoginMethod, }, NotAuthenticatedError, }; -// TODO: Clean up, the match is ugly pub(crate) async fn renew_token(client: &InternalClient) -> Result<(), LoginError> { - let tokens = { - let tokens_guard = client.tokens.read().expect("RwLock is not poisoned"); - match &*tokens_guard { - Tokens::SdkManaged(tokens) => (Some(tokens.clone()), None), - Tokens::ClientManaged(tokens) => (None, Some(tokens.clone())), - } - }; + let tokens_guard = client + .tokens + .read() + .expect("RwLock is not poisoned") + .clone(); - match tokens { - (Some(tokens), None) => renew_token_sdk_managed(client, tokens).await, - (None, Some(tokens)) => { - let token = tokens - .get_access_token() - .await - .ok_or(NotAuthenticatedError)?; - client.set_tokens_internal(token); - Ok(()) - } - _ => Err(NotAuthenticatedError.into()), + match tokens_guard { + Tokens::SdkManaged(tokens) => renew_token_sdk_managed(client, tokens).await, + Tokens::ClientManaged(tokens) => renew_token_client_managed(client, tokens).await, } } -pub(crate) async fn renew_token_sdk_managed( +async fn renew_token_client_managed( + client: &InternalClient, + tokens: Arc, +) -> Result<(), LoginError> { + let token = tokens + .get_access_token() + .await + .ok_or(NotAuthenticatedError)?; + client.set_tokens_internal(token); + Ok(()) +} + +async fn renew_token_sdk_managed( client: &InternalClient, tokens: SdkManagedTokens, ) -> Result<(), LoginError> { diff --git a/crates/bitwarden-core/src/client/internal.rs b/crates/bitwarden-core/src/client/internal.rs index a12eecd85..e253cb488 100644 --- a/crates/bitwarden-core/src/client/internal.rs +++ b/crates/bitwarden-core/src/client/internal.rs @@ -33,18 +33,21 @@ pub struct ApiConfigurations { pub device_type: DeviceType, } -#[derive(Debug)] +/// Access and refresh tokens used for authentication and authorization. +#[derive(Debug, Clone)] pub(crate) enum Tokens { SdkManaged(SdkManagedTokens), ClientManaged(Arc), } +/// Access tokens managed by client applications, such as the web or mobile apps. #[async_trait::async_trait] pub trait ClientManagedTokens: std::fmt::Debug + Send + Sync { /// Returns the access token, if available. async fn get_access_token(&self) -> Option; } +/// Tokens managed by the SDK, the SDK will automatically handle token renewal. #[derive(Debug, Default, Clone)] pub(crate) struct SdkManagedTokens { // These two fields are always written to, but they are not read From f05db2caef6166341fd82b4f4cb0461a2eecd259 Mon Sep 17 00:00:00 2001 From: Hinton Date: Mon, 30 Jun 2025 14:25:25 +0200 Subject: [PATCH 08/16] Wire up repository and return folder view --- crates/bitwarden-vault/src/folder_client.rs | 10 ++++++++-- crates/bitwarden-wasm-internal/src/platform/mod.rs | 8 +++++++- 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/crates/bitwarden-vault/src/folder_client.rs b/crates/bitwarden-vault/src/folder_client.rs index da7c5fd05..650316175 100644 --- a/crates/bitwarden-vault/src/folder_client.rs +++ b/crates/bitwarden-vault/src/folder_client.rs @@ -1,5 +1,6 @@ use bitwarden_api_api::{apis::folders_api, models::FolderRequestModel}; use bitwarden_core::{require, ApiError, Client, MissingFieldError}; +use bitwarden_crypto::Decryptable; use bitwarden_error::bitwarden_error; use bitwarden_state::repository::RepositoryError; use chrono::Utc; @@ -37,6 +38,8 @@ pub enum CreateFolderError { #[error(transparent)] Encrypt(#[from] EncryptError), #[error(transparent)] + Decrypt(#[from] DecryptError), + #[error(transparent)] Api(#[from] ApiError), #[error(transparent)] VaultParse(#[from] VaultParseError), @@ -70,7 +73,10 @@ impl FoldersClient { } /// Create a new folder and save it to the server. - pub async fn create(&self, request: FolderAddEditRequest) -> Result { + pub async fn create( + &self, + request: FolderAddEditRequest, + ) -> Result { // TODO: We should probably not use a Folder model here, but rather create // FolderRequestModel directly? let folder = self.encrypt(FolderView { @@ -99,7 +105,7 @@ impl FoldersClient { .set(require!(folder.id).to_string(), folder.clone()) .await?; - Ok(folder) + Ok(self.decrypt(folder)?) } /// Edit the folder. diff --git a/crates/bitwarden-wasm-internal/src/platform/mod.rs b/crates/bitwarden-wasm-internal/src/platform/mod.rs index 1bb5b551b..9d6c4274c 100644 --- a/crates/bitwarden-wasm-internal/src/platform/mod.rs +++ b/crates/bitwarden-wasm-internal/src/platform/mod.rs @@ -1,5 +1,5 @@ use bitwarden_core::Client; -use bitwarden_vault::Cipher; +use bitwarden_vault::{Cipher, Folder}; use wasm_bindgen::prelude::wasm_bindgen; mod repository; @@ -31,6 +31,7 @@ impl StateClient { } repository::create_wasm_repository!(CipherRepository, Cipher, "Repository"); +repository::create_wasm_repository!(FolderRepository, Folder, "Repository"); #[wasm_bindgen] impl StateClient { @@ -38,4 +39,9 @@ impl StateClient { let store = store.into_channel_impl(); self.0.platform().state().register_client_managed(store) } + + pub fn register_folder_repository(&self, store: FolderRepository) { + let store = store.into_channel_impl(); + self.0.platform().state().register_client_managed(store) + } } From 5261641befacd657f44b9f2889fff33270e51d61 Mon Sep 17 00:00:00 2001 From: Hinton Date: Mon, 30 Jun 2025 14:42:12 +0200 Subject: [PATCH 09/16] clippt --- crates/bitwarden-vault/src/folder_client.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/crates/bitwarden-vault/src/folder_client.rs b/crates/bitwarden-vault/src/folder_client.rs index 650316175..55a3cc1dc 100644 --- a/crates/bitwarden-vault/src/folder_client.rs +++ b/crates/bitwarden-vault/src/folder_client.rs @@ -1,6 +1,5 @@ use bitwarden_api_api::{apis::folders_api, models::FolderRequestModel}; use bitwarden_core::{require, ApiError, Client, MissingFieldError}; -use bitwarden_crypto::Decryptable; use bitwarden_error::bitwarden_error; use bitwarden_state::repository::RepositoryError; use chrono::Utc; From 44cec30a8eb8b13a92c8d12b7efbed0c6fb2cc7f Mon Sep 17 00:00:00 2001 From: Hinton Date: Mon, 30 Jun 2025 14:55:30 +0200 Subject: [PATCH 10/16] Fix linting --- crates/bitwarden-core/src/client/internal.rs | 2 +- crates/bitwarden-wasm-internal/src/platform/token_provider.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/bitwarden-core/src/client/internal.rs b/crates/bitwarden-core/src/client/internal.rs index e253cb488..b69aaf96a 100644 --- a/crates/bitwarden-core/src/client/internal.rs +++ b/crates/bitwarden-core/src/client/internal.rs @@ -141,7 +141,7 @@ impl InternalClient { self.set_tokens_internal(token); } - /// Used to set tokens for internal API clients, use [set_tokens] for SdkManagedTokens. + /// Used to set tokens for internal API clients, use `set_tokens` for SdkManagedTokens. pub(crate) fn set_tokens_internal(&self, token: String) { let mut guard = self .__api_configurations diff --git a/crates/bitwarden-wasm-internal/src/platform/token_provider.rs b/crates/bitwarden-wasm-internal/src/platform/token_provider.rs index 8693512f5..2cbc67d87 100644 --- a/crates/bitwarden-wasm-internal/src/platform/token_provider.rs +++ b/crates/bitwarden-wasm-internal/src/platform/token_provider.rs @@ -36,7 +36,7 @@ impl std::fmt::Debug for WasmClientManagedTokens { impl ClientManagedTokens for WasmClientManagedTokens { async fn get_access_token(&self) -> Option { self.0 - .run_in_thread(async move |c| c.get_access_token().await.as_string()) + .run_in_thread(|c| async move { c.get_access_token().await.as_string() }) .await .unwrap_or_default() } From 2c1bdf0e294a215de845bbd4dee09be3865ed715 Mon Sep 17 00:00:00 2001 From: Hinton Date: Tue, 1 Jul 2025 10:18:29 +0200 Subject: [PATCH 11/16] Remove folder client changes --- crates/bitwarden-vault/src/folder_client.rs | 91 +-------------------- 1 file changed, 2 insertions(+), 89 deletions(-) diff --git a/crates/bitwarden-vault/src/folder_client.rs b/crates/bitwarden-vault/src/folder_client.rs index 55a3cc1dc..90f62bd0b 100644 --- a/crates/bitwarden-vault/src/folder_client.rs +++ b/crates/bitwarden-vault/src/folder_client.rs @@ -1,53 +1,18 @@ -use bitwarden_api_api::{apis::folders_api, models::FolderRequestModel}; -use bitwarden_core::{require, ApiError, Client, MissingFieldError}; -use bitwarden_error::bitwarden_error; -use bitwarden_state::repository::RepositoryError; -use chrono::Utc; -use serde::{Deserialize, Serialize}; -use thiserror::Error; -#[cfg(feature = "wasm")] -use tsify_next::Tsify; +use bitwarden_core::Client; #[cfg(feature = "wasm")] use wasm_bindgen::prelude::*; use crate::{ error::{DecryptError, EncryptError}, - Folder, FolderView, VaultParseError, + Folder, FolderView, }; -/// Request to add or edit a folder. -#[derive(Serialize, Deserialize, Debug)] -#[serde(rename_all = "camelCase")] -#[cfg_attr(feature = "uniffi", derive(uniffi::Record))] -#[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi, from_wasm_abi))] -pub struct FolderAddEditRequest { - pub name: String, -} - #[allow(missing_docs)] #[cfg_attr(feature = "wasm", wasm_bindgen)] pub struct FoldersClient { pub(crate) client: Client, } -#[allow(missing_docs)] -#[bitwarden_error(flat)] -#[derive(Debug, Error)] -pub enum CreateFolderError { - #[error(transparent)] - Encrypt(#[from] EncryptError), - #[error(transparent)] - Decrypt(#[from] DecryptError), - #[error(transparent)] - Api(#[from] ApiError), - #[error(transparent)] - VaultParse(#[from] VaultParseError), - #[error(transparent)] - MissingField(#[from] MissingFieldError), - #[error(transparent)] - RepositoryError(#[from] RepositoryError), -} - #[cfg_attr(feature = "wasm", wasm_bindgen)] impl FoldersClient { #[allow(missing_docs)] @@ -70,56 +35,4 @@ impl FoldersClient { let views = key_store.decrypt_list(&folders)?; Ok(views) } - - /// Create a new folder and save it to the server. - pub async fn create( - &self, - request: FolderAddEditRequest, - ) -> Result { - // TODO: We should probably not use a Folder model here, but rather create - // FolderRequestModel directly? - let folder = self.encrypt(FolderView { - id: None, - name: request.name, - revision_date: Utc::now(), - })?; - - let config = self.client.internal.get_api_configurations().await; - let resp = folders_api::folders_post( - &config.api, - Some(FolderRequestModel { - name: folder.name.to_string(), - }), - ) - .await - .map_err(ApiError::from)?; - - let folder: Folder = resp.try_into()?; - - self.client - .platform() - .state() - .get_client_managed::() - .ok_or(MissingFieldError("Folder not found in state"))? - .set(require!(folder.id).to_string(), folder.clone()) - .await?; - - Ok(self.decrypt(folder)?) - } - - /// Edit the folder. - /// - /// TODO: Replace `old_folder` with `FolderId` and load the old folder from state. - /// TODO: Save the folder to the server and state. - pub fn edit_without_state( - &self, - old_folder: Folder, - folder: FolderAddEditRequest, - ) -> Result { - self.encrypt(FolderView { - id: old_folder.id, - name: folder.name, - revision_date: old_folder.revision_date, - }) - } } From 40335fa460766e476c67e726df71f3cd7573733c Mon Sep 17 00:00:00 2001 From: Hinton Date: Tue, 1 Jul 2025 10:19:23 +0200 Subject: [PATCH 12/16] Remove more folder changes --- crates/bitwarden-vault/src/folder.rs | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/crates/bitwarden-vault/src/folder.rs b/crates/bitwarden-vault/src/folder.rs index 016737132..8168b97aa 100644 --- a/crates/bitwarden-vault/src/folder.rs +++ b/crates/bitwarden-vault/src/folder.rs @@ -16,18 +16,16 @@ use {tsify_next::Tsify, wasm_bindgen::prelude::*}; use crate::VaultParseError; #[allow(missing_docs)] -#[derive(Serialize, Deserialize, Debug, Clone)] +#[derive(Serialize, Deserialize, Debug)] #[serde(rename_all = "camelCase")] #[cfg_attr(feature = "uniffi", derive(uniffi::Record))] #[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi, from_wasm_abi))] pub struct Folder { - pub id: Option, - pub name: EncString, - pub revision_date: DateTime, + id: Option, + name: EncString, + revision_date: DateTime, } -bitwarden_state::register_repository_item!(Folder, "Folder"); - #[allow(missing_docs)] #[derive(Serialize, Deserialize, Debug)] #[serde(rename_all = "camelCase")] From bc6f217f6c7ccb1ed860bc4f8a2f249ad9812dc3 Mon Sep 17 00:00:00 2001 From: Hinton Date: Tue, 1 Jul 2025 10:20:16 +0200 Subject: [PATCH 13/16] Remove folder from wasm --- crates/bitwarden-wasm-internal/src/platform/mod.rs | 6 ------ 1 file changed, 6 deletions(-) diff --git a/crates/bitwarden-wasm-internal/src/platform/mod.rs b/crates/bitwarden-wasm-internal/src/platform/mod.rs index 9d6c4274c..8b71f5867 100644 --- a/crates/bitwarden-wasm-internal/src/platform/mod.rs +++ b/crates/bitwarden-wasm-internal/src/platform/mod.rs @@ -31,7 +31,6 @@ impl StateClient { } repository::create_wasm_repository!(CipherRepository, Cipher, "Repository"); -repository::create_wasm_repository!(FolderRepository, Folder, "Repository"); #[wasm_bindgen] impl StateClient { @@ -39,9 +38,4 @@ impl StateClient { let store = store.into_channel_impl(); self.0.platform().state().register_client_managed(store) } - - pub fn register_folder_repository(&self, store: FolderRepository) { - let store = store.into_channel_impl(); - self.0.platform().state().register_client_managed(store) - } } From 5bb5b4d2de527e8f4dd594ee58f87907710f139e Mon Sep 17 00:00:00 2001 From: Hinton Date: Tue, 1 Jul 2025 10:20:58 +0200 Subject: [PATCH 14/16] lint --- crates/bitwarden-wasm-internal/src/platform/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/bitwarden-wasm-internal/src/platform/mod.rs b/crates/bitwarden-wasm-internal/src/platform/mod.rs index 8b71f5867..1bb5b551b 100644 --- a/crates/bitwarden-wasm-internal/src/platform/mod.rs +++ b/crates/bitwarden-wasm-internal/src/platform/mod.rs @@ -1,5 +1,5 @@ use bitwarden_core::Client; -use bitwarden_vault::{Cipher, Folder}; +use bitwarden_vault::Cipher; use wasm_bindgen::prelude::wasm_bindgen; mod repository; From caec5c526da1f7017d4eafc0597a81ba6f8cc831 Mon Sep 17 00:00:00 2001 From: Hinton Date: Tue, 1 Jul 2025 10:41:24 +0200 Subject: [PATCH 15/16] Document and clean up --- crates/bitwarden-core/src/auth/renew.rs | 2 +- crates/bitwarden-core/src/client/client.rs | 10 +++++----- crates/bitwarden-core/src/client/internal.rs | 6 +++--- .../src/platform/token_provider.rs | 1 + 4 files changed, 10 insertions(+), 9 deletions(-) diff --git a/crates/bitwarden-core/src/auth/renew.rs b/crates/bitwarden-core/src/auth/renew.rs index c5b5093af..959977f32 100644 --- a/crates/bitwarden-core/src/auth/renew.rs +++ b/crates/bitwarden-core/src/auth/renew.rs @@ -40,7 +40,7 @@ async fn renew_token_client_managed( .get_access_token() .await .ok_or(NotAuthenticatedError)?; - client.set_tokens_internal(token); + client.set_api_tokens_internal(token); Ok(()) } diff --git a/crates/bitwarden-core/src/client/client.rs b/crates/bitwarden-core/src/client/client.rs index d1f5ccf1a..6cd632eb4 100644 --- a/crates/bitwarden-core/src/client/client.rs +++ b/crates/bitwarden-core/src/client/client.rs @@ -25,20 +25,20 @@ pub struct Client { } impl Client { - #[allow(missing_docs)] + /// Create a new Bitwarden client with SDK-managed tokens. pub fn new(settings: Option) -> Self { - Self::new_tokens(settings, Tokens::SdkManaged(SdkManagedTokens::default())) + Self::new_internal(settings, Tokens::SdkManaged(SdkManagedTokens::default())) } - #[allow(missing_docs)] + /// Create a new Bitwarden client with client-managed tokens. pub fn new_with_client_tokens( settings: Option, tokens: Arc, ) -> Self { - Self::new_tokens(settings, Tokens::ClientManaged(tokens)) + Self::new_internal(settings, Tokens::ClientManaged(tokens)) } - fn new_tokens(settings_input: Option, tokens: Tokens) -> Self { + fn new_internal(settings_input: Option, tokens: Tokens) -> Self { let settings = settings_input.unwrap_or_default(); fn new_client_builder() -> reqwest::ClientBuilder { diff --git a/crates/bitwarden-core/src/client/internal.rs b/crates/bitwarden-core/src/client/internal.rs index 08cbc3db3..5c0e085b5 100644 --- a/crates/bitwarden-core/src/client/internal.rs +++ b/crates/bitwarden-core/src/client/internal.rs @@ -138,11 +138,11 @@ impl InternalClient { expires_on: Some(Utc::now().timestamp() + expires_in as i64), refresh_token, }); - self.set_tokens_internal(token); + self.set_api_tokens_internal(token); } - /// Used to set tokens for internal API clients, use `set_tokens` for SdkManagedTokens. - pub(crate) fn set_tokens_internal(&self, token: String) { + /// Sets api tokens for only internal API clients, use `set_tokens` for SdkManagedTokens. + pub(crate) fn set_api_tokens_internal(&self, token: String) { let mut guard = self .__api_configurations .write() diff --git a/crates/bitwarden-wasm-internal/src/platform/token_provider.rs b/crates/bitwarden-wasm-internal/src/platform/token_provider.rs index 2cbc67d87..461bc8b26 100644 --- a/crates/bitwarden-wasm-internal/src/platform/token_provider.rs +++ b/crates/bitwarden-wasm-internal/src/platform/token_provider.rs @@ -18,6 +18,7 @@ extern "C" { pub async fn get_access_token(this: &JsTokenProvider) -> JsValue; } +/// Thread-bound runner for JavaScript token provider pub(crate) struct WasmClientManagedTokens(ThreadBoundRunner); impl WasmClientManagedTokens { From 5c67cd4621f1dfb0affcdc0207a1cb01adbefbd6 Mon Sep 17 00:00:00 2001 From: Hinton Date: Tue, 1 Jul 2025 10:43:07 +0200 Subject: [PATCH 16/16] Remove wasm bindgen futures from vault --- Cargo.lock | 1 - crates/bitwarden-vault/Cargo.toml | 4 +--- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index d09378f89..82d1c09b0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -707,7 +707,6 @@ dependencies = [ "uniffi", "uuid", "wasm-bindgen", - "wasm-bindgen-futures", ] [[package]] diff --git a/crates/bitwarden-vault/Cargo.toml b/crates/bitwarden-vault/Cargo.toml index d49b95274..e06ce1460 100644 --- a/crates/bitwarden-vault/Cargo.toml +++ b/crates/bitwarden-vault/Cargo.toml @@ -23,8 +23,7 @@ uniffi = [ wasm = [ "bitwarden-core/wasm", "dep:tsify-next", - "dep:wasm-bindgen", - "dep:wasm-bindgen-futures" + "dep:wasm-bindgen" ] # WASM support [dependencies] @@ -49,7 +48,6 @@ tsify-next = { workspace = true, optional = true } uniffi = { workspace = true, optional = true } uuid = { workspace = true } wasm-bindgen = { workspace = true, optional = true } -wasm-bindgen-futures = { workspace = true, optional = true } [dev-dependencies] tokio = { workspace = true, features = ["rt"] }