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}