use dtg_credentials::{DTGCredential, DTGCredentialType, StatementObject};
use serde_json::Value;
pub use dtg_credentials::{
DTG_CONTEXT_V1, ENDORSES_V1, IssuerScope, PRESENTED_V1, PredicateAcceptList, VETTED_V1,
W3C_VC_V2_CONTEXT, WITNESSED_V1,
};
pub const MEMBER_IDENTIFIER_SCOPE: IssuerScope = IssuerScope::Directed;
pub const PERSONA_ANNOTATION_SCOPE: IssuerScope = IssuerScope::Directed;
#[must_use]
pub fn relationship_issuer_scope(issuer_is_persona: bool) -> IssuerScope {
if issuer_is_persona {
IssuerScope::Directed
} else {
IssuerScope::Pairwise
}
}
pub fn parse_conformant(value: &Value) -> Result<DTGCredential, String> {
let mut unsigned = value.clone();
if let Some(object) = unsigned.as_object_mut() {
object.remove("proof");
} else {
return Err("not a JSON object".to_string());
}
serde_json::from_value::<DTGCredential>(unsigned).map_err(|e| e.to_string())
}
#[must_use]
pub fn nonconformance(value: &Value) -> Option<String> {
parse_conformant(value).err()
}
#[must_use]
pub fn community_roles(credential: &DTGCredential) -> Option<Vec<String>> {
if credential.type_() != DTGCredentialType::Authority
|| credential.issuer_scope() != IssuerScope::Public
{
return None;
}
let authority = credential.credential().authority()?;
if authority.scope != credential.issuer() || authority.parent.is_some() {
return None;
}
let roles: Vec<String> = authority
.actions
.iter()
.filter_map(|a| vta_sdk::protocols::vetting::role_of_action(a))
.map(str::to_string)
.collect();
(!roles.is_empty()).then_some(roles)
}
#[must_use]
pub fn core_accept_list() -> PredicateAcceptList {
PredicateAcceptList::from_iris([ENDORSES_V1, WITNESSED_V1, VETTED_V1, PRESENTED_V1])
.expect("the core predicate IRIs are absolute NFC IRIs")
}
#[must_use]
pub fn predicate_label(predicate: &str) -> Option<&'static str> {
Some(match predicate {
ENDORSES_V1 => "endorses",
WITNESSED_V1 => "witnessed",
VETTED_V1 => "vetted (identity vetting)",
PRESENTED_V1 => "presented",
_ => return None,
})
}
pub const COMMUNITY_VERIFIED_LABEL: &str = "vetted (verified by the community)";
#[must_use]
pub fn is_community_vetting(credential: &DTGCredential) -> bool {
let Some(statement) = credential.statement() else {
return false;
};
if statement.predicate != VETTED_V1 {
return false;
}
let StatementObject::Value(value) = &statement.object else {
return false;
};
serde_json::from_value::<vta_sdk::protocols::vetting::VettedObjectValue>(value.clone())
.is_ok_and(|v| v.has_no_vetter_members() && v.community == credential.issuer())
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct StatementSummary {
pub predicate: String,
pub label: String,
pub known: bool,
pub object: String,
}
#[must_use]
pub fn describe_statement(credential: &DTGCredential) -> Option<StatementSummary> {
let statement = credential.statement()?;
let known = if is_community_vetting(credential) {
Some(COMMUNITY_VERIFIED_LABEL)
} else {
predicate_label(&statement.predicate)
};
let object = match &statement.object {
StatementObject::Id(id) => format!("id {id}"),
StatementObject::DigestMultibase(digest) => format!("digest {digest}"),
StatementObject::Value(value) => {
let text = value.to_string();
if text.chars().count() > 160 {
format!("{}…", text.chars().take(159).collect::<String>())
} else {
text
}
}
};
Some(StatementSummary {
predicate: statement.predicate.clone(),
label: known.map_or_else(|| statement.predicate.clone(), str::to_string),
known: known.is_some(),
object,
})
}
#[must_use]
pub fn describe(value: &Value) -> String {
let credential = match parse_conformant(value) {
Ok(c) => c,
Err(_) => return "Non-conformant (pre-v1) credential".to_string(),
};
match credential.type_() {
DTGCredentialType::Membership => "Membership".to_string(),
DTGCredentialType::Authority => match community_roles(&credential) {
Some(roles) => format!("Role: {}", roles.join(", ")),
None => "Authority".to_string(),
},
DTGCredentialType::Statement => match describe_statement(&credential) {
Some(s) => format!("Statement: {}", s.label),
None => "Statement".to_string(),
},
DTGCredentialType::Relationship => "Relationship".to_string(),
DTGCredentialType::Invitation => "Invitation".to_string(),
DTGCredentialType::Persona => "Persona".to_string(),
DTGCredentialType::Delegation => "Delegation".to_string(),
other => other.to_string(),
}
}
#[cfg(test)]
pub(crate) mod fixtures {
use chrono::{Duration, Utc};
use dtg_credentials::DTGCredential;
use serde_json::{Value, json};
pub(crate) const RETIRED_CONTEXT: &str = "https://firstperson.network/credentials/dtg/v1";
pub(crate) fn retired_role_endorsement(community: &str, member: &str) -> Value {
json!({
"@context": ["https://www.w3.org/ns/credentials/v2", RETIRED_CONTEXT],
"type": ["VerifiableCredential", "DTGCredential", "EndorsementCredential"],
"id": "urn:uuid:5b0c9d1e-0000-4000-8000-000000000001",
"issuer": community,
"validFrom": "2026-01-01T00:00:00Z",
"credentialSubject": { "id": member, "endorsement": {
"type": "CommunityRole", "role": "vetter", "communityDid": community
} }
})
}
pub(crate) fn grant(community: &str, member: &str) -> Value {
serde_json::to_value(DTGCredential::new_vmc(
community.to_string(),
member.to_string(),
Utc::now() - Duration::minutes(1),
Some(Utc::now() + Duration::days(365)),
false,
))
.expect("serialise")
}
pub(crate) fn role_vac(community: &str, member: &str, role: &str) -> Value {
serde_json::to_value(
DTGCredential::new_community_role_vac(
community.to_string(),
member.to_string(),
role,
Utc::now() - Duration::minutes(1),
Utc::now() + Duration::days(365),
)
.expect("a role VAC")
.with_max_attenuation(0)
.expect("a VAC"),
)
.expect("serialise")
}
pub(crate) fn vetted_statement(
issuer: &str,
scope: dtg_credentials::IssuerScope,
member: &str,
community: &str,
vetter: bool,
) -> Value {
use vta_sdk::protocols::vetting::{VettedObjectValue, VettingMethod, VettingRelationship};
let value = serde_json::to_value(VettedObjectValue {
community: community.into(),
method: VettingMethod::Video,
document_classes: vec!["passport".try_into().unwrap()],
claims_verified: vec!["name.legal".try_into().unwrap()],
liveness_confirmed: true,
identity_commitment: vetter.then(|| "zC".to_string()),
card_digest_multibase: vetter.then(|| "zD".to_string()),
declared_relationship: vetter.then_some(VettingRelationship::None),
attestation_text_digest: None,
})
.unwrap();
let task = json!({
"id": "urn:uuid:issue-request-1",
"type": "https://trusttasks.org/spec/vtc/endorsements/issue/0.1",
"issuer": "did:example:admin",
"recipient": issuer,
"issuedAt": "2026-10-01T09:30:00Z",
"payload": { "subjectDid": member }
});
serde_json::to_value(
DTGCredential::new_vetted_vsc(
issuer.into(),
scope,
member.into(),
value,
&task,
Utc::now() - Duration::minutes(1),
None,
)
.expect("a vetted/1 statement"),
)
.expect("serialise")
}
pub(crate) fn community_vetting(community: &str, member: &str) -> Value {
vetted_statement(
community,
dtg_credentials::IssuerScope::Public,
member,
community,
false,
)
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn a_relationship_scope_follows_the_identifier_it_uses() {
assert_eq!(relationship_issuer_scope(false), IssuerScope::Pairwise);
assert_eq!(relationship_issuer_scope(true), IssuerScope::Directed);
assert!(MEMBER_IDENTIFIER_SCOPE.satisfies(IssuerScope::Directed));
}
#[test]
fn the_retired_context_and_types_are_nonconformant() {
let old = fixtures::retired_role_endorsement("did:example:c", "did:example:m");
assert!(nonconformance(&old).is_some());
let mut relabelled = fixtures::grant("did:example:c", "did:example:m");
relabelled["@context"][1] = json!(fixtures::RETIRED_CONTEXT);
assert!(nonconformance(&relabelled).is_some());
assert_eq!(describe(&old), "Non-conformant (pre-v1) credential");
let grant = fixtures::grant("did:example:c", "did:example:m");
assert_eq!(nonconformance(&grant), None);
assert_eq!(grant["issuerScope"], "public");
assert_eq!(grant["@context"][1], DTG_CONTEXT_V1);
assert_eq!(describe(&grant), "Membership");
}
#[test]
fn a_community_role_vac_names_its_roles() {
let vac = fixtures::role_vac("did:example:c", "did:example:m", "vetter");
let parsed = parse_conformant(&vac).unwrap();
assert_eq!(community_roles(&parsed), Some(vec!["vetter".to_string()]));
assert_eq!(describe(&vac), "Role: vetter");
let other = DTGCredential::new_vac(
"did:example:c".into(),
IssuerScope::Public,
"did:example:m".into(),
"did:example:elsewhere".into(),
vec!["role:vetter".into()],
chrono::Utc::now(),
chrono::Utc::now() + chrono::Duration::days(1),
)
.unwrap();
assert_eq!(community_roles(&other), None);
}
fn vetted_value(community: &str, vetter: bool) -> Value {
use vta_sdk::protocols::vetting::{VettedObjectValue, VettingMethod, VettingRelationship};
serde_json::to_value(VettedObjectValue {
community: community.into(),
method: VettingMethod::Video,
document_classes: vec!["passport".try_into().unwrap()],
claims_verified: vec!["name.legal".try_into().unwrap()],
liveness_confirmed: true,
identity_commitment: vetter.then(|| "zC".to_string()),
card_digest_multibase: vetter.then(|| "zD".to_string()),
declared_relationship: vetter.then_some(VettingRelationship::None),
attestation_text_digest: None,
})
.unwrap()
}
fn vetted(issuer: &str, scope: IssuerScope, value: Value) -> DTGCredential {
let task = json!({
"id": "urn:uuid:issue-request-1",
"type": "https://trusttasks.org/spec/vtc/endorsements/issue/0.1",
"issuer": "did:example:admin",
"recipient": issuer,
"issuedAt": "2026-10-01T09:30:00Z",
"payload": { "subjectDid": "did:example:m" }
});
DTGCredential::new_vetted_vsc(
issuer.into(),
scope,
"did:example:m".into(),
value,
&task,
chrono::Utc::now(),
None,
)
.unwrap()
}
#[test]
fn a_community_issued_vetted_statement_reads_as_verified_by_the_community() {
let community = "did:example:c";
let own = vetted(
community,
IssuerScope::Public,
vetted_value(community, false),
);
assert!(is_community_vetting(&own));
let summary = describe_statement(&own).unwrap();
assert!(summary.known);
assert_eq!(summary.label, COMMUNITY_VERIFIED_LABEL);
assert_eq!(
describe(&serde_json::to_value(&own).unwrap()),
"Statement: vetted (verified by the community)"
);
assert!(core_accept_list().accept(&own).is_ok());
let vetters = vetted(
"did:example:v",
IssuerScope::Directed,
vetted_value(community, true),
);
assert!(!is_community_vetting(&vetters));
assert_eq!(
describe_statement(&vetters).unwrap().label,
"vetted (identity vetting)"
);
let stranger = vetted(
"did:example:v",
IssuerScope::Directed,
vetted_value(community, false),
);
assert!(!is_community_vetting(&stranger));
assert_eq!(
describe_statement(&stranger).unwrap().label,
"vetted (identity vetting)"
);
}
#[test]
fn an_unknown_predicate_degrades_to_its_iri() {
let vsc = DTGCredential::new_vsc(
"did:example:i".into(),
IssuerScope::Directed,
"did:example:s".into(),
"https://example.org/predicates/likes/1",
StatementObject::Value(json!({ "what": "tea" })),
chrono::Utc::now(),
None,
)
.unwrap();
let summary = describe_statement(&vsc).unwrap();
assert!(!summary.known);
assert_eq!(summary.label, "https://example.org/predicates/likes/1");
assert_eq!(summary.object, r#"{"what":"tea"}"#);
assert!(core_accept_list().accept(&vsc).is_err(), "fails closed");
}
}