#![allow(clippy::result_large_err)]
use std::sync::Arc;
use serde_json::{Value, json};
use tracing::{debug, info, warn};
use trust_tasks_rs::specs::webvh::sync::{
batch::v0_1 as sync_batch, delete::v0_2 as sync_delete, update::v0_2 as sync_update,
};
use trust_tasks_rs::{DeclaredErrorCode, ErrorPayload, RejectReason, TrustTask};
use did_hosting_common::didcomm_types::{MSG_SYNC_BATCH, MSG_SYNC_DELETE, MSG_SYNC_UPDATE};
use did_hosting_common::server::replay::ReplayError;
use did_hosting_common::server::trust_tasks::{
TransportBoundVerifier, identity_signing_secret, sign_document, verify_sender_bound,
};
use crate::server::AppState;
use crate::watcher_ops::{self, SyncApplied, SyncEntry, SyncRefusal};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Via {
Tsp,
Didcomm,
Https,
}
pub const SYNC_TASKS: &[&str] = &[MSG_SYNC_UPDATE, MSG_SYNC_DELETE, MSG_SYNC_BATCH];
pub fn build_verifier(
did_resolver: Option<&affinidi_did_resolver_cache_sdk::DIDCacheClient>,
) -> Option<Arc<TransportBoundVerifier>> {
did_hosting_common::server::trust_tasks::build_verifier(did_resolver)
}
fn not_authorized(type_uri: &str) -> Option<DeclaredErrorCode> {
Some(match type_uri {
MSG_SYNC_UPDATE => sync_update::error_codes::NOT_AUTHORIZED,
MSG_SYNC_DELETE => sync_delete::error_codes::NOT_AUTHORIZED,
MSG_SYNC_BATCH => sync_batch::error_codes::NOT_AUTHORIZED,
_ => return None,
})
}
pub async fn dispatch_inbound_document(
state: &AppState,
via: Via,
sender: Option<&str>,
doc: TrustTask<Value>,
) -> Option<Value> {
let type_uri = doc.type_uri.to_string();
if type_uri.contains("/trust-task-error/") {
return None;
}
let Some(my_did) = state.config.server_did.as_deref() else {
warn!(%type_uri, "trust task dropped: this watcher has no configured DID");
return None;
};
let Some(verifier) = state.trust_tasks_verifier.clone() else {
warn!(%type_uri, "trust task dropped: no DID resolver configured to verify it");
return None;
};
match doc.issuer.as_deref() {
Some(issuer) if state.config.sync.source_dids.iter().any(|s| s == issuer) => {}
claimed => {
warn!(
issuer = claimed.unwrap_or("unknown"),
%type_uri,
"trust task dropped: claimed issuer is not a configured source; refusing before DID resolution"
);
return None;
}
}
let principal = match verify_sender_bound(&doc, None, sender, my_did, &verifier).await {
Ok(p) => p,
Err(e) => {
warn!(
?via,
sender = sender.unwrap_or("unknown"),
%type_uri,
error = %e,
"trust task not answered: it does not verify"
);
return None;
}
};
let reply_id = || format!("urn:uuid:{}", uuid::Uuid::new_v4());
if !state.config.sync.source_dids.contains(&principal) {
warn!(issuer = %principal, %type_uri, "sync refused: the issuer is not a configured source");
let message = format!("{principal} is not a source of this watcher");
let payload = match not_authorized(&type_uri) {
Some(code) => ErrorPayload::from(code).with_message(message),
None => RejectReason::PermissionDenied { reason: message }.into(),
};
return seal(
state,
my_did,
&principal,
error_value(doc.reject_with(reply_id(), payload)),
)
.await;
}
match state.replay_cache.check(&principal, &doc.id) {
Ok(()) => {}
Err(ReplayError::Duplicate) => {
warn!(issuer = %principal, doc_id = %doc.id, "sync not answered: replay");
return None;
}
Err(ReplayError::Full) => {
let refusal =
doc.reject_with(reply_id(), RejectReason::Unavailable { retry_after: None });
return seal(state, my_did, &principal, error_value(refusal)).await;
}
}
let result = match type_uri.as_str() {
MSG_SYNC_UPDATE => sync_update_task(state, &principal, &doc).await,
MSG_SYNC_DELETE => sync_delete_task(state, &principal, &doc).await,
MSG_SYNC_BATCH => sync_batch_task(state, &principal, &doc).await,
other => Err(RejectReason::UnsupportedType {
type_uri: other.to_string(),
}
.into()),
};
let reply = match result {
Ok(payload) => doc.respond_with(reply_id(), payload),
Err(e) => error_value(doc.reject_with(reply_id(), e)),
};
seal(state, my_did, &principal, reply).await
}
fn error_value(err: trust_tasks_rs::ErrorResponse) -> TrustTask<Value> {
let value = serde_json::to_value(&err).expect("error document serialises");
serde_json::from_value(value).expect("error document re-reads as a TrustTask")
}
async fn seal(
state: &AppState,
my_did: &str,
requester: &str,
mut reply: TrustTask<Value>,
) -> Option<Value> {
let identity = state.identity.as_deref()?;
let secret = match identity_signing_secret(identity, my_did) {
Ok(s) => s,
Err(e) => {
warn!(error = %e, "cannot sign trust-task reply");
return None;
}
};
reply.issuer = Some(my_did.to_string());
reply.recipient = Some(requester.to_string());
reply.issued_at = Some(chrono::Utc::now());
reply.proof = None;
match sign_document(&reply, &secret).await {
Ok(signed) => serde_json::to_value(&signed).ok(),
Err(e) => {
warn!(error = %e, "cannot sign trust-task reply");
None
}
}
}
fn internal(e: impl std::fmt::Display) -> ErrorPayload {
RejectReason::InternalError {
reason: e.to_string(),
}
.into()
}
fn payload<P: serde::de::DeserializeOwned>(doc: &TrustTask<Value>) -> Result<P, ErrorPayload> {
serde_json::from_value(doc.payload.clone()).map_err(|e| {
RejectReason::MalformedRequest {
reason: format!("the payload does not fit its schema: {e}"),
}
.into()
})
}
fn reply<R: serde::Serialize + serde::de::DeserializeOwned>(
body: Value,
) -> Result<Value, ErrorPayload> {
let typed: R = serde_json::from_value(body).map_err(internal)?;
serde_json::to_value(typed).map_err(internal)
}
fn status_str(status: SyncApplied) -> &'static str {
match status {
SyncApplied::Applied => "applied",
SyncApplied::Unchanged => "unchanged",
}
}
fn refusal_payload(refusal: SyncRefusal) -> ErrorPayload {
match refusal {
SyncRefusal::InvalidLog(reason) => {
ErrorPayload::from(sync_update::error_codes::INVALID_LOG)
.with_message("the log does not verify")
.with_details(json!({ "reason": reason }))
}
SyncRefusal::HistoryRewrite(m) => {
ErrorPayload::from(sync_update::error_codes::HISTORY_REWRITE).with_message(m)
}
SyncRefusal::Deactivated(m) => {
ErrorPayload::from(sync_update::error_codes::DEACTIVATED).with_message(m)
}
SyncRefusal::NotOwner(m) => {
ErrorPayload::from(sync_update::error_codes::NOT_AUTHORIZED).with_message(m)
}
SyncRefusal::Transient(reason) => {
warn!(%reason, "sync not applied (transient); the source will re-send it");
RejectReason::InternalError { reason }.into()
}
}
}
fn refusal_code(refusal: &SyncRefusal) -> Option<&'static str> {
match refusal {
SyncRefusal::InvalidLog(_) => Some(sync_update::error_codes::INVALID_LOG.code),
SyncRefusal::HistoryRewrite(_) => Some(sync_update::error_codes::HISTORY_REWRITE.code),
SyncRefusal::Deactivated(_) => Some(sync_update::error_codes::DEACTIVATED.code),
SyncRefusal::NotOwner(_) => Some(sync_update::error_codes::NOT_AUTHORIZED.code),
SyncRefusal::Transient(_) => None,
}
}
fn entry(
mnemonic: String,
did_id: String,
log_content: String,
witness_content: Option<String>,
version_count: u64,
disabled: bool,
) -> SyncEntry {
SyncEntry {
mnemonic,
did_id,
log_content,
witness_content,
version_count,
disabled,
}
}
async fn sync_update_task(
state: &AppState,
source: &str,
doc: &TrustTask<Value>,
) -> Result<Value, ErrorPayload> {
let u: sync_update::Payload = payload(doc)?;
let e = entry(
u.mnemonic.to_string(),
u.did_id.to_string(),
u.log_content.to_string(),
u.witness_content.map(|w| w.to_string()),
u.version_count.get(),
u.disabled,
);
let _guard = state.sync_lock.lock().await;
let status = watcher_ops::apply_sync(&state.store, &state.dids_ks, source, &e)
.await
.map_err(refusal_payload)?;
info!(source, mnemonic = %e.mnemonic, ?status, "sync update applied");
reply::<sync_update::Response>(json!({ "mnemonic": e.mnemonic, "status": status_str(status) }))
}
async fn sync_batch_task(
state: &AppState,
source: &str,
doc: &TrustTask<Value>,
) -> Result<Value, ErrorPayload> {
let b: sync_batch::Payload = payload(doc)?;
let _guard = state.sync_lock.lock().await;
let mut results = Vec::with_capacity(b.updates.len());
let mut transient: Option<String> = None;
for u in b.updates {
let e = entry(
u.mnemonic.to_string(),
u.did_id.to_string(),
u.log_content.to_string(),
u.witness_content.map(|w| w.to_string()),
u.version_count.get(),
u.disabled,
);
match watcher_ops::apply_sync(&state.store, &state.dids_ks, source, &e).await {
Ok(status) => {
results.push(json!({ "mnemonic": e.mnemonic, "status": status_str(status) }))
}
Err(refusal) => match refusal_code(&refusal) {
Some(code) => {
warn!(mnemonic = %e.mnemonic, ?refusal, "sync-batch: entry refused");
results
.push(json!({ "mnemonic": e.mnemonic, "status": "refused", "code": code }));
}
None => {
transient.get_or_insert(format!("{refusal:?}"));
}
},
}
}
if let Some(reason) = transient {
return Err(RejectReason::InternalError {
reason: format!("a sync-batch entry could not be applied just now: {reason}"),
}
.into());
}
debug!(source, count = results.len(), "sync batch applied");
reply::<sync_batch::Response>(json!({ "results": results }))
}
async fn sync_delete_task(
state: &AppState,
source: &str,
doc: &TrustTask<Value>,
) -> Result<Value, ErrorPayload> {
let d: sync_delete::Payload = payload(doc)?;
let mnemonic = d.mnemonic.as_str();
did_hosting_common::server::mnemonic::validate_mnemonic(mnemonic).map_err(|e| {
ErrorPayload::from(RejectReason::MalformedRequest {
reason: format!("mnemonic: {e}"),
})
})?;
let _guard = state.sync_lock.lock().await;
let held = watcher_ops::delete_record(&state.store, &state.dids_ks, mnemonic, source)
.await
.map_err(internal)?;
info!(source, mnemonic, held, "sync delete applied");
reply::<sync_delete::Response>(json!({
"mnemonic": mnemonic,
"status": if held { "deleted" } else { "absent" },
}))
}