clawdentity-core 0.1.7

Core Rust library for Clawdentity identity, registry auth, relay, connector, and provider flows.
Documentation
use std::path::Path;

use base64::Engine;
use base64::engine::general_purpose::URL_SAFE_NO_PAD;
use tempfile::TempDir;
use wiremock::matchers::{body_json, method, path};
use wiremock::{Mock, MockServer, ResponseTemplate};

use crate::db::SqliteStore;
use crate::qr::encode_ticket_qr_png;

use super::{
    PairConfirmInput, PairProfile, PairStatusKind, PairStatusOptions, confirm_pairing,
    get_pairing_status, parse_pairing_ticket, parse_pairing_ticket_issuer_origin, start_pairing,
};

fn seed_agent_material(config_dir: &Path, agent_name: &str) {
    let agent_dir = config_dir.join("agents").join(agent_name);
    std::fs::create_dir_all(&agent_dir).expect("agent dir");
    let header = URL_SAFE_NO_PAD.encode(
        serde_json::to_vec(&serde_json::json!({"alg":"EdDSA","typ":"JWT","kid":"k1"}))
            .expect("header"),
    );
    let payload = URL_SAFE_NO_PAD.encode(
        serde_json::to_vec(&serde_json::json!({
            "sub":"did:cdi:registry.clawdentity.com:agent:01HF7YAT00W6W7CM7N3W5FDXT4",
            "ownerDid":"did:cdi:registry.clawdentity.com:human:01HF7YAT31JZHSMW1CG6Q6MHB7",
            "exp": 2208988800_u64,
            "framework":"openclaw",
            "cnf": {"jwk":{"kty":"OKP","crv":"Ed25519","x":"abc"}}
        }))
        .expect("payload"),
    );
    std::fs::write(
        agent_dir.join("ait.jwt"),
        format!("{header}.{payload}.local"),
    )
    .expect("ait");
    std::fs::write(
        agent_dir.join("secret.key"),
        URL_SAFE_NO_PAD.encode([7_u8; 32]),
    )
    .expect("secret");
}

#[test]
fn pairing_ticket_parsing_round_trip() {
    let payload = URL_SAFE_NO_PAD.encode(
        serde_json::to_vec(&serde_json::json!({
            "iss":"https://proxy.example",
            "sub":"did:cdi:registry.clawdentity.com:agent:01HF7YAT00W6W7CM7N3W5FDXT4",
        }))
        .expect("payload"),
    );
    let ticket = format!("clwpair1_{payload}");
    assert_eq!(parse_pairing_ticket(&ticket).expect("ticket"), ticket);
    assert_eq!(
        parse_pairing_ticket_issuer_origin(&ticket).expect("origin"),
        "https://proxy.example"
    );
}

