use serde_json::Value;
use crate::did::DidKeyResolver;
use crate::sync::ProvenRecord;
pub async fn fetch_proven(
http: &reqwest::Client,
resolver: &dyn DidKeyResolver,
did: &str,
collection: &str,
rkey: &str,
) -> anyhow::Result<Option<ProvenRecord>> {
let pds = resolver
.pds_fetch_endpoint(did)
.await
.map_err(|e| anyhow::anyhow!("resolving {did}'s PDS: {e:?}"))?
.ok_or_else(|| anyhow::anyhow!("{did} names no PDS"))?;
let key = resolver.repo_key(did).await.map_err(|e| anyhow::anyhow!("resolving {did}'s key: {e:?}"))?;
crate::sync::fetch_proven(http, pds.trim_end_matches('/'), did, collection, rkey, &key).await
}
pub async fn fetch_verified(
http: &reqwest::Client,
resolver: &dyn DidKeyResolver,
uri: &str,
cid: Option<&str>,
) -> Option<(String, Value)> {
let (did, collection, rkey) = techne_lexicon_check::parse_at_uri(uri)?;
let proven = match fetch_proven(http, resolver, did, collection, rkey).await {
Ok(found) => found?,
Err(error) => {
tracing::warn!(%uri, error = %format!("{error:#}"), "a record did not verify");
return None;
}
};
if cid.is_some_and(|c| c != proven.cid) {
tracing::warn!(%uri, claimed = ?cid, pds_cid = %proven.cid, "a record is not at the CID named");
return None;
}
Some((proven.cid, proven.value))
}
#[cfg(test)]
mod tests {
use std::collections::{BTreeMap, HashMap};
use axum::extract::{Query, State};
use axum::response::{IntoResponse, Response};
use serde_json::json;
use super::*;
use crate::did::InMemoryDidKeyResolver;
use crate::sync::{build_proof, multikey};
const DID: &str = "did:plc:fetchfetchfetchfetchfetc";
async fn fake_pds(car: Vec<u8>) -> String {
async fn get_record(State(car): State<Vec<u8>>, Query(_): Query<HashMap<String, String>>) -> Response {
([("content-type", "application/vnd.ipld.car")], car).into_response()
}
let app = axum::Router::new().route("/xrpc/com.atproto.sync.getRecord", axum::routing::get(get_record)).with_state(car);
let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
let addr = listener.local_addr().unwrap();
tokio::spawn(async move { axum::serve(listener, app).await.unwrap() });
format!("http://{addr}")
}
#[tokio::test]
async fn a_record_is_fetched_through_the_resolver_and_verified() {
let key = k256::ecdsa::SigningKey::from_slice(&[7u8; 32]).unwrap();
let body = json!({"$type": "app.example.thing", "name": "x"});
let car = build_proof(DID, "3kaaaaaaaaaaa", &key, &BTreeMap::from([("app.example.thing/a".to_owned(), body.clone())]));
let resolver = InMemoryDidKeyResolver::new();
resolver.insert_key(DID, multikey(&key));
resolver.insert_pds(DID, fake_pds(car).await);
let http = reqwest::Client::new();
let got = fetch_proven(&http, &resolver, DID, "app.example.thing", "a").await.unwrap().expect("present");
assert_eq!(got.value, body);
assert_eq!(got.rev, "3kaaaaaaaaaaa");
assert_eq!(fetch_proven(&http, &resolver, DID, "app.example.thing", "b").await.unwrap(), None, "a proven absence");
let uri = format!("at://{DID}/app.example.thing/a");
assert_eq!(fetch_verified(&http, &resolver, &uri, Some(&got.cid)).await, Some((got.cid.clone(), body.clone())));
assert_eq!(fetch_verified(&http, &resolver, &uri, None).await, Some((got.cid.clone(), body)));
assert_eq!(fetch_verified(&http, &resolver, &uri, Some("bafyother")).await, None, "another CID");
assert_eq!(fetch_verified(&http, &resolver, "not-a-uri", None).await, None);
let other = InMemoryDidKeyResolver::new();
other.insert_key(DID, multikey(&k256::ecdsa::SigningKey::from_slice(&[9u8; 32]).unwrap()));
other.insert_pds(DID, resolver.pds_endpoint(DID).await.unwrap().unwrap());
assert!(fetch_proven(&http, &other, DID, "app.example.thing", "a").await.is_err());
assert_eq!(fetch_verified(&http, &other, &uri, None).await, None);
}
}