Skip to main content

auths_sdk/domains/credentials/
error.rs

1use auths_core::error::AuthsErrorInfo;
2use thiserror::Error;
3
4/// Errors from credential issuance, revocation, listing, and verification.
5///
6/// A credential is an ACDC anchored to the issuer's KEL via a backerless TEL
7/// (`vcp`/`iss`/`rev`). These errors mirror the SDK delegation surfaces:
8/// `thiserror` only, no `anyhow`, with an [`AuthsErrorInfo`] code + suggestion.
9///
10/// Usage:
11/// ```ignore
12/// match credentials::issue(&ctx, &issuer, issuee, &caps, role, None) {
13///     Err(CredentialError::IssueeNotFound { did }) => { /* issuee has no KEL */ }
14///     Err(e) => return Err(e.into()),
15///     Ok(result) => { /* credential SAID */ }
16/// }
17/// ```
18#[derive(Debug, Error)]
19#[non_exhaustive]
20pub enum CredentialError {
21    /// The issuee (subject/holder) has no KEL — an issuee must be incepted before a
22    /// credential can be issued against it (hard fail, never lazily created).
23    #[error("issuee identity not found (no KEL): {did}")]
24    IssueeNotFound {
25        /// The issuee `did:keri:` that has no resolvable KEL.
26        did: String,
27    },
28
29    /// The lazy registry (`vcp`) inception or a TEL anchor (`iss`/`rev`) failed.
30    #[error("credential registry error: {0}")]
31    RegistryError(#[source] auths_id::keri::credential_registry::CredentialRegistryError),
32
33    /// The credential is already revoked — revocation is idempotent, so this is only
34    /// surfaced when a caller asks to distinguish "already revoked" from a fresh `rev`.
35    #[error("credential already revoked: {said}")]
36    AlreadyRevoked {
37        /// The credential SAID that was already revoked.
38        said: String,
39    },
40
41    /// The issuer's KEL is `kt≥2` — single-signature credential anchoring only.
42    #[error("issuer is multi-signature (kt≥2); credential anchoring is single-author only")]
43    KtThresholdUnsupported,
44
45    /// The pinned capability schema SAID could not be computed (build-time invariant).
46    #[error("capability schema unknown or uncomputable")]
47    SchemaUnknown,
48
49    /// A requested capability targets the reserved quantitative usage resource
50    /// (`calls:`) but its bound does not parse to a valid non-negative call count —
51    /// e.g. `calls:`, `calls:abc`, `calls:-1`. Refused at issuance: a quantitative
52    /// cap that does not parse must never be minted as an opaque, uncapped string
53    /// (a budget that would silently never fire).
54    #[error(
55        "malformed quantitative usage cap '{cap}': a calls: capability must carry a non-negative integer bound (e.g. calls:3)"
56    )]
57    MalformedUsageCap {
58        /// The malformed capability string as requested by the issuer.
59        cap: String,
60    },
61
62    /// Verification could not reach a fresh-enough witnessed tip to judge the
63    /// credential's status — fail-closed (the resolution layer, F.4, owns this).
64    #[error("credential status is stale or unresolvable: {reason}")]
65    StaleOrUnresolvable {
66        /// Why no fresh witnessed tip was reachable.
67        reason: String,
68    },
69
70    /// A cryptographic operation failed (issuer-sign, curve resolution).
71    #[error("crypto error: {0}")]
72    CryptoError(#[source] auths_core::AgentError),
73}
74
75impl From<auths_id::keri::credential_registry::CredentialRegistryError> for CredentialError {
76    fn from(err: auths_id::keri::credential_registry::CredentialRegistryError) -> Self {
77        use auths_id::keri::credential_registry::CredentialRegistryError as RegErr;
78        match err {
79            RegErr::ThresholdUnsupported { .. } => CredentialError::KtThresholdUnsupported,
80            other => CredentialError::RegistryError(other),
81        }
82    }
83}
84
85impl From<auths_core::AgentError> for CredentialError {
86    fn from(err: auths_core::AgentError) -> Self {
87        CredentialError::CryptoError(err)
88    }
89}
90
91impl AuthsErrorInfo for CredentialError {
92    fn error_code(&self) -> &'static str {
93        match self {
94            Self::IssueeNotFound { .. } => "AUTHS-E6101",
95            Self::RegistryError(_) => "AUTHS-E6102",
96            Self::AlreadyRevoked { .. } => "AUTHS-E6103",
97            Self::KtThresholdUnsupported => "AUTHS-E6104",
98            Self::SchemaUnknown => "AUTHS-E6105",
99            Self::StaleOrUnresolvable { .. } => "AUTHS-E6106",
100            Self::MalformedUsageCap { .. } => "AUTHS-E6107",
101            Self::CryptoError(e) => e.error_code(),
102        }
103    }
104
105    fn suggestion(&self) -> Option<&'static str> {
106        match self {
107            Self::IssueeNotFound { .. } => Some(
108                "The issuee must have an incepted identity (KEL) before it can be credentialed",
109            ),
110            Self::RegistryError(_) => {
111                Some("Check the issuer identity and registry storage are reachable")
112            }
113            Self::AlreadyRevoked { .. } => {
114                Some("This credential is already revoked; no further action is needed")
115            }
116            Self::KtThresholdUnsupported => {
117                Some("Credential issuance currently requires a single-signature (kt=1) issuer")
118            }
119            Self::SchemaUnknown => {
120                Some("The compiled-in capability schema is unavailable; this is a build defect")
121            }
122            Self::MalformedUsageCap { .. } => {
123                Some("Use a quantitative cap with a non-negative integer bound, e.g. --cap calls:3")
124            }
125            Self::StaleOrUnresolvable { .. } => Some(
126                "No fresh witnessed tip was reachable; sync the issuer KEL/receipts and retry, or relax --require-witnesses",
127            ),
128            Self::CryptoError(e) => e.suggestion(),
129        }
130    }
131}