r402-server 0.21.0

Resource server, paymentFlow, and SettlementMode scheduler for the x402 payment protocol.
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
//! Resource-server lifecycle hooks.

use std::fmt::{self, Debug, Formatter};
use std::future::Future;

use compact_str::CompactString;
use r402_facilitator::{BoxFuture, FailureRecovery};
use r402_protocol::error::FacilitatorError;
use r402_protocol::payment::{
    Extensions, PaymentPayload, PaymentRequirements, SettleResponse, VerifyResponse,
};

use crate::payment_flow::SettlePhase;

/// Wire payment payload with typed requirements and opaque scheme body.
pub type WirePaymentPayload = PaymentPayload<PaymentRequirements, serde_json::Value>;

/// Why a verified payment was canceled before settlement.
///
/// Wire-stable `snake_case` labels: `handler_threw` / `handler_failed` /
/// `after_verify_aborted`.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum CancelReason {
    /// Protected handler panicked or returned a transport error.
    HandlerThrew,
    /// Protected handler completed with a failing status (≥ 400).
    HandlerFailed,
    /// An `after_verify` hook aborted after a successful verify.
    AfterVerifyAborted,
}

impl CancelReason {
    /// Stable machine-readable label.
    #[must_use]
    pub const fn as_str(self) -> &'static str {
        match self {
            Self::HandlerThrew => "handler_threw",
            Self::HandlerFailed => "handler_failed",
            Self::AfterVerifyAborted => "after_verify_aborted",
        }
    }
}

impl fmt::Display for CancelReason {
    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
        f.write_str(self.as_str())
    }
}

/// Decision from a "before verify/settle" hook.
#[derive(Debug)]
#[non_exhaustive]
pub enum BeforeOpDecision<T> {
    /// Proceed with the facilitator call.
    Continue,
    /// Abort the operation with a structured reason.
    Abort {
        /// Machine-readable reason.
        reason: String,
        /// Human-readable description.
        message: String,
    },
    /// Short-circuit: use this local result instead of calling the facilitator.
    Skip {
        /// Locally produced response.
        result: T,
    },
}

/// In-process directive when an after-verify hook skips the resource handler.
///
/// Never appears on the facilitator wire; transports may use `body` as the
/// success response when settling inline.
#[derive(Debug, Clone, Default)]
#[non_exhaustive]
pub struct SkipHandlerDirective {
    /// Optional content type for the transport response body.
    pub content_type: Option<String>,
    /// Optional JSON body for the transport success response.
    pub body: Option<serde_json::Value>,
}

impl SkipHandlerDirective {
    /// Empty directive (settle inline, default success body).
    #[must_use]
    pub const fn empty() -> Self {
        Self {
            content_type: None,
            body: None,
        }
    }

    /// Builder: attach a content type.
    #[must_use]
    pub fn with_content_type(mut self, content_type: impl Into<String>) -> Self {
        self.content_type = Some(content_type.into());
        self
    }

    /// Builder: attach a JSON body.
    #[must_use]
    pub fn with_body(mut self, body: serde_json::Value) -> Self {
        self.body = Some(body);
        self
    }
}

/// Decision from an after-verify hook.
#[derive(Debug, Clone, Default)]
#[non_exhaustive]
pub enum AfterVerifyDecision {
    /// Continue to the resource handler (default).
    #[default]
    Continue,
    /// Fail closed: fire cancel (`after_verify_aborted`) and reject payment.
    Abort {
        /// Machine-readable reason.
        reason: String,
        /// Human-readable description.
        message: String,
    },
    /// Bypass the resource handler; transport should settle inline.
    SkipHandler {
        /// Optional success body for the transport.
        response: SkipHandlerDirective,
    },
}

/// Shared context for resource-server payment hooks.
#[derive(Clone)]
#[non_exhaustive]
pub struct PaymentHookContext {
    /// Client payment payload.
    pub payload: WirePaymentPayload,
    /// Matched payment requirements.
    pub requirements: PaymentRequirements,
}

impl Debug for PaymentHookContext {
    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
        f.debug_struct("PaymentHookContext")
            .field("requirements", &self.requirements)
            .finish_non_exhaustive()
    }
}

impl PaymentHookContext {
    /// Constructs a payment hook context.
    #[must_use]
    pub const fn new(payload: WirePaymentPayload, requirements: PaymentRequirements) -> Self {
        Self {
            payload,
            requirements,
        }
    }
}

/// Context for after-verify hooks (includes facilitator result).
#[derive(Clone)]
#[non_exhaustive]
pub struct VerifyResultContext {
    /// Base payment context.
    pub payment: PaymentHookContext,
    /// Facilitator (or skip/recover) verify response — always a success path
    /// entry (`Valid` or recovered result passed to after hooks).
    pub result: VerifyResponse,
}

