1use crate::errors::LitError;
9use serde::{Deserialize, Serialize};
10use sha3::{Digest, Sha3_256};
11use std::fs;
12use std::path::Path;
13
14#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
16pub struct Capability {
17 pub resource: String,
19 pub action: String,
21 #[serde(skip_serializing_if = "Option::is_none")]
23 pub caveats: Option<serde_json::Value>,
24}
25
26impl std::fmt::Display for Capability {
27 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
28 write!(f, "{}:{}", self.resource, self.action)
29 }
30}
31
32#[derive(Debug, Clone, Serialize, Deserialize)]
34pub struct UcanToken {
35 pub version: String,
37 pub issuer: String,
39 pub audience: String,
41 pub capabilities: Vec<Capability>,
43 pub expiration: i64,
45 #[serde(skip_serializing_if = "Option::is_none")]
47 pub not_before: Option<i64>,
48 pub nonce: String,
50 #[serde(default)]
52 pub proof: Vec<String>,
53 pub signature: String,
55}
56
57impl UcanToken {
58 pub fn new(
60 issuer: String,
61 audience: String,
62 capabilities: Vec<Capability>,
63 duration_secs: i64,
64 ) -> Self {
65 let now = chrono::Utc::now().timestamp();
66 let nonce = format!("{:016x}", now ^ std::process::id() as i64);
67
68 UcanToken {
69 version: "0.10.0".to_string(),
70 issuer,
71 audience,
72 capabilities,
73 expiration: now + duration_secs,
74 not_before: Some(now),
75 nonce,
76 proof: Vec::new(),
77 signature: String::new(),
78 }
79 }
80
81 pub fn sign(&mut self, private_key_hex: &str) -> Result<(), LitError> {
83 let payload = self.payload_bytes()?;
84 let private_bytes = hex::decode(private_key_hex)
85 .map_err(|e| LitError::general(format!("Invalid key: {}", e)))?;
86
87 let mut hasher = Sha3_256::new();
88 hasher.update(&private_bytes);
89 hasher.update(&payload);
90 let sig = hasher.finalize();
91 self.signature = hex::encode(sig);
92 Ok(())
93 }
94
95 pub fn verify(&self, issuer_public_key_hex: &str) -> Result<bool, LitError> {
97 let payload = self.payload_bytes()?;
98 let public_bytes = hex::decode(issuer_public_key_hex)
99 .map_err(|e| LitError::general(format!("Invalid key: {}", e)))?;
100
101 let mut hasher = Sha3_256::new();
102 hasher.update(&public_bytes);
103 hasher.update(&payload);
104 let expected = hasher.finalize();
105
106 let sig_bytes = hex::decode(&self.signature)
107 .map_err(|e| LitError::general(format!("Invalid signature: {}", e)))?;
108
109 Ok(subtle::ConstantTimeEq::ct_eq(sig_bytes.as_slice(), expected.as_slice()).into())
110 }
111
112 pub fn is_valid(&self) -> bool {
114 let now = chrono::Utc::now().timestamp();
115 if now > self.expiration {
116 return false;
117 }
118 if let Some(nb) = self.not_before {
119 if now < nb {
120 return false;
121 }
122 }
123 true
124 }
125
126 pub fn has_capability(&self, resource: &str, action: &str) -> bool {
128 self.capabilities.iter().any(|cap| {
129 let resource_match = cap.resource == "*"
130 || cap.resource == resource
131 || cap
132 .resource
133 .strip_suffix('*')
134 .map(|prefix| resource.starts_with(prefix))
135 .unwrap_or(false)
136 || resource.starts_with(&cap.resource);
137 resource_match && (cap.action == "*" || cap.action == action)
138 })
139 }
140
141 pub fn cid(&self) -> Result<String, LitError> {
143 let bytes = self.payload_bytes()?;
144 let hash = Sha3_256::digest(&bytes);
145 Ok(hex::encode(hash))
146 }
147
148 pub fn delegate(
150 &self,
151 new_audience: String,
152 capabilities: Vec<Capability>,
153 duration_secs: i64,
154 ) -> Result<UcanToken, LitError> {
155 for cap in &capabilities {
157 if !self.has_capability(&cap.resource, &cap.action) {
158 return Err(LitError::general(format!(
159 "Cannot delegate capability '{}:{}' — not held by parent token",
160 cap.resource, cap.action
161 )));
162 }
163 }
164
165 let parent_cid = self.cid()?;
166 let mut child = UcanToken::new(
167 self.audience.clone(), new_audience,
169 capabilities,
170 duration_secs,
171 );
172 child.proof.push(parent_cid);
173
174 for p in &self.proof {
176 child.proof.push(p.clone());
177 }
178
179 Ok(child)
180 }
181
182 fn payload_bytes(&self) -> Result<Vec<u8>, LitError> {
183 let payload = serde_json::json!({
185 "version": self.version,
186 "issuer": self.issuer,
187 "audience": self.audience,
188 "capabilities": self.capabilities,
189 "expiration": self.expiration,
190 "not_before": self.not_before,
191 "nonce": self.nonce,
192 "proof": self.proof,
193 });
194 serde_json::to_vec(&payload)
195 .map_err(|e| LitError::general(format!("Failed to serialize UCAN: {}", e)))
196 }
197}
198
199pub fn ucan_dir(repo_root: &Path) -> std::path::PathBuf {
201 repo_root.join(".lit").join("ucan")
202}
203
204pub fn save_token(repo_root: &Path, token: &UcanToken) -> Result<String, LitError> {
206 let dir = ucan_dir(repo_root);
207 fs::create_dir_all(&dir)
208 .map_err(|e| LitError::io(format!("Failed to create UCAN dir: {}", e)))?;
209
210 let cid = token.cid()?;
211 let path = dir.join(format!("{}.json", &cid[..16]));
212 let json = serde_json::to_string_pretty(token)
213 .map_err(|e| LitError::general(format!("Failed to serialize token: {}", e)))?;
214 fs::write(&path, json).map_err(|e| LitError::io(format!("Failed to write token: {}", e)))?;
215 Ok(cid)
216}
217
218pub fn load_tokens_for(repo_root: &Path, audience_did: &str) -> Result<Vec<UcanToken>, LitError> {
220 let dir = ucan_dir(repo_root);
221 if !dir.exists() {
222 return Ok(Vec::new());
223 }
224
225 let mut tokens = Vec::new();
226 for entry in fs::read_dir(&dir).map_err(|e| LitError::io(format!("IO error: {}", e)))? {
227 let entry = entry.map_err(|e| LitError::io(format!("IO error: {}", e)))?;
228 if entry.path().extension().is_some_and(|e| e == "json") {
229 if let Ok(json) = fs::read_to_string(entry.path()) {
230 if let Ok(token) = serde_json::from_str::<UcanToken>(&json) {
231 if token.audience == audience_did && token.is_valid() {
232 tokens.push(token);
233 }
234 }
235 }
236 }
237 }
238 Ok(tokens)
239}
240
241pub fn revoke_token(repo_root: &Path, cid_prefix: &str) -> Result<(), LitError> {
243 let dir = ucan_dir(repo_root);
244 let path = dir.join(format!("{}.json", cid_prefix));
245 if path.exists() {
246 fs::remove_file(&path)
247 .map_err(|e| LitError::io(format!("Failed to revoke token: {}", e)))?;
248 Ok(())
249 } else {
250 Err(LitError::general(format!(
251 "Token not found: {}",
252 cid_prefix
253 )))
254 }
255}
256
257#[cfg(test)]
258mod tests {
259 use super::*;
260
261 #[test]
262 fn test_ucan_creation() {
263 let token = UcanToken::new(
264 "did:lit:issuer123".to_string(),
265 "did:lit:audience456".to_string(),
266 vec![Capability {
267 resource: "branch:main".to_string(),
268 action: "push".to_string(),
269 caveats: None,
270 }],
271 3600,
272 );
273 assert!(token.is_valid());
274 assert!(token.has_capability("branch:main", "push"));
275 assert!(!token.has_capability("branch:main", "delete"));
276 }
277
278 #[test]
279 fn test_ucan_expiration() {
280 let mut token = UcanToken::new(
281 "did:lit:a".to_string(),
282 "did:lit:b".to_string(),
283 vec![],
284 -1, );
286 token.not_before = None;
287 assert!(!token.is_valid());
288 }
289
290 #[test]
291 fn test_ucan_delegation() {
292 let parent = UcanToken::new(
293 "did:lit:root".to_string(),
294 "did:lit:agent1".to_string(),
295 vec![Capability {
296 resource: "branch:*".to_string(),
297 action: "*".to_string(),
298 caveats: None,
299 }],
300 3600,
301 );
302
303 let child = parent
304 .delegate(
305 "did:lit:agent2".to_string(),
306 vec![Capability {
307 resource: "branch:feature".to_string(),
308 action: "push".to_string(),
309 caveats: None,
310 }],
311 1800,
312 )
313 .unwrap();
314
315 assert_eq!(child.issuer, "did:lit:agent1");
316 assert_eq!(child.audience, "did:lit:agent2");
317 assert!(!child.proof.is_empty());
318 }
319}