use affinidi_messaging_didcomm_service::{
DIDCommServiceError, HandlerContext, TspHandler, TspResponse,
};
use async_trait::async_trait;
use serde_json::Value;
use tracing::{info, warn};
use did_hosting_common::server::trust_tasks::TspTransportHandler;
use crate::messaging::{body_parse_error, dispatch_trust_task_doc};
use crate::server::AppState;
pub struct WebvhTspHandler {
state: AppState,
}
impl WebvhTspHandler {
pub fn new(state: AppState) -> Self {
Self { state }
}
}
#[async_trait]
impl TspHandler for WebvhTspHandler {
async fn handle(
&self,
_ctx: HandlerContext,
payload: Vec<u8>,
sender_vid: String,
) -> Result<Option<TspResponse>, DIDCommServiceError> {
info!(sender = %sender_vid, "inbound TSP: trust-task document");
match run_tsp_trust_task(&self.state, &sender_vid, &payload).await? {
Some(bytes) => Ok(Some(TspResponse::new(bytes))),
None => Ok(None),
}
}
}
pub(crate) async fn run_tsp_trust_task(
state: &AppState,
sender: &str,
payload: &[u8],
) -> Result<Option<Vec<u8>>, DIDCommServiceError> {
let doc: trust_tasks_rs::TrustTask<Value> = match serde_json::from_slice(payload) {
Ok(d) => d,
Err(e) => {
warn!(sender, error = %e, "TSP: payload did not parse as TrustTask<Value>");
let err_doc = body_parse_error(&e.to_string());
let body = serde_json::to_vec(&err_doc).expect("trust-task-error/0.1 serialises");
return Ok(Some(body));
}
};
let my_vid = state
.config
.server_did
.as_deref()
.ok_or_else(|| DIDCommServiceError::Internal("server_did not configured".into()))?;
let transport = TspTransportHandler::new(my_vid.to_string(), sender.to_string());
match dispatch_trust_task_doc(state, sender, &transport, doc).await? {
Some(value) => Ok(Some(
serde_json::to_vec(&value).expect("response serialises"),
)),
None => Ok(None),
}
}
#[cfg(test)]
mod tests {
use std::path::PathBuf;
use std::sync::{Arc, OnceLock};
use did_hosting_common::server::config::{
AuthConfig, FeaturesConfig, LogConfig, SecretsConfig, ServerConfig, StoreConfig, VtaConfig,
};
use did_hosting_common::server::stats_collector::StatsCollector;
use did_hosting_common::server::store::{
KS_ACL, KS_DIDS, KS_REGISTRY, KS_SESSIONS, KS_STATS, KS_TIMESERIES, Store,
};
use serde_json::{Value, json};
use crate::config::{AppConfig, RegistryConfig};
use crate::server::AppState;
use super::*;
const SERVICE_DID: &str = "did:webvh:test:control.example.com";
const SENDER_DID: &str = "did:web:admin.example";
async fn test_state() -> (AppState, tempfile::TempDir) {
let dir = tempfile::tempdir().expect("temp dir");
let store_config = StoreConfig {
data_dir: PathBuf::from(dir.path()),
..StoreConfig::default()
};
let store = Store::open(&store_config).await.expect("open store");
let config = AppConfig {
features: FeaturesConfig {
tsp: true,
..Default::default()
},
server_did: Some(SERVICE_DID.into()),
mediator_did: None,
public_url: Some("http://control.test".into()),
did_hosting_url: Some("http://control.test".into()),
server: ServerConfig::default(),
log: LogConfig::default(),
store: store_config,
auth: AuthConfig::default(),
secrets: SecretsConfig::default(),
vta: VtaConfig::default(),
registry: RegistryConfig::default(),
trust_tasks: Default::default(),
hosting: Default::default(),
identity: Default::default(),
config_path: PathBuf::new(),
};
let state = AppState {
store: store.clone(),
sessions_ks: store.keyspace(KS_SESSIONS).unwrap(),
acl_ks: store.keyspace(KS_ACL).unwrap(),
registry_ks: store.keyspace(KS_REGISTRY).unwrap(),
dids_ks: store.keyspace(KS_DIDS).unwrap(),
config: Arc::new(config),
did_resolver: None,
secrets_resolver: None,
identity: None,
trust_tasks_verifier: None,
jwt_keys: None,
webauthn: None,
http_client: reqwest::Client::new(),
didcomm_service: Arc::new(OnceLock::new()),
stats_collector: Arc::new(StatsCollector::new()),
stats_ks: store.keyspace(KS_STATS).unwrap(),
timeseries_ks: store.keyspace(KS_TIMESERIES).unwrap(),
signing_key_bytes: None,
replay_cache: Arc::new(crate::replay::ReplayCache::new()),
path_locks: crate::path_locks::PathLocks::new(),
acl_locks: did_hosting_common::server::path_locks::PathLocks::new(),
pending_challenges: Arc::new(crate::pending_challenges::PendingChallengeTracker::new()),
ip_rate_limiter: Arc::new(crate::rate_limit::IpRateLimiter::new()),
pending_confirms: Arc::new(tokio::sync::Mutex::new(std::collections::HashMap::new())),
outbox_notify: Arc::new(tokio::sync::Notify::new()),
};
(state, dir)
}
#[tokio::test]
async fn malformed_payload_yields_trust_task_error_doc() {
let (state, _dir) = test_state().await;
let out = run_tsp_trust_task(&state, SENDER_DID, b"{not json")
.await
.expect("handler does not error on bad input");
let bytes = out.expect("a response is emitted");
let doc: Value = serde_json::from_slice(&bytes).expect("response is JSON");
assert_eq!(
doc["type"],
"https://trusttasks.org/spec/trust-task-error/0.1"
);
}
#[tokio::test]
async fn well_formed_doc_routes_through_dispatch_inbound() {
let (state, _dir) = test_state().await;
let body = json!({
"id": "urn:uuid:11111111-1111-1111-1111-111111111111",
"type": "https://trusttasks.org/spec/acl/grant/0.1",
"recipient": SERVICE_DID,
"issuedAt": "2026-07-06T00:00:00Z",
"payload": {
"entry": {
"subject": "did:web:carol.example",
"role": "owner",
"ext": { "vnd.affinidi.webvh": { "domains": { "kind": "all" } } }
}
}
});
let payload = serde_json::to_vec(&body).unwrap();
let out = run_tsp_trust_task(&state, SENDER_DID, &payload)
.await
.expect("handler does not error");
let bytes = out.expect("a response is emitted");
let doc: Value = serde_json::from_slice(&bytes).expect("response is JSON");
assert!(
doc.get("type").and_then(Value::as_str).is_some(),
"dispatch produced a typed trust-task document: {doc}"
);
}
#[tokio::test]
async fn did_management_check_name_bridges_over_tsp() {
use did_hosting_common::server::acl::{AclEntry, Role, store_acl_entry};
use did_hosting_common::server::domain::DomainScope;
let (state, _dir) = test_state().await;
store_acl_entry(
&state.acl_ks,
&AclEntry {
did: SENDER_DID.into(),
role: Role::Admin,
label: None,
created_at: 1_700_000_000,
max_total_size: None,
max_did_count: None,
domains: DomainScope::All,
},
)
.await
.unwrap();
let body = json!({
"id": "urn:uuid:22222222-2222-2222-2222-222222222222",
"type": "https://trusttasks.org/spec/did-management/did/check-name/0.1",
"recipient": SERVICE_DID,
"issuedAt": "2026-07-06T00:00:00Z",
"payload": { "path": "alice", "reserve": false }
});
let payload = serde_json::to_vec(&body).unwrap();
let out = run_tsp_trust_task(&state, SENDER_DID, &payload)
.await
.expect("handler ok")
.expect("a response is emitted");
let doc: Value = serde_json::from_slice(&out).expect("response is JSON");
assert_eq!(
doc["type"],
"https://trusttasks.org/spec/did-management/did/check-name/0.1#response"
);
assert_eq!(doc["payload"]["available"], true);
assert_eq!(doc["payload"]["reserved"], false);
assert_eq!(doc["issuer"], SERVICE_DID);
assert_eq!(doc["recipient"], SENDER_DID);
assert_eq!(
doc["threadId"], "urn:uuid:22222222-2222-2222-2222-222222222222",
"response threads to the request id"
);
}
}