Skip to main content

auths_id/storage/registry/
backend.rs

1//! Registry backend trait and error types.
2//!
3//! # Architecture: Cache/Index, Not Source of Truth
4//!
5//! The registry is an **indexed materialized view** for performance:
6//!
7//! - **Identity KEL (Key Event Log)** = source of truth
8//!   - Events in `v1/identities/<shard>/<prefix>/events/` are canonical
9//!   - All other state is derived from the KEL
10//!
11//! - **Registry** = cached index for O(1) lookups
12//!   - `state.json`, `tip.json` are performance caches
13//!   - Can be rebuilt from KEL at any time
14//!   - NOT authoritative for trust or authorization decisions
15//!
16//! - **Backend validates structure, not trust**
17//!   - Backends may validate JSON structure, SAID integrity
18//!   - Backends may NOT decide trust, authorization, or expiry
19//!   - Policy engine (separate layer) handles authorization
20//!
21//! This separation ensures backends remain testable, replaceable, and
22//! free from policy coupling.
23//!
24//! # Overwrite Semantics
25//!
26//! Different data types have different overwrite semantics:
27//!
28//! | Data Type | Semantic | Behavior |
29//! |-----------|----------|----------|
30//! | KEL Events | **Append-only** | Once written, immutable. `append_event()` refuses overwrites. |
31//! | Device Attestations | **Latest-view** | `store_attestation()` overwrites existing. History in `attestation_history/`. |
32//! | Org Member Attestations | **Latest-view** | `store_org_member()` overwrites existing for same member DID. |
33//!
34//! The "latest-view" pattern means the current file represents only the latest state.
35//! Historical data is preserved separately for audit purposes.
36
37use std::ops::ControlFlow;
38use std::sync::Arc;
39
40use auths_core::storage::keychain::IdentityDID;
41use auths_verifier::core::{Attestation, ResourceId};
42use auths_verifier::types::CanonicalDid;
43use thiserror::Error;
44
45use crate::keri::event::Event;
46use crate::keri::state::KeyState;
47use crate::keri::{Prefix, Said};
48
49use super::org_member::{MemberFilter, MemberStatus, MemberView, OrgMemberEntry};
50use super::schemas::{RegistryMetadata, TipInfo};
51
52/// An operation to be committed atomically.
53///
54/// Collected into an [`AtomicWriteBatch`] and committed via
55/// [`RegistryBackend::commit_batch`]. Git backends write all operations
56/// in a single commit; non-Git backends fall back to sequential writes.
57#[derive(Debug, Clone)]
58pub enum AtomicWriteOp {
59    /// Store a device attestation (latest-view overwrite).
60    StoreAttestation(Attestation),
61    /// Store an org member attestation (latest-view overwrite).
62    StoreOrgMember { org: String, member: Attestation },
63    /// Append a KEL event with optional CESR signature attachment.
64    AppendEvent {
65        prefix: Prefix,
66        event: Event,
67        attachment: Vec<u8>,
68    },
69    /// Append a TEL (credential-status) event under an issuer's registry.
70    ///
71    /// Staged alongside the issuer's anchoring `ixn` (an [`AtomicWriteOp::AppendEvent`])
72    /// so the TEL event and its KEL anchor land in one commit. See the
73    /// `RegistryBackend` TEL doc-block for the atomicity justification.
74    AppendTelEvent {
75        /// Issuing AID controlling the registry.
76        issuer: Prefix,
77        /// Registry SAID (`vcp.d`).
78        registry_said: Said,
79        /// Credential SAID (`iss.i`/`rev.i`; equals the registry SAID for a `vcp`).
80        credential_said: Said,
81        /// TEL event sequence number (`0` for `vcp`/`iss`, `1` for `rev`).
82        sn: u128,
83        /// The TEL event's canonical insertion-order JSON bytes.
84        event_bytes: Vec<u8>,
85    },
86    /// Store an ACDC credential blob under an issuer's namespace.
87    ///
88    /// Staged alongside the `iss` TEL event and the issuer's anchoring `ixn` so the
89    /// blob, the TEL event, and the KEL anchor land in one commit.
90    StoreCredential {
91        /// Issuing AID.
92        issuer: Prefix,
93        /// Credential SAID (`acdc.d`).
94        credential_said: Said,
95        /// The ACDC credential's canonical insertion-order JSON bytes.
96        credential_bytes: Vec<u8>,
97    },
98}
99
100/// Batch of write operations to be committed atomically.
101///
102/// Build one via [`AtomicWriteBatch::new`], stage operations, then pass
103/// to [`RegistryBackend::commit_batch`].
104#[derive(Debug, Clone, Default)]
105pub struct AtomicWriteBatch {
106    ops: Vec<AtomicWriteOp>,
107}
108
109impl AtomicWriteBatch {
110    pub fn new() -> Self {
111        Self::default()
112    }
113
114    pub fn stage_attestation(&mut self, att: Attestation) -> &mut Self {
115        self.ops.push(AtomicWriteOp::StoreAttestation(att));
116        self
117    }
118
119    pub fn stage_org_member(&mut self, org: impl Into<String>, member: Attestation) -> &mut Self {
120        self.ops.push(AtomicWriteOp::StoreOrgMember {
121            org: org.into(),
122            member,
123        });
124        self
125    }
126
127    pub fn stage_event(&mut self, prefix: Prefix, event: Event, attachment: Vec<u8>) -> &mut Self {
128        self.ops.push(AtomicWriteOp::AppendEvent {
129            prefix,
130            event,
131            attachment,
132        });
133        self
134    }
135
136    /// Stage a TEL event (`vcp`/`iss`/`rev`) under an issuer's registry.
137    pub fn stage_tel_event(
138        &mut self,
139        issuer: Prefix,
140        registry_said: Said,
141        credential_said: Said,
142        sn: u128,
143        event_bytes: Vec<u8>,
144    ) -> &mut Self {
145        self.ops.push(AtomicWriteOp::AppendTelEvent {
146            issuer,
147            registry_said,
148            credential_said,
149            sn,
150            event_bytes,
151        });
152        self
153    }
154
155    /// Stage an ACDC credential blob under an issuer's namespace.
156    pub fn stage_credential(
157        &mut self,
158        issuer: Prefix,
159        credential_said: Said,
160        credential_bytes: Vec<u8>,
161    ) -> &mut Self {
162        self.ops.push(AtomicWriteOp::StoreCredential {
163            issuer,
164            credential_said,
165            credential_bytes,
166        });
167        self
168    }
169
170    pub fn ops(&self) -> &[AtomicWriteOp] {
171        &self.ops
172    }
173
174    pub fn is_empty(&self) -> bool {
175        self.ops.is_empty()
176    }
177}
178
179/// Specific reasons a tenant ID is rejected.
180///
181/// `PathTraversal` is intentionally absent: the strict allowlist
182/// (`[a-z0-9_-]`) makes traversal components unexpressible.
183/// If the allowed charset expands (e.g. to include `.`), reintroduce
184/// an explicit traversal check before adding it to the allowlist.
185#[derive(Debug, Error, PartialEq)]
186#[non_exhaustive]
187pub enum TenantIdError {
188    /// ID is empty or exceeds 64 characters.
189    #[error("must be 1–64 characters (got {0})")]
190    InvalidLength(usize),
191
192    /// ID contains a character outside `[a-z0-9_-]`.
193    /// Carries the first offending character for precise error messages.
194    #[error("contains disallowed character {0:?} (only [a-z0-9_-] allowed)")]
195    InvalidCharacter(char),
196
197    /// ID matches a reserved route segment or system name.
198    #[error("'{0}' is reserved")]
199    Reserved(String),
200}
201
202impl auths_core::error::AuthsErrorInfo for TenantIdError {
203    fn error_code(&self) -> &'static str {
204        match self {
205            Self::InvalidLength(_) => "AUTHS-E4851",
206            Self::InvalidCharacter(_) => "AUTHS-E4852",
207            Self::Reserved(_) => "AUTHS-E4853",
208        }
209    }
210
211    fn suggestion(&self) -> Option<&'static str> {
212        match self {
213            Self::InvalidLength(_) => Some("Tenant ID must be between 1 and 64 characters"),
214            Self::InvalidCharacter(_) => {
215                Some("Only lowercase letters, digits, hyphens, and underscores are allowed")
216            }
217            Self::Reserved(_) => Some("Choose a different tenant ID; this name is reserved"),
218        }
219    }
220}
221
222/// A tenant identifier that has been normalized (lowercased) and validated.
223///
224/// Construct via [`ValidatedTenantId::new`] which enforces:
225/// - Length 1–64 characters
226/// - Only `[a-z0-9_-]` characters (input is lowercased before checking)
227/// - Not a reserved name (`admin`, `health`, `metrics`)
228///
229/// Passing this type through boundaries guarantees the ID is safe for
230/// filesystem paths without further checking.
231///
232/// Usage:
233/// ```ignore
234/// let tid = ValidatedTenantId::new("Acme")?;
235/// assert_eq!(tid.as_str(), "acme");
236/// ```
237#[derive(Debug, Clone, PartialEq, Eq, Hash)]
238pub struct ValidatedTenantId(String);
239
240/// Reserved names that collide with current or planned API route segments.
241const RESERVED_TENANT_IDS: &[&str] = &["admin", "health", "metrics"];
242
243impl ValidatedTenantId {
244    /// Normalize and validate a raw tenant identifier.
245    ///
246    /// Input is lowercased, then checked against the validation rules.
247    pub fn new(raw: impl Into<String>) -> Result<Self, RegistryError> {
248        let normalized = raw.into().to_lowercase();
249        validate_tenant_id_inner(&normalized)?;
250        Ok(Self(normalized))
251    }
252
253    /// Returns the canonical (lowercase) tenant ID string.
254    pub fn as_str(&self) -> &str {
255        &self.0
256    }
257}
258
259impl std::fmt::Display for ValidatedTenantId {
260    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
261        f.write_str(&self.0)
262    }
263}
264
265impl AsRef<str> for ValidatedTenantId {
266    fn as_ref(&self) -> &str {
267        &self.0
268    }
269}
270
271impl From<ValidatedTenantId> for String {
272    fn from(tid: ValidatedTenantId) -> Self {
273        tid.0
274    }
275}
276
277fn validate_tenant_id_inner(tenant_id: &str) -> Result<(), RegistryError> {
278    let len = tenant_id.len();
279    if len == 0 || len > 64 {
280        return Err(RegistryError::InvalidTenantId {
281            tenant_id: tenant_id.into(),
282            kind: TenantIdError::InvalidLength(len),
283        });
284    }
285    if let Some(bad_char) = tenant_id
286        .chars()
287        .find(|c| !matches!(c, 'a'..='z' | '0'..='9' | '-' | '_'))
288    {
289        return Err(RegistryError::InvalidTenantId {
290            tenant_id: tenant_id.into(),
291            kind: TenantIdError::InvalidCharacter(bad_char),
292        });
293    }
294    if RESERVED_TENANT_IDS.contains(&tenant_id) {
295        return Err(RegistryError::InvalidTenantId {
296            tenant_id: tenant_id.into(),
297            kind: TenantIdError::Reserved(tenant_id.into()),
298        });
299    }
300    Ok(())
301}
302
303/// Errors that can occur during registry operations.
304#[derive(Debug, Error)]
305#[non_exhaustive]
306pub enum RegistryError {
307    /// Storage backend operation failed
308    #[error("Storage error: {0}")]
309    Storage(#[source] Box<dyn std::error::Error + Send + Sync>),
310
311    /// Invalid KERI prefix format
312    #[error("Invalid prefix '{prefix}': {reason}")]
313    InvalidPrefix { prefix: String, reason: String },
314
315    /// Invalid device DID format
316    #[error("Invalid device DID '{did}': {reason}")]
317    InvalidDeviceDid { did: String, reason: String },
318
319    /// Event already exists at this sequence number
320    #[error("Event already exists: {prefix} seq {seq}")]
321    EventExists { prefix: String, seq: u128 },
322
323    /// Sequence number gap detected
324    #[error("Sequence gap for {prefix}: expected {expected}, got {got}")]
325    SequenceGap {
326        prefix: String,
327        expected: u128,
328        got: u128,
329    },
330
331    /// Entity not found
332    #[error("Not found: {entity_type} '{id}'")]
333    NotFound { entity_type: String, id: String },
334
335    /// JSON serialization/deserialization error
336    #[error("Serialization error: {0}")]
337    Serialization(#[from] serde_json::Error),
338
339    /// Concurrent modification detected (CAS failure)
340    #[error("Concurrent modification: {0}")]
341    ConcurrentModification(String),
342
343    /// SAID (Self-Addressing Identifier) mismatch (computed != stored)
344    #[error("SAID mismatch: expected {expected}, got {actual}")]
345    SaidMismatch { expected: String, actual: String },
346
347    /// Invalid event structure (cannot compute SAID)
348    #[error("Invalid event: {reason}")]
349    InvalidEvent { reason: String },
350
351    /// I/O error
352    #[error("I/O error: {0}")]
353    Io(#[from] std::io::Error),
354
355    /// Internal error
356    #[error("Internal error: {0}")]
357    Internal(String),
358
359    /// Invalid tenant ID
360    #[error("invalid tenant ID '{tenant_id}': {kind}")]
361    InvalidTenantId {
362        tenant_id: String,
363        kind: TenantIdError,
364    },
365
366    /// Attestation validation error
367    #[error("Attestation error: {0}")]
368    Attestation(String),
369
370    /// Stale or replayed attestation rejected
371    #[error("Stale attestation: {0}")]
372    StaleAttestation(String),
373
374    /// Method is not yet implemented by this backend.
375    #[error("Not implemented: {method}")]
376    NotImplemented {
377        /// Name of the unimplemented method.
378        method: &'static str,
379    },
380
381    /// A batch operation failed validation at a specific event index.
382    #[error("Batch validation failed at index {index}: {source}")]
383    BatchValidationFailed {
384        /// Zero-based index of the failing event in the batch.
385        index: usize,
386        /// The underlying validation error.
387        source: Box<RegistryError>,
388    },
389}
390
391impl auths_core::error::AuthsErrorInfo for RegistryError {
392    fn error_code(&self) -> &'static str {
393        match self {
394            Self::Storage(_) => "AUTHS-E4861",
395            Self::InvalidPrefix { .. } => "AUTHS-E4862",
396            Self::InvalidDeviceDid { .. } => "AUTHS-E4863",
397            Self::EventExists { .. } => "AUTHS-E4864",
398            Self::SequenceGap { .. } => "AUTHS-E4865",
399            Self::NotFound { .. } => "AUTHS-E4866",
400            Self::Serialization(_) => "AUTHS-E4867",
401            Self::ConcurrentModification(_) => "AUTHS-E4868",
402            Self::SaidMismatch { .. } => "AUTHS-E4869",
403            Self::InvalidEvent { .. } => "AUTHS-E4870",
404            Self::Io(_) => "AUTHS-E4871",
405            Self::Internal(_) => "AUTHS-E4872",
406            Self::InvalidTenantId { .. } => "AUTHS-E4873",
407            Self::Attestation(_) => "AUTHS-E4874",
408            Self::StaleAttestation(_) => "AUTHS-E4875",
409            Self::NotImplemented { .. } => "AUTHS-E4876",
410            Self::BatchValidationFailed { .. } => "AUTHS-E4877",
411        }
412    }
413
414    fn suggestion(&self) -> Option<&'static str> {
415        match self {
416            Self::Storage(_) => Some("Check storage backend connectivity"),
417            Self::InvalidPrefix { .. } => Some("KERI prefixes must start with 'E' (Blake3 SAID)"),
418            Self::InvalidDeviceDid { .. } => Some("Device DIDs must be in 'did:key:z...' format"),
419            Self::EventExists { .. } => Some("This event has already been appended to the KEL"),
420            Self::SequenceGap { .. } => Some("Events must be appended in strict sequence order"),
421            Self::NotFound { .. } => None,
422            Self::Serialization(_) => None,
423            Self::ConcurrentModification(_) => {
424                Some("Retry the operation; another process modified the registry")
425            }
426            Self::SaidMismatch { .. } => Some("The event content does not match its declared SAID"),
427            Self::InvalidEvent { .. } => None,
428            Self::Io(_) => Some("Check file permissions and disk space"),
429            Self::Internal(_) => None,
430            Self::InvalidTenantId { .. } => None,
431            Self::Attestation(_) => None,
432            Self::StaleAttestation(_) => {
433                Some("The attestation has been superseded by a newer version")
434            }
435            Self::NotImplemented { .. } => {
436                Some("This operation is not supported by the current backend")
437            }
438            Self::BatchValidationFailed { .. } => None,
439        }
440    }
441}
442
443impl RegistryError {
444    /// Create a storage error from any error type.
445    ///
446    /// Use this to wrap backend-specific errors (e.g., git2::Error) in the
447    /// generic Storage variant.
448    pub fn storage<E: std::error::Error + Send + Sync + 'static>(err: E) -> Self {
449        Self::Storage(Box::new(err))
450    }
451
452    /// Create a not-found error for an identity.
453    pub fn identity_not_found(prefix: &Prefix) -> Self {
454        Self::NotFound {
455            entity_type: "identity".into(),
456            id: prefix.as_str().into(),
457        }
458    }
459
460    /// Create a not-found error for an event.
461    pub fn event_not_found(prefix: &Prefix, seq: u128) -> Self {
462        Self::NotFound {
463            entity_type: "event".into(),
464            id: format!("{} seq {}", prefix.as_str(), seq),
465        }
466    }
467
468    /// Create a not-found error for a device.
469    pub fn device_not_found(did: &str) -> Self {
470        Self::NotFound {
471            entity_type: "device".into(),
472            id: did.into(),
473        }
474    }
475
476    /// Create a not-found error for an org.
477    pub fn org_not_found(org: &str) -> Self {
478        Self::NotFound {
479            entity_type: "org".into(),
480            id: org.into(),
481        }
482    }
483}
484
485/// Trait for registry storage backends.
486///
487/// This trait defines operations for storing and retrieving:
488/// - KERI events and key states
489/// - Device attestations
490/// - Organization memberships
491///
492/// Implementations must be thread-safe (`Send + Sync`).
493///
494/// # FROZEN SURFACE
495///
496/// This trait's method set is frozen. Do not add new methods without:
497/// 1. Documented justification in a doc comment
498/// 2. Review confirming the method is required for registry operations
499///
500/// The registry is a cache/index, not a source of truth. New methods should
501/// only be added if they are essential for indexing or retrieval operations.
502pub trait RegistryBackend: Send + Sync {
503    // =========================================================================
504    // KEL Operations
505    // =========================================================================
506
507    /// Append an event to an identity's KEL.
508    ///
509    /// # Semantics: Append-Only
510    ///
511    /// Events are **append-only** - once written, they are immutable.
512    /// This function **refuses to overwrite** existing events.
513    ///
514    /// # Constraints (enforced by implementation)
515    ///
516    /// - Refuses if event file already exists (append-only, no rewrites)
517    /// - Refuses if seq != tip.sequence + 1 (except seq 0 for inception)
518    /// - Validates event.said() matches content hash expectations
519    ///
520    /// Also writes state.json with updated KeyState after each append.
521    ///
522    /// # Arguments
523    ///
524    /// * `prefix` - The KERI identifier prefix
525    /// * `event` - The event to append
526    fn append_event(&self, prefix: &Prefix, event: &Event) -> Result<(), RegistryError>;
527
528    /// Append an event together with its CESR-attachment bytes (externalized
529    /// signatures and other CESR groups). Default impl delegates to
530    /// `append_event`, dropping the attachment for backends that don't yet
531    /// persist it. Backends supporting attachments override this.
532    fn append_signed_event(
533        &self,
534        prefix: &Prefix,
535        event: &Event,
536        _attachment: &[u8],
537    ) -> Result<(), RegistryError> {
538        self.append_event(prefix, event)
539    }
540
541    /// Read the CESR-attachment bytes for an event at the given sequence,
542    /// or `None` if no attachment was written. Default impl returns `None`.
543    fn get_attachment(
544        &self,
545        _prefix: &Prefix,
546        _seq: u128,
547    ) -> Result<Option<Vec<u8>>, RegistryError> {
548        Ok(None)
549    }
550
551    /// Get a single event by sequence number (random access).
552    ///
553    /// # Arguments
554    ///
555    /// * `prefix` - The KERI identifier prefix
556    /// * `seq` - The sequence number of the event
557    fn get_event(&self, prefix: &Prefix, seq: u128) -> Result<Event, RegistryError>;
558
559    /// Visit events starting from a sequence (streaming via visitor pattern).
560    ///
561    /// Calls `visitor` for each event. Return `ControlFlow::Break(())` to stop early.
562    ///
563    /// # Arguments
564    ///
565    /// * `prefix` - The KERI identifier prefix
566    /// * `from_seq` - Start reading from this sequence number
567    /// * `visitor` - Callback invoked for each event
568    fn visit_events(
569        &self,
570        prefix: &Prefix,
571        from_seq: u128,
572        visitor: &mut dyn FnMut(&Event) -> ControlFlow<()>,
573    ) -> Result<(), RegistryError>;
574
575    /// Get tip info without reading events.
576    ///
577    /// # Arguments
578    ///
579    /// * `prefix` - The KERI identifier prefix
580    fn get_tip(&self, prefix: &Prefix) -> Result<TipInfo, RegistryError>;
581
582    /// Get key state.
583    ///
584    /// First tries state.json (verified against tip.said).
585    /// Falls back to full KEL replay if state.json missing or stale.
586    ///
587    /// # Arguments
588    ///
589    /// * `prefix` - The KERI identifier prefix
590    fn get_key_state(&self, prefix: &Prefix) -> Result<KeyState, RegistryError>;
591
592    /// Write a pre-computed `KeyState` directly into the registry tree.
593    ///
594    /// # When to Use
595    ///
596    /// This is for the archival worker's write-through path: the worker
597    /// receives a `KeyState` from the mpsc channel (which was already
598    /// validated and written to Redis Tier 0) and needs to persist it
599    /// to Git Tier 1 without re-running KEL validation.
600    ///
601    /// This is **not** the append path for new KERI events. Use `append_event`
602    /// for that. This method only updates `state.json` — it does not append
603    /// any event blob or update `tip.json`. This is valid because the caller
604    /// received the `KeyState` from a successful `append_event` call.
605    ///
606    /// # Arguments
607    ///
608    /// * `prefix` - The KERI identifier prefix
609    /// * `state` - The pre-validated `KeyState` to persist
610    fn write_key_state(&self, prefix: &Prefix, state: &KeyState) -> Result<(), RegistryError>;
611
612    /// Visit all identity prefixes (streaming for tooling/export).
613    ///
614    /// Calls `visitor` for each identity prefix. Return `ControlFlow::Break(())` to stop early.
615    fn visit_identities(
616        &self,
617        visitor: &mut dyn FnMut(&str) -> ControlFlow<()>,
618    ) -> Result<(), RegistryError>;
619
620    // =========================================================================
621    // Attestation Operations
622    // =========================================================================
623
624    /// Store a device attestation.
625    ///
626    /// # Semantics: Latest-View (overwrites)
627    ///
628    /// Attestations use **latest-view** semantics - the current file represents
629    /// the latest state only. This function **overwrites any existing attestation**
630    /// for the same device DID.
631    ///
632    /// Historical attestations are preserved in `attestation_history/` via
633    /// `visit_attestation_history`.
634    ///
635    /// # Arguments
636    ///
637    /// * `attestation` - The attestation to store
638    fn store_attestation(&self, attestation: &Attestation) -> Result<(), RegistryError>;
639
640    /// Load an attestation for a device.
641    ///
642    /// Returns `None` if no attestation exists for the device.
643    ///
644    /// # Arguments
645    ///
646    /// * `did` - The device DID
647    fn load_attestation(&self, did: &CanonicalDid) -> Result<Option<Attestation>, RegistryError>;
648
649    /// Visit attestation history for a device (append-only audit trail).
650    ///
651    /// Iterates historical attestations in chronological order (oldest first).
652    /// Each entry represents a point-in-time snapshot of the device's attestation.
653    ///
654    /// # Arguments
655    ///
656    /// * `did` - The device DID
657    /// * `visitor` - Callback invoked for each historical attestation
658    fn visit_attestation_history(
659        &self,
660        did: &CanonicalDid,
661        visitor: &mut dyn FnMut(&Attestation) -> ControlFlow<()>,
662    ) -> Result<(), RegistryError>;
663
664    /// Visit all device DIDs in the registry.
665    ///
666    /// Calls `visitor` for each device DID. Return `ControlFlow::Break(())` to stop early.
667    fn visit_devices(
668        &self,
669        visitor: &mut dyn FnMut(&CanonicalDid) -> ControlFlow<()>,
670    ) -> Result<(), RegistryError>;
671
672    // =========================================================================
673    // Org Operations
674    // =========================================================================
675
676    /// Store an org member attestation.
677    ///
678    /// # Semantics: Latest-View (overwrites)
679    ///
680    /// Member attestations use **latest-view** semantics - the current file represents
681    /// the latest state only. This function **overwrites any existing attestation**
682    /// for the same member DID within the org.
683    ///
684    /// # Arguments
685    ///
686    /// * `org` - The org DID prefix
687    /// * `member` - The member's attestation
688    fn store_org_member(&self, org: &str, member: &Attestation) -> Result<(), RegistryError>;
689
690    /// Visit all members of an org (low-level, validation-aware).
691    ///
692    /// Iterates all member entries with their validation status.
693    /// Even invalid entries call visitor (for audit/tooling).
694    ///
695    /// # Arguments
696    ///
697    /// * `org` - The org DID prefix
698    /// * `visitor` - Callback invoked for each member entry
699    fn visit_org_member_attestations(
700        &self,
701        org: &str,
702        visitor: &mut dyn FnMut(&OrgMemberEntry) -> ControlFlow<()>,
703    ) -> Result<(), RegistryError>;
704
705    /// List org members with filtering (high-level API).
706    ///
707    /// Returns validated, filtered member views sorted deterministically by DID.
708    ///
709    /// # No Policy Decisions
710    ///
711    /// This method does NOT compute revoked/expired status. All valid attestations
712    /// are returned with `status: Active`. Use the `revoked_at` and `expires_at` fields
713    /// to compute actual status in your policy layer:
714    ///
715    /// ```ignore
716    /// let actual_status = compute_status_from_view(&view, now);
717    /// ```
718    ///
719    /// The `include_statuses` filter is ignored (status filtering is policy). The
720    /// `roles_any`/`capabilities_*` filters are also inert: role/caps authority is
721    /// KEL-native (delegator-anchored scope seal), not the attestation, so this method
722    /// no longer reads attestation role/caps to filter or populate views.
723    ///
724    /// # Arguments
725    ///
726    /// * `org` - The org DID prefix
727    /// * `_filter` - Filter criteria (retained for API stability; role/caps filters are inert)
728    fn list_org_members(
729        &self,
730        org: &str,
731        _filter: &MemberFilter,
732    ) -> Result<Vec<MemberView>, RegistryError> {
733        let mut members = Vec::new();
734
735        self.visit_org_member_attestations(org, &mut |entry| {
736            let (status, att_opt, revoked_at) = match &entry.attestation {
737                Ok(att) => (MemberStatus::Active, Some(att), att.revoked_at),
738                Err(reason) => (
739                    MemberStatus::Invalid {
740                        reason: reason.clone(),
741                    },
742                    None,
743                    None,
744                ),
745            };
746
747            // For valid attestations, surface identity facts only.
748            //
749            // Role/capabilities authority is KEL-native (the delegator-anchored scope
750            // seal — see `crate::storage::registry::org_member` and the SDK org
751            // delegation path), NOT the attestation. The scope-seal-derived values are
752            // not available at this storage seam, so `MemberView.role`/`.capabilities`
753            // are left empty here (display-only, fail-closed): a consumer that needs
754            // authority must resolve it from the KEL, never from these fields.
755            // The `roles_any`/`capabilities_*` filters are no longer applied for the
756            // same reason — filtering on attestation-borne role/caps would re-introduce
757            // the retired authority reader.
758            if let Some(att) = att_opt {
759                members.push(MemberView {
760                    did: entry.did.clone(),
761                    status,
762                    role: None,
763                    capabilities: vec![],
764                    #[allow(clippy::disallowed_methods)] // INVARIANT: att.issuer is a CanonicalDid parsed from validated attestation JSON
765                    issuer: IdentityDID::new_unchecked(att.issuer.as_str()),
766                    rid: att.rid.clone(),
767                    revoked_at,
768                    expires_at: att.expires_at,
769                    timestamp: att.timestamp,
770                    source_filename: entry.filename.clone(),
771                });
772            } else {
773                members.push(MemberView {
774                    did: entry.did.clone(),
775                    status,
776                    role: None,
777                    capabilities: vec![],
778                    issuer: entry.org.clone(),
779                    rid: ResourceId::new(""),
780                    revoked_at: None,
781                    expires_at: None,
782                    timestamp: None,
783                    source_filename: entry.filename.clone(),
784                });
785            }
786
787            ControlFlow::Continue(())
788        })?;
789
790        // Sort deterministically by DID only (status not computed here)
791        members.sort_by(|a, b| a.did.to_string().cmp(&b.did.to_string()));
792
793        Ok(members)
794    }
795
796    // =========================================================================
797    // Lifecycle
798    // =========================================================================
799
800    /// Initialize the registry if it has not been set up yet.
801    ///
802    /// Creates the initial registry structure (e.g., first Git commit, database
803    /// schema). Returns `true` if initialization was performed, `false` if the
804    /// registry was already initialized.
805    ///
806    /// Usage:
807    /// ```ignore
808    /// let was_initialized = backend.init_if_needed()?;
809    /// ```
810    fn init_if_needed(&self) -> Result<bool, RegistryError>;
811
812    // =========================================================================
813    // Metadata
814    // =========================================================================
815
816    /// Get registry metadata.
817    fn metadata(&self) -> Result<RegistryMetadata, RegistryError>;
818
819    // =========================================================================
820    // Fast-path operations (index-accelerated)
821    // =========================================================================
822
823    /// List org members using the SQLite index when available.
824    ///
825    /// # Justification for addition to frozen surface
826    ///
827    /// This is a performance optimization of the existing `list_org_members` method,
828    /// not a new capability. The default implementation delegates to `list_org_members`
829    /// (O(n) Git scan). Backends with an index override this for O(1) SQLite lookup.
830    ///
831    /// # Filter limitations
832    ///
833    /// The index stores only DID, rid, issuer, revoked_at, and expires_at.
834    /// If the filter includes `roles_any`, `capabilities_any`, or `capabilities_all`,
835    /// the implementation falls back to the Git scan (those fields require the full
836    /// attestation blob). Status filtering is always deferred to the policy layer.
837    ///
838    /// # Arguments
839    ///
840    /// * `org` - The org DID prefix
841    /// * `filter` - Filter criteria for members
842    fn list_org_members_fast(
843        &self,
844        org: &str,
845        filter: &MemberFilter,
846    ) -> Result<Vec<MemberView>, RegistryError> {
847        self.list_org_members(org, filter)
848    }
849
850    // =========================================================================
851    // TEL (Transaction Event Log) Operations (FROZEN-TRAIT EXCEPTION)
852    //
853    // Justification (atomicity — same exception that justified the write-batch
854    // surface above): a credential issuance writes three artifacts that MUST be
855    // consistent — the ACDC blob, the `iss` TEL event, and the issuer KEL `ixn`
856    // carrying the TEL anchor seal. A crash between any two leaves a dangling
857    // state: an anchored-but-absent TEL event, or a credential blob with no
858    // status log, or a TEL event the KEL never anchored (forgeable). These three
859    // writes therefore commit together via `commit_batch`, exactly as the
860    // attestation-blob + KEL-ixn pair does. The TEL is a derived, KEL-anchored
861    // index (the registry remains a cache, not a source of truth); these methods
862    // add only persistence + retrieval of that index, never trust decisions.
863    //
864    // Reviewed and approved as a frozen-surface extension for Epic F (credential
865    // registry). Backends that do not persist a TEL may leave the default
866    // `NotImplemented` stubs.
867    // =========================================================================
868
869    /// Append a TEL event (`vcp`/`iss`/`rev`) under an issuer's registry.
870    ///
871    /// # Semantics: Append-Only
872    ///
873    /// TEL events are immutable once written. Implementations refuse to overwrite
874    /// an existing event at the same `(registry, credential, sn)` coordinate.
875    ///
876    /// Prefer staging this in an [`AtomicWriteBatch`] alongside the anchoring `ixn`
877    /// (and, for an `iss`, the ACDC blob) so all three land in one commit.
878    ///
879    /// # Arguments
880    ///
881    /// * `issuer` - Issuing AID controlling the registry.
882    /// * `registry_said` - Registry SAID (`vcp.d`).
883    /// * `credential_said` - Credential SAID (equals the registry SAID for a `vcp`).
884    /// * `sn` - TEL event sequence number.
885    /// * `event_bytes` - The TEL event's canonical insertion-order JSON bytes.
886    fn append_tel_event(
887        &self,
888        _issuer: &Prefix,
889        _registry_said: &Said,
890        _credential_said: &Said,
891        _sn: u128,
892        _event_bytes: &[u8],
893    ) -> Result<(), RegistryError> {
894        Err(RegistryError::NotImplemented {
895            method: "append_tel_event",
896        })
897    }
898
899    /// Visit the persisted TEL events for one credential, oldest first.
900    ///
901    /// Calls `visitor` with each event's raw JSON bytes in ascending `sn` order.
902    /// Return `ControlFlow::Break(())` to stop early. A credential with no TEL
903    /// events visits nothing and returns `Ok(())`.
904    ///
905    /// # Arguments
906    ///
907    /// * `issuer` - Issuing AID.
908    /// * `registry_said` - Registry SAID.
909    /// * `credential_said` - Credential SAID.
910    /// * `visitor` - Callback invoked for each TEL event's JSON bytes.
911    fn visit_tel_events(
912        &self,
913        _issuer: &Prefix,
914        _registry_said: &Said,
915        _credential_said: &Said,
916        _visitor: &mut dyn FnMut(&[u8]) -> ControlFlow<()>,
917    ) -> Result<(), RegistryError> {
918        Err(RegistryError::NotImplemented {
919            method: "visit_tel_events",
920        })
921    }
922
923    /// Store an ACDC credential blob under an issuer's namespace.
924    ///
925    /// Latest-view by credential SAID; since a credential SAID is content-addressed,
926    /// re-storing the same SAID is idempotent.
927    ///
928    /// # Arguments
929    ///
930    /// * `issuer` - Issuing AID.
931    /// * `credential_said` - Credential SAID (`acdc.d`).
932    /// * `credential_bytes` - The ACDC's canonical insertion-order JSON bytes.
933    fn store_credential(
934        &self,
935        _issuer: &Prefix,
936        _credential_said: &Said,
937        _credential_bytes: &[u8],
938    ) -> Result<(), RegistryError> {
939        Err(RegistryError::NotImplemented {
940            method: "store_credential",
941        })
942    }
943
944    /// Load an ACDC credential blob, or `None` if absent.
945    ///
946    /// # Arguments
947    ///
948    /// * `issuer` - Issuing AID.
949    /// * `credential_said` - Credential SAID.
950    fn load_credential(
951        &self,
952        _issuer: &Prefix,
953        _credential_said: &Said,
954    ) -> Result<Option<Vec<u8>>, RegistryError> {
955        Err(RegistryError::NotImplemented {
956            method: "load_credential",
957        })
958    }
959
960    // =========================================================================
961    // Atomic Batch Writes (FROZEN-TRAIT EXCEPTION)
962    //
963    // Justification: Attestation blob + KEL ixn event must land in a single
964    // Git commit. Without this, a crash between two writes leaves orphaned
965    // blobs or dangling seals — a security gap where forged attestations
966    // could exist without KEL evidence. See verification_bindings.md Gap 1.
967    // =========================================================================
968
969    /// Commit a batch of write operations atomically.
970    ///
971    /// Git backends override this to stage all operations in a single
972    /// `TreeMutator` and produce one commit. The default implementation
973    /// falls back to sequential individual writes (non-atomic, acceptable
974    /// for in-memory test backends).
975    fn commit_batch(&self, batch: &AtomicWriteBatch) -> Result<(), RegistryError> {
976        for op in batch.ops() {
977            match op {
978                AtomicWriteOp::StoreAttestation(att) => self.store_attestation(att)?,
979                AtomicWriteOp::StoreOrgMember { org, member } => {
980                    self.store_org_member(org, member)?
981                }
982                AtomicWriteOp::AppendEvent {
983                    prefix,
984                    event,
985                    attachment,
986                } => {
987                    if attachment.is_empty() {
988                        self.append_event(prefix, event)?;
989                    } else {
990                        self.append_signed_event(prefix, event, attachment)?;
991                    }
992                }
993                AtomicWriteOp::AppendTelEvent {
994                    issuer,
995                    registry_said,
996                    credential_said,
997                    sn,
998                    event_bytes,
999                } => {
1000                    self.append_tel_event(issuer, registry_said, credential_said, *sn, event_bytes)?
1001                }
1002                AtomicWriteOp::StoreCredential {
1003                    issuer,
1004                    credential_said,
1005                    credential_bytes,
1006                } => self.store_credential(issuer, credential_said, credential_bytes)?,
1007            }
1008        }
1009        Ok(())
1010    }
1011}
1012
1013/// Blanket impl so `Arc<dyn RegistryBackend + Send + Sync>` can be used directly
1014/// as a `RegistryBackend` at call sites (e.g., tests, extractors).
1015impl<T: RegistryBackend + ?Sized> RegistryBackend for Arc<T> {
1016    fn append_event(&self, prefix: &Prefix, event: &Event) -> Result<(), RegistryError> {
1017        (**self).append_event(prefix, event)
1018    }
1019
1020    fn get_event(&self, prefix: &Prefix, seq: u128) -> Result<Event, RegistryError> {
1021        (**self).get_event(prefix, seq)
1022    }
1023
1024    fn visit_events(
1025        &self,
1026        prefix: &Prefix,
1027        from_seq: u128,
1028        visitor: &mut dyn FnMut(&Event) -> ControlFlow<()>,
1029    ) -> Result<(), RegistryError> {
1030        (**self).visit_events(prefix, from_seq, visitor)
1031    }
1032
1033    fn get_tip(&self, prefix: &Prefix) -> Result<TipInfo, RegistryError> {
1034        (**self).get_tip(prefix)
1035    }
1036
1037    fn get_key_state(&self, prefix: &Prefix) -> Result<KeyState, RegistryError> {
1038        (**self).get_key_state(prefix)
1039    }
1040
1041    fn write_key_state(&self, prefix: &Prefix, state: &KeyState) -> Result<(), RegistryError> {
1042        (**self).write_key_state(prefix, state)
1043    }
1044
1045    fn visit_identities(
1046        &self,
1047        visitor: &mut dyn FnMut(&str) -> ControlFlow<()>,
1048    ) -> Result<(), RegistryError> {
1049        (**self).visit_identities(visitor)
1050    }
1051
1052    fn store_attestation(&self, attestation: &Attestation) -> Result<(), RegistryError> {
1053        (**self).store_attestation(attestation)
1054    }
1055
1056    fn load_attestation(&self, did: &CanonicalDid) -> Result<Option<Attestation>, RegistryError> {
1057        (**self).load_attestation(did)
1058    }
1059
1060    fn visit_attestation_history(
1061        &self,
1062        did: &CanonicalDid,
1063        visitor: &mut dyn FnMut(&Attestation) -> ControlFlow<()>,
1064    ) -> Result<(), RegistryError> {
1065        (**self).visit_attestation_history(did, visitor)
1066    }
1067
1068    fn visit_devices(
1069        &self,
1070        visitor: &mut dyn FnMut(&CanonicalDid) -> ControlFlow<()>,
1071    ) -> Result<(), RegistryError> {
1072        (**self).visit_devices(visitor)
1073    }
1074
1075    fn store_org_member(&self, org: &str, member: &Attestation) -> Result<(), RegistryError> {
1076        (**self).store_org_member(org, member)
1077    }
1078
1079    fn visit_org_member_attestations(
1080        &self,
1081        org: &str,
1082        visitor: &mut dyn FnMut(&OrgMemberEntry) -> ControlFlow<()>,
1083    ) -> Result<(), RegistryError> {
1084        (**self).visit_org_member_attestations(org, visitor)
1085    }
1086
1087    fn init_if_needed(&self) -> Result<bool, RegistryError> {
1088        (**self).init_if_needed()
1089    }
1090
1091    fn metadata(&self) -> Result<RegistryMetadata, RegistryError> {
1092        (**self).metadata()
1093    }
1094
1095    fn append_tel_event(
1096        &self,
1097        issuer: &Prefix,
1098        registry_said: &Said,
1099        credential_said: &Said,
1100        sn: u128,
1101        event_bytes: &[u8],
1102    ) -> Result<(), RegistryError> {
1103        (**self).append_tel_event(issuer, registry_said, credential_said, sn, event_bytes)
1104    }
1105
1106    fn visit_tel_events(
1107        &self,
1108        issuer: &Prefix,
1109        registry_said: &Said,
1110        credential_said: &Said,
1111        visitor: &mut dyn FnMut(&[u8]) -> ControlFlow<()>,
1112    ) -> Result<(), RegistryError> {
1113        (**self).visit_tel_events(issuer, registry_said, credential_said, visitor)
1114    }
1115
1116    fn store_credential(
1117        &self,
1118        issuer: &Prefix,
1119        credential_said: &Said,
1120        credential_bytes: &[u8],
1121    ) -> Result<(), RegistryError> {
1122        (**self).store_credential(issuer, credential_said, credential_bytes)
1123    }
1124
1125    fn load_credential(
1126        &self,
1127        issuer: &Prefix,
1128        credential_said: &Said,
1129    ) -> Result<Option<Vec<u8>>, RegistryError> {
1130        (**self).load_credential(issuer, credential_said)
1131    }
1132
1133    fn commit_batch(&self, batch: &AtomicWriteBatch) -> Result<(), RegistryError> {
1134        (**self).commit_batch(batch)
1135    }
1136}
1137
1138#[cfg(test)]
1139mod tests {
1140    use super::*;
1141
1142    #[test]
1143    fn error_identity_not_found() {
1144        let prefix = Prefix::new_unchecked("ETest123".to_string());
1145        let err = RegistryError::identity_not_found(&prefix);
1146        assert!(err.to_string().contains("identity"));
1147        assert!(err.to_string().contains("ETest123"));
1148    }
1149
1150    #[test]
1151    fn error_event_not_found() {
1152        let prefix = Prefix::new_unchecked("ETest123".to_string());
1153        let err = RegistryError::event_not_found(&prefix, 5);
1154        assert!(err.to_string().contains("event"));
1155        assert!(err.to_string().contains("seq 5"));
1156    }
1157
1158    #[test]
1159    fn error_device_not_found() {
1160        let err = RegistryError::device_not_found("did:key:test");
1161        assert!(err.to_string().contains("device"));
1162        assert!(err.to_string().contains("did:key:test"));
1163    }
1164
1165    #[test]
1166    fn error_sequence_gap() {
1167        let err = RegistryError::SequenceGap {
1168            prefix: "ETest".into(),
1169            expected: 5,
1170            got: 7,
1171        };
1172        assert!(err.to_string().contains("expected 5"));
1173        assert!(err.to_string().contains("got 7"));
1174    }
1175
1176    #[test]
1177    fn error_concurrent_modification() {
1178        let err = RegistryError::ConcurrentModification("Registry was modified".into());
1179        assert!(err.to_string().contains("Concurrent"));
1180        assert!(err.to_string().contains("modified"));
1181    }
1182
1183    #[test]
1184    fn validated_tenant_id_normalizes_to_lowercase() {
1185        let tid = ValidatedTenantId::new("ACME").unwrap();
1186        assert_eq!(tid.as_str(), "acme");
1187    }
1188
1189    #[test]
1190    fn validated_tenant_id_rejects_empty() {
1191        assert!(ValidatedTenantId::new("").is_err());
1192    }
1193
1194    #[test]
1195    fn validated_tenant_id_rejects_reserved() {
1196        assert!(ValidatedTenantId::new("admin").is_err());
1197    }
1198
1199    #[test]
1200    fn validated_tenant_id_rejects_bad_chars() {
1201        assert!(ValidatedTenantId::new("acme/sub").is_err());
1202        assert!(ValidatedTenantId::new("../escape").is_err());
1203    }
1204
1205    #[test]
1206    fn validated_tenant_id_accepts_valid() {
1207        let tid = ValidatedTenantId::new("my-tenant_123").unwrap();
1208        assert_eq!(tid.as_str(), "my-tenant_123");
1209    }
1210
1211    #[test]
1212    fn validated_tenant_id_display() {
1213        let tid = ValidatedTenantId::new("acme").unwrap();
1214        assert_eq!(format!("{}", tid), "acme");
1215    }
1216
1217    #[test]
1218    fn validated_tenant_id_into_string() {
1219        let tid = ValidatedTenantId::new("acme").unwrap();
1220        let s: String = tid.into();
1221        assert_eq!(s, "acme");
1222    }
1223}