use vta_sdk::webvh::{WebvhDidRecord, WebvhServerRecord};
use vti_common::error::AppError;
use vti_common::store::KeyspaceHandle;
fn server_key(id: &str) -> String {
format!("server:{id}")
}
fn did_key(did: &str) -> String {
format!("did:{did}")
}
fn log_key(did: &str) -> String {
format!("log:{did}")
}
fn published_key(did: &str) -> String {
format!("published:{did}")
}
fn scid_key(scid_did: &str) -> String {
format!("scid:{scid_did}")
}
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "camelCase")]
#[non_exhaustive]
pub struct ScidIdentity {
pub scid_did: String,
pub webvh_did: String,
pub presented: bool,
pub alias_listed: bool,
}
impl ScidIdentity {
pub fn of(webvh_did: &str, presented: bool, alias_listed: bool) -> Option<Self> {
let (scid_did, _) = vta_sdk::did_scid::scid_forms(webvh_did)?;
Some(Self {
scid_did,
webvh_did: webvh_did.to_string(),
presented,
alias_listed,
})
}
pub fn did_url(&self) -> String {
vta_sdk::did_scid::scid_forms(&self.webvh_did)
.map(|(_, url)| url)
.unwrap_or_else(|| self.scid_did.clone())
}
pub fn identity(&self) -> &str {
if self.presented {
&self.scid_did
} else {
&self.webvh_did
}
}
}
pub async fn store_scid_identity(
ks: &KeyspaceHandle,
identity: &ScidIdentity,
) -> Result<(), AppError> {
ks.insert(scid_key(&identity.scid_did), identity).await
}
pub async fn get_scid_identity(
ks: &KeyspaceHandle,
did: &str,
) -> Result<Option<ScidIdentity>, AppError> {
let scid_did = if vta_sdk::did_scid::is_scid(did) {
vta_sdk::did_scid::identity(did)
} else if let Some((bare, _)) = vta_sdk::did_scid::scid_forms(did) {
bare
} else {
return Ok(None);
};
let found: Option<ScidIdentity> = ks.get(scid_key(&scid_did)).await?;
Ok(found.filter(|f| vta_sdk::did_scid::is_scid(did) || f.webvh_did == did))
}
pub async fn delete_scid_identity(ks: &KeyspaceHandle, scid_did: &str) -> Result<(), AppError> {
ks.remove(scid_key(&vta_sdk::did_scid::identity(scid_did)))
.await
}
pub async fn list_scid_identities(ks: &KeyspaceHandle) -> Result<Vec<ScidIdentity>, AppError> {
let raw = ks.prefix_iter_raw("scid:").await?;
let mut out = Vec::with_capacity(raw.len());
for (_key, value) in raw {
out.push(serde_json::from_slice(&value)?);
}
Ok(out)
}
pub async fn log_did_for(ks: &KeyspaceHandle, did: &str) -> Result<Option<String>, AppError> {
if !vta_sdk::did_scid::is_scid(did) {
return Ok(Some(did.to_string()));
}
Ok(get_scid_identity(ks, did).await?.map(|i| i.webvh_did))
}
pub async fn get_server(
ks: &KeyspaceHandle,
id: &str,
) -> Result<Option<WebvhServerRecord>, AppError> {
ks.get(server_key(id)).await
}
pub async fn store_server(ks: &KeyspaceHandle, record: &WebvhServerRecord) -> Result<(), AppError> {
ks.insert(server_key(&record.id), record).await
}
pub async fn delete_server(ks: &KeyspaceHandle, id: &str) -> Result<(), AppError> {
ks.remove(server_key(id)).await
}
pub async fn list_servers(ks: &KeyspaceHandle) -> Result<Vec<WebvhServerRecord>, AppError> {
let raw = ks.prefix_iter_raw("server:").await?;
let mut servers = Vec::with_capacity(raw.len());
for (_key, value) in raw {
let record: WebvhServerRecord = serde_json::from_slice(&value)?;
servers.push(record);
}
Ok(servers)
}
pub async fn get_did(ks: &KeyspaceHandle, did: &str) -> Result<Option<WebvhDidRecord>, AppError> {
ks.get(did_key(did)).await
}
pub async fn store_did(ks: &KeyspaceHandle, record: &WebvhDidRecord) -> Result<(), AppError> {
ks.insert(did_key(&record.did), record).await
}
pub async fn delete_did(ks: &KeyspaceHandle, did: &str) -> Result<(), AppError> {
ks.remove(did_key(did)).await
}
pub async fn list_dids(ks: &KeyspaceHandle) -> Result<Vec<WebvhDidRecord>, AppError> {
let raw = ks.prefix_iter_raw("did:").await?;
let mut dids = Vec::with_capacity(raw.len());
for (_key, value) in raw {
let record: WebvhDidRecord = serde_json::from_slice(&value)?;
dids.push(record);
}
Ok(dids)
}
pub async fn get_did_log(ks: &KeyspaceHandle, did: &str) -> Result<Option<String>, AppError> {
let bytes = ks.get_raw(log_key(did)).await?;
Ok(bytes.map(|b| String::from_utf8_lossy(&b).into_owned()))
}
pub async fn store_did_log(
ks: &KeyspaceHandle,
did: &str,
log_content: &str,
) -> Result<(), AppError> {
ks.insert_raw(log_key(did), log_content.as_bytes().to_vec())
.await
}
pub async fn get_published_version(
ks: &KeyspaceHandle,
did: &str,
) -> Result<Option<String>, AppError> {
let bytes = ks.get_raw(published_key(did)).await?;
Ok(bytes.map(|b| String::from_utf8_lossy(&b).into_owned()))
}
pub async fn set_published_version(
ks: &KeyspaceHandle,
did: &str,
version_id: &str,
) -> Result<(), AppError> {
ks.insert_raw(published_key(did), version_id.as_bytes().to_vec())
.await
}
#[cfg(test)]
mod tests {
use super::*;
use chrono::Utc;
use vti_common::config::StoreConfig as VtiStoreConfig;
use vti_common::store::Store;
async fn setup_ks() -> (tempfile::TempDir, KeyspaceHandle) {
let dir = tempfile::tempdir().unwrap();
let store = Store::open(&VtiStoreConfig {
data_dir: dir.path().into(),
})
.unwrap();
let ks = store.keyspace(vta_keyspaces::WEBVH).unwrap();
(dir, ks)
}
#[tokio::test]
async fn scid_identity_round_trips_by_either_form() {
let (_dir, ks) = setup_ks().await;
let webvh = "did:webvh:QmScid:host.example:alice";
let identity = ScidIdentity::of(webvh, true, true).unwrap();
assert_eq!(identity.scid_did, "did:scid:vh:1:QmScid");
assert_eq!(
identity.did_url(),
"did:scid:vh:1:QmScid?src=host.example/alice"
);
assert_eq!(identity.identity(), "did:scid:vh:1:QmScid");
store_scid_identity(&ks, &identity).await.unwrap();
for did in [
"did:scid:vh:1:QmScid",
"did:scid:vh:1:QmScid?src=elsewhere.example",
webvh,
] {
assert_eq!(
get_scid_identity(&ks, did).await.unwrap(),
Some(identity.clone()),
"{did}"
);
}
assert_eq!(
get_scid_identity(&ks, "did:webvh:QmScid:other.example")
.await
.unwrap(),
None
);
assert_eq!(
log_did_for(&ks, "did:scid:vh:1:QmScid")
.await
.unwrap()
.as_deref(),
Some(webvh)
);
assert_eq!(
log_did_for(&ks, webvh).await.unwrap().as_deref(),
Some(webvh)
);
assert_eq!(
log_did_for(&ks, "did:scid:vh:1:QmNone").await.unwrap(),
None
);
assert!(list_dids(&ks).await.unwrap().is_empty());
assert_eq!(
list_scid_identities(&ks).await.unwrap(),
vec![identity.clone()]
);
delete_scid_identity(&ks, "did:scid:vh:1:QmScid?src=x")
.await
.unwrap();
assert_eq!(get_scid_identity(&ks, webvh).await.unwrap(), None);
}
#[tokio::test]
async fn published_version_marker_round_trips() {
let (_dir, ks) = setup_ks().await;
let did = "did:webvh:abc:host.example:alice";
assert_eq!(get_published_version(&ks, did).await.unwrap(), None);
set_published_version(&ks, did, "3-QmHead").await.unwrap();
assert_eq!(
get_published_version(&ks, did).await.unwrap().as_deref(),
Some("3-QmHead")
);
set_published_version(&ks, did, "4-QmNext").await.unwrap();
assert_eq!(
get_published_version(&ks, did).await.unwrap().as_deref(),
Some("4-QmNext")
);
assert_eq!(get_did_log(&ks, did).await.unwrap(), None);
}
fn sample_server(id: &str) -> WebvhServerRecord {
let now = Utc::now();
WebvhServerRecord {
id: id.into(),
did: format!("did:web:{id}.example"),
label: None,
created_at: now,
updated_at: now,
}
}
#[tokio::test]
async fn delete_server_removes_the_record() {
let (_dir, ks) = setup_ks().await;
store_server(&ks, &sample_server("prod")).await.unwrap();
assert_eq!(list_servers(&ks).await.unwrap().len(), 1);
delete_server(&ks, "prod").await.unwrap();
assert!(get_server(&ks, "prod").await.unwrap().is_none());
assert!(list_servers(&ks).await.unwrap().is_empty());
}
}