# Target public API
> **TARGET architecture — implementation pending.** The signatures below are
> illustrative, normative direction. They are not implemented and are not the
> current crate ABI.
## Core vocabulary
```rust,ignore
#[non_exhaustive]
pub struct ProviderId { /* validated, private */ }
pub struct CaptchaToken { /* private, redacted Debug; non-serde */ }
#[non_exhaustive]
pub struct VerificationRequest { /* private */ }
impl VerificationRequest {
pub fn new(token: CaptchaToken) -> Self;
pub fn with_remote_ip(self, remote_ip: IpAddr) -> Self;
}
```
`ProviderId` accepts validated third-party identifiers, rather than limiting
adapters to a closed provider enum. `CaptchaToken` is opaque: core code never
parses it or assumes portability. Requests do not contain expected action,
hostname, score, or age; those expectations belong to `VerificationPolicy`.
## Cloneable facade
```rust,ignore
#[derive(Clone)]
pub struct Captcha {
inner: Arc<CaptchaInner>,
}
#[non_exhaustive]
pub enum ProviderVerdict {
Accepted { evidence: ProviderEvidence },
Rejected {
rejection: ProviderRejection,
evidence: Option<ProviderEvidence>,
},
}
#[non_exhaustive]
pub enum Decision {
Allow,
Deny { failures: Vec<PolicyFailure> },
}
#[non_exhaustive]
pub struct Assessment {
/* provider_verdict + decision, private */
}
impl Captcha {
// Advanced/custom-adapter path.
pub fn builder(adapter: impl ProviderAdapter) -> CaptchaBuilder;
pub fn capabilities(&self) -> &Capabilities;
pub async fn verify(
&self,
request: VerificationRequest,
) -> Result<ProviderVerdict, CaptchaError>;
pub async fn assess(
&self,
request: VerificationRequest,
) -> Result<Assessment, CaptchaError>;
pub async fn verify_with_options(
&self,
request: VerificationRequest,
options: AttemptOptions,
) -> Result<ProviderVerdict, CaptchaError>;
pub async fn assess_with_options(
&self,
request: VerificationRequest,
options: AttemptOptions,
) -> Result<Assessment, CaptchaError>;
}
impl CaptchaBuilder {
pub fn policy(self, policy: VerificationPolicy) -> Self;
pub fn timeout(self, timeout: Duration) -> Self;
pub fn retry_policy(self, retry_policy: RetryPolicy) -> Self;
pub fn build(self) -> Result<Captcha, CaptchaError>;
}
```
`Captcha` is a cheap cloneable configured handle. Its private
`Arc<CaptchaInner>` illustratively shares immutable adapter configuration,
policy, the default HTTP transport, and connection pools. Callers clone
`Captcha` directly; they do not need `Arc::new(captcha)`. The presence of an
`Arc` does not imply a mutex, and no mutex is added solely because state is
shared. Any interior mutability must be justified by a specific transport or
adapter need.
`verify` and `assess` remain `pub async fn` for a uniform facade across remote,
local, and hybrid adapters. Small local verification may return immediately;
expensive CPU verification must use an internal blocking worker rather than
blocking the async executor. Builders, getters, capability inspection, and pure
policy evaluation remain synchronous. The advanced object-safe SPI may erase
its async return as a boxed `Future + Send`.
`verify` exposes the normalized provider outcome without applying application
policy. `Captcha::assess` retains that complete `ProviderVerdict` and adds a
`Decision`. Helpers such as `ProviderVerdict::is_accepted()` and
`Decision::is_allowed()` provide convenience without recreating invalid
`bool + reasons` states.
Rejected tokens—including invalid, expired, and reused tokens—are successful
protocol exchanges and return `Ok(ProviderVerdict::Rejected { .. })`.
Facade methods return the root `CaptchaError`. Configuration failures are
`CaptchaError::Configuration`; inability to obtain a trustworthy verdict due
to transport, timeout, provider availability, or protocol failure is
`CaptchaError::Verification`. See the authoritative [error taxonomy](errors.md).
The `_with_options` methods are the explicit per-attempt override path; the
short methods remain the common default. `AttemptOptions`, cancellation,
`TokenState`, and `RetryPolicy` are defined by
[Attempts and safe retries](attempts-and-retries.md).
## Evidence and policy
```rust,ignore
#[non_exhaustive]
pub struct ProviderEvidence { /* typed optional fields, private */ }
impl ProviderEvidence {
pub fn provider_id(&self) -> &ProviderId;
pub fn score(&self) -> Option<f64>;
pub fn action(&self) -> Option<&str>;
pub fn hostname(&self) -> Option<&str>;
pub fn challenge_time(&self) -> Option<OffsetDateTime>;
}
#[non_exhaustive]
pub enum ProviderRejectionReason {
InvalidToken,
Expired,
AlreadyUsed,
Malformed,
UnknownCode { provider_code: String },
}
#[non_exhaustive]
pub enum PolicyFailure {
ActionMismatch,
HostnameMismatch,
ScoreTooLow,
ChallengeTooOld,
MissingEvidence(Capability),
}
```
Provider rejection reasons and policy failures are intentionally disjoint.
Expected action, allowed hostname, minimum score, and maximum challenge age are
configured through a `VerificationPolicyBuilder`, never inferred from a
provider rejection.
Remote IP is primarily request input and a capability of the configured
verifier, not generic provider evidence. An adapter may expose a separate typed
field only when its protocol actually attests or echoes that value; merely
sending an IP must not cause it to appear in `ProviderEvidence`.
Small values use `new` plus `with_*` methods. Multi-field configuration,
`VerificationPolicy`, and `Captcha` use builders whose `build` methods validate
invariants and capability compatibility. `VerificationPolicy::builder().build()`
is independent; a provider or advanced `CaptchaBuilder::build()` checks it
against effective capabilities. All data-bearing
public types expose getters rather than public fields.
## Normal construction
```rust,ignore
use std::time::Duration;
use captcha_sdk::{
Captcha, CaptchaToken, VerificationPolicy, VerificationRequest,
};
use captcha_sdk::providers::turnstile::Turnstile;
let policy = VerificationPolicy::builder()
.expected_action("login")
.build()?;
let captcha: Captcha = Turnstile::builder(secret)
.policy(policy)
.timeout(Duration::from_secs(3))
.build()?;
let request = VerificationRequest::new(CaptchaToken::new(token)?)
.with_remote_ip(ip);
let assessment = captcha.assess(request).await?;
```
Simple required provider secrets are required arguments, for example
`Turnstile::builder(secret: impl Into<String>)`, and are immediately moved into
private, redacted, non-serializable credential storage. There is no universal
public secret type or public secret getter, and mandatory input does not use a
later optional setter.
Public credential types are reserved for genuinely structured authentication.
For example, `recaptcha::enterprise::EnterpriseCredentials` may offer
`api_key(...)` and `service_account(...)` constructors, and
`recaptcha::enterprise::Recaptcha::builder(credentials)` requires that value.
ALTCHA/HMAC builders may accept provider-appropriate bytes or strings directly
unless their authentication becomes structurally complex. Initial rotation
replaces the configured handle; hot mutation and credential-source integrations
are later work.
The complete storage and rotation contract is in
[Credential lifecycle](credentials.md).
`.timeout(Duration)` is the total operation budget: transport, any safe internal
retries, response parsing, and verdict production all consume it. An absolute
deadline is deferred; `AttemptOptions::timeout` may only shorten one call.
## Advanced construction boundary
Normal users configure a built-in adapter and default HTTP transport through a
provider builder. Advanced users can inject `ProviderAdapter` and
`HttpTransport` implementations through explicitly advanced APIs. Endpoint
override is dangerous, test-only, and feature-gated; it must not be a routine
production builder option.
```rust,ignore
let captcha = Captcha::builder(custom_adapter)
.policy(policy)
.timeout(Duration::from_secs(3))
.retry_policy(RetryPolicy::Safe { max_attempts: 3 })
.build()?;
let options = AttemptOptions::new().timeout(Duration::from_secs(1));
match captcha.verify_with_options(request, options).await {
Err(CaptchaError::Verification(error)) => {
match error.token_state() {
TokenState::NotSent => { /* application may start a new challenge */ }
TokenState::PossiblyConsumed => { /* never assume replay is safe */ }
_ => {}
}
}
result => { /* handle verdict or configuration error */ }
}
```
Built-in provider builders may also expose `.transport(custom_transport)` on
the advanced path. Normal users receive the secure default. The privileged SPI
and an illustrative proxy example are in the
[HTTP transport boundary](http-transport.md); injected transports can see
serialized credentials and token bytes.
The SDK does not expose a public constructor for an authorization-bearing
`VerifiedCaptcha`. Applications authorize from a fresh `Assessment::decision()`
produced by this SDK, not from user-constructed or deserialized proof objects.