Skip to main content

r402_core/resource_server/
hooks.rs

1//! Resource-server lifecycle hooks (official `X402ResourceServer` V2 surface).
2//!
3//! Distinct from [`crate::facilitator::FacilitatorHooks`] (chain-node verify/settle)
4//! and HTTP-only paygate hooks. These fire for every payment the resource
5//! server orchestrates, regardless of transport (HTTP / MCP).
6//!
7//! ## Lifecycle
8//!
9//! **Verify**
10//! 1. `before_verify` — `Continue` | `Abort` | `Skip` (local verify result)
11//! 2. facilitator `verify` (unless skipped)
12//! 3. on facilitator error: `on_verify_failure` (optional recover)
13//! 4. on success (or recovered / skip): `after_verify` —
14//!    `Continue` | `Abort` (fires cancel) | `SkipHandler`
15//!
16//! **Settle**
17//! 1. `before_settle` — `Continue` | `Abort` | `Skip`
18//! 2. facilitator `settle`
19//! 3. on error: `on_settle_failure`
20//! 4. on success: `after_settle`
21//!
22//! **Cancel** — `on_verified_payment_canceled` when a verified payment is not
23//! settled (`handler_threw` / `handler_failed` / `after_verify_aborted`).
24
25use std::fmt::{self, Debug, Formatter};
26use std::future::Future;
27use std::pin::Pin;
28
29use crate::error::FacilitatorError;
30use crate::facilitator::BoxFuture;
31use crate::facilitator::FailureRecovery;
32use crate::wire::{PaymentPayload, PaymentRequirements, SettleResponse, VerifyResponse};
33
34/// Wire payment payload with typed requirements and opaque scheme body.
35pub type WirePaymentPayload = PaymentPayload<PaymentRequirements, serde_json::Value>;
36
37/// Why a verified payment was canceled before settlement.
38///
39/// Wire-stable `snake_case` labels match the official SDK
40/// (`handler_threw` / `handler_failed` / `after_verify_aborted`).
41#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
42#[non_exhaustive]
43pub enum CancelReason {
44    /// Protected handler panicked or returned a transport error.
45    HandlerThrew,
46    /// Protected handler completed with a failing status (≥ 400).
47    HandlerFailed,
48    /// An `after_verify` hook aborted after a successful verify.
49    AfterVerifyAborted,
50}
51
52impl CancelReason {
53    /// Stable machine-readable label.
54    #[must_use]
55    pub const fn as_str(self) -> &'static str {
56        match self {
57            Self::HandlerThrew => "handler_threw",
58            Self::HandlerFailed => "handler_failed",
59            Self::AfterVerifyAborted => "after_verify_aborted",
60        }
61    }
62}
63
64impl fmt::Display for CancelReason {
65    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
66        f.write_str(self.as_str())
67    }
68}
69
70/// Decision from a "before verify/settle" hook.
71#[derive(Debug)]
72#[non_exhaustive]
73pub enum BeforeOpDecision<T> {
74    /// Proceed with the facilitator call.
75    Continue,
76    /// Abort the operation with a structured reason.
77    Abort {
78        /// Machine-readable reason.
79        reason: String,
80        /// Human-readable description.
81        message: String,
82    },
83    /// Short-circuit: use this local result instead of calling the facilitator.
84    Skip {
85        /// Locally produced response.
86        result: T,
87    },
88}
89
90/// In-process directive when an after-verify hook skips the resource handler.
91///
92/// Never appears on the facilitator wire; transports may use `body` as the
93/// success response when settling inline (e.g. cooperative refund).
94#[derive(Debug, Clone, Default)]
95#[non_exhaustive]
96pub struct SkipHandlerDirective {
97    /// Optional content type for the transport response body.
98    pub content_type: Option<String>,
99    /// Optional JSON body for the transport success response.
100    pub body: Option<serde_json::Value>,
101}
102
103impl SkipHandlerDirective {
104    /// Empty directive (settle inline, default success body).
105    #[must_use]
106    pub const fn empty() -> Self {
107        Self {
108            content_type: None,
109            body: None,
110        }
111    }
112
113    /// Builder: attach a content type.
114    #[must_use]
115    pub fn with_content_type(mut self, content_type: impl Into<String>) -> Self {
116        self.content_type = Some(content_type.into());
117        self
118    }
119
120    /// Builder: attach a JSON body.
121    #[must_use]
122    pub fn with_body(mut self, body: serde_json::Value) -> Self {
123        self.body = Some(body);
124        self
125    }
126}
127
128/// Decision from an after-verify hook.
129#[derive(Debug, Clone, Default)]
130#[non_exhaustive]
131pub enum AfterVerifyDecision {
132    /// Continue to the resource handler (default).
133    #[default]
134    Continue,
135    /// Fail closed: fire cancel (`after_verify_aborted`) and reject payment.
136    Abort {
137        /// Machine-readable reason.
138        reason: String,
139        /// Human-readable description.
140        message: String,
141    },
142    /// Bypass the resource handler; transport should settle inline.
143    SkipHandler {
144        /// Optional success body for the transport.
145        response: SkipHandlerDirective,
146    },
147}
148
149/// Shared context for resource-server payment hooks.
150#[derive(Clone)]
151#[non_exhaustive]
152pub struct PaymentHookContext {
153    /// Client payment payload.
154    pub payload: WirePaymentPayload,
155    /// Matched payment requirements.
156    pub requirements: PaymentRequirements,
157}
158
159impl Debug for PaymentHookContext {
160    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
161        f.debug_struct("PaymentHookContext")
162            .field("requirements", &self.requirements)
163            .finish_non_exhaustive()
164    }
165}
166
167/// Context for after-verify hooks (includes facilitator result).
168#[derive(Clone)]
169#[non_exhaustive]
170pub struct VerifyResultContext {
171    /// Base payment context.
172    pub payment: PaymentHookContext,
173    /// Facilitator (or skip/recover) verify response — always a success path
174    /// entry (`Valid` or recovered result passed to after hooks).
175    pub result: VerifyResponse,
176}
177
178impl Debug for VerifyResultContext {
179    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
180        f.debug_struct("VerifyResultContext")
181            .field("payment", &self.payment)
182            .field("result_valid", &self.result.is_valid())
183            .finish_non_exhaustive()
184    }
185}
186
187/// Context for after-settle hooks.
188#[derive(Clone)]
189#[non_exhaustive]
190pub struct SettleResultContext {
191    /// Base payment context.
192    pub payment: PaymentHookContext,
193    /// Facilitator settle response.
194    pub result: SettleResponse,
195}
196
197impl Debug for SettleResultContext {
198    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
199        f.debug_struct("SettleResultContext")
200            .field("payment", &self.payment)
201            .field("result_success", &self.result.is_success())
202            .finish_non_exhaustive()
203    }
204}
205
206/// Context for verified-payment cancellation.
207#[derive(Clone)]
208#[non_exhaustive]
209pub struct VerifiedPaymentCanceledContext {
210    /// Base payment context (same fields as settle context).
211    pub payment: PaymentHookContext,
212    /// Cancellation reason.
213    pub reason: CancelReason,
214    /// Optional error message from the handler / hook.
215    pub error: Option<String>,
216    /// Optional HTTP/transport status from a failed handler response.
217    pub response_status: Option<u16>,
218}
219
220impl Debug for VerifiedPaymentCanceledContext {
221    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
222        f.debug_struct("VerifiedPaymentCanceledContext")
223            .field("reason", &self.reason)
224            .field("response_status", &self.response_status)
225            .finish_non_exhaustive()
226    }
227}
228
229/// Lifecycle hooks for the resource server (transport-agnostic).
230///
231/// All methods default to no-ops. Override only what you need.
232pub trait ResourceServerHooks: Send + Sync {
233    /// Runs before facilitator verify.
234    fn before_verify<'a>(
235        &'a self,
236        _ctx: &'a PaymentHookContext,
237    ) -> impl Future<Output = BeforeOpDecision<VerifyResponse>> + Send + 'a {
238        async { BeforeOpDecision::Continue }
239    }
240
241    /// Runs after a successful verify (including skip / failure recovery).
242    fn after_verify<'a>(
243        &'a self,
244        _ctx: &'a VerifyResultContext,
245    ) -> impl Future<Output = AfterVerifyDecision> + Send + 'a {
246        async { AfterVerifyDecision::Continue }
247    }
248
249    /// Runs when facilitator verify returns an error.
250    fn on_verify_failure<'a>(
251        &'a self,
252        _ctx: &'a PaymentHookContext,
253        _error: &'a FacilitatorError,
254    ) -> impl Future<Output = FailureRecovery<VerifyResponse>> + Send + 'a {
255        async { FailureRecovery::Propagate }
256    }
257
258    /// Runs before facilitator settle.
259    fn before_settle<'a>(
260        &'a self,
261        _ctx: &'a PaymentHookContext,
262    ) -> impl Future<Output = BeforeOpDecision<SettleResponse>> + Send + 'a {
263        async { BeforeOpDecision::Continue }
264    }
265
266    /// Runs after a successful settle (including skip / failure recovery).
267    fn after_settle<'a>(
268        &'a self,
269        _ctx: &'a SettleResultContext,
270    ) -> impl Future<Output = ()> + Send + 'a {
271        async {}
272    }
273
274    /// Runs when facilitator settle returns an error.
275    fn on_settle_failure<'a>(
276        &'a self,
277        _ctx: &'a PaymentHookContext,
278        _error: &'a FacilitatorError,
279    ) -> impl Future<Output = FailureRecovery<SettleResponse>> + Send + 'a {
280        async { FailureRecovery::Propagate }
281    }
282
283    /// Runs when a verified payment will not be settled.
284    fn on_verified_payment_canceled<'a>(
285        &'a self,
286        _ctx: &'a VerifiedPaymentCanceledContext,
287    ) -> impl Future<Output = ()> + Send + 'a {
288        async {}
289    }
290}
291
292/// Object-safe erasure of [`ResourceServerHooks`].
293pub trait DynResourceServerHooks: Send + Sync {
294    /// See [`ResourceServerHooks::before_verify`].
295    fn before_verify<'a>(
296        &'a self,
297        ctx: &'a PaymentHookContext,
298    ) -> Pin<Box<dyn Future<Output = BeforeOpDecision<VerifyResponse>> + Send + 'a>>;
299
300    /// See [`ResourceServerHooks::after_verify`].
301    fn after_verify<'a>(
302        &'a self,
303        ctx: &'a VerifyResultContext,
304    ) -> Pin<Box<dyn Future<Output = AfterVerifyDecision> + Send + 'a>>;
305
306    /// See [`ResourceServerHooks::on_verify_failure`].
307    fn on_verify_failure<'a>(
308        &'a self,
309        ctx: &'a PaymentHookContext,
310        error: &'a FacilitatorError,
311    ) -> BoxFuture<'a, FailureRecovery<VerifyResponse>>;
312
313    /// See [`ResourceServerHooks::before_settle`].
314    fn before_settle<'a>(
315        &'a self,
316        ctx: &'a PaymentHookContext,
317    ) -> Pin<Box<dyn Future<Output = BeforeOpDecision<SettleResponse>> + Send + 'a>>;
318
319    /// See [`ResourceServerHooks::after_settle`].
320    fn after_settle<'a>(&'a self, ctx: &'a SettleResultContext) -> BoxFuture<'a, ()>;
321
322    /// See [`ResourceServerHooks::on_settle_failure`].
323    fn on_settle_failure<'a>(
324        &'a self,
325        ctx: &'a PaymentHookContext,
326        error: &'a FacilitatorError,
327    ) -> BoxFuture<'a, FailureRecovery<SettleResponse>>;
328
329    /// See [`ResourceServerHooks::on_verified_payment_canceled`].
330    fn on_verified_payment_canceled<'a>(
331        &'a self,
332        ctx: &'a VerifiedPaymentCanceledContext,
333    ) -> BoxFuture<'a, ()>;
334}
335
336impl<T: ResourceServerHooks + ?Sized> DynResourceServerHooks for T {
337    fn before_verify<'a>(
338        &'a self,
339        ctx: &'a PaymentHookContext,
340    ) -> Pin<Box<dyn Future<Output = BeforeOpDecision<VerifyResponse>> + Send + 'a>> {
341        Box::pin(<Self as ResourceServerHooks>::before_verify(self, ctx))
342    }
343
344    fn after_verify<'a>(
345        &'a self,
346        ctx: &'a VerifyResultContext,
347    ) -> Pin<Box<dyn Future<Output = AfterVerifyDecision> + Send + 'a>> {
348        Box::pin(<Self as ResourceServerHooks>::after_verify(self, ctx))
349    }
350
351    fn on_verify_failure<'a>(
352        &'a self,
353        ctx: &'a PaymentHookContext,
354        error: &'a FacilitatorError,
355    ) -> BoxFuture<'a, FailureRecovery<VerifyResponse>> {
356        Box::pin(<Self as ResourceServerHooks>::on_verify_failure(
357            self, ctx, error,
358        ))
359    }
360
361    fn before_settle<'a>(
362        &'a self,
363        ctx: &'a PaymentHookContext,
364    ) -> Pin<Box<dyn Future<Output = BeforeOpDecision<SettleResponse>> + Send + 'a>> {
365        Box::pin(<Self as ResourceServerHooks>::before_settle(self, ctx))
366    }
367
368    fn after_settle<'a>(&'a self, ctx: &'a SettleResultContext) -> BoxFuture<'a, ()> {
369        Box::pin(<Self as ResourceServerHooks>::after_settle(self, ctx))
370    }
371
372    fn on_settle_failure<'a>(
373        &'a self,
374        ctx: &'a PaymentHookContext,
375        error: &'a FacilitatorError,
376    ) -> BoxFuture<'a, FailureRecovery<SettleResponse>> {
377        Box::pin(<Self as ResourceServerHooks>::on_settle_failure(
378            self, ctx, error,
379        ))
380    }
381
382    fn on_verified_payment_canceled<'a>(
383        &'a self,
384        ctx: &'a VerifiedPaymentCanceledContext,
385    ) -> BoxFuture<'a, ()> {
386        Box::pin(<Self as ResourceServerHooks>::on_verified_payment_canceled(
387            self, ctx,
388        ))
389    }
390}