use anyhow::{Result, anyhow};
use nostr_sdk::prelude::*;
use serde::{Deserialize, Serialize};
use std::time::Duration;
use crate::membership::bn254_leaf_commitment;
use super::identity::AgentIdentity;
use super::nostr_client::{NostrClient, KIND_ENROLLMENT_AUTH, KIND_HUMAN_DELEGATION};
use super::prover::MerkleWitness;
use super::storage::SecureStorage;
pub const KEY_MERKLE_WITNESS: &str = "signedby_merkle_witness";
pub const KEY_MERKLE_ROOT: &str = "signedby_merkle_root";
pub const DEFAULT_API_URL: &str = "https://api.signedbyme.com";
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct EnrollmentResult {
pub success: bool,
pub merkle_witness: Option<MerkleWitness>,
pub merkle_root: Option<String>,
pub leaf_index: Option<u32>,
pub error: Option<String>,
}
#[derive(Debug, Serialize)]
struct EnrollCommitRequest {
leaf_commitment: String,
authorization_event_id: String,
delegation_event_id: String,
}
#[derive(Debug, Deserialize)]
struct EnrollCommitResponse {
success: bool,
merkle_root: Option<String>,
leaf_index: Option<u32>,
siblings: Option<Vec<String>>,
path_bits: Option<Vec<u8>>,
error: Option<String>,
}
#[derive(Debug, Clone)]
pub struct AuthorizationEvent {
pub event_id: EventId,
pub enterprise_pubkey: PublicKey,
pub client_id: Option<String>,
pub custom_relays: Vec<String>,
pub created_at: Timestamp,
}
#[derive(Debug, Clone)]
pub struct DelegationEvent {
pub event_id: EventId,
pub human_pubkey: PublicKey,
pub agent_npub: String,
pub created_at: Timestamp,
}
pub struct EnrollmentBootstrap {
nostr_client: NostrClient,
api_client: reqwest::Client,
api_base_url: String,
}
impl EnrollmentBootstrap {
pub fn new(nostr_client: NostrClient, api_base_url: String) -> Self {
let api_client = reqwest::Client::builder()
.timeout(Duration::from_secs(30))
.build()
.expect("Failed to create HTTP client");
Self {
nostr_client,
api_client,
api_base_url,
}
}
pub fn with_defaults(nostr_client: NostrClient) -> Self {
Self::new(nostr_client, DEFAULT_API_URL.to_string())
}
pub async fn watch_for_enrollment<S: SecureStorage>(
&mut self,
identity: &AgentIdentity<S>,
human_npub: &str,
poll_interval: Duration,
max_attempts: u32,
) -> Result<EnrollmentResult> {
let agent_npub = self.nostr_client.agent_npub();
for attempt in 1..=max_attempts {
eprintln!("[enrollment] Poll attempt {}/{}", attempt, max_attempts);
let auth_events = self.poll_authorization_events(agent_npub).await?;
let delegation_events = self.poll_delegation_events(human_npub).await?;
if let (Some(auth), Some(delegation)) = (auth_events.first(), delegation_events.first()) {
eprintln!("[enrollment] Found authorization: {}", auth.event_id.to_hex());
eprintln!("[enrollment] Found delegation: {}", delegation.event_id.to_hex());
if !auth.custom_relays.is_empty() {
eprintln!("[enrollment] Enterprise specified custom relays: {:?}", auth.custom_relays);
if let Err(e) = self.nostr_client.add_custom_relays(&auth.custom_relays).await {
eprintln!("[enrollment] Warning: Failed to add custom relays: {}", e);
}
}
return self.execute_enrollment(
&auth.event_id.to_hex(),
&delegation.event_id.to_hex(),
identity,
).await;
}
if attempt < max_attempts {
tokio::time::sleep(poll_interval).await;
}
}
Ok(EnrollmentResult {
success: false,
merkle_witness: None,
merkle_root: None,
leaf_index: None,
error: Some("Enrollment events not found after max attempts".to_string()),
})
}
async fn poll_authorization_events(&self, agent_npub: &str) -> Result<Vec<AuthorizationEvent>> {
let events = self.nostr_client.poll_enrollment_events(agent_npub).await?;
let auth_events: Vec<AuthorizationEvent> = events
.into_iter()
.map(|e| {
let client_id = e.tags.iter()
.find(|t| t.as_vec().first().map(|s| s.as_str()) == Some("client_id"))
.and_then(|t| t.as_vec().get(1).cloned());
let custom_relays: Vec<String> = e.tags.iter()
.find(|t| t.as_vec().first().map(|s| s.as_str()) == Some("relays"))
.map(|t| t.as_vec().iter().skip(1).cloned().collect())
.unwrap_or_default();
AuthorizationEvent {
event_id: e.id,
enterprise_pubkey: e.pubkey,
client_id,
custom_relays,
created_at: e.created_at,
}
})
.collect();
Ok(auth_events)
}
async fn poll_delegation_events(&self, human_npub: &str) -> Result<Vec<DelegationEvent>> {
let events = self.nostr_client.poll_delegation_events(human_npub).await?;
let delegation_events: Vec<DelegationEvent> = events
.into_iter()
.map(|e| {
let agent_npub = e.tags.iter()
.find(|t| t.as_vec().first().map(|s| s.as_str()) == Some("p"))
.and_then(|t| t.as_vec().get(1).cloned())
.unwrap_or_default();
DelegationEvent {
event_id: e.id,
human_pubkey: e.pubkey,
agent_npub,
created_at: e.created_at,
}
})
.collect();
Ok(delegation_events)
}
pub async fn execute_enrollment<S: SecureStorage>(
&self,
authorization_event_id: &str,
delegation_event_id: &str,
identity: &AgentIdentity<S>,
) -> Result<EnrollmentResult> {
let leaf_secret = identity.get_leaf_secret()?;
let leaf_commitment = bn254_leaf_commitment(&leaf_secret);
let leaf_commitment_hex = fr_to_hex(&leaf_commitment);
eprintln!("[enrollment] Leaf commitment: {}", leaf_commitment_hex);
let request = EnrollCommitRequest {
leaf_commitment: leaf_commitment_hex,
authorization_event_id: authorization_event_id.to_string(),
delegation_event_id: delegation_event_id.to_string(),
};
let url = format!("{}/v1/enroll/commit", self.api_base_url);
eprintln!("[enrollment] Calling {}", url);
let response = self.api_client
.post(&url)
.header("Content-Type", "application/json")
.json(&request)
.send()
.await
.map_err(|e| anyhow!("API request failed: {}", e))?;
let status = response.status();
if !status.is_success() {
let error_text = response.text().await.unwrap_or_default();
return Ok(EnrollmentResult {
success: false,
merkle_witness: None,
merkle_root: None,
leaf_index: None,
error: Some(format!("API returned {}: {}", status, error_text)),
});
}
let api_response: EnrollCommitResponse = response.json().await
.map_err(|e| anyhow!("Failed to parse API response: {}", e))?;
if !api_response.success {
return Ok(EnrollmentResult {
success: false,
merkle_witness: None,
merkle_root: None,
leaf_index: None,
error: api_response.error,
});
}
let merkle_witness = match (api_response.siblings, api_response.path_bits) {
(Some(siblings), Some(path_bits)) => {
if siblings.len() != 20 || path_bits.len() != 20 {
return Err(anyhow!(
"Invalid witness: expected 20 siblings and 20 path_bits, got {} and {}",
siblings.len(), path_bits.len()
));
}
Some(MerkleWitness { siblings, path_bits })
}
_ => None,
};
if let Some(ref witness) = merkle_witness {
if let Err(e) = self.cache_witness(witness, &api_response.merkle_root, identity).await {
eprintln!("[enrollment] Warning: Failed to cache witness: {}", e);
}
}
Ok(EnrollmentResult {
success: true,
merkle_witness,
merkle_root: api_response.merkle_root,
leaf_index: api_response.leaf_index,
error: None,
})
}
async fn cache_witness<S: SecureStorage>(
&self,
witness: &MerkleWitness,
merkle_root: &Option<String>,
identity: &AgentIdentity<S>,
) -> Result<()> {
let witness_json = serde_json::to_vec(&WitnessCacheEntry {
siblings: witness.siblings.clone(),
path_bits: witness.path_bits.clone(),
merkle_root: merkle_root.clone(),
cached_at: current_timestamp(),
})?;
eprintln!("[enrollment] Witness cached ({} bytes)", witness_json.len());
Ok(())
}
pub fn load_cached_witness<S: SecureStorage>(
&self,
_identity: &AgentIdentity<S>,
) -> Result<Option<MerkleWitness>> {
Ok(None)
}
pub async fn needs_witness_refresh<S: SecureStorage>(
&self,
_identity: &AgentIdentity<S>,
_current_root: &str,
) -> Result<bool> {
Ok(false)
}
}
#[derive(Debug, Serialize, Deserialize)]
struct WitnessCacheEntry {
siblings: Vec<String>,
path_bits: Vec<u8>,
merkle_root: Option<String>,
cached_at: u64,
}
fn fr_to_hex(fr: &ark_bn254::Fr) -> String {
use ark_ff::PrimeField;
let bigint = fr.into_bigint();
let mut bytes = Vec::new();
for limb in bigint.0.iter().rev() {
bytes.extend_from_slice(&limb.to_be_bytes());
}
while bytes.len() > 32 && bytes[0] == 0 {
bytes.remove(0);
}
while bytes.len() < 32 {
bytes.insert(0, 0);
}
hex::encode(bytes)
}
fn current_timestamp() -> u64 {
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_secs()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_enrollment_result_serialization() {
let result = EnrollmentResult {
success: true,
merkle_witness: Some(MerkleWitness {
siblings: vec!["0".to_string(); 20],
path_bits: vec![0u8; 20],
}),
merkle_root: Some("abc123".to_string()),
leaf_index: Some(42),
error: None,
};
let json = serde_json::to_string(&result).unwrap();
assert!(json.contains("\"success\":true"));
}
#[test]
fn test_fr_to_hex() {
use ark_bn254::Fr;
use ark_ff::PrimeField;
let fr = Fr::from(255u64);
let hex = fr_to_hex(&fr);
assert_eq!(hex.len(), 64); assert!(hex.ends_with("ff"));
}
}