diff --git a/crates/gitlawb-node/src/api/agents.rs b/crates/gitlawb-node/src/api/agents.rs index 39e7063..dea088a 100644 --- a/crates/gitlawb-node/src/api/agents.rs +++ b/crates/gitlawb-node/src/api/agents.rs @@ -8,6 +8,37 @@ use serde::{Deserialize, Serialize}; use crate::error::{AppError, Result}; use crate::state::AppState; +fn normalize_agent_did(did: &str) -> String { + if did.starts_with("did:") { + did.to_string() + } else { + format!("did:key:{did}") + } +} + +fn agent_key_segment(did: &str) -> &str { + did.split(':').next_back().unwrap_or(did) +} + +async fn resolve_agent_did(state: &AppState, did: &str) -> Result { + let normalized_did = normalize_agent_did(did); + if state.db.get_agent(&normalized_did).await?.is_some() { + return Ok(normalized_did); + } + + let requested_key = agent_key_segment(&normalized_did); + let matching_agent = state + .db + .list_agents(None) + .await? + .into_iter() + .find(|agent| agent_key_segment(&agent.did).starts_with(requested_key)); + + Ok(matching_agent + .map(|agent| agent.did) + .unwrap_or(normalized_did)) +} + #[derive(Debug, Serialize)] pub struct TrustResponse { pub did: String, @@ -66,11 +97,12 @@ pub async fn show_agent( State(state): State, Path(did): Path, ) -> Result<(StatusCode, Json)> { + let normalized_did = resolve_agent_did(&state, &did).await?; let agent = state .db - .get_agent(&did) + .get_agent(&normalized_did) .await? - .ok_or_else(|| AppError::NotFound(format!("agent {did} not found")))?; + .ok_or_else(|| AppError::NotFound(format!("agent {normalized_did} not found")))?; Ok(( StatusCode::OK, Json(AgentResponse { @@ -88,14 +120,38 @@ pub async fn get_trust( State(state): State, Path(did): Path, ) -> Result> { - let trust_score = state.db.get_trust_score(&did).await?; - let push_count = state.db.get_push_count(&did).await?; + let normalized_did = resolve_agent_did(&state, &did).await?; + let trust_score = state.db.get_trust_score(&normalized_did).await?; + let push_count = state.db.get_push_count(&normalized_did).await?; let level = trust_level(trust_score); Ok(Json(TrustResponse { - did, + did: normalized_did, trust_score, push_count, level, })) } + +#[cfg(test)] +mod tests { + use super::{agent_key_segment, normalize_agent_did}; + + #[test] + fn normalize_agent_did_preserves_full_did() { + let did = "did:key:z6MkExample"; + + assert_eq!(normalize_agent_did(did), did); + } + + #[test] + fn normalize_agent_did_expands_bare_key() { + assert_eq!(normalize_agent_did("z6MkExample"), "did:key:z6MkExample"); + } + + #[test] + fn agent_key_segment_extracts_did_key_material() { + assert_eq!(agent_key_segment("did:key:z6MkExample"), "z6MkExample"); + assert_eq!(agent_key_segment("z6MkExample"), "z6MkExample"); + } +} diff --git a/crates/gl/src/cert.rs b/crates/gl/src/cert.rs index be51e1b..7940252 100644 --- a/crates/gl/src/cert.rs +++ b/crates/gl/src/cert.rs @@ -7,6 +7,7 @@ use clap::{Args, Subcommand}; use serde_json::Value; use crate::http::NodeClient; +use crate::identity::load_keypair_from_dir; #[derive(Args)] pub struct CertArgs { @@ -41,20 +42,25 @@ pub async fn run(args: CertArgs) -> Result<()> { } } -/// Resolve "repo" into (owner, name) — if no slash, use the node's own DID short form. +/// Resolve "repo" into (owner, name) using the caller's DID when no slash is given. async fn resolve_repo(repo: &str, node: &str) -> Result<(String, String)> { if let Some((owner, name)) = repo.split_once('/') { Ok((owner.to_string(), name.to_string())) } else { - let client = NodeClient::new(node, None); - let info: Value = client - .get("/") - .await? - .json() - .await - .context("failed to fetch node info")?; - let did = info["did"].as_str().context("node info missing 'did'")?; - let short = did.split(':').next_back().unwrap_or(did).to_string(); + let short = if let Ok(kp) = load_keypair_from_dir(None) { + let did = kp.did().to_string(); + did.split(':').next_back().unwrap_or(&did).to_string() + } else { + let client = NodeClient::new(node, None); + let info: Value = client + .get("/") + .await? + .json() + .await + .context("failed to fetch node info")?; + let did = info["did"].as_str().context("node info missing 'did'")?; + did.split(':').next_back().unwrap_or(did).to_string() + }; Ok((short, repo.to_string())) } } @@ -94,15 +100,16 @@ async fn cmd_show(repo: String, id: String, node: String) -> Result<()> { let (owner, name) = resolve_repo(&repo, &node).await?; let client = NodeClient::new(&node, None); + let id = resolve_cert_id(&client, &owner, &name, &id).await?; // Fetch the certificate let path = format!("/api/v1/repos/{owner}/{name}/certs/{id}"); - let cert: Value = client + let resp = client .get(&path) .await? - .json() - .await + .error_for_status() .context("certificate not found")?; + let cert: Value = resp.json().await.context("certificate not found")?; let cert_id = cert["id"].as_str().unwrap_or("?"); let ref_name = cert["ref_name"].as_str().unwrap_or("?"); @@ -155,3 +162,33 @@ async fn cmd_show(repo: String, id: String, node: String) -> Result<()> { Ok(()) } + +async fn resolve_cert_id(client: &NodeClient, owner: &str, name: &str, id: &str) -> Result { + if id.len() >= 36 { + return Ok(id.to_string()); + } + + let path = format!("/api/v1/repos/{owner}/{name}/certs"); + let resp: Value = client + .get(&path) + .await? + .error_for_status() + .context("failed to list certificates")? + .json() + .await + .context("failed to list certificates")?; + + let certs = resp["certificates"].as_array().cloned().unwrap_or_default(); + let matches: Vec = certs + .iter() + .filter_map(|cert| cert["id"].as_str()) + .filter(|cert_id| cert_id.starts_with(id)) + .map(ToString::to_string) + .collect(); + + match matches.as_slice() { + [full_id] => Ok(full_id.to_string()), + [] => Ok(id.to_string()), + _ => anyhow::bail!("certificate prefix {id} matches multiple certificates"), + } +}