use axum::extract::State;
use axum::http::StatusCode;
use axum::response::{IntoResponse, Response};
use serde_json::Value;
use trust_tasks_https::{HttpsHandler, status_for_code};
use trust_tasks_rs::{ErrorPayload, ProofPolicy, RejectReason, TrustTask};
use uuid::Uuid;
use did_hosting_common::server::trust_tasks::{
DispatchOutcome, TransportBoundVerifier, TrustTaskContext, build_dispatcher, dispatch_inbound,
};
use crate::auth::AuthClaims;
use crate::error::AppError;
use crate::server::AppState;
pub async fn dispatch_trust_task(
auth: AuthClaims,
State(state): State<AppState>,
body: axum::body::Bytes,
) -> Result<Response, AppError> {
let my_vid = state
.config
.server_did
.as_deref()
.ok_or_else(|| AppError::Config("server_did not configured".into()))?;
let doc: TrustTask<Value> = match serde_json::from_slice(&body) {
Ok(d) => d,
Err(e) => {
return Ok(into_response(DispatchOutcome::Rejected(body_parse_error(
&e.to_string(),
))));
}
};
if let Some(proof) = doc.proof.as_ref() {
if let Some(pk) = auth.session_pubkey_b58btc.as_deref() {
let expected_vm = format!("did:key:{pk}#{pk}");
if proof.verification_method != expected_vm {
tracing::warn!(
actual_vm = %proof.verification_method,
expected_vm,
"trust-task proof verificationMethod does not match the JWT-bound \
session pubkey — rejecting as proof_invalid"
);
let reject = RejectReason::ProofInvalid {
reason: "proof verificationMethod is not bound to this session".to_string(),
};
let routed = doc.reject_with(format!("urn:uuid:{}", Uuid::new_v4()), reject);
return Ok(into_response(DispatchOutcome::Rejected(routed)));
}
} else {
let proof_did = proof
.verification_method
.split_once('#')
.map(|(d, _)| d)
.unwrap_or("");
if proof_did != auth.did {
tracing::warn!(
proof_did = %proof_did,
auth_did = %auth.did,
"trust-task proof verificationMethod DID does not match the \
authenticated caller — rejecting as proof_invalid"
);
let reject = RejectReason::ProofInvalid {
reason: "proof verificationMethod DID does not match the authenticated caller"
.to_string(),
};
let routed = doc.reject_with(format!("urn:uuid:{}", Uuid::new_v4()), reject);
return Ok(into_response(DispatchOutcome::Rejected(routed)));
}
}
}
let type_uri = doc.type_uri.to_string();
if !build_dispatcher()
.registered_uris()
.contains(&type_uri.as_str())
{
let value = crate::messaging::bridge_did_management(&state, &auth.did, my_vid, &doc)
.await
.map_err(|e| AppError::Internal(e.to_string()))?;
return Ok((
StatusCode::OK,
[(axum::http::header::CONTENT_TYPE, "application/json")],
serde_json::to_vec(&value).expect("Trust Task response serialises"),
)
.into_response());
}
let transport = HttpsHandler::new(my_vid.to_string(), auth.did);
let ctx = TrustTaskContext {
acl_ks: &state.acl_ks,
acl_locks: &state.acl_locks,
my_vid,
};
let policy: ProofPolicy<'_, TransportBoundVerifier> = match (
state.config.trust_tasks.enforce_proofs,
state.trust_tasks_verifier.as_deref(),
) {
(true, Some(v)) => ProofPolicy::Verify(v),
_ => ProofPolicy::RejectIfPresent,
};
let outcome = dispatch_inbound::<TransportBoundVerifier>(&ctx, &transport, policy, doc).await;
Ok(into_response(outcome))
}
fn body_parse_error(reason: &str) -> trust_tasks_rs::ErrorResponse {
let reject = RejectReason::MalformedRequest {
reason: format!("body did not parse as a Trust Task document: {reason}"),
};
let payload: ErrorPayload = reject.into();
trust_tasks_rs::ErrorResponse {
id: format!("urn:uuid:{}", Uuid::new_v4()),
thread_id: None,
type_uri: error_type_uri(),
issuer: None,
recipient: None,
issued_at: Some(chrono::Utc::now()),
expires_at: None,
payload,
context: None,
proof: None,
extra: Default::default(),
}
}
fn error_type_uri() -> trust_tasks_rs::TypeUri {
"https://trusttasks.org/spec/trust-task-error/0.1"
.parse()
.expect("framework error Type URI parses")
}
fn into_response(outcome: DispatchOutcome) -> Response {
match outcome {
DispatchOutcome::Handled(doc) => {
let body = serde_json::to_vec(&doc)
.expect("Handled response document serialises (TrustTask<Value>)");
(
StatusCode::OK,
[(axum::http::header::CONTENT_TYPE, "application/json")],
body,
)
.into_response()
}
DispatchOutcome::Rejected(err_doc) => {
let status_u16 = status_for_code(&err_doc.payload.code);
let status = StatusCode::from_u16(status_u16).unwrap_or_else(|_| {
tracing::error!(status_u16, "unexpected status code from status_for_code");
StatusCode::INTERNAL_SERVER_ERROR
});
let body = serde_json::to_vec(&err_doc)
.expect("error document serialises (trust-task-error/0.1)");
(
status,
[(axum::http::header::CONTENT_TYPE, "application/json")],
body,
)
.into_response()
}
DispatchOutcome::Suppressed => {
tracing::error!(
should_not_happen = true,
"trust-tasks dispatch returned Suppressed on HTTPS — bearer auth always resolves a peer"
);
StatusCode::NO_CONTENT.into_response()
}
}
}
#[cfg(test)]
mod tests {
use trust_tasks_rs::{Payload, specs::acl::grant::v0_1 as grant};
#[test]
fn body_parse_error_shape() {
let err = super::body_parse_error("expected `,`");
assert!(err.id.starts_with("urn:uuid:"));
assert!(err.thread_id.is_none());
assert_eq!(
err.type_uri.to_string(),
"https://trusttasks.org/spec/trust-task-error/0.1"
);
assert!(err.issuer.is_none());
assert!(err.recipient.is_none());
assert_eq!(
err.payload.code,
trust_tasks_rs::TrustTaskCode::Standard(trust_tasks_rs::StandardCode::MalformedRequest)
);
assert!(
err.payload
.message
.as_deref()
.unwrap()
.contains("did not parse as a Trust Task document")
);
}
#[test]
fn well_formed_grant_envelope_round_trips() {
let body = serde_json::json!({
"id": "urn:uuid:5b3c5e2a-1b81-4d3e-9b51-7a3c89e3d1f2",
"type": grant::Payload::TYPE_URI,
"issuer": "did:web:admin.example",
"recipient": "did:web:maintainer.example",
"issuedAt": "2026-05-18T10:00:00Z",
"payload": {
"entry": {
"subject": "did:web:alice.example",
"role": "owner",
"ext": {
"vnd.affinidi.webvh": {
"domains": { "kind": "all" }
}
}
}
}
});
let doc: trust_tasks_rs::TrustTask<serde_json::Value> =
serde_json::from_value(body).expect("envelope parses");
assert_eq!(doc.type_uri.to_string(), grant::Payload::TYPE_URI);
}
}