use serde_json::{Map, Value, json};
use vta_sdk::protocols::join_requests::manifest::{v0_2, v0_3};
use vta_sdk::protocols::vetting::{CheckShape, read_branding};
use vta_sdk::vetting::requirements::{REQUIREMENTS_DIGEST_MEMBER, requirements_digest};
use vti_common::error::AppError;
use crate::community::branding;
use crate::schemas::accepts::{AcceptsCriterion, published_criteria};
use crate::server::AppState;
async fn community_did(state: &AppState) -> Result<String, AppError> {
state
.config
.read()
.await
.vtc_did
.clone()
.filter(|d| !d.is_empty())
.ok_or_else(|| AppError::Internal("VTC DID not configured".into()))
}
pub async fn manifest_v0_3(state: &AppState) -> Result<Value, AppError> {
let branding = Some(branding::load_branding(&state.community_ks).await?)
.filter(|b| !branding::is_empty(b));
response_v0_3(
community_did(state).await?,
published_criteria(&state.schemas_ks).await?,
branding,
crate::community::requested_attributes::load_requested(&state.community_ks).await?,
)
}
pub fn response_v0_3(
community_did: String,
stored: Vec<AcceptsCriterion>,
branding: Option<v0_2::CommunityBranding>,
requested_attributes: Vec<v0_2::ResponseRequestedAttributesItem>,
) -> Result<Value, AppError> {
let branding = branding.filter(|branding| {
if branding.check_shape().is_ok() {
return true;
}
let members = invalid_branding_members(branding);
tracing::warn!(
members = %if members.is_empty() { "branding".to_string() } else { members.join(", ") },
"the stored community branding breaks the join manifest's CommunityBranding \
definition; serving the manifest without branding until an admin replaces it"
);
false
});
let served = stored
.into_iter()
.map(manifest_criterion)
.collect::<Result<Vec<_>, _>>()
.map_err(stored_fault)?;
let mut typed = json!({
"communityDid": community_did,
"criteria": served.iter().map(|s| s.json.clone()).collect::<Vec<_>>(),
});
let map = typed.as_object_mut().expect("object literal");
if let Some(branding) = branding {
map.insert("branding".into(), encode(&branding, "branding")?);
}
if !requested_attributes.is_empty() {
map.insert(
"requestedAttributes".into(),
encode(&requested_attributes, "requested attributes")?,
);
}
let response: v0_3::Response = serde_json::from_value(typed)
.map_err(|e| AppError::Internal(format!("manifest 0.3: {e}")))?;
let mut out = encode(&response, "manifest 0.3")?;
if let Some(map) = out.as_object_mut() {
map.insert(
"criteria".to_string(),
Value::Array(served.into_iter().map(|s| s.json).collect()),
);
}
Ok(out)
}
fn encode(value: &impl serde::Serialize, what: &str) -> Result<Value, AppError> {
serde_json::to_value(value).map_err(|e| AppError::Internal(format!("{what} encode: {e}")))
}
fn invalid_branding_members(branding: &v0_2::CommunityBranding) -> Vec<String> {
let Ok(Value::Object(members)) = serde_json::to_value(branding) else {
return Vec::new();
};
members
.into_iter()
.filter(|(name, value)| {
let alone = Value::Object(Map::from_iter([(name.clone(), value.clone())]));
read_branding(&alone).is_err()
})
.map(|(name, _)| name)
.collect()
}
fn stored_fault(e: AppError) -> AppError {
AppError::Internal(format!(
"a stored accepts criterion does not project onto the manifest: {e}"
))
}
#[derive(Debug, Clone)]
pub struct ServedCriterion {
pub stored: AcceptsCriterion,
pub criterion: v0_3::Criterion,
pub json: Value,
pub digest: String,
}
pub fn manifest_criterion(stored: AcceptsCriterion) -> Result<ServedCriterion, AppError> {
let mut delivered = json!({
"id": stored.id,
"admission": stored.admission.as_str(),
});
let map = delivered.as_object_mut().expect("object literal");
if let Some(description) = &stored.description {
map.insert("description".into(), description.clone().into());
}
if let Some(query) = &stored.query {
map.insert(
"presentationDefinition".into(),
Value::Object(query_object(query.clone())?),
);
}
if let Some(issuers) = stored.credential_issuers {
map.insert("credentialIssuers".into(), issuers.as_str().into());
}
if stored.invitation_required {
map.insert("invitationRequired".into(), true.into());
}
if let Some(vetting) = &stored.vetting {
map.insert("vetting".into(), encode(vetting, "vetting requirements")?);
}
#[cfg(feature = "vetting-pcs")]
if let Some(raw) = stored.hidden_vetting.as_ref() {
inject_hidden_vetting(&mut delivered, raw)?;
}
let digest = requirements_digest(&delivered)
.map_err(|e| AppError::Internal(format!("requirements digest: {e}")))?;
if let Some(map) = delivered.as_object_mut() {
map.insert(
REQUIREMENTS_DIGEST_MEMBER.to_string(),
Value::String(digest.clone()),
);
}
let criterion: v0_3::Criterion =
serde_json::from_value(delivered.clone()).map_err(|e| refused("criterion", e))?;
Ok(ServedCriterion {
stored,
criterion,
json: delivered,
digest,
})
}
#[cfg(feature = "vetting-pcs")]
fn inject_hidden_vetting(delivered: &mut Value, raw: &Value) -> Result<(), AppError> {
let Some(vetting) = delivered.get_mut("vetting").and_then(Value::as_object_mut) else {
return Ok(());
};
let config: crate::vetting::pcs::HiddenVettingConfig = serde_json::from_value(raw.clone())
.map_err(|e| {
AppError::Internal(format!(
"stored hidden-vetting parameters are unreadable: {e}"
))
})?;
let ext = vetting
.entry("ext")
.or_insert_with(|| Value::Object(Map::new()));
if let Some(ext) = ext.as_object_mut() {
ext.insert(
crate::vetting::pcs::HIDDEN_VETTING_NS.to_string(),
config.published(),
);
}
Ok(())
}
fn refused(member: &str, e: impl std::fmt::Display) -> AppError {
AppError::Validation(format!(
"the join manifest cannot carry this criterion's {member}: {e}"
))
}
fn query_object(query: Value) -> Result<Map<String, Value>, AppError> {
match query {
Value::Object(query) => Ok(query),
_ => Err(AppError::Validation(
"an accepts criterion's query must be a JSON object".into(),
)),
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::schemas::accepts::{Admission, CredentialIssuers};
use serde_json::json;
use vta_sdk::protocols::vetting::VettingRequirements;
fn requirements(min: u32) -> VettingRequirements {
serde_json::from_value(json!({
"version": "0.1",
"statementType": "https://registry.trustoverip.org/dtg/vsc/vetted/1",
"minStatements": min,
"acceptedMethods": ["inPerson", "video"],
"eligibleVetters": { "role": "vetter" }
}))
.unwrap()
}
fn stored(vetting: Option<VettingRequirements>) -> AcceptsCriterion {
let mut c = AcceptsCriterion::new("kernel-developer", Admission::Review, "did:key:zAdmin");
c.query = Some(json!({ "credentials": [{ "id": "vetting", "format": "ldp_vc" }] }));
c.credential_issuers = Some(CredentialIssuers::Any);
c.description = Some("Two vetters".into());
c.vetting = vetting;
c
}
#[test]
fn the_digest_recomputes_from_what_the_applicant_receives() {
let received = manifest_criterion(stored(Some(requirements(2))))
.unwrap()
.json;
assert_eq!(received["vetting"]["minStatements"], 2);
assert_eq!(received["admission"], "review");
assert_eq!(received["credentialIssuers"], "any");
assert_eq!(
received["requirementsDigest"].as_str().unwrap(),
requirements_digest(&received).unwrap(),
"the digest must be checkable by the applicant from the delivered criterion"
);
}
#[test]
fn changing_any_requirement_or_the_admission_changes_the_digest() {
let digest = |c: AcceptsCriterion| manifest_criterion(c).unwrap().digest;
let base = digest(stored(Some(requirements(2))));
assert_ne!(base, digest(stored(Some(requirements(3)))));
let mut automatic = stored(Some(requirements(2)));
automatic.admission = Admission::Automatic;
assert_ne!(base, digest(automatic));
let mut invited = stored(Some(requirements(2)));
invited.invitation_required = true;
assert_ne!(base, digest(invited));
let mut community = stored(Some(requirements(2)));
community.credential_issuers = Some(CredentialIssuers::Community);
assert_ne!(base, digest(community));
}
#[test]
fn a_criterion_that_requires_nothing_publishes_no_requirements() {
let open = AcceptsCriterion::new("open", Admission::Automatic, "did:key:zAdmin");
let served = manifest_criterion(open).unwrap().json;
let mut members: Vec<_> = served.as_object().unwrap().keys().cloned().collect();
members.sort();
assert_eq!(members, ["admission", "id", "requirementsDigest"]);
}
#[test]
fn the_manifest_lists_criteria_in_the_order_given() {
let first = AcceptsCriterion::new("zeta", Admission::Automatic, "did:key:zAdmin");
let second = AcceptsCriterion::new("alpha", Admission::Review, "did:key:zAdmin");
let answer = response_v0_3(
"did:web:vtc.example".into(),
vec![first, second],
None,
vec![],
)
.unwrap();
let ids: Vec<_> = answer["criteria"]
.as_array()
.unwrap()
.iter()
.map(|c| c["id"].as_str().unwrap().to_string())
.collect();
assert_eq!(ids, ["zeta", "alpha"]);
}
#[test]
fn a_community_with_no_criteria_publishes_an_empty_list() {
let answer = response_v0_3("did:web:vtc.example".into(), vec![], None, vec![]).unwrap();
assert_eq!(answer["criteria"], json!([]));
}
#[cfg(feature = "vetting-pcs")]
#[test]
fn a_hidden_criterion_publishes_its_parameters_where_a_client_reads_them() {
let mut c = stored(Some(requirements(3)));
c.hidden_vetting = Some(serde_json::to_value(hidden_config()).unwrap());
let served = manifest_criterion(c).unwrap().json;
let ext = &served["vetting"]["ext"][crate::vetting::pcs::HIDDEN_VETTING_NS];
assert_eq!(ext["suite"], vti_vetting_pcs::wire::SUITE);
assert_eq!(ext["helperKey"], "zHvk");
assert_eq!(ext["tokenKey"], "zTvk");
assert_eq!(ext["vetterLabels"][0], "vetter/2026-09");
assert_eq!(ext["tokenLabels"][0], "token/2026-09");
assert_eq!(ext["dripPerTick"], 3);
assert!(served["vetting"].get("extCritical").is_none());
}
#[cfg(feature = "vetting-pcs")]
#[test]
fn the_digest_covers_the_published_parameters() {
let digest_with = |tvk: &str| {
let mut c = stored(Some(requirements(3)));
let mut config = hidden_config();
config.tvk = tvk.into();
c.hidden_vetting = Some(serde_json::to_value(config).unwrap());
let served = manifest_criterion(c).unwrap().json;
assert_eq!(
served["requirementsDigest"].as_str().unwrap(),
requirements_digest(&served).unwrap()
);
served["requirementsDigest"].as_str().unwrap().to_string()
};
assert_ne!(digest_with("zTvk"), digest_with("zOtherTvk"));
}
#[cfg(feature = "vetting-pcs")]
#[test]
fn a_new_drip_rate_keeps_the_digest() {
let digest_with = |drip: usize| {
let mut c = stored(Some(requirements(3)));
let mut config = hidden_config();
config.drip_per_tick = drip;
c.hidden_vetting = Some(serde_json::to_value(config).unwrap());
let served = manifest_criterion(c).unwrap().json;
assert_eq!(
served["requirementsDigest"].as_str().unwrap(),
requirements_digest(&served).unwrap()
);
served["requirementsDigest"].as_str().unwrap().to_string()
};
assert_eq!(digest_with(100), digest_with(20));
}
#[cfg(feature = "vetting-pcs")]
#[test]
fn an_events_approver_is_never_published() {
let mut config = hidden_config();
config.events.push(crate::vetting::pcs::HiddenVettingEvent {
event_id: "kernel-summit-2026".into(),
start_date: chrono::NaiveDate::from_ymd_opt(2026, 10, 12).unwrap(),
end_date: chrono::NaiveDate::from_ymd_opt(2026, 10, 14).unwrap(),
grace_days: 14,
group_floor: 3,
tiers: vec![crate::vetting::pcs::HiddenVettingTier {
name: "desk".into(),
drip_per_tick: 20,
}],
approved_by: Some("did:key:zTheApprover".into()),
});
let mut c = stored(Some(requirements(3)));
c.hidden_vetting = Some(serde_json::to_value(config).unwrap());
let served = manifest_criterion(c).unwrap().json;
let event = &served["vetting"]["ext"][crate::vetting::pcs::HIDDEN_VETTING_NS]["events"][0];
assert_eq!(event["eventId"], "kernel-summit-2026");
assert_eq!(event["groupFloor"], 3);
assert_eq!(event["tiers"][0]["dripPerTick"], 20);
assert!(event.get("approvedBy").is_none(), "{event}");
assert!(event.get("graceDays").is_none(), "{event}");
assert!(
!serde_json::to_string(&served)
.unwrap()
.contains("zTheApprover"),
"the approver's DID must not appear anywhere in what is served"
);
}
#[cfg(feature = "vetting-pcs")]
#[test]
fn a_criterion_without_vetting_publishes_no_parameters() {
let mut c = stored(None);
c.hidden_vetting = Some(serde_json::to_value(hidden_config()).unwrap());
let served = manifest_criterion(c).unwrap().json;
assert!(served.get("vetting").is_none());
}
#[cfg(feature = "vetting-pcs")]
fn hidden_config() -> crate::vetting::pcs::HiddenVettingConfig {
crate::vetting::pcs::HiddenVettingConfig {
suite: vti_vetting_pcs::wire::SUITE.into(),
hvk: "zHvk".into(),
tvk: "zTvk".into(),
live_periods: vec!["2026-09".into()],
live_token_labels: vec!["token/2026-09".into()],
drip_per_tick: 3,
tick_length: crate::vetting::pcs::DEFAULT_TICK_LENGTH.into(),
events: Vec::new(),
}
}
#[test]
fn a_criterion_the_manifest_cannot_carry_is_refused() {
let mut long_id = stored(None);
long_id.id = "x".repeat(129);
assert!(matches!(
manifest_criterion(long_id),
Err(AppError::Validation(_))
));
let mut not_an_object = stored(None);
not_an_object.query = Some(json!(["credentials"]));
assert!(matches!(
manifest_criterion(not_an_object),
Err(AppError::Validation(_))
));
let mut long_description = stored(None);
long_description.description = Some("x".repeat(1025));
assert!(matches!(
manifest_criterion(long_description),
Err(AppError::Validation(_))
));
}
fn branding(logo: &str) -> v0_2::CommunityBranding {
serde_json::from_value(json!({ "displayName": "Kernel", "logoUrl": logo })).unwrap()
}
#[test]
fn branding_the_manifest_cannot_carry_is_omitted_and_the_manifest_still_served() {
let answer = |logo: &str| {
response_v0_3(
"did:web:vtc.example".into(),
vec![stored(None)],
Some(branding(logo)),
Vec::new(),
)
.expect("branding must never fail the manifest an applicant joins by")
};
let good = answer("https://kernel.example/logo.svg");
assert_eq!(
good["branding"]["logoUrl"].as_str(),
Some("https://kernel.example/logo.svg")
);
for bad in [
"https://kernel.example/my logo.svg",
"https://kernel.example/logo\u{7}.svg",
"http://kernel.example/logo.svg",
] {
let manifest = answer(bad);
assert!(manifest.get("branding").is_none(), "{bad:?}");
assert_eq!(
manifest["criteria"].as_array().map(Vec::len),
Some(1),
"{bad:?}"
);
}
}
#[test]
fn the_warning_names_the_member_at_fault_not_its_value() {
assert_eq!(
invalid_branding_members(&branding("https://kernel.example/my logo.svg")),
vec!["logoUrl".to_string()]
);
assert!(invalid_branding_members(&branding("https://kernel.example/logo.svg")).is_empty());
}
}