pub(crate) const SVC_TSP: &str = vta_sdk::protocol::matching::TSP_SERVICE_TYPE;
pub(crate) const SVC_DIDCOMM: &str = vta_sdk::protocol::matching::DIDCOMM_SERVICE_TYPE;
pub(crate) const SVC_TRUST_TASK_HTTPS: &str =
vta_sdk::protocol::matching::TRUST_TASK_HTTPS_SERVICE_TYPE;
pub(crate) const SVC_WEBVH_HOSTING: &str = "WebVHHosting";
const HOSTING_TRUST_TASK_BASE_PATH: &str = "/api";
pub(crate) trait ServiceEntry {
fn types(&self) -> &[String];
fn endpoint_uri(&self) -> Option<String>;
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct HostReach {
pub https_base: Option<String>,
}
pub(crate) fn resolve_host_reach<S: ServiceEntry>(services: &[S]) -> Option<HostReach> {
let https_base = services
.iter()
.filter(|s| s.types().iter().any(|t| t == SVC_WEBVH_HOSTING))
.filter_map(|s| s.endpoint_uri())
.map(|raw| raw.trim_matches('"').trim_end_matches('/').to_string())
.find(|origin| origin_is_secure(origin))
.map(|origin| format!("{origin}{HOSTING_TRUST_TASK_BASE_PATH}"));
let seam = services.iter().any(|s| {
s.types()
.iter()
.any(|t| t == SVC_TSP || t == SVC_DIDCOMM || t == SVC_TRUST_TASK_HTTPS)
});
(seam || https_base.is_some()).then_some(HostReach { https_base })
}
fn origin_is_secure(origin: &str) -> bool {
let Ok(url) = url::Url::parse(origin) else {
return false;
};
match url.scheme() {
"https" => url.host().is_some(),
"http" => match url.host() {
Some(url::Host::Domain(d)) => d.eq_ignore_ascii_case("localhost"),
Some(url::Host::Ipv4(ip)) => ip.is_loopback(),
Some(url::Host::Ipv6(ip)) => ip.is_loopback(),
None => false,
},
_ => false,
}
}
pub(crate) const SUPPORTED_TYPES_HUMAN: &str =
"TSPTransport, DIDCommMessaging, TrustTaskHTTPS, or WebVHHosting at an https:// origin";
impl ServiceEntry for affinidi_tdk::did_common::service::Service {
fn types(&self) -> &[String] {
&self.type_
}
fn endpoint_uri(&self) -> Option<String> {
self.service_endpoint.get_uri()
}
}
#[cfg(test)]
mod tests {
use super::*;
struct TestService {
types: Vec<String>,
uri: Option<String>,
}
impl TestService {
fn new(types: &[&str], uri: Option<&str>) -> Self {
Self {
types: types.iter().map(|s| s.to_string()).collect(),
uri: uri.map(String::from),
}
}
}
impl ServiceEntry for TestService {
fn types(&self) -> &[String] {
&self.types
}
fn endpoint_uri(&self) -> Option<String> {
self.uri.clone()
}
}
fn reach(services: &[TestService]) -> Option<HostReach> {
resolve_host_reach(services)
}
fn base(url: &str) -> Option<HostReach> {
Some(HostReach {
https_base: Some(url.to_string()),
})
}
#[test]
fn the_live_hosting_entry_yields_the_origin_api_base() {
let svc: affinidi_tdk::did_common::service::Service = serde_json::from_str(
r#"{
"id": "did:webvh:QmUcyd...:webvh.storm.ws#webvh-hosting",
"type": "WebVHHosting",
"serviceEndpoint": { "hostingPath": "/webvh", "uri": "https://webvh.storm.ws" }
}"#,
)
.expect("the live WebVHHosting entry parses");
assert_eq!(
resolve_host_reach(std::slice::from_ref(&svc)),
base("https://webvh.storm.ws/api")
);
}
#[test]
fn the_service_types_are_the_sdk_ones() {
assert_eq!(SVC_TSP, "TSPTransport");
assert_eq!(SVC_DIDCOMM, "DIDCommMessaging");
assert_eq!(SVC_TRUST_TASK_HTTPS, "TrustTaskHTTPS");
}
#[test]
fn nothing_the_seam_can_use_is_unreachable() {
assert_eq!(reach(&[]), None);
assert_eq!(
reach(&[TestService::new(&["LinkedDomains"], Some("https://x"))]),
None
);
assert_eq!(
reach(&[TestService::new(
&["WebVHHostingService"],
Some("https://x")
)]),
None
);
}
#[test]
fn a_sealing_transport_alone_reaches_the_host_with_no_https_base() {
for t in [SVC_TSP, SVC_DIDCOMM, SVC_TRUST_TASK_HTTPS] {
assert_eq!(
reach(&[TestService::new(&[t], Some("did:example:mediator"))]),
Some(HostReach { https_base: None }),
"{t}"
);
}
}
#[test]
fn the_hosting_service_shape_carries_its_https_base() {
let services = vec![
TestService::new(&[SVC_TSP], Some("did:example:mediator")),
TestService::new(&[SVC_DIDCOMM], Some("did:example:mediator")),
TestService::new(&[SVC_WEBVH_HOSTING], Some("https://host.example")),
];
assert_eq!(reach(&services), base("https://host.example/api"));
}
#[test]
fn the_hosting_origin_is_normalised() {
for raw in [
"https://host.example",
"https://host.example/",
"\"https://host.example\"",
"\"https://host.example/\"",
] {
assert_eq!(
reach(&[TestService::new(&[SVC_WEBVH_HOSTING], Some(raw))]),
base("https://host.example/api"),
"{raw}"
);
}
}
#[test]
fn a_hosting_entry_without_a_usable_uri_is_skipped() {
assert_eq!(reach(&[TestService::new(&[SVC_WEBVH_HOSTING], None)]), None);
assert_eq!(
reach(&[
TestService::new(&[SVC_WEBVH_HOSTING], Some("\"\"")),
TestService::new(&[SVC_WEBVH_HOSTING], Some("https://second.example")),
]),
base("https://second.example/api")
);
}
#[test]
fn a_plaintext_hosting_origin_is_refused_off_loopback() {
for raw in [
"http://host.example",
"http://10.0.0.5:8080",
"ftp://host.example",
"not a url",
] {
assert_eq!(
reach(&[TestService::new(&[SVC_WEBVH_HOSTING], Some(raw))]),
None,
"{raw}"
);
}
for (raw, expected) in [
("http://127.0.0.1:8530", "http://127.0.0.1:8530/api"),
("http://localhost:8530", "http://localhost:8530/api"),
("http://[::1]:8530", "http://[::1]:8530/api"),
] {
assert_eq!(
reach(&[TestService::new(&[SVC_WEBVH_HOSTING], Some(raw))]),
base(expected),
"{raw}"
);
}
assert_eq!(
reach(&[
TestService::new(&[SVC_DIDCOMM], Some("did:example:mediator")),
TestService::new(&[SVC_WEBVH_HOSTING], Some("http://host.example")),
]),
Some(HostReach { https_base: None })
);
}
#[test]
fn a_multi_typed_entry_matches_any_type() {
assert_eq!(
reach(&[TestService::new(
&["LinkedDomains", SVC_WEBVH_HOSTING],
Some("https://host.example")
)]),
base("https://host.example/api")
);
}
}