use crate::error::StorageError;
use crate::storage::attestation::AttestationSource;
use crate::storage::layout::StorageLayoutConfig;
use auths_index::{AttestationIndex, IndexedAttestation, rebuild_attestations_from_git};
use auths_verifier::core::{Attestation, CommitOid};
use auths_verifier::types::{CanonicalDid, IdentityDID};
use chrono::{DateTime, Utc};
use std::path::{Path, PathBuf};
pub struct IndexedAttestationStorage {
inner: Box<dyn AttestationSource>,
index: AttestationIndex,
repo_path: PathBuf,
config: StorageLayoutConfig,
}
impl IndexedAttestationStorage {
pub fn open(
inner: Box<dyn AttestationSource>,
repo_path: impl AsRef<Path>,
config: StorageLayoutConfig,
) -> Result<Self, StorageError> {
let repo_path = repo_path.as_ref().to_path_buf();
let index_path = repo_path.join(".auths-index.db");
let index = AttestationIndex::open_or_create(&index_path)
.map_err(|e| StorageError::Index(e.to_string()))?;
if index.count().unwrap_or(0) == 0 {
log::info!("Index is empty, rebuilding from Git refs...");
rebuild_attestations_from_git(
&index,
&repo_path,
&config.device_attestation_prefix,
&config.attestation_blob_name,
)
.map_err(|e| StorageError::Index(e.to_string()))?;
}
Ok(Self {
inner,
index,
repo_path,
config,
})
}
pub fn index(&self) -> &AttestationIndex {
&self.index
}
pub fn rebuild_index(&self) -> Result<(), StorageError> {
rebuild_attestations_from_git(
&self.index,
&self.repo_path,
&self.config.device_attestation_prefix,
&self.config.attestation_blob_name,
)
.map_err(|e| StorageError::Index(e.to_string()))?;
Ok(())
}
pub fn update_index(
&self,
att: &Attestation,
git_ref: &str,
commit_oid: &str,
now: DateTime<Utc>,
) -> Result<(), StorageError> {
let issuer_did = IdentityDID::parse(att.issuer.as_str())
.map_err(|e| StorageError::InvalidData(e.to_string()))?;
let indexed = IndexedAttestation {
rid: att.rid.clone(),
issuer_did,
device_did: att.subject.clone(),
git_ref: git_ref.to_string(),
commit_oid: CommitOid::parse(commit_oid).ok(),
revoked_at: att.revoked_at,
expires_at: att.expires_at,
updated_at: att.timestamp.unwrap_or(now),
};
self.index
.upsert_attestation(&indexed)
.map_err(|e| StorageError::Index(e.to_string()))
}
pub fn load_attestations_indexed(
&self,
device_did: &CanonicalDid,
) -> Result<Vec<Attestation>, StorageError> {
let indexed = self
.index
.query_by_device(device_did.as_str())
.map_err(|e| StorageError::Index(e.to_string()))?;
if indexed.is_empty() {
log::debug!(
"No index entries for device {}, falling back to Git",
device_did
);
return self.inner.load_attestations_for_device(device_did);
}
self.inner.load_attestations_for_device(device_did)
}
}
impl AttestationSource for IndexedAttestationStorage {
fn load_attestations_for_device(
&self,
device_did: &CanonicalDid,
) -> Result<Vec<Attestation>, StorageError> {
self.load_attestations_indexed(device_did)
}
fn load_all_attestations(&self) -> Result<Vec<Attestation>, StorageError> {
self.inner.load_all_attestations()
}
fn discover_device_dids(&self) -> Result<Vec<CanonicalDid>, StorageError> {
let active = self
.index
.query_active()
.map_err(|e| StorageError::Index(e.to_string()))?;
if active.is_empty() {
return self.inner.discover_device_dids();
}
let mut dids: Vec<CanonicalDid> = active
.into_iter()
.filter_map(|a| CanonicalDid::parse(a.device_did.as_str()).ok())
.collect();
dids.sort_by(|a, b| a.as_str().cmp(b.as_str()));
dids.dedup();
Ok(dids)
}
}
#[cfg(test)]
mod tests {
use super::*;
use auths_verifier::AttestationBuilder;
#[test]
fn update_index_refuses_non_keri_issuer() {
let att = AttestationBuilder::default()
.issuer("did:key:z6MkNotAKeriIdentity")
.subject("did:key:z6MkDevice")
.build();
assert!(
IdentityDID::parse(att.issuer.as_str()).is_err(),
"a non-did:keri issuer must be refused before it reaches the index"
);
}
#[test]
fn update_index_accepts_keri_issuer() {
let att = AttestationBuilder::default()
.issuer("did:keri:EIssuer123")
.subject("did:key:z6MkDevice")
.build();
let issuer = IdentityDID::parse(att.issuer.as_str()).expect("valid did:keri issuer");
assert_eq!(issuer.as_str(), "did:keri:EIssuer123");
}
}