1use std::fmt;
2use std::sync::Arc;
3
4use sha2::{Digest, Sha256};
5
6pub type AgentId = Arc<str>;
7pub type Channel = &'static str;
8
9pub const WHATSAPP: Channel = "whatsapp";
10pub const TELEGRAM: Channel = "telegram";
11pub const GOOGLE: Channel = "google";
12pub const EMAIL: Channel = "email";
13
14#[derive(Copy, Clone, PartialEq, Eq, Hash)]
18pub struct Fingerprint([u8; 8]);
19
20impl Fingerprint {
21 pub fn of(value: &str) -> Self {
22 let mut hasher = Sha256::new();
23 hasher.update(value.as_bytes());
24 let digest = hasher.finalize();
25 let mut out = [0u8; 8];
26 out.copy_from_slice(&digest[..8]);
27 Self(out)
28 }
29
30 pub fn as_bytes(&self) -> &[u8; 8] {
31 &self.0
32 }
33
34 pub fn to_hex(&self) -> String {
35 hex::encode(self.0)
36 }
37}
38
39impl fmt::Debug for Fingerprint {
40 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
41 f.write_str(&self.to_hex())
42 }
43}
44
45impl fmt::Display for Fingerprint {
46 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
47 f.write_str(&self.to_hex())
48 }
49}
50
51#[derive(Clone)]
57pub struct CredentialHandle {
58 channel: Channel,
59 account_id: Arc<str>,
60 agent_id: AgentId,
61 fingerprint: Fingerprint,
62}
63
64impl CredentialHandle {
65 pub fn new(channel: Channel, account_id: &str, agent_id: &str) -> Self {
66 Self {
67 channel,
68 account_id: Arc::from(account_id),
69 agent_id: Arc::from(agent_id),
70 fingerprint: Fingerprint::of(account_id),
71 }
72 }
73
74 pub fn channel(&self) -> Channel {
75 self.channel
76 }
77
78 pub fn agent_id(&self) -> &str {
79 &self.agent_id
80 }
81
82 pub fn fingerprint(&self) -> Fingerprint {
83 self.fingerprint
84 }
85
86 pub fn account_id_raw(&self) -> &str {
89 &self.account_id
90 }
91}
92
93impl fmt::Debug for CredentialHandle {
94 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
95 f.debug_struct("CredentialHandle")
96 .field("channel", &self.channel)
97 .field("agent", &self.agent_id)
98 .field("fp", &self.fingerprint)
99 .finish()
100 }
101}
102
103#[cfg(test)]
104mod tests {
105 use super::*;
106
107 #[test]
108 fn fingerprint_is_stable() {
109 let a = Fingerprint::of("ana@gmail.com");
110 let b = Fingerprint::of("ana@gmail.com");
111 assert_eq!(a, b);
112 }
113
114 #[test]
115 fn fingerprint_differs_between_ids() {
116 let a = Fingerprint::of("ana@gmail.com");
117 let b = Fingerprint::of("kate@gmail.com");
118 assert_ne!(a, b);
119 }
120
121 #[test]
122 fn handle_debug_does_not_leak_account_id() {
123 let h = CredentialHandle::new(WHATSAPP, "+573001234567", "ana");
124 let rendered = format!("{:?}", h);
125 assert!(!rendered.contains("573001234567"));
126 assert!(rendered.contains("whatsapp"));
127 assert!(rendered.contains("ana"));
128 assert!(rendered.contains(&h.fingerprint().to_hex()));
129 }
130
131 #[test]
132 fn fingerprint_display_is_hex() {
133 let fp = Fingerprint::of("x");
134 assert_eq!(fp.to_hex().len(), 16);
135 assert!(fp.to_hex().chars().all(|c| c.is_ascii_hexdigit()));
136 }
137}