clawdentity-core 0.1.7

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

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

use crate::config::{CliConfig, ConfigPathOptions, write_config};

use super::{
    AgentAuthRecord, CreateAgentInput, create_agent, inspect_agent, refresh_agent_auth,
    revoke_agent_auth,
};

fn options(home: &Path, registry_url: &str) -> ConfigPathOptions {
    ConfigPathOptions {
        home_dir: Some(home.to_path_buf()),
        registry_url_hint: Some(registry_url.to_string()),
    }
}

fn seed_config(options: &ConfigPathOptions, registry_url: &str) {
    let config = CliConfig {
        registry_url: registry_url.to_string(),
        proxy_url: None,
        api_key: Some("pat_local_test".to_string()),
        human_name: Some("alice".to_string()),
    };
    let _ = write_config(&config, options).expect("seed config");
}

fn test_ait(agent_did: &str) -> String {
    let header = URL_SAFE_NO_PAD.encode(
        serde_json::to_vec(&serde_json::json!({
            "alg": "EdDSA",
            "typ": "JWT",
            "kid": "test-kid",
        }))
        .expect("header"),
    );
    let payload = URL_SAFE_NO_PAD.encode(
        serde_json::to_vec(&serde_json::json!({
            "sub": agent_did,
            "ownerDid": "did:cdi:registry.clawdentity.com:human:01HF7YAT31JZHSMW1CG6Q6MHB7",
            "exp": 2_208_988_800_u64,
            "framework": "openclaw",
            "cnf": { "jwk": { "kty": "OKP", "crv": "Ed25519", "x": "public-key-b64url" } }
        }))
        .expect("payload"),
    );
    format!("{header}.{payload}.local")
}

#[tokio::test]
async fn create_and_inspect_agent_round_trip() {
    let server = MockServer::start().await;
    Mock::given(method("POST"))
        .and(path("/v1/agents/challenge"))
        .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
            "challengeId": "01JCHALLENGEID1234567890ABC",
            "nonce": "nonce-b64url",
            "ownerDid": "did:cdi:registry.clawdentity.com:human:01HF7YAT31JZHSMW1CG6Q6MHB7",
            "expiresAt": "2030-01-01T00:00:00.000Z",
            "algorithm": "Ed25519",
            "messageTemplate": "clawdentity.register.v1",
        })))
        .mount(&server)
        .await;
    Mock::given(method("POST"))
        .and(path("/v1/agents"))
        .respond_with(ResponseTemplate::new(201).set_body_json(serde_json::json!({
            "agent": {
                "did": "did:cdi:registry.clawdentity.com:agent:01HF7YAT00W6W7CM7N3W5FDXT4",
                "name": "alpha",
                "framework": "openclaw",
                "expiresAt": "2030-01-01T00:00:00.000Z"
            },
            "ait": test_ait("did:cdi:registry.clawdentity.com:agent:01HF7YAT00W6W7CM7N3W5FDXT4"),
            "agentAuth": {
                "tokenType": "Bearer",
                "accessToken": "clw_agt_access_token",
                "accessExpiresAt": "2030-01-01T01:00:00.000Z",
                "refreshToken": "clw_rft_refresh_token",
                "refreshExpiresAt": "2030-01-02T00:00:00.000Z"
            }
        })))
        .mount(&server)
        .await;

    let tmp = TempDir::new().expect("temp dir");
    let options = options(tmp.path(), &server.uri());
    seed_config(&options, &server.uri());

    let create_options = options.clone();
    let created = tokio::task::spawn_blocking(move || {
        create_agent(
            &create_options,
            CreateAgentInput {
                name: "alpha".to_string(),
                framework: Some("openclaw".to_string()),
                ttl_days: Some(30),
            },
        )
    })
    .await
    .expect("join")
    .expect("create");
    assert_eq!(created.name, "alpha");
    assert_eq!(
        created.did,
        "did:cdi:registry.clawdentity.com:agent:01HF7YAT00W6W7CM7N3W5FDXT4"
    );

    let inspect_options = options.clone();
    let inspect = tokio::task::spawn_blocking(move || inspect_agent(&inspect_options, "alpha"))
        .await
        .expect("join")
        .expect("inspect");
    assert_eq!(inspect.framework, "openclaw");
    assert_eq!(inspect.did, created.did);
    assert_eq!(
        inspect.owner_did,
        "did:cdi:registry.clawdentity.com:human:01HF7YAT31JZHSMW1CG6Q6MHB7"
    );

    let auth_path = tmp
        .path()
        .join(".clawdentity/states/local/agents/alpha/registry-auth.json");
    let auth: AgentAuthRecord = super::read_json(&auth_path).expect("auth json");
    assert_eq!(auth.token_type, "Bearer");
    assert_eq!(auth.refresh_token, "clw_rft_refresh_token");
}

