use serde_json::json;
use vta_sdk::credentials::CredentialBundle;
use vta_sdk::did_key::secrets_from_bundle;
use vta_sdk::did_secrets::DidSecretsBundle;
use vta_sdk::integration::{SecretCache, VtaServiceConfig, startup};
use super::loaders::{environment::optional_env, load};
use super::secret_store;
type BoxError = Box<dyn std::error::Error + Send + Sync>;
pub struct VtaSecretCache;
impl SecretCache for VtaSecretCache {
async fn store(&self, bundle: &DidSecretsBundle) -> Result<(), BoxError> {
let json = serde_json::to_string(bundle).map_err(|e| Box::new(e) as BoxError)?;
let cfg = secret_store::secrets_config_from_env();
secret_store::write_profile(&cfg, &secret_store::data_dir(), &json)
.await
.map_err(BoxError::from)?;
Ok(())
}
async fn load(&self) -> Result<Option<DidSecretsBundle>, BoxError> {
let cfg = secret_store::secrets_config_from_env();
match secret_store::read_profile(&cfg, &secret_store::data_dir())
.await
.map_err(BoxError::from)?
{
Some(json) => Ok(Some(
serde_json::from_str(&json).map_err(|e| Box::new(e) as BoxError)?,
)),
None => Ok(None),
}
}
}
async fn vta_config_from_env() -> Result<Option<VtaServiceConfig>, String> {
let Some(credential_uri) = optional_env("TR_VTA_CREDENTIAL") else {
return Ok(None);
};
let context_id = optional_env("TR_VTA_CONTEXT_ID")
.ok_or_else(|| "TR_VTA_CREDENTIAL is set but TR_VTA_CONTEXT_ID is missing".to_string())?;
let credential_json = load(&credential_uri).await?;
let credential: CredentialBundle = serde_json::from_str(&credential_json)
.map_err(|e| format!("invalid TR_VTA_CREDENTIAL bundle: {e}"))?;
let mut config = VtaServiceConfig::new(credential, context_id);
if let Some(url) = optional_env("TR_VTA_URL") {
config.auth.url_override = Some(url);
}
Ok(Some(config))
}
pub async fn startup_profile_json() -> Result<Option<String>, String> {
let Some(config) = vta_config_from_env().await? else {
return Ok(None);
};
let cache = VtaSecretCache;
let result = startup(&config, &cache)
.await
.map_err(|e| format!("VTA startup failed: {e}"))?;
let secrets = secrets_from_bundle(&result.bundle)
.map_err(|e| format!("failed to decode VTA secrets bundle: {e}"))?;
let alias = optional_env("TR_ALIAS").unwrap_or_else(|| "Trust Registry".to_string());
let profile = json!({
"alias": alias,
"did": result.did,
"secrets": secrets,
});
Ok(Some(profile.to_string()))
}
pub async fn rotate_did(
did: &str,
pre_rotation_count: Option<u32>,
label: Option<String>,
) -> Result<(String, String, String), String> {
use vta_sdk::client::VtaClient;
use vta_sdk::protocols::did_management::update::RotateDidWebvhKeysBody;
let credential_uri = optional_env("TR_VTA_CREDENTIAL")
.ok_or_else(|| "VTA is not configured (TR_VTA_CREDENTIAL unset)".to_string())?;
did.strip_prefix("did:webvh:")
.and_then(|rest| rest.split(':').next())
.filter(|s| !s.is_empty())
.ok_or_else(|| format!("DID {did} is not a did:webvh; cannot rotate its keys"))?;
let credential_json = load(&credential_uri).await?;
let credential: CredentialBundle = serde_json::from_str(&credential_json)
.map_err(|e| format!("invalid TR_VTA_CREDENTIAL bundle: {e}"))?;
let url_override = optional_env("TR_VTA_URL");
let client = VtaClient::from_credential(&credential, url_override.as_deref())
.await
.map_err(|e| format!("VTA authentication failed: {e}"))?;
let body = RotateDidWebvhKeysBody {
pre_rotation_count,
label,
};
let result = client
.rotate_did_webvh_keys_by_did(did, body)
.await
.map_err(|e| format!("VTA did:webvh key rotation failed: {e}"))?;
Ok((result.did, result.new_scid, result.new_version_id))
}
#[cfg(test)]
mod tests {
use super::*;
use serial_test::serial;
fn clear_vta_env() {
for k in ["TR_VTA_CREDENTIAL", "TR_VTA_CONTEXT_ID", "TR_VTA_URL"] {
unsafe { std::env::remove_var(k) };
}
}
#[tokio::test]
#[serial]
async fn no_vta_configured_returns_none() {
clear_vta_env();
assert!(vta_config_from_env().await.expect("ok").is_none());
assert!(startup_profile_json().await.expect("ok").is_none());
}
#[tokio::test]
#[serial]
async fn credential_without_context_errors() {
clear_vta_env();
unsafe {
std::env::set_var(
"TR_VTA_CREDENTIAL",
r#"string://{"did":"did:web:svc","privateKeyMultibase":"z0","vtaDid":"did:web:vta"}"#,
);
}
assert!(vta_config_from_env().await.is_err());
clear_vta_env();
}
#[tokio::test]
#[serial]
async fn valid_credential_builds_config() {
clear_vta_env();
unsafe {
std::env::set_var(
"TR_VTA_CREDENTIAL",
r#"string://{"did":"did:web:svc","privateKeyMultibase":"z6Mkexample","vtaDid":"did:web:vta","vtaUrl":"https://vta.example"}"#,
);
std::env::set_var("TR_VTA_CONTEXT_ID", "ctx-1");
}
let cfg = vta_config_from_env().await.expect("ok").expect("some");
assert_eq!(cfg.context.id, "ctx-1");
assert_eq!(cfg.auth.credential.did, "did:web:svc");
clear_vta_env();
}
}