solid_pod_rs_forge/
ownership.rs1use solid_pod_rs::did_nostr_types::is_valid_hex_pubkey;
12
13#[derive(Debug, Clone, PartialEq, Eq)]
16pub enum ForgeAgent {
17 Pod {
19 webid: String,
21 username: String,
23 },
24 Nostr {
26 pubkey_hex: String,
28 },
29 Anonymous,
31}
32
33impl ForgeAgent {
34 #[must_use]
37 pub fn owner(&self) -> Option<&str> {
38 match self {
39 ForgeAgent::Pod { username, .. } => Some(username),
40 ForgeAgent::Nostr { pubkey_hex } => Some(pubkey_hex),
41 ForgeAgent::Anonymous => None,
42 }
43 }
44
45 #[must_use]
49 pub fn author_id(&self) -> String {
50 match self {
51 ForgeAgent::Pod { webid, .. } => webid.clone(),
52 ForgeAgent::Nostr { pubkey_hex } => format!("did:nostr:{pubkey_hex}"),
53 ForgeAgent::Anonymous => "anonymous".to_string(),
54 }
55 }
56
57 #[must_use]
60 pub fn is_nostr(&self) -> bool {
61 matches!(self, ForgeAgent::Nostr { .. })
62 }
63
64 #[must_use]
68 pub fn can_write_namespace(&self, owner_segment: &str) -> bool {
69 match self.owner() {
70 Some(o) => o == owner_segment,
71 None => false,
72 }
73 }
74}
75
76#[derive(Debug, Clone, Copy, PartialEq, Eq)]
78pub enum OwnerKind {
79 PodUsername,
81 NostrHex,
83}
84
85#[must_use]
88pub fn classify_owner(segment: &str) -> OwnerKind {
89 if is_valid_hex_pubkey(segment) {
90 OwnerKind::NostrHex
91 } else {
92 OwnerKind::PodUsername
93 }
94}
95
96#[must_use]
101pub fn valid_name_segment(seg: &str) -> bool {
102 if seg.is_empty() || seg.len() > 100 {
103 return false;
104 }
105 if seg == "." || seg == ".." || seg.starts_with('.') {
106 return false;
107 }
108 if seg.contains("..") {
109 return false;
110 }
111 seg.chars()
112 .all(|c| c.is_ascii_alphanumeric() || c == '-' || c == '_' || c == '.')
113}
114
115#[cfg(test)]
116mod tests {
117 use super::*;
118
119 fn hex64() -> String {
120 "a".repeat(64)
121 }
122
123 #[test]
124 fn pod_agent_owner_and_author() {
125 let a = ForgeAgent::Pod {
126 webid: "https://pod.example/alice/profile/card#me".into(),
127 username: "alice".into(),
128 };
129 assert_eq!(a.owner(), Some("alice"));
130 assert_eq!(a.author_id(), "https://pod.example/alice/profile/card#me");
131 assert!(!a.is_nostr());
132 }
133
134 #[test]
135 fn nostr_agent_owner_and_author() {
136 let a = ForgeAgent::Nostr {
137 pubkey_hex: hex64(),
138 };
139 assert_eq!(a.owner(), Some(hex64().as_str()));
140 assert_eq!(a.author_id(), format!("did:nostr:{}", hex64()));
141 assert!(a.is_nostr());
142 }
143
144 #[test]
145 fn anonymous_cannot_write() {
146 let a = ForgeAgent::Anonymous;
147 assert_eq!(a.owner(), None);
148 assert!(!a.can_write_namespace("alice"));
149 assert_eq!(a.author_id(), "anonymous");
150 }
151
152 #[test]
153 fn namespace_guard_matches_own_namespace_only() {
154 let alice = ForgeAgent::Pod {
155 webid: "w".into(),
156 username: "alice".into(),
157 };
158 assert!(alice.can_write_namespace("alice"));
159 assert!(!alice.can_write_namespace("bob"));
160 }
161
162 #[test]
163 fn classify_distinguishes_hex_from_username() {
164 assert_eq!(classify_owner(&hex64()), OwnerKind::NostrHex);
165 assert_eq!(classify_owner("alice"), OwnerKind::PodUsername);
166 assert_eq!(classify_owner(&"a".repeat(63)), OwnerKind::PodUsername);
168 }
169
170 #[test]
171 fn name_segment_validation() {
172 assert!(valid_name_segment("my-repo"));
173 assert!(valid_name_segment("repo_2"));
174 assert!(valid_name_segment("v0.5.0"));
175 assert!(!valid_name_segment(""));
176 assert!(!valid_name_segment(".."));
177 assert!(!valid_name_segment(".hidden"));
178 assert!(!valid_name_segment("a/b"));
179 assert!(!valid_name_segment("a..b"));
180 assert!(!valid_name_segment("space bar"));
181 assert!(!valid_name_segment(&"x".repeat(101)));
182 }
183}