Skip to main content

st/collab/
identity.rs

1//! Identity Resolution - Who are you?
2//!
3//! Supports multiple identity providers:
4//! - i1.is - Our identity service (primary)
5//! - GitHub - OAuth + SSH keys
6//! - Local - Machine users (for standalone)
7
8use anyhow::Result;
9use serde::{Deserialize, Serialize};
10use std::fmt;
11
12/// Identity provider
13#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
14pub enum IdentityProvider {
15    /// i1.is identity service
16    I1is,
17    /// GitHub OAuth
18    GitHub,
19    /// Local machine user
20    Local,
21}
22
23/// A user identity
24#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
25pub struct Identity {
26    /// The provider
27    pub provider: IdentityProvider,
28    /// Username/identifier
29    pub username: String,
30    /// Optional display name
31    pub display_name: Option<String>,
32    /// Public key (for verification)
33    pub public_key: Option<String>,
34}
35
36impl Identity {
37    /// Create an i1.is identity
38    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    /// Create a GitHub identity
48    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    /// Create a local identity
58    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    /// Parse an identity string
68    ///
69    /// Formats:
70    /// - "user@i1.is" -> i1.is identity
71    /// - "github:user" -> GitHub identity
72    /// - "user" -> Local identity (fallback)
73    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            // Assume i1.is format: user@i1.is
82            let username = s.split('@').next().unwrap_or(s);
83            Ok(Identity::i1is(username))
84        } else {
85            // Local user
86            Ok(Identity::local(s))
87        }
88    }
89
90    /// Get the canonical string representation
91    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    /// Check if this is an AI identity
100    pub fn is_ai(&self) -> bool {
101        // AI identities typically have special prefixes
102        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
114/// Resolve an identity from a provider
115pub struct IdentityResolver;
116
117impl IdentityResolver {
118    /// Resolve an i1.is identity (fetch public key, display name, etc.)
119    pub async fn resolve_i1is(username: &str) -> Result<Identity> {
120        // TODO: Call i1.is API to get user info
121        // For now, return basic identity
122        Ok(Identity {
123            provider: IdentityProvider::I1is,
124            username: username.to_string(),
125            display_name: None,
126            public_key: None,
127        })
128    }
129
130    /// Resolve a GitHub identity via API
131    pub async fn resolve_github(username: &str) -> Result<Identity> {
132        // TODO: Call GitHub API to get user info + SSH keys
133        // For now, return basic identity
134        Ok(Identity {
135            provider: IdentityProvider::GitHub,
136            username: username.to_string(),
137            display_name: None,
138            public_key: None,
139        })
140    }
141
142    /// Get current user's identity from environment
143    pub fn current() -> Result<Identity> {
144        // Check for i1.is identity first
145        if let Ok(id) = std::env::var("I1IS_USER") {
146            return Ok(Identity::i1is(&id));
147        }
148
149        // Check for GitHub user
150        if let Ok(user) = std::env::var("GITHUB_USER") {
151            return Ok(Identity::github(&user));
152        }
153
154        // Fall back to local user
155        let username = whoami::username();
156        Ok(Identity::local(&username))
157    }
158
159    /// Verify an identity signature
160    pub fn verify(_identity: &Identity, _message: &[u8], _signature: &[u8]) -> Result<bool> {
161        // TODO: Implement signature verification using public key
162        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}