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 {}