Skip to main content

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}