use techne_lexicon_check::chain;
use techne_lexicon_check::{parse_at_uri, same_uri};
use techne_lexicon_check::{dag_cbor, decide, Decision, Endorsements, Role, Vouch};
use serde::{Deserialize, Serialize};
use serde_json::Value;
#[derive(Debug, Clone, Deserialize)]
pub struct Record {
pub uri: String,
pub cid: String,
pub value: Value,
}
impl Record {
fn as_chain(&self) -> chain::Record<'_> {
chain::Record { uri: &self.uri, cid: &self.cid, body: &self.value }
}
pub fn intact(&self) -> bool {
dag_cbor::cid_for_json(&self.value).is_ok_and(|c| c.to_string() == self.cid)
}
}
#[derive(Debug, Clone, Deserialize)]
pub struct KnownRole {
pub member: String,
pub collective: String,
pub role: Option<i16>,
}
#[derive(Debug, Clone, Deserialize)]
pub struct KnownAdmission {
pub author: String,
pub space: String,
pub admitted: bool,
}
#[derive(Debug, Clone, Default, Deserialize)]
#[serde(default)]
pub struct Claims {
pub root: Option<String>,
pub records: Vec<Record>,
pub roles: Vec<KnownRole>,
pub admissions: Vec<KnownAdmission>,
}
impl Claims {
fn intact(&self) -> impl Iterator<Item = &Record> {
self.records.iter().filter(|r| r.intact())
}
fn held_vouch(&self, did: &str) -> Option<Vouch<'_>> {
let revoked = |cred: &Record| {
cred.value.get("revocation").and_then(Value::as_str).is_some_and(|at| self.records.iter().any(|r| same_uri(&r.uri, at)))
};
let root = self.root.as_deref().unwrap_or_default();
let pairs = self
.intact()
.filter(|decl| parse_at_uri(&decl.uri).is_some_and(|(repo, _, _)| repo == did))
.flat_map(|decl| {
self.intact()
.filter(|cred| cred.value.get("record").and_then(Value::as_str).is_some_and(|r| same_uri(r, &decl.uri)))
.map(move |cred| Vouch { declaration: decl.as_chain(), credential: cred.as_chain(), revocation_present: revoked(cred) })
});
let pairs: Vec<Vouch<'_>> = pairs.collect();
let vouches = |v: &Vouch<'_>| chain::vouched(root, v.credential, v.declaration, v.revocation_present);
pairs.iter().find(|v| vouches(v)).or(pairs.first()).copied()
}
fn vouched_declaration(&self, did: &str) -> Option<&Value> {
let root = self.root.as_deref()?;
self.held_vouch(did)
.filter(|v| chain::vouched(root, v.credential, v.declaration, v.revocation_present))
.map(|v| v.declaration.body)
}
}
impl Endorsements for Claims {
fn root(&self) -> Option<&str> {
self.root.as_deref()
}
fn endorses(&self, did: &str, collection: &str) -> bool {
self.vouched_declaration(did).is_some_and(|decl| chain::writer(decl, collection))
}
}
impl techne_lexicon_check::Claims for Claims {
fn role_at(&self, member: &str, collective: &str, _at: &str) -> Role {
match self.roles.iter().find(|r| r.member == member && r.collective == collective) {
Some(KnownRole { role: Some(n), .. }) => Role::Held(*n),
Some(_) => Role::NotHeld,
None => Role::Unknown,
}
}
fn admitted(&self, author: &str, space: &str, _at: &str) -> Option<bool> {
self.admissions.iter().find(|a| a.author == author && a.space == space).map(|a| a.admitted)
}
fn vouch(&self, author: &str) -> Option<Vouch<'_>> {
self.held_vouch(author)
}
}
#[derive(Debug, Clone, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct Verification {
pub valid: bool,
pub signer: String,
pub chain_verified: bool,
pub problems: Vec<String>,
}
pub fn verify(record: &Record, claims: &Claims) -> Verification {
let Some((signer, collection, rkey)) = parse_at_uri(&record.uri) else {
return Verification { valid: false, signer: String::new(), chain_verified: false, problems: vec!["uri: not an AT-URI".into()] };
};
let intact = record.intact();
let mut problems = vec![];
if !intact {
problems.push(format!("cid: the body does not hash to {}", record.cid));
}
let version = techne_lexicon_check::Version { collection, author: signer, rkey, body: &record.value, space: None };
match decide(&version, "", claims) {
Decision::Valid => {}
Decision::Pending => problems.push("pending: a claim it needs is not known".into()),
Decision::Invalid { reason } => problems.push(reason),
}
Verification {
valid: problems.is_empty(),
signer: signer.to_string(),
chain_verified: intact && claims.endorses(signer, collection),
problems,
}
}