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, 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, ask.context.clone(), None, None, false).await?;
crate::provision_client::result::admin_rotation_response_to_reply(&seed, nonce, response)
}
#[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;
if let Some(ref id) = webvh_server_id {
ask.integration_template_vars.insert(
"WEBVH_SERVER".to_string(),
serde_json::Value::String(id.clone()),
);
}
if let Some(ref p) = webvh_path {
ask.integration_template_vars.insert(
"WEBVH_PATH".to_string(),
serde_json::Value::String(p.clone()),
);
}
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));
}
}
}