auths_id/storage/
layout.rs1use auths_verifier::types::DeviceDID;
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 did_keri_to_prefix(did: &str) -> Option<Prefix> {
173 did.strip_prefix("did:keri:")
174 .map(|s| Prefix::new_unchecked(s.to_string()))
175}
176
177#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
184pub struct StorageLayoutConfig {
185 pub identity_ref: GitRef,
188
189 pub device_attestation_prefix: GitRef,
192
193 pub attestation_blob_name: BlobName,
196
197 pub identity_blob_name: BlobName,
200}
201impl Default for StorageLayoutConfig {
202 fn default() -> Self {
203 Self {
204 identity_ref: GitRef::new("refs/auths/identity"),
205 device_attestation_prefix: GitRef::new("refs/auths/keys"),
206 attestation_blob_name: BlobName::new(ATTESTATION_JSON),
207 identity_blob_name: BlobName::new(IDENTITY_JSON),
208 }
209 }
210}
211
212impl StorageLayoutConfig {
213 pub fn radicle() -> Self {
215 Self {
216 identity_ref: GitRef::new("refs/rad/id"),
217 device_attestation_prefix: GitRef::new("refs/keys"),
218 attestation_blob_name: BlobName::new("link-attestation.json"),
219 identity_blob_name: BlobName::new("radicle-identity.json"),
220 }
221 }
222
223 pub fn gitoxide() -> Self {
225 Self {
226 identity_ref: GitRef::new("refs/auths/id"),
227 device_attestation_prefix: GitRef::new("refs/auths/devices"),
228 attestation_blob_name: BlobName::new(ATTESTATION_JSON),
229 identity_blob_name: BlobName::new(IDENTITY_JSON),
230 }
231 }
232
233 pub fn org_member_ref(&self, org_did: &str, member_did: &DeviceDID) -> String {
237 format!(
238 "refs/auths/org/{}/members/{}",
239 sanitize_did_for_ref(org_did),
240 member_did.ref_name()
241 )
242 }
243
244 pub fn org_members_prefix(&self, org_did: &str) -> String {
246 format!("refs/auths/org/{}/members", sanitize_did_for_ref(org_did))
247 }
248
249 pub fn org_identity_ref(&self, org_did: &str) -> String {
251 format!("refs/auths/org/{}/identity", sanitize_did_for_ref(org_did))
252 }
253}
254
255pub fn sanitize_did_for_ref(did: &str) -> String {
257 did.chars()
258 .map(|c| if c.is_alphanumeric() { c } else { '_' })
259 .collect()
260}
261
262#[cfg(feature = "git-storage")]
264pub fn resolve_repo_path(repo_arg: Option<PathBuf>) -> Result<PathBuf, StorageError> {
265 match repo_arg {
266 Some(pathbuf) if !pathbuf.as_os_str().is_empty() => Ok(pathbuf),
267 _ => {
268 let home = dirs::home_dir()
269 .ok_or_else(|| StorageError::NotFound("Could not find HOME directory".into()))?;
270 Ok(home.join(TOOL_PATH))
271 }
272 }
273}
274
275pub fn device_namespace_prefix(did: &str) -> String {
277 format!("refs/namespaces/{}", did_to_nid(did))
278}
279
280fn did_to_nid(did: &str) -> String {
282 did.replace(':', "-")
283}
284
285pub fn identity_ref(config: &StorageLayoutConfig) -> &str {
287 &config.identity_ref
288}
289
290pub fn identity_blob_name(config: &StorageLayoutConfig) -> &str {
292 &config.identity_blob_name
293}
294
295pub fn attestation_blob_name(config: &StorageLayoutConfig) -> &str {
297 &config.attestation_blob_name
298}
299
300pub fn attestation_ref_for_device(config: &StorageLayoutConfig, device_did: &DeviceDID) -> String {
302 format!(
303 "{}/{}/signatures",
304 config
305 .device_attestation_prefix
306 .as_str()
307 .trim_end_matches('/'),
308 device_did.ref_name()
309 )
310}
311
312pub fn default_attestation_prefixes(config: &StorageLayoutConfig) -> Vec<String> {
314 vec![config.device_attestation_prefix.as_str().to_string()]
315}
316
317#[cfg(test)]
318mod tests {
319 use super::*;
320
321 #[test]
322 fn test_config_defaults_are_agnostic() {
323 let config = StorageLayoutConfig::default();
324 assert_eq!(config.identity_ref.as_str(), "refs/auths/identity");
325 assert_eq!(config.device_attestation_prefix.as_str(), "refs/auths/keys");
326 assert_eq!(config.attestation_blob_name.as_str(), "attestation.json");
327 assert_eq!(config.identity_blob_name.as_str(), "identity.json");
328 }
329
330 #[test]
331 fn test_attestation_ref_for_device() {
332 let prefix = Prefix::new_unchecked("EABC123".to_string());
333 let expected = "refs/did/keri/EABC123/kel";
334 assert_eq!(keri_kel_ref(&prefix), expected);
335 }
336
337 #[test]
338 fn git_ref_join() {
339 let base = GitRef::new("refs/auths/keys");
340 let joined = base.join("device1");
341 assert_eq!(joined.as_str(), "refs/auths/keys/device1");
342 }
343
344 #[test]
345 fn git_ref_deref() {
346 let r = GitRef::new("refs/auths/id");
347 let s: &str = &r;
348 assert_eq!(s, "refs/auths/id");
349 }
350
351 #[test]
352 fn blob_name_deref() {
353 let b = BlobName::new("attestation.json");
354 let s: &str = &b;
355 assert_eq!(s, "attestation.json");
356 }
357
358 #[test]
359 fn layout_roundtrips() {
360 let config = StorageLayoutConfig::default();
361 let json = serde_json::to_string(&config).unwrap();
362 let parsed: StorageLayoutConfig = serde_json::from_str(&json).unwrap();
363 assert_eq!(config, parsed);
364 }
365}