Skip to main content

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 {}