use std::fmt;
use std::sync::Arc;
use sha2::{Digest, Sha256};
pub type AgentId = Arc<str>;
pub type Channel = &'static str;
pub const WHATSAPP: Channel = "whatsapp";
pub const TELEGRAM: Channel = "telegram";
pub const GOOGLE: Channel = "google";
#[derive(Copy, Clone, PartialEq, Eq, Hash)]
pub struct Fingerprint([u8; 8]);
impl Fingerprint {
pub fn of(value: &str) -> Self {
let mut hasher = Sha256::new();
hasher.update(value.as_bytes());
let digest = hasher.finalize();
let mut out = [0u8; 8];
out.copy_from_slice(&digest[..8]);
Self(out)
}
pub fn as_bytes(&self) -> &[u8; 8] {
&self.0
}
pub fn to_hex(&self) -> String {
hex::encode(self.0)
}
}
impl fmt::Debug for Fingerprint {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.to_hex())
}
}
impl fmt::Display for Fingerprint {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.to_hex())
}
}
#[derive(Clone)]
pub struct CredentialHandle {
channel: Channel,
account_id: Arc<str>,
agent_id: AgentId,
fingerprint: Fingerprint,
}
impl CredentialHandle {
pub fn new(channel: Channel, account_id: &str, agent_id: &str) -> Self {
Self {
channel,
account_id: Arc::from(account_id),
agent_id: Arc::from(agent_id),
fingerprint: Fingerprint::of(account_id),
}
}
pub fn channel(&self) -> Channel {
self.channel
}
pub fn agent_id(&self) -> &str {
&self.agent_id
}
pub fn fingerprint(&self) -> Fingerprint {
self.fingerprint
}
pub fn account_id_raw(&self) -> &str {
&self.account_id
}
}
impl fmt::Debug for CredentialHandle {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("CredentialHandle")
.field("channel", &self.channel)
.field("agent", &self.agent_id)
.field("fp", &self.fingerprint)
.finish()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn fingerprint_is_stable() {
let a = Fingerprint::of("ana@gmail.com");
let b = Fingerprint::of("ana@gmail.com");
assert_eq!(a, b);
}
#[test]
fn fingerprint_differs_between_ids() {
let a = Fingerprint::of("ana@gmail.com");
let b = Fingerprint::of("kate@gmail.com");
assert_ne!(a, b);
}
#[test]
fn handle_debug_does_not_leak_account_id() {
let h = CredentialHandle::new(WHATSAPP, "+573001234567", "ana");
let rendered = format!("{:?}", h);
assert!(!rendered.contains("573001234567"));
assert!(rendered.contains("whatsapp"));
assert!(rendered.contains("ana"));
assert!(rendered.contains(&h.fingerprint().to_hex()));
}
#[test]
fn fingerprint_display_is_hex() {
let fp = Fingerprint::of("x");
assert_eq!(fp.to_hex().len(), 16);
assert!(fp.to_hex().chars().all(|c| c.is_ascii_hexdigit()));
}
}