use solid_pod_rs::did_nostr_types::is_valid_hex_pubkey;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ForgeAgent {
Pod {
webid: String,
username: String,
},
Nostr {
pubkey_hex: String,
},
Anonymous,
}
impl ForgeAgent {
#[must_use]
pub fn owner(&self) -> Option<&str> {
match self {
ForgeAgent::Pod { username, .. } => Some(username),
ForgeAgent::Nostr { pubkey_hex } => Some(pubkey_hex),
ForgeAgent::Anonymous => None,
}
}
#[must_use]
pub fn author_id(&self) -> String {
match self {
ForgeAgent::Pod { webid, .. } => webid.clone(),
ForgeAgent::Nostr { pubkey_hex } => format!("did:nostr:{pubkey_hex}"),
ForgeAgent::Anonymous => "anonymous".to_string(),
}
}
#[must_use]
pub fn is_nostr(&self) -> bool {
matches!(self, ForgeAgent::Nostr { .. })
}
#[must_use]
pub fn can_write_namespace(&self, owner_segment: &str) -> bool {
match self.owner() {
Some(o) => o == owner_segment,
None => false,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum OwnerKind {
PodUsername,
NostrHex,
}
#[must_use]
pub fn classify_owner(segment: &str) -> OwnerKind {
if is_valid_hex_pubkey(segment) {
OwnerKind::NostrHex
} else {
OwnerKind::PodUsername
}
}
#[must_use]
pub fn valid_name_segment(seg: &str) -> bool {
if seg.is_empty() || seg.len() > 100 {
return false;
}
if seg == "." || seg == ".." || seg.starts_with('.') {
return false;
}
if seg.contains("..") {
return false;
}
seg.chars()
.all(|c| c.is_ascii_alphanumeric() || c == '-' || c == '_' || c == '.')
}
#[cfg(test)]
mod tests {
use super::*;
fn hex64() -> String {
"a".repeat(64)
}
#[test]
fn pod_agent_owner_and_author() {
let a = ForgeAgent::Pod {
webid: "https://pod.example/alice/profile/card#me".into(),
username: "alice".into(),
};
assert_eq!(a.owner(), Some("alice"));
assert_eq!(a.author_id(), "https://pod.example/alice/profile/card#me");
assert!(!a.is_nostr());
}
#[test]
fn nostr_agent_owner_and_author() {
let a = ForgeAgent::Nostr {
pubkey_hex: hex64(),
};
assert_eq!(a.owner(), Some(hex64().as_str()));
assert_eq!(a.author_id(), format!("did:nostr:{}", hex64()));
assert!(a.is_nostr());
}
#[test]
fn anonymous_cannot_write() {
let a = ForgeAgent::Anonymous;
assert_eq!(a.owner(), None);
assert!(!a.can_write_namespace("alice"));
assert_eq!(a.author_id(), "anonymous");
}
#[test]
fn namespace_guard_matches_own_namespace_only() {
let alice = ForgeAgent::Pod {
webid: "w".into(),
username: "alice".into(),
};
assert!(alice.can_write_namespace("alice"));
assert!(!alice.can_write_namespace("bob"));
}
#[test]
fn classify_distinguishes_hex_from_username() {
assert_eq!(classify_owner(&hex64()), OwnerKind::NostrHex);
assert_eq!(classify_owner("alice"), OwnerKind::PodUsername);
assert_eq!(classify_owner(&"a".repeat(63)), OwnerKind::PodUsername);
}
#[test]
fn name_segment_validation() {
assert!(valid_name_segment("my-repo"));
assert!(valid_name_segment("repo_2"));
assert!(valid_name_segment("v0.5.0"));
assert!(!valid_name_segment(""));
assert!(!valid_name_segment(".."));
assert!(!valid_name_segment(".hidden"));
assert!(!valid_name_segment("a/b"));
assert!(!valid_name_segment("a..b"));
assert!(!valid_name_segment("space bar"));
assert!(!valid_name_segment(&"x".repeat(101)));
}
}