use crate::client::VtaClient;
use crate::error::VtaError;
#[cfg(feature = "session")]
use super::TransportPreference;
use super::VtaServiceConfig;
#[cfg(feature = "session")]
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) enum TransportPlan {
DidCommThenRest { mediator_did: String },
DidCommOnly { mediator_did: String },
RestOnly,
DidCommUnavailable,
}
#[cfg(feature = "session")]
pub(crate) fn decide_transport(
preference: TransportPreference,
mediator_did: Option<&str>,
) -> TransportPlan {
match (preference, mediator_did) {
(TransportPreference::PreferRest, _) => TransportPlan::RestOnly,
(TransportPreference::DidCommOnly, None) => TransportPlan::DidCommUnavailable,
(TransportPreference::DidCommOnly, Some(m)) => TransportPlan::DidCommOnly {
mediator_did: m.to_string(),
},
(TransportPreference::Auto, None) => TransportPlan::RestOnly,
(TransportPreference::Auto, Some(m)) => TransportPlan::DidCommThenRest {
mediator_did: m.to_string(),
},
}
}
pub async fn authenticate(config: &VtaServiceConfig) -> Result<VtaClient, VtaError> {
let url_override = config.auth.url_override.as_deref();
#[cfg(feature = "session")]
{
let effective_mediator = resolve_effective_mediator_did(config).await?;
let plan = decide_transport(
config.context.transport_preference,
effective_mediator.as_deref(),
);
let context_id = &config.context.id;
match plan {
TransportPlan::DidCommThenRest { mediator_did } => {
match try_didcomm(config, &mediator_did).await {
Ok(client) => return Ok(client),
Err(e) => {
tracing::warn!(
event = "transport_downgrade",
from = "didcomm",
to = "rest",
context = context_id,
mediator_did = %mediator_did,
error = %e,
"DIDComm connect failed; falling back to REST tiers",
);
}
}
}
TransportPlan::DidCommOnly { mediator_did } => {
return try_didcomm(config, &mediator_did).await.map_err(|e| {
tracing::error!(
event = "transport_didcomm_only_failed",
context = context_id,
mediator_did = %mediator_did,
error = %e,
"DIDComm connect failed and transport_preference is DidCommOnly; \
not falling back to REST",
);
e
});
}
TransportPlan::DidCommUnavailable => {
return Err(VtaError::Validation(
"transport_preference is DidCommOnly but no mediator_did is configured — \
set VtaContextConfig::mediator_did or relax to TransportPreference::Auto"
.into(),
));
}
TransportPlan::RestOnly => { }
}
}
try_rest(config, url_override).await
}
#[cfg(feature = "session")]
async fn resolve_effective_mediator_did(
config: &VtaServiceConfig,
) -> Result<Option<String>, VtaError> {
if let Some(m) = config.context.mediator_did.as_deref() {
return Ok(Some(m.to_string()));
}
if matches!(
config.context.transport_preference,
TransportPreference::PreferRest
) {
return Ok(None);
}
let vta_did = &config.auth.credential.vta_did;
let context_id = &config.context.id;
let result = match config.context.did_resolver.as_ref() {
Some(resolver) => {
crate::session::resolve_mediator_did_with_resolver(vta_did, resolver.as_ref()).await
}
None => crate::session::resolve_mediator_did(vta_did).await,
};
match result {
Ok(Some(mediator)) => {
tracing::info!(
context = context_id,
vta_did = %vta_did,
mediator_did = %mediator,
"Auto-resolved mediator DID from VTA DID document",
);
Ok(Some(mediator))
}
Ok(None) => {
tracing::debug!(
context = context_id,
vta_did = %vta_did,
"VTA DID document has no DIDCommMessaging service; DIDComm tier unavailable",
);
Ok(None)
}
Err(e) => match config.context.transport_preference {
TransportPreference::DidCommOnly => Err(VtaError::UnsupportedTransport(format!(
"mediator DID auto-resolve failed and transport_preference is DidCommOnly \
(no REST fallback): {e}"
))),
_ => {
tracing::warn!(
context = context_id,
vta_did = %vta_did,
error = %e,
"Mediator DID auto-resolve failed; will try REST tiers",
);
Ok(None)
}
},
}
}
#[cfg(feature = "session")]
async fn try_didcomm(config: &VtaServiceConfig, mediator_did: &str) -> Result<VtaClient, VtaError> {
let credential = &config.auth.credential;
let client = VtaClient::connect_didcomm(
&credential.did,
&credential.private_key_multibase,
&credential.vta_did,
mediator_did,
credential.vta_url.clone(),
)
.await?;
tracing::info!(
context = config.context.id,
mediator_did = %mediator_did,
vta_did = %credential.vta_did,
"Connected to VTA (DIDComm)",
);
Ok(client)
}
async fn try_rest(
config: &VtaServiceConfig,
url_override: Option<&str>,
) -> Result<VtaClient, VtaError> {
let context_id = &config.context.id;
match VtaClient::from_credential(&config.auth.credential, url_override).await {
Ok(client) => {
tracing::info!(
context = context_id,
vta_url = client.endpoint_label(),
"Connected to VTA (REST, auto-refresh enabled)",
);
Ok(client)
}
Err(e) if e.is_network() => Err(e),
Err(e) => {
tracing::debug!(
context = context_id,
error = %e,
"Lightweight REST auth failed (non-network); trying session REST",
);
let credential = &config.auth.credential;
let vta_url = url_override
.or(credential.vta_url.as_deref())
.ok_or_else(|| {
VtaError::Validation("VTA URL not found in credential or url_override".into())
})?;
let token_result = crate::session::challenge_response(
vta_url,
&credential.did,
&credential.private_key_multibase,
&credential.vta_did,
)
.await
.map_err(|e| VtaError::Auth(format!("session challenge-response failed: {e}")))?;
let client = VtaClient::new(vta_url);
client.set_token_async(token_result.access_token).await;
tracing::info!(
context = context_id,
vta_url = vta_url,
"Connected to VTA (REST, session auth)",
);
Ok(client)
}
}
}
#[cfg(all(test, feature = "session"))]
mod tests {
use super::*;
#[test]
fn plan_auto_with_mediator_picks_didcomm_then_rest() {
let plan = decide_transport(TransportPreference::Auto, Some("did:key:zMed"));
assert_eq!(
plan,
TransportPlan::DidCommThenRest {
mediator_did: "did:key:zMed".into()
}
);
}
#[test]
fn plan_auto_without_mediator_picks_rest_only() {
let plan = decide_transport(TransportPreference::Auto, None);
assert_eq!(plan, TransportPlan::RestOnly);
}
#[test]
fn plan_prefer_rest_ignores_mediator() {
assert_eq!(
decide_transport(TransportPreference::PreferRest, Some("did:key:zMed")),
TransportPlan::RestOnly
);
assert_eq!(
decide_transport(TransportPreference::PreferRest, None),
TransportPlan::RestOnly
);
}
#[test]
fn plan_didcomm_only_with_mediator_does_not_fall_back() {
let plan = decide_transport(TransportPreference::DidCommOnly, Some("did:key:zMed"));
assert_eq!(
plan,
TransportPlan::DidCommOnly {
mediator_did: "did:key:zMed".into()
}
);
}
#[test]
fn plan_didcomm_only_without_mediator_errors() {
let plan = decide_transport(TransportPreference::DidCommOnly, None);
assert_eq!(plan, TransportPlan::DidCommUnavailable);
}
}