use serde_json::Value;
use super::did_document::{
DIDCOMM_SERVICE_TYPE, REST_SERVICE_TYPE, TSP_SERVICE_TYPE, TransportFlags,
};
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub struct Transports {
pub tsp: bool,
pub didcomm: bool,
pub rest: bool,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Severity {
Error,
Info,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Finding {
pub severity: Severity,
pub message: String,
}
fn service_is(entry: &Value, wanted: &str) -> bool {
match entry.get("type") {
Some(Value::String(t)) => t == wanted,
Some(Value::Array(types)) => types.iter().any(|t| t.as_str() == Some(wanted)),
_ => false,
}
}
pub fn advertised_in(document: &Value) -> Transports {
let services = document
.get("service")
.and_then(Value::as_array)
.map(Vec::as_slice)
.unwrap_or_default();
Transports {
tsp: services.iter().any(|s| service_is(s, TSP_SERVICE_TYPE)),
didcomm: services.iter().any(|s| service_is(s, DIDCOMM_SERVICE_TYPE)),
rest: services.iter().any(|s| service_is(s, REST_SERVICE_TYPE)),
}
}
pub fn served(flags: &TransportFlags) -> Transports {
Transports {
tsp: flags.tsp && cfg!(feature = "tsp"),
didcomm: flags.didcomm,
rest: flags.rest,
}
}
pub fn findings(advertised: &Transports, served: &Transports) -> Vec<Finding> {
let mut out = Vec::new();
if advertised.tsp && !served.tsp {
out.push(Finding {
severity: Severity::Error,
message: format!(
"this registry's published DID document advertises {TSP_SERVICE_TYPE}, but this \
build cannot serve it, so every TSP frame a peer sends is dropped unread and \
the peer sees only a timeout. Either run a binary built with `--features tsp` \
and set ENABLE_TSP=true, or remove the TSP service entry from the published \
document."
),
});
}
if advertised.didcomm && !served.didcomm {
out.push(Finding {
severity: Severity::Error,
message: format!(
"this registry's published DID document advertises {DIDCOMM_SERVICE_TYPE}, but \
ENABLE_DIDCOMM is not set, so no listener is running and messages queue at the \
mediator unread. Either set ENABLE_DIDCOMM=true, or remove the DIDComm service \
entry from the published document."
),
});
}
if advertised.rest && !served.rest {
out.push(Finding {
severity: Severity::Error,
message: format!(
"this registry's published DID document advertises {REST_SERVICE_TYPE}, but \
ENABLE_REST is not set, so nothing answers on the HTTP surface. Either set \
ENABLE_REST=true, or remove the REST service entry from the published document."
),
});
}
for (servable, advertised_it, name) in [
(served.tsp, advertised.tsp, TSP_SERVICE_TYPE),
(served.didcomm, advertised.didcomm, DIDCOMM_SERVICE_TYPE),
(served.rest, advertised.rest, REST_SERVICE_TYPE),
] {
if servable && !advertised_it {
out.push(Finding {
severity: Severity::Info,
message: format!(
"this registry serves {name} but its published DID document does not \
advertise it, so no peer will choose it. Normal mid-rollout; add the \
service entry to start receiving that traffic."
),
});
}
}
out
}
pub async fn report_at_startup(own_did: &str, flags: &TransportFlags) {
use affinidi_tdk::did_resolver::{DIDCacheClient, config::DIDCacheConfigBuilder};
let resolved = match DIDCacheClient::new(DIDCacheConfigBuilder::default().build()).await {
Ok(client) => client.resolve(own_did).await,
Err(e) => {
tracing::debug!(
error = %e,
"no DID resolver available; skipping the advertised-vs-served transport check",
);
return;
}
};
let document = match resolved {
Ok(r) => match serde_json::to_value(&r.doc) {
Ok(v) => v,
Err(e) => {
tracing::debug!(error = %e, "could not read this registry's own DID document");
return;
}
},
Err(e) => {
tracing::debug!(
did = own_did,
error = %e,
"could not resolve this registry's own DID; skipping the advertised-vs-served \
transport check",
);
return;
}
};
let advertised = advertised_in(&document);
let served = served(flags);
for finding in findings(&advertised, &served) {
match finding.severity {
Severity::Error => tracing::error!("{}", finding.message),
Severity::Info => tracing::info!("{}", finding.message),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
fn doc_with(services: Value) -> Value {
json!({ "id": "did:webvh:scid:registry.example", "service": services })
}
#[test]
fn a_type_array_is_read_the_same_as_a_bare_string() {
let as_string = doc_with(json!([{ "id": "#didcomm", "type": "DIDCommMessaging" }]));
let as_array = doc_with(json!([{ "id": "#service", "type": ["DIDCommMessaging"] }]));
assert!(advertised_in(&as_string).didcomm);
assert!(advertised_in(&as_array).didcomm);
}
#[test]
fn the_id_fragment_is_never_what_matching_keys_on() {
let odd_fragments = doc_with(json!([
{ "id": "#tsp-transport", "type": "TSPTransport" },
{ "id": "#vta-didcomm", "type": "DIDCommMessaging" },
]));
let found = advertised_in(&odd_fragments);
assert!(found.tsp);
assert!(found.didcomm);
let liar = doc_with(json!([{ "id": "#tsp", "type": "SomethingElse" }]));
assert!(!advertised_in(&liar).tsp);
}
#[test]
fn a_document_with_no_services_advertises_nothing() {
assert_eq!(
advertised_in(&json!({ "id": "did:example:x" })),
Transports::default()
);
assert_eq!(advertised_in(&doc_with(json!([]))), Transports::default());
}
#[test]
fn advertising_a_transport_this_build_cannot_serve_is_an_error() {
let advertised = Transports {
tsp: true,
didcomm: true,
rest: false,
};
let served = Transports {
tsp: false,
didcomm: true,
rest: false,
};
let out = findings(&advertised, &served);
assert_eq!(out.len(), 1, "exactly the TSP mismatch: {out:?}");
assert_eq!(out[0].severity, Severity::Error);
assert!(out[0].message.contains(TSP_SERVICE_TYPE));
assert!(out[0].message.contains("--features tsp"));
assert!(out[0].message.contains("ENABLE_TSP=true"));
}
#[test]
fn serving_a_transport_the_document_omits_is_only_informational() {
let advertised = Transports {
tsp: false,
didcomm: true,
rest: false,
};
let served = Transports {
tsp: true,
didcomm: true,
rest: false,
};
let out = findings(&advertised, &served);
assert_eq!(out.len(), 1, "{out:?}");
assert_eq!(out[0].severity, Severity::Info);
assert!(out[0].message.contains(TSP_SERVICE_TYPE));
}
#[test]
fn a_document_that_matches_the_build_reports_nothing() {
let both = Transports {
tsp: true,
didcomm: true,
rest: true,
};
assert!(findings(&both, &both).is_empty());
}
#[test]
fn the_flag_alone_does_not_count_as_serving_tsp() {
let flags = TransportFlags {
rest: true,
didcomm: true,
tsp: true,
};
assert_eq!(served(&flags).tsp, cfg!(feature = "tsp"));
let off = TransportFlags {
rest: true,
didcomm: true,
tsp: false,
};
assert!(!served(&off).tsp, "flag off is never served");
}
}