Skip to main content

auths_verifier/
types.rs

1//! Verification types: reports, statuses, and device DIDs.
2
3use crate::witness::WitnessQuorum;
4use chrono::{DateTime, Utc};
5use serde::{Deserialize, Serialize};
6
7// ============================================================================
8// Verification Report Types
9// ============================================================================
10
11/// Machine-readable verification result containing status, chain details, and warnings.
12#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
13pub struct VerificationReport {
14    /// The overall verification status
15    pub status: VerificationStatus,
16    /// Details of each link in the verification chain
17    pub chain: Vec<ChainLink>,
18    /// Non-fatal warnings encountered during verification
19    pub warnings: Vec<String>,
20    /// Optional witness quorum result (present when witness verification was performed)
21    #[serde(default, skip_serializing_if = "Option::is_none")]
22    pub witness_quorum: Option<WitnessQuorum>,
23}
24
25impl VerificationReport {
26    /// Returns true only when the verification status is Valid.
27    pub fn is_valid(&self) -> bool {
28        matches!(self.status, VerificationStatus::Valid)
29    }
30
31    /// Creates a new valid VerificationReport with the given chain.
32    pub fn valid(chain: Vec<ChainLink>) -> Self {
33        Self {
34            status: VerificationStatus::Valid,
35            chain,
36            warnings: Vec::new(),
37            witness_quorum: None,
38        }
39    }
40
41    /// Creates a new VerificationReport with the given status and chain.
42    pub fn with_status(status: VerificationStatus, chain: Vec<ChainLink>) -> Self {
43        Self {
44            status,
45            chain,
46            warnings: Vec::new(),
47            witness_quorum: None,
48        }
49    }
50}
51
52/// Verification outcome indicating success or the type of failure.
53#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
54#[serde(tag = "type")]
55pub enum VerificationStatus {
56    /// The attestation(s) are valid
57    Valid,
58    /// The attestation has expired
59    Expired {
60        /// When the attestation expired
61        at: DateTime<Utc>,
62    },
63    /// The attestation has been revoked
64    Revoked {
65        /// When the attestation was revoked (if known)
66        at: Option<DateTime<Utc>>,
67    },
68    /// A signature in the chain is invalid
69    InvalidSignature {
70        /// The step in the chain where the invalid signature was found (0-indexed)
71        step: usize,
72    },
73    /// The chain has a broken link (issuer→subject mismatch or missing attestation)
74    BrokenChain {
75        /// Description of the missing link
76        missing_link: String,
77    },
78    /// Insufficient witness receipts to meet quorum threshold
79    InsufficientWitnesses {
80        /// Number of witnesses required
81        required: usize,
82        /// Number of witnesses that verified successfully
83        verified: usize,
84    },
85}
86
87/// A single link in a verification chain, representing one attestation's verification result.
88#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
89pub struct ChainLink {
90    /// The issuer DID of this attestation
91    pub issuer: String,
92    /// The subject DID of this attestation
93    pub subject: String,
94    /// Whether this link's signature is valid
95    pub valid: bool,
96    /// Error message if verification failed
97    pub error: Option<String>,
98}
99
100impl ChainLink {
101    /// Creates a new valid chain link.
102    pub fn valid(issuer: String, subject: String) -> Self {
103        Self {
104            issuer,
105            subject,
106            valid: true,
107            error: None,
108        }
109    }
110
111    /// Creates a new invalid chain link with an error message.
112    pub fn invalid(issuer: String, subject: String, error: String) -> Self {
113        Self {
114            issuer,
115            subject,
116            valid: false,
117            error: Some(error),
118        }
119    }
120}
121
122// ============================================================================
123// DID Types
124// ============================================================================
125
126use std::borrow::Borrow;
127use std::fmt;
128use std::ops::Deref;
129use std::str::FromStr;
130
131// ============================================================================
132// IdentityDID Type
133// ============================================================================
134
135/// Strongly-typed wrapper for identity DIDs (e.g., `"did:keri:E..."`).
136///
137/// Usage:
138/// ```rust
139/// # use auths_verifier::IdentityDID;
140/// let did = IdentityDID::parse("did:keri:Eabc123").unwrap();
141/// assert_eq!(did.as_str(), "did:keri:Eabc123");
142///
143/// let s: String = did.into_inner();
144/// ```
145#[derive(Debug, Clone, Serialize, PartialEq, Eq, PartialOrd, Ord, Hash)]
146#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
147#[repr(transparent)]
148pub struct IdentityDID(String);
149
150impl IdentityDID {
151    /// Wraps a DID string without validation (for trusted internal paths).
152    pub fn new_unchecked<S: Into<String>>(s: S) -> Self {
153        Self(s.into())
154    }
155
156    /// Validates and parses a `did:keri:` string into an `IdentityDID`.
157    ///
158    /// Args:
159    /// * `s`: A DID string that must start with `did:keri:` followed by a non-empty KERI prefix.
160    ///
161    /// Usage:
162    /// ```rust
163    /// # use auths_verifier::IdentityDID;
164    /// let did = IdentityDID::parse("did:keri:EPrefix123").unwrap();
165    /// assert_eq!(did.as_str(), "did:keri:EPrefix123");
166    /// ```
167    pub fn parse(s: &str) -> Result<Self, DidParseError> {
168        match s.strip_prefix("did:keri:") {
169            Some("") => Err(DidParseError::EmptyIdentifier),
170            Some(_) => Ok(Self(s.to_string())),
171            None => Err(DidParseError::InvalidIdentityPrefix(s.to_string())),
172        }
173    }
174
175    /// Builds an `IdentityDID` from a raw KERI prefix string.
176    ///
177    /// Args:
178    /// * `prefix`: The KERI prefix without the `did:keri:` scheme (e.g., `"EOrg123"`).
179    ///
180    /// Usage:
181    /// ```rust
182    /// # use auths_verifier::IdentityDID;
183    /// let did = IdentityDID::from_prefix("EOrg123").unwrap();
184    /// assert_eq!(did.as_str(), "did:keri:EOrg123");
185    /// ```
186    pub fn from_prefix(prefix: &str) -> Result<Self, DidParseError> {
187        if prefix.is_empty() {
188            return Err(DidParseError::EmptyIdentifier);
189        }
190        Ok(Self(format!("did:keri:{}", prefix)))
191    }
192
193    /// Returns the KERI prefix portion of the DID (after `did:keri:`).
194    ///
195    /// Usage:
196    /// ```rust
197    /// # use auths_verifier::IdentityDID;
198    /// let did = IdentityDID::parse("did:keri:EOrg123").unwrap();
199    /// assert_eq!(did.prefix(), "EOrg123");
200    /// ```
201    pub fn prefix(&self) -> &str {
202        self.0.strip_prefix("did:keri:").unwrap_or(&self.0)
203    }
204
205    /// Returns the DID as a string slice.
206    pub fn as_str(&self) -> &str {
207        &self.0
208    }
209
210    /// Consumes self and returns the inner String.
211    pub fn into_inner(self) -> String {
212        self.0
213    }
214}
215
216impl fmt::Display for IdentityDID {
217    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
218        f.write_str(&self.0)
219    }
220}
221
222impl FromStr for IdentityDID {
223    type Err = DidParseError;
224
225    fn from_str(s: &str) -> Result<Self, Self::Err> {
226        Self::parse(s)
227    }
228}
229
230impl TryFrom<&str> for IdentityDID {
231    type Error = DidParseError;
232
233    fn try_from(s: &str) -> Result<Self, Self::Error> {
234        Self::parse(s)
235    }
236}
237
238impl TryFrom<String> for IdentityDID {
239    type Error = DidParseError;
240
241    fn try_from(s: String) -> Result<Self, Self::Error> {
242        Self::parse(&s)
243    }
244}
245
246impl From<IdentityDID> for String {
247    fn from(did: IdentityDID) -> String {
248        did.0
249    }
250}
251
252impl<'de> serde::Deserialize<'de> for IdentityDID {
253    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
254    where
255        D: serde::Deserializer<'de>,
256    {
257        let s = String::deserialize(deserializer)?;
258        Self::parse(&s).map_err(serde::de::Error::custom)
259    }
260}
261
262impl Deref for IdentityDID {
263    type Target = str;
264
265    fn deref(&self) -> &Self::Target {
266        &self.0
267    }
268}
269
270impl AsRef<str> for IdentityDID {
271    fn as_ref(&self) -> &str {
272        &self.0
273    }
274}
275
276impl Borrow<str> for IdentityDID {
277    fn borrow(&self) -> &str {
278        &self.0
279    }
280}
281
282impl PartialEq<str> for IdentityDID {
283    fn eq(&self, other: &str) -> bool {
284        self.0 == other
285    }
286}
287
288impl PartialEq<&str> for IdentityDID {
289    fn eq(&self, other: &&str) -> bool {
290        self.0 == *other
291    }
292}
293
294impl PartialEq<IdentityDID> for str {
295    fn eq(&self, other: &IdentityDID) -> bool {
296        self == other.0
297    }
298}
299
300impl PartialEq<IdentityDID> for &str {
301    fn eq(&self, other: &IdentityDID) -> bool {
302        *self == other.0
303    }
304}
305
306// ============================================================================
307// DeviceDID Type
308// ============================================================================
309
310/// Wrapper around a device DID string that ensures Git-safe ref formatting.
311#[derive(Debug, Clone, Serialize, PartialEq, Eq, PartialOrd, Ord, Hash)]
312#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
313pub struct DeviceDID(String);
314
315impl DeviceDID {
316    /// Wraps a DID string without validation (for trusted internal paths).
317    pub fn new_unchecked<S: Into<String>>(s: S) -> Self {
318        DeviceDID(s.into())
319    }
320
321    /// Validates and parses a `did:key:z` string into a `DeviceDID`.
322    ///
323    /// Args:
324    /// * `s`: A DID string that must start with `did:key:z` followed by non-empty base58 content.
325    ///
326    /// Usage:
327    /// ```rust
328    /// # use auths_verifier::DeviceDID;
329    /// let did = DeviceDID::parse("did:key:z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK").unwrap();
330    /// assert_eq!(did.as_str(), "did:key:z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK");
331    /// ```
332    pub fn parse(s: &str) -> Result<Self, DidParseError> {
333        match s.strip_prefix("did:key:z") {
334            Some("") => Err(DidParseError::EmptyIdentifier),
335            Some(_) => Ok(Self(s.to_string())),
336            None => Err(DidParseError::InvalidDevicePrefix(s.to_string())),
337        }
338    }
339
340    /// Constructs a `did:key:z...` identifier from a 32-byte Ed25519 public key.
341    ///
342    /// This uses the multicodec prefix for Ed25519 (0xED 0x01) and encodes it with base58btc.
343    pub fn from_ed25519(pubkey: &[u8; 32]) -> Self {
344        let mut prefixed = vec![0xED, 0x01];
345        prefixed.extend_from_slice(pubkey);
346
347        let encoded = bs58::encode(prefixed).into_string();
348        Self(format!("did:key:z{}", encoded))
349    }
350
351    /// Returns a sanitized version of the DID for use in Git refs,
352    /// replacing all non-alphanumeric characters with `_`.
353    pub fn ref_name(&self) -> String {
354        self.0
355            .chars()
356            .map(|c| if c.is_ascii_alphanumeric() { c } else { '_' })
357            .collect()
358    }
359
360    /// Compares a sanitized DID ref name to this real DeviceDID.
361    /// Used to match Git refs to known device DIDs.
362    pub fn matches_sanitized_ref(&self, ref_name: &str) -> bool {
363        self.ref_name() == ref_name
364    }
365
366    /// Tries to reverse-lookup a real DID from a sanitized string,
367    /// given a list of known real DIDs.
368    pub fn from_sanitized<'a>(
369        sanitized: &str,
370        known_dids: &'a [DeviceDID],
371    ) -> Option<&'a DeviceDID> {
372        known_dids.iter().find(|did| did.ref_name() == sanitized)
373    }
374
375    /// Optionally expose the inner raw DID
376    pub fn as_str(&self) -> &str {
377        &self.0
378    }
379}
380
381impl fmt::Display for DeviceDID {
382    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
383        self.0.fmt(f)
384    }
385}
386
387impl FromStr for DeviceDID {
388    type Err = DidParseError;
389
390    fn from_str(s: &str) -> Result<Self, Self::Err> {
391        Self::parse(s)
392    }
393}
394
395impl TryFrom<&str> for DeviceDID {
396    type Error = DidParseError;
397
398    fn try_from(s: &str) -> Result<Self, Self::Error> {
399        Self::parse(s)
400    }
401}
402
403impl TryFrom<String> for DeviceDID {
404    type Error = DidParseError;
405
406    fn try_from(s: String) -> Result<Self, Self::Error> {
407        Self::parse(&s)
408    }
409}
410
411impl From<DeviceDID> for String {
412    fn from(did: DeviceDID) -> String {
413        did.0
414    }
415}
416
417impl<'de> serde::Deserialize<'de> for DeviceDID {
418    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
419    where
420        D: serde::Deserializer<'de>,
421    {
422        let s = String::deserialize(deserializer)?;
423        Self::parse(&s).map_err(serde::de::Error::custom)
424    }
425}
426
427impl Deref for DeviceDID {
428    type Target = str;
429
430    fn deref(&self) -> &Self::Target {
431        &self.0
432    }
433}
434
435// ============================================================================
436// DID Utility Functions
437// ============================================================================
438
439/// Convert a hex-encoded Ed25519 public key to a `did:key:` device DID.
440///
441/// The hex string must decode to exactly 32 bytes.
442///
443/// ```rust
444/// # use auths_verifier::types::signer_hex_to_did;
445/// let did = signer_hex_to_did("d75a980182b10ab7d54bfed3c964073a0ee172f3daa3f4a18446b7ddc8").unwrap_err();
446/// // (example key is wrong length — a real 32-byte hex key would succeed)
447/// ```
448pub fn signer_hex_to_did(hex_key: &str) -> Result<DeviceDID, DidConversionError> {
449    let bytes = hex::decode(hex_key).map_err(|e| DidConversionError::InvalidHex(e.to_string()))?;
450    let arr: [u8; 32] = bytes
451        .try_into()
452        .map_err(|v: Vec<u8>| DidConversionError::WrongKeyLength(v.len()))?;
453    Ok(DeviceDID::from_ed25519(&arr))
454}
455
456/// Validate a DID string (accepts both `did:keri:` and `did:key:` formats).
457///
458/// Returns `true` if the DID has a recognized scheme and non-empty identifier.
459pub fn validate_did(did_str: &str) -> bool {
460    if let Some(rest) = did_str.strip_prefix("did:keri:") {
461        !rest.is_empty()
462    } else if let Some(rest) = did_str.strip_prefix("did:key:") {
463        !rest.is_empty()
464    } else {
465        false
466    }
467}
468
469/// Errors from DID conversion operations.
470#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
471pub enum DidConversionError {
472    /// The input is not valid hexadecimal.
473    #[error("invalid hex: {0}")]
474    InvalidHex(String),
475    /// The decoded key is not 32 bytes.
476    #[error("expected 32-byte Ed25519 key, got {0} bytes")]
477    WrongKeyLength(usize),
478}
479
480/// Errors from DID string parsing and validation.
481#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
482#[non_exhaustive]
483pub enum DidParseError {
484    /// DeviceDID must start with `did:key:z`.
485    #[error("DeviceDID must start with 'did:key:z', got: {0}")]
486    InvalidDevicePrefix(String),
487    /// IdentityDID must start with `did:keri:`.
488    #[error("IdentityDID must start with 'did:keri:', got: {0}")]
489    InvalidIdentityPrefix(String),
490    /// The method-specific identifier portion is empty.
491    #[error("DID method-specific identifier is empty")]
492    EmptyIdentifier,
493    /// Generic DID format validation failure (used by `CanonicalDid`).
494    #[error("{0}")]
495    InvalidFormat(String),
496    /// DID string contains control characters.
497    #[error("DID contains control characters")]
498    ControlCharacters,
499}
500
501// ============================================================================
502// CanonicalDid Type
503// ============================================================================
504
505/// A validated, canonical DID that accepts any method (`did:keri:`, `did:key:`, etc.).
506///
507/// Use this for fields that can hold either identity or device DIDs,
508/// such as attestation issuers which may be `did:keri:` or `did:key:`.
509///
510/// Constructed via `parse()` which enforces:
511/// - Starts with `did:`
512/// - Has at least method and id segments: `did:method:id`
513/// - Lowercased method (KERI, key methods are case-sensitive in id, not method)
514/// - No trailing whitespace or control characters
515#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize)]
516#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
517#[serde(transparent)]
518pub struct CanonicalDid(String);
519
520impl CanonicalDid {
521    /// Parse and validate a DID string into canonical form.
522    pub fn parse(raw: &str) -> Result<Self, DidParseError> {
523        if raw.chars().any(|c| c.is_control()) {
524            return Err(DidParseError::ControlCharacters);
525        }
526        let trimmed = raw.trim();
527        if trimmed.is_empty() {
528            return Err(DidParseError::EmptyIdentifier);
529        }
530        let parts: Vec<&str> = trimmed.splitn(3, ':').collect();
531        if parts.len() < 3 || parts[0] != "did" || parts[1].is_empty() || parts[2].is_empty() {
532            return Err(DidParseError::InvalidFormat(format!(
533                "invalid DID format: '{}'",
534                trimmed
535            )));
536        }
537        let canonical = format!("did:{}:{}", parts[1].to_lowercase(), parts[2]);
538        Ok(Self(canonical))
539    }
540
541    /// Wraps a DID string without validation (for trusted internal paths).
542    pub fn new_unchecked<S: Into<String>>(s: S) -> Self {
543        Self(s.into())
544    }
545
546    /// Returns the canonical DID as a string slice.
547    pub fn as_str(&self) -> &str {
548        &self.0
549    }
550
551    /// Returns the method-specific identifier (the part after `did:method:`).
552    pub fn method_specific_id(&self) -> &str {
553        self.0.splitn(3, ':').nth(2).unwrap_or("")
554    }
555
556    /// Validates that this DID uses the `keri` method with a valid KERI prefix.
557    pub fn require_keri(self) -> Result<Self, DidParseError> {
558        let parts: Vec<&str> = self.0.splitn(3, ':').collect();
559        if parts[1] != "keri" {
560            return Err(DidParseError::InvalidFormat(format!(
561                "expected did:keri: DID, got did:{}:",
562                parts[1]
563            )));
564        }
565        let id = parts[2];
566        if id.len() < 2 || id.len() > 128 {
567            return Err(DidParseError::InvalidFormat(
568                "invalid KERI prefix: length must be 2–128 characters".into(),
569            ));
570        }
571        if !id.starts_with(|c: char| c.is_ascii_uppercase()) {
572            return Err(DidParseError::InvalidFormat(format!(
573                "invalid KERI prefix: must start with an uppercase derivation code, got '{}'",
574                &id[..1]
575            )));
576        }
577        Ok(self)
578    }
579
580    /// Consumes self and returns the inner String.
581    pub fn into_inner(self) -> String {
582        self.0
583    }
584}
585
586impl TryFrom<String> for CanonicalDid {
587    type Error = DidParseError;
588    fn try_from(s: String) -> Result<Self, Self::Error> {
589        Self::parse(&s)
590    }
591}
592
593impl TryFrom<&str> for CanonicalDid {
594    type Error = DidParseError;
595    fn try_from(s: &str) -> Result<Self, Self::Error> {
596        Self::parse(s)
597    }
598}
599
600impl From<CanonicalDid> for String {
601    fn from(d: CanonicalDid) -> Self {
602        d.0
603    }
604}
605
606impl fmt::Display for CanonicalDid {
607    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
608        f.write_str(&self.0)
609    }
610}
611
612impl Deref for CanonicalDid {
613    type Target = str;
614    fn deref(&self) -> &str {
615        &self.0
616    }
617}
618
619impl AsRef<str> for CanonicalDid {
620    fn as_ref(&self) -> &str {
621        &self.0
622    }
623}
624
625impl Borrow<str> for CanonicalDid {
626    fn borrow(&self) -> &str {
627        &self.0
628    }
629}
630
631impl<'de> serde::Deserialize<'de> for CanonicalDid {
632    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
633    where
634        D: serde::Deserializer<'de>,
635    {
636        let s = String::deserialize(deserializer)?;
637        Self::parse(&s).map_err(serde::de::Error::custom)
638    }
639}
640
641impl PartialEq<str> for CanonicalDid {
642    fn eq(&self, other: &str) -> bool {
643        self.0 == other
644    }
645}
646
647impl PartialEq<&str> for CanonicalDid {
648    fn eq(&self, other: &&str) -> bool {
649        self.0 == *other
650    }
651}
652
653impl From<IdentityDID> for CanonicalDid {
654    fn from(did: IdentityDID) -> Self {
655        Self(did.into_inner())
656    }
657}
658
659impl From<DeviceDID> for CanonicalDid {
660    fn from(did: DeviceDID) -> Self {
661        Self(did.0)
662    }
663}
664
665// ============================================================================
666// AssuranceLevel Type
667// ============================================================================
668
669/// Cryptographic assurance level of a platform identity claim.
670///
671/// Variants are ordered from weakest to strongest so that `Ord` comparisons
672/// reflect trust strength: `SelfAsserted < TokenVerified < Authenticated < Sovereign`.
673///
674/// Usage:
675/// ```rust
676/// # use auths_verifier::types::AssuranceLevel;
677/// assert!(AssuranceLevel::Sovereign > AssuranceLevel::Authenticated);
678/// assert!(AssuranceLevel::SelfAsserted < AssuranceLevel::TokenVerified);
679/// ```
680#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
681#[serde(rename_all = "snake_case")]
682#[non_exhaustive]
683pub enum AssuranceLevel {
684    /// Self-reported identity, signed only by the claimant's own key (e.g., PyPI).
685    SelfAsserted,
686    /// Bearer token validated against a platform API at time of claim (e.g., npm).
687    TokenVerified,
688    /// OAuth/OIDC challenge-response proving account control (e.g., GitHub).
689    Authenticated,
690    /// End-to-end cryptographic identity chain with no third-party trust (auths native).
691    Sovereign,
692}
693
694/// Error returned when parsing an `AssuranceLevel` from a string fails.
695#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
696#[error(
697    "invalid assurance level '{0}': expected one of: sovereign, authenticated, token_verified, self_asserted"
698)]
699pub struct AssuranceLevelParseError(pub String);
700
701impl AssuranceLevel {
702    /// Human-readable label for display.
703    pub fn label(&self) -> &'static str {
704        match self {
705            Self::SelfAsserted => "Self-Asserted",
706            Self::TokenVerified => "Token-Verified",
707            Self::Authenticated => "Authenticated",
708            Self::Sovereign => "Sovereign",
709        }
710    }
711
712    /// Numeric score (1–4) for the assurance level.
713    pub fn score(&self) -> u8 {
714        match self {
715            Self::SelfAsserted => 1,
716            Self::TokenVerified => 2,
717            Self::Authenticated => 3,
718            Self::Sovereign => 4,
719        }
720    }
721}
722
723impl fmt::Display for AssuranceLevel {
724    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
725        f.write_str(self.label())
726    }
727}
728
729impl FromStr for AssuranceLevel {
730    type Err = AssuranceLevelParseError;
731
732    fn from_str(s: &str) -> Result<Self, Self::Err> {
733        match s.trim().to_lowercase().as_str() {
734            "sovereign" => Ok(Self::Sovereign),
735            "authenticated" => Ok(Self::Authenticated),
736            "token_verified" => Ok(Self::TokenVerified),
737            "self_asserted" => Ok(Self::SelfAsserted),
738            _ => Err(AssuranceLevelParseError(s.to_string())),
739        }
740    }
741}
742
743#[cfg(test)]
744mod tests {
745    use super::*;
746    use crate::keri::Said;
747
748    #[test]
749    fn report_without_witness_quorum_deserializes() {
750        // JSON from before witness_quorum field existed
751        let json = r#"{
752            "status": {"type": "Valid"},
753            "chain": [],
754            "warnings": []
755        }"#;
756        let report: VerificationReport = serde_json::from_str(json).unwrap();
757        assert!(report.is_valid());
758        assert!(report.witness_quorum.is_none());
759    }
760
761    #[test]
762    fn insufficient_witnesses_serializes_correctly() {
763        let status = VerificationStatus::InsufficientWitnesses {
764            required: 3,
765            verified: 1,
766        };
767        let json = serde_json::to_string(&status).unwrap();
768        let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
769        assert_eq!(parsed["type"], "InsufficientWitnesses");
770        assert_eq!(parsed["required"], 3);
771        assert_eq!(parsed["verified"], 1);
772
773        // Roundtrip
774        let roundtripped: VerificationStatus = serde_json::from_str(&json).unwrap();
775        assert_eq!(roundtripped, status);
776    }
777
778    #[test]
779    fn report_with_witness_quorum_roundtrips() {
780        use crate::witness::{WitnessQuorum, WitnessReceiptResult};
781
782        let report = VerificationReport {
783            status: VerificationStatus::Valid,
784            chain: vec![],
785            warnings: vec![],
786            witness_quorum: Some(WitnessQuorum {
787                required: 2,
788                verified: 2,
789                receipts: vec![
790                    WitnessReceiptResult {
791                        witness_id: "did:key:w1".into(),
792                        receipt_said: Said::new_unchecked("EReceipt1".into()),
793                        verified: true,
794                    },
795                    WitnessReceiptResult {
796                        witness_id: "did:key:w2".into(),
797                        receipt_said: Said::new_unchecked("EReceipt2".into()),
798                        verified: true,
799                    },
800                ],
801            }),
802        };
803
804        let json = serde_json::to_string(&report).unwrap();
805        let parsed: VerificationReport = serde_json::from_str(&json).unwrap();
806        assert_eq!(report, parsed);
807        assert!(parsed.witness_quorum.is_some());
808        assert_eq!(parsed.witness_quorum.unwrap().verified, 2);
809    }
810
811    #[test]
812    fn report_without_witness_quorum_skips_in_json() {
813        let report = VerificationReport::valid(vec![]);
814        let json = serde_json::to_string(&report).unwrap();
815        // witness_quorum should be omitted from JSON when None
816        assert!(!json.contains("witness_quorum"));
817    }
818
819    // ── AssuranceLevel Tests ──────────────────────────────────────────
820
821    #[test]
822    fn assurance_level_ordering() {
823        assert!(AssuranceLevel::SelfAsserted < AssuranceLevel::TokenVerified);
824        assert!(AssuranceLevel::TokenVerified < AssuranceLevel::Authenticated);
825        assert!(AssuranceLevel::Authenticated < AssuranceLevel::Sovereign);
826    }
827
828    #[test]
829    fn assurance_level_serde_roundtrip() {
830        let variants = [
831            AssuranceLevel::SelfAsserted,
832            AssuranceLevel::TokenVerified,
833            AssuranceLevel::Authenticated,
834            AssuranceLevel::Sovereign,
835        ];
836        for level in variants {
837            let json = serde_json::to_string(&level).unwrap();
838            let parsed: AssuranceLevel = serde_json::from_str(&json).unwrap();
839            assert_eq!(parsed, level);
840        }
841    }
842
843    #[test]
844    fn assurance_level_serde_snake_case() {
845        assert_eq!(
846            serde_json::to_string(&AssuranceLevel::SelfAsserted).unwrap(),
847            "\"self_asserted\""
848        );
849        assert_eq!(
850            serde_json::to_string(&AssuranceLevel::TokenVerified).unwrap(),
851            "\"token_verified\""
852        );
853        assert_eq!(
854            serde_json::to_string(&AssuranceLevel::Authenticated).unwrap(),
855            "\"authenticated\""
856        );
857        assert_eq!(
858            serde_json::to_string(&AssuranceLevel::Sovereign).unwrap(),
859            "\"sovereign\""
860        );
861    }
862
863    #[test]
864    fn assurance_level_from_str() {
865        assert_eq!(
866            "sovereign".parse::<AssuranceLevel>().unwrap(),
867            AssuranceLevel::Sovereign
868        );
869        assert_eq!(
870            "authenticated".parse::<AssuranceLevel>().unwrap(),
871            AssuranceLevel::Authenticated
872        );
873        assert_eq!(
874            "token_verified".parse::<AssuranceLevel>().unwrap(),
875            AssuranceLevel::TokenVerified
876        );
877        assert_eq!(
878            "self_asserted".parse::<AssuranceLevel>().unwrap(),
879            AssuranceLevel::SelfAsserted
880        );
881        assert!("invalid".parse::<AssuranceLevel>().is_err());
882    }
883
884    #[test]
885    fn assurance_level_from_str_case_insensitive() {
886        assert_eq!(
887            "SOVEREIGN".parse::<AssuranceLevel>().unwrap(),
888            AssuranceLevel::Sovereign
889        );
890        assert_eq!(
891            "Authenticated".parse::<AssuranceLevel>().unwrap(),
892            AssuranceLevel::Authenticated
893        );
894    }
895
896    #[test]
897    fn assurance_level_score() {
898        assert_eq!(AssuranceLevel::SelfAsserted.score(), 1);
899        assert_eq!(AssuranceLevel::TokenVerified.score(), 2);
900        assert_eq!(AssuranceLevel::Authenticated.score(), 3);
901        assert_eq!(AssuranceLevel::Sovereign.score(), 4);
902    }
903
904    #[test]
905    fn assurance_level_display() {
906        assert_eq!(AssuranceLevel::SelfAsserted.to_string(), "Self-Asserted");
907        assert_eq!(AssuranceLevel::TokenVerified.to_string(), "Token-Verified");
908        assert_eq!(AssuranceLevel::Authenticated.to_string(), "Authenticated");
909        assert_eq!(AssuranceLevel::Sovereign.to_string(), "Sovereign");
910    }
911}