pub mod auth;
pub mod cache;
pub use auth::authenticate;
pub use cache::SecretCache;
use crate::did_secrets::DidSecretsBundle;
use crate::error::VtaError;
use std::time::Duration;
const DEFAULT_STARTUP_TIMEOUT: Duration = Duration::from_secs(30);
#[derive(Clone, Debug)]
pub struct VtaServiceConfig {
pub credential: String,
pub context: String,
pub url_override: Option<String>,
pub timeout: Option<Duration>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SecretSource {
Vta,
Cache,
}
pub struct StartupResult {
pub did: String,
pub bundle: DidSecretsBundle,
pub source: SecretSource,
pub client: Option<crate::client::VtaClient>,
}
#[derive(Debug)]
pub enum VtaIntegrationError {
NoCachedSecrets,
EmptySecretsBundle(String),
CacheError(String),
Vta(VtaError),
}
impl std::fmt::Display for VtaIntegrationError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::NoCachedSecrets => write!(
f,
"VTA is unreachable and no cached secrets exist. \
Run the setup wizard or ensure the VTA is accessible for the first startup."
),
Self::EmptySecretsBundle(ctx) => write!(
f,
"VTA context '{ctx}' returned zero secrets. \
Provision keys via the setup wizard or VTA admin tools."
),
Self::CacheError(e) => write!(f, "secret cache error: {e}"),
Self::Vta(e) => write!(f, "VTA error: {e}"),
}
}
}
impl std::error::Error for VtaIntegrationError {}
impl From<VtaError> for VtaIntegrationError {
fn from(e: VtaError) -> Self {
Self::Vta(e)
}
}
pub async fn startup(
config: &VtaServiceConfig,
cache: &(impl SecretCache + ?Sized),
) -> Result<StartupResult, VtaIntegrationError> {
let timeout = config.timeout.unwrap_or(DEFAULT_STARTUP_TIMEOUT);
let vta_result = tokio::time::timeout(timeout, async {
let client = authenticate(config).await?;
let bundle = client
.fetch_did_secrets_bundle(&config.context)
.await
.map_err(VtaIntegrationError::from)?;
Ok::<_, VtaIntegrationError>((client, bundle))
})
.await;
match vta_result {
Ok(Ok((client, bundle))) => {
if bundle.secrets.is_empty() {
return Err(VtaIntegrationError::EmptySecretsBundle(
config.context.clone(),
));
}
if let Err(e) = cache.store(&bundle).await {
tracing::warn!("Failed to cache VTA secrets locally: {e}");
}
tracing::info!(
context = config.context,
secrets = bundle.secrets.len(),
"Loaded fresh secrets from VTA",
);
Ok(StartupResult {
did: bundle.did.clone(),
bundle,
source: SecretSource::Vta,
client: Some(client),
})
}
Ok(Err(e)) => {
tracing::warn!("VTA startup failed: {e}");
load_from_cache(cache, &config.context).await
}
Err(_elapsed) => {
tracing::warn!(
"VTA startup timed out after {}s, falling back to cached secrets",
timeout.as_secs()
);
load_from_cache(cache, &config.context).await
}
}
}
async fn load_from_cache(
cache: &(impl SecretCache + ?Sized),
context: &str,
) -> Result<StartupResult, VtaIntegrationError> {
match cache.load().await {
Ok(Some(bundle)) => {
if bundle.secrets.is_empty() {
return Err(VtaIntegrationError::EmptySecretsBundle(context.to_string()));
}
tracing::warn!(
context = context,
secrets = bundle.secrets.len(),
"Using CACHED secrets — keys may be stale",
);
Ok(StartupResult {
did: bundle.did.clone(),
bundle,
source: SecretSource::Cache,
client: None,
})
}
Ok(None) => Err(VtaIntegrationError::NoCachedSecrets),
Err(e) => Err(VtaIntegrationError::CacheError(e.to_string())),
}
}