use std::path::Path;
use solid_pod_rs::did_nostr_types::{render_did_document, NostrPubkey};
use crate::config::{find_git_dir, run_git_config};
use crate::error::GitError;
pub const AGENT_DID_FILE: &str = "agent.did.json";
pub const NOSTR_PRIVKEY_KEY: &str = "nostr.privkey";
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct AgentIdentityWritten {
pub did_path: std::path::PathBuf,
pub did: String,
pub privkey_configured: bool,
}
pub async fn write_agent_identity(
pod_root: &Path,
pubkey_hex: &str,
privkey_hex: Option<&str>,
) -> Result<AgentIdentityWritten, GitError> {
let pk = NostrPubkey::from_hex(pubkey_hex)
.map_err(|e| GitError::PathTraversal(format!("agent.did.json: invalid pubkey: {e}")))?;
let doc = render_did_document(&pk);
let did = doc["id"].as_str().unwrap_or_default().to_string();
let body = serde_json::to_string_pretty(&doc)
.map_err(|e| GitError::MalformedCgi(format!("agent.did.json serialise: {e}")))?;
let did_path = pod_root.join(AGENT_DID_FILE);
tokio::fs::write(&did_path, body.as_bytes())
.await
.map_err(GitError::Io)?;
let mut privkey_configured = false;
if let Some(sk_hex) = privkey_hex {
match find_git_dir(pod_root) {
Ok(Some(git_dir)) => {
match run_git_config(pod_root, &git_dir.git_dir, NOSTR_PRIVKEY_KEY, sk_hex).await {
Ok(()) => privkey_configured = true,
Err(e) => tracing::debug!(
target: "solid_pod_rs_git::identity",
"git config {NOSTR_PRIVKEY_KEY} skipped (non-fatal): {e}"
),
}
}
_ => tracing::debug!(
target: "solid_pod_rs_git::identity",
"pod_root {} is not a git repo; nostr.privkey not configured",
pod_root.display()
),
}
}
Ok(AgentIdentityWritten {
did_path,
did,
privkey_configured,
})
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::TempDir;
use tokio::process::Command;
const PK_HEX: &str = "0000000000000000000000000000000000000000000000000000000000000001";
#[tokio::test]
async fn writes_canonical_agent_did_json() {
let td = TempDir::new().unwrap();
let out = write_agent_identity(td.path(), PK_HEX, None)
.await
.unwrap();
assert_eq!(out.did, format!("did:nostr:{PK_HEX}"));
assert!(!out.privkey_configured, "no privkey supplied");
let written = std::fs::read_to_string(out.did_path).unwrap();
let v: serde_json::Value = serde_json::from_str(&written).unwrap();
assert_eq!(v["type"], "DIDNostr");
assert_eq!(v["@context"][0], "https://www.w3.org/ns/cid/v1");
assert_eq!(v["@context"][1], "https://w3id.org/nostr/context");
let vm = &v["verificationMethod"][0];
assert_eq!(vm["type"], "Multikey");
assert_eq!(vm["publicKeyMultibase"], format!("fe70102{PK_HEX}"));
assert!(vm.get("publicKeyHex").is_none(), "2019 publicKeyHex dropped");
assert_eq!(v["authentication"][0], "#key1");
assert!(v["service"].as_array().unwrap().is_empty());
}
#[tokio::test]
async fn rejects_malformed_pubkey() {
let td = TempDir::new().unwrap();
let res = write_agent_identity(td.path(), "not-hex", None).await;
assert!(res.is_err(), "malformed pubkey must be refused, not written");
assert!(!td.path().join(AGENT_DID_FILE).exists());
}
#[tokio::test]
async fn configures_nostr_privkey_in_repo() {
let td = TempDir::new().unwrap();
let repo = td.path();
let status = Command::new("git")
.arg("init")
.arg(repo)
.output()
.await;
let status = match status {
Ok(o) => o.status,
Err(_) => return, };
assert!(status.success());
let sk_hex = "1111111111111111111111111111111111111111111111111111111111111111";
let out = write_agent_identity(repo, PK_HEX, Some(sk_hex))
.await
.unwrap();
assert!(out.privkey_configured, "privkey must be git-configured in a repo");
let gd = find_git_dir(repo).unwrap().unwrap();
let read = Command::new("git")
.arg("config")
.arg("--local")
.arg(NOSTR_PRIVKEY_KEY)
.current_dir(repo)
.env("GIT_DIR", &gd.git_dir)
.output()
.await
.unwrap();
assert!(read.status.success());
assert_eq!(String::from_utf8_lossy(&read.stdout).trim(), sk_hex);
let doc = std::fs::read_to_string(out.did_path).unwrap();
assert!(
!doc.contains(sk_hex),
"privkey must NEVER appear in agent.did.json"
);
}
}