impl Debug for VerifyResultContext {
    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
        f.debug_struct("VerifyResultContext")
            .field("payment", &self.payment)
            .field("result_valid", &self.result.is_valid())
            .finish_non_exhaustive()
    }
}

/// Context for before-settle / settle-failure hooks.
#[derive(Clone)]
#[non_exhaustive]
pub struct SettleContext {
    /// Base payment context.
    pub payment: PaymentHookContext,
    /// Extension IDs declared on the 402 for this settle.
    pub declared_extensions: Extensions,
    /// Which settle invocation is running.
    pub phase: SettlePhase,
    /// Resource URL from the 402 / request, when known.
    pub resource_url: Option<CompactString>,
}

impl Debug for SettleContext {
    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
        f.debug_struct("SettleContext")
            .field("phase", &self.phase)
            .field("payment", &self.payment)
            .finish_non_exhaustive()
    }
}

impl SettleContext {
    /// Constructs a settle context with empty declared extensions.
    #[must_use]
    pub fn new(payment: PaymentHookContext, phase: SettlePhase) -> Self {
        Self {
            payment,
            declared_extensions: Extensions::new(),
            phase,
            resource_url: None,
        }
    }
}

/// Context for after-settle hooks.
#[derive(Clone)]
#[non_exhaustive]
pub struct SettleResultContext {
    /// Settle invocation that produced `result`.
    pub settle: SettleContext,
    /// Facilitator settle response.
    pub result: SettleResponse,
}

impl Debug for SettleResultContext {
    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
        f.debug_struct("SettleResultContext")
            .field("settle", &self.settle)
            .field("result_success", &self.result.is_success())
            .finish_non_exhaustive()
    }
}

/// Context for verified-payment cancellation.
#[derive(Clone)]
#[non_exhaustive]
pub struct VerifiedPaymentCanceledContext {
    /// Base payment context.
    pub payment: PaymentHookContext,
    /// Cancellation reason.
    pub reason: CancelReason,
    /// Optional error message from the handler / hook.
    pub error: Option<String>,
    /// Optional transport status from a failed handler response.
    pub response_status: Option<u16>,
    /// Settle phases already completed for this payment.
    pub settled_phases: Vec<SettlePhase>,
}

impl Debug for VerifiedPaymentCanceledContext {
    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
        f.debug_struct("VerifiedPaymentCanceledContext")
            .field("reason", &self.reason)
            .field("response_status", &self.response_status)
            .field("settled_phases", &self.settled_phases)
            .finish_non_exhaustive()
    }
}

impl VerifiedPaymentCanceledContext {
    /// Constructs a cancel context.
    #[must_use]
    pub const fn new(
        payment: PaymentHookContext,
        reason: CancelReason,
        settled_phases: Vec<SettlePhase>,
    ) -> Self {
        Self {
            payment,
            reason,
            error: None,
            response_status: None,
            settled_phases,
        }
    }
}

/// Lifecycle hooks for the resource server (transport-agnostic).
///
/// All methods default to no-ops. Override only what you need.
pub trait ResourceServerHooks: Send + Sync {
    /// Runs before facilitator verify.
    fn before_verify<'a>(
        &'a self,
        _ctx: &'a PaymentHookContext,
    ) -> impl Future<Output = BeforeOpDecision<VerifyResponse>> + Send + 'a {
        async { BeforeOpDecision::Continue }
    }

    /// Runs after a successful verify (including skip / failure recovery).
    fn after_verify<'a>(
        &'a self,
        _ctx: &'a VerifyResultContext,
    ) -> impl Future<Output = AfterVerifyDecision> + Send + 'a {
        async { AfterVerifyDecision::Continue }
    }

    /// Runs when facilitator verify returns an error.
    fn on_verify_failure<'a>(
        &'a self,
        _ctx: &'a PaymentHookContext,
        _error: &'a FacilitatorError,
    ) -> impl Future<Output = FailureRecovery<VerifyResponse>> + Send + 'a {
        async { FailureRecovery::Propagate }
    }

    /// Runs before facilitator settle.
    fn before_settle<'a>(
        &'a self,
        _ctx: &'a SettleContext,
    ) -> impl Future<Output = BeforeOpDecision<SettleResponse>> + Send + 'a {
        async { BeforeOpDecision::Continue }
    }

    /// Runs after a successful settle (including skip / failure recovery).
    fn after_settle<'a>(
        &'a self,
        _ctx: &'a SettleResultContext,
    ) -> impl Future<Output = ()> + Send + 'a {
        async {}
    }

    /// Runs when facilitator settle returns an error.
    fn on_settle_failure<'a>(
        &'a self,
        _ctx: &'a SettleContext,
        _error: &'a FacilitatorError,
    ) -> impl Future<Output = FailureRecovery<SettleResponse>> + Send + 'a {
        async { FailureRecovery::Propagate }
    }

    /// Runs when a verified payment will not be settled.
    fn on_verified_payment_canceled<'a>(
        &'a self,
        _ctx: &'a VerifiedPaymentCanceledContext,
    ) -> impl Future<Output = ()> + Send + 'a {
        async {}
    }
}

