Skip to main content

r402_http/server/
hooks.rs

1//! Paygate-level lifecycle hooks.
2//!
3//! Unlike facilitator-level [`FacilitatorHooks`](r402_core::hooks::FacilitatorHooks)
4//! which fire **inside** `verify` / `settle`, these hooks fire at the HTTP
5//! paygate layer and let integrators:
6//!
7//! - bypass payment for API-key holders,
8//! - enforce IP allow-lists / KYT checks,
9//! - short-circuit the request with a custom error response.
10
11use std::future::Future;
12use std::pin::Pin;
13
14use axum_core::body::Body;
15use http::{Request, StatusCode};
16
17/// Outcome returned by [`PaygateHooks::on_protected_request`].
18#[derive(Debug)]
19#[non_exhaustive]
20pub enum ProtectedRequestOutcome {
21    /// Continue with the standard x402 payment flow.
22    Continue,
23    /// Bypass payment and forward the request to the downstream service.
24    GrantAccess,
25    /// Abort with the given status code and optional body.
26    Abort {
27        /// HTTP status to return.
28        status: StatusCode,
29        /// Optional response body (served as `text/plain`).
30        body: Option<String>,
31    },
32}
33
34/// Lifecycle hooks for the HTTP paygate layer.
35///
36/// All methods have no-op defaults so implementors can override only what
37/// they need. The trait is `Send + Sync` because the paygate itself is
38/// `Send + Sync` and holds a shared reference to the hook object.
39pub trait PaygateHooks: Send + Sync {
40    /// Fires on every request that reaches a protected route, before the
41    /// payment check.
42    fn on_protected_request<'a>(
43        &'a self,
44        _req: &'a Request<Body>,
45    ) -> impl Future<Output = ProtectedRequestOutcome> + Send + 'a {
46        async { ProtectedRequestOutcome::Continue }
47    }
48
49    /// Fires after the facilitator verifies the payment but before the
50    /// downstream handler runs. The request is passed by `&mut` so hooks can
51    /// attach tenant / payer metadata as extensions.
52    fn on_payment_verified<'a>(
53        &'a self,
54        _req: &'a mut Request<Body>,
55    ) -> impl Future<Output = ()> + Send + 'a {
56        async {}
57    }
58}
59
60/// Dyn-compatible shim over [`PaygateHooks`].
61///
62/// The generic trait uses AFIT (`-> impl Future`) which prevents its direct
63/// use behind `dyn`. The [`Paygate`](super::paygate::Paygate) needs to hold
64/// hooks as a trait object so the middleware layer remains non-generic; this
65/// shim bridges the gap by boxing the futures.
66pub trait DynPaygateHooks: Send + Sync {
67    /// Dyn-compatible version of [`PaygateHooks::on_protected_request`].
68    fn on_protected_request<'a>(
69        &'a self,
70        req: &'a Request<Body>,
71    ) -> Pin<Box<dyn Future<Output = ProtectedRequestOutcome> + Send + 'a>>;
72
73    /// Dyn-compatible version of [`PaygateHooks::on_payment_verified`].
74    fn on_payment_verified<'a>(
75        &'a self,
76        req: &'a mut Request<Body>,
77    ) -> Pin<Box<dyn Future<Output = ()> + Send + 'a>>;
78}
79
80impl<T> DynPaygateHooks for T
81where
82    T: PaygateHooks + ?Sized,
83{
84    fn on_protected_request<'a>(
85        &'a self,
86        req: &'a Request<Body>,
87    ) -> Pin<Box<dyn Future<Output = ProtectedRequestOutcome> + Send + 'a>> {
88        Box::pin(<T as PaygateHooks>::on_protected_request(self, req))
89    }
90
91    fn on_payment_verified<'a>(
92        &'a self,
93        req: &'a mut Request<Body>,
94    ) -> Pin<Box<dyn Future<Output = ()> + Send + 'a>> {
95        Box::pin(<T as PaygateHooks>::on_payment_verified(self, req))
96    }
97}
98
99/// No-op [`PaygateHooks`] implementation used as the default when no hooks
100/// are configured.
101#[derive(Debug, Clone, Copy, Default)]
102pub struct NoopPaygateHooks;
103
104impl PaygateHooks for NoopPaygateHooks {}