Skip to main content

auths_verifier/
types.rs

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