use std::sync::{Arc, RwLock};
use std::time::Duration;
use affinidi_messaging_delivery::{Delivery, MessagingService, MessagingStatus};
use affinidi_tdk::didcomm::Message;
use affinidi_tdk::messaging::ATM;
use tracing::debug;
use crate::error::{AppError, bad_gateway_error};
use vta_sdk::protocols::{PROBLEM_REPORT_TYPE, extract_problem_report};
fn problem_report_to_app_error(code: &str, comment: &str) -> AppError {
let detail = format!("remote peer rejected the request: {comment} [{code}]");
match code.rsplit('.').next().unwrap_or_default() {
"unauthorized" | "forbidden" => AppError::Forbidden(detail),
"path-unavailable" | "conflict" => AppError::Conflict(detail),
"mnemonic-not-found" | "not-found" => AppError::NotFound(detail),
"path-invalid" | "invalid-log" | "witness-invalid" | "validation-error" | "bad-request"
| "replay-detected" | "size-exceeded" | "quota-exceeded" => AppError::Validation(detail),
_ => bad_gateway_error(detail),
}
}
struct BridgeInner {
service: Arc<MessagingService>,
atm: ATM,
vta_did: String,
}
pub struct DIDCommBridge {
inner: RwLock<Option<Arc<BridgeInner>>>,
#[allow(dead_code)]
listener_id: String,
}
impl DIDCommBridge {
pub fn new(listener_id: impl Into<String>) -> Self {
Self {
inner: RwLock::new(None),
listener_id: listener_id.into(),
}
}
pub fn placeholder() -> Self {
Self::new("")
}
pub fn set_messaging(&self, service: Arc<MessagingService>, atm: ATM, vta_did: String) {
let replacing = {
let mut guard = self.write_inner();
guard
.replace(Arc::new(BridgeInner {
service,
atm,
vta_did,
}))
.is_some()
};
if replacing {
debug!("DIDComm bridge wiring replaced (mediator reconnect)");
}
}
pub fn clear_messaging(&self) {
if self.write_inner().take().is_some() {
debug!("DIDComm bridge wiring cleared (mediator session ended)");
}
}
pub fn messaging_handle(&self) -> Option<Arc<MessagingService>> {
self.snapshot().map(|i| i.service.clone())
}
pub fn atm(&self) -> Option<ATM> {
self.snapshot().map(|i| i.atm.clone())
}
pub fn vta_did(&self) -> Option<String> {
self.snapshot().map(|i| i.vta_did.clone())
}
pub fn messaging_status_str(&self) -> Option<String> {
self.snapshot().map(|i| {
match i.service.status() {
MessagingStatus::Connected => "connected",
MessagingStatus::Degraded => "degraded",
_ => "disconnected",
}
.to_string()
})
}
fn snapshot(&self) -> Option<Arc<BridgeInner>> {
match self.inner.read() {
Ok(guard) => guard.clone(),
Err(poisoned) => poisoned.into_inner().clone(),
}
}
fn write_inner(&self) -> std::sync::RwLockWriteGuard<'_, Option<Arc<BridgeInner>>> {
self.inner
.write()
.unwrap_or_else(|poisoned| poisoned.into_inner())
}
fn inner(&self) -> Result<Arc<BridgeInner>, AppError> {
self.snapshot()
.ok_or_else(|| AppError::Internal("DIDComm messaging not initialized".into()))
}
async fn pack(
inner: &BridgeInner,
recipient: &str,
msg_type: &str,
body: serde_json::Value,
timeout_secs: Option<u64>,
) -> Result<(String, Vec<u8>), AppError> {
let msg_id = uuid::Uuid::new_v4().to_string();
let now = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_secs();
let mut builder = Message::build(msg_id.clone(), msg_type.to_string(), body)
.from(inner.vta_did.clone())
.to(recipient.to_string())
.created_time(now);
if let Some(secs) = timeout_secs {
builder = builder.expires_time(now + secs);
}
let msg = builder.finalize();
let (packed, _meta) = inner
.atm
.pack_encrypted(&msg, recipient, Some(&inner.vta_did), Some(&inner.vta_did))
.await
.map_err(|e| bad_gateway_error(format!("failed to pack message: {e}")))?;
Ok((msg_id, packed.into_bytes()))
}
pub async fn send_guaranteed(
&self,
_listener_id: &str,
recipient_did: &str,
msg_type: &str,
body: serde_json::Value,
idempotency_key: Option<String>,
deliver_by: Duration,
) -> Result<(), AppError> {
let inner = self.inner()?;
let (_msg_id, packed) = Self::pack(&inner, recipient_did, msg_type, body, None).await?;
inner
.service
.send(
recipient_did,
packed,
Delivery::Guaranteed {
idempotency_key,
ordering_key: None,
deliver_by,
},
)
.await
.map_err(|e| bad_gateway_error(format!("failed to enqueue guaranteed push: {e}")))?;
Ok(())
}
#[allow(clippy::too_many_arguments)]
pub async fn send_and_wait(
&self,
server_did: &str,
msg_type: &str,
body: serde_json::Value,
expected_type: &str,
problem_report_type: &str,
timeout_secs: u64,
) -> Result<Message, AppError> {
let inner = self.inner()?;
let (msg_id, packed) =
Self::pack(&inner, server_did, msg_type, body, Some(timeout_secs)).await?;
let received = inner
.service
.request(
server_did,
packed,
&msg_id,
Duration::from_secs(timeout_secs),
)
.await
.map_err(|e| bad_gateway_error(format!("failed to send message: {e}")))?;
Self::validate_reply(received.payload, expected_type, problem_report_type)
}
#[allow(clippy::too_many_arguments)]
pub async fn send_and_wait_via(
&self,
listener_id: &str,
recipient_did: &str,
msg_type: &str,
body: serde_json::Value,
expected_type: &str,
problem_report_type: &str,
timeout_secs: u64,
) -> Result<Message, AppError> {
let inner = self.inner()?;
let (msg_id, packed) =
Self::pack(&inner, recipient_did, msg_type, body, Some(timeout_secs)).await?;
let received = inner
.service
.request_via(
listener_id,
recipient_did,
packed,
&msg_id,
Duration::from_secs(timeout_secs),
)
.await
.map_err(|e| bad_gateway_error(format!("failed to send message: {e}")))?;
Self::validate_reply(received.payload, expected_type, problem_report_type)
}
fn validate_reply(
payload: Vec<u8>,
expected_type: &str,
problem_report_type: &str,
) -> Result<Message, AppError> {
let response: Message = serde_json::from_slice(&payload)
.map_err(|e| bad_gateway_error(format!("failed to parse DIDComm response: {e}")))?;
if response.typ == problem_report_type || response.typ == PROBLEM_REPORT_TYPE {
let (code, comment) = extract_problem_report(&response.body);
return Err(problem_report_to_app_error(&code, &comment));
}
if response.typ != expected_type {
return Err(bad_gateway_error(format!(
"unexpected response type: expected {expected_type}, got {}",
response.typ
)));
}
Ok(response)
}
}
#[cfg(test)]
mod tests {
use super::*;
use axum::http::StatusCode;
use axum::response::IntoResponse;
fn status_of(code: &str) -> StatusCode {
problem_report_to_app_error(code, "boom")
.into_response()
.status()
}
#[test]
fn remote_client_errors_keep_their_meaning() {
assert_eq!(status_of("e.p.did.path-invalid"), StatusCode::BAD_REQUEST);
assert_eq!(status_of("e.p.did.invalid-log"), StatusCode::BAD_REQUEST);
assert_eq!(
status_of("e.p.did.witness-invalid"),
StatusCode::BAD_REQUEST
);
assert_eq!(
status_of("e.p.did.validation-error"),
StatusCode::BAD_REQUEST
);
assert_eq!(status_of("e.p.did.quota-exceeded"), StatusCode::BAD_REQUEST);
assert_eq!(status_of("e.p.did.size-exceeded"), StatusCode::BAD_REQUEST);
assert_eq!(
status_of("e.p.did.replay-detected"),
StatusCode::BAD_REQUEST
);
assert_eq!(status_of("e.p.did.path-unavailable"), StatusCode::CONFLICT);
assert_eq!(
status_of("e.p.did.mnemonic-not-found"),
StatusCode::NOT_FOUND
);
}
#[test]
fn remote_auth_denial_is_forbidden_not_unauthorized() {
for code in [
"e.p.did.unauthorized",
"e.p.registration.unauthorized",
"e.p.stats.unauthorized",
"e.p.msg.forbidden",
] {
assert_eq!(status_of(code), StatusCode::FORBIDDEN, "code {code}");
}
}
#[test]
fn upstream_failures_and_unknown_codes_stay_bad_gateway() {
for code in [
"e.p.did.internal-error",
"e.p.registration.internal-error",
"e.p.did.some-code-we-have-never-seen",
"",
] {
assert_eq!(status_of(code), StatusCode::BAD_GATEWAY, "code {code}");
}
}
#[test]
fn detail_carries_remote_comment_and_code() {
let err = problem_report_to_app_error(
"e.p.did.path-invalid",
"path segments must contain only lowercase letters, digits, and hyphens",
);
let msg = err.to_string();
assert!(msg.contains("lowercase letters"), "lost comment: {msg}");
assert!(msg.contains("e.p.did.path-invalid"), "lost code: {msg}");
}
}