cratefield_core/ports/captcha.rs
1//! The `Captcha` port. The Turnstile adapter is the reference
2//! implementation (issue #7).
3
4use async_trait::async_trait;
5use thiserror::Error;
6
7/// A captcha verification verdict. `ok: false` with a `reason` from the
8/// provider's `error-codes`; transport-level unavailability surfaces as
9/// `ok: false, reason: "unavailable"` unless the adapter is configured
10/// fail-open (staging only).
11#[derive(Debug, Clone, PartialEq, Eq)]
12pub struct Verdict {
13 pub ok: bool,
14 pub reason: Option<String>,
15}
16
17#[derive(Debug, Clone, Error)]
18pub enum CaptchaError {
19 #[error("captcha transport error: {0}")]
20 Transport(String),
21}
22
23/// What a captcha adapter is actually bound to (issue #133). The harness
24/// reads this at build time to decide whether a production venture's
25/// [`HumanForm`] routes can really be verified — providing the port is
26/// not the same as configuring it.
27///
28/// [`HumanForm`]: crate::route_policy::RoutePolicy::HumanForm
29#[derive(Debug, Clone, Copy, PartialEq, Eq)]
30pub struct CaptchaBinding {
31 /// Response hostnames are checked against the venture's own host.
32 /// Without this, a token minted on any site using the same widget
33 /// verifies here (Turnstile's hostname check is the site's job).
34 pub hostname_bound: bool,
35 /// The expected Turnstile `action` is configured and checked, so a
36 /// widget embedded for some other flow on the same site does not
37 /// authorize this one.
38 pub action_bound: bool,
39 /// Transport failures verify as OK. A staging-only posture: an
40 /// adapter left fail-open cannot support a production form write.
41 pub fail_open: bool,
42}
43
44#[async_trait]
45pub trait Captcha: Send + Sync {
46 async fn verify(&self, token: &str, remote_ip: Option<&str>) -> Result<Verdict, CaptchaError>;
47 /// What this adapter is bound to, as far as it can say. `None` (the
48 /// default) means the adapter does not report: the harness treats it
49 /// as effective when the port is present, and the adapter carries the
50 /// duty to fail closed per request. Adapters that *can* report should
51 /// override this so `HarnessBuilder::build` can refuse a production
52 /// venture that would boot with verification silently unavailable.
53 #[must_use]
54 fn binding(&self) -> Option<CaptchaBinding> {
55 None
56 }
57}