Skip to main content

r402_server/
verify.rs

1//! Verify-payment orchestration.
2
3use r402_facilitator::{DynFacilitator, FailureRecovery};
4use r402_protocol::error::{ErrorReason, FacilitatorError, VerificationError};
5use r402_protocol::payment::{
6    ExtensionEntry, Extensions, PaymentRequirements, TypedVerifyRequest, V2, VerifyRequest,
7    VerifyResponse,
8};
9use r402_protocol::{schema_has_external_ref, validate_extension_echoes};
10use serde::Serialize;
11
12use crate::hooks::{
13    AfterVerifyDecision, BeforeOpDecision, CancelReason, PaymentHookContext, SkipHandlerDirective,
14    VerifyResultContext, WirePaymentPayload,
15};
16use crate::payment_flow::resolve_payment_flow_phases;
17use crate::resource::ResourceServer;
18
19/// Successful verify path outcome (after hooks).
20#[derive(Debug, Clone)]
21#[non_exhaustive]
22pub struct VerifyPaymentOutcome {
23    /// Facilitator (or skipped/recovered) verify response.
24    pub response: VerifyResponse,
25    /// When set, the transport must skip the resource handler and settle inline.
26    pub skip_handler: Option<SkipHandlerDirective>,
27}
28
29impl ResourceServer {
30    /// Before hooks → facilitator → after hooks.
31    ///
32    /// Every accept must have a registered [`crate::SchemeNetworkServer`]. Empty
33    /// `schemes` is [`crate::PaymentFlowError::UnregisteredScheme`], not an
34    /// always-facilitator fallback. When the resolved flow has
35    /// `!verify_before_handler`, returns Valid without the facilitator and
36    /// without `after_verify`.
37    ///
38    /// # Errors
39    ///
40    /// Propagates facilitator errors (unless recovered), before-hook aborts,
41    /// after-verify aborts ([`FacilitatorError::Aborted`]), and payment-flow
42    /// resolution failures.
43    pub async fn verify_payment(
44        &self,
45        payload: &WirePaymentPayload,
46        requirements: &PaymentRequirements,
47        advertised: Option<&Extensions>,
48    ) -> Result<VerifyPaymentOutcome, FacilitatorError> {
49        validate_extension_echoes(advertised, &payload.extensions, |key| {
50            self.extensions
51                .get(key)
52                .map_or(&[], |ext| ext.dynamic_info_fields())
53        })
54        .map_err(FacilitatorError::from)?;
55        if advertised.is_some_and(bazaar_schema_has_external_ref)
56            || bazaar_schema_has_external_ref(&payload.extensions)
57        {
58            return Err(VerificationError::InvalidFormat(
59                "schema must not contain external $ref/$id references".into(),
60            )
61            .into());
62        }
63        let payment = PaymentHookContext {
64            payload: payload.clone(),
65            requirements: requirements.clone(),
66        };
67
68        let mut skipped: Option<VerifyResponse> = None;
69        for hook in &self.hooks {
70            match hook.before_verify(&payment).await {
71                BeforeOpDecision::Continue => {}
72                BeforeOpDecision::Abort { reason, message } => {
73                    return Err(FacilitatorError::Aborted { reason, message });
74                }
75                BeforeOpDecision::Skip { result } => {
76                    skipped = Some(result);
77                    break;
78                }
79            }
80        }
81
82        let response = if let Some(local) = skipped {
83            local
84        } else {
85            let flow = self.get_payment_flow(requirements).map_err(|err| {
86                FacilitatorError::Verification(VerificationError::InvalidFormat(err.to_string()))
87            })?;
88            if resolve_payment_flow_phases(flow).verify_before_handler {
89                self.call_verify_with_failure_hooks(&payment, payload, requirements)
90                    .await?
91            } else {
92                return Ok(VerifyPaymentOutcome {
93                    response: VerifyResponse::valid(""),
94                    skip_handler: None,
95                });
96            }
97        };
98
99        self.run_after_verify(payload, requirements, payment, response)
100            .await
101    }
102
103    async fn run_after_verify(
104        &self,
105        payload: &WirePaymentPayload,
106        requirements: &PaymentRequirements,
107        payment: PaymentHookContext,
108        response: VerifyResponse,
109    ) -> Result<VerifyPaymentOutcome, FacilitatorError> {
110        if !response.is_valid() {
111            return Ok(VerifyPaymentOutcome {
112                response,
113                skip_handler: None,
114            });
115        }
116
117        let result_ctx = VerifyResultContext {
118            payment,
119            result: response.clone(),
120        };
121
122        let mut skip_handler: Option<SkipHandlerDirective> = None;
123        for hook in &self.hooks {
124            match hook.after_verify(&result_ctx).await {
125                AfterVerifyDecision::Continue => {}
126                AfterVerifyDecision::Abort { reason, message } => {
127                    self.notify_verified_payment_canceled(
128                        payload,
129                        requirements,
130                        CancelReason::AfterVerifyAborted,
131                        Some(message.as_str()),
132                        None,
133                    )
134                    .await;
135                    return Err(FacilitatorError::Aborted { reason, message });
136                }
137                AfterVerifyDecision::SkipHandler {
138                    response: directive,
139                } => {
140                    skip_handler = Some(directive);
141                }
142            }
143        }
144
145        Ok(VerifyPaymentOutcome {
146            response,
147            skip_handler,
148        })
149    }
150
151    async fn call_verify_with_failure_hooks(
152        &self,
153        payment: &PaymentHookContext,
154        payload: &WirePaymentPayload,
155        requirements: &PaymentRequirements,
156    ) -> Result<VerifyResponse, FacilitatorError> {
157        let request = build_verify_request(payload, requirements)?;
158        #[cfg(feature = "metrics")]
159        let started = std::time::Instant::now();
160        let result = match DynFacilitator::verify(self.facilitator.as_ref(), request).await {
161            Ok(r) if r.is_valid() => Ok(r),
162            Ok(invalid) => {
163                let error = facilitator_error_from_invalid(&invalid);
164                self.recover_verify(payment, error).await.or(Ok(invalid))
165            }
166            Err(error) => self.recover_verify(payment, error).await,
167        };
168        #[cfg(feature = "metrics")]
169        record_verify_metrics(&result, started.elapsed());
170        result
171    }
172
173    async fn recover_verify(
174        &self,
175        payment: &PaymentHookContext,
176        error: FacilitatorError,
177    ) -> Result<VerifyResponse, FacilitatorError> {
178        for hook in &self.hooks {
179            if let FailureRecovery::Recovered(r) = hook.on_verify_failure(payment, &error).await {
180                return Ok(r);
181            }
182        }
183        Err(error)
184    }
185}
186
187pub(crate) fn build_verify_request(
188    payload: &WirePaymentPayload,
189    requirements: &PaymentRequirements,
190) -> Result<VerifyRequest, FacilitatorError> {
191    let typed = TypedVerifyRequest {
192        x402_version: V2,
193        payment_payload: payload.clone(),
194        payment_requirements: requirements.clone(),
195    };
196    to_verify_request(&typed)
197}
198
199pub(crate) fn to_verify_request<T: Serialize>(
200    typed: &T,
201) -> Result<VerifyRequest, FacilitatorError> {
202    let json = serde_json::to_value(typed).map_err(FacilitatorError::internal)?;
203    Ok(VerifyRequest::from(json))
204}
205
206fn bazaar_schema_has_external_ref(extensions: &Extensions) -> bool {
207    extensions
208        .get("bazaar")
209        .and_then(ExtensionEntry::as_schema)
210        .is_some_and(schema_has_external_ref)
211}
212
213fn facilitator_error_from_invalid(invalid: &VerifyResponse) -> FacilitatorError {
214    match invalid {
215        VerifyResponse::Invalid {
216            reason, message, ..
217        } => {
218            let reason = reason.clone().unwrap_or(ErrorReason::UnexpectedVerifyError);
219            let detail = message
220                .as_ref()
221                .map_or_else(|| reason.to_string(), |m| format!("{reason}: {m}"));
222            FacilitatorError::Verification(VerificationError::InvalidFormat(detail))
223        }
224        _ => FacilitatorError::Verification(VerificationError::InvalidFormat(
225            "expected invalid verify response".into(),
226        )),
227    }
228}
229
230#[cfg(feature = "metrics")]
231fn record_verify_metrics(
232    result: &Result<VerifyResponse, FacilitatorError>,
233    elapsed: std::time::Duration,
234) {
235    let label = match result {
236        Ok(r) if r.is_valid() => "valid",
237        Ok(_) => "invalid",
238        Err(_) => "error",
239    };
240    ::metrics::counter!(
241        r402_protocol::metrics::FACILITATOR_VERIFY_TOTAL,
242        "result" => label,
243    )
244    .increment(1);
245    ::metrics::histogram!(r402_protocol::metrics::FACILITATOR_VERIFY_DURATION_SECONDS)
246        .record(elapsed.as_secs_f64());
247}