1use auths_verifier::types::CanonicalDid;
2use serde::{Deserialize, Serialize};
3use std::fmt;
4use std::ops::Deref;
5use std::path::PathBuf;
6
7use crate::error::StorageError;
8
9use crate::keri::{Prefix, Said};
10
11pub const TOOL_PATH: &str = ".auths";
15pub const ATTESTATION_JSON: &str = "attestation.json";
17pub const IDENTITY_JSON: &str = "identity.json";
19
20#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
24#[serde(transparent)]
25pub struct GitRef(String);
26
27impl GitRef {
28 pub fn new(s: impl Into<String>) -> Self {
29 Self(s.into())
30 }
31
32 pub fn as_str(&self) -> &str {
33 &self.0
34 }
35
36 pub fn join(&self, segment: &str) -> GitRef {
38 GitRef(format!("{}/{}", self.0.trim_end_matches('/'), segment))
39 }
40}
41
42impl fmt::Display for GitRef {
43 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
44 f.write_str(&self.0)
45 }
46}
47
48impl Deref for GitRef {
49 type Target = str;
50 fn deref(&self) -> &str {
51 &self.0
52 }
53}
54
55impl PartialEq<str> for GitRef {
56 fn eq(&self, other: &str) -> bool {
57 self.0 == other
58 }
59}
60
61impl PartialEq<&str> for GitRef {
62 fn eq(&self, other: &&str) -> bool {
63 self.0 == *other
64 }
65}
66
67impl From<String> for GitRef {
68 fn from(s: String) -> Self {
69 Self(s)
70 }
71}
72
73#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
75#[serde(transparent)]
76pub struct BlobName(String);
77
78impl BlobName {
79 pub fn new(s: impl Into<String>) -> Self {
80 Self(s.into())
81 }
82
83 pub fn as_str(&self) -> &str {
84 &self.0
85 }
86}
87
88impl fmt::Display for BlobName {
89 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
90 f.write_str(&self.0)
91 }
92}
93
94impl Deref for BlobName {
95 type Target = str;
96 fn deref(&self) -> &str {
97 &self.0
98 }
99}
100
101impl PartialEq<str> for BlobName {
102 fn eq(&self, other: &str) -> bool {
103 self.0 == other
104 }
105}
106
107impl PartialEq<&str> for BlobName {
108 fn eq(&self, other: &&str) -> bool {
109 self.0 == *other
110 }
111}
112
113impl From<String> for BlobName {
114 fn from(s: String) -> Self {
115 Self(s)
116 }
117}
118
119pub const KERI_DID_REF_NAMESPACE_PREFIX: &str = "refs/did/keri";
123
124pub fn keri_kel_ref(did_prefix: &Prefix) -> String {
129 format!(
130 "{}/{}/kel",
131 KERI_DID_REF_NAMESPACE_PREFIX.trim_end_matches('/'),
132 did_prefix.as_str()
133 )
134}
135
136pub fn keri_receipts_ref(did_prefix: &Prefix, event_said: &Said) -> String {
140 format!(
141 "{}/{}/receipts/{}",
142 KERI_DID_REF_NAMESPACE_PREFIX.trim_end_matches('/'),
143 did_prefix.as_str(),
144 event_said.as_str()
145 )
146}
147
148pub fn keri_receipts_prefix(did_prefix: &Prefix) -> String {
152 format!(
153 "{}/{}/receipts",
154 KERI_DID_REF_NAMESPACE_PREFIX.trim_end_matches('/'),
155 did_prefix.as_str()
156 )
157}
158
159pub fn keri_document_ref(did_prefix: &Prefix) -> String {
164 format!(
165 "{}/{}/document",
166 KERI_DID_REF_NAMESPACE_PREFIX.trim_end_matches('/'),
167 did_prefix.as_str()
168 )
169}
170
171pub fn keri_tel_event_ref(
190 issuer: &Prefix,
191 registry_said: &Said,
192 credential_said: &Said,
193 sn: u128,
194) -> String {
195 format!(
196 "{}/{}/tel/{}/{}/{}",
197 KERI_DID_REF_NAMESPACE_PREFIX.trim_end_matches('/'),
198 issuer.as_str(),
199 registry_said.as_str(),
200 credential_said.as_str(),
201 sn
202 )
203}
204
205pub fn keri_tel_registry_ref(issuer: &Prefix, registry_said: &Said) -> String {
221 keri_tel_event_ref(issuer, registry_said, registry_said, 0)
222}
223
224pub fn keri_credential_ref(issuer: &Prefix, credential_said: &Said) -> String {
237 format!(
238 "{}/{}/credentials/{}",
239 KERI_DID_REF_NAMESPACE_PREFIX.trim_end_matches('/'),
240 issuer.as_str(),
241 credential_said.as_str()
242 )
243}
244
245pub fn did_keri_to_prefix(did: &str) -> Option<Prefix> {
247 did.strip_prefix("did:keri:")
248 .map(|s| Prefix::new_unchecked(s.to_string()))
249}
250
251#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
258pub struct StorageLayoutConfig {
259 pub identity_ref: GitRef,
262
263 pub device_attestation_prefix: GitRef,
266
267 pub attestation_blob_name: BlobName,
270
271 pub identity_blob_name: BlobName,
274}
275impl Default for StorageLayoutConfig {
276 fn default() -> Self {
277 Self {
278 identity_ref: GitRef::new("refs/auths/identity"),
279 device_attestation_prefix: GitRef::new("refs/auths/keys"),
280 attestation_blob_name: BlobName::new(ATTESTATION_JSON),
281 identity_blob_name: BlobName::new(IDENTITY_JSON),
282 }
283 }
284}
285
286impl StorageLayoutConfig {
287 pub fn radicle() -> Self {
289 Self {
290 identity_ref: GitRef::new("refs/rad/id"),
291 device_attestation_prefix: GitRef::new("refs/keys"),
292 attestation_blob_name: BlobName::new("link-attestation.json"),
293 identity_blob_name: BlobName::new("radicle-identity.json"),
294 }
295 }
296
297 pub fn gitoxide() -> Self {
299 Self {
300 identity_ref: GitRef::new("refs/auths/id"),
301 device_attestation_prefix: GitRef::new("refs/auths/devices"),
302 attestation_blob_name: BlobName::new(ATTESTATION_JSON),
303 identity_blob_name: BlobName::new(IDENTITY_JSON),
304 }
305 }
306
307 pub fn org_member_ref(&self, org_did: &str, member_did: &CanonicalDid) -> String {
311 format!(
312 "refs/auths/org/{}/members/{}",
313 sanitize_did(org_did),
314 member_did.ref_name()
315 )
316 }
317
318 pub fn org_members_prefix(&self, org_did: &str) -> String {
320 format!("refs/auths/org/{}/members", sanitize_did(org_did))
321 }
322
323 pub fn org_identity_ref(&self, org_did: &str) -> String {
325 format!("refs/auths/org/{}/identity", sanitize_did(org_did))
326 }
327}
328
329use crate::storage::registry::shard::sanitize_did;
332
333#[cfg(feature = "git-storage")]
339#[allow(clippy::disallowed_methods)] pub fn resolve_repo_path(repo_arg: Option<PathBuf>) -> Result<PathBuf, StorageError> {
341 match repo_arg {
342 Some(pathbuf) if !pathbuf.as_os_str().is_empty() => {
343 auths_utils::path::expand_tilde(&pathbuf)
344 .map_err(|e| StorageError::NotFound(e.to_string()))
345 }
346 _ => {
347 let home = dirs::home_dir()
348 .ok_or_else(|| StorageError::NotFound("Could not find HOME directory".into()))?;
349 Ok(home.join(TOOL_PATH))
350 }
351 }
352}
353
354pub fn device_namespace_prefix(did: &str) -> String {
356 format!("refs/namespaces/{}", did_to_nid(did))
357}
358
359fn did_to_nid(did: &str) -> String {
361 did.replace(':', "-")
362}
363
364pub fn identity_ref(config: &StorageLayoutConfig) -> &str {
366 &config.identity_ref
367}
368
369pub fn identity_blob_name(config: &StorageLayoutConfig) -> &str {
371 &config.identity_blob_name
372}
373
374pub fn attestation_blob_name(config: &StorageLayoutConfig) -> &str {
376 &config.attestation_blob_name
377}
378
379pub fn attestation_ref_for_device(
381 config: &StorageLayoutConfig,
382 device_did: &CanonicalDid,
383) -> String {
384 format!(
385 "{}/{}/signatures",
386 config
387 .device_attestation_prefix
388 .as_str()
389 .trim_end_matches('/'),
390 device_did.ref_name()
391 )
392}
393
394pub fn default_attestation_prefixes(config: &StorageLayoutConfig) -> Vec<String> {
396 vec![config.device_attestation_prefix.as_str().to_string()]
397}
398
399#[cfg(test)]
400#[allow(clippy::disallowed_methods)]
401mod tests {
402 use super::*;
403
404 #[test]
405 fn test_config_defaults_are_agnostic() {
406 let config = StorageLayoutConfig::default();
407 assert_eq!(config.identity_ref.as_str(), "refs/auths/identity");
408 assert_eq!(config.device_attestation_prefix.as_str(), "refs/auths/keys");
409 assert_eq!(config.attestation_blob_name.as_str(), "attestation.json");
410 assert_eq!(config.identity_blob_name.as_str(), "identity.json");
411 }
412
413 #[test]
414 fn test_attestation_ref_for_device() {
415 let prefix = Prefix::new_unchecked("EABC123".to_string());
416 let expected = "refs/did/keri/EABC123/kel";
417 assert_eq!(keri_kel_ref(&prefix), expected);
418 }
419
420 #[test]
421 fn git_ref_join() {
422 let base = GitRef::new("refs/auths/keys");
423 let joined = base.join("device1");
424 assert_eq!(joined.as_str(), "refs/auths/keys/device1");
425 }
426
427 #[test]
428 fn git_ref_deref() {
429 let r = GitRef::new("refs/auths/id");
430 let s: &str = &r;
431 assert_eq!(s, "refs/auths/id");
432 }
433
434 #[test]
435 fn blob_name_deref() {
436 let b = BlobName::new("attestation.json");
437 let s: &str = &b;
438 assert_eq!(s, "attestation.json");
439 }
440
441 #[test]
442 fn layout_roundtrips() {
443 let config = StorageLayoutConfig::default();
444 let json = serde_json::to_string(&config).unwrap();
445 let parsed: StorageLayoutConfig = serde_json::from_str(&json).unwrap();
446 assert_eq!(config, parsed);
447 }
448
449 #[cfg(feature = "git-storage")]
450 mod repo_path {
451 use super::super::*;
452 use std::path::PathBuf;
453
454 #[test]
455 fn resolve_repo_path_expands_tilde_in_override() {
456 let result = resolve_repo_path(Some(PathBuf::from("~/.auths"))).unwrap();
457 #[allow(clippy::disallowed_methods)]
458 let home = dirs::home_dir().unwrap();
459 assert_eq!(result, home.join(".auths"));
460 }
461
462 #[test]
463 fn resolve_repo_path_defaults_to_home_auths() {
464 let result = resolve_repo_path(None).unwrap();
465 #[allow(clippy::disallowed_methods)]
466 let home = dirs::home_dir().unwrap();
467 assert_eq!(result, home.join(".auths"));
468 }
469
470 #[test]
471 fn resolve_repo_path_preserves_absolute_override() {
472 let result = resolve_repo_path(Some(PathBuf::from("/tmp/custom-auths"))).unwrap();
473 assert_eq!(result, PathBuf::from("/tmp/custom-auths"));
474 }
475 }
476}