use std::ops::ControlFlow;
use auths_id::ports::registry::{RegistryBackend, RegistryError};
use auths_keri::{Event, Prefix};
use git2::Repository;
use tempfile::TempDir;
use super::{GitRegistryBackend, REGISTRY_REF, RegistryConfig};
pub const MAX_KEL_EVENTS: usize = 10_000;
pub const MAX_KEL_BYTES: usize = 4 * 1024 * 1024;
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum RemoteKelError {
#[error("git fetch from '{url}' failed: {source}")]
Fetch {
url: String,
#[source]
source: git2::Error,
},
#[error("remote has no registry ref ({reference})")]
NoRegistry {
reference: String,
},
#[error("KEL not found on remote for {0}")]
NotFound(String),
#[error("fetched KEL exceeds size bound ({what})")]
Oversized {
what: String,
},
#[error("fetched KEL is truncated: inception (seq 0) is missing")]
Truncated,
#[error("temp repository setup failed: {0}")]
Setup(#[source] std::io::Error),
#[error("reading fetched KEL failed: {0}")]
Read(#[source] RegistryError),
#[error("event serialization failed: {0}")]
Serialize(String),
}
pub struct RemoteKelSource {
remote_url: String,
}
impl RemoteKelSource {
pub fn new(remote_url: impl Into<String>) -> Self {
Self {
remote_url: remote_url.into(),
}
}
pub fn url(&self) -> &str {
&self.remote_url
}
pub fn fetch_kel(&self, prefix: &Prefix) -> Result<Vec<Event>, RemoteKelError> {
let tmp = TempDir::new().map_err(RemoteKelError::Setup)?;
let repo = Repository::init(tmp.path()).map_err(|e| RemoteKelError::Fetch {
url: self.remote_url.clone(),
source: e,
})?;
fetch_registry_ref(&repo, &self.remote_url)?;
let backend =
GitRegistryBackend::from_config_unchecked(RegistryConfig::single_tenant(tmp.path()));
collect_capped_with(&backend, prefix, MAX_KEL_EVENTS, MAX_KEL_BYTES)
}
}
fn fetch_registry_ref(repo: &Repository, url: &str) -> Result<(), RemoteKelError> {
let mut remote = repo
.remote_anonymous(url)
.map_err(|e| RemoteKelError::Fetch {
url: url.to_string(),
source: e,
})?;
let refspec = format!("+{REGISTRY_REF}:{REGISTRY_REF}");
remote
.fetch(&[refspec.as_str()], None, None)
.map_err(|e| RemoteKelError::Fetch {
url: url.to_string(),
source: e,
})?;
if repo.find_reference(REGISTRY_REF).is_err() {
return Err(RemoteKelError::NoRegistry {
reference: REGISTRY_REF.to_string(),
});
}
Ok(())
}
fn collect_capped_with(
backend: &dyn RegistryBackend,
prefix: &Prefix,
max_events: usize,
max_bytes: usize,
) -> Result<Vec<Event>, RemoteKelError> {
let mut events = Vec::new();
let mut total_bytes = 0usize;
let mut cap_error: Option<RemoteKelError> = None;
let visit = backend.visit_events(prefix, 0, &mut |event| {
if events.len() >= max_events {
cap_error = Some(RemoteKelError::Oversized {
what: format!("> {max_events} events"),
});
return ControlFlow::Break(());
}
match serde_json::to_vec(event) {
Ok(bytes) => {
total_bytes += bytes.len();
if total_bytes > max_bytes {
cap_error = Some(RemoteKelError::Oversized {
what: format!("> {max_bytes} bytes"),
});
return ControlFlow::Break(());
}
}
Err(e) => {
cap_error = Some(RemoteKelError::Serialize(e.to_string()));
return ControlFlow::Break(());
}
}
events.push(event.clone());
ControlFlow::Continue(())
});
match visit {
Ok(()) => {}
Err(RegistryError::NotFound { .. }) => {
return Err(RemoteKelError::NotFound(format!("did:keri:{prefix}")));
}
Err(e) => return Err(RemoteKelError::Read(e)),
}
if let Some(err) = cap_error {
return Err(err);
}
if events.is_empty() {
return Err(RemoteKelError::NotFound(format!("did:keri:{prefix}")));
}
if events[0].sequence().value() != 0 {
return Err(RemoteKelError::Truncated);
}
Ok(events)
}
#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::expect_used)]
mod tests {
use super::*;
use auths_keri::{
CesrKey, IcpEvent, KeriPublicKey, KeriSequence, Said, Threshold, VersionString,
compute_next_commitment, finalize_icp_event,
};
fn icp_event() -> (Event, Prefix) {
let key = KeriPublicKey::ed25519(&[7u8; 32]).unwrap();
let next = KeriPublicKey::ed25519(&[8u8; 32]).unwrap();
let icp = IcpEvent {
v: VersionString::placeholder(),
d: Said::default(),
i: Prefix::default(),
s: KeriSequence::new(0),
kt: Threshold::Simple(1),
k: vec![CesrKey::new_unchecked(key.to_qb64().unwrap())],
nt: Threshold::Simple(1),
n: vec![compute_next_commitment(&next)],
bt: Threshold::Simple(0),
b: vec![],
c: vec![],
a: vec![],
};
let finalized = finalize_icp_event(icp).unwrap();
let prefix = finalized.i.clone();
(Event::Icp(finalized), prefix)
}
fn source_registry() -> (TempDir, Event, Prefix) {
let dir = TempDir::new().unwrap();
let backend =
GitRegistryBackend::from_config_unchecked(RegistryConfig::single_tenant(dir.path()));
backend.init_if_needed().unwrap();
let (event, prefix) = icp_event();
backend.append_event(&prefix, &event).unwrap();
(dir, event, prefix)
}
#[test]
fn fetches_kel_from_file_remote_with_no_local_seeding() {
let (src, event, prefix) = source_registry();
let url = format!("file://{}", src.path().display());
let fetched = RemoteKelSource::new(url).fetch_kel(&prefix).unwrap();
assert_eq!(fetched, vec![event]);
}
#[test]
fn unknown_identity_on_remote_is_not_found() {
let (src, _event, _prefix) = source_registry();
let url = format!("file://{}", src.path().display());
let missing =
Prefix::new_unchecked("ENeverProvisionedHere000000000000000000000000".to_string());
let err = RemoteKelSource::new(url).fetch_kel(&missing).unwrap_err();
assert!(matches!(err, RemoteKelError::NotFound(_)));
}
#[test]
fn event_cap_rejects_oversized_kel() {
let dir = TempDir::new().unwrap();
let backend =
GitRegistryBackend::from_config_unchecked(RegistryConfig::single_tenant(dir.path()));
backend.init_if_needed().unwrap();
let (event, prefix) = icp_event();
backend.append_event(&prefix, &event).unwrap();
let err = collect_capped_with(&backend, &prefix, 0, MAX_KEL_BYTES).unwrap_err();
assert!(matches!(err, RemoteKelError::Oversized { .. }));
}
#[test]
fn byte_cap_rejects_oversized_kel() {
let dir = TempDir::new().unwrap();
let backend =
GitRegistryBackend::from_config_unchecked(RegistryConfig::single_tenant(dir.path()));
backend.init_if_needed().unwrap();
let (event, prefix) = icp_event();
backend.append_event(&prefix, &event).unwrap();
let err = collect_capped_with(&backend, &prefix, MAX_KEL_EVENTS, 1).unwrap_err();
assert!(matches!(err, RemoteKelError::Oversized { .. }));
}
}