use tracing::{debug, info, warn};
use vti_common::vault::{StoredVaultEntry, VaultStatus, delete_vault_entry};
use crate::error::AppError;
use crate::store::KeyspaceHandle;
use crate::vault::model::StoredCredential;
use crate::vault::storage as cred_storage;
pub async fn sweep_expired(
vault_ks: &KeyspaceHandle,
audit_ks: &KeyspaceHandle,
) -> Result<(), AppError> {
let now = chrono::Utc::now().to_rfc3339();
let mut purged = 0usize;
for (_key, value) in vault_ks.prefix_iter_raw("vault:").await? {
let stored: StoredVaultEntry = match serde_json::from_slice(&value) {
Ok(s) => s,
Err(e) => {
debug!(error = %e, "vault sweeper: skipping unreadable vault row");
continue;
}
};
if is_purgeable(
stored.entry.status,
stored.entry.grace_until.as_deref(),
&now,
) {
delete_vault_entry(vault_ks, &stored.entry.id).await?;
purged += 1;
audit_purge(
audit_ks,
"vault.purge",
&stored.entry.id,
Some(stored.entry.context_id.as_str()),
)
.await;
}
}
for (_key, value) in vault_ks.prefix_iter_raw("cred:").await? {
let cred: StoredCredential = match serde_json::from_slice(&value) {
Ok(c) => c,
Err(e) => {
debug!(error = %e, "vault sweeper: skipping unreadable cred row");
continue;
}
};
if is_purgeable(cred.lifecycle, cred.grace_until.as_deref(), &now) {
cred_storage::delete(vault_ks, &cred.id).await?;
purged += 1;
audit_purge(
audit_ks,
"vault.cred.purge",
&cred.id,
cred.community_did.as_deref(),
)
.await;
}
}
if purged > 0 {
info!(
vault_purged = purged,
"vault sweeper purged grace-expired tombstones"
);
}
Ok(())
}
fn is_purgeable(status: VaultStatus, grace_until: Option<&str>, now: &str) -> bool {
matches!(status, VaultStatus::Deleted) && grace_until.is_some_and(|g| now >= g)
}
async fn audit_purge(audit_ks: &KeyspaceHandle, action: &str, id: &str, context_id: Option<&str>) {
if let Err(e) = crate::audit::record(
audit_ks,
action,
"system:sweeper",
Some(id),
"success:grace-expired",
None,
context_id,
)
.await
{
warn!(error = %e, action, "vault sweeper: purge succeeded but audit::record failed");
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn purgeable_only_when_deleted_and_past_grace() {
let now = "2026-06-18T12:00:00+00:00";
assert!(!is_purgeable(VaultStatus::Active, None, now));
assert!(!is_purgeable(
VaultStatus::Archived,
Some("2020-01-01T00:00:00+00:00"),
now
));
assert!(!is_purgeable(
VaultStatus::Deleted,
Some("2026-07-18T12:00:00+00:00"),
now
));
assert!(is_purgeable(
VaultStatus::Deleted,
Some("2026-06-01T00:00:00+00:00"),
now
));
assert!(!is_purgeable(VaultStatus::Deleted, None, now));
}
}