anp 0.7.0

Rust SDK for Agent Network Protocol (ANP)
Documentation
use serde::{Deserialize, Serialize};
use serde_json::{json, Value};

use crate::authentication::{
    resolve_did_wba_document_with_options, DidResolutionOptions,
};

use super::models::HandleStatus;
use super::resolver::{resolve_handle_with_options, ResolveHandleOptions};
use super::validator::{build_resolution_url, validate_handle};

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct BindingVerificationResult {
    pub is_valid: bool,
    pub handle: String,
    pub did: String,
    pub forward_verified: bool,
    pub reverse_verified: bool,
    pub error_message: Option<String>,
}

#[derive(Debug, Clone)]
pub struct BindingVerificationOptions {
    pub did_document: Option<Value>,
    pub resolution_options: ResolveHandleOptions,
    pub did_resolution_options: DidResolutionOptions,
}

impl Default for BindingVerificationOptions {
    fn default() -> Self {
        Self {
            did_document: None,
            resolution_options: ResolveHandleOptions::default(),
            did_resolution_options: DidResolutionOptions::default(),
        }
    }
}

pub async fn verify_handle_binding(
    handle: &str,
) -> BindingVerificationResult {
    verify_handle_binding_with_options(handle, BindingVerificationOptions::default()).await
}

pub async fn verify_handle_binding_with_options(
    handle: &str,
    options: BindingVerificationOptions,
) -> BindingVerificationResult {
    let bare_handle = handle.strip_prefix("wba://").unwrap_or(handle);
    let (local_part, domain) = match validate_handle(bare_handle) {
        Ok(value) => value,
        Err(err) => {
            return BindingVerificationResult {
                is_valid: false,
                handle: bare_handle.to_string(),
                did: String::new(),
                forward_verified: false,
                reverse_verified: false,
                error_message: Some(err.message),
            };
        }
    };
    let normalized_handle = format!("{}.{}", local_part, domain);
    let expected_endpoint = build_resolution_url(&local_part, &domain);

    let resolution = match resolve_handle_with_options(&normalized_handle, &options.resolution_options).await {
        Ok(value) => value,
        Err(err) => {
            return BindingVerificationResult {
                is_valid: false,
                handle: normalized_handle,
                did: String::new(),
                forward_verified: false,
                reverse_verified: false,
                error_message: Some(format!("Forward resolution failed: {}", err.message)),
            };
        }
    };
    if resolution.status != HandleStatus::Active {
        return BindingVerificationResult {
            is_valid: false,
            handle: normalized_handle,
            did: resolution.did.clone(),
            forward_verified: false,
            reverse_verified: false,
            error_message: Some(format!("Handle status is '{:?}', expected 'active'", resolution.status).to_ascii_lowercase()),
        };
    }
    let did_value = resolution.did.clone();
    if !did_value.starts_with("did:wba:") {
        return BindingVerificationResult {
            is_valid: false,
            handle: normalized_handle,
            did: did_value.clone(),
            forward_verified: true,
            reverse_verified: false,
            error_message: Some("DID does not use did:wba method".to_string()),
        };
    }
    let did_domain = did_value.split(':').nth(2).unwrap_or_default().to_string();
    if did_domain.to_ascii_lowercase() != domain {
        return BindingVerificationResult {
            is_valid: false,
            handle: normalized_handle,
            did: resolution.did,
            forward_verified: true,
            reverse_verified: false,
            error_message: Some(format!(
                "Domain mismatch: handle domain '{}' != DID domain '{}'",
                domain, did_domain
            )),
        };
    }

    let did_document = if let Some(value) = options.did_document {
        value
    } else {
        match resolve_did_wba_document_with_options(
            &resolution.did,
            false,
            &options.did_resolution_options,
        )
        .await
        {
            Ok(value) => value,
            Err(err) => {
                return BindingVerificationResult {
                    is_valid: false,
                    handle: normalized_handle,
                    did: resolution.did,
                    forward_verified: true,
                    reverse_verified: false,
                    error_message: Some(format!("Failed to resolve DID Document: {}", err)),
                };
            }
        }
    };

    let handle_services = extract_handle_service_from_did_document(&did_document);
    let reverse_verified = handle_services.iter().any(|service| {
        service
            .get("serviceEndpoint")
            .and_then(Value::as_str)
            .map(|value| value.trim_end_matches('/') == expected_endpoint.trim_end_matches('/'))
            .unwrap_or(false)
    });
    if !reverse_verified {
        return BindingVerificationResult {
            is_valid: false,
            handle: normalized_handle,
            did: resolution.did,
            forward_verified: true,
            reverse_verified: false,
            error_message: Some(format!(
                "DID Document does not contain a HandleService entry pointing to '{}'",
                expected_endpoint
            )),
        };
    }

    BindingVerificationResult {
        is_valid: true,
        handle: normalized_handle,
        did: resolution.did,
        forward_verified: true,
        reverse_verified: true,
        error_message: None,
    }
}

pub fn build_handle_service_entry(did: &str, local_part: &str, domain: &str) -> Value {
    json!({
        "id": format!("{did}#handle"),
        "type": "HandleService",
        "serviceEndpoint": build_resolution_url(local_part, domain),
    })
}

pub fn extract_handle_service_from_did_document(did_document: &Value) -> Vec<Value> {
    did_document
        .get("service")
        .and_then(Value::as_array)
        .map(|services| {
            services
                .iter()
                .filter(|service| service.get("type").and_then(Value::as_str) == Some("HandleService"))
                .cloned()
                .collect::<Vec<Value>>()
        })
        .unwrap_or_default()
}