use anyhow::{Context, Result};
use serde::{Deserialize, Serialize};
use super::{Config, ProxyAuth};
use crate::patch::ProviderApiKeyIntents;
trait CredentialResolver {
fn resolve(
&self,
credential_ref: &crate::CredentialRef,
) -> crate::ConfigStoreResult<Option<crate::SecretValue>>;
}
impl CredentialResolver for crate::CredentialStore {
fn resolve(
&self,
credential_ref: &crate::CredentialRef,
) -> crate::ConfigStoreResult<Option<crate::SecretValue>> {
crate::CredentialStore::resolve(self, credential_ref)
}
}
impl CredentialResolver for crate::credential_store::CredentialDocumentLkg {
fn resolve(
&self,
credential_ref: &crate::CredentialRef,
) -> crate::ConfigStoreResult<Option<crate::SecretValue>> {
self.resolve(credential_ref)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub(crate) struct AccessVerifierRecord {
pub hash: String,
pub salt: String,
}
pub fn access_password_credential_ref() -> crate::ConfigStoreResult<crate::CredentialRef> {
crate::credential_ref("access", "root", "password_verifier")
}
pub fn access_device_credential_ref(
device_id: &str,
) -> crate::ConfigStoreResult<crate::CredentialRef> {
crate::credential_ref("access", device_id, "device_token_verifier")
}
pub(crate) fn encode_access_verifier(hash: &str, salt: &str) -> crate::ConfigStoreResult<String> {
validate_access_verifier(hash, salt)?;
serde_json::to_string(&AccessVerifierRecord {
hash: hash.to_string(),
salt: salt.to_string(),
})
.map_err(Into::into)
}
fn validate_access_verifier(hash: &str, salt: &str) -> crate::ConfigStoreResult<()> {
if hash.len() != 64
|| !hash.bytes().all(|byte| byte.is_ascii_hexdigit())
|| salt.is_empty()
|| !salt.len().is_multiple_of(2)
|| !salt.bytes().all(|byte| byte.is_ascii_hexdigit())
{
return Err(crate::ConfigStoreError::Validation(
"access-control verifier is invalid".to_string(),
));
}
Ok(())
}
pub(crate) fn decode_access_verifier(
secret: &str,
) -> crate::ConfigStoreResult<AccessVerifierRecord> {
let record: AccessVerifierRecord = serde_json::from_str(secret).map_err(|_| {
crate::ConfigStoreError::Validation(
"access-control verifier credential is invalid".to_string(),
)
})?;
validate_access_verifier(&record.hash, &record.salt)?;
Ok(record)
}
fn hydrate_header_credentials(
resolver: &impl CredentialResolver,
headers: &mut [bamboo_domain::mcp_config::HeaderConfig],
) -> crate::ConfigStoreResult<()> {
for header in headers {
if !header.value.is_empty() {
continue;
}
let Some(raw_reference) = header.credential_ref.as_ref() else {
continue;
};
let reference = crate::CredentialRef::parse(raw_reference.clone())?;
header.value = resolver
.resolve(&reference)?
.ok_or_else(|| {
crate::ConfigStoreError::Validation(
"referenced MCP credential is unavailable".to_string(),
)
})?
.expose()
.to_string();
}
Ok(())
}
impl crate::BrokerClientConfig {
pub fn hydrate_credential_from_store(
&mut self,
data_dir: &std::path::Path,
) -> crate::ConfigStoreResult<()> {
self.token_encrypted = None;
let Some(reference) = self.credential_ref.as_ref() else {
if self.configured {
return Err(crate::ConfigStoreError::Validation(
"configured broker credential reference is missing".to_string(),
));
}
self.token.clear();
return Ok(());
};
match crate::CredentialStore::open(data_dir).resolve(reference)? {
Some(secret) => {
self.token = secret.expose().to_string();
self.configured = true;
Ok(())
}
None => Err(crate::ConfigStoreError::Validation(
"referenced broker credential is unavailable".to_string(),
)),
}
}
}
impl Config {
pub fn hydrate_proxy_auth_from_encrypted(&mut self) {
if self.proxy_auth_credential_ref.is_some() {
self.proxy_auth_encrypted = None;
return;
}
if self.proxy_auth.is_some() {
return;
}
if self
.proxy_auth_encrypted
.as_deref()
.map(|s| s.trim().is_empty())
.unwrap_or(true)
{
let legacy = self
.extra
.get("https_proxy_auth_encrypted")
.and_then(|v| v.as_str())
.or_else(|| {
self.extra
.get("http_proxy_auth_encrypted")
.and_then(|v| v.as_str())
})
.map(|s| s.trim())
.filter(|s| !s.is_empty())
.map(|s| s.to_string());
if let Some(legacy) = legacy {
self.proxy_auth_encrypted = Some(legacy);
}
}
let Some(encrypted) = self.proxy_auth_encrypted.as_deref() else {
return;
};
match crate::encryption::decrypt(encrypted) {
Ok(decrypted) => match serde_json::from_str::<ProxyAuth>(&decrypted) {
Ok(auth) => {
self.proxy_auth = Some(auth);
self.extra.remove("http_proxy_auth_encrypted");
self.extra.remove("https_proxy_auth_encrypted");
}
Err(e) => tracing::warn!("Failed to parse decrypted proxy auth JSON: {}", e),
},
Err(e) => tracing::warn!("Failed to decrypt proxy auth: {}", e),
}
}
pub fn refresh_proxy_auth_encrypted(&mut self) -> Result<()> {
if self.proxy_auth_credential_ref.is_some() {
self.proxy_auth_encrypted = None;
return Ok(());
}
let Some(auth) = self.proxy_auth.as_ref() else {
self.proxy_auth_encrypted = None;
return Ok(());
};
let auth_str = serde_json::to_string(auth).context("Failed to serialize proxy auth")?;
let encrypted =
crate::encryption::encrypt(&auth_str).context("Failed to encrypt proxy auth")?;
self.proxy_auth_encrypted = Some(encrypted);
Ok(())
}
pub fn hydrate_proxy_auth_from_store(&mut self, data_dir: &std::path::Path) -> Result<()> {
let store = crate::CredentialStore::open(data_dir);
self.hydrate_proxy_auth_from_resolver(&store)
.map_err(anyhow::Error::from)
}
pub(crate) fn hydrate_proxy_auth_from_snapshot(
&mut self,
snapshot: &crate::credential_store::CredentialDocumentLkg,
) -> crate::ConfigStoreResult<()> {
self.hydrate_proxy_auth_from_resolver(snapshot)
}
fn hydrate_proxy_auth_from_resolver(
&mut self,
resolver: &impl CredentialResolver,
) -> crate::ConfigStoreResult<()> {
let Some(reference) = self.proxy_auth_credential_ref.as_ref() else {
return Ok(());
};
let value = resolver.resolve(reference)?;
let Some(value) = value else {
self.proxy_auth = None;
return Ok(());
};
self.proxy_auth = Some(serde_json::from_str(value.expose()).map_err(|_| {
crate::ConfigStoreError::Validation("proxy auth credential is invalid".to_string())
})?);
self.proxy_auth_encrypted = None;
Ok(())
}
pub fn hydrate_provider_api_keys_from_encrypted(&mut self) {
if let Some(openai) = self.providers.openai.as_mut() {
if openai.api_key.trim().is_empty() {
if let Some(encrypted) = openai.api_key_encrypted.as_deref() {
match crate::encryption::decrypt(encrypted) {
Ok(value) => openai.api_key = value,
Err(e) => tracing::warn!("Failed to decrypt OpenAI api_key: {}", e),
}
}
}
}
if let Some(anthropic) = self.providers.anthropic.as_mut() {
if anthropic.api_key.trim().is_empty() {
if let Some(encrypted) = anthropic.api_key_encrypted.as_deref() {
match crate::encryption::decrypt(encrypted) {
Ok(value) => anthropic.api_key = value,
Err(e) => tracing::warn!("Failed to decrypt Anthropic api_key: {}", e),
}
}
}
}
if let Some(gemini) = self.providers.gemini.as_mut() {
if gemini.api_key.trim().is_empty() {
if let Some(encrypted) = gemini.api_key_encrypted.as_deref() {
match crate::encryption::decrypt(encrypted) {
Ok(value) => gemini.api_key = value,
Err(e) => tracing::warn!("Failed to decrypt Gemini api_key: {}", e),
}
}
}
}
if let Some(bodhi) = self.providers.bodhi.as_mut() {
if bodhi.api_key.trim().is_empty() {
if let Some(encrypted) = bodhi.api_key_encrypted.as_deref() {
match crate::encryption::decrypt(encrypted) {
Ok(value) => bodhi.api_key = value,
Err(e) => tracing::warn!("Failed to decrypt Bodhi api_key: {}", e),
}
}
}
}
}
pub fn hydrate_provider_credentials_from_store(
&mut self,
data_dir: &std::path::Path,
) -> crate::ConfigStoreResult<()> {
let store = crate::CredentialStore::open(data_dir);
self.hydrate_provider_credentials_from_resolver(&store)
}
pub(crate) fn hydrate_provider_credentials_from_snapshot(
&mut self,
snapshot: &crate::credential_store::CredentialDocumentLkg,
) -> crate::ConfigStoreResult<()> {
self.hydrate_provider_credentials_from_resolver(snapshot)
}
fn hydrate_provider_credentials_from_resolver(
&mut self,
resolver: &impl CredentialResolver,
) -> crate::ConfigStoreResult<()> {
macro_rules! hydrate {
($provider:expr) => {
if let Some(provider) = $provider {
if provider.api_key.trim().is_empty() {
if let Some(reference) = provider.credential_ref.as_ref() {
provider.api_key = resolver
.resolve(reference)?
.ok_or_else(|| {
crate::ConfigStoreError::Validation(
"referenced provider credential is unavailable".to_string(),
)
})?
.expose()
.to_string();
}
}
}
};
}
hydrate!(self.providers.openai.as_mut());
hydrate!(self.providers.anthropic.as_mut());
hydrate!(self.providers.gemini.as_mut());
hydrate!(self.providers.bodhi.as_mut());
for instance in self.provider_instances.values_mut() {
if instance.api_key.trim().is_empty() {
if let Some(reference) = instance.credential_ref.as_ref() {
instance.api_key = resolver
.resolve(reference)?
.ok_or_else(|| {
crate::ConfigStoreError::Validation(
"referenced provider credential is unavailable".to_string(),
)
})?
.expose()
.to_string();
}
}
}
Ok(())
}
pub fn refresh_provider_api_keys_encrypted(&mut self) -> Result<()> {
if let Some(openai) = self.providers.openai.as_mut() {
if openai.credential_ref.is_some() {
openai.api_key_encrypted = None;
} else if !openai.api_key_from_env {
let api_key = openai.api_key.trim();
if !api_key.is_empty() {
openai.api_key_encrypted = Some(
crate::encryption::encrypt(api_key)
.context("Failed to encrypt OpenAI api_key")?,
);
}
}
}
if let Some(anthropic) = self.providers.anthropic.as_mut() {
if anthropic.credential_ref.is_some() {
anthropic.api_key_encrypted = None;
} else if !anthropic.api_key_from_env {
let api_key = anthropic.api_key.trim();
if !api_key.is_empty() {
anthropic.api_key_encrypted = Some(
crate::encryption::encrypt(api_key)
.context("Failed to encrypt Anthropic api_key")?,
);
}
}
}
if let Some(gemini) = self.providers.gemini.as_mut() {
if gemini.credential_ref.is_some() {
gemini.api_key_encrypted = None;
} else if !gemini.api_key_from_env {
let api_key = gemini.api_key.trim();
if !api_key.is_empty() {
gemini.api_key_encrypted = Some(
crate::encryption::encrypt(api_key)
.context("Failed to encrypt Gemini api_key")?,
);
}
}
}
if let Some(bodhi) = self.providers.bodhi.as_mut() {
if bodhi.credential_ref.is_some() {
bodhi.api_key_encrypted = None;
return Ok(());
}
let api_key = bodhi.api_key.trim();
if !api_key.is_empty() {
bodhi.api_key_encrypted = Some(
crate::encryption::encrypt(api_key)
.context("Failed to encrypt Bodhi api_key")?,
);
}
}
Ok(())
}
pub fn hydrate_provider_instance_api_keys_from_encrypted(&mut self) {
for (id, instance) in self.provider_instances.iter_mut() {
if instance.api_key.trim().is_empty() {
if let Some(encrypted) = instance.api_key_encrypted.as_deref() {
match crate::encryption::decrypt(encrypted) {
Ok(value) => instance.api_key = value,
Err(e) => {
tracing::warn!(instance_id = id, "Failed to decrypt api_key: {}", e)
}
}
}
}
}
}
pub fn refresh_provider_instance_api_keys_encrypted(&mut self) -> Result<()> {
for (id, instance) in self.provider_instances.iter_mut() {
if instance.credential_ref.is_some() {
instance.api_key_encrypted = None;
continue;
}
let api_key = instance.api_key.trim();
if !api_key.is_empty() {
instance.api_key_encrypted = Some(crate::encryption::encrypt(api_key).context(
format!("Failed to encrypt api_key for provider instance '{}'", id),
)?);
}
}
Ok(())
}
pub fn ensure_provider_instance_credentials_isolated(&mut self) -> Result<()> {
for (id, instance) in &mut self.provider_instances {
if instance.credential_ref.is_some() {
instance.api_key_encrypted = None;
continue;
}
if !instance.api_key.trim().is_empty() || instance.api_key_encrypted.is_some() {
anyhow::bail!(
"provider instance '{id}' secret requires credential transaction before persistence"
);
}
}
Ok(())
}
pub fn hydrate_mcp_secrets_from_encrypted(&mut self) {
for server in self.mcp.servers.iter_mut() {
match &mut server.transport {
bamboo_domain::mcp_config::TransportConfig::Stdio(stdio) => {
if stdio.env_encrypted.is_empty() {
continue;
}
for (key, encrypted) in stdio.env_encrypted.clone() {
let should_hydrate = stdio
.env
.get(&key)
.map(|v| v.trim().is_empty())
.unwrap_or(true);
if !should_hydrate {
continue;
}
match crate::encryption::decrypt(&encrypted) {
Ok(value) => {
stdio.env.insert(key, value);
}
Err(e) => tracing::warn!("Failed to decrypt MCP stdio env var: {}", e),
}
}
}
bamboo_domain::mcp_config::TransportConfig::Sse(sse) => {
for header in sse.headers.iter_mut() {
if !header.value.trim().is_empty() {
continue;
}
let Some(encrypted) = header.value_encrypted.as_deref() else {
continue;
};
match crate::encryption::decrypt(encrypted) {
Ok(value) => header.value = value,
Err(e) => {
tracing::warn!("Failed to decrypt MCP SSE header value: {}", e)
}
}
}
}
bamboo_domain::mcp_config::TransportConfig::StreamableHttp(sh) => {
for header in sh.headers.iter_mut() {
if !header.value.trim().is_empty() {
continue;
}
let Some(encrypted) = header.value_encrypted.as_deref() else {
continue;
};
match crate::encryption::decrypt(encrypted) {
Ok(value) => header.value = value,
Err(e) => {
tracing::warn!(
"Failed to decrypt MCP StreamableHTTP header value: {}",
e
)
}
}
}
}
}
}
}
pub fn hydrate_mcp_credentials_from_store(
&mut self,
data_dir: &std::path::Path,
) -> crate::ConfigStoreResult<()> {
let store = crate::CredentialStore::open(data_dir);
self.hydrate_mcp_credentials_from_resolver(&store)
}
pub(crate) fn hydrate_mcp_credentials_from_snapshot(
&mut self,
snapshot: &crate::credential_store::CredentialDocumentLkg,
) -> crate::ConfigStoreResult<()> {
self.hydrate_mcp_credentials_from_resolver(snapshot)
}
fn hydrate_mcp_credentials_from_resolver(
&mut self,
resolver: &impl CredentialResolver,
) -> crate::ConfigStoreResult<()> {
for server in &mut self.mcp.servers {
match &mut server.transport {
bamboo_domain::mcp_config::TransportConfig::Stdio(stdio) => {
for (name, raw_reference) in &stdio.env_credential_refs {
if stdio.env.get(name).is_some_and(|value| !value.is_empty()) {
continue;
}
let reference = crate::CredentialRef::parse(raw_reference.clone())?;
let secret = resolver.resolve(&reference)?.ok_or_else(|| {
crate::ConfigStoreError::Validation(
"referenced MCP credential is unavailable".to_string(),
)
})?;
stdio.env.insert(name.clone(), secret.expose().to_string());
}
}
bamboo_domain::mcp_config::TransportConfig::Sse(config) => {
hydrate_header_credentials(resolver, &mut config.headers)?;
}
bamboo_domain::mcp_config::TransportConfig::StreamableHttp(config) => {
hydrate_header_credentials(resolver, &mut config.headers)?;
}
}
}
Ok(())
}
pub fn refresh_mcp_secrets_encrypted(&mut self) -> Result<()> {
for server in self.mcp.servers.iter_mut() {
match &mut server.transport {
bamboo_domain::mcp_config::TransportConfig::Stdio(stdio) => {
stdio.env_encrypted.clear();
for (key, value) in &stdio.env {
let encrypted = crate::encryption::encrypt(value).with_context(|| {
format!("Failed to encrypt MCP stdio env var '{key}'")
})?;
stdio.env_encrypted.insert(key.clone(), encrypted);
}
}
bamboo_domain::mcp_config::TransportConfig::Sse(sse) => {
for header in sse.headers.iter_mut() {
let configured = !header.value.trim().is_empty();
header.value_encrypted = if !configured {
None
} else {
Some(crate::encryption::encrypt(&header.value).with_context(|| {
format!("Failed to encrypt MCP SSE header '{}'", header.name)
})?)
};
}
}
bamboo_domain::mcp_config::TransportConfig::StreamableHttp(sh) => {
for header in sh.headers.iter_mut() {
let configured = !header.value.trim().is_empty();
header.value_encrypted = if !configured {
None
} else {
Some(crate::encryption::encrypt(&header.value).with_context(|| {
format!(
"Failed to encrypt MCP StreamableHTTP header '{}'",
header.name
)
})?)
};
}
}
}
}
Ok(())
}
pub fn sanitize_mcp_credential_refs_for_disk(&mut self) {
for server in &mut self.mcp.servers {
match &mut server.transport {
bamboo_domain::mcp_config::TransportConfig::Stdio(stdio) => {
for name in stdio
.env_credential_refs
.keys()
.cloned()
.collect::<Vec<_>>()
{
stdio.env.remove(&name);
stdio.env_encrypted.remove(&name);
}
}
bamboo_domain::mcp_config::TransportConfig::Sse(config) => {
for header in &mut config.headers {
if header.credential_ref.is_some() {
header.value.clear();
header.value_encrypted = None;
}
}
}
bamboo_domain::mcp_config::TransportConfig::StreamableHttp(config) => {
for header in &mut config.headers {
if header.credential_ref.is_some() {
header.value.clear();
header.value_encrypted = None;
}
}
}
}
}
}
pub fn hydrate_env_vars_from_encrypted(&mut self) {
for entry in &mut self.env_vars {
if !entry.secret {
continue;
}
if !entry.value.trim().is_empty() {
continue;
}
let Some(encrypted) = &entry.value_encrypted else {
continue;
};
match crate::encryption::decrypt(encrypted) {
Ok(value) => entry.value = value,
Err(e) => tracing::warn!("Failed to decrypt env var '{}': {}", entry.name, e),
}
}
}
pub fn hydrate_env_var_credentials_from_store(
&mut self,
data_dir: &std::path::Path,
) -> crate::ConfigStoreResult<()> {
let store = crate::CredentialStore::open(data_dir);
self.hydrate_env_var_credentials_from_resolver(&store)
}
pub(crate) fn hydrate_env_var_credentials_from_snapshot(
&mut self,
snapshot: &crate::credential_store::CredentialDocumentLkg,
) -> crate::ConfigStoreResult<()> {
self.hydrate_env_var_credentials_from_resolver(snapshot)
}
fn hydrate_env_var_credentials_from_resolver(
&mut self,
resolver: &impl CredentialResolver,
) -> crate::ConfigStoreResult<()> {
for entry in &mut self.env_vars {
if !entry.secret {
entry.credential_ref = None;
entry.configured = !entry.value.is_empty();
continue;
}
let Some(reference) = entry.credential_ref.as_ref() else {
entry.configured = false;
continue;
};
match resolver.resolve(reference)? {
Some(secret) => {
entry.value = secret.expose().to_string();
entry.configured = true;
}
None if entry.configured => {
return Err(crate::ConfigStoreError::Validation(
"referenced env credential is unavailable".to_string(),
));
}
None => {
entry.value.clear();
}
}
}
Ok(())
}
pub fn refresh_env_vars_encrypted(&mut self) -> Result<()> {
for entry in &mut self.env_vars {
if entry.secret && entry.credential_ref.is_some() {
entry.value_encrypted = None;
} else if entry.secret && !entry.value.trim().is_empty() {
entry.value_encrypted = Some(
crate::encryption::encrypt(&entry.value)
.with_context(|| format!("Failed to encrypt env var '{}'", entry.name))?,
);
} else if !entry.secret {
entry.value_encrypted = None;
}
}
Ok(())
}
pub fn sanitize_env_vars_for_disk(&mut self) {
for entry in &mut self.env_vars {
if entry.secret {
entry.value = String::new();
entry.value_encrypted = None;
} else {
entry.credential_ref = None;
entry.configured = !entry.value.is_empty();
}
}
}
pub fn hydrate_broker_token_from_encrypted(&mut self) {
let Some(broker) = self.subagents.broker.as_mut() else {
return;
};
if !broker.token.trim().is_empty() {
return; }
if let Some(encrypted) = &broker.token_encrypted {
match crate::encryption::decrypt(encrypted) {
Ok(value) => broker.token = value,
Err(e) => tracing::warn!("Failed to decrypt broker token: {}", e),
}
}
}
pub fn refresh_broker_token_encrypted(&mut self) -> Result<()> {
let Some(broker) = self.subagents.broker.as_mut() else {
return Ok(());
};
if broker.token.trim().is_empty() {
return Ok(());
}
broker.token_encrypted = Some(
crate::encryption::encrypt(&broker.token).context("Failed to encrypt broker token")?,
);
Ok(())
}
pub fn sanitize_broker_token_for_disk(&mut self) {
if let Some(broker) = self.subagents.broker.as_mut() {
broker.token = String::new();
}
}
pub fn hydrate_notifications_from_encrypted(&mut self) {
let ntfy = &mut self.notifications.ntfy;
if ntfy
.token
.as_deref()
.map(str::trim)
.unwrap_or("")
.is_empty()
{
if let Some(encrypted) = ntfy.token_encrypted.as_deref() {
match crate::encryption::decrypt(encrypted) {
Ok(value) => ntfy.token = Some(value),
Err(e) => tracing::warn!("Failed to decrypt ntfy token: {}", e),
}
}
}
let bark = &mut self.notifications.bark;
if bark
.device_key
.as_deref()
.map(str::trim)
.unwrap_or("")
.is_empty()
{
if let Some(encrypted) = bark.device_key_encrypted.as_deref() {
match crate::encryption::decrypt(encrypted) {
Ok(value) => bark.device_key = Some(value),
Err(e) => tracing::warn!("Failed to decrypt Bark device key: {}", e),
}
}
}
}
pub fn hydrate_notification_credentials_from_store(
&mut self,
data_dir: &std::path::Path,
) -> crate::ConfigStoreResult<()> {
let store = crate::CredentialStore::open(data_dir);
self.hydrate_notification_credentials_from_resolver(&store)
}
pub(crate) fn hydrate_notification_credentials_from_snapshot(
&mut self,
snapshot: &crate::credential_store::CredentialDocumentLkg,
) -> crate::ConfigStoreResult<()> {
self.hydrate_notification_credentials_from_resolver(snapshot)
}
fn hydrate_notification_credentials_from_resolver(
&mut self,
resolver: &impl CredentialResolver,
) -> crate::ConfigStoreResult<()> {
let reference_counts = crate::credential_store::config_credential_ref_counts(self)?;
for reference in [
self.notifications.ntfy.credential_ref.as_ref(),
self.notifications.bark.credential_ref.as_ref(),
]
.into_iter()
.flatten()
{
if reference_counts.get(reference).copied() != Some(1) {
return Err(crate::ConfigStoreError::Validation(
"notification credential reference is shared by another config consumer"
.to_string(),
));
}
}
let ntfy = &mut self.notifications.ntfy;
if let Some(reference) = ntfy.credential_ref.as_ref() {
match resolver.resolve(reference)? {
Some(secret) => {
ntfy.token = Some(secret.expose().to_string());
ntfy.configured = true;
}
None if ntfy.configured => {
return Err(crate::ConfigStoreError::Validation(
"referenced ntfy credential is unavailable".to_string(),
));
}
None => ntfy.token = None,
}
} else if ntfy.configured {
return Err(crate::ConfigStoreError::Validation(
"configured ntfy credential reference is missing".to_string(),
));
}
let bark = &mut self.notifications.bark;
if let Some(reference) = bark.credential_ref.as_ref() {
match resolver.resolve(reference)? {
Some(secret) => {
bark.device_key = Some(secret.expose().to_string());
bark.configured = true;
}
None if bark.configured => {
return Err(crate::ConfigStoreError::Validation(
"referenced Bark credential is unavailable".to_string(),
));
}
None => bark.device_key = None,
}
} else if bark.configured {
return Err(crate::ConfigStoreError::Validation(
"configured Bark credential reference is missing".to_string(),
));
}
Ok(())
}
pub fn refresh_notifications_encrypted(&mut self) -> Result<()> {
let ntfy = &mut self.notifications.ntfy;
if ntfy.credential_ref.is_some() {
ntfy.token_encrypted = None;
ntfy.configured = ntfy
.token
.as_deref()
.is_some_and(|value| !value.trim().is_empty())
|| ntfy.configured;
} else {
let token = ntfy.token.as_deref().unwrap_or("").trim();
if !token.is_empty() {
ntfy.token_encrypted = Some(
crate::encryption::encrypt(token).context("Failed to encrypt ntfy token")?,
);
}
}
let bark = &mut self.notifications.bark;
if bark.credential_ref.is_some() {
bark.device_key_encrypted = None;
bark.configured = bark
.device_key
.as_deref()
.is_some_and(|value| !value.trim().is_empty())
|| bark.configured;
} else {
let device_key = bark.device_key.as_deref().unwrap_or("").trim();
if !device_key.is_empty() {
bark.device_key_encrypted = Some(
crate::encryption::encrypt(device_key)
.context("Failed to encrypt Bark device key")?,
);
}
}
Ok(())
}
pub fn sanitize_notifications_for_disk(&mut self) {
self.notifications.ntfy.token = None;
self.notifications.ntfy.token_encrypted = None;
self.notifications.bark.device_key = None;
self.notifications.bark.device_key_encrypted = None;
}
pub fn hydrate_connect_platform_tokens_from_encrypted(&mut self) {
for platform in &mut self.connect.platforms {
let has_plaintext = platform
.token
.as_deref()
.map(str::trim)
.map(|value| !value.is_empty())
.unwrap_or(false);
if !has_plaintext {
if let Some(encrypted) = platform.token_encrypted.as_deref() {
match crate::encryption::decrypt(encrypted) {
Ok(value) => platform.token = Some(value),
Err(e) => tracing::warn!(
"Failed to decrypt connect platform '{}' token: {}",
platform.platform_type,
e
),
}
}
}
let has_app_secret_plaintext = platform
.app_secret
.as_deref()
.map(str::trim)
.map(|value| !value.is_empty())
.unwrap_or(false);
if !has_app_secret_plaintext {
if let Some(encrypted) = platform.app_secret_encrypted.as_deref() {
match crate::encryption::decrypt(encrypted) {
Ok(value) => platform.app_secret = Some(value),
Err(e) => tracing::warn!(
"Failed to decrypt connect platform '{}' app_secret: {}",
platform.platform_type,
e
),
}
}
}
}
}
pub fn hydrate_connect_credentials_from_store(
&mut self,
data_dir: &std::path::Path,
) -> crate::ConfigStoreResult<()> {
let store = crate::CredentialStore::open(data_dir);
let allow_legacy_runtime_value = !crate::section_layout_is_active(data_dir)?;
self.hydrate_connect_credentials_from_resolver(&store, allow_legacy_runtime_value)
}
pub(crate) fn hydrate_connect_credentials_from_snapshot(
&mut self,
snapshot: &crate::credential_store::CredentialDocumentLkg,
) -> crate::ConfigStoreResult<()> {
self.hydrate_connect_credentials_from_resolver(snapshot, false)
}
fn hydrate_connect_credentials_from_resolver(
&mut self,
resolver: &impl CredentialResolver,
allow_legacy_runtime_value: bool,
) -> crate::ConfigStoreResult<()> {
for platform in &mut self.connect.platforms {
hydrate_optional_connect_secret(
resolver,
platform.token_credential_ref.as_ref(),
platform.token_configured,
&mut platform.token,
allow_legacy_runtime_value,
)?;
hydrate_optional_connect_secret(
resolver,
platform.app_secret_credential_ref.as_ref(),
platform.app_secret_configured,
&mut platform.app_secret,
allow_legacy_runtime_value,
)?;
platform.token_encrypted = None;
platform.app_secret_encrypted = None;
}
Ok(())
}
pub fn hydrate_access_control_credentials_from_store(
&mut self,
data_dir: &std::path::Path,
) -> crate::ConfigStoreResult<()> {
let store = crate::CredentialStore::open(data_dir);
self.hydrate_access_control_credentials_from_resolver(&store)
}
pub(crate) fn hydrate_access_control_credentials_from_snapshot(
&mut self,
snapshot: &crate::credential_store::CredentialDocumentLkg,
) -> crate::ConfigStoreResult<()> {
self.hydrate_access_control_credentials_from_resolver(snapshot)
}
fn hydrate_access_control_credentials_from_resolver(
&mut self,
resolver: &impl CredentialResolver,
) -> crate::ConfigStoreResult<()> {
let Some(access) = self.access_control.as_mut() else {
return Ok(());
};
match access.password_credential_ref.as_ref() {
Some(reference) => {
if reference != &access_password_credential_ref()? || !access.password_configured {
return Err(crate::ConfigStoreError::Validation(
"access-control password credential metadata is invalid".to_string(),
));
}
let secret = resolver.resolve(reference)?.ok_or_else(|| {
crate::ConfigStoreError::Validation(
"access-control password verifier is unavailable".to_string(),
)
})?;
let record = decode_access_verifier(secret.expose())?;
access.password_hash = Some(record.hash);
access.password_salt = Some(record.salt);
}
None if access.password_configured || access.password_enabled => {
return Err(crate::ConfigStoreError::Validation(
"access-control password verifier metadata is incomplete".to_string(),
));
}
None => {
access.password_hash = None;
access.password_salt = None;
}
}
for device in &mut access.devices {
let reference = device.token_credential_ref.as_ref().ok_or_else(|| {
crate::ConfigStoreError::Validation(
"access-control device verifier metadata is incomplete".to_string(),
)
})?;
if reference != &access_device_credential_ref(&device.device_id)?
|| !device.token_configured
{
return Err(crate::ConfigStoreError::Validation(
"access-control device credential metadata is invalid".to_string(),
));
}
let secret = resolver.resolve(reference)?.ok_or_else(|| {
crate::ConfigStoreError::Validation(
"access-control device verifier is unavailable".to_string(),
)
})?;
let record = decode_access_verifier(secret.expose())?;
device.token_hash = record.hash;
device.token_salt = record.salt;
}
Ok(())
}
pub fn clear_access_control_runtime_verifiers(&mut self) {
if let Some(access) = self.access_control.as_mut() {
access.password_hash = None;
access.password_salt = None;
for device in &mut access.devices {
device.token_hash.clear();
device.token_salt.clear();
}
}
}
pub fn sanitize_access_control_for_disk(&mut self) {
self.clear_access_control_runtime_verifiers();
}
pub fn refresh_connect_platform_tokens_encrypted(&mut self) -> Result<()> {
for platform in &mut self.connect.platforms {
if platform.token_credential_ref.is_some() {
platform.token_encrypted = None;
}
let token = platform.token.as_deref().unwrap_or("").trim();
if platform.token_credential_ref.is_none() && !token.is_empty() {
platform.token_encrypted =
Some(crate::encryption::encrypt(token).with_context(|| {
format!(
"Failed to encrypt connect platform '{}' token",
platform.platform_type
)
})?);
}
if platform.app_secret_credential_ref.is_some() {
platform.app_secret_encrypted = None;
}
let app_secret = platform.app_secret.as_deref().unwrap_or("").trim();
if platform.app_secret_credential_ref.is_none() && !app_secret.is_empty() {
platform.app_secret_encrypted =
Some(crate::encryption::encrypt(app_secret).with_context(|| {
format!(
"Failed to encrypt connect platform '{}' app_secret",
platform.platform_type
)
})?);
}
}
Ok(())
}
pub fn sanitize_connect_credentials_for_disk(&mut self) {
for platform in &mut self.connect.platforms {
platform.token = None;
platform.token_encrypted = None;
platform.app_secret = None;
platform.app_secret_encrypted = None;
}
}
pub fn preserve_env_sourced_provider_keys(&mut self, previous: &Config) {
macro_rules! restore_env_key {
($field:ident) => {
if let (Some(current), Some(prev)) = (
self.providers.$field.as_mut(),
previous.providers.$field.as_ref(),
) {
if prev.api_key_from_env && current.api_key.trim().is_empty() {
current.api_key = prev.api_key.clone();
current.api_key_from_env = true;
}
}
};
}
restore_env_key!(openai);
restore_env_key!(anthropic);
restore_env_key!(gemini);
}
pub fn preserve_provider_instance_plaintext_keys(
&mut self,
previous: &Config,
intents: &ProviderApiKeyIntents,
) {
for (id, instance) in self.provider_instances.iter_mut() {
if intents.provider_instances.contains(id) {
continue;
}
if !instance.api_key.trim().is_empty() || instance.api_key_encrypted.is_some() {
continue;
}
if let Some(previous) = previous.provider_instances.get(id) {
if !previous.api_key.trim().is_empty() || previous.api_key_encrypted.is_some() {
instance.api_key = previous.api_key.clone();
instance.api_key_encrypted = previous.api_key_encrypted.clone();
}
}
}
}
pub fn refresh_encrypted_secrets(&mut self) -> Result<()> {
self.refresh_proxy_auth_encrypted()?;
self.refresh_provider_api_keys_encrypted()?;
self.refresh_provider_instance_api_keys_encrypted()?;
self.refresh_env_vars_encrypted()?;
self.refresh_cluster_fabric_encrypted()?;
self.refresh_notifications_encrypted()?;
self.refresh_connect_platform_tokens_encrypted()?;
Ok(())
}
}
fn hydrate_optional_connect_secret(
resolver: &impl CredentialResolver,
reference: Option<&crate::CredentialRef>,
configured: bool,
target: &mut Option<String>,
allow_legacy_runtime_value: bool,
) -> crate::ConfigStoreResult<()> {
match reference {
Some(reference) => match resolver.resolve(reference)? {
Some(secret) => *target = Some(secret.expose().to_string()),
None if configured => {
return Err(crate::ConfigStoreError::Validation(
"referenced connect credential is unavailable".to_string(),
));
}
None => *target = None,
},
None if configured => {
return Err(crate::ConfigStoreError::Validation(
"connect credential metadata is inconsistent".to_string(),
));
}
None if allow_legacy_runtime_value && target.is_some() => {}
None => *target = None,
}
Ok(())
}