techne-sdk 0.2.0

TechneSDK's logic written once: version ranking (eventState) and offline record verification over the published check, shipped to TypeScript as WebAssembly; with opt-in atproto plumbing (service auth, DID resolution, signed labels, proven record fetches, PDS password sessions) behind the `crypto` and `net` features.
Documentation
//! A public record read from its author's own PDS with its proof, verified (`net`).
//!
//! Ported from regenOS `regenos_appview::hint` (`fetch_proven`, `fetch_verified`; P20.7 G4): resolve
//! the repo's PDS and `#atproto` key through a [`DidKeyResolver`], read `com.atproto.sync.getRecord`
//! and check the proof ([`crate::sync`]). The one "resolve the PDS, resolve the key, fetch proven" a
//! platform, a rules runner and a third-party service all read through.

use serde_json::Value;

use crate::did::DidKeyResolver;
use crate::sync::ProvenRecord;

/// `did`'s record `collection/rkey` from its own PDS (found through `resolver`), with its proof
/// verified against the repo's `#atproto` key. `Ok(None)`: the PDS does not hold it. An unresolvable
/// DID, an unreachable PDS or a proof that does not verify is an error.
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
}

/// Fetch the public record `uri` (`at://did/collection/rkey`) from its author's PDS with its proof and
/// verify it, requiring CID `cid` when one is named. `(cid, value)`, or `None` when the URI is not a
/// record URI, or the record is not there, cannot be fetched, does not verify, or is at another CID.
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";

    /// A PDS answering every `sync.getRecord` with the same CAR.
    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);

        // A key the repo did not sign with: the proof is refused.
        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);
    }
}