captcha-sdk 0.0.1

Unified server-side CAPTCHA verification for Rust
Documentation
# Error taxonomy

> **TARGET architecture — implementation pending.** This document is the
> authoritative target error contract. The signatures are design direction,
> not the current scaffold ABI.

## Root error

Normal facade methods expose one root with two recovery categories:

```rust,ignore
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum CaptchaError {
    #[error(transparent)]
    Configuration(#[from] ConfigurationError),

    #[error(transparent)]
    Verification(#[from] VerificationError),
}
```

The variant is `Verification`, never `VerificationError`. A new root variant is
justified only when callers need a materially different recovery or action
category—not merely because a provider returned a new code. There is no
`CaptchaError::ProviderRejected`: a provider-declared client-token failure is
`Ok(ProviderVerdict::Rejected { .. })`.

`CaptchaBuilder::build`, `Captcha::verify`, `Captcha::assess`, and
`ProviderAdapter::verify` return `Result<_, CaptchaError>`. Narrow standalone
constructors and parsers may retain their narrow error type when callers are
already operating below the facade boundary.

## Configuration errors

`ConfigurationError` means verification cannot be trusted until configuration,
credentials, account state, or policy changes:

```rust,ignore
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum ConfigurationError {
    #[error("invalid provider configuration for {provider}")]
    InvalidProviderConfiguration { provider: ProviderId },

    #[error("provider credentials were rejected for {provider}")]
    ProviderCredentialsRejected { provider: ProviderId },

    #[error("invalid verification policy")]
    InvalidPolicy,

    #[error("{provider} does not support required capability {capability:?}")]
    UnsupportedCapability {
        provider: ProviderId,
        capability: Capability,
    },
}
```

Credentials, sitekey, account status, or equivalent provider codes belong here
even when they can only be discovered during a remote `verify` call. Discovery
time does not turn a configuration problem into a transient verification
failure. Implementations may add safe typed sources to variants when they
preserve this category and the redaction rules below.

## Verification errors

`VerificationError` means this attempt did not obtain a trustworthy provider
verdict:

```rust,ignore
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum TokenState {
    NotSent,
    PossiblyConsumed,
}

#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum VerificationError {
    #[error("transport failure while contacting {provider}")]
    Transport {
        provider: ProviderId,
        #[source]
        source: TransportError,
        token_state: TokenState,
    },

    #[error("verification timed out for {provider}")]
    Timeout {
        provider: ProviderId,
        token_state: TokenState,
    },

    #[error("provider unavailable: {provider}")]
    ProviderUnavailable {
        provider: ProviderId,
        retry_after: Option<Duration>,
        token_state: TokenState,
    },

    #[error("invalid provider protocol response from {provider}")]
    Protocol {
        provider: ProviderId,
        #[source]
        source: ProtocolError,
        token_state: TokenState,
    },
}

impl VerificationError {
    pub fn token_state(&self) -> TokenState;
}
```

`NotSent` is returned only when the SDK can prove the token did not cross a
possible consumption boundary. Any ambiguity—including timeout, cancellation
after send as tracked internally, partial write, or a response that cannot be interpreted—maps to
`PossiblyConsumed`. A protocol or availability error may report `NotSent` only
when the official exchange proves the token was not processed. Configuration
errors happen before an attempt and do not have token state. See
[Attempts and safe retries](attempts-and-retries.md).

Rate limiting maps to `ProviderUnavailable { retry_after }` unless a future API
can demonstrate materially distinct caller semantics that justify a separate
variant. It does not justify a speculative root category.

`VerificationError::is_transient()` and `retry_after()` may help an application
choose its outage behavior. They are **not permission to replay a token**. The
API does not expose `is_retryable()`: only the adapter can decide whether a
bounded retry is safe under the provider's single-use and idempotency rules.

## Protocol errors

`ProtocolError` gives typed, bounded reasons without retaining raw bodies:

```rust,ignore
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum ProtocolError {
    #[error("provider response exceeded the configured size limit")]
    ResponseTooLarge,
    #[error("provider response was malformed")]
    MalformedResponse,
    #[error("provider response omitted a required field")]
    MissingRequiredField,
    #[error("provider response contained an invalid field")]
    InvalidField,
    #[error("provider response version or shape is unsupported")]
    UnsupportedResponse,
    #[error("provider returned an unexpected HTTP status")]
    UnexpectedStatus,
}
```

Implementation may add safe structured details, such as a closed normalized
field identifier or status class. It must not retain a raw body, token, secret,
credential-bearing URL, or arbitrary provider string.

## Provider-code mapping

Every adapter maps provider outcomes into exactly one of three buckets:

| Provider signal | SDK result | Caller action |
| --- | --- | --- |
| Client token is invalid, expired, malformed, duplicated, or already used | `Ok(ProviderVerdict::Rejected { .. })` with a normalized `ProviderRejectionReason` | Challenge the client again or deny |
| Credentials, sitekey, account, key type, or provider configuration is invalid | `Err(CaptchaError::Configuration(..))` | Fix operator configuration; do not blame the client |
| Outage, rate limit, transport failure, timeout, or malformed/unsupported protocol | `Err(CaptchaError::Verification(..))` | Apply explicit outage policy; adapter alone controls safe retry |

Unknown provider codes fail closed into the closest safe non-accepting bucket.
They never become `Ok(Accepted)` and never cause provider rejection to be added
as a root error.

## Traits, sources, and redaction

All public error types implement [`std::error::Error`](https://doc.rust-lang.org/std/error/trait.Error.html),
`Debug`, `Display`, `Send`, `Sync`, and `'static`. Public error enums are
[`#[non_exhaustive]`](https://doc.rust-lang.org/reference/attributes/type_system.html#the-non_exhaustive-attribute).
The root uses transparent `#[from]` wrapping; leaf variants use `#[source]`
where preserving a safe causal chain helps diagnosis. This follows the Rust
[error interoperability guideline](https://rust-lang.github.io/api-guidelines/interoperability.html#c-good-err)
and can be implemented with [`thiserror`](https://docs.rs/thiserror/latest/thiserror/).

`Debug`, `Display`, and source chains must redact tokens, secrets, raw provider
bodies, sensitive URLs, and arbitrary upstream messages that may contain them.
Transport wrappers normalize unsafe library errors before exposing a source.

## Stable serialized codes

Error objects themselves implement neither `Serialize` nor `Deserialize`.
Serialization uses a separate safe projection:

```rust,ignore
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize)]
#[serde(rename_all = "snake_case")]
#[non_exhaustive]
pub enum CaptchaErrorCode {
    InvalidProviderConfiguration,
    ProviderCredentialsRejected,
    InvalidPolicy,
    UnsupportedCapability,
    Transport,
    Timeout,
    ProviderUnavailable,
    ProtocolResponseTooLarge,
    ProtocolMalformedResponse,
    ProtocolMissingRequiredField,
    ProtocolInvalidField,
    ProtocolUnsupportedResponse,
    ProtocolUnexpectedStatus,
}
```

`CaptchaError::code()` may return this projection. Codes have stable
snake_case serialization and contain no messages, URLs, payloads, sources,
tokens, secrets, or provider-supplied arbitrary strings. The code is telemetry
and API data, not a deserialized error or authorization artifact.