# Adapter contract
> **TARGET architecture — implementation pending.** `ProviderAdapter` is an
> advanced object-safe async SPI. Exact borrowing and future-alias syntax may be
> refined during implementation without changing its responsibilities.
## Responsibilities
The public `adapter` module lets built-in and third-party adapters participate
in the same verification pipeline. A `ProviderAdapter` must:
- expose its validated `ProviderId` and effective `Capabilities`;
- validate product configuration before the verifier becomes usable;
- accept `VerificationRequest` containing an opaque `CaptchaToken` and optional
remote IP;
- use an injected `HttpTransport` when provider networking is required;
- translate the provider response into `ProviderVerdict` and typed
`ProviderEvidence`;
- map provider-declared failure codes into `ProviderRejectionReason` without
confusing them with `PolicyFailure`;
- return configuration and verification problems through `CaptchaError`;
- redact secrets and tokens from debug, display, errors, tracing, and metrics.
Illustrative direction:
```rust,ignore
pub type AdapterFuture<'a, T> =
Pin<Box<dyn Future<Output = T> + Send + 'a>>;
pub trait ProviderAdapter: Send + Sync + 'static {
fn provider_id(&self) -> &ProviderId;
fn capabilities(&self) -> &Capabilities;
fn verify(
&self,
request: VerificationRequest,
context: AttemptContext<'_>,
) -> AdapterFuture<'_, Result<ProviderVerdict, CaptchaError>>;
}
```
`AttemptContext` is an SDK-owned, non-constructible orchestration context for
advanced adapter implementations. It provides the selected `HttpTransport`,
remaining total timeout budget, configured `RetryPolicy`, conservative token
state tracking, and internal idempotency support without exposing those as
generic request fields. Its exact borrowing layout may be refined.
The boxed `Future + Send` preserves object safety for the advanced SPI. The
public `Captcha::verify` and `Captcha::assess` facade remains async even for a
local adapter; expensive CPU verification must be moved to an internal blocking
worker rather than blocking the async executor.
Normal application users should not need this SPI; provider builders and
`Captcha` are the ergonomic path. The SPI is small so independent crates can
implement adapters without depending on internal HTTP client or policy types.
## Normalization boundary
Adapters may parse provider-specific structures internally, but the stable
contract contains no raw JSON or generic metadata map. Promote useful,
security-reviewed concepts into typed `ProviderEvidence`. Unknown response
fields are ignored unless required to determine protocol correctness.
An adapter must never synthesize acceptance when the provider response is
ambiguous. Malformed JSON, missing required protocol fields, unexpected status
semantics, and unsupported response versions produce a protocol-flavored
`CaptchaError::Verification(VerificationError::Protocol { .. })`.
The same applies when provider documentation suggests fail-open behavior for
service outages. The adapter does not fabricate `ProviderVerdict::Accepted`;
it returns the appropriate `CaptchaError::Verification` variant. Applications
may implement an explicit outage policy after receiving that error.
Each `HttpTransport::send` performs exactly one exchange. Transport does not
retry or interpret CAPTCHA protocol data; see the authoritative
[HTTP transport boundary](http-transport.md).
## Retry contract
The adapter, not a generic middleware, knows whether retry is safe. The complete
contract is [Attempts and safe retries](attempts-and-retries.md). In summary:
- never retry `ProviderVerdict::Rejected`;
- `RetryPolicy::Never` is the default, while `Safe` only permits a retry;
- retry only after the previous try is definitely `TokenState::NotSent` or an
official provider idempotency mechanism makes replay safe;
- retry only bounded transient failures within the configured total operation
timeout;
- generate an internal per-attempt idempotency key where supported, preserve it
across safe retries, and destroy it after the attempt;
- do not fall back to another provider with the same token.
If the adapter cannot prove `NotSent` and has no applicable idempotency
mechanism, it stops and returns the attempt error classified
`TokenState::PossiblyConsumed`. A transient error alone never proves replay is
safe. Cancellation follows the same conservative classification.
Cloudflare documents Turnstile tokens as single-use, valid for five minutes,
and at most 2048 characters; its validation API supports an idempotency key.
The Turnstile adapter can use that mechanism for conservative retry. These are
provider-specific rules, not defaults for every adapter. See
[official validation documentation](https://developers.cloudflare.com/turnstile/get-started/server-side-validation/).
## Conformance tests
The implementation should publish reusable adapter contract tests covering:
- accepted and rejected verdict invariants;
- redaction under `Debug` and error paths;
- capability accuracy and policy compatibility;
- missing, malformed, and unknown provider response fields;
- timeout-budget propagation, cancellation, and retry bounds;
- `NotSent` versus `PossiblyConsumed` classification at each send boundary;
- no retry on rejection and no cross-provider calls;
- evidence serialization without request or credential material.