/// Object-safe erasure of [`ResourceServerHooks`].
pub trait DynResourceServerHooks: Send + Sync {
    /// See [`ResourceServerHooks::before_verify`].
    fn before_verify<'a>(
        &'a self,
        ctx: &'a PaymentHookContext,
    ) -> BoxFuture<'a, BeforeOpDecision<VerifyResponse>>;

    /// See [`ResourceServerHooks::after_verify`].
    fn after_verify<'a>(
        &'a self,
        ctx: &'a VerifyResultContext,
    ) -> BoxFuture<'a, AfterVerifyDecision>;

    /// See [`ResourceServerHooks::on_verify_failure`].
    fn on_verify_failure<'a>(
        &'a self,
        ctx: &'a PaymentHookContext,
        error: &'a FacilitatorError,
    ) -> BoxFuture<'a, FailureRecovery<VerifyResponse>>;

    /// See [`ResourceServerHooks::before_settle`].
    fn before_settle<'a>(
        &'a self,
        ctx: &'a SettleContext,
    ) -> BoxFuture<'a, BeforeOpDecision<SettleResponse>>;

    /// See [`ResourceServerHooks::after_settle`].
    fn after_settle<'a>(&'a self, ctx: &'a SettleResultContext) -> BoxFuture<'a, ()>;

    /// See [`ResourceServerHooks::on_settle_failure`].
    fn on_settle_failure<'a>(
        &'a self,
        ctx: &'a SettleContext,
        error: &'a FacilitatorError,
    ) -> BoxFuture<'a, FailureRecovery<SettleResponse>>;

    /// See [`ResourceServerHooks::on_verified_payment_canceled`].
    fn on_verified_payment_canceled<'a>(
        &'a self,
        ctx: &'a VerifiedPaymentCanceledContext,
    ) -> BoxFuture<'a, ()>;
}

impl<T: ResourceServerHooks + ?Sized> DynResourceServerHooks for T {
    fn before_verify<'a>(
        &'a self,
        ctx: &'a PaymentHookContext,
    ) -> BoxFuture<'a, BeforeOpDecision<VerifyResponse>> {
        Box::pin(<Self as ResourceServerHooks>::before_verify(self, ctx))
    }

    fn after_verify<'a>(
        &'a self,
        ctx: &'a VerifyResultContext,
    ) -> BoxFuture<'a, AfterVerifyDecision> {
        Box::pin(<Self as ResourceServerHooks>::after_verify(self, ctx))
    }

    fn on_verify_failure<'a>(
        &'a self,
        ctx: &'a PaymentHookContext,
        error: &'a FacilitatorError,
    ) -> BoxFuture<'a, FailureRecovery<VerifyResponse>> {
        Box::pin(<Self as ResourceServerHooks>::on_verify_failure(
            self, ctx, error,
        ))
    }

    fn before_settle<'a>(
        &'a self,
        ctx: &'a SettleContext,
    ) -> BoxFuture<'a, BeforeOpDecision<SettleResponse>> {
        Box::pin(<Self as ResourceServerHooks>::before_settle(self, ctx))
    }

    fn after_settle<'a>(&'a self, ctx: &'a SettleResultContext) -> BoxFuture<'a, ()> {
        Box::pin(<Self as ResourceServerHooks>::after_settle(self, ctx))
    }

    fn on_settle_failure<'a>(
        &'a self,
        ctx: &'a SettleContext,
        error: &'a FacilitatorError,
    ) -> BoxFuture<'a, FailureRecovery<SettleResponse>> {
        Box::pin(<Self as ResourceServerHooks>::on_settle_failure(
            self, ctx, error,
        ))
    }

    fn on_verified_payment_canceled<'a>(
        &'a self,
        ctx: &'a VerifiedPaymentCanceledContext,
    ) -> BoxFuture<'a, ()> {
        Box::pin(<Self as ResourceServerHooks>::on_verified_payment_canceled(
            self, ctx,
        ))
    }
}