1use anyhow::Result;
9use serde::{Deserialize, Serialize};
10use std::fmt;
11
12#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
14pub enum IdentityProvider {
15 I1is,
17 GitHub,
19 Local,
21}
22
23#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
25pub struct Identity {
26 pub provider: IdentityProvider,
28 pub username: String,
30 pub display_name: Option<String>,
32 pub public_key: Option<String>,
34}
35
36impl Identity {
37 pub fn i1is(username: &str) -> Self {
39 Identity {
40 provider: IdentityProvider::I1is,
41 username: username.to_string(),
42 display_name: None,
43 public_key: None,
44 }
45 }
46
47 pub fn github(username: &str) -> Self {
49 Identity {
50 provider: IdentityProvider::GitHub,
51 username: username.to_string(),
52 display_name: None,
53 public_key: None,
54 }
55 }
56
57 pub fn local(username: &str) -> Self {
59 Identity {
60 provider: IdentityProvider::Local,
61 username: username.to_string(),
62 display_name: None,
63 public_key: None,
64 }
65 }
66
67 pub fn parse(s: &str) -> Result<Self> {
74 if s.ends_with("@i1.is") {
75 let username = s.trim_end_matches("@i1.is");
76 Ok(Identity::i1is(username))
77 } else if s.starts_with("github:") {
78 let username = s.trim_start_matches("github:");
79 Ok(Identity::github(username))
80 } else if s.contains('@') {
81 let username = s.split('@').next().unwrap_or(s);
83 Ok(Identity::i1is(username))
84 } else {
85 Ok(Identity::local(s))
87 }
88 }
89
90 pub fn canonical(&self) -> String {
92 match self.provider {
93 IdentityProvider::I1is => format!("{}@i1.is", self.username),
94 IdentityProvider::GitHub => format!("github:{}", self.username),
95 IdentityProvider::Local => format!("local:{}", self.username),
96 }
97 }
98
99 pub fn is_ai(&self) -> bool {
101 self.username.starts_with("claude-")
103 || self.username.starts_with("ai-")
104 || self.username.starts_with("assistant-")
105 }
106}
107
108impl fmt::Display for Identity {
109 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
110 write!(f, "{}", self.canonical())
111 }
112}
113
114pub struct IdentityResolver;
116
117impl IdentityResolver {
118 pub async fn resolve_i1is(username: &str) -> Result<Identity> {
120 Ok(Identity {
123 provider: IdentityProvider::I1is,
124 username: username.to_string(),
125 display_name: None,
126 public_key: None,
127 })
128 }
129
130 pub async fn resolve_github(username: &str) -> Result<Identity> {
132 Ok(Identity {
135 provider: IdentityProvider::GitHub,
136 username: username.to_string(),
137 display_name: None,
138 public_key: None,
139 })
140 }
141
142 pub fn current() -> Result<Identity> {
144 if let Ok(id) = std::env::var("I1IS_USER") {
146 return Ok(Identity::i1is(&id));
147 }
148
149 if let Ok(user) = std::env::var("GITHUB_USER") {
151 return Ok(Identity::github(&user));
152 }
153
154 let username = whoami::username();
156 Ok(Identity::local(&username))
157 }
158
159 pub fn verify(_identity: &Identity, _message: &[u8], _signature: &[u8]) -> Result<bool> {
161 Ok(true)
163 }
164}
165
166#[cfg(test)]
167mod tests {
168 use super::*;
169
170 #[test]
171 fn test_parse_identity() {
172 let i1 = Identity::parse("hue@i1.is").unwrap();
173 assert_eq!(i1.provider, IdentityProvider::I1is);
174 assert_eq!(i1.username, "hue");
175
176 let gh = Identity::parse("github:8b-is").unwrap();
177 assert_eq!(gh.provider, IdentityProvider::GitHub);
178 assert_eq!(gh.username, "8b-is");
179
180 let local = Identity::parse("alice").unwrap();
181 assert_eq!(local.provider, IdentityProvider::Local);
182 assert_eq!(local.username, "alice");
183 }
184
185 #[test]
186 fn test_canonical() {
187 assert_eq!(Identity::i1is("hue").canonical(), "hue@i1.is");
188 assert_eq!(Identity::github("8b-is").canonical(), "github:8b-is");
189 assert_eq!(Identity::local("alice").canonical(), "local:alice");
190 }
191}