use std::sync::Arc;
use tokio::sync::mpsc::UnboundedSender;
use crate::did_key::decode_private_key_multibase;
use crate::didcomm_session::DIDCommSession;
use crate::protocols::did_management::servers::ListWebvhServersResultBody;
use crate::protocols::did_management::{LIST_WEBVH_SERVERS, LIST_WEBVH_SERVERS_RESULT};
use crate::provision_integration::didcomm::provision_integration_didcomm;
use super::ask::ProvisionAsk;
use super::diagnostics::{DiagCheck, DiagStatus, Protocol};
use super::error::ProvisionError;
use super::event::{AttemptOutcome, VtaEvent};
use super::intent::{AdminCredentialReply, VtaIntent, VtaReply};
use super::messages::OperatorMessages;
use super::result::{ProvisionResult, decode_nonce_b64url, response_to_result};
pub async fn provision_via_didcomm(
setup_did: &str,
setup_private_key_mb: &str,
vta_did: &str,
mediator_did: &str,
ask: &ProvisionAsk,
) -> Result<ProvisionResult, ProvisionError> {
let seed = decode_private_key_multibase(setup_private_key_mb)
.map_err(|e| ProvisionError::SetupKeyMalformed(e.to_string()))?;
let session = DIDCommSession::connect(setup_did, setup_private_key_mb, vta_did, mediator_did)
.await
.map_err(|e| ProvisionError::SessionOpen(e.to_string()))?;
let vp = ask.to_builder().sign_with(&seed, setup_did).await?;
let nonce = decode_nonce_b64url(&vp.nonce).map_err(ProvisionError::Armor)?;
let response =
provision_integration_didcomm(&session, vp, Some(ask.context.clone()), None, None, false)
.await?;
response_to_result(&seed, nonce, response)
}
#[allow(clippy::too_many_arguments)]
pub(crate) async fn run_didcomm_attempt(
intent: VtaIntent,
vta_did: String,
mediator_did: String,
rest_url: Option<String>,
setup_did: String,
setup_privkey_mb: String,
ask: ProvisionAsk,
tx: &UnboundedSender<VtaEvent>,
) -> AttemptOutcome {
let _ = tx.send(VtaEvent::CheckStart(DiagCheck::AuthenticateDIDComm));
match intent {
VtaIntent::FullSetup => {
let session = match DIDCommSession::connect(
&setup_did,
&setup_privkey_mb,
&vta_did,
&mediator_did,
)
.await
{
Ok(s) => {
let _ = tx.send(VtaEvent::CheckDone(
DiagCheck::AuthenticateDIDComm,
DiagStatus::Ok(format!("DIDComm session as {setup_did}")),
));
s
}
Err(e) => {
let msg = e.to_string();
let _ = tx.send(VtaEvent::CheckDone(
DiagCheck::AuthenticateDIDComm,
DiagStatus::Failed(msg.clone()),
));
let _ = tx.send(VtaEvent::CheckDone(
DiagCheck::ListWebvhServers,
DiagStatus::Skipped("session did not open".into()),
));
let _ = tx.send(VtaEvent::CheckDone(
DiagCheck::ProvisionIntegration,
DiagStatus::Skipped("session did not open".into()),
));
return AttemptOutcome::PreAuthFailure(format!(
"Could not open an authenticated DIDComm session to the VTA. \
Confirm the `pnm acl create` command ran successfully for \
this setup DID and that the VTA's mediator service is \
reachable. ({msg})"
));
}
};
let _ = tx.send(VtaEvent::CheckStart(DiagCheck::ListWebvhServers));
let servers = match session
.send_and_wait::<ListWebvhServersResultBody>(
LIST_WEBVH_SERVERS,
serde_json::json!({}),
LIST_WEBVH_SERVERS_RESULT,
30,
)
.await
{
Ok(body) => {
let detail = match body.servers.len() {
0 => "no registered servers — serverless path".into(),
1 => format!("1 registered server ({})", body.servers[0].id),
n => format!("{n} registered servers"),
};
let _ = tx.send(VtaEvent::CheckDone(
DiagCheck::ListWebvhServers,
DiagStatus::Ok(detail),
));
body.servers
}
Err(e) => {
let msg = e.to_string();
let _ = tx.send(VtaEvent::CheckDone(
DiagCheck::ListWebvhServers,
DiagStatus::Failed(format!(
"could not list — continuing serverless ({msg})"
)),
));
Vec::new()
}
};
AttemptOutcome::PreflightOk {
rest_url,
mediator_did,
servers,
}
}
VtaIntent::AdminOnly => {
match DIDCommSession::connect(&setup_did, &setup_privkey_mb, &vta_did, &mediator_did)
.await
{
Ok(_session) => {
let _ = tx.send(VtaEvent::CheckDone(
DiagCheck::AuthenticateDIDComm,
DiagStatus::Ok(format!("DIDComm session as {setup_did}")),
));
let _ = tx.send(VtaEvent::CheckDone(
DiagCheck::ListWebvhServers,
DiagStatus::Skipped(
"AdminOnly — no VTA-minted DID so no webvh host needed".into(),
),
));
let _ = tx.send(VtaEvent::CheckDone(
DiagCheck::ProvisionIntegration,
DiagStatus::Skipped(
"AdminOnly — setup did:key is the long-term admin credential; \
no template render, no rollover"
.into(),
),
));
AttemptOutcome::Connected(VtaReply::AdminOnly(AdminCredentialReply {
admin_did: setup_did,
admin_private_key_mb: setup_privkey_mb,
}))
}
Err(e) => {
let msg = e.to_string();
let _ = tx.send(VtaEvent::CheckDone(
DiagCheck::AuthenticateDIDComm,
DiagStatus::Failed(msg.clone()),
));
let _ = tx.send(VtaEvent::CheckDone(
DiagCheck::ListWebvhServers,
DiagStatus::Skipped("session did not open".into()),
));
let _ = tx.send(VtaEvent::CheckDone(
DiagCheck::ProvisionIntegration,
DiagStatus::Skipped("session did not open".into()),
));
AttemptOutcome::PreAuthFailure(format!(
"Could not open an authenticated DIDComm session to the VTA. \
Confirm the `pnm acl create` command ran successfully for \
this DID and that the VTA's mediator service is reachable. \
({msg})"
))
}
}
}
VtaIntent::AdminRotated => {
let _ = match DIDCommSession::connect(
&setup_did,
&setup_privkey_mb,
&vta_did,
&mediator_did,
)
.await
{
Ok(s) => {
let _ = tx.send(VtaEvent::CheckDone(
DiagCheck::AuthenticateDIDComm,
DiagStatus::Ok(format!("DIDComm session as {setup_did}")),
));
s
}
Err(e) => {
let msg = e.to_string();
let _ = tx.send(VtaEvent::CheckDone(
DiagCheck::AuthenticateDIDComm,
DiagStatus::Failed(msg.clone()),
));
let _ = tx.send(VtaEvent::CheckDone(
DiagCheck::ListWebvhServers,
DiagStatus::Skipped("session did not open".into()),
));
let _ = tx.send(VtaEvent::CheckDone(
DiagCheck::ProvisionIntegration,
DiagStatus::Skipped("session did not open".into()),
));
return AttemptOutcome::PreAuthFailure(format!(
"Could not open an authenticated DIDComm session to the VTA. \
Confirm the `pnm acl create` command ran successfully for \
this setup DID and that the VTA's mediator service is reachable. \
({msg})"
));
}
};
let _ = tx.send(VtaEvent::CheckDone(
DiagCheck::ListWebvhServers,
DiagStatus::Skipped(
"AdminRotated — no integration DID minted so no webvh host needed".into(),
),
));
let _ = tx.send(VtaEvent::CheckStart(DiagCheck::ProvisionIntegration));
match provision_admin_rotation_via_didcomm(
&setup_did,
&setup_privkey_mb,
&vta_did,
&mediator_did,
&ask,
)
.await
{
Ok(reply) => {
let _ = tx.send(VtaEvent::CheckDone(
DiagCheck::ProvisionIntegration,
DiagStatus::Ok(format!(
"admin DID rotated: {} (via {})",
reply.admin_did,
ask.admin_template.as_deref().unwrap_or(
crate::provision_client::ask::BUILTIN_VTA_ADMIN_TEMPLATE
),
)),
));
AttemptOutcome::Connected(VtaReply::AdminOnly(reply))
}
Err(err) => {
let msg = err.to_string();
let _ = tx.send(VtaEvent::CheckDone(
DiagCheck::ProvisionIntegration,
DiagStatus::Failed(msg.clone()),
));
AttemptOutcome::PostAuthFailure(format!(
"AdminRotation provisioning failed after auth. ({msg})"
))
}
}
}
}
}
pub async fn provision_admin_rotation_via_didcomm(
setup_did: &str,
setup_private_key_mb: &str,
vta_did: &str,
mediator_did: &str,
ask: &ProvisionAsk,
) -> Result<AdminCredentialReply, ProvisionError> {
let seed = decode_private_key_multibase(setup_private_key_mb)
.map_err(|e| ProvisionError::SetupKeyMalformed(e.to_string()))?;
let session = DIDCommSession::connect(setup_did, setup_private_key_mb, vta_did, mediator_did)
.await
.map_err(|e| ProvisionError::SessionOpen(e.to_string()))?;
let vp = ask.to_builder().sign_with(&seed, setup_did).await?;
let nonce = decode_nonce_b64url(&vp.nonce).map_err(ProvisionError::Armor)?;
let response =
provision_integration_didcomm(&session, vp, Some(ask.context.clone()), None, None, false)
.await?;
crate::provision_client::result::admin_rotation_response_to_reply(&seed, nonce, response)
}
pub(crate) fn inject_webvh_vars(
ask: &mut ProvisionAsk,
webvh_server_id: Option<&str>,
webvh_path: Option<&str>,
) {
if let Some(id) = webvh_server_id {
ask.integration_template_vars.insert(
"WEBVH_SERVER".to_string(),
serde_json::Value::String(id.to_string()),
);
}
if let Some(p) = webvh_path {
ask.integration_template_vars.insert(
"WEBVH_PATH".to_string(),
serde_json::Value::String(p.to_string()),
);
}
}
#[allow(clippy::too_many_arguments)]
pub async fn run_provision_flight(
vta_did: String,
setup_did: String,
setup_privkey_mb: String,
mediator_did: String,
rest_url: Option<String>,
ask: ProvisionAsk,
webvh_server_id: Option<String>,
webvh_path: Option<String>,
messages: Arc<dyn OperatorMessages>,
tx: UnboundedSender<VtaEvent>,
) {
let _ = tx.send(VtaEvent::CheckStart(DiagCheck::ProvisionIntegration));
let mut ask = ask;
inject_webvh_vars(&mut ask, webvh_server_id.as_deref(), webvh_path.as_deref());
if ask.label.is_none() {
ask.label = Some(format!(
"{} setup — {}",
messages.integration_label_lower(),
ask.context
));
}
match provision_via_didcomm(&setup_did, &setup_privkey_mb, &vta_did, &mediator_did, &ask).await
{
Ok(result) => {
let webvh_note = webvh_server_id
.as_ref()
.map(|id| format!(", webvh server: {id}"))
.unwrap_or_else(|| ", webvh: serverless".into());
let _ = tx.send(VtaEvent::CheckDone(
DiagCheck::ProvisionIntegration,
DiagStatus::Ok(format!(
"admin DID: {} (rolled: {}), integration DID: {}{webvh_note}",
result.admin_did(),
result.summary.admin_rolled_over,
result.integration_did().unwrap_or("(none)"),
)),
));
let _ = tx.send(VtaEvent::Connected {
protocol: Protocol::DidComm,
rest_url,
mediator_did: Some(mediator_did),
reply: VtaReply::Full(Box::new(result)),
});
}
Err(err) => {
let msg = err.to_string();
let _ = tx.send(VtaEvent::CheckDone(
DiagCheck::ProvisionIntegration,
DiagStatus::Failed(msg.clone()),
));
let hint = if msg.to_lowercase().contains("forbidden")
|| msg.contains("401")
|| msg.contains("403")
{
format!(
"The VTA rejected the provisioning request. Confirm the \
`pnm acl create` command ran successfully for setup DID \
{setup_did} in context `{}`, then retry.",
ask.context
)
} else if msg.to_lowercase().contains("template") {
format!(
"VTA rejected the template render — check the `{}` template \
is present and your `WEBVH_SERVER` (if any) matches a \
registered server. Details: {msg}",
ask.integration_template
.as_deref()
.unwrap_or("(admin rotation)")
)
} else {
format!("Provisioning failed. Details: {msg}")
};
let _ = tx.send(VtaEvent::Failed(hint));
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::provision_client::runner::webvh_path_from_url;
use serde_json::json;
#[test]
fn server_managed_url_path_is_injected_as_webvh_path() {
let mut ask = ProvisionAsk::for_template(
"did-host-http-didcomm",
[(
"URL".to_string(),
json!("https://host.example.com/dids/daemon"),
)]
.into_iter()
.collect(),
"ctx",
);
let server_id = Some("srv-1".to_string());
let derived = server_id
.as_ref()
.and_then(|_| ask.integration_template_vars.get("URL"))
.and_then(|v| v.as_str())
.and_then(webvh_path_from_url);
assert_eq!(derived.as_deref(), Some("dids/daemon"));
inject_webvh_vars(&mut ask, server_id.as_deref(), derived.as_deref());
assert_eq!(
ask.integration_template_vars.get("WEBVH_PATH"),
Some(&json!("dids/daemon"))
);
assert_eq!(
ask.integration_template_vars.get("WEBVH_SERVER"),
Some(&json!("srv-1"))
);
}
#[test]
fn serverless_leaves_webvh_path_unset() {
let mut ask = ProvisionAsk::for_template(
"did-host-http-didcomm",
[(
"URL".to_string(),
json!("https://host.example.com/dids/daemon"),
)]
.into_iter()
.collect(),
"ctx",
);
inject_webvh_vars(&mut ask, None, None);
assert!(!ask.integration_template_vars.contains_key("WEBVH_PATH"));
assert!(!ask.integration_template_vars.contains_key("WEBVH_SERVER"));
}
}