Skip to main content

codlet_core/
error.rs

1//! Error types for codlet-core.
2//!
3//! This is the internal error layer (RFC-012/021): structured, useful for
4//! developers and operators, and safe to log because no variant carries a
5//! plaintext secret. The public, enumeration-resistant error layer
6//! (`PublicAuthFailure`) is introduced with the redemption flow (RFC-012) once
7//! the store traits exist.
8
9use thiserror::Error;
10
11/// Randomness could not be obtained. Generation fails closed on this error;
12/// codlet never substitutes a deterministic value (INV-3, SR-29-adjacent).
13#[derive(Debug, Error, PartialEq, Eq)]
14#[error("secure randomness unavailable")]
15pub struct RandomError;
16
17/// A key provider could not supply usable key material.
18///
19/// Carries no key bytes. Missing material is fatal to the operation; there is
20/// no fallback key (INV-2, SR-29).
21#[derive(Debug, Error, PartialEq, Eq)]
22pub enum KeyError {
23    /// No active key is configured.
24    #[error("no active HMAC key configured")]
25    MissingActiveKey,
26    /// The requested historical key version is not available. Validation fails
27    /// closed for that candidate rather than falling back.
28    #[error("HMAC key version not available")]
29    MissingKeyVersion,
30    /// Key material was present but unusable (e.g. empty).
31    #[error("HMAC key material is invalid")]
32    InvalidKeyMaterial,
33}
34
35/// A [`crate::code::CodePolicy`] was constructed with an impossible or unsafe
36/// shape (RFC-003 §11.1).
37#[derive(Debug, Error, PartialEq, Eq)]
38pub enum PolicyError {
39    /// Alphabet has fewer than two distinct symbols.
40    #[error("alphabet must contain at least 2 symbols")]
41    AlphabetTooSmall,
42    /// Alphabet contains a duplicate symbol, which would bias generation.
43    #[error("alphabet contains duplicate symbols")]
44    AlphabetNotUnique,
45    /// Alphabet contains a non-ASCII or otherwise unsupported byte.
46    #[error("alphabet contains an unsupported (non-ASCII) symbol")]
47    AlphabetNotAscii,
48    /// Requested code length is below the secure minimum and no explicit
49    /// short-code opt-in was used.
50    #[error("code length {got} is below the secure minimum of {min}")]
51    LengthBelowMinimum {
52        /// Requested length.
53        got: usize,
54        /// Enforced minimum.
55        min: usize,
56    },
57    /// Requested code length is zero.
58    #[error("code length must be non-zero")]
59    ZeroLength,
60}
61
62/// Rejection of user-supplied code input during validation (RFC-003 FR-2).
63///
64/// All variants map to the same generic public message; the distinction here
65/// exists only for internal diagnostics and metrics, never for user display
66/// (INV-8).
67#[derive(Debug, Error, PartialEq, Eq)]
68pub enum CodeInputError {
69    /// Input was empty after trimming.
70    #[error("code input is empty")]
71    Empty,
72    /// Raw input exceeded the maximum accepted length before normalization.
73    #[error("code input exceeds maximum raw length")]
74    TooLongRaw,
75    /// Normalized input length does not match the configured code length.
76    #[error("normalized code length does not match policy")]
77    WrongLength,
78    /// Normalized input contains a character outside the accepted set.
79    #[error("code input contains unsupported characters")]
80    UnsupportedCharacters,
81}
82
83// ── RFC-012/021: two-layer error model ──────────────────────────────────────
84
85/// Internal reason a code redemption failed. Rich enough for logs and metrics;
86/// never shown to the user (INV-8, RFC-012 §10.1).
87#[derive(Debug, Clone, PartialEq, Eq)]
88pub enum RedemptionFailReason {
89    /// Code input was malformed (too long, wrong length, unsupported chars).
90    InvalidFormat,
91    /// No redeemable record matched the lookup key(s).
92    NotFound,
93    /// A matching record exists but `expires_at` has passed.
94    Expired,
95    /// A matching record exists but it was explicitly revoked.
96    Revoked,
97    /// A matching record exists but was already claimed.
98    AlreadyUsed,
99    /// The rate-limit threshold was exceeded before the lookup.
100    RateLimited,
101    /// The store could not be reached; the operation was not attempted.
102    StoreUnavailable,
103    /// Key material was unavailable or invalid.
104    KeyFailure,
105}
106
107/// Public-safe redemption failure (RFC-012 §4, RFC-021).
108///
109/// All enumeration-sensitive reasons (`NotFound`, `Expired`, `Revoked`,
110/// `AlreadyUsed`, `InvalidFormat`) collapse to `InvalidOrExpired`. The caller
111/// must not expose the internal [`RedemptionFailReason`] to end users.
112#[derive(Debug, Error, Clone, PartialEq, Eq)]
113pub enum PublicRedemptionError {
114    /// The code was not accepted. Reason intentionally omitted.
115    #[error("invalid or expired code")]
116    InvalidOrExpired,
117    /// The caller has exceeded the rate limit. Safe to surface as a throttle
118    /// hint (does not reveal code existence).
119    #[error("too many attempts — please wait and try again")]
120    RateLimited,
121    /// A transient problem prevented the check. The code was not consumed.
122    #[error("service temporarily unavailable")]
123    TemporarilyUnavailable,
124}
125
126impl PublicRedemptionError {
127    /// Map an internal reason to its public-safe equivalent (RFC-012 §4).
128    #[must_use]
129    pub fn from_reason(reason: &RedemptionFailReason) -> Self {
130        match reason {
131            RedemptionFailReason::InvalidFormat
132            | RedemptionFailReason::NotFound
133            | RedemptionFailReason::Expired
134            | RedemptionFailReason::Revoked
135            | RedemptionFailReason::AlreadyUsed => Self::InvalidOrExpired,
136            RedemptionFailReason::RateLimited => Self::RateLimited,
137            RedemptionFailReason::StoreUnavailable | RedemptionFailReason::KeyFailure => {
138                Self::TemporarilyUnavailable
139            }
140        }
141    }
142}
143
144/// Public-safe form-token / CSRF failure (RFC-012, RFC-021).
145#[derive(Debug, Error, Clone, PartialEq, Eq)]
146pub enum PublicFormError {
147    /// The form could not be submitted. The token was missing, expired, or
148    /// already consumed. No distinction is made between these states.
149    #[error("form expired or invalid — please reload the page and try again")]
150    ExpiredOrInvalid,
151    /// A transient problem prevented the check.
152    #[error("service temporarily unavailable")]
153    TemporarilyUnavailable,
154}
155
156/// Public-safe session failure (RFC-012, RFC-021).
157#[derive(Debug, Error, Clone, PartialEq, Eq)]
158pub enum PublicSessionError {
159    /// No valid session — missing cookie, expired, or revoked. No distinction.
160    #[error("session missing or expired — please sign in again")]
161    MissingOrExpired,
162    /// A transient problem prevented the check.
163    #[error("service temporarily unavailable")]
164    TemporarilyUnavailable,
165}