1use std::sync::Arc;
17
18use axum_core::body::Body;
19use axum_core::extract::Request;
20use axum_core::response::{IntoResponse, Response};
21use http::{HeaderMap, HeaderValue, StatusCode};
22use r402::facilitator::Facilitator;
23use r402::proto;
24use r402::proto::Base64Bytes;
25use r402::proto::v2;
26use serde_json::json;
27use tower::Service;
28#[cfg(feature = "telemetry")]
29use tracing::{Instrument, instrument};
30use url::Url;
31
32const PAYMENT_HEADER: &str = "Payment-Signature";
33
34#[derive(Debug, thiserror::Error)]
36pub enum VerificationError {
37 #[error("Payment-Signature header is required")]
39 PaymentHeaderMissing,
40 #[error("Invalid or malformed payment header")]
42 InvalidPaymentHeader,
43 #[error("Unable to find matching payment requirements")]
45 NoPaymentMatching,
46 #[error("Verification failed: {0}")]
48 VerificationFailed(String),
49}
50
51#[derive(Debug, thiserror::Error)]
53pub enum PaygateError {
54 #[error(transparent)]
56 Verification(#[from] VerificationError),
57 #[error("Settlement failed: {0}")]
59 Settlement(String),
60}
61
62type PaymentPayload = v2::PaymentPayload<v2::PaymentRequirements, serde_json::Value>;
63
64#[derive(Debug, Clone)]
69pub struct ResourceTemplate {
70 pub description: String,
72 pub mime_type: String,
74 pub url: Option<String>,
76}
77
78impl Default for ResourceTemplate {
79 fn default() -> Self {
80 Self {
81 description: String::new(),
82 mime_type: "application/json".to_owned(),
83 url: None,
84 }
85 }
86}
87
88impl ResourceTemplate {
89 #[allow(clippy::unwrap_used)]
100 pub fn resolve(&self, base_url: Option<&Url>, req: &Request) -> v2::ResourceInfo {
101 let url = self.url.clone().unwrap_or_else(|| {
102 let mut url = base_url.cloned().unwrap_or_else(|| {
103 let host = req
104 .headers()
105 .get("host")
106 .and_then(|h| h.to_str().ok())
107 .unwrap_or("localhost");
108 let origin = format!("http://{host}");
109 let url =
110 Url::parse(&origin).unwrap_or_else(|_| Url::parse("http://localhost").unwrap());
111 #[cfg(feature = "telemetry")]
112 tracing::warn!(
113 "X402Middleware base_url is not configured; \
114 using {url} as origin for resource resolution"
115 );
116 url
117 });
118 url.set_path(req.uri().path());
119 url.set_query(req.uri().query());
120 url.to_string()
121 });
122 v2::ResourceInfo {
123 description: self.description.clone(),
124 mime_type: self.mime_type.clone(),
125 url,
126 }
127 }
128}
129
130#[allow(missing_debug_implementations)]
141pub struct PaygateBuilder<TFacilitator> {
142 facilitator: TFacilitator,
143 accepts: Vec<v2::PriceTag>,
144 resource: Option<v2::ResourceInfo>,
145}
146
147impl<TFacilitator> PaygateBuilder<TFacilitator> {
148 #[must_use]
150 pub fn accept(mut self, price_tag: v2::PriceTag) -> Self {
151 self.accepts.push(price_tag);
152 self
153 }
154
155 #[must_use]
157 pub fn accepts(mut self, price_tags: impl IntoIterator<Item = v2::PriceTag>) -> Self {
158 self.accepts.extend(price_tags);
159 self
160 }
161
162 #[must_use]
164 pub fn resource(mut self, resource: v2::ResourceInfo) -> Self {
165 self.resource = Some(resource);
166 self
167 }
168
169 pub fn build(self) -> Paygate<TFacilitator> {
173 Paygate {
174 facilitator: self.facilitator,
175 accepts: Arc::new(self.accepts),
176 resource: self.resource.unwrap_or_else(|| v2::ResourceInfo {
177 description: String::new(),
178 mime_type: "application/json".to_owned(),
179 url: String::new(),
180 }),
181 }
182 }
183}
184
185#[allow(missing_debug_implementations)]
196pub struct Paygate<TFacilitator> {
197 pub(crate) facilitator: TFacilitator,
198 pub(crate) accepts: Arc<Vec<v2::PriceTag>>,
199 pub(crate) resource: v2::ResourceInfo,
200}
201
202impl<TFacilitator> Paygate<TFacilitator> {
203 pub const fn builder(facilitator: TFacilitator) -> PaygateBuilder<TFacilitator> {
205 PaygateBuilder {
206 facilitator,
207 accepts: Vec::new(),
208 resource: None,
209 }
210 }
211
212 pub const fn facilitator(&self) -> &TFacilitator {
214 &self.facilitator
215 }
216
217 pub fn accepts(&self) -> &[v2::PriceTag] {
219 &self.accepts
220 }
221
222 pub const fn resource(&self) -> &v2::ResourceInfo {
224 &self.resource
225 }
226
227 #[must_use]
237 pub fn error_response(&self, err: PaygateError) -> Response {
238 match err {
239 PaygateError::Verification(ve) => {
240 let payment_required = v2::PaymentRequired {
241 error: Some(ve.to_string()),
242 accepts: self
243 .accepts
244 .iter()
245 .map(|pt| pt.requirements.clone())
246 .collect(),
247 x402_version: v2::V2,
248 resource: self.resource.clone(),
249 extensions: None,
250 };
251 let body_bytes =
252 serde_json::to_vec(&payment_required).expect("serialization failed");
253 let header_value =
254 HeaderValue::from_bytes(Base64Bytes::encode(&body_bytes).as_ref())
255 .expect("invalid header value");
256
257 Response::builder()
258 .status(StatusCode::PAYMENT_REQUIRED)
259 .header("Payment-Required", header_value)
260 .header("Content-Type", "application/json")
261 .body(Body::from(body_bytes))
262 .expect("failed to construct response")
263 }
264 PaygateError::Settlement(ref detail) => {
265 #[cfg(feature = "telemetry")]
266 tracing::error!(details = %detail, "Settlement failed");
267 let body = json!({ "error": "Settlement failed", "details": detail }).to_string();
268
269 Response::builder()
270 .status(StatusCode::PAYMENT_REQUIRED)
271 .header("Content-Type", "application/json")
272 .body(Body::from(body))
273 .expect("failed to construct response")
274 }
275 }
276 }
277}
278
279impl<TFacilitator> Paygate<TFacilitator>
280where
281 TFacilitator: Facilitator + Sync,
282{
283 pub async fn enrich_accepts(&mut self) {
285 let capabilities = self.facilitator.supported().await.unwrap_or_default();
286 let accepts = (*self.accepts)
287 .clone()
288 .into_iter()
289 .map(|mut pt| {
290 pt.enrich(&capabilities);
291 pt
292 })
293 .collect();
294 self.accepts = Arc::new(accepts);
295 }
296
297 #[cfg_attr(feature = "telemetry", instrument(name = "x402.verify_only", skip_all))]
308 pub async fn verify_only(&self, headers: &HeaderMap) -> Result<VerifiedPayment, PaygateError> {
309 let header_bytes = headers
310 .get(PAYMENT_HEADER)
311 .map(HeaderValue::as_bytes)
312 .ok_or(VerificationError::PaymentHeaderMissing)?;
313
314 let payload: PaymentPayload =
315 decode_payment_payload(header_bytes).ok_or(VerificationError::InvalidPaymentHeader)?;
316
317 let verify_request = build_verify_request(payload, &self.accepts)?;
318
319 let verify_response = self
320 .facilitator
321 .verify(verify_request.clone())
322 .await
323 .map_err(|e| VerificationError::VerificationFailed(format!("{e}")))?;
324
325 if let proto::VerifyResponse::Invalid { reason, .. } = verify_response {
326 return Err(VerificationError::VerificationFailed(reason).into());
327 }
328
329 Ok(VerifiedPayment {
330 settle_request: verify_request.into(),
331 })
332 }
333
334 #[cfg_attr(
346 feature = "telemetry",
347 instrument(name = "x402.handle_request", skip_all)
348 )]
349 pub async fn handle_request<
350 ReqBody,
351 ResBody,
352 S: Service<http::Request<ReqBody>, Response = http::Response<ResBody>>,
353 >(
354 &self,
355 inner: S,
356 req: http::Request<ReqBody>,
357 ) -> Result<Response, PaygateError>
358 where
359 S::Response: IntoResponse,
360 S::Error: IntoResponse,
361 S::Future: Send,
362 {
363 let verified = self.verify_only(req.headers()).await?;
364
365 let response = match call_inner(inner, req).await {
366 Ok(r) => r,
367 Err(err) => return Ok(err.into_response()),
368 };
369
370 if response.status().is_client_error() || response.status().is_server_error() {
371 return Ok(response.into_response());
372 }
373
374 let settlement = verified.settle(&self.facilitator).await?;
375 let header_value = settlement_to_header(&settlement)?;
376
377 let mut res = response;
378 res.headers_mut().insert("Payment-Response", header_value);
379 Ok(res.into_response())
380 }
381}
382
383impl<TFacilitator> Paygate<TFacilitator>
384where
385 TFacilitator: Facilitator + Clone + Send + Sync + 'static,
386{
387 #[cfg_attr(
401 feature = "telemetry",
402 instrument(name = "x402.handle_request_concurrent", skip_all)
403 )]
404 pub async fn handle_request_concurrent<
405 ReqBody,
406 ResBody,
407 S: Service<http::Request<ReqBody>, Response = http::Response<ResBody>>,
408 >(
409 &self,
410 inner: S,
411 req: http::Request<ReqBody>,
412 ) -> Result<Response, PaygateError>
413 where
414 S::Response: IntoResponse,
415 S::Error: IntoResponse,
416 S::Future: Send + 'static,
417 ReqBody: Send + 'static,
418 {
419 let verified = self.verify_only(req.headers()).await?;
420
421 let facilitator = self.facilitator.clone();
422 let settle_handle = tokio::spawn(async move { verified.settle(&facilitator).await });
423
424 let response = match call_inner(inner, req).await {
425 Ok(r) => r,
426 Err(err) => {
427 detach(settle_handle);
428 return Ok(err.into_response());
429 }
430 };
431
432 if response.status().is_client_error() || response.status().is_server_error() {
433 detach(settle_handle);
434 return Ok(response.into_response());
435 }
436
437 let settlement = settle_handle
438 .await
439 .map_err(|e| PaygateError::Settlement(format!("settle task panicked: {e}")))??;
440 let header_value = settlement_to_header(&settlement)?;
441
442 let mut res = response;
443 res.headers_mut().insert("Payment-Response", header_value);
444 Ok(res.into_response())
445 }
446}
447
448#[derive(Debug)]
454pub struct VerifiedPayment {
455 settle_request: proto::SettleRequest,
456}
457
458impl VerifiedPayment {
459 pub async fn settle<F: Facilitator>(
466 self,
467 facilitator: &F,
468 ) -> Result<proto::SettleResponse, PaygateError> {
469 let settlement = facilitator
470 .settle(self.settle_request)
471 .await
472 .map_err(|e| PaygateError::Settlement(format!("{e}")))?;
473
474 if let proto::SettleResponse::Error {
475 reason, message, ..
476 } = &settlement
477 {
478 let detail = message.as_deref().unwrap_or(reason.as_str());
479 return Err(PaygateError::Settlement(detail.to_owned()));
480 }
481
482 Ok(settlement)
483 }
484
485 #[must_use]
487 pub const fn settle_request(&self) -> &proto::SettleRequest {
488 &self.settle_request
489 }
490}
491
492pub fn settlement_to_header(
499 settlement: &proto::SettleResponse,
500) -> Result<HeaderValue, PaygateError> {
501 let encoded = settlement
502 .encode_base64()
503 .ok_or_else(|| PaygateError::Settlement("cannot encode error settlement".to_owned()))?;
504 HeaderValue::from_bytes(encoded.as_ref()).map_err(|e| PaygateError::Settlement(e.to_string()))
505}
506
507async fn call_inner<
509 ReqBody,
510 ResBody,
511 S: Service<http::Request<ReqBody>, Response = http::Response<ResBody>>,
512>(
513 mut inner: S,
514 req: http::Request<ReqBody>,
515) -> Result<http::Response<ResBody>, S::Error>
516where
517 S::Future: Send,
518{
519 #[cfg(feature = "telemetry")]
520 {
521 inner
522 .call(req)
523 .instrument(tracing::info_span!("inner"))
524 .await
525 }
526 #[cfg(not(feature = "telemetry"))]
527 {
528 inner.call(req).await
529 }
530}
531
532fn decode_payment_payload<T: serde::de::DeserializeOwned>(header_bytes: &[u8]) -> Option<T> {
534 let decoded = Base64Bytes::from(header_bytes).decode().ok()?;
535 serde_json::from_slice(decoded.as_ref()).ok()
536}
537
538fn build_verify_request(
541 payload: PaymentPayload,
542 accepts: &[v2::PriceTag],
543) -> Result<proto::VerifyRequest, VerificationError> {
544 let selected = accepts
545 .iter()
546 .find(|pt| **pt == payload.accepted)
547 .ok_or(VerificationError::NoPaymentMatching)?;
548
549 let verify = v2::VerifyRequest {
550 x402_version: v2::V2,
551 payment_payload: payload,
552 payment_requirements: selected.requirements.clone(),
553 };
554
555 let json = serde_json::to_value(&verify)
556 .map_err(|e| VerificationError::VerificationFailed(format!("{e}")))?;
557
558 Ok(proto::VerifyRequest::from(json))
559}
560
561fn detach(handle: tokio::task::JoinHandle<Result<proto::SettleResponse, PaygateError>>) {
563 tokio::spawn(async move {
564 let _ = handle.await;
565 });
566}