1use 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#[derive(Debug, Clone)]
21#[non_exhaustive]
22pub struct VerifyPaymentOutcome {
23 pub response: VerifyResponse,
25 pub skip_handler: Option<SkipHandlerDirective>,
27}
28
29impl ResourceServer {
30 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}