Skip to main content

rho_core/providers/
mod.rs

1use std::path::PathBuf;
2
3use crate::{RhoResult, validate_relative_safe_path};
4use serde::{Deserialize, Serialize};
5
6pub mod github;
7
8#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
9pub struct IdentityLocator {
10    pub provider: String,
11    pub handle: String,
12}
13
14#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
15pub struct RhoIdentity {
16    pub id: String,
17    pub provider: String,
18    pub handle: String,
19    #[serde(default, skip_serializing_if = "Option::is_none")]
20    pub display_name: Option<String>,
21    #[serde(default, skip_serializing_if = "Vec::is_empty")]
22    pub keys: Vec<IdentityKey>,
23    #[serde(default, skip_serializing_if = "Vec::is_empty")]
24    pub proofs: Vec<IdentityProof>,
25    #[serde(default, skip_serializing_if = "Vec::is_empty")]
26    pub discovery: Vec<DiscoveryLocator>,
27}
28
29#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
30pub struct IdentityKey {
31    pub id: String,
32    pub kind: IdentityKeyKind,
33    pub algorithm: String,
34    pub public_key: String,
35    pub fingerprint: String,
36}
37
38#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
39#[serde(rename_all = "snake_case")]
40pub enum IdentityKeyKind {
41    Signing,
42    Encryption,
43    Transport,
44}
45
46#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
47pub struct IdentityProof {
48    pub kind: String,
49    #[serde(default, skip_serializing_if = "Option::is_none")]
50    pub provider_url: Option<String>,
51    #[serde(default, skip_serializing_if = "Option::is_none")]
52    pub claim: Option<String>,
53    #[serde(default, skip_serializing_if = "Option::is_none")]
54    pub proof_url: Option<String>,
55    #[serde(default, skip_serializing_if = "Option::is_none")]
56    pub verified_at: Option<String>,
57}
58
59#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
60pub struct DiscoveryLocator {
61    pub kind: String,
62    pub url: String,
63}
64
65pub trait IdentityProvider {
66    fn provider(&self) -> &'static str;
67    fn validate_handle(&self, handle: &str) -> RhoResult<()>;
68
69    fn identity_id(&self, handle: &str) -> RhoResult<String> {
70        self.validate_handle(handle)?;
71        Ok(format!("rho://id/{}/{}", self.provider(), handle))
72    }
73
74    fn handle_from_identity_id(&self, identity_id: &str) -> RhoResult<String> {
75        let locator = parse_identity_id(identity_id)?;
76        if locator.provider != self.provider() {
77            return Err(format!(
78                "unsupported identity provider for {}: {}",
79                self.provider(),
80                identity_id
81            )
82            .into());
83        }
84        self.validate_handle(&locator.handle)?;
85        Ok(locator.handle)
86    }
87
88    fn identity(&self, handle: &str) -> RhoResult<RhoIdentity> {
89        self.validate_handle(handle)?;
90        Ok(RhoIdentity {
91            id: self.identity_id(handle)?,
92            provider: self.provider().to_string(),
93            handle: handle.to_string(),
94            display_name: None,
95            keys: Vec::new(),
96            proofs: Vec::new(),
97            discovery: Vec::new(),
98        })
99    }
100}
101
102pub fn parse_identity_id(identity_id: &str) -> RhoResult<IdentityLocator> {
103    let Some(rest) = identity_id.strip_prefix("rho://id/") else {
104        return Err(format!("identity id must start with rho://id/: {identity_id}").into());
105    };
106    let Some((provider, handle)) = rest.split_once('/') else {
107        return Err(format!("identity id must include provider and handle: {identity_id}").into());
108    };
109    if provider.is_empty() || handle.is_empty() || handle.contains('/') {
110        return Err(format!("invalid identity id: {identity_id}").into());
111    }
112    validate_relative_safe_path(provider)?;
113    validate_relative_safe_path(handle)?;
114    Ok(IdentityLocator {
115        provider: provider.to_string(),
116        handle: handle.to_string(),
117    })
118}
119
120pub fn identity_inbox_relative_path(identity_id: &str) -> RhoResult<PathBuf> {
121    let locator = parse_identity_id(identity_id)?;
122    Ok(PathBuf::from("id")
123        .join(locator.provider)
124        .join(locator.handle))
125}
126
127#[cfg(test)]
128mod tests {
129    use super::*;
130
131    #[derive(Debug, Clone, Copy)]
132    struct TestProvider;
133
134    impl IdentityProvider for TestProvider {
135        fn provider(&self) -> &'static str {
136            "test"
137        }
138
139        fn validate_handle(&self, handle: &str) -> RhoResult<()> {
140            if handle.is_empty() || handle.contains('/') {
141                return Err("invalid handle".into());
142            }
143            Ok(())
144        }
145    }
146
147    #[test]
148    fn builds_provider_neutral_identity() {
149        let identity = TestProvider.identity("alice").unwrap();
150        assert_eq!(identity.id, "rho://id/test/alice");
151        assert_eq!(identity.provider, "test");
152        assert!(identity.keys.is_empty());
153    }
154
155    #[test]
156    fn parses_identity_locator() {
157        let locator = parse_identity_id("rho://id/nostr/npub1abc").unwrap();
158        assert_eq!(locator.provider, "nostr");
159        assert_eq!(locator.handle, "npub1abc");
160    }
161}