use crate::auth::session::now_epoch;
use crate::config::AppConfig;
use crate::did_ops;
use crate::store::KeyspaceHandle;
use did_hosting_common::{SyncDeleteRequest, SyncDidRequest};
use serde::{Deserialize, Serialize};
use std::sync::Arc;
use tracing::warn;
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct WatcherSyncStatus {
pub watcher_url: String,
pub last_synced_version_id: Option<String>,
pub last_synced_at: Option<u64>,
pub last_error: Option<String>,
pub ok: bool,
}
fn normalize_url(url: &str) -> String {
url.trim_end_matches('/').to_string()
}
pub fn notify_watchers_did(
config: &Arc<AppConfig>,
http: &reqwest::Client,
dids_ks: &KeyspaceHandle,
mnemonic: String,
) {
let http = http.clone();
let config = config.clone();
let dids_ks = dids_ks.clone();
tokio::spawn(async move {
let record = match dids_ks
.get::<did_ops::DidRecord>(did_ops::did_key(&mnemonic))
.await
{
Ok(Some(r)) => r,
Ok(None) => return,
Err(e) => {
warn!(mnemonic = %mnemonic, error = %e, "watcher push: failed to read record");
return;
}
};
let log_content = match dids_ks.get_raw(did_ops::content_log_key(&mnemonic)).await {
Ok(Some(bytes)) => match String::from_utf8(bytes) {
Ok(s) => s,
Err(_) => {
warn!(mnemonic = %mnemonic, "watcher push: invalid UTF-8 in log content");
return;
}
},
Ok(None) => return,
Err(e) => {
warn!(mnemonic = %mnemonic, error = %e, "watcher push: failed to read log");
return;
}
};
let witness_content = match dids_ks
.get_raw(did_ops::content_witness_key(&mnemonic))
.await
{
Ok(Some(bytes)) => String::from_utf8(bytes).ok(),
_ => None,
};
let meta = did_ops::extract_log_metadata(&log_content);
if meta.watcher_urls.is_empty() && config.watchers.is_empty() {
return;
}
let payload = SyncDidRequest {
mnemonic: mnemonic.clone(),
did_id: record.did_id,
log_content,
witness_content,
source_url: config.public_base_url(),
updated_at: record.updated_at,
disabled: record.disabled,
};
let did_watcher_urls: Vec<String> =
meta.watcher_urls.iter().map(|u| normalize_url(u)).collect();
let mut statuses: Vec<WatcherSyncStatus> = Vec::new();
let configured_urls: Vec<String> = config
.watchers
.iter()
.map(|w| normalize_url(&w.url))
.collect();
for watcher in &config.watchers {
let norm = normalize_url(&watcher.url);
if !did_watcher_urls.is_empty() && !did_watcher_urls.contains(&norm) {
continue;
}
let url = format!("{}/api/sync/did", norm);
let mut req = http.post(&url).json(&payload);
if let Some(token) = &watcher.token {
req = req.bearer_auth(token);
}
let (ok, last_error) = match req.send().await {
Ok(resp) if resp.status().is_success() => (true, None),
Ok(resp) => (false, Some(format!("HTTP {}", resp.status()))),
Err(e) => {
warn!(url = %url, error = %e, "failed to push DID to watcher");
(false, Some(e.to_string()))
}
};
statuses.push(WatcherSyncStatus {
watcher_url: watcher.url.clone(),
last_synced_version_id: if ok {
meta.latest_version_id.clone()
} else {
None
},
last_synced_at: if ok { Some(now_epoch()) } else { None },
last_error,
ok,
});
}
for did_url in &meta.watcher_urls {
let norm = normalize_url(did_url);
if !configured_urls.contains(&norm) {
statuses.push(WatcherSyncStatus {
watcher_url: did_url.clone(),
last_synced_version_id: None,
last_synced_at: None,
last_error: Some("watcher not configured on this server".into()),
ok: false,
});
}
}
if !statuses.is_empty()
&& let Err(e) = dids_ks
.insert(did_ops::watcher_sync_key(&mnemonic), &statuses)
.await
{
warn!(mnemonic = %mnemonic, error = %e, "failed to persist watcher sync status");
}
});
}
pub fn notify_watchers_delete(
config: &Arc<AppConfig>,
http: &reqwest::Client,
dids_ks: &KeyspaceHandle,
mnemonic: String,
) {
let http = http.clone();
let config = config.clone();
let dids_ks = dids_ks.clone();
tokio::spawn(async move {
let _ = dids_ks.remove(did_ops::watcher_sync_key(&mnemonic)).await;
if config.watchers.is_empty() {
return;
}
let payload = SyncDeleteRequest {
mnemonic: mnemonic.clone(),
source_url: config.public_base_url(),
};
for watcher in &config.watchers {
let url = format!("{}/api/sync/delete", watcher.url.trim_end_matches('/'));
let mut req = http.post(&url).json(&payload);
if let Some(token) = &watcher.token {
req = req.bearer_auth(token);
}
if let Err(e) = req.send().await {
warn!(url = %url, error = %e, "failed to push DID delete to watcher");
}
}
});
}