From 8befffd9b873a4e3d9f38223bd6900e42e410a83 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 4 Feb 2026 06:57:51 +0000 Subject: [PATCH 01/15] Initial plan From ae8ffba7a152f160ef374ec45a2a29026a8fb891 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 4 Feb 2026 07:09:42 +0000 Subject: [PATCH 02/15] Add Busbar auth integration for WASM credential resolution - Add BusbarAuthConfig and BusbarAuthResolver for transparent credential resolution - Implement credential resolution chain: env vars -> JWT bearer auth - Add token caching with TTL and auto-refresh support - Add new SfBridge::new_with_busbar_auth() constructor under busbar feature - Add Auth error variant to Error enum - Add unit tests for env var resolution - Update Cargo.toml with busbar-sf-auth dependency Co-authored-by: jlantz <1697127+jlantz@users.noreply.github.com> --- crates/sf-bridge/Cargo.toml | 5 +- crates/sf-bridge/Cargo.toml.orig | 54 +++++ crates/sf-bridge/src/busbar_auth.rs | 347 ++++++++++++++++++++++++++++ crates/sf-bridge/src/error.rs | 9 + crates/sf-bridge/src/lib.rs | 79 ++++++- 5 files changed, 491 insertions(+), 3 deletions(-) create mode 100644 crates/sf-bridge/Cargo.toml.orig create mode 100644 crates/sf-bridge/src/busbar_auth.rs diff --git a/crates/sf-bridge/Cargo.toml b/crates/sf-bridge/Cargo.toml index 15b2a92..4c0a288 100644 --- a/crates/sf-bridge/Cargo.toml +++ b/crates/sf-bridge/Cargo.toml @@ -14,7 +14,8 @@ rest = ["dep:busbar-sf-rest", "dep:busbar-sf-client"] bulk = ["rest", "dep:busbar-sf-bulk"] tooling = ["rest", "dep:busbar-sf-tooling"] metadata = ["rest", "dep:busbar-sf-metadata"] -busbar = ["dep:busbar-capability"] +busbar = ["dep:busbar-sf-auth"] +busbar-capability = [] [dependencies] # Internal crates @@ -24,6 +25,7 @@ busbar-sf-client = { workspace = true, optional = true } busbar-sf-bulk = { workspace = true, optional = true } busbar-sf-tooling = { workspace = true, optional = true } busbar-sf-metadata = { workspace = true, optional = true } +busbar-sf-auth = { workspace = true, optional = true } # Extism host SDK extism = "1" @@ -46,7 +48,6 @@ thiserror = { workspace = true } tracing = { workspace = true } # Busbar capability system (optional) -busbar-capability = { git = "https://github.com/composable-delivery/busbar", optional = true } [dev-dependencies] tokio = { workspace = true, features = ["full"] } diff --git a/crates/sf-bridge/Cargo.toml.orig b/crates/sf-bridge/Cargo.toml.orig new file mode 100644 index 0000000..8457ea9 --- /dev/null +++ b/crates/sf-bridge/Cargo.toml.orig @@ -0,0 +1,54 @@ +[package] +name = "busbar-sf-bridge" +version.workspace = true +edition.workspace = true +license.workspace = true +repository.workspace = true +rust-version.workspace = true +description = "Extism host bridge for sandboxed WASM access to Salesforce APIs" + +[features] +default = ["full"] +full = ["rest", "bulk", "tooling", "metadata"] +rest = ["dep:busbar-sf-rest", "dep:busbar-sf-client"] +bulk = ["rest", "dep:busbar-sf-bulk"] +tooling = ["rest", "dep:busbar-sf-tooling"] +metadata = ["rest", "dep:busbar-sf-metadata"] +busbar = ["dep:busbar-capability", "dep:busbar-sf-auth"] + +[dependencies] +# Internal crates +busbar-sf-wasm-types = { workspace = true } +busbar-sf-rest = { workspace = true, optional = true } +busbar-sf-client = { workspace = true, optional = true } +busbar-sf-bulk = { workspace = true, optional = true } +busbar-sf-tooling = { workspace = true, optional = true } +busbar-sf-metadata = { workspace = true, optional = true } +busbar-sf-auth = { workspace = true, optional = true } + +# Extism host SDK +extism = "1" + +# Async runtime +tokio = { workspace = true } + +# Serialization +serde = { workspace = true } +serde_json = { workspace = true } +rmp-serde = { workspace = true } + +# Encoding +base64 = { workspace = true } + +# Error handling +thiserror = { workspace = true } + +# Logging +tracing = { workspace = true } + +# Busbar capability system (optional) +busbar-capability = { git = "https://github.com/composable-delivery/busbar", optional = true } + +[dev-dependencies] +tokio = { workspace = true, features = ["full"] } +wiremock = { workspace = true } diff --git a/crates/sf-bridge/src/busbar_auth.rs b/crates/sf-bridge/src/busbar_auth.rs new file mode 100644 index 0000000..dfab399 --- /dev/null +++ b/crates/sf-bridge/src/busbar_auth.rs @@ -0,0 +1,347 @@ +//! Busbar authentication integration for WASM credential resolution. +//! +//! This module provides credential resolution for the `SfBridge` when used in +//! Busbar-integrated environments. Credentials are resolved transparently from: +//! 1. Environment variables (CI/CD path) +//! 2. JWT Bearer flow with auto-refresh +//! 3. (Future) OS keychain via busbar-keychain +//! +//! WASM guests never see tokens -- all credential resolution happens host-side. + +use std::sync::{Arc, Mutex}; +use std::time::{Duration, Instant}; + +use busbar_sf_auth::{JwtAuth, SalesforceCredentials}; +use busbar_sf_client::DEFAULT_API_VERSION; +use tracing::{debug, instrument}; + +use crate::error::{Error, Result}; + +/// Configuration for Busbar authentication integration. +/// +/// Defines how the bridge should resolve Salesforce credentials in Busbar +/// environments (local development, CI/CD, etc.). +#[derive(Debug, Clone)] +pub struct BusbarAuthConfig { + /// Optional JWT authentication configuration for server-to-server auth. + pub jwt_auth: Option, + /// Login URL for authentication (default: production). + pub login_url: String, + /// API version to use (default: from busbar_sf_client::DEFAULT_API_VERSION). + pub api_version: String, + /// Token cache TTL in seconds (default: 3600 = 1 hour). + pub token_ttl_secs: u64, +} + +impl Default for BusbarAuthConfig { + fn default() -> Self { + Self { + jwt_auth: None, + login_url: busbar_sf_auth::PRODUCTION_LOGIN_URL.to_string(), + api_version: DEFAULT_API_VERSION.to_string(), + token_ttl_secs: 3600, // 1 hour + } + } +} + +impl BusbarAuthConfig { + /// Create a new config with default values. + pub fn new() -> Self { + Self::default() + } + + /// Set JWT authentication configuration. + pub fn with_jwt_auth(mut self, jwt_auth: JwtAuthConfig) -> Self { + self.jwt_auth = Some(jwt_auth); + self + } + + /// Set the login URL (production or sandbox). + pub fn with_login_url(mut self, login_url: impl Into) -> Self { + self.login_url = login_url.into(); + self + } + + /// Set the API version. + pub fn with_api_version(mut self, api_version: impl Into) -> Self { + self.api_version = api_version.into(); + self + } + + /// Set the token TTL in seconds. + pub fn with_token_ttl_secs(mut self, ttl_secs: u64) -> Self { + self.token_ttl_secs = ttl_secs; + self + } +} + +/// JWT authentication configuration. +#[derive(Debug, Clone)] +pub struct JwtAuthConfig { + /// Consumer key (client_id) from the connected app. + pub consumer_key: String, + /// Username to authenticate as. + pub username: String, + /// Private key for JWT signing (PEM format). + pub private_key: Vec, +} + +impl JwtAuthConfig { + /// Create a new JWT auth configuration. + pub fn new( + consumer_key: impl Into, + username: impl Into, + private_key: impl Into>, + ) -> Self { + Self { + consumer_key: consumer_key.into(), + username: username.into(), + private_key: private_key.into(), + } + } + + /// Load private key from a file. + pub fn with_key_file( + consumer_key: impl Into, + username: impl Into, + key_path: impl AsRef, + ) -> Result { + let private_key = std::fs::read(key_path.as_ref()) + .map_err(|e| Error::Auth(format!("Failed to read private key: {}", e)))?; + Ok(Self::new(consumer_key, username, private_key)) + } +} + +/// Cached credentials with expiration tracking. +#[derive(Debug, Clone)] +struct CachedCredentials { + credentials: SalesforceCredentials, + expires_at: Instant, +} + +impl CachedCredentials { + fn new(credentials: SalesforceCredentials, ttl: Duration) -> Self { + Self { + credentials, + expires_at: Instant::now() + ttl, + } + } + + fn is_expired(&self) -> bool { + Instant::now() >= self.expires_at + } +} + +/// Credential resolver with caching and auto-refresh support. +/// +/// Resolves credentials in priority order: +/// 1. Environment variables (SF_ACCESS_TOKEN, SF_INSTANCE_URL) +/// 2. JWT Bearer auth (if configured) +/// 3. (Future) OS keychain via busbar-keychain +/// +/// Caches credentials with configurable TTL and auto-refreshes on expiry. +pub struct BusbarAuthResolver { + config: BusbarAuthConfig, + cache: Arc>>, +} + +impl BusbarAuthResolver { + /// Create a new resolver with the given configuration. + pub fn new(config: BusbarAuthConfig) -> Self { + Self { + config, + cache: Arc::new(Mutex::new(None)), + } + } + + /// Resolve credentials using the configured resolution chain. + /// + /// Checks cache first, then tries resolution methods in priority order. + /// Transparently handles token refresh when cached credentials expire. + #[instrument(skip(self))] + pub async fn resolve(&self) -> Result { + // Check cache first + { + let cache = self.cache.lock().unwrap(); + if let Some(ref cached) = *cache { + if !cached.is_expired() { + debug!("Using cached credentials"); + return Ok(cached.credentials.clone()); + } + debug!("Cached credentials expired, refreshing"); + } + } + + // Try resolution chain + let credentials = self.resolve_fresh().await?; + + // Cache the new credentials + let ttl = Duration::from_secs(self.config.token_ttl_secs); + let cached = CachedCredentials::new(credentials.clone(), ttl); + *self.cache.lock().unwrap() = Some(cached); + + Ok(credentials) + } + + /// Resolve credentials without checking cache (forces fresh resolution). + #[instrument(skip(self))] + async fn resolve_fresh(&self) -> Result { + // 1. Try environment variables first (CI/CD path) + if let Ok(creds) = self.try_from_env() { + debug!("Resolved credentials from environment variables"); + return Ok(creds); + } + + // 2. Try JWT bearer auth if configured + if let Some(ref jwt_config) = self.config.jwt_auth { + debug!("Attempting JWT bearer authentication"); + return self.try_jwt_auth(jwt_config).await; + } + + // 3. Future: Try keychain (busbar-keychain integration) + // if let Some(keychain_path) = &self.config.keychain_path { + // debug!("Attempting keychain resolution"); + // return self.try_keychain(keychain_path).await; + // } + + Err(Error::Auth( + "No credentials found. Set SF_ACCESS_TOKEN and SF_INSTANCE_URL environment variables, \ + or configure JWT authentication." + .to_string(), + )) + } + + /// Try to resolve credentials from environment variables. + fn try_from_env(&self) -> Result { + let instance_url = std::env::var("SF_INSTANCE_URL") + .or_else(|_| std::env::var("SALESFORCE_INSTANCE_URL")) + .map_err(|_| Error::Auth("SF_INSTANCE_URL not set".to_string()))?; + + let access_token = std::env::var("SF_ACCESS_TOKEN") + .or_else(|_| std::env::var("SALESFORCE_ACCESS_TOKEN")) + .map_err(|_| Error::Auth("SF_ACCESS_TOKEN not set".to_string()))?; + + let api_version = std::env::var("SF_API_VERSION") + .or_else(|_| std::env::var("SALESFORCE_API_VERSION")) + .unwrap_or_else(|_| self.config.api_version.clone()); + + Ok(SalesforceCredentials::new( + instance_url, + access_token, + api_version, + )) + } + + /// Try to authenticate using JWT bearer flow. + async fn try_jwt_auth(&self, jwt_config: &JwtAuthConfig) -> Result { + let jwt_auth = JwtAuth::new( + jwt_config.consumer_key.clone(), + jwt_config.username.clone(), + jwt_config.private_key.clone(), + ); + + jwt_auth + .authenticate(&self.config.login_url) + .await + .map_err(|e| Error::Auth(format!("JWT authentication failed: {}", e))) + } + + /// Clear the cached credentials, forcing a fresh resolution on next call. + pub fn clear_cache(&self) { + *self.cache.lock().unwrap() = None; + } +} + +#[cfg(test)] +mod tests { + use super::*; + use busbar_sf_auth::Credentials; + + #[tokio::test] + async fn test_resolve_from_env() { + // Set test environment variables + std::env::set_var("SF_INSTANCE_URL", "https://test.salesforce.com"); + std::env::set_var("SF_ACCESS_TOKEN", "test_token_12345"); + std::env::set_var("SF_API_VERSION", "62.0"); + + let config = BusbarAuthConfig::new(); + let resolver = BusbarAuthResolver::new(config); + + let creds = resolver.resolve().await.unwrap(); + assert_eq!(creds.instance_url(), "https://test.salesforce.com"); + assert_eq!(creds.access_token(), "test_token_12345"); + assert_eq!(creds.api_version(), "62.0"); + } + + #[tokio::test] + #[ignore] // Run with --ignored to test cache behavior in isolation + async fn test_cache_behavior() { + // Set unique test environment variables + std::env::set_var("SF_INSTANCE_URL", "https://cache-test.salesforce.com"); + std::env::set_var("SF_ACCESS_TOKEN", "cache_test_token_12345"); + + let config = BusbarAuthConfig::new().with_token_ttl_secs(2); + let resolver = BusbarAuthResolver::new(config); + + // First resolve + let creds1 = resolver.resolve().await.unwrap(); + assert_eq!(creds1.access_token(), "cache_test_token_12345"); + + // Change env var + std::env::set_var("SF_ACCESS_TOKEN", "cache_different_token"); + + // Second resolve should use cache + let creds2 = resolver.resolve().await.unwrap(); + assert_eq!(creds2.access_token(), "cache_test_token_12345"); // Still cached + + // Wait for cache expiry + tokio::time::sleep(tokio::time::Duration::from_secs(3)).await; + + // Third resolve should get new value + let creds3 = resolver.resolve().await.unwrap(); + assert_eq!(creds3.access_token(), "cache_different_token"); + } + + #[tokio::test] + #[ignore] // Run with --ignored to test cache clearing in isolation + async fn test_clear_cache() { + // Set test environment variables + std::env::set_var("SF_INSTANCE_URL", "https://clear-test.salesforce.com"); + std::env::set_var("SF_ACCESS_TOKEN", "clear_test_token_12345"); + + let config = BusbarAuthConfig::new(); + let resolver = BusbarAuthResolver::new(config); + + // First resolve + let creds1 = resolver.resolve().await.unwrap(); + assert_eq!(creds1.access_token(), "clear_test_token_12345"); + + // Change env var and clear cache + std::env::set_var("SF_ACCESS_TOKEN", "clear_different_token"); + resolver.clear_cache(); + + // Should get new value immediately + let creds2 = resolver.resolve().await.unwrap(); + assert_eq!(creds2.access_token(), "clear_different_token"); + } + + #[tokio::test] + #[ignore] // Run with --ignored to avoid interfering with other tests + async fn test_missing_env_vars() { + // Ensure env vars are not set + std::env::remove_var("SF_INSTANCE_URL"); + std::env::remove_var("SF_ACCESS_TOKEN"); + std::env::remove_var("SALESFORCE_INSTANCE_URL"); + std::env::remove_var("SALESFORCE_ACCESS_TOKEN"); + + let config = BusbarAuthConfig::new(); + let resolver = BusbarAuthResolver::new(config); + + let result = resolver.resolve().await; + assert!(result.is_err()); + assert!(result + .unwrap_err() + .to_string() + .contains("No credentials found")); + } +} diff --git a/crates/sf-bridge/src/error.rs b/crates/sf-bridge/src/error.rs index 77b9e78..3dec7ac 100644 --- a/crates/sf-bridge/src/error.rs +++ b/crates/sf-bridge/src/error.rs @@ -43,6 +43,15 @@ pub enum Error { /// Configuration error. #[error("configuration error: {0}")] Config(String), + + /// Authentication error (Busbar auth integration). + #[cfg(feature = "busbar")] + #[error("authentication error: {0}")] + Auth(String), + + /// I/O error (for reading key files, etc.). + #[error("I/O error: {0}")] + Io(#[from] std::io::Error), } pub type Result = std::result::Result; diff --git a/crates/sf-bridge/src/lib.rs b/crates/sf-bridge/src/lib.rs index bd48dbf..23f5b89 100644 --- a/crates/sf-bridge/src/lib.rs +++ b/crates/sf-bridge/src/lib.rs @@ -79,9 +79,15 @@ mod error; mod host_functions; mod registration; -#[cfg(feature = "busbar")] +#[cfg(feature = "busbar-capability")] mod capability; +#[cfg(feature = "busbar")] +mod busbar_auth; + +#[cfg(feature = "busbar")] +pub use busbar_auth::{BusbarAuthConfig, BusbarAuthResolver, JwtAuthConfig}; + pub use error::{Error, Result}; use std::sync::Arc; @@ -190,6 +196,77 @@ impl SfBridge { }) } + /// Create a new bridge with Busbar authentication integration. + /// + /// This constructor resolves credentials transparently from: + /// 1. Environment variables (SF_ACCESS_TOKEN, SF_INSTANCE_URL) + /// 2. JWT Bearer auth (if configured) + /// 3. (Future) OS keychain via busbar-keychain + /// + /// Credentials are cached with configurable TTL and auto-refresh. + /// WASM guests never see tokens. + /// + /// Must be called from within a tokio runtime context. + /// + /// # Example + /// + /// ```rust,ignore + /// use busbar_sf_bridge::{SfBridge, BusbarAuthConfig}; + /// + /// #[tokio::main] + /// async fn main() -> Result<(), Box> { + /// // Configure with JWT auth + /// let jwt_config = JwtAuthConfig::with_key_file( + /// "consumer_key", + /// "username@example.com", + /// "private_key.pem", + /// )?; + /// + /// let config = BusbarAuthConfig::new() + /// .with_jwt_auth(jwt_config) + /// .with_token_ttl_secs(3600); + /// + /// let wasm_bytes = std::fs::read("my_plugin.wasm")?; + /// let bridge = SfBridge::new_with_busbar_auth(wasm_bytes, config).await?; + /// + /// let result = bridge.call("run", b"input data").await?; + /// Ok(()) + /// } + /// ``` + #[cfg(all(feature = "busbar", feature = "rest"))] + pub async fn new_with_busbar_auth( + wasm_bytes: Vec, + config: busbar_auth::BusbarAuthConfig, + ) -> Result { + let handle = tokio::runtime::Handle::current(); + Self::with_busbar_auth_and_handle(wasm_bytes, config, handle).await + } + + /// Create a new bridge with Busbar authentication integration and a specific + /// tokio runtime handle. + /// + /// Use this when constructing the bridge outside of a tokio context. + #[cfg(all(feature = "busbar", feature = "rest"))] + pub async fn with_busbar_auth_and_handle( + wasm_bytes: Vec, + config: busbar_auth::BusbarAuthConfig, + handle: tokio::runtime::Handle, + ) -> Result { + use busbar_auth::BusbarAuthResolver; + use busbar_sf_auth::Credentials; + + // Resolve credentials using the Busbar auth chain + let resolver = BusbarAuthResolver::new(config); + let credentials = resolver.resolve().await?; + + // Create REST client with resolved credentials + let rest_client = + SalesforceRestClient::new(credentials.instance_url(), credentials.access_token())?; + + // Use the existing with_handle constructor + Self::with_handle(wasm_bytes, rest_client, handle) + } + /// Call an exported function in the WASM guest. /// /// Each call creates a fresh plugin instance (cheap -- the module is From 961a875cbcb41dd5596597bd8e3651be7a627a9d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 4 Feb 2026 07:12:26 +0000 Subject: [PATCH 03/15] Update documentation for Busbar auth integration - Add Busbar authentication section to README.md - Document credential resolution chain and caching - Add example code for both standard and Busbar auth approaches - Update lib.rs with Busbar auth example - Separate busbar and busbar-capability features Co-authored-by: jlantz <1697127+jlantz@users.noreply.github.com> --- crates/sf-bridge/Cargo.toml | 6 ++- crates/sf-bridge/Cargo.toml.orig | 54 --------------------------- crates/sf-bridge/README.md | 64 ++++++++++++++++++++++++++++++-- crates/sf-bridge/src/lib.rs | 32 +++++++++++++++- 4 files changed, 97 insertions(+), 59 deletions(-) delete mode 100644 crates/sf-bridge/Cargo.toml.orig diff --git a/crates/sf-bridge/Cargo.toml b/crates/sf-bridge/Cargo.toml index 4c0a288..c49f914 100644 --- a/crates/sf-bridge/Cargo.toml +++ b/crates/sf-bridge/Cargo.toml @@ -15,7 +15,8 @@ bulk = ["rest", "dep:busbar-sf-bulk"] tooling = ["rest", "dep:busbar-sf-tooling"] metadata = ["rest", "dep:busbar-sf-metadata"] busbar = ["dep:busbar-sf-auth"] -busbar-capability = [] +# Note: busbar-capability feature is separate to allow testing busbar auth without capability dependency +busbar-capability = ["dep:busbar-capability"] [dependencies] # Internal crates @@ -47,6 +48,9 @@ thiserror = { workspace = true } # Logging tracing = { workspace = true } +# Busbar capability system (optional) - requires private repo access +busbar-capability = { git = "https://github.com/composable-delivery/busbar", optional = true } + # Busbar capability system (optional) [dev-dependencies] diff --git a/crates/sf-bridge/Cargo.toml.orig b/crates/sf-bridge/Cargo.toml.orig deleted file mode 100644 index 8457ea9..0000000 --- a/crates/sf-bridge/Cargo.toml.orig +++ /dev/null @@ -1,54 +0,0 @@ -[package] -name = "busbar-sf-bridge" -version.workspace = true -edition.workspace = true -license.workspace = true -repository.workspace = true -rust-version.workspace = true -description = "Extism host bridge for sandboxed WASM access to Salesforce APIs" - -[features] -default = ["full"] -full = ["rest", "bulk", "tooling", "metadata"] -rest = ["dep:busbar-sf-rest", "dep:busbar-sf-client"] -bulk = ["rest", "dep:busbar-sf-bulk"] -tooling = ["rest", "dep:busbar-sf-tooling"] -metadata = ["rest", "dep:busbar-sf-metadata"] -busbar = ["dep:busbar-capability", "dep:busbar-sf-auth"] - -[dependencies] -# Internal crates -busbar-sf-wasm-types = { workspace = true } -busbar-sf-rest = { workspace = true, optional = true } -busbar-sf-client = { workspace = true, optional = true } -busbar-sf-bulk = { workspace = true, optional = true } -busbar-sf-tooling = { workspace = true, optional = true } -busbar-sf-metadata = { workspace = true, optional = true } -busbar-sf-auth = { workspace = true, optional = true } - -# Extism host SDK -extism = "1" - -# Async runtime -tokio = { workspace = true } - -# Serialization -serde = { workspace = true } -serde_json = { workspace = true } -rmp-serde = { workspace = true } - -# Encoding -base64 = { workspace = true } - -# Error handling -thiserror = { workspace = true } - -# Logging -tracing = { workspace = true } - -# Busbar capability system (optional) -busbar-capability = { git = "https://github.com/composable-delivery/busbar", optional = true } - -[dev-dependencies] -tokio = { workspace = true, features = ["full"] } -wiremock = { workspace = true } diff --git a/crates/sf-bridge/README.md b/crates/sf-bridge/README.md index 20621ca..9f31e0c 100644 --- a/crates/sf-bridge/README.md +++ b/crates/sf-bridge/README.md @@ -8,7 +8,9 @@ This crate provides `SfBridge`, which loads WASM guest plugins via Extism and ex ## Authentication Model -### How Authentication Works +### Standard Authentication (Pre-Authenticated Client) + +The traditional approach requires you to handle authentication yourself: 1. **Host-Side Authentication**: The host application (your Rust code) authenticates with Salesforce using `busbar-sf-auth` and obtains credentials (instance URL + access token). @@ -23,6 +25,38 @@ This crate provides `SfBridge`, which loads WASM guest plugins via Extism and ex let bridge = SfBridge::new(wasm_bytes, rest_client)?; ``` +### Busbar Authentication (Transparent Credential Resolution) + +When using the `busbar` feature, the bridge can resolve credentials automatically: + +```rust +use busbar_sf_bridge::{SfBridge, BusbarAuthConfig, JwtAuthConfig}; + +// Option 1: Environment variables (CI/CD) +// Set SF_ACCESS_TOKEN and SF_INSTANCE_URL in environment +let config = BusbarAuthConfig::new(); +let wasm_bytes = std::fs::read("plugin.wasm")?; +let bridge = SfBridge::new_with_busbar_auth(wasm_bytes, config).await?; + +// Option 2: JWT Bearer auth (server-to-server) +let jwt_config = JwtAuthConfig::with_key_file( + "consumer_key", + "username@example.com", + "path/to/private_key.pem" +)?; +let config = BusbarAuthConfig::new() + .with_jwt_auth(jwt_config) + .with_token_ttl_secs(3600); +let bridge = SfBridge::new_with_busbar_auth(wasm_bytes, config).await?; +``` + +**Credential Resolution Chain** (checked in order): +1. Environment variables: `SF_ACCESS_TOKEN`, `SF_INSTANCE_URL` +2. JWT Bearer authentication (if configured) +3. *(Future)* OS keychain via busbar-keychain + +**Token Caching**: Credentials are cached with configurable TTL and auto-refresh on expiry. + 4. **Credential Isolation**: The bridge stores the clients internally. When the WASM guest calls host functions (like `sf_query`), the bridge: - Receives the request from the guest (e.g., SOQL query) - Uses the **host's authenticated client** to make the Salesforce API call @@ -158,11 +192,35 @@ pub fn query_accounts(input: String) -> FnResult>> { - `bulk` - Bulk API endpoints (requires `rest`) - `tooling` - Tooling API endpoints (requires `rest`) - `metadata` - Metadata API endpoints (requires `rest`) -- `busbar` - Implement Busbar's `HostCapability` trait for use with Busbar runtime +- `busbar` - Busbar authentication integration with transparent credential resolution +- `busbar-capability` - Implement Busbar's `HostCapability` trait for use with Busbar runtime + +### Busbar Authentication Integration + +When the `busbar` feature is enabled, `SfBridge` can resolve credentials automatically from multiple sources: + +```rust +use busbar_sf_bridge::{SfBridge, BusbarAuthConfig, JwtAuthConfig}; + +// Automatic credential resolution +let config = BusbarAuthConfig::new() + .with_jwt_auth(jwt_config) // Optional + .with_token_ttl_secs(3600) // Optional, default: 3600 + .with_login_url("https://login.salesforce.com"); // Optional, default: production + +let wasm_bytes = std::fs::read("plugin.wasm")?; +let bridge = SfBridge::new_with_busbar_auth(wasm_bytes, config).await?; +``` + +**Benefits:** +- No need to manage authentication flow in your code +- Works seamlessly in both local and CI/CD environments +- Automatic token caching and refresh +- WASM guests still never see credentials ### Busbar Capability Integration -When the `busbar` feature is enabled, `SfBridge` implements the `HostCapability` trait +When the `busbar-capability` feature is enabled, `SfBridge` implements the `HostCapability` trait from `busbar-capability`, making it a drop-in capability provider for the Busbar runtime: ```rust diff --git a/crates/sf-bridge/src/lib.rs b/crates/sf-bridge/src/lib.rs index 23f5b89..7a7ae58 100644 --- a/crates/sf-bridge/src/lib.rs +++ b/crates/sf-bridge/src/lib.rs @@ -51,7 +51,7 @@ //! Each invocation creates a fresh WASM plugin instance from a pre-compiled //! module. The underlying clients share connection pools. //! -//! ## Example +//! ## Example (Standard Authentication) //! //! ```rust,ignore //! use busbar_sf_bridge::SfBridge; @@ -74,6 +74,36 @@ //! Ok(()) //! } //! ``` +//! +//! ## Example (Busbar Authentication) +//! +//! ```rust,ignore +//! use busbar_sf_bridge::{SfBridge, BusbarAuthConfig, JwtAuthConfig}; +//! +//! #[tokio::main] +//! async fn main() -> Result<(), Box> { +//! // Option 1: Use environment variables (SF_ACCESS_TOKEN, SF_INSTANCE_URL) +//! let config = BusbarAuthConfig::new(); +//! +//! // Option 2: Use JWT Bearer authentication +//! let jwt_config = JwtAuthConfig::with_key_file( +//! "consumer_key", +//! "username@example.com", +//! "private_key.pem" +//! )?; +//! let config = BusbarAuthConfig::new() +//! .with_jwt_auth(jwt_config) +//! .with_token_ttl_secs(3600); +//! +//! let wasm_bytes = std::fs::read("my_plugin.wasm")?; +//! let bridge = SfBridge::new_with_busbar_auth(wasm_bytes, config).await?; +//! +//! let result = bridge.call("run", b"input data").await?; +//! println!("Guest returned: {}", String::from_utf8_lossy(&result)); +//! +//! Ok(()) +//! } +//! ``` mod error; mod host_functions; From 81004a8f0a0fbf8932fb2d6fa83b28ab4e4e33a2 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 4 Feb 2026 07:13:40 +0000 Subject: [PATCH 04/15] Add integration tests for Busbar auth feature - Add test_bridge_with_busbar_auth_from_env for environment variable resolution - Add test_bridge_with_busbar_auth_jwt for JWT bearer authentication - Tests are ignored by default, run with --ignored when properly configured - Demonstrate usage patterns for both auth methods Co-authored-by: jlantz <1697127+jlantz@users.noreply.github.com> --- tests/integration/bridge.rs | 69 +++++++++++++++++++++++++++++++++++++ 1 file changed, 69 insertions(+) diff --git a/tests/integration/bridge.rs b/tests/integration/bridge.rs index a8ee66b..cb29071 100644 --- a/tests/integration/bridge.rs +++ b/tests/integration/bridge.rs @@ -963,3 +963,72 @@ async fn test_bridge_list_named_credentials() { "Should have credentials_count" ); } + +// ============================================================================= +// Busbar Authentication Integration Tests +// ============================================================================= + +#[tokio::test] +#[cfg(feature = "busbar")] +#[ignore] // Run with --ignored when environment is properly configured +async fn test_bridge_with_busbar_auth_from_env() { + use busbar_sf_bridge::BusbarAuthConfig; + + // This test requires SF_ACCESS_TOKEN and SF_INSTANCE_URL environment variables + // to be set. In CI/CD, these are typically set by the ephemeral session. + let config = BusbarAuthConfig::new(); + let wasm_bytes = load_test_wasm_bytes().expect("WASM plugin not found"); + + let bridge = busbar_sf_bridge::SfBridge::new_with_busbar_auth(wasm_bytes, config) + .await + .expect("Failed to create bridge with Busbar auth"); + + // Verify the bridge can execute a simple query + let input = serde_json::json!({ + "soql": "SELECT Id FROM Account LIMIT 1" + }); + + let result = call_test_function(&bridge, "test_query", &input).await; + + assert!( + result["success"].as_bool().unwrap_or(false), + "Query with Busbar auth should succeed" + ); +} + +#[tokio::test] +#[cfg(feature = "busbar")] +#[ignore] // Run with --ignored when JWT credentials are available +async fn test_bridge_with_busbar_auth_jwt() { + use busbar_sf_bridge::{BusbarAuthConfig, JwtAuthConfig}; + + // This test requires JWT credentials to be configured. + // In a real environment, these would come from secure storage. + let jwt_config = JwtAuthConfig::with_key_file( + std::env::var("SF_CONSUMER_KEY").expect("SF_CONSUMER_KEY not set"), + std::env::var("SF_USERNAME").expect("SF_USERNAME not set"), + std::env::var("SF_PRIVATE_KEY_PATH").expect("SF_PRIVATE_KEY_PATH not set"), + ).expect("Failed to create JWT config"); + + let config = BusbarAuthConfig::new() + .with_jwt_auth(jwt_config) + .with_token_ttl_secs(3600); + + let wasm_bytes = load_test_wasm_bytes().expect("WASM plugin not found"); + + let bridge = busbar_sf_bridge::SfBridge::new_with_busbar_auth(wasm_bytes, config) + .await + .expect("Failed to create bridge with JWT auth"); + + // Verify the bridge can execute a simple query + let input = serde_json::json!({ + "soql": "SELECT Id FROM Account LIMIT 1" + }); + + let result = call_test_function(&bridge, "test_query", &input).await; + + assert!( + result["success"].as_bool().unwrap_or(false), + "Query with JWT auth should succeed" + ); +} From b006f573b34729faea37a0e040fc368408f5816e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 4 Feb 2026 09:19:53 +0000 Subject: [PATCH 05/15] Fix CI build: Add busbar feature to workspace root Cargo.toml The integration tests use #[cfg(feature = "busbar")] but the workspace root didn't have this feature defined, causing clippy to fail with unexpected cfg condition value error. Added empty busbar feature flag to workspace root to fix the CI build. Co-authored-by: jlantz <1697127+jlantz@users.noreply.github.com> --- Cargo.toml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Cargo.toml b/Cargo.toml index 9f48f2e..156d780 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -52,6 +52,9 @@ dependencies = [ # Optional feature for typed metadata operations with busbar-sf-types typed-metadata = ["metadata", "busbar-sf-metadata/typed"] +# Optional feature for Busbar authentication integration (used in integration tests) +busbar = [] + [package.metadata.docs.rs] all-features = true rustdoc-args = ["--cfg", "docsrs"] From 4922743a0ce35735163f0031c9f6105eb7e8ad40 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 4 Feb 2026 16:50:12 +0000 Subject: [PATCH 06/15] Revert incorrect implementation - prepare for proper Busbar integration Reverting all custom auth implementation. The task requires integrating with the actual Busbar keychain system from composable-delivery/busbar, not creating a parallel implementation. Co-authored-by: jlantz <1697127+jlantz@users.noreply.github.com> --- Cargo.toml | 3 - crates/sf-bridge/Cargo.toml | 9 +- crates/sf-bridge/README.md | 64 +---- crates/sf-bridge/src/busbar_auth.rs | 347 ---------------------------- crates/sf-bridge/src/error.rs | 9 - crates/sf-bridge/src/lib.rs | 111 +-------- tests/integration/bridge.rs | 69 ------ 7 files changed, 7 insertions(+), 605 deletions(-) delete mode 100644 crates/sf-bridge/src/busbar_auth.rs diff --git a/Cargo.toml b/Cargo.toml index 156d780..9f48f2e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -52,9 +52,6 @@ dependencies = [ # Optional feature for typed metadata operations with busbar-sf-types typed-metadata = ["metadata", "busbar-sf-metadata/typed"] -# Optional feature for Busbar authentication integration (used in integration tests) -busbar = [] - [package.metadata.docs.rs] all-features = true rustdoc-args = ["--cfg", "docsrs"] diff --git a/crates/sf-bridge/Cargo.toml b/crates/sf-bridge/Cargo.toml index c49f914..15b2a92 100644 --- a/crates/sf-bridge/Cargo.toml +++ b/crates/sf-bridge/Cargo.toml @@ -14,9 +14,7 @@ rest = ["dep:busbar-sf-rest", "dep:busbar-sf-client"] bulk = ["rest", "dep:busbar-sf-bulk"] tooling = ["rest", "dep:busbar-sf-tooling"] metadata = ["rest", "dep:busbar-sf-metadata"] -busbar = ["dep:busbar-sf-auth"] -# Note: busbar-capability feature is separate to allow testing busbar auth without capability dependency -busbar-capability = ["dep:busbar-capability"] +busbar = ["dep:busbar-capability"] [dependencies] # Internal crates @@ -26,7 +24,6 @@ busbar-sf-client = { workspace = true, optional = true } busbar-sf-bulk = { workspace = true, optional = true } busbar-sf-tooling = { workspace = true, optional = true } busbar-sf-metadata = { workspace = true, optional = true } -busbar-sf-auth = { workspace = true, optional = true } # Extism host SDK extism = "1" @@ -48,10 +45,8 @@ thiserror = { workspace = true } # Logging tracing = { workspace = true } -# Busbar capability system (optional) - requires private repo access -busbar-capability = { git = "https://github.com/composable-delivery/busbar", optional = true } - # Busbar capability system (optional) +busbar-capability = { git = "https://github.com/composable-delivery/busbar", optional = true } [dev-dependencies] tokio = { workspace = true, features = ["full"] } diff --git a/crates/sf-bridge/README.md b/crates/sf-bridge/README.md index 9f31e0c..20621ca 100644 --- a/crates/sf-bridge/README.md +++ b/crates/sf-bridge/README.md @@ -8,9 +8,7 @@ This crate provides `SfBridge`, which loads WASM guest plugins via Extism and ex ## Authentication Model -### Standard Authentication (Pre-Authenticated Client) - -The traditional approach requires you to handle authentication yourself: +### How Authentication Works 1. **Host-Side Authentication**: The host application (your Rust code) authenticates with Salesforce using `busbar-sf-auth` and obtains credentials (instance URL + access token). @@ -25,38 +23,6 @@ The traditional approach requires you to handle authentication yourself: let bridge = SfBridge::new(wasm_bytes, rest_client)?; ``` -### Busbar Authentication (Transparent Credential Resolution) - -When using the `busbar` feature, the bridge can resolve credentials automatically: - -```rust -use busbar_sf_bridge::{SfBridge, BusbarAuthConfig, JwtAuthConfig}; - -// Option 1: Environment variables (CI/CD) -// Set SF_ACCESS_TOKEN and SF_INSTANCE_URL in environment -let config = BusbarAuthConfig::new(); -let wasm_bytes = std::fs::read("plugin.wasm")?; -let bridge = SfBridge::new_with_busbar_auth(wasm_bytes, config).await?; - -// Option 2: JWT Bearer auth (server-to-server) -let jwt_config = JwtAuthConfig::with_key_file( - "consumer_key", - "username@example.com", - "path/to/private_key.pem" -)?; -let config = BusbarAuthConfig::new() - .with_jwt_auth(jwt_config) - .with_token_ttl_secs(3600); -let bridge = SfBridge::new_with_busbar_auth(wasm_bytes, config).await?; -``` - -**Credential Resolution Chain** (checked in order): -1. Environment variables: `SF_ACCESS_TOKEN`, `SF_INSTANCE_URL` -2. JWT Bearer authentication (if configured) -3. *(Future)* OS keychain via busbar-keychain - -**Token Caching**: Credentials are cached with configurable TTL and auto-refresh on expiry. - 4. **Credential Isolation**: The bridge stores the clients internally. When the WASM guest calls host functions (like `sf_query`), the bridge: - Receives the request from the guest (e.g., SOQL query) - Uses the **host's authenticated client** to make the Salesforce API call @@ -192,35 +158,11 @@ pub fn query_accounts(input: String) -> FnResult>> { - `bulk` - Bulk API endpoints (requires `rest`) - `tooling` - Tooling API endpoints (requires `rest`) - `metadata` - Metadata API endpoints (requires `rest`) -- `busbar` - Busbar authentication integration with transparent credential resolution -- `busbar-capability` - Implement Busbar's `HostCapability` trait for use with Busbar runtime - -### Busbar Authentication Integration - -When the `busbar` feature is enabled, `SfBridge` can resolve credentials automatically from multiple sources: - -```rust -use busbar_sf_bridge::{SfBridge, BusbarAuthConfig, JwtAuthConfig}; - -// Automatic credential resolution -let config = BusbarAuthConfig::new() - .with_jwt_auth(jwt_config) // Optional - .with_token_ttl_secs(3600) // Optional, default: 3600 - .with_login_url("https://login.salesforce.com"); // Optional, default: production - -let wasm_bytes = std::fs::read("plugin.wasm")?; -let bridge = SfBridge::new_with_busbar_auth(wasm_bytes, config).await?; -``` - -**Benefits:** -- No need to manage authentication flow in your code -- Works seamlessly in both local and CI/CD environments -- Automatic token caching and refresh -- WASM guests still never see credentials +- `busbar` - Implement Busbar's `HostCapability` trait for use with Busbar runtime ### Busbar Capability Integration -When the `busbar-capability` feature is enabled, `SfBridge` implements the `HostCapability` trait +When the `busbar` feature is enabled, `SfBridge` implements the `HostCapability` trait from `busbar-capability`, making it a drop-in capability provider for the Busbar runtime: ```rust diff --git a/crates/sf-bridge/src/busbar_auth.rs b/crates/sf-bridge/src/busbar_auth.rs deleted file mode 100644 index dfab399..0000000 --- a/crates/sf-bridge/src/busbar_auth.rs +++ /dev/null @@ -1,347 +0,0 @@ -//! Busbar authentication integration for WASM credential resolution. -//! -//! This module provides credential resolution for the `SfBridge` when used in -//! Busbar-integrated environments. Credentials are resolved transparently from: -//! 1. Environment variables (CI/CD path) -//! 2. JWT Bearer flow with auto-refresh -//! 3. (Future) OS keychain via busbar-keychain -//! -//! WASM guests never see tokens -- all credential resolution happens host-side. - -use std::sync::{Arc, Mutex}; -use std::time::{Duration, Instant}; - -use busbar_sf_auth::{JwtAuth, SalesforceCredentials}; -use busbar_sf_client::DEFAULT_API_VERSION; -use tracing::{debug, instrument}; - -use crate::error::{Error, Result}; - -/// Configuration for Busbar authentication integration. -/// -/// Defines how the bridge should resolve Salesforce credentials in Busbar -/// environments (local development, CI/CD, etc.). -#[derive(Debug, Clone)] -pub struct BusbarAuthConfig { - /// Optional JWT authentication configuration for server-to-server auth. - pub jwt_auth: Option, - /// Login URL for authentication (default: production). - pub login_url: String, - /// API version to use (default: from busbar_sf_client::DEFAULT_API_VERSION). - pub api_version: String, - /// Token cache TTL in seconds (default: 3600 = 1 hour). - pub token_ttl_secs: u64, -} - -impl Default for BusbarAuthConfig { - fn default() -> Self { - Self { - jwt_auth: None, - login_url: busbar_sf_auth::PRODUCTION_LOGIN_URL.to_string(), - api_version: DEFAULT_API_VERSION.to_string(), - token_ttl_secs: 3600, // 1 hour - } - } -} - -impl BusbarAuthConfig { - /// Create a new config with default values. - pub fn new() -> Self { - Self::default() - } - - /// Set JWT authentication configuration. - pub fn with_jwt_auth(mut self, jwt_auth: JwtAuthConfig) -> Self { - self.jwt_auth = Some(jwt_auth); - self - } - - /// Set the login URL (production or sandbox). - pub fn with_login_url(mut self, login_url: impl Into) -> Self { - self.login_url = login_url.into(); - self - } - - /// Set the API version. - pub fn with_api_version(mut self, api_version: impl Into) -> Self { - self.api_version = api_version.into(); - self - } - - /// Set the token TTL in seconds. - pub fn with_token_ttl_secs(mut self, ttl_secs: u64) -> Self { - self.token_ttl_secs = ttl_secs; - self - } -} - -/// JWT authentication configuration. -#[derive(Debug, Clone)] -pub struct JwtAuthConfig { - /// Consumer key (client_id) from the connected app. - pub consumer_key: String, - /// Username to authenticate as. - pub username: String, - /// Private key for JWT signing (PEM format). - pub private_key: Vec, -} - -impl JwtAuthConfig { - /// Create a new JWT auth configuration. - pub fn new( - consumer_key: impl Into, - username: impl Into, - private_key: impl Into>, - ) -> Self { - Self { - consumer_key: consumer_key.into(), - username: username.into(), - private_key: private_key.into(), - } - } - - /// Load private key from a file. - pub fn with_key_file( - consumer_key: impl Into, - username: impl Into, - key_path: impl AsRef, - ) -> Result { - let private_key = std::fs::read(key_path.as_ref()) - .map_err(|e| Error::Auth(format!("Failed to read private key: {}", e)))?; - Ok(Self::new(consumer_key, username, private_key)) - } -} - -/// Cached credentials with expiration tracking. -#[derive(Debug, Clone)] -struct CachedCredentials { - credentials: SalesforceCredentials, - expires_at: Instant, -} - -impl CachedCredentials { - fn new(credentials: SalesforceCredentials, ttl: Duration) -> Self { - Self { - credentials, - expires_at: Instant::now() + ttl, - } - } - - fn is_expired(&self) -> bool { - Instant::now() >= self.expires_at - } -} - -/// Credential resolver with caching and auto-refresh support. -/// -/// Resolves credentials in priority order: -/// 1. Environment variables (SF_ACCESS_TOKEN, SF_INSTANCE_URL) -/// 2. JWT Bearer auth (if configured) -/// 3. (Future) OS keychain via busbar-keychain -/// -/// Caches credentials with configurable TTL and auto-refreshes on expiry. -pub struct BusbarAuthResolver { - config: BusbarAuthConfig, - cache: Arc>>, -} - -impl BusbarAuthResolver { - /// Create a new resolver with the given configuration. - pub fn new(config: BusbarAuthConfig) -> Self { - Self { - config, - cache: Arc::new(Mutex::new(None)), - } - } - - /// Resolve credentials using the configured resolution chain. - /// - /// Checks cache first, then tries resolution methods in priority order. - /// Transparently handles token refresh when cached credentials expire. - #[instrument(skip(self))] - pub async fn resolve(&self) -> Result { - // Check cache first - { - let cache = self.cache.lock().unwrap(); - if let Some(ref cached) = *cache { - if !cached.is_expired() { - debug!("Using cached credentials"); - return Ok(cached.credentials.clone()); - } - debug!("Cached credentials expired, refreshing"); - } - } - - // Try resolution chain - let credentials = self.resolve_fresh().await?; - - // Cache the new credentials - let ttl = Duration::from_secs(self.config.token_ttl_secs); - let cached = CachedCredentials::new(credentials.clone(), ttl); - *self.cache.lock().unwrap() = Some(cached); - - Ok(credentials) - } - - /// Resolve credentials without checking cache (forces fresh resolution). - #[instrument(skip(self))] - async fn resolve_fresh(&self) -> Result { - // 1. Try environment variables first (CI/CD path) - if let Ok(creds) = self.try_from_env() { - debug!("Resolved credentials from environment variables"); - return Ok(creds); - } - - // 2. Try JWT bearer auth if configured - if let Some(ref jwt_config) = self.config.jwt_auth { - debug!("Attempting JWT bearer authentication"); - return self.try_jwt_auth(jwt_config).await; - } - - // 3. Future: Try keychain (busbar-keychain integration) - // if let Some(keychain_path) = &self.config.keychain_path { - // debug!("Attempting keychain resolution"); - // return self.try_keychain(keychain_path).await; - // } - - Err(Error::Auth( - "No credentials found. Set SF_ACCESS_TOKEN and SF_INSTANCE_URL environment variables, \ - or configure JWT authentication." - .to_string(), - )) - } - - /// Try to resolve credentials from environment variables. - fn try_from_env(&self) -> Result { - let instance_url = std::env::var("SF_INSTANCE_URL") - .or_else(|_| std::env::var("SALESFORCE_INSTANCE_URL")) - .map_err(|_| Error::Auth("SF_INSTANCE_URL not set".to_string()))?; - - let access_token = std::env::var("SF_ACCESS_TOKEN") - .or_else(|_| std::env::var("SALESFORCE_ACCESS_TOKEN")) - .map_err(|_| Error::Auth("SF_ACCESS_TOKEN not set".to_string()))?; - - let api_version = std::env::var("SF_API_VERSION") - .or_else(|_| std::env::var("SALESFORCE_API_VERSION")) - .unwrap_or_else(|_| self.config.api_version.clone()); - - Ok(SalesforceCredentials::new( - instance_url, - access_token, - api_version, - )) - } - - /// Try to authenticate using JWT bearer flow. - async fn try_jwt_auth(&self, jwt_config: &JwtAuthConfig) -> Result { - let jwt_auth = JwtAuth::new( - jwt_config.consumer_key.clone(), - jwt_config.username.clone(), - jwt_config.private_key.clone(), - ); - - jwt_auth - .authenticate(&self.config.login_url) - .await - .map_err(|e| Error::Auth(format!("JWT authentication failed: {}", e))) - } - - /// Clear the cached credentials, forcing a fresh resolution on next call. - pub fn clear_cache(&self) { - *self.cache.lock().unwrap() = None; - } -} - -#[cfg(test)] -mod tests { - use super::*; - use busbar_sf_auth::Credentials; - - #[tokio::test] - async fn test_resolve_from_env() { - // Set test environment variables - std::env::set_var("SF_INSTANCE_URL", "https://test.salesforce.com"); - std::env::set_var("SF_ACCESS_TOKEN", "test_token_12345"); - std::env::set_var("SF_API_VERSION", "62.0"); - - let config = BusbarAuthConfig::new(); - let resolver = BusbarAuthResolver::new(config); - - let creds = resolver.resolve().await.unwrap(); - assert_eq!(creds.instance_url(), "https://test.salesforce.com"); - assert_eq!(creds.access_token(), "test_token_12345"); - assert_eq!(creds.api_version(), "62.0"); - } - - #[tokio::test] - #[ignore] // Run with --ignored to test cache behavior in isolation - async fn test_cache_behavior() { - // Set unique test environment variables - std::env::set_var("SF_INSTANCE_URL", "https://cache-test.salesforce.com"); - std::env::set_var("SF_ACCESS_TOKEN", "cache_test_token_12345"); - - let config = BusbarAuthConfig::new().with_token_ttl_secs(2); - let resolver = BusbarAuthResolver::new(config); - - // First resolve - let creds1 = resolver.resolve().await.unwrap(); - assert_eq!(creds1.access_token(), "cache_test_token_12345"); - - // Change env var - std::env::set_var("SF_ACCESS_TOKEN", "cache_different_token"); - - // Second resolve should use cache - let creds2 = resolver.resolve().await.unwrap(); - assert_eq!(creds2.access_token(), "cache_test_token_12345"); // Still cached - - // Wait for cache expiry - tokio::time::sleep(tokio::time::Duration::from_secs(3)).await; - - // Third resolve should get new value - let creds3 = resolver.resolve().await.unwrap(); - assert_eq!(creds3.access_token(), "cache_different_token"); - } - - #[tokio::test] - #[ignore] // Run with --ignored to test cache clearing in isolation - async fn test_clear_cache() { - // Set test environment variables - std::env::set_var("SF_INSTANCE_URL", "https://clear-test.salesforce.com"); - std::env::set_var("SF_ACCESS_TOKEN", "clear_test_token_12345"); - - let config = BusbarAuthConfig::new(); - let resolver = BusbarAuthResolver::new(config); - - // First resolve - let creds1 = resolver.resolve().await.unwrap(); - assert_eq!(creds1.access_token(), "clear_test_token_12345"); - - // Change env var and clear cache - std::env::set_var("SF_ACCESS_TOKEN", "clear_different_token"); - resolver.clear_cache(); - - // Should get new value immediately - let creds2 = resolver.resolve().await.unwrap(); - assert_eq!(creds2.access_token(), "clear_different_token"); - } - - #[tokio::test] - #[ignore] // Run with --ignored to avoid interfering with other tests - async fn test_missing_env_vars() { - // Ensure env vars are not set - std::env::remove_var("SF_INSTANCE_URL"); - std::env::remove_var("SF_ACCESS_TOKEN"); - std::env::remove_var("SALESFORCE_INSTANCE_URL"); - std::env::remove_var("SALESFORCE_ACCESS_TOKEN"); - - let config = BusbarAuthConfig::new(); - let resolver = BusbarAuthResolver::new(config); - - let result = resolver.resolve().await; - assert!(result.is_err()); - assert!(result - .unwrap_err() - .to_string() - .contains("No credentials found")); - } -} diff --git a/crates/sf-bridge/src/error.rs b/crates/sf-bridge/src/error.rs index 3dec7ac..77b9e78 100644 --- a/crates/sf-bridge/src/error.rs +++ b/crates/sf-bridge/src/error.rs @@ -43,15 +43,6 @@ pub enum Error { /// Configuration error. #[error("configuration error: {0}")] Config(String), - - /// Authentication error (Busbar auth integration). - #[cfg(feature = "busbar")] - #[error("authentication error: {0}")] - Auth(String), - - /// I/O error (for reading key files, etc.). - #[error("I/O error: {0}")] - Io(#[from] std::io::Error), } pub type Result = std::result::Result; diff --git a/crates/sf-bridge/src/lib.rs b/crates/sf-bridge/src/lib.rs index 7a7ae58..bd48dbf 100644 --- a/crates/sf-bridge/src/lib.rs +++ b/crates/sf-bridge/src/lib.rs @@ -51,7 +51,7 @@ //! Each invocation creates a fresh WASM plugin instance from a pre-compiled //! module. The underlying clients share connection pools. //! -//! ## Example (Standard Authentication) +//! ## Example //! //! ```rust,ignore //! use busbar_sf_bridge::SfBridge; @@ -74,49 +74,13 @@ //! Ok(()) //! } //! ``` -//! -//! ## Example (Busbar Authentication) -//! -//! ```rust,ignore -//! use busbar_sf_bridge::{SfBridge, BusbarAuthConfig, JwtAuthConfig}; -//! -//! #[tokio::main] -//! async fn main() -> Result<(), Box> { -//! // Option 1: Use environment variables (SF_ACCESS_TOKEN, SF_INSTANCE_URL) -//! let config = BusbarAuthConfig::new(); -//! -//! // Option 2: Use JWT Bearer authentication -//! let jwt_config = JwtAuthConfig::with_key_file( -//! "consumer_key", -//! "username@example.com", -//! "private_key.pem" -//! )?; -//! let config = BusbarAuthConfig::new() -//! .with_jwt_auth(jwt_config) -//! .with_token_ttl_secs(3600); -//! -//! let wasm_bytes = std::fs::read("my_plugin.wasm")?; -//! let bridge = SfBridge::new_with_busbar_auth(wasm_bytes, config).await?; -//! -//! let result = bridge.call("run", b"input data").await?; -//! println!("Guest returned: {}", String::from_utf8_lossy(&result)); -//! -//! Ok(()) -//! } -//! ``` mod error; mod host_functions; mod registration; -#[cfg(feature = "busbar-capability")] -mod capability; - #[cfg(feature = "busbar")] -mod busbar_auth; - -#[cfg(feature = "busbar")] -pub use busbar_auth::{BusbarAuthConfig, BusbarAuthResolver, JwtAuthConfig}; +mod capability; pub use error::{Error, Result}; @@ -226,77 +190,6 @@ impl SfBridge { }) } - /// Create a new bridge with Busbar authentication integration. - /// - /// This constructor resolves credentials transparently from: - /// 1. Environment variables (SF_ACCESS_TOKEN, SF_INSTANCE_URL) - /// 2. JWT Bearer auth (if configured) - /// 3. (Future) OS keychain via busbar-keychain - /// - /// Credentials are cached with configurable TTL and auto-refresh. - /// WASM guests never see tokens. - /// - /// Must be called from within a tokio runtime context. - /// - /// # Example - /// - /// ```rust,ignore - /// use busbar_sf_bridge::{SfBridge, BusbarAuthConfig}; - /// - /// #[tokio::main] - /// async fn main() -> Result<(), Box> { - /// // Configure with JWT auth - /// let jwt_config = JwtAuthConfig::with_key_file( - /// "consumer_key", - /// "username@example.com", - /// "private_key.pem", - /// )?; - /// - /// let config = BusbarAuthConfig::new() - /// .with_jwt_auth(jwt_config) - /// .with_token_ttl_secs(3600); - /// - /// let wasm_bytes = std::fs::read("my_plugin.wasm")?; - /// let bridge = SfBridge::new_with_busbar_auth(wasm_bytes, config).await?; - /// - /// let result = bridge.call("run", b"input data").await?; - /// Ok(()) - /// } - /// ``` - #[cfg(all(feature = "busbar", feature = "rest"))] - pub async fn new_with_busbar_auth( - wasm_bytes: Vec, - config: busbar_auth::BusbarAuthConfig, - ) -> Result { - let handle = tokio::runtime::Handle::current(); - Self::with_busbar_auth_and_handle(wasm_bytes, config, handle).await - } - - /// Create a new bridge with Busbar authentication integration and a specific - /// tokio runtime handle. - /// - /// Use this when constructing the bridge outside of a tokio context. - #[cfg(all(feature = "busbar", feature = "rest"))] - pub async fn with_busbar_auth_and_handle( - wasm_bytes: Vec, - config: busbar_auth::BusbarAuthConfig, - handle: tokio::runtime::Handle, - ) -> Result { - use busbar_auth::BusbarAuthResolver; - use busbar_sf_auth::Credentials; - - // Resolve credentials using the Busbar auth chain - let resolver = BusbarAuthResolver::new(config); - let credentials = resolver.resolve().await?; - - // Create REST client with resolved credentials - let rest_client = - SalesforceRestClient::new(credentials.instance_url(), credentials.access_token())?; - - // Use the existing with_handle constructor - Self::with_handle(wasm_bytes, rest_client, handle) - } - /// Call an exported function in the WASM guest. /// /// Each call creates a fresh plugin instance (cheap -- the module is diff --git a/tests/integration/bridge.rs b/tests/integration/bridge.rs index cb29071..a8ee66b 100644 --- a/tests/integration/bridge.rs +++ b/tests/integration/bridge.rs @@ -963,72 +963,3 @@ async fn test_bridge_list_named_credentials() { "Should have credentials_count" ); } - -// ============================================================================= -// Busbar Authentication Integration Tests -// ============================================================================= - -#[tokio::test] -#[cfg(feature = "busbar")] -#[ignore] // Run with --ignored when environment is properly configured -async fn test_bridge_with_busbar_auth_from_env() { - use busbar_sf_bridge::BusbarAuthConfig; - - // This test requires SF_ACCESS_TOKEN and SF_INSTANCE_URL environment variables - // to be set. In CI/CD, these are typically set by the ephemeral session. - let config = BusbarAuthConfig::new(); - let wasm_bytes = load_test_wasm_bytes().expect("WASM plugin not found"); - - let bridge = busbar_sf_bridge::SfBridge::new_with_busbar_auth(wasm_bytes, config) - .await - .expect("Failed to create bridge with Busbar auth"); - - // Verify the bridge can execute a simple query - let input = serde_json::json!({ - "soql": "SELECT Id FROM Account LIMIT 1" - }); - - let result = call_test_function(&bridge, "test_query", &input).await; - - assert!( - result["success"].as_bool().unwrap_or(false), - "Query with Busbar auth should succeed" - ); -} - -#[tokio::test] -#[cfg(feature = "busbar")] -#[ignore] // Run with --ignored when JWT credentials are available -async fn test_bridge_with_busbar_auth_jwt() { - use busbar_sf_bridge::{BusbarAuthConfig, JwtAuthConfig}; - - // This test requires JWT credentials to be configured. - // In a real environment, these would come from secure storage. - let jwt_config = JwtAuthConfig::with_key_file( - std::env::var("SF_CONSUMER_KEY").expect("SF_CONSUMER_KEY not set"), - std::env::var("SF_USERNAME").expect("SF_USERNAME not set"), - std::env::var("SF_PRIVATE_KEY_PATH").expect("SF_PRIVATE_KEY_PATH not set"), - ).expect("Failed to create JWT config"); - - let config = BusbarAuthConfig::new() - .with_jwt_auth(jwt_config) - .with_token_ttl_secs(3600); - - let wasm_bytes = load_test_wasm_bytes().expect("WASM plugin not found"); - - let bridge = busbar_sf_bridge::SfBridge::new_with_busbar_auth(wasm_bytes, config) - .await - .expect("Failed to create bridge with JWT auth"); - - // Verify the bridge can execute a simple query - let input = serde_json::json!({ - "soql": "SELECT Id FROM Account LIMIT 1" - }); - - let result = call_test_function(&bridge, "test_query", &input).await; - - assert!( - result["success"].as_bool().unwrap_or(false), - "Query with JWT auth should succeed" - ); -} From 05700641f6039e70c6eebd2f87012024f88d1840 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 4 Feb 2026 16:56:28 +0000 Subject: [PATCH 07/15] Add proper Busbar keychain integration for credential resolution - Add busbar-keychain dependency from composable-delivery/busbar - Create keychain_auth module integrating with busbar-keychain::SecretStore - Add KeychainAuthConfig for configuration - Add JwtAuthConfig for JWT bearer auth - Implement KeychainAuthResolver with priority chain: 1. Environment variables (SF_ACCESS_TOKEN, SF_INSTANCE_URL) 2. Busbar keychain 3. JWT bearer auth - Add SfBridge::new_with_keychain_auth() constructor - Feature is optional and off by default (busbar-keychain feature) Co-authored-by: jlantz <1697127+jlantz@users.noreply.github.com> --- crates/sf-bridge/Cargo.toml | 6 + crates/sf-bridge/src/keychain_auth.rs | 264 ++++++++++++++++++++++++++ crates/sf-bridge/src/lib.rs | 73 +++++++ 3 files changed, 343 insertions(+) create mode 100644 crates/sf-bridge/src/keychain_auth.rs diff --git a/crates/sf-bridge/Cargo.toml b/crates/sf-bridge/Cargo.toml index 15b2a92..2682a74 100644 --- a/crates/sf-bridge/Cargo.toml +++ b/crates/sf-bridge/Cargo.toml @@ -15,6 +15,8 @@ bulk = ["rest", "dep:busbar-sf-bulk"] tooling = ["rest", "dep:busbar-sf-tooling"] metadata = ["rest", "dep:busbar-sf-metadata"] busbar = ["dep:busbar-capability"] +# Busbar keychain integration (optional, off by default) +busbar-keychain = ["rest", "dep:busbar-keychain", "dep:busbar-sf-auth"] [dependencies] # Internal crates @@ -24,6 +26,10 @@ busbar-sf-client = { workspace = true, optional = true } busbar-sf-bulk = { workspace = true, optional = true } busbar-sf-tooling = { workspace = true, optional = true } busbar-sf-metadata = { workspace = true, optional = true } +busbar-sf-auth = { workspace = true, optional = true } + +# Busbar keychain (optional, from composable-delivery/busbar) +busbar-keychain = { git = "https://github.com/composable-delivery/busbar", optional = true } # Extism host SDK extism = "1" diff --git a/crates/sf-bridge/src/keychain_auth.rs b/crates/sf-bridge/src/keychain_auth.rs new file mode 100644 index 0000000..3901a59 --- /dev/null +++ b/crates/sf-bridge/src/keychain_auth.rs @@ -0,0 +1,264 @@ +//! Busbar keychain integration for credential resolution. +//! +//! This module integrates with the Busbar keychain system from the +//! composable-delivery/busbar repository to resolve Salesforce credentials +//! transparently without requiring pre-authenticated clients. +//! +//! Credentials are resolved from: +//! 1. Environment variables (SF_ACCESS_TOKEN, SF_INSTANCE_URL) +//! 2. Busbar keychain (via busbar-keychain::SecretStore) +//! 3. JWT bearer auth (if configured) +//! +//! WASM guests never see tokens - all credential resolution happens host-side. + +use busbar_keychain::SecretStore; +use busbar_sf_auth::{Credentials, JwtAuth, SalesforceCredentials}; +use busbar_sf_client::DEFAULT_API_VERSION; +use std::sync::Arc; +use tracing::{debug, instrument}; + +use crate::error::{Error, Result}; + +/// Configuration for Busbar keychain-based credential resolution. +/// +/// This configuration allows the bridge to resolve Salesforce credentials +/// from the Busbar keychain system without requiring pre-authenticated clients. +#[derive(Debug, Clone)] +pub struct KeychainAuthConfig { + /// Path prefix in the keychain for Salesforce credentials. + /// Example: "sf/production" will look for "sf_production_access_token" + pub keychain_prefix: Option, + + /// Optional JWT authentication for server-to-server flows. + pub jwt_auth: Option, + + /// Salesforce login URL (default: production). + pub login_url: String, + + /// API version to use (default: from busbar_sf_client::DEFAULT_API_VERSION). + pub api_version: String, +} + +impl Default for KeychainAuthConfig { + fn default() -> Self { + Self { + keychain_prefix: None, + jwt_auth: None, + login_url: busbar_sf_auth::PRODUCTION_LOGIN_URL.to_string(), + api_version: DEFAULT_API_VERSION.to_string(), + } + } +} + +impl KeychainAuthConfig { + /// Create a new configuration with default values. + pub fn new() -> Self { + Self::default() + } + + /// Set the keychain prefix for credential lookups. + /// + /// Example: `with_keychain_prefix("sf/production")` will look for + /// credentials at "sf_production_access_token" and "sf_production_instance_url". + pub fn with_keychain_prefix(mut self, prefix: impl Into) -> Self { + self.keychain_prefix = Some(prefix.into()); + self + } + + /// Set JWT authentication configuration. + pub fn with_jwt_auth(mut self, jwt_auth: JwtAuthConfig) -> Self { + self.jwt_auth = Some(jwt_auth); + self + } + + /// Set the Salesforce login URL. + pub fn with_login_url(mut self, login_url: impl Into) -> Self { + self.login_url = login_url.into(); + self + } + + /// Set the API version. + pub fn with_api_version(mut self, api_version: impl Into) -> Self { + self.api_version = api_version.into(); + self + } +} + +/// JWT authentication configuration. +#[derive(Debug, Clone)] +pub struct JwtAuthConfig { + /// Consumer key (client_id) from the connected app. + pub consumer_key: String, + /// Username to authenticate as. + pub username: String, + /// Private key for JWT signing (PEM format). + pub private_key: Vec, +} + +impl JwtAuthConfig { + /// Create a new JWT auth configuration. + pub fn new( + consumer_key: impl Into, + username: impl Into, + private_key: impl Into>, + ) -> Self { + Self { + consumer_key: consumer_key.into(), + username: username.into(), + private_key: private_key.into(), + } + } + + /// Load private key from a file. + pub fn with_key_file( + consumer_key: impl Into, + username: impl Into, + key_path: impl AsRef, + ) -> Result { + let private_key = std::fs::read(key_path.as_ref()) + .map_err(|e| Error::Config(format!("Failed to read private key: {}", e)))?; + Ok(Self::new(consumer_key, username, private_key)) + } +} + +/// Credential resolver that integrates with Busbar keychain. +/// +/// Resolves credentials in priority order: +/// 1. Environment variables (SF_ACCESS_TOKEN, SF_INSTANCE_URL) +/// 2. Busbar keychain (via SecretStore) +/// 3. JWT bearer auth (if configured) +pub struct KeychainAuthResolver { + config: KeychainAuthConfig, + store: Arc, +} + +impl KeychainAuthResolver { + /// Create a new resolver with the given configuration. + pub async fn new(config: KeychainAuthConfig) -> Result { + let store = SecretStore::new() + .await + .map_err(|e| Error::Config(format!("Failed to initialize secret store: {}", e)))?; + + Ok(Self { + config, + store: Arc::new(store), + }) + } + + /// Create a resolver with an existing SecretStore. + pub fn with_store(config: KeychainAuthConfig, store: Arc) -> Self { + Self { config, store } + } + + /// Resolve Salesforce credentials using the configured resolution chain. + /// + /// This method tries: + /// 1. Environment variables (SF_ACCESS_TOKEN, SF_INSTANCE_URL) + /// 2. Busbar keychain + /// 3. JWT bearer auth (if configured) + #[instrument(skip(self))] + pub async fn resolve(&self) -> Result { + // 1. Try environment variables first (CI/CD path) + if let Ok(creds) = self.try_from_env() { + debug!("Resolved credentials from environment variables"); + return Ok(creds); + } + + // 2. Try Busbar keychain + if let Ok(creds) = self.try_from_keychain().await { + debug!("Resolved credentials from Busbar keychain"); + return Ok(creds); + } + + // 3. Try JWT bearer auth if configured + if let Some(ref jwt_config) = self.config.jwt_auth { + debug!("Attempting JWT bearer authentication"); + return self.try_jwt_auth(jwt_config).await; + } + + Err(Error::Config( + "No credentials found. Set SF_ACCESS_TOKEN and SF_INSTANCE_URL environment variables, \ + configure credentials in Busbar keychain, or configure JWT authentication." + .to_string(), + )) + } + + /// Try to resolve credentials from environment variables. + fn try_from_env(&self) -> Result { + let instance_url = std::env::var("SF_INSTANCE_URL") + .or_else(|_| std::env::var("SALESFORCE_INSTANCE_URL")) + .map_err(|_| Error::Config("SF_INSTANCE_URL not set".to_string()))?; + + let access_token = std::env::var("SF_ACCESS_TOKEN") + .or_else(|_| std::env::var("SALESFORCE_ACCESS_TOKEN")) + .map_err(|_| Error::Config("SF_ACCESS_TOKEN not set".to_string()))?; + + let api_version = std::env::var("SF_API_VERSION") + .or_else(|_| std::env::var("SALESFORCE_API_VERSION")) + .unwrap_or_else(|_| self.config.api_version.clone()); + + Ok(SalesforceCredentials::new( + instance_url, + access_token, + api_version, + )) + } + + /// Try to resolve credentials from the Busbar keychain. + async fn try_from_keychain(&self) -> Result { + let prefix = self.config.keychain_prefix.as_deref().unwrap_or("sf"); + + let access_token_key = format!("{}/access_token", prefix); + let instance_url_key = format!("{}/instance_url", prefix); + + let access_token = self.store.get(&access_token_key).await.map_err(|e| { + Error::Config(format!("Failed to get access token from keychain: {}", e)) + })?; + + let instance_url = self.store.get(&instance_url_key).await.map_err(|e| { + Error::Config(format!("Failed to get instance URL from keychain: {}", e)) + })?; + + Ok(SalesforceCredentials::new( + instance_url, + access_token, + self.config.api_version.clone(), + )) + } + + /// Try to authenticate using JWT bearer flow. + async fn try_jwt_auth(&self, jwt_config: &JwtAuthConfig) -> Result { + let jwt_auth = JwtAuth::new( + jwt_config.consumer_key.clone(), + jwt_config.username.clone(), + jwt_config.private_key.clone(), + ); + + jwt_auth + .authenticate(&self.config.login_url) + .await + .map_err(|e| Error::Config(format!("JWT authentication failed: {}", e))) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[tokio::test] + #[ignore] // Requires environment setup + async fn test_resolve_from_env() { + std::env::set_var("SF_INSTANCE_URL", "https://test.salesforce.com"); + std::env::set_var("SF_ACCESS_TOKEN", "test_token_12345"); + + let config = KeychainAuthConfig::new(); + let resolver = KeychainAuthResolver::new(config).await.unwrap(); + + let creds = resolver.resolve().await.unwrap(); + assert_eq!(creds.instance_url(), "https://test.salesforce.com"); + assert_eq!(creds.access_token(), "test_token_12345"); + + std::env::remove_var("SF_INSTANCE_URL"); + std::env::remove_var("SF_ACCESS_TOKEN"); + } +} diff --git a/crates/sf-bridge/src/lib.rs b/crates/sf-bridge/src/lib.rs index bd48dbf..c92eb11 100644 --- a/crates/sf-bridge/src/lib.rs +++ b/crates/sf-bridge/src/lib.rs @@ -82,6 +82,12 @@ mod registration; #[cfg(feature = "busbar")] mod capability; +#[cfg(feature = "busbar-keychain")] +pub mod keychain_auth; + +#[cfg(feature = "busbar-keychain")] +pub use keychain_auth::{JwtAuthConfig, KeychainAuthConfig, KeychainAuthResolver}; + pub use error::{Error, Result}; use std::sync::Arc; @@ -190,6 +196,73 @@ impl SfBridge { }) } + /// Create a new bridge with Busbar keychain-based credential resolution. + /// + /// This constructor integrates with the Busbar keychain system to resolve + /// Salesforce credentials transparently. No pre-authenticated client is needed. + /// + /// Credentials are resolved in priority order: + /// 1. Environment variables (SF_ACCESS_TOKEN, SF_INSTANCE_URL) + /// 2. Busbar keychain (via busbar-keychain::SecretStore) + /// 3. JWT bearer auth (if configured in the config) + /// + /// WASM guests never see tokens - all credential resolution happens host-side. + /// + /// Must be called from within a tokio runtime context. + /// + /// # Example + /// + /// ```rust,ignore + /// use busbar_sf_bridge::{SfBridge, KeychainAuthConfig}; + /// + /// #[tokio::main] + /// async fn main() -> Result<(), Box> { + /// // Resolve from environment variables or keychain + /// let config = KeychainAuthConfig::new() + /// .with_keychain_prefix("sf/production"); + /// + /// let wasm_bytes = std::fs::read("my_plugin.wasm")?; + /// let bridge = SfBridge::new_with_keychain_auth(wasm_bytes, config).await?; + /// + /// let result = bridge.call("run", b"input data").await?; + /// Ok(()) + /// } + /// ``` + #[cfg(all(feature = "busbar-keychain", feature = "rest"))] + pub async fn new_with_keychain_auth( + wasm_bytes: Vec, + config: keychain_auth::KeychainAuthConfig, + ) -> Result { + let handle = tokio::runtime::Handle::current(); + Self::with_keychain_auth_and_handle(wasm_bytes, config, handle).await + } + + /// Create a new bridge with Busbar keychain auth and a specific tokio runtime handle. + /// + /// Use this when constructing the bridge outside of a tokio context. + #[cfg(all(feature = "busbar-keychain", feature = "rest"))] + pub async fn with_keychain_auth_and_handle( + wasm_bytes: Vec, + config: keychain_auth::KeychainAuthConfig, + handle: tokio::runtime::Handle, + ) -> Result { + use keychain_auth::KeychainAuthResolver; + + // Resolve credentials using the Busbar keychain + let resolver = KeychainAuthResolver::new(config).await?; + let credentials = resolver.resolve().await?; + + // Import Credentials trait for access to methods + use busbar_sf_auth::Credentials; + + // Create REST client with resolved credentials + let rest_client = + SalesforceRestClient::new(credentials.instance_url(), credentials.access_token())?; + + // Use the existing with_handle constructor + Self::with_handle(wasm_bytes, rest_client, handle) + } + /// Call an exported function in the WASM guest. /// /// Each call creates a fresh plugin instance (cheap -- the module is From d0d3fa2c6390b9f697c6ebd83ae7dd7a09ad5949 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 4 Feb 2026 16:57:25 +0000 Subject: [PATCH 08/15] Add documentation for Busbar keychain integration - Update README.md with busbar-keychain feature documentation - Add examples for KeychainAuthConfig usage - Document credential resolution chain - Update lib.rs with keychain auth example - Explain optional nature of feature (off by default) Co-authored-by: jlantz <1697127+jlantz@users.noreply.github.com> --- crates/sf-bridge/README.md | 27 +++++++++++++++++++++++++++ crates/sf-bridge/src/lib.rs | 25 ++++++++++++++++++++++++- 2 files changed, 51 insertions(+), 1 deletion(-) diff --git a/crates/sf-bridge/README.md b/crates/sf-bridge/README.md index 20621ca..a89c7b8 100644 --- a/crates/sf-bridge/README.md +++ b/crates/sf-bridge/README.md @@ -159,6 +159,33 @@ pub fn query_accounts(input: String) -> FnResult>> { - `tooling` - Tooling API endpoints (requires `rest`) - `metadata` - Metadata API endpoints (requires `rest`) - `busbar` - Implement Busbar's `HostCapability` trait for use with Busbar runtime +- `busbar-keychain` - Busbar keychain integration for transparent credential resolution (optional, off by default) + +### Busbar Keychain Integration + +When the `busbar-keychain` feature is enabled, `SfBridge` can resolve credentials automatically from the Busbar keychain system: + +```rust +use busbar_sf_bridge::{SfBridge, KeychainAuthConfig}; + +// Automatic credential resolution from Busbar keychain +let config = KeychainAuthConfig::new() + .with_keychain_prefix("sf/production"); // Looks for "sf/production/access_token" in keychain + +let wasm_bytes = std::fs::read("plugin.wasm")?; +let bridge = SfBridge::new_with_keychain_auth(wasm_bytes, config).await?; +``` + +**Benefits:** +- No need to manage authentication flow in your code +- Secure credential storage via Busbar keychain (fnox backend) +- Works seamlessly in both local and CI/CD environments +- WASM guests still never see credentials + +**Credential Resolution Chain:** +1. Environment variables (`SF_ACCESS_TOKEN`, `SF_INSTANCE_URL`) - CI/CD path +2. Busbar keychain via `busbar-keychain::SecretStore` - Local development +3. JWT bearer authentication (if configured) - Server-to-server ### Busbar Capability Integration diff --git a/crates/sf-bridge/src/lib.rs b/crates/sf-bridge/src/lib.rs index c92eb11..7d8947a 100644 --- a/crates/sf-bridge/src/lib.rs +++ b/crates/sf-bridge/src/lib.rs @@ -51,7 +51,7 @@ //! Each invocation creates a fresh WASM plugin instance from a pre-compiled //! module. The underlying clients share connection pools. //! -//! ## Example +//! ## Example (Standard Authentication) //! //! ```rust,ignore //! use busbar_sf_bridge::SfBridge; @@ -74,6 +74,29 @@ //! Ok(()) //! } //! ``` +//! +//! ## Example (Busbar Keychain Authentication) +//! +//! When the `busbar-keychain` feature is enabled: +//! +//! ```rust,ignore +//! use busbar_sf_bridge::{SfBridge, KeychainAuthConfig}; +//! +//! #[tokio::main] +//! async fn main() -> Result<(), Box> { +//! // Resolve credentials from Busbar keychain or environment variables +//! let config = KeychainAuthConfig::new() +//! .with_keychain_prefix("sf/production"); +//! +//! let wasm_bytes = std::fs::read("my_plugin.wasm")?; +//! let bridge = SfBridge::new_with_keychain_auth(wasm_bytes, config).await?; +//! +//! let result = bridge.call("run", b"input data").await?; +//! println!("Guest returned: {}", String::from_utf8_lossy(&result)); +//! +//! Ok(()) +//! } +//! ``` mod error; mod host_functions; From 3c4b76a7dd24b6f93b61c53930554c0994099270 Mon Sep 17 00:00:00 2001 From: Jason Lantz Date: Thu, 5 Feb 2026 02:35:52 +0000 Subject: [PATCH 09/15] Update Dockerfile and CI configuration for improved integration and add .sf and .sfdx to .gitignore --- .devcontainer/Dockerfile | 2 +- .github/workflows/ci.yml | 9 +++++++++ .gitignore | 3 +++ 3 files changed, 13 insertions(+), 1 deletion(-) diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile index 586c95c..197c461 100644 --- a/.devcontainer/Dockerfile +++ b/.devcontainer/Dockerfile @@ -3,7 +3,7 @@ # - rustup/cargo preinstalled # - a non-root user # - sensible defaults for VS Code + rust-analyzer -FROM mcr.microsoft.com/devcontainers/rust:1-1.88-bookworm +FROM mcr.microsoft.com/devcontainers/rust:1-bookworm # Dev container images should not try to build the project at image-build time. # Codespaces will mount the repository into /workspaces/. diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 68ef239..05d257c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -117,12 +117,21 @@ jobs: msrv: name: MSRV runs-on: ubuntu-latest + environment: GitHub composable-delivery steps: - uses: actions/checkout@v6 - uses: dtolnay/rust-toolchain@master with: toolchain: "1.88" - uses: Swatinem/rust-cache@v2 + + - name: Configure git credentials for private repos + env: + GH_TOKEN_BUILDS: ${{ secrets.GH_TOKEN_BUILDS }} + run: | + git config --global credential.helper store + echo "https://${GH_TOKEN_BUILDS}:x-oauth-basic@github.com" > ~/.git-credentials + - run: cargo check --workspace # ── Integration tests (real Salesforce org, excluding WASM bridge) ── diff --git a/.gitignore b/.gitignore index 0942df5..0e775fb 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,9 @@ /target/ Cargo.lock +.sf/ +.sfdx/ + # Excluded crates also have target dirs /crates/sf-guest-sdk/target/ /tests/wasm-test-plugin/target/ From 7103553b2232ff13badcfd27b4ce254d655e0cbc Mon Sep 17 00:00:00 2001 From: Jason Lantz Date: Thu, 5 Feb 2026 03:00:10 +0000 Subject: [PATCH 10/15] Refactor CI and integration workflows to use updated GitHub token environment variable --- .github/workflows/ci.yml | 24 ++++++++++++------------ .github/workflows/integration-tests.yml | 4 ++-- .github/workflows/release.yml | 4 ++-- crates/sf-bridge/src/keychain_auth.rs | 2 +- 4 files changed, 17 insertions(+), 17 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 05d257c..16d1d4d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -40,10 +40,10 @@ jobs: - name: Configure git credentials for private repos env: - GH_TOKEN_BUILDS: ${{ secrets.GH_TOKEN_BUILDS }} + KANTEXT_BUILD_GITHUB_TOKENS: ${{ secrets.KANTEXT_BUILD_GITHUB_TOKENS }} run: | git config --global credential.helper store - echo "https://${GH_TOKEN_BUILDS}:x-oauth-basic@github.com" > ~/.git-credentials + echo "https://${KANTEXT_BUILD_GITHUB_TOKENS}:x-oauth-basic@github.com" > ~/.git-credentials - run: cargo clippy --workspace --all-targets --all-features -- -D warnings @@ -60,10 +60,10 @@ jobs: - name: Configure git credentials for private repos env: - GH_TOKEN_BUILDS: ${{ secrets.GH_TOKEN_BUILDS }} + KANTEXT_BUILD_GITHUB_TOKENS: ${{ secrets.KANTEXT_BUILD_GITHUB_TOKENS }} run: | git config --global credential.helper store - echo "https://${GH_TOKEN_BUILDS}:x-oauth-basic@github.com" > ~/.git-credentials + echo "https://${KANTEXT_BUILD_GITHUB_TOKENS}:x-oauth-basic@github.com" > ~/.git-credentials - name: Clean coverage artifacts run: cargo llvm-cov clean --workspace @@ -104,10 +104,10 @@ jobs: - name: Configure git credentials for private repos env: - GH_TOKEN_BUILDS: ${{ secrets.GH_TOKEN_BUILDS }} + KANTEXT_BUILD_GITHUB_TOKENS: ${{ secrets.KANTEXT_BUILD_GITHUB_TOKENS }} run: | git config --global credential.helper store - echo "https://${GH_TOKEN_BUILDS}:x-oauth-basic@github.com" > ~/.git-credentials + echo "https://${KANTEXT_BUILD_GITHUB_TOKENS}:x-oauth-basic@github.com" > ~/.git-credentials - run: cargo doc --workspace --no-deps --all-features env: @@ -127,10 +127,10 @@ jobs: - name: Configure git credentials for private repos env: - GH_TOKEN_BUILDS: ${{ secrets.GH_TOKEN_BUILDS }} + KANTEXT_BUILD_GITHUB_TOKENS: ${{ secrets.KANTEXT_BUILD_GITHUB_TOKENS }} run: | git config --global credential.helper store - echo "https://${GH_TOKEN_BUILDS}:x-oauth-basic@github.com" > ~/.git-credentials + echo "https://${KANTEXT_BUILD_GITHUB_TOKENS}:x-oauth-basic@github.com" > ~/.git-credentials - run: cargo check --workspace @@ -147,10 +147,10 @@ jobs: - name: Configure git credentials for private repos env: - GH_TOKEN_BUILDS: ${{ secrets.GH_TOKEN_BUILDS }} + KANTEXT_BUILD_GITHUB_TOKENS: ${{ secrets.KANTEXT_BUILD_GITHUB_TOKENS }} run: | git config --global credential.helper store - echo "https://${GH_TOKEN_BUILDS}:x-oauth-basic@github.com" > ~/.git-credentials + echo "https://${KANTEXT_BUILD_GITHUB_TOKENS}:x-oauth-basic@github.com" > ~/.git-credentials - name: Verify credentials env: @@ -209,10 +209,10 @@ jobs: - name: Configure git credentials for private repos env: - GH_TOKEN_BUILDS: ${{ secrets.GH_TOKEN_BUILDS }} + KANTEXT_BUILD_GITHUB_TOKENS: ${{ secrets.KANTEXT_BUILD_GITHUB_TOKENS }} run: | git config --global credential.helper store - echo "https://${GH_TOKEN_BUILDS}:x-oauth-basic@github.com" > ~/.git-credentials + echo "https://${KANTEXT_BUILD_GITHUB_TOKENS}:x-oauth-basic@github.com" > ~/.git-credentials - name: Verify credentials env: diff --git a/.github/workflows/integration-tests.yml b/.github/workflows/integration-tests.yml index 8970827..50220bc 100644 --- a/.github/workflows/integration-tests.yml +++ b/.github/workflows/integration-tests.yml @@ -23,10 +23,10 @@ jobs: - name: Configure git credentials for private repos env: - GH_TOKEN_BUILDS: ${{ secrets.GH_TOKEN_BUILDS }} + KANTEXT_BUILD_GITHUB_TOKENS: ${{ secrets.KANTEXT_BUILD_GITHUB_TOKENS }} run: | git config --global credential.helper store - echo "https://${GH_TOKEN_BUILDS}:x-oauth-basic@github.com" > ~/.git-credentials + echo "https://${KANTEXT_BUILD_GITHUB_TOKENS}:x-oauth-basic@github.com" > ~/.git-credentials - name: Verify credentials env: diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index c496863..f428297 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -83,10 +83,10 @@ jobs: - name: Configure git credentials for private repos env: - GH_TOKEN_BUILDS: ${{ secrets.GH_TOKEN_BUILDS }} + KANTEXT_BUILD_GITHUB_TOKENS: ${{ secrets.KANTEXT_BUILD_GITHUB_TOKENS }} run: | git config --global credential.helper store - echo "https://${GH_TOKEN_BUILDS}:x-oauth-basic@github.com" > ~/.git-credentials + echo "https://${KANTEXT_BUILD_GITHUB_TOKENS}:x-oauth-basic@github.com" > ~/.git-credentials - name: Cache cargo registry uses: actions/cache@v5 diff --git a/crates/sf-bridge/src/keychain_auth.rs b/crates/sf-bridge/src/keychain_auth.rs index 3901a59..a07e356 100644 --- a/crates/sf-bridge/src/keychain_auth.rs +++ b/crates/sf-bridge/src/keychain_auth.rs @@ -12,7 +12,7 @@ //! WASM guests never see tokens - all credential resolution happens host-side. use busbar_keychain::SecretStore; -use busbar_sf_auth::{Credentials, JwtAuth, SalesforceCredentials}; +use busbar_sf_auth::{JwtAuth, SalesforceCredentials}; use busbar_sf_client::DEFAULT_API_VERSION; use std::sync::Arc; use tracing::{debug, instrument}; From 53aa4de1ff1235c405b70e10c6f7de5b69dc1f99 Mon Sep 17 00:00:00 2001 From: Jason Lantz Date: Thu, 5 Feb 2026 03:03:16 +0000 Subject: [PATCH 11/15] Update GitHub Actions workflows to use KANTEXT_BUILD_GITHUB_TOKEN instead of KANTEXT_BUILD_GITHUB_TOKENS for private repo access --- .github/workflows/ci.yml | 24 ++++++++++++------------ .github/workflows/integration-tests.yml | 4 ++-- .github/workflows/release.yml | 4 ++-- 3 files changed, 16 insertions(+), 16 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 16d1d4d..1aff707 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -40,10 +40,10 @@ jobs: - name: Configure git credentials for private repos env: - KANTEXT_BUILD_GITHUB_TOKENS: ${{ secrets.KANTEXT_BUILD_GITHUB_TOKENS }} + KANTEXT_BUILD_GITHUB_TOKEN: ${{ secrets.KANTEXT_BUILD_GITHUB_TOKEN }} run: | git config --global credential.helper store - echo "https://${KANTEXT_BUILD_GITHUB_TOKENS}:x-oauth-basic@github.com" > ~/.git-credentials + echo "https://${KANTEXT_BUILD_GITHUB_TOKEN}:x-oauth-basic@github.com" > ~/.git-credentials - run: cargo clippy --workspace --all-targets --all-features -- -D warnings @@ -60,10 +60,10 @@ jobs: - name: Configure git credentials for private repos env: - KANTEXT_BUILD_GITHUB_TOKENS: ${{ secrets.KANTEXT_BUILD_GITHUB_TOKENS }} + KANTEXT_BUILD_GITHUB_TOKEN: ${{ secrets.KANTEXT_BUILD_GITHUB_TOKEN }} run: | git config --global credential.helper store - echo "https://${KANTEXT_BUILD_GITHUB_TOKENS}:x-oauth-basic@github.com" > ~/.git-credentials + echo "https://${KANTEXT_BUILD_GITHUB_TOKEN}:x-oauth-basic@github.com" > ~/.git-credentials - name: Clean coverage artifacts run: cargo llvm-cov clean --workspace @@ -104,10 +104,10 @@ jobs: - name: Configure git credentials for private repos env: - KANTEXT_BUILD_GITHUB_TOKENS: ${{ secrets.KANTEXT_BUILD_GITHUB_TOKENS }} + KANTEXT_BUILD_GITHUB_TOKEN: ${{ secrets.KANTEXT_BUILD_GITHUB_TOKEN }} run: | git config --global credential.helper store - echo "https://${KANTEXT_BUILD_GITHUB_TOKENS}:x-oauth-basic@github.com" > ~/.git-credentials + echo "https://${KANTEXT_BUILD_GITHUB_TOKEN}:x-oauth-basic@github.com" > ~/.git-credentials - run: cargo doc --workspace --no-deps --all-features env: @@ -127,10 +127,10 @@ jobs: - name: Configure git credentials for private repos env: - KANTEXT_BUILD_GITHUB_TOKENS: ${{ secrets.KANTEXT_BUILD_GITHUB_TOKENS }} + KANTEXT_BUILD_GITHUB_TOKEN: ${{ secrets.KANTEXT_BUILD_GITHUB_TOKEN }} run: | git config --global credential.helper store - echo "https://${KANTEXT_BUILD_GITHUB_TOKENS}:x-oauth-basic@github.com" > ~/.git-credentials + echo "https://${KANTEXT_BUILD_GITHUB_TOKEN}:x-oauth-basic@github.com" > ~/.git-credentials - run: cargo check --workspace @@ -147,10 +147,10 @@ jobs: - name: Configure git credentials for private repos env: - KANTEXT_BUILD_GITHUB_TOKENS: ${{ secrets.KANTEXT_BUILD_GITHUB_TOKENS }} + KANTEXT_BUILD_GITHUB_TOKEN: ${{ secrets.KANTEXT_BUILD_GITHUB_TOKEN }} run: | git config --global credential.helper store - echo "https://${KANTEXT_BUILD_GITHUB_TOKENS}:x-oauth-basic@github.com" > ~/.git-credentials + echo "https://${KANTEXT_BUILD_GITHUB_TOKEN}:x-oauth-basic@github.com" > ~/.git-credentials - name: Verify credentials env: @@ -209,10 +209,10 @@ jobs: - name: Configure git credentials for private repos env: - KANTEXT_BUILD_GITHUB_TOKENS: ${{ secrets.KANTEXT_BUILD_GITHUB_TOKENS }} + KANTEXT_BUILD_GITHUB_TOKEN: ${{ secrets.KANTEXT_BUILD_GITHUB_TOKEN }} run: | git config --global credential.helper store - echo "https://${KANTEXT_BUILD_GITHUB_TOKENS}:x-oauth-basic@github.com" > ~/.git-credentials + echo "https://${KANTEXT_BUILD_GITHUB_TOKEN}:x-oauth-basic@github.com" > ~/.git-credentials - name: Verify credentials env: diff --git a/.github/workflows/integration-tests.yml b/.github/workflows/integration-tests.yml index 50220bc..e249eb6 100644 --- a/.github/workflows/integration-tests.yml +++ b/.github/workflows/integration-tests.yml @@ -23,10 +23,10 @@ jobs: - name: Configure git credentials for private repos env: - KANTEXT_BUILD_GITHUB_TOKENS: ${{ secrets.KANTEXT_BUILD_GITHUB_TOKENS }} + KANTEXT_BUILD_GITHUB_TOKEN: ${{ secrets.KANTEXT_BUILD_GITHUB_TOKEN }} run: | git config --global credential.helper store - echo "https://${KANTEXT_BUILD_GITHUB_TOKENS}:x-oauth-basic@github.com" > ~/.git-credentials + echo "https://${KANTEXT_BUILD_GITHUB_TOKEN}:x-oauth-basic@github.com" > ~/.git-credentials - name: Verify credentials env: diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index f428297..5acfcff 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -83,10 +83,10 @@ jobs: - name: Configure git credentials for private repos env: - KANTEXT_BUILD_GITHUB_TOKENS: ${{ secrets.KANTEXT_BUILD_GITHUB_TOKENS }} + KANTEXT_BUILD_GITHUB_TOKEN: ${{ secrets.KANTEXT_BUILD_GITHUB_TOKEN }} run: | git config --global credential.helper store - echo "https://${KANTEXT_BUILD_GITHUB_TOKENS}:x-oauth-basic@github.com" > ~/.git-credentials + echo "https://${KANTEXT_BUILD_GITHUB_TOKEN}:x-oauth-basic@github.com" > ~/.git-credentials - name: Cache cargo registry uses: actions/cache@v5 From dd50b85f0d205687784e6e86f61cb9d1ed445c78 Mon Sep 17 00:00:00 2001 From: Jason Lantz Date: Thu, 5 Feb 2026 05:18:05 +0000 Subject: [PATCH 12/15] Enhance Busbar integration by consolidating keychain and capability features into a single `busbar` feature --- crates/sf-bridge/Cargo.toml | 5 ++--- crates/sf-bridge/README.md | 17 ++++++++++------- crates/sf-bridge/src/lib.rs | 10 +++++----- 3 files changed, 17 insertions(+), 15 deletions(-) diff --git a/crates/sf-bridge/Cargo.toml b/crates/sf-bridge/Cargo.toml index 2682a74..35d3248 100644 --- a/crates/sf-bridge/Cargo.toml +++ b/crates/sf-bridge/Cargo.toml @@ -14,9 +14,8 @@ rest = ["dep:busbar-sf-rest", "dep:busbar-sf-client"] bulk = ["rest", "dep:busbar-sf-bulk"] tooling = ["rest", "dep:busbar-sf-tooling"] metadata = ["rest", "dep:busbar-sf-metadata"] -busbar = ["dep:busbar-capability"] -# Busbar keychain integration (optional, off by default) -busbar-keychain = ["rest", "dep:busbar-keychain", "dep:busbar-sf-auth"] +# Busbar ecosystem integration (Salesforce capability + keychain auth) +busbar = ["rest", "dep:busbar-capability", "dep:busbar-keychain", "dep:busbar-sf-auth"] [dependencies] # Internal crates diff --git a/crates/sf-bridge/README.md b/crates/sf-bridge/README.md index a89c7b8..7090a5d 100644 --- a/crates/sf-bridge/README.md +++ b/crates/sf-bridge/README.md @@ -158,12 +158,15 @@ pub fn query_accounts(input: String) -> FnResult>> { - `bulk` - Bulk API endpoints (requires `rest`) - `tooling` - Tooling API endpoints (requires `rest`) - `metadata` - Metadata API endpoints (requires `rest`) -- `busbar` - Implement Busbar's `HostCapability` trait for use with Busbar runtime -- `busbar-keychain` - Busbar keychain integration for transparent credential resolution (optional, off by default) +- `busbar` - Busbar ecosystem integration (Salesforce capability provider + keychain-based credential resolution, optional, off by default) -### Busbar Keychain Integration +### Busbar Integration -When the `busbar-keychain` feature is enabled, `SfBridge` can resolve credentials automatically from the Busbar keychain system: +When the `busbar` feature is enabled, `SfBridge` provides two key integrations: + +#### 1. Keychain-based Credential Resolution + +`SfBridge` can resolve credentials automatically from the Busbar keychain system: ```rust use busbar_sf_bridge::{SfBridge, KeychainAuthConfig}; @@ -187,10 +190,10 @@ let bridge = SfBridge::new_with_keychain_auth(wasm_bytes, config).await?; 2. Busbar keychain via `busbar-keychain::SecretStore` - Local development 3. JWT bearer authentication (if configured) - Server-to-server -### Busbar Capability Integration +#### 2. Busbar Capability Integration -When the `busbar` feature is enabled, `SfBridge` implements the `HostCapability` trait -from `busbar-capability`, making it a drop-in capability provider for the Busbar runtime: +`SfBridge` implements the `HostCapability` trait from `busbar-capability`, making it +a drop-in capability provider for the Busbar runtime: ```rust use busbar_sf_bridge::SfBridge; diff --git a/crates/sf-bridge/src/lib.rs b/crates/sf-bridge/src/lib.rs index 7d8947a..6c03e5a 100644 --- a/crates/sf-bridge/src/lib.rs +++ b/crates/sf-bridge/src/lib.rs @@ -77,7 +77,7 @@ //! //! ## Example (Busbar Keychain Authentication) //! -//! When the `busbar-keychain` feature is enabled: +//! When the `busbar` feature is enabled: //! //! ```rust,ignore //! use busbar_sf_bridge::{SfBridge, KeychainAuthConfig}; @@ -105,10 +105,10 @@ mod registration; #[cfg(feature = "busbar")] mod capability; -#[cfg(feature = "busbar-keychain")] +#[cfg(feature = "busbar")] pub mod keychain_auth; -#[cfg(feature = "busbar-keychain")] +#[cfg(feature = "busbar")] pub use keychain_auth::{JwtAuthConfig, KeychainAuthConfig, KeychainAuthResolver}; pub use error::{Error, Result}; @@ -251,7 +251,7 @@ impl SfBridge { /// Ok(()) /// } /// ``` - #[cfg(all(feature = "busbar-keychain", feature = "rest"))] + #[cfg(all(feature = "busbar", feature = "rest"))] pub async fn new_with_keychain_auth( wasm_bytes: Vec, config: keychain_auth::KeychainAuthConfig, @@ -263,7 +263,7 @@ impl SfBridge { /// Create a new bridge with Busbar keychain auth and a specific tokio runtime handle. /// /// Use this when constructing the bridge outside of a tokio context. - #[cfg(all(feature = "busbar-keychain", feature = "rest"))] + #[cfg(all(feature = "busbar", feature = "rest"))] pub async fn with_keychain_auth_and_handle( wasm_bytes: Vec, config: keychain_auth::KeychainAuthConfig, From 74571de9ea5fc865889fc862dad848eb3a7de106 Mon Sep 17 00:00:00 2001 From: Jason Lantz Date: Thu, 5 Feb 2026 05:28:50 +0000 Subject: [PATCH 13/15] Integrate Busbar authentication and capability features into the sf-bridge crate --- Cargo.toml | 4 ++++ crates/sf-bridge/Cargo.toml | 7 ++++++- crates/sf-bridge/src/keychain_auth.rs | 1 + 3 files changed, 11 insertions(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 9f48f2e..5771ad6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -52,6 +52,9 @@ dependencies = [ # Optional feature for typed metadata operations with busbar-sf-types typed-metadata = ["metadata", "busbar-sf-metadata/typed"] +# Busbar ecosystem integration (WASM bridge with keychain auth and capability provider) +busbar = ["dep:busbar-sf-bridge"] + [package.metadata.docs.rs] all-features = true rustdoc-args = ["--cfg", "docsrs"] @@ -63,6 +66,7 @@ busbar-sf-rest = { workspace = true, optional = true } busbar-sf-bulk = { workspace = true, optional = true } busbar-sf-metadata = { workspace = true, optional = true } busbar-sf-tooling = { workspace = true, optional = true } +busbar-sf-bridge = { workspace = true, optional = true, features = ["busbar"] } tokio.workspace = true chrono.workspace = true serde.workspace = true diff --git a/crates/sf-bridge/Cargo.toml b/crates/sf-bridge/Cargo.toml index 35d3248..32fe875 100644 --- a/crates/sf-bridge/Cargo.toml +++ b/crates/sf-bridge/Cargo.toml @@ -15,7 +15,12 @@ bulk = ["rest", "dep:busbar-sf-bulk"] tooling = ["rest", "dep:busbar-sf-tooling"] metadata = ["rest", "dep:busbar-sf-metadata"] # Busbar ecosystem integration (Salesforce capability + keychain auth) -busbar = ["rest", "dep:busbar-capability", "dep:busbar-keychain", "dep:busbar-sf-auth"] +busbar = [ + "rest", + "dep:busbar-capability", + "dep:busbar-keychain", + "dep:busbar-sf-auth", +] [dependencies] # Internal crates diff --git a/crates/sf-bridge/src/keychain_auth.rs b/crates/sf-bridge/src/keychain_auth.rs index a07e356..2ec4153 100644 --- a/crates/sf-bridge/src/keychain_auth.rs +++ b/crates/sf-bridge/src/keychain_auth.rs @@ -244,6 +244,7 @@ impl KeychainAuthResolver { #[cfg(test)] mod tests { use super::*; + use busbar_sf_auth::Credentials; #[tokio::test] #[ignore] // Requires environment setup From b1aff16508558ef0c330874e10dbcfd78064ea5e Mon Sep 17 00:00:00 2001 From: Jason Lantz Date: Thu, 5 Feb 2026 05:31:14 +0000 Subject: [PATCH 14/15] Refactor Cargo.toml to streamline dependency exclusions and fix formatting in dependencies section --- Cargo.toml | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 5771ad6..23de11c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,10 +12,7 @@ members = [ # sf-guest-sdk is excluded: it compiles to wasm32-unknown-unknown only. # See examples/wasm-guest-plugin for usage. ] -exclude = [ - "crates/sf-guest-sdk", - "examples/wasm-guest-plugin", -] +exclude = ["crates/sf-guest-sdk", "examples/wasm-guest-plugin"] # Root package for integration tests [package] @@ -46,7 +43,7 @@ tooling = ["auth", "dep:busbar-sf-tooling"] dependencies = [ "busbar-sf-client/dependencies", "busbar-sf-tooling/dependencies", - "busbar-sf-bulk/dependencies" + "busbar-sf-bulk/dependencies", ] # Optional feature for typed metadata operations with busbar-sf-types From 3231cfb6bde0f0b430896c4a4a5a537e1ecd534d Mon Sep 17 00:00:00 2001 From: Jason Lantz Date: Thu, 5 Feb 2026 05:54:18 +0000 Subject: [PATCH 15/15] Refactor CI workflow and update Cargo.toml for Busbar integration --- .github/workflows/ci.yml | 506 +++++++++++++++++++-------------------- Cargo.toml | 4 +- 2 files changed, 255 insertions(+), 255 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1aff707..0fbceab 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,262 +1,262 @@ name: CI on: - push: - branches: [main] - pull_request: - branches: [main] - workflow_dispatch: + push: + branches: [main] + pull_request: + branches: [main] + workflow_dispatch: permissions: - contents: read + contents: read env: - RUST_BACKTRACE: 1 - CARGO_TERM_COLOR: always + RUST_BACKTRACE: 1 + CARGO_TERM_COLOR: always jobs: - # ── Fast checks (no compilation) ────────────────────────────────────── - fmt: - name: Format - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v6 - - uses: dtolnay/rust-toolchain@stable - with: - components: rustfmt - - run: cargo fmt --all --check - - # ── Lint ────────────────────────────────────────────────────────────── - clippy: - name: Clippy - runs-on: ubuntu-latest - environment: GitHub composable-delivery - steps: - - uses: actions/checkout@v6 - - uses: dtolnay/rust-toolchain@stable - with: - components: clippy - - uses: Swatinem/rust-cache@v2 - - - name: Configure git credentials for private repos - env: - KANTEXT_BUILD_GITHUB_TOKEN: ${{ secrets.KANTEXT_BUILD_GITHUB_TOKEN }} - run: | - git config --global credential.helper store - echo "https://${KANTEXT_BUILD_GITHUB_TOKEN}:x-oauth-basic@github.com" > ~/.git-credentials - - - run: cargo clippy --workspace --all-targets --all-features -- -D warnings - - # ── Unit tests with coverage ────────────────────────────────────────── - test: - name: Test - runs-on: ubuntu-latest - environment: GitHub composable-delivery - steps: - - uses: actions/checkout@v6 - - uses: dtolnay/rust-toolchain@stable - - uses: taiki-e/install-action@cargo-llvm-cov - - uses: Swatinem/rust-cache@v2 - - - name: Configure git credentials for private repos - env: - KANTEXT_BUILD_GITHUB_TOKEN: ${{ secrets.KANTEXT_BUILD_GITHUB_TOKEN }} - run: | - git config --global credential.helper store - echo "https://${KANTEXT_BUILD_GITHUB_TOKEN}:x-oauth-basic@github.com" > ~/.git-credentials - - - name: Clean coverage artifacts - run: cargo llvm-cov clean --workspace - - - name: Run unit tests with coverage - run: cargo llvm-cov --workspace --all-features --lib --lcov --output-path lcov-unit.info - - - name: Run doc tests - if: always() - run: cargo test --workspace --doc - - - name: Generate summary - if: always() - run: | - echo "### Unit Tests" >> "$GITHUB_STEP_SUMMARY" - cargo llvm-cov report --workspace --summary-only 2>&1 | tail -20 >> "$GITHUB_STEP_SUMMARY" - - - name: Upload coverage to Codecov - if: always() - uses: codecov/codecov-action@v5 - with: - files: lcov-unit.info - flags: unit-tests - disable_search: true - fail_ci_if_error: false - env: - CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} - - # ── Documentation ───────────────────────────────────────────────────── - docs: - name: Documentation - runs-on: ubuntu-latest - environment: GitHub composable-delivery - steps: - - uses: actions/checkout@v6 - - uses: dtolnay/rust-toolchain@stable - - uses: Swatinem/rust-cache@v2 - - - name: Configure git credentials for private repos - env: - KANTEXT_BUILD_GITHUB_TOKEN: ${{ secrets.KANTEXT_BUILD_GITHUB_TOKEN }} - run: | - git config --global credential.helper store - echo "https://${KANTEXT_BUILD_GITHUB_TOKEN}:x-oauth-basic@github.com" > ~/.git-credentials - - - run: cargo doc --workspace --no-deps --all-features - env: - RUSTDOCFLAGS: -D warnings - - # ── MSRV ────────────────────────────────────────────────────────────── - msrv: - name: MSRV - runs-on: ubuntu-latest - environment: GitHub composable-delivery - steps: - - uses: actions/checkout@v6 - - uses: dtolnay/rust-toolchain@master - with: - toolchain: "1.88" - - uses: Swatinem/rust-cache@v2 - - - name: Configure git credentials for private repos - env: - KANTEXT_BUILD_GITHUB_TOKEN: ${{ secrets.KANTEXT_BUILD_GITHUB_TOKEN }} - run: | - git config --global credential.helper store - echo "https://${KANTEXT_BUILD_GITHUB_TOKEN}:x-oauth-basic@github.com" > ~/.git-credentials - - - run: cargo check --workspace - - # ── Integration tests (real Salesforce org, excluding WASM bridge) ── - integration: - name: Integration Tests - runs-on: ubuntu-latest - environment: scratch - steps: - - uses: actions/checkout@v6 - - uses: dtolnay/rust-toolchain@stable - - uses: taiki-e/install-action@cargo-llvm-cov - - uses: Swatinem/rust-cache@v2 - - - name: Configure git credentials for private repos - env: - KANTEXT_BUILD_GITHUB_TOKEN: ${{ secrets.KANTEXT_BUILD_GITHUB_TOKEN }} - run: | - git config --global credential.helper store - echo "https://${KANTEXT_BUILD_GITHUB_TOKEN}:x-oauth-basic@github.com" > ~/.git-credentials - - - name: Verify credentials - env: - SF_AUTH_URL: ${{ secrets.SF_AUTH_URL }} - run: | - if [ -z "$SF_AUTH_URL" ]; then - echo "::error::SF_AUTH_URL secret is not configured" - exit 1 - fi - - - name: Setup scratch org test data - env: - SF_AUTH_URL: ${{ secrets.SF_AUTH_URL }} - run: cargo run --bin setup-scratch-org - - - name: Clean coverage artifacts - run: cargo llvm-cov clean --workspace - - - name: Run integration tests with coverage (excluding bridge) - env: - SF_AUTH_URL: ${{ secrets.SF_AUTH_URL }} - run: | - cargo llvm-cov --workspace --all-features --test integration --lcov --output-path lcov-integration.info -- --nocapture --skip bridge:: - - - name: Generate summary - if: always() - env: - SF_AUTH_URL: ${{ secrets.SF_AUTH_URL }} - run: | - echo "### Integration Tests" >> "$GITHUB_STEP_SUMMARY" - cargo llvm-cov report --workspace --summary-only 2>&1 | tail -20 >> "$GITHUB_STEP_SUMMARY" - - - name: Upload coverage to Codecov - if: always() - uses: codecov/codecov-action@v5 - with: - files: lcov-integration.info - flags: integration-tests - disable_search: true - fail_ci_if_error: false - env: - CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} - - # ── WASM bridge integration tests (real Salesforce org) ────────────── - wasm-integration: - name: WASM Bridge Integration Tests - runs-on: ubuntu-latest - environment: scratch - steps: - - uses: actions/checkout@v6 - - uses: dtolnay/rust-toolchain@stable - with: - targets: wasm32-unknown-unknown - - uses: taiki-e/install-action@cargo-llvm-cov - - uses: Swatinem/rust-cache@v2 - - - name: Configure git credentials for private repos - env: - KANTEXT_BUILD_GITHUB_TOKEN: ${{ secrets.KANTEXT_BUILD_GITHUB_TOKEN }} - run: | - git config --global credential.helper store - echo "https://${KANTEXT_BUILD_GITHUB_TOKEN}:x-oauth-basic@github.com" > ~/.git-credentials - - - name: Verify credentials - env: - SF_AUTH_URL: ${{ secrets.SF_AUTH_URL }} - run: | - if [ -z "$SF_AUTH_URL" ]; then - echo "::error::SF_AUTH_URL secret is not configured" - exit 1 - fi - - - name: Setup scratch org test data - env: - SF_AUTH_URL: ${{ secrets.SF_AUTH_URL }} - run: cargo run --bin setup-scratch-org - - - name: Build WASM test plugin - run: | - cargo build --manifest-path tests/wasm-test-plugin/Cargo.toml \ - --target wasm32-unknown-unknown --release - - - name: Clean coverage artifacts - run: cargo llvm-cov clean --workspace - - - name: Run WASM bridge integration tests with coverage - env: - SF_AUTH_URL: ${{ secrets.SF_AUTH_URL }} - run: | - cargo llvm-cov --workspace --all-features --test integration --lcov --output-path lcov-wasm-integration.info -- --nocapture bridge:: - - - name: Generate summary - if: always() - env: - SF_AUTH_URL: ${{ secrets.SF_AUTH_URL }} - run: | - echo "### WASM Bridge Integration Tests" >> "$GITHUB_STEP_SUMMARY" - cargo llvm-cov report --workspace --summary-only 2>&1 | tail -20 >> "$GITHUB_STEP_SUMMARY" - - - name: Upload coverage to Codecov - if: always() - uses: codecov/codecov-action@v5 - with: - files: lcov-wasm-integration.info - flags: wasm-integration-tests - disable_search: true - fail_ci_if_error: false - env: - CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} + # ── Fast checks (no compilation) ────────────────────────────────────── + fmt: + name: Format + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + - uses: dtolnay/rust-toolchain@stable + with: + components: rustfmt + - run: cargo fmt --all --check + + # ── Lint ────────────────────────────────────────────────────────────── + clippy: + name: Clippy + runs-on: ubuntu-latest + environment: GitHub composable-delivery + steps: + - uses: actions/checkout@v6 + - uses: dtolnay/rust-toolchain@stable + with: + components: clippy + - uses: Swatinem/rust-cache@v2 + + - name: Configure git credentials for private repos + env: + KANTEXT_BUILD_GITHUB_TOKEN: ${{ secrets.KANTEXT_BUILD_GITHUB_TOKEN }} + run: | + git config --global credential.helper store + echo "https://${KANTEXT_BUILD_GITHUB_TOKEN}:x-oauth-basic@github.com" > ~/.git-credentials + + - run: cargo clippy --workspace --all-targets -- -D warnings + + # ── Unit tests with coverage ────────────────────────────────────────── + test: + name: Test + runs-on: ubuntu-latest + environment: GitHub composable-delivery + steps: + - uses: actions/checkout@v6 + - uses: dtolnay/rust-toolchain@stable + - uses: taiki-e/install-action@cargo-llvm-cov + - uses: Swatinem/rust-cache@v2 + + - name: Configure git credentials for private repos + env: + KANTEXT_BUILD_GITHUB_TOKEN: ${{ secrets.KANTEXT_BUILD_GITHUB_TOKEN }} + run: | + git config --global credential.helper store + echo "https://${KANTEXT_BUILD_GITHUB_TOKEN}:x-oauth-basic@github.com" > ~/.git-credentials + + - name: Clean coverage artifacts + run: cargo llvm-cov clean --workspace + + - name: Run unit tests with coverage + run: cargo llvm-cov --workspace --lib --lcov --output-path lcov-unit.info + + - name: Run doc tests + if: always() + run: cargo test --workspace --doc + + - name: Generate summary + if: always() + run: | + echo "### Unit Tests" >> "$GITHUB_STEP_SUMMARY" + cargo llvm-cov report --workspace --summary-only 2>&1 | tail -20 >> "$GITHUB_STEP_SUMMARY" + + - name: Upload coverage to Codecov + if: always() + uses: codecov/codecov-action@v5 + with: + files: lcov-unit.info + flags: unit-tests + disable_search: true + fail_ci_if_error: false + env: + CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} + + # ── Documentation ───────────────────────────────────────────────────── + docs: + name: Documentation + runs-on: ubuntu-latest + environment: GitHub composable-delivery + steps: + - uses: actions/checkout@v6 + - uses: dtolnay/rust-toolchain@stable + - uses: Swatinem/rust-cache@v2 + + - name: Configure git credentials for private repos + env: + KANTEXT_BUILD_GITHUB_TOKEN: ${{ secrets.KANTEXT_BUILD_GITHUB_TOKEN }} + run: | + git config --global credential.helper store + echo "https://${KANTEXT_BUILD_GITHUB_TOKEN}:x-oauth-basic@github.com" > ~/.git-credentials + + - run: cargo doc --workspace --no-deps + env: + RUSTDOCFLAGS: -D warnings + + # ── MSRV ────────────────────────────────────────────────────────────── + msrv: + name: MSRV + runs-on: ubuntu-latest + environment: GitHub composable-delivery + steps: + - uses: actions/checkout@v6 + - uses: dtolnay/rust-toolchain@master + with: + toolchain: "1.88" + - uses: Swatinem/rust-cache@v2 + + - name: Configure git credentials for private repos + env: + KANTEXT_BUILD_GITHUB_TOKEN: ${{ secrets.KANTEXT_BUILD_GITHUB_TOKEN }} + run: | + git config --global credential.helper store + echo "https://${KANTEXT_BUILD_GITHUB_TOKEN}:x-oauth-basic@github.com" > ~/.git-credentials + + - run: cargo check --workspace + + # ── Integration tests (real Salesforce org, excluding WASM bridge) ── + integration: + name: Integration Tests + runs-on: ubuntu-latest + environment: scratch + steps: + - uses: actions/checkout@v6 + - uses: dtolnay/rust-toolchain@stable + - uses: taiki-e/install-action@cargo-llvm-cov + - uses: Swatinem/rust-cache@v2 + + - name: Configure git credentials for private repos + env: + KANTEXT_BUILD_GITHUB_TOKEN: ${{ secrets.KANTEXT_BUILD_GITHUB_TOKEN }} + run: | + git config --global credential.helper store + echo "https://${KANTEXT_BUILD_GITHUB_TOKEN}:x-oauth-basic@github.com" > ~/.git-credentials + + - name: Verify credentials + env: + SF_AUTH_URL: ${{ secrets.SF_AUTH_URL }} + run: | + if [ -z "$SF_AUTH_URL" ]; then + echo "::error::SF_AUTH_URL secret is not configured" + exit 1 + fi + + - name: Setup scratch org test data + env: + SF_AUTH_URL: ${{ secrets.SF_AUTH_URL }} + run: cargo run --bin setup-scratch-org + + - name: Clean coverage artifacts + run: cargo llvm-cov clean --workspace + + - name: Run integration tests with coverage (excluding bridge) + env: + SF_AUTH_URL: ${{ secrets.SF_AUTH_URL }} + run: | + cargo llvm-cov --workspace --test integration --lcov --output-path lcov-integration.info -- --nocapture --skip bridge:: + + - name: Generate summary + if: always() + env: + SF_AUTH_URL: ${{ secrets.SF_AUTH_URL }} + run: | + echo "### Integration Tests" >> "$GITHUB_STEP_SUMMARY" + cargo llvm-cov report --workspace --summary-only 2>&1 | tail -20 >> "$GITHUB_STEP_SUMMARY" + + - name: Upload coverage to Codecov + if: always() + uses: codecov/codecov-action@v5 + with: + files: lcov-integration.info + flags: integration-tests + disable_search: true + fail_ci_if_error: false + env: + CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} + + # ── WASM bridge integration tests (real Salesforce org) ────────────── + wasm-integration: + name: WASM Bridge Integration Tests + runs-on: ubuntu-latest + environment: scratch + steps: + - uses: actions/checkout@v6 + - uses: dtolnay/rust-toolchain@stable + with: + targets: wasm32-unknown-unknown + - uses: taiki-e/install-action@cargo-llvm-cov + - uses: Swatinem/rust-cache@v2 + + - name: Configure git credentials for private repos + env: + KANTEXT_BUILD_GITHUB_TOKEN: ${{ secrets.KANTEXT_BUILD_GITHUB_TOKEN }} + run: | + git config --global credential.helper store + echo "https://${KANTEXT_BUILD_GITHUB_TOKEN}:x-oauth-basic@github.com" > ~/.git-credentials + + - name: Verify credentials + env: + SF_AUTH_URL: ${{ secrets.SF_AUTH_URL }} + run: | + if [ -z "$SF_AUTH_URL" ]; then + echo "::error::SF_AUTH_URL secret is not configured" + exit 1 + fi + + - name: Setup scratch org test data + env: + SF_AUTH_URL: ${{ secrets.SF_AUTH_URL }} + run: cargo run --bin setup-scratch-org + + - name: Build WASM test plugin + run: | + cargo build --manifest-path tests/wasm-test-plugin/Cargo.toml \ + --target wasm32-unknown-unknown --release + + - name: Clean coverage artifacts + run: cargo llvm-cov clean --workspace + + - name: Run WASM bridge integration tests with coverage + env: + SF_AUTH_URL: ${{ secrets.SF_AUTH_URL }} + run: | + cargo llvm-cov --workspace --test integration --lcov --output-path lcov-wasm-integration.info -- --nocapture bridge:: + + - name: Generate summary + if: always() + env: + SF_AUTH_URL: ${{ secrets.SF_AUTH_URL }} + run: | + echo "### WASM Bridge Integration Tests" >> "$GITHUB_STEP_SUMMARY" + cargo llvm-cov report --workspace --summary-only 2>&1 | tail -20 >> "$GITHUB_STEP_SUMMARY" + + - name: Upload coverage to Codecov + if: always() + uses: codecov/codecov-action@v5 + with: + files: lcov-wasm-integration.info + flags: wasm-integration-tests + disable_search: true + fail_ci_if_error: false + env: + CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} diff --git a/Cargo.toml b/Cargo.toml index 23de11c..16c4ab0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -50,7 +50,7 @@ dependencies = [ typed-metadata = ["metadata", "busbar-sf-metadata/typed"] # Busbar ecosystem integration (WASM bridge with keychain auth and capability provider) -busbar = ["dep:busbar-sf-bridge"] +busbar = ["dep:busbar-sf-bridge", "busbar-sf-bridge/busbar"] [package.metadata.docs.rs] all-features = true @@ -63,7 +63,7 @@ busbar-sf-rest = { workspace = true, optional = true } busbar-sf-bulk = { workspace = true, optional = true } busbar-sf-metadata = { workspace = true, optional = true } busbar-sf-tooling = { workspace = true, optional = true } -busbar-sf-bridge = { workspace = true, optional = true, features = ["busbar"] } +busbar-sf-bridge = { workspace = true, optional = true } tokio.workspace = true chrono.workspace = true serde.workspace = true