anp 0.8.7

Rust SDK for Agent Network Protocol (ANP)
Documentation
use std::collections::BTreeMap;

use anp::authentication::{
    create_did_wba_document, generate_auth_header, generate_http_signature_headers,
    DidDocumentOptions, DidProfile,
};
use anp::{PrivateKeyMaterial, PublicKeyMaterial};
use serde_json::json;

fn main() {
    let args = std::env::args().skip(1).collect::<Vec<String>>();
    if args.is_empty() {
        eprintln!(
            "Usage: cargo run --example interop_cli -- <did-fixture|auth-fixture|verify-key-fixture> [options]"
        );
        std::process::exit(1);
    }

    match args[0].as_str() {
        "did-fixture" => run_did_fixture(&args[1..]),
        "auth-fixture" => run_auth_fixture(&args[1..]),
        "verify-key-fixture" => run_verify_key_fixture(&args[1..]),
        other => {
            eprintln!("Unsupported subcommand: {}", other);
            std::process::exit(1);
        }
    }
}

fn run_did_fixture(args: &[String]) {
    let profile = read_option(args, "--profile").unwrap_or_else(|| "e1".to_string());
    let hostname = read_option(args, "--hostname").unwrap_or_else(|| "example.com".to_string());
    let bundle = create_bundle(&hostname, &profile);
    println!(
        "{}",
        serde_json::to_string(&json!({
            "profile": profile,
            "did_document": bundle.did_document,
            "keys": bundle.keys,
        }))
        .expect("fixture should serialize")
    );
}

fn run_verify_key_fixture(args: &[String]) {
    let fixture_path = read_option(args, "--fixture").expect("--fixture is required");
    let content = std::fs::read_to_string(&fixture_path).expect("fixture should be readable");
    let fixture: serde_json::Value =
        serde_json::from_str(&content).expect("fixture should be valid JSON");
    let keys = fixture["keys"]
        .as_object()
        .expect("keys should be an object");
    for (fragment, key_pair) in keys {
        let private_pem = key_pair["private_key_pem"]
            .as_str()
            .expect("private_key_pem should be a string");
        let public_pem = key_pair["public_key_pem"]
            .as_str()
            .expect("public_key_pem should be a string");
        assert!(
            private_pem.starts_with("-----BEGIN PRIVATE KEY-----"),
            "{fragment} private key must be PKCS#8 PEM"
        );
        assert!(
            public_pem.starts_with("-----BEGIN PUBLIC KEY-----"),
            "{fragment} public key must be SPKI PEM"
        );
        assert!(!private_pem.contains("ANP "));
        assert!(!public_pem.contains("ANP "));

        let private_key = PrivateKeyMaterial::from_pem(private_pem)
            .expect("private key should parse as standard PEM");
        let public_key =
            PublicKeyMaterial::from_pem(public_pem).expect("public key should parse as SPKI PEM");
        if !matches!(public_key, PublicKeyMaterial::X25519(_)) {
            let signature = private_key
                .sign_message(b"cross-language standard pem")
                .expect("signature should be created");
            public_key
                .verify_message(b"cross-language standard pem", &signature)
                .expect("signature should verify");
        }
    }
    println!(
        "{}",
        serde_json::to_string(&json!({"verified": true, "key_count": keys.len()}))
            .expect("result should serialize")
    );
}

fn run_auth_fixture(args: &[String]) {
    let profile = read_option(args, "--profile").unwrap_or_else(|| "e1".to_string());
    let hostname = read_option(args, "--hostname").unwrap_or_else(|| "example.com".to_string());
    let scheme = read_option(args, "--scheme").unwrap_or_else(|| "http".to_string());
    let service_domain =
        read_option(args, "--service-domain").unwrap_or_else(|| "api.example.com".to_string());
    let request_url =
        read_option(args, "--url").unwrap_or_else(|| format!("https://{}/orders", service_domain));
    let request_method = read_option(args, "--method").unwrap_or_else(|| "GET".to_string());
    let body = read_option(args, "--body").unwrap_or_default();

    let bundle = create_bundle(&hostname, &profile);
    let private_key = bundle
        .load_private_key("key-1")
        .expect("private key should load");

    let output = match scheme.as_str() {
        "legacy" => {
            let header =
                generate_auth_header(&bundle.did_document, &service_domain, &private_key, "1.1")
                    .expect("legacy auth header should generate");
            json!({
                "profile": profile,
                "scheme": scheme,
                "service_domain": service_domain,
                "did_document": bundle.did_document,
                "keys": bundle.keys,
                "headers": {"Authorization": header},
            })
        }
        "http" => {
            let body_bytes = if body.is_empty() {
                None
            } else {
                Some(body.as_bytes())
            };
            let headers = generate_http_signature_headers(
                &bundle.did_document,
                &request_url,
                &request_method,
                &private_key,
                Some(&BTreeMap::new()),
                body_bytes,
                Default::default(),
            )
            .expect("HTTP signature headers should generate");
            json!({
                "profile": profile,
                "scheme": scheme,
                "service_domain": service_domain,
                "request_url": request_url,
                "request_method": request_method,
                "body": body,
                "did_document": bundle.did_document,
                "keys": bundle.keys,
                "headers": headers,
            })
        }
        other => panic!("Unsupported scheme: {}", other),
    };

    println!(
        "{}",
        serde_json::to_string(&output).expect("fixture should serialize")
    );
}

fn create_bundle(hostname: &str, profile: &str) -> anp::authentication::DidDocumentBundle {
    let did_profile =
        DidProfile::from_str(profile).expect("profile must be one of: e1, k1, plain_legacy");
    create_did_wba_document(
        hostname,
        DidDocumentOptions::default()
            .with_profile(did_profile)
            .with_path_segments(["user", "interop"]),
    )
    .expect("DID fixture should be created")
}

fn read_option(args: &[String], flag: &str) -> Option<String> {
    args.windows(2)
        .find(|window| window[0] == flag)
        .map(|window| window[1].clone())
}