use std::path::{Path, PathBuf};
use serde::{Deserialize, Serialize};
use serde_json::{json, Map};
use super::client::QueuedEvent;
use super::events::Event;
use super::TelemetryHandle;
#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
pub struct Identity {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub user_db_id: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub org_id: Option<String>,
#[serde(default)]
pub aliased_pairs: Vec<AliasedPair>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct AliasedPair {
pub agent_id: String,
pub user_db_id: String,
}
pub fn identity_path(openlatch_dir: &Path) -> PathBuf {
openlatch_dir.join("identity.json")
}
pub fn read(path: &Path) -> Option<Identity> {
let raw = std::fs::read_to_string(path).ok()?;
serde_json::from_str(&raw).ok()
}
pub fn write(path: &Path, identity: &Identity) -> std::io::Result<()> {
if let Some(parent) = path.parent() {
std::fs::create_dir_all(parent)?;
}
let serialized = serde_json::to_string_pretty(identity).map_err(std::io::Error::other)?;
std::fs::write(path, serialized)
}
pub fn already_aliased(identity: &Identity, agent_id: &str, user_db_id: &str) -> bool {
identity
.aliased_pairs
.iter()
.any(|p| p.agent_id == agent_id && p.user_db_id == user_db_id)
}
pub fn create_alias_event(agent_id: &str, user_db_id: &str) -> Event {
let mut props = Map::new();
props.insert("alias".into(), json!(agent_id));
props.insert("distinct_id".into(), json!(user_db_id));
Event {
name: "$create_alias".into(),
properties: props,
}
}
pub fn record_auth_success(
handle: &TelemetryHandle,
openlatch_dir: &Path,
agent_id: &str,
user_db_id: &str,
org_id: Option<&str>,
) {
let path = identity_path(openlatch_dir);
let mut identity = read(&path).unwrap_or_default();
if !already_aliased(&identity, agent_id, user_db_id) {
handle.capture(create_alias_event(agent_id, user_db_id));
identity.aliased_pairs.push(AliasedPair {
agent_id: agent_id.into(),
user_db_id: user_db_id.into(),
});
}
identity.user_db_id = Some(user_db_id.into());
if let Some(o) = org_id {
identity.org_id = Some(o.into());
}
let _ = write(&path, &identity);
}
pub fn resolve_distinct_id(openlatch_dir: &Path, agent_id: &str) -> String {
read(&identity_path(openlatch_dir))
.and_then(|i| i.user_db_id)
.unwrap_or_else(|| agent_id.to_string())
}
#[allow(dead_code)]
pub fn is_alias_event(event: &QueuedEvent) -> bool {
event.name == "$create_alias"
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::TempDir;
#[test]
fn test_create_alias_event_shape() {
let e = create_alias_event("agt_x", "user_42");
assert_eq!(e.name, "$create_alias");
assert_eq!(e.properties["alias"], "agt_x");
assert_eq!(e.properties["distinct_id"], "user_42");
}
#[test]
fn test_already_aliased_returns_true_after_record() {
let mut id = Identity::default();
id.aliased_pairs.push(AliasedPair {
agent_id: "agt_x".into(),
user_db_id: "user_42".into(),
});
assert!(already_aliased(&id, "agt_x", "user_42"));
assert!(!already_aliased(&id, "agt_x", "user_99"));
assert!(!already_aliased(&id, "agt_y", "user_42"));
}
#[test]
fn test_write_then_read_roundtrip() {
let tmp = TempDir::new().unwrap();
let path = identity_path(tmp.path());
let identity = Identity {
user_db_id: Some("user_42".into()),
org_id: Some("org_7".into()),
aliased_pairs: vec![AliasedPair {
agent_id: "agt_x".into(),
user_db_id: "user_42".into(),
}],
};
write(&path, &identity).unwrap();
let loaded = read(&path).unwrap();
assert_eq!(loaded, identity);
}
#[test]
fn test_resolve_distinct_id_prefers_user_db_id_when_present() {
let tmp = TempDir::new().unwrap();
let identity = Identity {
user_db_id: Some("user_42".into()),
..Default::default()
};
write(&identity_path(tmp.path()), &identity).unwrap();
assert_eq!(resolve_distinct_id(tmp.path(), "agt_x"), "user_42");
}
#[test]
fn test_resolve_distinct_id_falls_back_to_agent_id() {
let tmp = TempDir::new().unwrap();
assert_eq!(resolve_distinct_id(tmp.path(), "agt_x"), "agt_x");
}
#[test]
fn test_record_auth_success_aliases_once_and_persists() {
let tmp = TempDir::new().unwrap();
let handle = TelemetryHandle::disabled("agt_x".into(), false);
record_auth_success(&handle, tmp.path(), "agt_x", "user_42", Some("org_7"));
let loaded = read(&identity_path(tmp.path())).unwrap();
assert_eq!(loaded.user_db_id.as_deref(), Some("user_42"));
assert_eq!(loaded.org_id.as_deref(), Some("org_7"));
assert_eq!(loaded.aliased_pairs.len(), 1);
record_auth_success(&handle, tmp.path(), "agt_x", "user_42", Some("org_7"));
let loaded = read(&identity_path(tmp.path())).unwrap();
assert_eq!(loaded.aliased_pairs.len(), 1);
}
}