use std::sync::Arc;
use tokio::sync::mpsc::{self, UnboundedSender};
use super::ask::ProvisionAsk;
use super::diagnostics::{DiagCheck, DiagStatus, Protocol};
use super::error::ProvisionError;
use super::event::{AttemptOutcome, AttemptResultKind, VtaEvent};
use super::intent::{VtaIntent, VtaReply};
use super::messages::OperatorMessages;
use super::resolve::{ResolvedVta, resolve_vta};
use super::result::ProvisionResult;
use super::runner_didcomm::{run_didcomm_attempt, run_provision_flight};
use super::runner_rest::{
run_rest_attempt_admin_only, run_rest_attempt_admin_rotated, run_rest_attempt_full_setup,
};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum InitialChoice {
BothAvailable,
DIDCommOnly,
RestOnly,
Neither,
}
pub fn select_initial_transport(resolved: &ResolvedVta) -> InitialChoice {
match (resolved.mediator_did.is_some(), resolved.rest_url.is_some()) {
(true, true) => InitialChoice::BothAvailable,
(true, false) => InitialChoice::DIDCommOnly,
(false, true) => InitialChoice::RestOnly,
(false, false) => InitialChoice::Neither,
}
}
pub async fn run_connection_test(
intent: VtaIntent,
vta_did: String,
setup_did: String,
setup_privkey_mb: String,
ask: ProvisionAsk,
force_transport: Option<Protocol>,
tx: UnboundedSender<VtaEvent>,
) {
let _ = tx.send(VtaEvent::CheckStart(DiagCheck::ResolveDid));
let resolved = match resolve_vta(&vta_did).await {
Ok(r) => {
let detail = match (&r.mediator_did, &r.rest_url) {
(Some(m), _) => format!("mediator DID: {m}"),
(None, Some(u)) => format!("REST: {u}"),
(None, None) => "resolved (no endpoints)".into(),
};
let _ = tx.send(VtaEvent::CheckDone(
DiagCheck::ResolveDid,
DiagStatus::Ok(detail),
));
let _ = tx.send(VtaEvent::Resolved(r.clone()));
r
}
Err(e) => {
let _ = tx.send(VtaEvent::CheckDone(
DiagCheck::ResolveDid,
DiagStatus::Failed(e.to_string()),
));
let _ = tx.send(VtaEvent::Failed(format!(
"Could not resolve {vta_did}. Verify the DID is correct and its \
publication endpoint is reachable."
)));
return;
}
};
let _ = tx.send(VtaEvent::CheckStart(DiagCheck::EnumerateServices));
let rest_url = resolved.rest_url.clone();
let mediator_did_opt = resolved.mediator_did.clone();
let enum_detail = format!(
"REST: {}, DIDCommMessaging: {}",
if rest_url.is_some() { "yes" } else { "no" },
if mediator_did_opt.is_some() {
"yes"
} else {
"no"
},
);
let auto_choice = select_initial_transport(&resolved);
let choice = match force_transport {
Some(Protocol::Rest) if resolved.rest_url.is_some() => InitialChoice::RestOnly,
Some(Protocol::DidComm) if resolved.mediator_did.is_some() => InitialChoice::DIDCommOnly,
_ => auto_choice,
};
if matches!(choice, InitialChoice::Neither) {
let _ = tx.send(VtaEvent::CheckDone(
DiagCheck::EnumerateServices,
DiagStatus::Failed(enum_detail),
));
let _ = tx.send(VtaEvent::CheckDone(
DiagCheck::AuthenticateDIDComm,
DiagStatus::Skipped("no DIDComm endpoint".into()),
));
let _ = tx.send(VtaEvent::CheckDone(
DiagCheck::AuthenticateREST,
DiagStatus::Skipped("no REST endpoint".into()),
));
let _ = tx.send(VtaEvent::CheckDone(
DiagCheck::ListWebvhServers,
DiagStatus::Skipped("no transport".into()),
));
let _ = tx.send(VtaEvent::CheckDone(
DiagCheck::ProvisionIntegration,
DiagStatus::Skipped("no transport".into()),
));
let _ = tx.send(VtaEvent::Failed(
"VTA DID document advertises neither a DIDComm mediator endpoint \
nor a REST endpoint. Use the offline sealed-handoff flow."
.into(),
));
return;
}
let _ = tx.send(VtaEvent::CheckDone(
DiagCheck::EnumerateServices,
DiagStatus::Ok(enum_detail),
));
match choice {
InitialChoice::BothAvailable | InitialChoice::DIDCommOnly => {
let mediator_did = mediator_did_opt.expect("DIDComm path requires mediator_did");
let rest_skip_msg = if matches!(choice, InitialChoice::BothAvailable) {
"DIDComm-first VTA — REST fallback handled by consumer"
} else {
"DIDComm-only VTA"
};
let _ = tx.send(VtaEvent::CheckDone(
DiagCheck::AuthenticateREST,
DiagStatus::Skipped(rest_skip_msg.into()),
));
let outcome = run_didcomm_attempt(
intent,
vta_did,
mediator_did.clone(),
rest_url.clone(),
setup_did,
setup_privkey_mb,
ask.clone(),
&tx,
)
.await;
match outcome {
AttemptOutcome::Connected(reply) => {
let _ = tx.send(VtaEvent::AttemptCompleted {
protocol: Protocol::DidComm,
outcome: AttemptResultKind::Connected,
});
let _ = tx.send(VtaEvent::Connected {
protocol: Protocol::DidComm,
rest_url,
mediator_did: Some(mediator_did),
reply,
});
}
AttemptOutcome::PreflightOk {
rest_url,
mediator_did,
servers,
} => {
let _ = tx.send(VtaEvent::PreflightDone {
rest_url,
mediator_did,
servers,
});
}
AttemptOutcome::PreAuthFailure(reason) => {
let _ = tx.send(VtaEvent::AttemptCompleted {
protocol: Protocol::DidComm,
outcome: AttemptResultKind::PreAuthFailure(reason.clone()),
});
let _ = tx.send(VtaEvent::Failed(reason));
}
AttemptOutcome::PostAuthFailure(reason) => {
let _ = tx.send(VtaEvent::AttemptCompleted {
protocol: Protocol::DidComm,
outcome: AttemptResultKind::PostAuthFailure(reason.clone()),
});
let _ = tx.send(VtaEvent::Failed(reason));
}
}
}
InitialChoice::RestOnly => {
let rest_url_str = rest_url.clone().expect("REST path requires rest_url");
let _ = tx.send(VtaEvent::CheckDone(
DiagCheck::AuthenticateDIDComm,
DiagStatus::Skipped("REST-only VTA".into()),
));
let outcome = match intent {
VtaIntent::AdminOnly => {
run_rest_attempt_admin_only(
&rest_url_str,
&vta_did,
setup_did,
setup_privkey_mb,
&tx,
)
.await
}
VtaIntent::FullSetup => {
run_rest_attempt_full_setup(
&rest_url_str,
&vta_did,
setup_did,
setup_privkey_mb,
ask,
&tx,
)
.await
}
VtaIntent::AdminRotated => {
run_rest_attempt_admin_rotated(
&rest_url_str,
&vta_did,
setup_did,
setup_privkey_mb,
ask,
&tx,
)
.await
}
};
match outcome {
AttemptOutcome::Connected(reply) => {
let _ = tx.send(VtaEvent::AttemptCompleted {
protocol: Protocol::Rest,
outcome: AttemptResultKind::Connected,
});
let _ = tx.send(VtaEvent::Connected {
protocol: Protocol::Rest,
rest_url,
mediator_did: None,
reply,
});
}
AttemptOutcome::PreflightOk { .. } => {
let _ = tx.send(VtaEvent::Failed(
"REST attempt produced an unexpected PreflightOk outcome — \
wiring bug; please report."
.into(),
));
}
AttemptOutcome::PreAuthFailure(reason) => {
let _ = tx.send(VtaEvent::AttemptCompleted {
protocol: Protocol::Rest,
outcome: AttemptResultKind::PreAuthFailure(reason.clone()),
});
let _ = tx.send(VtaEvent::Failed(reason));
}
AttemptOutcome::PostAuthFailure(reason) => {
let _ = tx.send(VtaEvent::AttemptCompleted {
protocol: Protocol::Rest,
outcome: AttemptResultKind::PostAuthFailure(reason.clone()),
});
let _ = tx.send(VtaEvent::Failed(reason));
}
}
}
InitialChoice::Neither => unreachable!("handled above"),
}
}
#[allow(clippy::too_many_arguments)]
pub async fn run_provision(
intent: VtaIntent,
vta_did: String,
setup_did: String,
setup_privkey_mb: String,
ask: ProvisionAsk,
force_transport: Option<Protocol>,
messages: Arc<dyn OperatorMessages>,
events: UnboundedSender<VtaEvent>,
) -> Result<VtaReply, ProvisionError> {
let (internal_tx, mut internal_rx) = mpsc::unbounded_channel();
let task_intent = intent;
let task_vta_did = vta_did.clone();
let task_setup_did = setup_did.clone();
let task_setup_pk = setup_privkey_mb.clone();
let task_ask = ask.clone();
tokio::spawn(async move {
run_connection_test(
task_intent,
task_vta_did,
task_setup_did,
task_setup_pk,
task_ask,
force_transport,
internal_tx,
)
.await;
});
while let Some(ev) = internal_rx.recv().await {
match ev {
VtaEvent::Connected {
protocol,
rest_url,
mediator_did,
reply,
} => {
let reply_clone = reply.clone();
let _ = events.send(VtaEvent::Connected {
protocol,
rest_url,
mediator_did,
reply,
});
return Ok(reply_clone);
}
VtaEvent::Failed(msg) => {
let _ = events.send(VtaEvent::Failed(msg.clone()));
return Err(ProvisionError::WorkflowFailed(msg));
}
VtaEvent::PreflightDone {
rest_url,
mediator_did,
servers,
} => {
let webvh_server_id = match servers.len() {
0 => None,
1 => Some(servers[0].id.clone()),
n => {
let msg = format!(
"VTA has {n} registered webvh servers; auto-pick is \
ambiguous. Use run_connection_test + run_provision_flight \
directly to drive an interactive picker."
);
let _ = events.send(VtaEvent::Failed(msg.clone()));
return Err(ProvisionError::WorkflowFailed(msg));
}
};
let mediator_did_clone = mediator_did.clone();
let rest_url_clone = rest_url.clone();
let _ = events.send(VtaEvent::PreflightDone {
rest_url,
mediator_did,
servers,
});
let (flight_tx, mut flight_rx) = mpsc::unbounded_channel();
let flight_messages = messages.clone();
let flight_vta_did = vta_did.clone();
let flight_setup_did = setup_did.clone();
let flight_setup_pk = setup_privkey_mb.clone();
let flight_ask = ask.clone();
tokio::spawn(async move {
run_provision_flight(
flight_vta_did,
flight_setup_did,
flight_setup_pk,
mediator_did_clone,
rest_url_clone,
flight_ask,
webvh_server_id,
None,
flight_messages,
flight_tx,
)
.await;
});
while let Some(fev) = flight_rx.recv().await {
match fev {
VtaEvent::Connected {
protocol,
rest_url,
mediator_did,
reply,
} => {
let reply_clone = reply.clone();
let _ = events.send(VtaEvent::Connected {
protocol,
rest_url,
mediator_did,
reply,
});
return Ok(reply_clone);
}
VtaEvent::Failed(msg) => {
let _ = events.send(VtaEvent::Failed(msg.clone()));
return Err(ProvisionError::WorkflowFailed(msg));
}
other => {
let _ = events.send(other);
}
}
}
return Err(ProvisionError::WorkflowAbandoned);
}
other => {
let _ = events.send(other);
}
}
}
Err(ProvisionError::WorkflowAbandoned)
}
pub async fn provision_via_rest(
rest_url: &str,
vta_did: &str,
setup_did: String,
setup_privkey_mb: String,
ask: ProvisionAsk,
) -> Result<ProvisionResult, ProvisionError> {
let (tx, _rx) = mpsc::unbounded_channel();
let outcome =
run_rest_attempt_full_setup(rest_url, vta_did, setup_did, setup_privkey_mb, ask, &tx).await;
match outcome {
AttemptOutcome::Connected(VtaReply::Full(result)) => Ok(*result),
AttemptOutcome::Connected(VtaReply::AdminOnly(_)) => Err(ProvisionError::WorkflowFailed(
"AdminOnly reply on FullSetup REST flow — wiring bug".into(),
)),
AttemptOutcome::PreflightOk { .. } => Err(ProvisionError::WorkflowFailed(
"REST flow produced PreflightOk — wiring bug".into(),
)),
AttemptOutcome::PreAuthFailure(reason) => Err(ProvisionError::WorkflowFailed(reason)),
AttemptOutcome::PostAuthFailure(reason) => Err(ProvisionError::WorkflowFailed(reason)),
}
}
#[cfg(test)]
mod tests {
use super::*;
fn resolved(mediator_did: Option<&str>, rest_url: Option<&str>) -> ResolvedVta {
ResolvedVta {
vta_did: "did:webvh:vta.test".into(),
mediator_did: mediator_did.map(str::to_string),
rest_url: rest_url.map(str::to_string),
}
}
#[test]
fn select_returns_both_when_both_advertised() {
let r = resolved(Some("did:webvh:mediator.test"), Some("https://vta.test"));
assert_eq!(select_initial_transport(&r), InitialChoice::BothAvailable);
}
#[test]
fn select_returns_didcomm_only_when_only_didcomm_advertised() {
let r = resolved(Some("did:webvh:mediator.test"), None);
assert_eq!(select_initial_transport(&r), InitialChoice::DIDCommOnly);
}
#[test]
fn select_returns_rest_only_when_only_rest_advertised() {
let r = resolved(None, Some("https://vta.test"));
assert_eq!(select_initial_transport(&r), InitialChoice::RestOnly);
}
#[test]
fn select_returns_neither_when_no_transport_advertised() {
let r = resolved(None, None);
assert_eq!(select_initial_transport(&r), InitialChoice::Neither);
}
}