#[tokio::test]
async fn refresh_agent_auth_updates_registry_auth_bundle() {
    let server = MockServer::start().await;
    Mock::given(method("POST"))
        .and(path("/v1/agents/challenge"))
        .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
            "challengeId": "01JCHALLENGEID1234567890ABC",
            "nonce": "nonce-b64url",
            "ownerDid": "did:cdi:registry.clawdentity.com:human:01HF7YAT31JZHSMW1CG6Q6MHB7",
            "expiresAt": "2030-01-01T00:00:00.000Z",
        })))
        .mount(&server)
        .await;
    Mock::given(method("POST"))
        .and(path("/v1/agents"))
        .respond_with(ResponseTemplate::new(201).set_body_json(serde_json::json!({
            "agent": {
                "did": "did:cdi:registry.clawdentity.com:agent:01HF7YAT00W6W7CM7N3W5FDXT4",
                "name": "beta",
                "framework": "openclaw",
                "expiresAt": "2030-01-01T00:00:00.000Z"
            },
            "ait": test_ait("did:cdi:registry.clawdentity.com:agent:01HF7YAT00W6W7CM7N3W5FDXT4"),
            "agentAuth": {
                "tokenType": "Bearer",
                "accessToken": "clw_agt_old_access_token",
                "accessExpiresAt": "2030-01-01T01:00:00.000Z",
                "refreshToken": "clw_rft_old_refresh_token",
                "refreshExpiresAt": "2030-01-02T00:00:00.000Z"
            }
        })))
        .mount(&server)
        .await;
    Mock::given(method("POST"))
        .and(path("/v1/agents/auth/refresh"))
        .and(header_exists("x-claw-timestamp"))
        .and(header_exists("x-claw-nonce"))
        .and(header_exists("x-claw-body-sha256"))
        .and(header_exists("x-claw-proof"))
        .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
            "agentAuth": {
                "tokenType": "Bearer",
                "accessToken": "clw_agt_new_access_token",
                "accessExpiresAt": "2030-01-03T01:00:00.000Z",
                "refreshToken": "clw_rft_new_refresh_token",
                "refreshExpiresAt": "2030-01-04T00:00:00.000Z"
            }
        })))
        .mount(&server)
        .await;

    let tmp = TempDir::new().expect("temp dir");
    let options = options(tmp.path(), &server.uri());
    seed_config(&options, &server.uri());
    let create_options = options.clone();
    let _ = tokio::task::spawn_blocking(move || {
        create_agent(
            &create_options,
            CreateAgentInput {
                name: "beta".to_string(),
                framework: None,
                ttl_days: None,
            },
        )
    })
    .await
    .expect("join")
    .expect("create");

    let refresh_options = options.clone();
    let refreshed =
        tokio::task::spawn_blocking(move || refresh_agent_auth(&refresh_options, "beta"))
            .await
            .expect("join")
            .expect("refresh");
    assert_eq!(refreshed.status, "refreshed");

    let auth_path = tmp
        .path()
        .join(".clawdentity/states/local/agents/beta/registry-auth.json");
    let auth: AgentAuthRecord = super::read_json(&auth_path).expect("auth json");
    assert_eq!(auth.access_token, "clw_agt_new_access_token");
    assert_eq!(auth.refresh_token, "clw_rft_new_refresh_token");
}

#[test]
fn auth_revoke_returns_not_supported() {
    let tmp = TempDir::new().expect("temp dir");
    let options = options(tmp.path(), "https://registry.clawdentity.com");
    seed_config(&options, "https://registry.clawdentity.com");

    let agent_dir = tmp.path().join(".clawdentity/states/prod/agents/gamma");
    fs::create_dir_all(&agent_dir).expect("agent dir");
    fs::write(
            agent_dir.join("registry-auth.json"),
            "{\n  \"tokenType\": \"Bearer\",\n  \"accessToken\": \"a\",\n  \"accessExpiresAt\": \"b\",\n  \"refreshToken\": \"c\",\n  \"refreshExpiresAt\": \"d\"\n}\n",
        )
        .expect("auth");
    let result = revoke_agent_auth(&options, "gamma").expect("revoke");
    assert_eq!(result.status, "not_supported");
}