codlet_core/auth/error.rs
1//! Typed errors and outcomes for the orchestration layer (RFC-013).
2//!
3//! Every manager operation returns a structured result that carries both the
4//! internal cause (for logs and metrics) and its public-safe mapping. Callers
5//! must not expose the internal cause to end users (INV-8).
6
7use crate::error::{
8 PublicFormError, PublicRedemptionError, PublicSessionError, RedemptionFailReason,
9};
10use crate::secret::{SessionId, SubjectId};
11use crate::state::ClaimOutcome;
12use crate::store::error::StoreError;
13
14// ── Code redemption ──────────────────────────────────────────────────────────
15
16/// Why a code redemption flow failed (RFC-013 §10.3, RFC-012).
17///
18/// Carries the internal reason alongside the public-safe error so callers can
19/// log the internal cause without showing it to users.
20#[derive(Debug)]
21pub enum RedeemError {
22 /// Input validation or normalization failed before any store access.
23 InvalidInput {
24 /// Internal reason (log, do not display).
25 reason: RedemptionFailReason,
26 /// Public-safe mapping.
27 public: PublicRedemptionError,
28 },
29 /// Rate-limit threshold exceeded before lookup.
30 RateLimited {
31 /// Public-safe mapping.
32 public: PublicRedemptionError,
33 },
34 /// No redeemable record found, or the record was expired/used/revoked.
35 NotRedeemable {
36 /// Internal reason (log, do not display).
37 reason: RedemptionFailReason,
38 /// Public-safe mapping (always `InvalidOrExpired`).
39 public: PublicRedemptionError,
40 },
41 /// The atomic claim was lost to a concurrent caller.
42 ClaimLost {
43 /// Public-safe mapping.
44 public: PublicRedemptionError,
45 },
46 /// A transient store or key failure prevented the operation.
47 Internal {
48 /// Internal diagnostic (log, do not display).
49 cause: String,
50 /// Public-safe mapping.
51 public: PublicRedemptionError,
52 },
53}
54
55impl RedeemError {
56 /// The public-safe error to return to callers / map to HTTP responses.
57 #[must_use]
58 pub fn public(&self) -> &PublicRedemptionError {
59 match self {
60 Self::InvalidInput { public, .. }
61 | Self::NotRedeemable { public, .. }
62 | Self::ClaimLost { public }
63 | Self::Internal { public, .. }
64 | Self::RateLimited { public } => public,
65 }
66 }
67
68 /// Convenience: construct from a [`StoreError`].
69 pub(crate) fn from_store(e: StoreError) -> Self {
70 Self::Internal {
71 cause: format!("{e}"),
72 public: PublicRedemptionError::TemporarilyUnavailable,
73 }
74 }
75
76 /// Convenience: construct from a key / hashing error.
77 pub(crate) fn from_key(e: crate::error::KeyError) -> Self {
78 Self::Internal {
79 cause: format!("key error: {e}"),
80 public: PublicRedemptionError::TemporarilyUnavailable,
81 }
82 }
83}
84
85impl std::fmt::Display for RedeemError {
86 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
87 // Display the public-safe message only. The internal cause stays in Debug.
88 write!(f, "{}", self.public())
89 }
90}
91
92impl std::error::Error for RedeemError {}
93
94/// A successfully completed code redemption.
95///
96/// The `claim` proof certifies that exactly one concurrent caller won the
97/// atomic race. The caller must not issue a session or perform host-side
98/// effects without this proof (RFC-013 §5, RFC-005 §14.5).
99#[derive(Debug)]
100pub struct RedeemSuccess {
101 /// The authenticated subject returned by the host callback (or passed
102 /// directly in the two-step flow).
103 pub subject: SubjectId,
104 /// Opaque grant payload from the code record. Passed to the host callback;
105 /// not interpreted by codlet.
106 pub grant: Option<String>,
107 /// Proof that `claim_code` returned `Won`. Structurally prevents issuing
108 /// a session without going through the claim path.
109 pub(crate) _claim_proof: ClaimProof,
110}
111
112/// Zero-size proof token that `claim_code` returned [`ClaimOutcome::Won`].
113/// Not constructible outside this module; prevents session issuance without
114/// a confirmed claim.
115#[derive(Debug)]
116pub(crate) struct ClaimProof(());
117
118impl ClaimProof {
119 pub(crate) fn new(outcome: ClaimOutcome) -> Option<Self> {
120 match outcome {
121 ClaimOutcome::Won => Some(Self(())),
122 ClaimOutcome::Lost => None,
123 }
124 }
125}
126
127// ── Session ──────────────────────────────────────────────────────────────────
128
129/// Why a session operation failed.
130#[derive(Debug)]
131pub enum SessionError {
132 /// No valid session matched the bearer credential.
133 NotFound {
134 /// Public-safe mapping.
135 public: PublicSessionError,
136 },
137 /// Transient store or key failure.
138 Internal {
139 /// Internal diagnostic (log, do not display).
140 cause: String,
141 /// Public-safe mapping.
142 public: PublicSessionError,
143 },
144}
145
146impl SessionError {
147 /// The public-safe error.
148 #[must_use]
149 pub fn public(&self) -> &PublicSessionError {
150 match self {
151 Self::NotFound { public } | Self::Internal { public, .. } => public,
152 }
153 }
154
155 pub(crate) fn from_store(e: StoreError) -> Self {
156 Self::Internal {
157 cause: format!("{e}"),
158 public: PublicSessionError::TemporarilyUnavailable,
159 }
160 }
161
162 pub(crate) fn from_key(e: crate::error::KeyError) -> Self {
163 Self::Internal {
164 cause: format!("key error: {e}"),
165 public: PublicSessionError::TemporarilyUnavailable,
166 }
167 }
168}
169
170impl std::fmt::Display for SessionError {
171 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
172 write!(f, "{}", self.public())
173 }
174}
175
176impl std::error::Error for SessionError {}
177
178/// A successfully issued session.
179#[derive(Debug)]
180pub struct IssuedSession {
181 /// The opaque session record identifier. Not a bearer credential.
182 pub session_id: SessionId,
183 /// The `Set-Cookie` header value to send to the client. Contains the
184 /// plaintext bearer secret; must not be logged.
185 pub set_cookie: String,
186}
187
188// ── Form token ───────────────────────────────────────────────────────────────
189
190/// Why a form-token operation failed.
191#[derive(Debug)]
192pub enum FormTokenError {
193 /// Token invalid, expired, or binding mismatch.
194 Invalid {
195 /// Public-safe mapping.
196 public: PublicFormError,
197 },
198 /// Transient store or key failure.
199 Internal {
200 /// Internal diagnostic (log, do not display).
201 cause: String,
202 /// Public-safe mapping.
203 public: PublicFormError,
204 },
205}
206
207impl FormTokenError {
208 /// The public-safe error.
209 #[must_use]
210 pub fn public(&self) -> &PublicFormError {
211 match self {
212 Self::Invalid { public } | Self::Internal { public, .. } => public,
213 }
214 }
215
216 pub(crate) fn from_store(e: StoreError) -> Self {
217 Self::Internal {
218 cause: format!("{e}"),
219 public: PublicFormError::TemporarilyUnavailable,
220 }
221 }
222
223 pub(crate) fn from_key(e: crate::error::KeyError) -> Self {
224 Self::Internal {
225 cause: format!("key error: {e}"),
226 public: PublicFormError::TemporarilyUnavailable,
227 }
228 }
229}
230
231impl std::fmt::Display for FormTokenError {
232 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
233 write!(f, "{}", self.public())
234 }
235}
236
237impl std::error::Error for FormTokenError {}