use anyhow::Result;
use serde::{Deserialize, Serialize};
use std::fmt;
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
pub enum IdentityProvider {
I1is,
GitHub,
Local,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
pub struct Identity {
pub provider: IdentityProvider,
pub username: String,
pub display_name: Option<String>,
pub public_key: Option<String>,
}
impl Identity {
pub fn i1is(username: &str) -> Self {
Identity {
provider: IdentityProvider::I1is,
username: username.to_string(),
display_name: None,
public_key: None,
}
}
pub fn github(username: &str) -> Self {
Identity {
provider: IdentityProvider::GitHub,
username: username.to_string(),
display_name: None,
public_key: None,
}
}
pub fn local(username: &str) -> Self {
Identity {
provider: IdentityProvider::Local,
username: username.to_string(),
display_name: None,
public_key: None,
}
}
pub fn parse(s: &str) -> Result<Self> {
if s.ends_with("@i1.is") {
let username = s.trim_end_matches("@i1.is");
Ok(Identity::i1is(username))
} else if s.starts_with("github:") {
let username = s.trim_start_matches("github:");
Ok(Identity::github(username))
} else if s.contains('@') {
let username = s.split('@').next().unwrap_or(s);
Ok(Identity::i1is(username))
} else {
Ok(Identity::local(s))
}
}
pub fn canonical(&self) -> String {
match self.provider {
IdentityProvider::I1is => format!("{}@i1.is", self.username),
IdentityProvider::GitHub => format!("github:{}", self.username),
IdentityProvider::Local => format!("local:{}", self.username),
}
}
pub fn is_ai(&self) -> bool {
self.username.starts_with("claude-")
|| self.username.starts_with("ai-")
|| self.username.starts_with("assistant-")
}
}
impl fmt::Display for Identity {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.canonical())
}
}
pub struct IdentityResolver;
impl IdentityResolver {
pub async fn resolve_i1is(username: &str) -> Result<Identity> {
Ok(Identity {
provider: IdentityProvider::I1is,
username: username.to_string(),
display_name: None,
public_key: None,
})
}
pub async fn resolve_github(username: &str) -> Result<Identity> {
Ok(Identity {
provider: IdentityProvider::GitHub,
username: username.to_string(),
display_name: None,
public_key: None,
})
}
pub fn current() -> Result<Identity> {
if let Ok(id) = std::env::var("I1IS_USER") {
return Ok(Identity::i1is(&id));
}
if let Ok(user) = std::env::var("GITHUB_USER") {
return Ok(Identity::github(&user));
}
let username = whoami::username();
Ok(Identity::local(&username))
}
pub fn verify(_identity: &Identity, _message: &[u8], _signature: &[u8]) -> Result<bool> {
Ok(true)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_identity() {
let i1 = Identity::parse("hue@i1.is").unwrap();
assert_eq!(i1.provider, IdentityProvider::I1is);
assert_eq!(i1.username, "hue");
let gh = Identity::parse("github:8b-is").unwrap();
assert_eq!(gh.provider, IdentityProvider::GitHub);
assert_eq!(gh.username, "8b-is");
let local = Identity::parse("alice").unwrap();
assert_eq!(local.provider, IdentityProvider::Local);
assert_eq!(local.username, "alice");
}
#[test]
fn test_canonical() {
assert_eq!(Identity::i1is("hue").canonical(), "hue@i1.is");
assert_eq!(Identity::github("8b-is").canonical(), "github:8b-is");
assert_eq!(Identity::local("alice").canonical(), "local:alice");
}
}