use affinidi_data_integrity::{DataIntegrityProof, VerifyOptions};
use serde_json::Value as JsonValue;
use tracing::info;
use vti_common::error::AppError;
use vta_sdk::protocols::members::VERIFIABLE_MEMBERSHIP_CREDENTIAL_TYPE;
use crate::credentials::vm_resolver::{DidVmResolver, check_issuer_binding};
use crate::members::{get_member, store_member};
use crate::server::AppState;
pub struct MemberVmcOutcome {
pub member_did: String,
pub vmc_id: String,
pub recorded: bool,
}
pub async fn receive_member_vmc_inner(
state: &AppState,
member_did: String,
vc: JsonValue,
) -> Result<MemberVmcOutcome, AppError> {
let community_did = state
.config
.read()
.await
.vtc_did
.clone()
.filter(|d| !d.is_empty())
.ok_or_else(|| {
AppError::Internal("VTC DID not configured — cannot accept a member VMC".into())
})?;
let vmc_id = verify_member_vmc(state, &vc, &member_did, &community_did).await?;
let mut member = get_member(&state.members_ks, &member_did)
.await?
.filter(|m| !m.is_removed())
.ok_or_else(|| AppError::NotFound(format!("no active member: {member_did}")))?;
if member.member_vmc_id.as_deref() == Some(vmc_id.as_str()) {
return Ok(MemberVmcOutcome {
member_did,
vmc_id,
recorded: false,
});
}
member.record_member_vmc(vmc_id.clone(), vc);
store_member(&state.members_ks, &member).await?;
info!(
member = %member_did,
vmc_id = %vmc_id,
"stored member-issued VMC (member → community half of the pair)"
);
Ok(MemberVmcOutcome {
member_did,
vmc_id,
recorded: true,
})
}
async fn verify_member_vmc(
state: &AppState,
vc: &JsonValue,
member_did: &str,
community_did: &str,
) -> Result<String, AppError> {
let obj = vc
.as_object()
.ok_or_else(|| AppError::Validation("member vmc is not a JSON object".into()))?;
let issuer = match obj.get("issuer") {
Some(JsonValue::String(s)) => s.clone(),
Some(JsonValue::Object(o)) => o
.get("id")
.and_then(JsonValue::as_str)
.unwrap_or_default()
.to_string(),
_ => String::new(),
};
if issuer != member_did {
return Err(AppError::Validation(format!(
"member vmc issuer `{issuer}` is not the member `{member_did}`"
)));
}
let has_type = obj
.get("type")
.and_then(JsonValue::as_array)
.is_some_and(|a| {
a.iter()
.filter_map(JsonValue::as_str)
.any(|t| t == VERIFIABLE_MEMBERSHIP_CREDENTIAL_TYPE)
});
if !has_type {
return Err(AppError::Validation(format!(
"member vmc `type` must include `{VERIFIABLE_MEMBERSHIP_CREDENTIAL_TYPE}`"
)));
}
let subject_id = obj
.get("credentialSubject")
.and_then(JsonValue::as_object)
.and_then(|s| s.get("id"))
.and_then(JsonValue::as_str)
.unwrap_or_default();
if subject_id != community_did {
return Err(AppError::Validation(format!(
"member vmc subject `{subject_id}` is not this community `{community_did}`"
)));
}
let proof_value = obj
.get("proof")
.ok_or_else(|| AppError::Validation("member vmc has no issuer `proof`".into()))?;
let proof: DataIntegrityProof = serde_json::from_value(proof_value.clone()).map_err(|e| {
AppError::Validation(format!("member vmc proof is not Data-Integrity: {e}"))
})?;
check_issuer_binding(&proof.verification_method, member_did)?;
let resolver = DidVmResolver::new(state.did_resolver.clone());
let mut unsigned = vc.clone();
if let Some(o) = unsigned.as_object_mut() {
o.remove("proof");
}
proof
.verify(&unsigned, &resolver, VerifyOptions::new())
.await
.map_err(|e| {
AppError::Validation(format!("member vmc issuer proof did not verify: {e}"))
})?;
obj.get("id")
.and_then(JsonValue::as_str)
.map(str::to_string)
.ok_or_else(|| AppError::Validation("member vmc has no top-level `id`".into()))
}