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}