use crate::credential::VerifiableCredential;
use anyhow::{Context, Result};
use std::path::{Path, PathBuf};
use tracing::{debug, warn};
pub struct CredentialStore {
dir: PathBuf,
}
impl CredentialStore {
pub fn new(dir: PathBuf) -> Result<Self> {
std::fs::create_dir_all(&dir)
.with_context(|| format!("creating credentials directory: {}", dir.display()))?;
Ok(Self { dir })
}
pub fn default_dir() -> PathBuf {
kwaainet_home().join("credentials")
}
pub fn open_default() -> Result<Self> {
Self::new(Self::default_dir())
}
pub fn save(&self, vc: &VerifiableCredential) -> Result<()> {
let filename = vc_filename(vc);
let path = self.dir.join(&filename);
let json = serde_json::to_string_pretty(vc)?;
std::fs::write(&path, json)
.with_context(|| format!("writing credential: {}", path.display()))?;
debug!("Saved credential: {}", filename);
Ok(())
}
pub fn import_file(&self, path: &Path) -> Result<VerifiableCredential> {
let json = std::fs::read_to_string(path)
.with_context(|| format!("reading credential file: {}", path.display()))?;
let vc: VerifiableCredential =
serde_json::from_str(&json).context("parsing credential JSON")?;
self.save(&vc)?;
Ok(vc)
}
pub fn load_all(&self) -> Vec<VerifiableCredential> {
let Ok(entries) = std::fs::read_dir(&self.dir) else {
return Vec::new();
};
let mut vcs = Vec::new();
for entry in entries.flatten() {
let path = entry.path();
if path.extension().and_then(|e| e.to_str()) != Some("json") {
continue;
}
match std::fs::read_to_string(&path) {
Ok(json) => match serde_json::from_str::<VerifiableCredential>(&json) {
Ok(vc) => vcs.push(vc),
Err(e) => warn!("Skipping malformed credential {}: {}", path.display(), e),
},
Err(e) => warn!("Could not read {}: {}", path.display(), e),
}
}
vcs
}
pub fn load_for_subject(&self, subject_did: &str) -> Vec<VerifiableCredential> {
self.load_all()
.into_iter()
.filter(|vc| vc.subject_did() == subject_did)
.collect()
}
pub fn load_valid_for_subject(&self, subject_did: &str) -> Vec<VerifiableCredential> {
self.load_for_subject(subject_did)
.into_iter()
.filter(|vc| !vc.is_expired())
.collect()
}
pub fn dir(&self) -> &Path {
&self.dir
}
}
fn vc_filename(vc: &VerifiableCredential) -> String {
let vc_type = vc
.kwaai_type()
.map(|t| t.as_str().to_lowercase())
.unwrap_or_else(|| "credential".to_string());
let issued = vc.issuance_date.format("%Y%m%d-%H%M%S");
let issuer_prefix = vc
.issuer_did()
.strip_prefix("did:peer:")
.map(|b| b.chars().take(8).collect::<String>())
.unwrap_or_else(|| "unknown".to_string());
format!("{}-{}-{}.json", vc_type, issued, issuer_prefix)
}
fn kwaainet_home() -> PathBuf {
dirs::home_dir()
.unwrap_or_else(|| PathBuf::from("."))
.join(".kwaainet")
}