use affinidi_data_integrity::{DataIntegrityProof, SignOptions, crypto_suites::CryptoSuite};
use serde_json::{Value, json};
use vti_common::error::AppError;
use crate::policy::consent::PendingTaskConsent;
use crate::policy::effects::Effect;
use crate::policy::types::TaskClass;
use crate::server::AppState;
pub(super) const TASK_CONSENT_REQUEST_0_1: &str =
"https://trusttasks.org/spec/task-consent/request/0.1";
pub(super) async fn mint_signed_requests(
state: &AppState,
pending: &PendingTaskConsent,
members: &[String],
class: TaskClass,
effects: &[Effect],
subject: Option<&str>,
origin: Option<&str>,
) -> Result<Vec<Value>, AppError> {
let vta_did =
state.config.read().await.vta_did.clone().ok_or_else(|| {
AppError::Internal("VTA DID not configured; cannot sign consent".into())
})?;
let secret =
crate::operations::credentials::load_vta_issuer_secret(state, &vta_did, "task-consent")
.await?;
let class_value = serde_json::to_value(class)
.map_err(|e| AppError::Internal(format!("serialize task class: {e}")))?;
let expires_at = chrono::DateTime::from_timestamp(pending.expires_at as i64, 0)
.ok_or_else(|| AppError::Internal("consent expiry out of range".into()))?
.to_rfc3339_opts(chrono::SecondsFormat::Secs, true);
let mut signed = Vec::new();
for approver in members {
if pending.exclude_requester && approver == &pending.requester_did {
continue;
}
let mut payload = json!({
"challenge": pending.challenge,
"taskType": pending.type_uri,
"payloadDigest": pending.wire_digest,
"sideEffects": class_value.get("sideEffects"),
"exposure": class_value.get("exposure"),
"effects": effects,
"requester": pending.requester_did,
"approverSet": pending.approver_set,
"minApprovals": pending.min_approvals,
"excludeRequester": pending.exclude_requester,
"expiresAt": expires_at,
});
if let Some(s) = subject {
payload["subject"] = json!(s);
}
if let Some(o) = origin {
payload["origin"] = json!(o);
}
if let Some(pin) = &pending.state_pin {
payload["statePin"] = serde_json::to_value(pin)
.map_err(|e| AppError::Internal(format!("serialize state pin: {e}")))?;
}
let unsigned = json!({
"id": format!("urn:uuid:{}", uuid::Uuid::new_v4()),
"type": TASK_CONSENT_REQUEST_0_1,
"issuer": vta_did,
"recipient": approver,
"issuedAt": chrono::Utc::now().to_rfc3339_opts(chrono::SecondsFormat::Secs, true),
"payload": payload,
});
let proof = DataIntegrityProof::sign(
&unsigned,
&secret,
SignOptions::new()
.with_proof_purpose("assertionMethod")
.with_cryptosuite(CryptoSuite::EddsaJcs2022),
)
.await
.map_err(|e| AppError::Internal(format!("sign task-consent request: {e}")))?;
let mut doc = unsigned;
doc["proof"] = serde_json::to_value(&proof)
.map_err(|e| AppError::Internal(format!("serialize proof: {e}")))?;
signed.push(doc);
}
Ok(signed)
}
pub(super) async fn push_signed_requests(state: &AppState, requests: &[Value]) {
for request in requests {
let Some(approver) = request.get("recipient").and_then(Value::as_str) else {
continue;
};
push_one(state, approver, request).await;
}
}
async fn push_one(
state: &AppState,
approver: &str,
#[cfg_attr(not(feature = "didcomm"), allow(unused))] request: &Value,
) {
let mediator_did = {
let cfg = state.config.read().await;
super::step_up::approver_mediator(
approver,
cfg.messaging.as_ref().map(|m| m.mediator_did.as_str()),
)
};
#[cfg_attr(not(feature = "didcomm"), allow(unused))]
let Some(mediator_did) = mediator_did else {
tracing::debug!(
approver = %approver,
"no mediator route for consent approver; the relay fallback applies"
);
return;
};
#[cfg(feature = "didcomm")]
{
let pending = crate::messaging::registry::PendingResponse {
recipient_did: approver.to_string(),
message_type: TASK_CONSENT_REQUEST_0_1.to_string(),
body: request.clone(),
thread_id: request
.get("id")
.and_then(|v| v.as_str())
.map(str::to_string),
};
if let Err(e) = state
.mediator_registry
.buffer_outbound(&mediator_did, pending)
.await
{
tracing::warn!(
error = %e, approver = %approver, mediator = %mediator_did,
"failed to buffer task-consent request; relay fallback applies"
);
}
if let Err(e) = state
.didcomm_bridge
.send_oneway(
"vta-main",
approver,
TASK_CONSENT_REQUEST_0_1,
request.clone(),
)
.await
{
tracing::warn!(
error = %e, approver = %approver,
"task-consent request send failed; relay fallback applies"
);
}
super::step_up::trigger_gateway_wake(state, approver, &mediator_did).await;
}
}