1use std::convert::Infallible;
8use std::sync::Arc;
9
10use axum_core::body::Body;
11use axum_core::extract::Request;
12use axum_core::response::{IntoResponse, Response};
13use http::{HeaderMap, HeaderValue, StatusCode};
14use r402::facilitator::Facilitator;
15use r402::proto;
16use r402::proto::Base64Bytes;
17use r402::proto::v2;
18use serde_json::json;
19use tower::Service;
20#[cfg(feature = "telemetry")]
21use tracing::{Instrument, instrument};
22use url::Url;
23
24use super::{PaygateError, VerificationError};
25
26#[derive(Debug, Clone)]
28pub struct ResourceInfoBuilder {
29 pub description: String,
31 pub mime_type: String,
33 pub url: Option<String>,
35}
36
37impl Default for ResourceInfoBuilder {
38 fn default() -> Self {
39 Self {
40 description: String::new(),
41 mime_type: "application/json".to_string(),
42 url: None,
43 }
44 }
45}
46
47impl ResourceInfoBuilder {
48 #[allow(clippy::unwrap_used)]
57 pub fn as_resource_info(&self, base_url: Option<&Url>, req: &Request) -> v2::ResourceInfo {
58 let url = self.url.clone().unwrap_or_else(|| {
59 let mut url = base_url.cloned().unwrap_or_else(|| {
60 let host = req.headers().get("host").and_then(|h| h.to_str().ok()).unwrap_or("localhost");
61 let origin = format!("http://{host}");
62 let url = Url::parse(&origin).unwrap_or_else(|_| Url::parse("http://localhost").unwrap());
63 #[cfg(feature = "telemetry")]
64 tracing::warn!(
65 "X402Middleware base_url is not configured; using {url} as origin for resource resolution"
66 );
67 url
68 });
69 let request_uri = req.uri();
70 url.set_path(request_uri.path());
71 url.set_query(request_uri.query());
72 url.to_string()
73 });
74 v2::ResourceInfo {
75 description: self.description.clone(),
76 mime_type: self.mime_type.clone(),
77 url,
78 }
79 }
80}
81
82#[allow(missing_debug_implementations)]
93pub struct Paygate<TFacilitator> {
94 pub(crate) facilitator: TFacilitator,
95 pub(crate) accepts: Arc<Vec<v2::PriceTag>>,
96 pub(crate) resource: v2::ResourceInfo,
97}
98
99#[allow(missing_debug_implementations)]
110pub struct PaygateBuilder<TFacilitator> {
111 facilitator: TFacilitator,
112 accepts: Vec<v2::PriceTag>,
113 resource: Option<v2::ResourceInfo>,
114}
115
116impl<TFacilitator> Paygate<TFacilitator> {
117 pub const fn builder(facilitator: TFacilitator) -> PaygateBuilder<TFacilitator> {
119 PaygateBuilder {
120 facilitator,
121 accepts: Vec::new(),
122 resource: None,
123 }
124 }
125
126 pub const fn facilitator(&self) -> &TFacilitator {
128 &self.facilitator
129 }
130
131 pub fn accepts(&self) -> &[v2::PriceTag] {
133 &self.accepts
134 }
135
136 pub const fn resource(&self) -> &v2::ResourceInfo {
138 &self.resource
139 }
140
141 #[must_use]
148 pub fn error_response(&self, err: PaygateError) -> Response {
149 error_into_response(err, &self.accepts, &self.resource)
150 }
151}
152
153impl<TFacilitator> PaygateBuilder<TFacilitator> {
154 #[must_use]
156 pub fn accept(mut self, price_tag: v2::PriceTag) -> Self {
157 self.accepts.push(price_tag);
158 self
159 }
160
161 #[must_use]
163 pub fn accepts(mut self, price_tags: impl IntoIterator<Item = v2::PriceTag>) -> Self {
164 self.accepts.extend(price_tags);
165 self
166 }
167
168 #[must_use]
170 pub fn resource(mut self, resource: v2::ResourceInfo) -> Self {
171 self.resource = Some(resource);
172 self
173 }
174
175 pub fn build(self) -> Paygate<TFacilitator> {
179 Paygate {
180 facilitator: self.facilitator,
181 accepts: Arc::new(self.accepts),
182 resource: self.resource.unwrap_or_else(|| v2::ResourceInfo {
183 description: String::new(),
184 mime_type: "application/json".to_owned(),
185 url: String::new(),
186 }),
187 }
188 }
189}
190
191pub const PAYMENT_HEADER_NAME: &str = "Payment-Signature";
193
194pub type V2PaymentPayload = v2::PaymentPayload<v2::PaymentRequirements, serde_json::Value>;
196
197#[derive(Debug)]
210pub struct VerifiedPayment {
211 settle_request: proto::SettleRequest,
213}
214
215impl VerifiedPayment {
216 pub async fn settle<F: Facilitator>(
225 self,
226 facilitator: &F,
227 ) -> Result<proto::SettleResponse, PaygateError> {
228 let settlement = facilitator
229 .settle(self.settle_request)
230 .await
231 .map_err(|e| PaygateError::Settlement(format!("{e}")))?;
232
233 if let proto::SettleResponse::Error {
234 reason, message, ..
235 } = &settlement
236 {
237 let detail = message.as_deref().unwrap_or(reason.as_str());
238 return Err(PaygateError::Settlement(detail.to_owned()));
239 }
240
241 Ok(settlement)
242 }
243
244 #[must_use]
246 pub const fn settle_request(&self) -> &proto::SettleRequest {
247 &self.settle_request
248 }
249}
250
251impl<TFacilitator> Paygate<TFacilitator> {
252 async fn call_inner<
254 ReqBody,
255 ResBody,
256 S: Service<http::Request<ReqBody>, Response = http::Response<ResBody>>,
257 >(
258 mut inner: S,
259 req: http::Request<ReqBody>,
260 ) -> Result<http::Response<ResBody>, S::Error>
261 where
262 S::Future: Send,
263 {
264 #[cfg(feature = "telemetry")]
265 {
266 inner
267 .call(req)
268 .instrument(tracing::info_span!("inner"))
269 .await
270 }
271 #[cfg(not(feature = "telemetry"))]
272 {
273 inner.call(req).await
274 }
275 }
276}
277
278impl<TFacilitator> Paygate<TFacilitator>
279where
280 TFacilitator: Facilitator + Sync,
281{
282 #[cfg_attr(
291 feature = "telemetry",
292 instrument(name = "x402.handle_request", skip_all)
293 )]
294 pub async fn handle_request<
295 ReqBody,
296 ResBody,
297 S: Service<http::Request<ReqBody>, Response = http::Response<ResBody>>,
298 >(
299 &self,
300 inner: S,
301 req: http::Request<ReqBody>,
302 ) -> Result<Response, Infallible>
303 where
304 S::Response: IntoResponse,
305 S::Error: IntoResponse,
306 S::Future: Send,
307 {
308 match self.handle_request_fallible(inner, req).await {
309 Ok(response) => Ok(response),
310 Err(err) => Ok(error_into_response(err, &self.accepts, &self.resource)),
311 }
312 }
313
314 pub async fn enrich_accepts(&mut self) {
316 let capabilities = self.facilitator.supported().await.unwrap_or_default();
317
318 let accepts = (*self.accepts)
319 .clone()
320 .into_iter()
321 .map(|mut pt| {
322 pt.enrich(&capabilities);
323 pt
324 })
325 .collect::<Vec<_>>();
326 self.accepts = Arc::new(accepts);
327 }
328
329 #[cfg_attr(feature = "telemetry", instrument(name = "x402.verify_only", skip_all))]
343 pub async fn verify_only(&self, headers: &HeaderMap) -> Result<VerifiedPayment, PaygateError> {
344 let header = extract_payment_header(headers, PAYMENT_HEADER_NAME).ok_or(
345 VerificationError::PaymentHeaderRequired(PAYMENT_HEADER_NAME),
346 )?;
347 let payment_payload = extract_payment_payload::<V2PaymentPayload>(header)
348 .ok_or(VerificationError::InvalidPaymentHeader)?;
349
350 let verify_request = make_verify_request(payment_payload, &self.accepts)?;
351
352 let verify_response = self
353 .facilitator
354 .verify(verify_request.clone())
355 .await
356 .map_err(|e| VerificationError::VerificationFailed(format!("{e}")))?;
357
358 validate_verify_response(verify_response)?;
359
360 Ok(VerifiedPayment {
361 settle_request: verify_request.into(),
362 })
363 }
364
365 pub async fn handle_request_fallible<
378 ReqBody,
379 ResBody,
380 S: Service<http::Request<ReqBody>, Response = http::Response<ResBody>>,
381 >(
382 &self,
383 inner: S,
384 req: http::Request<ReqBody>,
385 ) -> Result<Response, PaygateError>
386 where
387 S::Response: IntoResponse,
388 S::Error: IntoResponse,
389 S::Future: Send,
390 {
391 let verified = self.verify_only(req.headers()).await?;
393
394 let response = match Self::call_inner(inner, req).await {
396 Ok(response) => response,
397 Err(err) => return Ok(err.into_response()),
398 };
399
400 if response.status().is_client_error() || response.status().is_server_error() {
402 return Ok(response.into_response());
403 }
404
405 let settlement = verified.settle(&self.facilitator).await?;
407 let header_value = settlement_to_header(&settlement)?;
408
409 let mut res = response;
410 res.headers_mut().insert("Payment-Response", header_value);
411 Ok(res.into_response())
412 }
413}
414
415pub fn extract_payment_header<'a>(
417 header_map: &'a HeaderMap,
418 header_name: &'a str,
419) -> Option<&'a [u8]> {
420 header_map.get(header_name).map(HeaderValue::as_bytes)
421}
422
423#[must_use]
425pub fn extract_payment_payload<T>(header_bytes: &[u8]) -> Option<T>
426where
427 T: serde::de::DeserializeOwned,
428{
429 let base64 = Base64Bytes::from(header_bytes).decode().ok()?;
430 let value = serde_json::from_slice(base64.as_ref()).ok()?;
431 Some(value)
432}
433
434pub fn settlement_to_header(
444 settlement: &proto::SettleResponse,
445) -> Result<HeaderValue, PaygateError> {
446 let encoded = settlement
447 .encode_base64()
448 .ok_or_else(|| PaygateError::Settlement("cannot encode error settlement".to_owned()))?;
449 HeaderValue::from_bytes(encoded.as_ref())
450 .map_err(|err| PaygateError::Settlement(err.to_string()))
451}
452
453pub fn make_verify_request(
460 payment_payload: V2PaymentPayload,
461 accepts: &[v2::PriceTag],
462) -> Result<proto::VerifyRequest, VerificationError> {
463 let accepted = &payment_payload.accepted;
464
465 let selected = accepts
466 .iter()
467 .find(|price_tag| **price_tag == *accepted)
468 .ok_or(VerificationError::NoPaymentMatching)?;
469
470 let verify_request = v2::VerifyRequest {
471 x402_version: v2::V2,
472 payment_payload,
473 payment_requirements: selected.requirements.clone(),
474 };
475
476 let json = serde_json::to_value(&verify_request)
477 .map_err(|e| VerificationError::VerificationFailed(format!("{e}")))?;
478
479 Ok(proto::VerifyRequest::from(json))
480}
481
482pub fn validate_verify_response(
488 verify_response: proto::VerifyResponse,
489) -> Result<(), VerificationError> {
490 match verify_response {
491 proto::VerifyResponse::Valid { .. } => Ok(()),
492 proto::VerifyResponse::Invalid { reason, .. } => {
493 Err(VerificationError::VerificationFailed(reason))
494 }
495 _ => Err(VerificationError::VerificationFailed(
496 "unknown verify response variant".into(),
497 )),
498 }
499}
500
501pub fn error_into_response(
508 err: PaygateError,
509 accepts: &[v2::PriceTag],
510 resource: &v2::ResourceInfo,
511) -> Response {
512 match err {
513 PaygateError::Verification(err) => {
514 let payment_required_response = v2::PaymentRequired {
515 error: Some(err.to_string()),
516 accepts: accepts.iter().map(|pt| pt.requirements.clone()).collect(),
517 x402_version: v2::V2,
518 resource: resource.clone(),
519 extensions: None,
520 };
521 let payment_required_bytes =
522 serde_json::to_vec(&payment_required_response).expect("serialization failed");
523 let payment_required_header = Base64Bytes::encode(&payment_required_bytes);
524 let header_value = HeaderValue::from_bytes(payment_required_header.as_ref())
525 .expect("Failed to create header value");
526
527 Response::builder()
528 .status(StatusCode::PAYMENT_REQUIRED)
529 .header("Payment-Required", header_value)
530 .header("Content-Type", "application/json")
531 .body(Body::from(payment_required_bytes))
532 .expect("Fail to construct response")
533 }
534 PaygateError::Settlement(ref err) => {
535 #[cfg(feature = "telemetry")]
536 tracing::error!(details = %err, "Settlement failed");
537 let body = Body::from(
538 json!({ "error": "Settlement failed", "details": err.clone() }).to_string(),
539 );
540 Response::builder()
541 .status(StatusCode::PAYMENT_REQUIRED)
542 .header("Content-Type", "application/json")
543 .body(body)
544 .expect("Fail to construct response")
545 }
546 }
547}