use affinidi_data_integrity::{DataIntegrityProof, VerifyOptions};
use serde_json::Value as JsonValue;
use tracing::info;
use uuid::Uuid;
use vti_common::audit::{AuditEvent, MembershipReciprocatedData};
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::join::{JoinStatus, get_join_request};
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 request_id: Option<Uuid>,
}
pub async fn receive_member_vmc_inner(
state: &AppState,
member_did: String,
vc: JsonValue,
request_id: Option<Uuid>,
) -> 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?;
if let Some(req_id) = request_id {
let req = get_join_request(&state.join_requests_ks, req_id)
.await?
.ok_or_else(|| AppError::NotFound(format!("join request not found: {req_id}")))?;
if req.applicant_did != member_did {
return Err(AppError::Validation(format!(
"join request {req_id} does not belong to the delivering member"
)));
}
if req.status != JoinStatus::Approved {
return Err(AppError::Conflict(format!(
"join request {req_id} is {:?}; only an Approved request has a membership to reciprocate",
req.status
)));
}
}
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,
request_id,
});
}
member.record_member_vmc(vmc_id.clone(), vc);
if request_id.is_some() {
member.record_reciprocation(vmc_id.clone());
}
store_member(&state.members_ks, &member).await?;
if let Some(req_id) = request_id {
let audit_writer = state
.audit_writer
.as_ref()
.ok_or_else(|| AppError::Internal("audit_writer not initialised".into()))?;
audit_writer
.write(
&member_did,
Some(&member_did),
AuditEvent::MembershipReciprocated(MembershipReciprocatedData {
request_id: req_id.to_string(),
vmc_id: vmc_id.clone(),
reciprocal_vc_id: vmc_id.clone(),
}),
)
.await?;
}
info!(
member = %member_did,
vmc_id = %vmc_id,
closed_request = ?request_id,
"stored member-issued VMC (member → community half of the pair)"
);
Ok(MemberVmcOutcome {
member_did,
vmc_id,
recorded: true,
request_id,
})
}
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()))
}