Skip to main content

auths_id/storage/
layout.rs

1use 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
11// --- General Constants ---
12
13/// Default directory name within the user's home directory for storing repositories.
14pub const TOOL_PATH: &str = ".auths";
15/// Default filename for storing attestation data within Git commits.
16pub const ATTESTATION_JSON: &str = "attestation.json";
17/// Default filename for storing identity data within Git commits.
18pub const IDENTITY_JSON: &str = "identity.json";
19
20// --- Typed Git ref and blob name newtypes ---
21
22/// A Git reference path (e.g. `refs/auths/identity`).
23#[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    /// Join a path segment to this ref, separated by `/`.
37    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/// A blob filename within a Git tree (e.g. `attestation.json`).
74#[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
119// --- KERI Specific Constants & Layout  ---
120
121/// The base Git reference namespace prefix for storing KERI DID information.
122pub const KERI_DID_REF_NAMESPACE_PREFIX: &str = "refs/did/keri";
123
124/// Constructs the full Git reference path for a KERI Key Event Log (KEL)
125/// based on the DID prefix (AID).
126///
127/// Example: `refs/did/keri/<did_prefix>/kel`
128pub 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
136/// Constructs the Git reference path for storing receipts for a specific event.
137///
138/// Example: `refs/did/keri/<did_prefix>/receipts/<event_said>`
139pub 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
148/// Returns the base Git reference prefix for all receipts of an identity.
149///
150/// Example: `refs/did/keri/<did_prefix>/receipts`
151pub 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
159/// (Optional) Constructs the full Git reference path for a cached KERI DID Document
160/// based on the DID prefix (AID).
161///
162/// Example: `refs/did/keri/<did_prefix>/document`
163pub 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
171/// Extracts the KERI prefix (AID) from a full `did:keri:` identifier string.
172pub 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// --- Configurable Layout (Primarily for did:key Identity & Attestations) ---
178
179/// Configuration defining the Git reference layout for primary identity and device attestation data.
180///
181/// This struct allows consumers of the `auths-id` library to define custom
182/// Git repository layouts.
183#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
184pub struct StorageLayoutConfig {
185    /// The Git reference pointing to the commit containing the primary identity document.
186    /// Default: `"refs/auths/identity"`
187    pub identity_ref: GitRef,
188
189    /// The base Git reference prefix for storing device attestations.
190    /// Default: `"refs/auths/keys"`
191    pub device_attestation_prefix: GitRef,
192
193    /// Standard filename for the blob containing attestation data.
194    /// Default: `"attestation.json"`
195    pub attestation_blob_name: BlobName,
196
197    /// Standard filename for the blob containing identity data.
198    /// Default: `"identity.json"`
199    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    /// Radicle-compatible layout preset (uses `refs/rad/` namespace).
214    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    /// Gitoxide-compatible layout preset (uses `refs/auths/` namespace).
224    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    // --- Organization Reference Helpers ---
234
235    /// Constructs the full Git reference path for storing an organization member's attestation.
236    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    /// Returns the base Git reference prefix for listing all members of an organization.
245    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    /// Returns the Git reference path for storing organization identity/metadata.
250    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
255/// Sanitizes a DID string for use in Git reference paths.
256pub 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/// Determines the actual repository path from an optional `--repo` argument.
263#[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
275/// Creates a Git namespace prefix string for a given DID.
276pub fn device_namespace_prefix(did: &str) -> String {
277    format!("refs/namespaces/{}", did_to_nid(did))
278}
279
280/// Sanitizes a DID string into a node ID (NID) suitable for use in Git refs.
281fn did_to_nid(did: &str) -> String {
282    did.replace(':', "-")
283}
284
285/// Gets the primary identity Git reference from the configuration.
286pub fn identity_ref(config: &StorageLayoutConfig) -> &str {
287    &config.identity_ref
288}
289
290/// Gets the standard identity blob filename from the configuration.
291pub fn identity_blob_name(config: &StorageLayoutConfig) -> &str {
292    &config.identity_blob_name
293}
294
295/// Gets the standard attestation blob filename from the configuration.
296pub fn attestation_blob_name(config: &StorageLayoutConfig) -> &str {
297    &config.attestation_blob_name
298}
299
300/// Constructs the full Git reference path for storing a specific device's attestations.
301pub 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
312/// Returns the list of Git reference prefixes to scan when discovering device attestations.
313pub 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}