#[tokio::test]
async fn start_confirm_and_status_flow() {
    let server = MockServer::start().await;
    let ticket_payload = URL_SAFE_NO_PAD.encode(
        serde_json::to_vec(&serde_json::json!({
            "iss": server.uri(),
            "sub":"did:cdi:registry.clawdentity.com:agent:01HF7YAT00W6W7CM7N3W5FDXT4"
        }))
        .expect("payload"),
    );
    let ticket = format!("clwpair1_{ticket_payload}");

    Mock::given(method("POST"))
        .and(path("/pair/start"))
        .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
            "ticket": ticket,
            "initiatorAgentDid": "did:cdi:registry.clawdentity.com:agent:01HF7YAT00W6W7CM7N3W5FDXT4",
            "initiatorProfile": { "agentName":"alpha", "humanName":"alice" },
            "expiresAt": "2030-01-01T00:00:00.000Z"
        })))
        .mount(&server)
        .await;
    Mock::given(method("POST"))
            .and(path("/pair/confirm"))
            .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
                "paired": true,
                "initiatorAgentDid": "did:cdi:registry.clawdentity.com:agent:01HF7YAT00W6W7CM7N3W5FDXT4",
                "initiatorProfile": { "agentName":"alpha", "humanName":"alice", "proxyOrigin": server.uri() },
                "responderAgentDid": "did:cdi:registry.clawdentity.com:agent:01HF7YAT00W6W7CM7N3W5FDXT5",
                "responderProfile": { "agentName":"beta", "humanName":"bob" }
            })))
            .mount(&server)
            .await;
    Mock::given(method("POST"))
            .and(path("/pair/status"))
            .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
                "status": "confirmed",
                "initiatorAgentDid": "did:cdi:registry.clawdentity.com:agent:01HF7YAT00W6W7CM7N3W5FDXT4",
                "initiatorProfile": { "agentName":"alpha", "humanName":"alice", "proxyOrigin": server.uri() },
                "responderAgentDid": "did:cdi:registry.clawdentity.com:agent:01HF7YAT00W6W7CM7N3W5FDXT4",
                "responderProfile": { "agentName":"beta", "humanName":"bob" },
                "expiresAt": "2030-01-01T00:00:00.000Z",
                "confirmedAt": "2030-01-01T00:00:10.000Z"
            })))
            .mount(&server)
            .await;

    let temp = TempDir::new().expect("temp dir");
    seed_agent_material(temp.path(), "alpha");
    let store = SqliteStore::open_path(temp.path().join("db.sqlite3")).expect("db");

    let start_temp = temp.path().to_path_buf();
    let start_server_uri = server.uri();
    let start = tokio::task::spawn_blocking(move || {
        start_pairing(
            &start_temp,
            "alpha",
            &start_server_uri,
            PairProfile {
                agent_name: "alpha".to_string(),
                human_name: "alice".to_string(),
                proxy_origin: None,
            },
            None,
        )
    })
    .await
    .expect("join")
    .expect("start");
    assert_eq!(start.ticket, ticket);

    let qr_path = temp.path().join("ticket.png");
    let png = encode_ticket_qr_png(&ticket).expect("qr");
    std::fs::write(&qr_path, png).expect("qr file");
    let confirm_temp = temp.path().to_path_buf();
    let confirm_store = store.clone();
    let confirm = tokio::task::spawn_blocking(move || {
        confirm_pairing(
            &confirm_temp,
            &confirm_store,
            "alpha",
            PairConfirmInput::QrFile(qr_path),
            PairProfile {
                agent_name: "beta".to_string(),
                human_name: "bob".to_string(),
                proxy_origin: None,
            },
        )
    })
    .await
    .expect("join")
    .expect("confirm");
    assert!(confirm.paired);

    let status_temp = temp.path().to_path_buf();
    let status_store = store.clone();
    let status_server_uri = server.uri();
    let status_ticket = ticket.clone();
    let status = tokio::task::spawn_blocking(move || {
        get_pairing_status(
            &status_temp,
            &status_store,
            "alpha",
            &status_server_uri,
            &status_ticket,
            PairStatusOptions {
                wait: false,
                wait_seconds: 1,
                poll_interval_seconds: 1,
            },
        )
    })
    .await
    .expect("join")
    .expect("status");
    assert_eq!(status.status, PairStatusKind::Confirmed);
}

#[tokio::test]
async fn start_pairing_omits_ttl_seconds_when_not_provided() {
    let server = MockServer::start().await;
    let ticket_payload = URL_SAFE_NO_PAD.encode(
        serde_json::to_vec(&serde_json::json!({
            "iss": server.uri(),
            "sub":"did:cdi:registry.clawdentity.com:agent:01HF7YAT00W6W7CM7N3W5FDXT4"
        }))
        .expect("payload"),
    );
    let ticket = format!("clwpair1_{ticket_payload}");

    Mock::given(method("POST"))
        .and(path("/pair/start"))
        .and(body_json(serde_json::json!({
            "initiatorProfile": { "agentName":"alpha", "humanName":"alice" }
        })))
        .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
            "ticket": ticket,
            "initiatorAgentDid": "did:cdi:registry.clawdentity.com:agent:01HF7YAT00W6W7CM7N3W5FDXT4",
            "initiatorProfile": { "agentName":"alpha", "humanName":"alice" },
            "expiresAt": "2030-01-01T00:00:00.000Z"
        })))
        .mount(&server)
        .await;

    let temp = TempDir::new().expect("temp dir");
    seed_agent_material(temp.path(), "alpha");
    let start_temp = temp.path().to_path_buf();
    let start_server_uri = server.uri();

    let start = tokio::task::spawn_blocking(move || {
        start_pairing(
            &start_temp,
            "alpha",
            &start_server_uri,
            PairProfile {
                agent_name: "alpha".to_string(),
                human_name: "alice".to_string(),
                proxy_origin: None,
            },
            None,
        )
    })
    .await
    .expect("join")
    .expect("start");

    assert_eq!(start.ticket, ticket);
}