Skip to main content

dyolo_kya/
error.rs

1use thiserror::Error;
2
3#[derive(Debug, Clone, PartialEq, Eq)]
4pub enum StorageErrorKind {
5    Transient,
6    Permanent,
7}
8
9#[derive(Debug, Clone)]
10pub struct KyaStorageError {
11    pub kind: StorageErrorKind,
12    pub message: String,
13}
14
15impl PartialEq for KyaStorageError {
16    fn eq(&self, other: &Self) -> bool {
17        self.kind == other.kind
18    }
19}
20
21impl Eq for KyaStorageError {}
22
23impl KyaStorageError {
24    pub fn transient(msg: impl Into<String>) -> Self {
25        Self {
26            kind: StorageErrorKind::Transient,
27            message: msg.into(),
28        }
29    }
30
31    pub fn permanent(msg: impl Into<String>) -> Self {
32        Self {
33            kind: StorageErrorKind::Permanent,
34            message: msg.into(),
35        }
36    }
37
38    pub fn is_transient(&self) -> bool {
39        self.kind == StorageErrorKind::Transient
40    }
41}
42
43impl std::fmt::Display for KyaStorageError {
44    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
45        let label = match self.kind {
46            StorageErrorKind::Transient => "transient",
47            StorageErrorKind::Permanent => "permanent",
48        };
49        write!(f, "{label} storage error: {}", self.message)
50    }
51}
52
53impl std::error::Error for KyaStorageError {}
54
55#[derive(Debug, Error, PartialEq, Eq)]
56#[non_exhaustive]
57pub enum KyaError {
58    #[error("delegation chain is empty")]
59    EmptyChain,
60
61    #[error("storage backend failure: {0}")]
62    StorageFailure(KyaStorageError),
63
64    #[error("chain does not anchor to the declared principal")]
65    RootMismatch,
66
67    #[error("delegation linkage broken at hop {0}")]
68    BrokenLinkage(usize),
69
70    #[error("invalid signature at hop {0}")]
71    InvalidSignature(usize),
72
73    #[error("delegation at hop {0} not yet valid (issued_at={1}, now={2})")]
74    NotYetValid(usize, u64, u64),
75
76    #[error("delegation at hop {0} has expired (expiry={1}, now={2})")]
77    Expired(usize, u64, u64),
78
79    #[error("temporal violation at hop {0}: child expiry {1} exceeds parent expiry {2}")]
80    TemporalViolation(usize, u64, u64),
81
82    #[error("depth limit exceeded at hop {0} (limit={1})")]
83    MaxDepthExceeded(usize, u8),
84
85    #[error("sub-scope proof is structurally invalid")]
86    InvalidSubScopeProof,
87
88    #[error(
89        "scope escalation at hop {0}: delegated scope is not within the delegator's authorization"
90    )]
91    ScopeEscalation(usize),
92
93    #[error("executing agent is not the terminal delegate")]
94    UnauthorizedLeaf,
95
96    #[error("execution intent is not within the terminal scope")]
97    ScopeViolation,
98
99    #[error("nonce has already been consumed")]
100    NonceReplay,
101
102    #[error("delegation certificate has been revoked")]
103    Revoked,
104
105    #[error("intent is not present in this tree")]
106    IntentNotFound,
107
108    #[error("intent tree requires at least one intent")]
109    EmptyTree,
110
111    #[error("wire format error: {0}")]
112    WireFormatError(String),
113
114    #[error("unsupported certificate version: expected {expected}, got {got}")]
115    UnsupportedVersion { expected: u8, got: u8 },
116
117    #[error("policy violation: {0}")]
118    PolicyViolation(String),
119
120    #[error("batch authorization failed at index {index}: {reason}")]
121    BatchItemFailed { index: usize, reason: String },
122
123    #[error("MAC verification failed")]
124    MacVerificationFailed,
125
126    #[error("namespace mismatch: chain namespace is '{chain}', authorization requested for '{requested}'")]
127    NamespaceMismatch { chain: String, requested: String },
128
129    #[error("rate limit exceeded for key")]
130    RateLimitExceeded,
131
132    #[error("storage health check failed: {0}")]
133    StorageUnhealthy(String),
134}
135
136impl KyaError {
137    pub fn as_storage_error(&self) -> Option<&KyaStorageError> {
138        if let Self::StorageFailure(e) = self {
139            Some(e)
140        } else {
141            None
142        }
143    }
144
145    pub fn is_transient_storage_failure(&self) -> bool {
146        self.as_storage_error().is_some_and(|e| e.is_transient())
147    }
148
149    pub fn error_code(&self) -> &'static str {
150        match self {
151            Self::Expired(..) => "CERT_EXPIRED",
152            Self::Revoked => "CERT_REVOKED",
153            Self::NonceReplay => "NONCE_REPLAY",
154            Self::ScopeViolation => "SCOPE_VIOLATION",
155            Self::ScopeEscalation(_) => "SCOPE_ESCALATION",
156            Self::InvalidSignature(_) => "INVALID_SIGNATURE",
157            Self::BrokenLinkage(_) => "CHAIN_BROKEN_LINKAGE",
158            Self::MaxDepthExceeded(..) => "CHAIN_DEPTH_EXCEEDED",
159            Self::PolicyViolation(_) => "POLICY_VIOLATION",
160            Self::StorageFailure(_) => "STORAGE_ERROR",
161            Self::BatchItemFailed { .. } => "BATCH_ITEM_FAILED",
162            Self::MacVerificationFailed => "MAC_VERIFICATION_FAILED",
163            Self::NamespaceMismatch { .. } => "NAMESPACE_MISMATCH",
164            Self::RateLimitExceeded => "RATE_LIMIT_EXCEEDED",
165            Self::StorageUnhealthy(_) => "STORAGE_UNHEALTHY",
166            _ => "AUTHORIZATION_FAILED",
167        }
168    }
169
170    pub fn http_status(&self) -> u16 {
171        match self {
172            Self::StorageFailure(e) if e.is_transient() => 503,
173            Self::StorageFailure(_) => 500,
174            Self::StorageUnhealthy(_) => 503,
175            Self::RateLimitExceeded => 429,
176            Self::EmptyChain | Self::WireFormatError(_) | Self::UnsupportedVersion { .. } => 400,
177            Self::Revoked | Self::Expired(..) | Self::NotYetValid(..) | Self::NonceReplay => 401,
178            Self::ScopeViolation | Self::ScopeEscalation(_) | Self::UnauthorizedLeaf => 403,
179            Self::InvalidSignature(_) | Self::RootMismatch | Self::BrokenLinkage(_) => 403,
180            Self::PolicyViolation(_) | Self::NamespaceMismatch { .. } => 403,
181            Self::MacVerificationFailed => 401,
182            _ => 403,
183        }
184    }
185}