use auths_verifier::types::CanonicalDid;
use serde::{Deserialize, Serialize};
use std::fmt;
use std::ops::Deref;
use std::path::PathBuf;
use crate::error::StorageError;
use crate::keri::{Prefix, Said};
pub const TOOL_PATH: &str = ".auths";
pub const ATTESTATION_JSON: &str = "attestation.json";
pub const IDENTITY_JSON: &str = "identity.json";
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(transparent)]
pub struct GitRef(String);
impl GitRef {
pub fn new(s: impl Into<String>) -> Self {
Self(s.into())
}
pub fn as_str(&self) -> &str {
&self.0
}
pub fn join(&self, segment: &str) -> GitRef {
GitRef(format!("{}/{}", self.0.trim_end_matches('/'), segment))
}
}
impl fmt::Display for GitRef {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.0)
}
}
impl Deref for GitRef {
type Target = str;
fn deref(&self) -> &str {
&self.0
}
}
impl PartialEq<str> for GitRef {
fn eq(&self, other: &str) -> bool {
self.0 == other
}
}
impl PartialEq<&str> for GitRef {
fn eq(&self, other: &&str) -> bool {
self.0 == *other
}
}
impl From<String> for GitRef {
fn from(s: String) -> Self {
Self(s)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(transparent)]
pub struct BlobName(String);
impl BlobName {
pub fn new(s: impl Into<String>) -> Self {
Self(s.into())
}
pub fn as_str(&self) -> &str {
&self.0
}
}
impl fmt::Display for BlobName {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.0)
}
}
impl Deref for BlobName {
type Target = str;
fn deref(&self) -> &str {
&self.0
}
}
impl PartialEq<str> for BlobName {
fn eq(&self, other: &str) -> bool {
self.0 == other
}
}
impl PartialEq<&str> for BlobName {
fn eq(&self, other: &&str) -> bool {
self.0 == *other
}
}
impl From<String> for BlobName {
fn from(s: String) -> Self {
Self(s)
}
}
pub const KERI_DID_REF_NAMESPACE_PREFIX: &str = "refs/did/keri";
pub fn keri_kel_ref(did_prefix: &Prefix) -> String {
format!(
"{}/{}/kel",
KERI_DID_REF_NAMESPACE_PREFIX.trim_end_matches('/'),
did_prefix.as_str()
)
}
pub fn keri_receipts_ref(did_prefix: &Prefix, event_said: &Said) -> String {
format!(
"{}/{}/receipts/{}",
KERI_DID_REF_NAMESPACE_PREFIX.trim_end_matches('/'),
did_prefix.as_str(),
event_said.as_str()
)
}
pub fn keri_receipts_prefix(did_prefix: &Prefix) -> String {
format!(
"{}/{}/receipts",
KERI_DID_REF_NAMESPACE_PREFIX.trim_end_matches('/'),
did_prefix.as_str()
)
}
pub fn keri_document_ref(did_prefix: &Prefix) -> String {
format!(
"{}/{}/document",
KERI_DID_REF_NAMESPACE_PREFIX.trim_end_matches('/'),
did_prefix.as_str()
)
}
pub fn keri_tel_event_ref(
issuer: &Prefix,
registry_said: &Said,
credential_said: &Said,
sn: u128,
) -> String {
format!(
"{}/{}/tel/{}/{}/{}",
KERI_DID_REF_NAMESPACE_PREFIX.trim_end_matches('/'),
issuer.as_str(),
registry_said.as_str(),
credential_said.as_str(),
sn
)
}
pub fn keri_tel_registry_ref(issuer: &Prefix, registry_said: &Said) -> String {
keri_tel_event_ref(issuer, registry_said, registry_said, 0)
}
pub fn keri_credential_ref(issuer: &Prefix, credential_said: &Said) -> String {
format!(
"{}/{}/credentials/{}",
KERI_DID_REF_NAMESPACE_PREFIX.trim_end_matches('/'),
issuer.as_str(),
credential_said.as_str()
)
}
pub fn did_keri_to_prefix(did: &str) -> Option<Prefix> {
did.strip_prefix("did:keri:")
.map(|s| Prefix::new_unchecked(s.to_string()))
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct StorageLayoutConfig {
pub identity_ref: GitRef,
pub device_attestation_prefix: GitRef,
pub attestation_blob_name: BlobName,
pub identity_blob_name: BlobName,
}
impl Default for StorageLayoutConfig {
fn default() -> Self {
Self {
identity_ref: GitRef::new("refs/auths/identity"),
device_attestation_prefix: GitRef::new("refs/auths/keys"),
attestation_blob_name: BlobName::new(ATTESTATION_JSON),
identity_blob_name: BlobName::new(IDENTITY_JSON),
}
}
}
impl StorageLayoutConfig {
pub fn radicle() -> Self {
Self {
identity_ref: GitRef::new("refs/rad/id"),
device_attestation_prefix: GitRef::new("refs/keys"),
attestation_blob_name: BlobName::new("link-attestation.json"),
identity_blob_name: BlobName::new("radicle-identity.json"),
}
}
pub fn gitoxide() -> Self {
Self {
identity_ref: GitRef::new("refs/auths/id"),
device_attestation_prefix: GitRef::new("refs/auths/devices"),
attestation_blob_name: BlobName::new(ATTESTATION_JSON),
identity_blob_name: BlobName::new(IDENTITY_JSON),
}
}
pub fn org_member_ref(&self, org_did: &str, member_did: &CanonicalDid) -> String {
format!(
"refs/auths/org/{}/members/{}",
sanitize_did(org_did),
member_did.ref_name()
)
}
pub fn org_members_prefix(&self, org_did: &str) -> String {
format!("refs/auths/org/{}/members", sanitize_did(org_did))
}
pub fn org_identity_ref(&self, org_did: &str) -> String {
format!("refs/auths/org/{}/identity", sanitize_did(org_did))
}
}
use crate::storage::registry::shard::sanitize_did;
#[cfg(feature = "git-storage")]
#[allow(clippy::disallowed_methods)] pub fn resolve_repo_path(repo_arg: Option<PathBuf>) -> Result<PathBuf, StorageError> {
match repo_arg {
Some(pathbuf) if !pathbuf.as_os_str().is_empty() => {
auths_utils::path::expand_tilde(&pathbuf)
.map_err(|e| StorageError::NotFound(e.to_string()))
}
_ => {
let home = dirs::home_dir()
.ok_or_else(|| StorageError::NotFound("Could not find HOME directory".into()))?;
Ok(home.join(TOOL_PATH))
}
}
}
pub fn device_namespace_prefix(did: &str) -> String {
format!("refs/namespaces/{}", did_to_nid(did))
}
fn did_to_nid(did: &str) -> String {
did.replace(':', "-")
}
pub fn identity_ref(config: &StorageLayoutConfig) -> &str {
&config.identity_ref
}
pub fn identity_blob_name(config: &StorageLayoutConfig) -> &str {
&config.identity_blob_name
}
pub fn attestation_blob_name(config: &StorageLayoutConfig) -> &str {
&config.attestation_blob_name
}
pub fn attestation_ref_for_device(
config: &StorageLayoutConfig,
device_did: &CanonicalDid,
) -> String {
format!(
"{}/{}/signatures",
config
.device_attestation_prefix
.as_str()
.trim_end_matches('/'),
device_did.ref_name()
)
}
pub fn default_attestation_prefixes(config: &StorageLayoutConfig) -> Vec<String> {
vec![config.device_attestation_prefix.as_str().to_string()]
}
#[cfg(test)]
#[allow(clippy::disallowed_methods)]
mod tests {
use super::*;
#[test]
fn test_config_defaults_are_agnostic() {
let config = StorageLayoutConfig::default();
assert_eq!(config.identity_ref.as_str(), "refs/auths/identity");
assert_eq!(config.device_attestation_prefix.as_str(), "refs/auths/keys");
assert_eq!(config.attestation_blob_name.as_str(), "attestation.json");
assert_eq!(config.identity_blob_name.as_str(), "identity.json");
}
#[test]
fn test_attestation_ref_for_device() {
let prefix = Prefix::new_unchecked("EABC123".to_string());
let expected = "refs/did/keri/EABC123/kel";
assert_eq!(keri_kel_ref(&prefix), expected);
}
#[test]
fn git_ref_join() {
let base = GitRef::new("refs/auths/keys");
let joined = base.join("device1");
assert_eq!(joined.as_str(), "refs/auths/keys/device1");
}
#[test]
fn git_ref_deref() {
let r = GitRef::new("refs/auths/id");
let s: &str = &r;
assert_eq!(s, "refs/auths/id");
}
#[test]
fn blob_name_deref() {
let b = BlobName::new("attestation.json");
let s: &str = &b;
assert_eq!(s, "attestation.json");
}
#[test]
fn layout_roundtrips() {
let config = StorageLayoutConfig::default();
let json = serde_json::to_string(&config).unwrap();
let parsed: StorageLayoutConfig = serde_json::from_str(&json).unwrap();
assert_eq!(config, parsed);
}
#[cfg(feature = "git-storage")]
mod repo_path {
use super::super::*;
use std::path::PathBuf;
#[test]
fn resolve_repo_path_expands_tilde_in_override() {
let result = resolve_repo_path(Some(PathBuf::from("~/.auths"))).unwrap();
#[allow(clippy::disallowed_methods)]
let home = dirs::home_dir().unwrap();
assert_eq!(result, home.join(".auths"));
}
#[test]
fn resolve_repo_path_defaults_to_home_auths() {
let result = resolve_repo_path(None).unwrap();
#[allow(clippy::disallowed_methods)]
let home = dirs::home_dir().unwrap();
assert_eq!(result, home.join(".auths"));
}
#[test]
fn resolve_repo_path_preserves_absolute_override() {
let result = resolve_repo_path(Some(PathBuf::from("/tmp/custom-auths"))).unwrap();
assert_eq!(result, PathBuf::from("/tmp/custom-auths"));
}
}
}