Skip to main content

r402_http/
facilitator.rs

1//! HTTP-based facilitator client for the x402 protocol.
2//!
3//! Provides [`HttpFacilitatorClient`] which implements
4//! [`r402::server::FacilitatorClient`] by making HTTP calls to a remote
5//! facilitator service (e.g., the CDP facilitator at `x402.org`).
6//!
7//! Also provides [`AuthProvider`] for authenticated facilitator endpoints
8//! (e.g., CDP API key authentication).
9//!
10//! Corresponds to Python SDK's `http/facilitator_client_base.py` +
11//! `http/facilitator_client.py`.
12
13use std::time::Duration;
14
15use r402::proto::{
16    PaymentPayload, PaymentRequirements, SettleResponse, SupportedResponse, VerifyResponse,
17};
18use r402::scheme::{BoxFuture, SchemeError};
19use r402::server::FacilitatorClient;
20use reqwest::header::{CONTENT_TYPE, HeaderMap, HeaderValue};
21use serde::Serialize;
22
23use crate::constants::DEFAULT_FACILITATOR_URL;
24
25/// Per-endpoint authentication headers.
26///
27/// Corresponds to Python SDK's `AuthHeaders` in `facilitator_client_base.py`.
28#[derive(Debug, Clone, Default)]
29pub struct AuthHeaders {
30    /// Headers to include in verify requests.
31    pub verify: HeaderMap,
32    /// Headers to include in settle requests.
33    pub settle: HeaderMap,
34    /// Headers to include in get-supported requests.
35    pub supported: HeaderMap,
36}
37
38/// Generates authentication headers for facilitator requests.
39///
40/// Implement this trait to provide custom authentication (e.g., CDP API
41/// keys, OAuth tokens) for facilitator endpoints.
42///
43/// Corresponds to Python SDK's `AuthProvider` protocol.
44pub trait AuthProvider: Send + Sync {
45    /// Returns authentication headers for each facilitator endpoint.
46    fn get_auth_headers(&self) -> AuthHeaders;
47}
48
49/// [`AuthProvider`] that wraps a static set of headers applied to all endpoints.
50///
51/// Useful for simple API key authentication where the same header is sent
52/// to all facilitator endpoints.
53#[derive(Debug, Clone)]
54pub struct StaticAuthProvider {
55    headers: HeaderMap,
56}
57
58impl StaticAuthProvider {
59    /// Creates a new provider that sends the same headers to all endpoints.
60    #[must_use]
61    pub fn new(headers: HeaderMap) -> Self {
62        Self { headers }
63    }
64
65    /// Creates a provider from a single bearer token.
66    ///
67    /// # Panics
68    ///
69    /// Panics if `token` contains invalid header characters.
70    #[must_use]
71    pub fn bearer(token: &str) -> Self {
72        let mut headers = HeaderMap::new();
73        let value = HeaderValue::from_str(&format!("Bearer {token}")).expect("valid bearer token");
74        headers.insert(reqwest::header::AUTHORIZATION, value);
75        Self { headers }
76    }
77}
78
79impl AuthProvider for StaticAuthProvider {
80    fn get_auth_headers(&self) -> AuthHeaders {
81        AuthHeaders {
82            verify: self.headers.clone(),
83            settle: self.headers.clone(),
84            supported: self.headers.clone(),
85        }
86    }
87}
88
89/// [`AuthProvider`] that wraps a per-endpoint header map callback.
90///
91/// Adapts the dict-style `create_headers` function (as used by CDP SDK)
92/// to the [`AuthProvider`] trait.
93///
94/// Corresponds to Python SDK's `CreateHeadersAuthProvider`.
95pub struct CallbackAuthProvider<F> {
96    create_headers: F,
97}
98
99impl<F> std::fmt::Debug for CallbackAuthProvider<F> {
100    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
101        f.debug_struct("CallbackAuthProvider")
102            .finish_non_exhaustive()
103    }
104}
105
106impl<F> CallbackAuthProvider<F>
107where
108    F: Fn() -> AuthHeaders + Send + Sync,
109{
110    /// Creates a new provider from a callback that returns [`AuthHeaders`].
111    pub fn new(create_headers: F) -> Self {
112        Self { create_headers }
113    }
114}
115
116impl<F> AuthProvider for CallbackAuthProvider<F>
117where
118    F: Fn() -> AuthHeaders + Send + Sync,
119{
120    fn get_auth_headers(&self) -> AuthHeaders {
121        (self.create_headers)()
122    }
123}
124
125/// Configuration for [`HttpFacilitatorClient`].
126///
127/// Corresponds to Python SDK's `FacilitatorConfig` in
128/// `facilitator_client_base.py`.
129pub struct FacilitatorConfig {
130    /// Facilitator service base URL (without trailing slash).
131    pub url: String,
132
133    /// HTTP request timeout.
134    pub timeout: Duration,
135
136    /// Optional authentication provider.
137    pub auth_provider: Option<Box<dyn AuthProvider>>,
138
139    /// Optional pre-configured reqwest client. If `None`, a new client is
140    /// created with the configured timeout.
141    pub http_client: Option<reqwest::Client>,
142
143    /// Optional human-readable identifier for this facilitator
144    /// (defaults to URL).
145    pub identifier: Option<String>,
146}
147
148impl Default for FacilitatorConfig {
149    fn default() -> Self {
150        Self {
151            url: DEFAULT_FACILITATOR_URL.to_owned(),
152            timeout: Duration::from_secs(30),
153            auth_provider: None,
154            http_client: None,
155            identifier: None,
156        }
157    }
158}
159
160impl FacilitatorConfig {
161    /// Creates a config with the given facilitator URL.
162    #[must_use]
163    pub fn new(url: impl Into<String>) -> Self {
164        Self {
165            url: url.into(),
166            ..Self::default()
167        }
168    }
169
170    /// Sets the request timeout.
171    #[must_use]
172    pub fn with_timeout(mut self, timeout: Duration) -> Self {
173        self.timeout = timeout;
174        self
175    }
176
177    /// Sets the authentication provider.
178    #[must_use]
179    pub fn with_auth(mut self, provider: impl AuthProvider + 'static) -> Self {
180        self.auth_provider = Some(Box::new(provider));
181        self
182    }
183
184    /// Sets a pre-configured reqwest client.
185    #[must_use]
186    pub fn with_http_client(mut self, client: reqwest::Client) -> Self {
187        self.http_client = Some(client);
188        self
189    }
190
191    /// Sets the identifier.
192    #[must_use]
193    pub fn with_identifier(mut self, id: impl Into<String>) -> Self {
194        self.identifier = Some(id.into());
195        self
196    }
197}
198
199impl std::fmt::Debug for FacilitatorConfig {
200    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
201        f.debug_struct("FacilitatorConfig")
202            .field("url", &self.url)
203            .field("timeout", &self.timeout)
204            .field("has_auth_provider", &self.auth_provider.is_some())
205            .field("has_http_client", &self.http_client.is_some())
206            .field("identifier", &self.identifier)
207            .finish()
208    }
209}
210
211/// Wire format for verify/settle request bodies sent to the facilitator.
212#[derive(Debug, Serialize)]
213#[serde(rename_all = "camelCase")]
214struct FacilitatorRequestBody {
215    x402_version: u32,
216    payment_payload: serde_json::Value,
217    payment_requirements: serde_json::Value,
218}
219
220/// Async HTTP-based facilitator client.
221///
222/// Communicates with a remote x402 facilitator service over HTTP.
223/// Implements [`FacilitatorClient`] so it can be used with
224/// [`r402::server::X402ResourceServer`].
225///
226/// # Example
227///
228/// ```no_run
229/// use r402_http::facilitator::{HttpFacilitatorClient, FacilitatorConfig};
230///
231/// let client = HttpFacilitatorClient::new(FacilitatorConfig::default());
232/// // Use with X402ResourceServer::with_facilitator(Box::new(client))
233/// ```
234///
235/// Corresponds to Python SDK's `HTTPFacilitatorClient` in
236/// `facilitator_client.py`.
237pub struct HttpFacilitatorClient {
238    url: String,
239    identifier: String,
240    auth_provider: Option<Box<dyn AuthProvider>>,
241    client: reqwest::Client,
242}
243
244impl HttpFacilitatorClient {
245    /// Creates a new HTTP facilitator client from the given configuration.
246    pub fn new(config: FacilitatorConfig) -> Self {
247        let url = config.url.trim_end_matches('/').to_owned();
248        let identifier = config.identifier.unwrap_or_else(|| url.clone());
249
250        let client = config.http_client.unwrap_or_else(|| {
251            reqwest::Client::builder()
252                .timeout(config.timeout)
253                .redirect(reqwest::redirect::Policy::limited(10))
254                .build()
255                .expect("failed to build reqwest::Client")
256        });
257
258        Self {
259            url,
260            identifier,
261            auth_provider: config.auth_provider,
262            client,
263        }
264    }
265
266    /// Creates a client with the default CDP facilitator URL.
267    #[must_use]
268    pub fn default_cdp() -> Self {
269        Self::new(FacilitatorConfig::default())
270    }
271
272    /// Returns the facilitator base URL.
273    #[must_use]
274    pub fn url(&self) -> &str {
275        &self.url
276    }
277
278    /// Returns the effective identifier.
279    #[must_use]
280    pub fn identifier(&self) -> &str {
281        &self.identifier
282    }
283
284    /// Builds headers for a verify request.
285    fn verify_headers(&self) -> HeaderMap {
286        let mut headers = HeaderMap::new();
287        headers.insert(CONTENT_TYPE, HeaderValue::from_static("application/json"));
288        if let Some(auth) = &self.auth_provider {
289            let auth_headers = auth.get_auth_headers();
290            headers.extend(auth_headers.verify);
291        }
292        headers
293    }
294
295    /// Builds headers for a settle request.
296    fn settle_headers(&self) -> HeaderMap {
297        let mut headers = HeaderMap::new();
298        headers.insert(CONTENT_TYPE, HeaderValue::from_static("application/json"));
299        if let Some(auth) = &self.auth_provider {
300            let auth_headers = auth.get_auth_headers();
301            headers.extend(auth_headers.settle);
302        }
303        headers
304    }
305
306    /// Builds headers for a get-supported request.
307    fn supported_headers(&self) -> HeaderMap {
308        let mut headers = HeaderMap::new();
309        headers.insert(CONTENT_TYPE, HeaderValue::from_static("application/json"));
310        if let Some(auth) = &self.auth_provider {
311            let auth_headers = auth.get_auth_headers();
312            headers.extend(auth_headers.supported);
313        }
314        headers
315    }
316
317    /// Builds the JSON request body for verify/settle.
318    fn build_request_body(
319        version: u32,
320        payload: &serde_json::Value,
321        requirements: &serde_json::Value,
322    ) -> FacilitatorRequestBody {
323        FacilitatorRequestBody {
324            x402_version: version,
325            payment_payload: payload.clone(),
326            payment_requirements: requirements.clone(),
327        }
328    }
329
330    /// Internal: POST to facilitator verify endpoint.
331    async fn verify_http(
332        &self,
333        version: u32,
334        payload_value: serde_json::Value,
335        requirements_value: serde_json::Value,
336    ) -> Result<VerifyResponse, SchemeError> {
337        let body = Self::build_request_body(version, &payload_value, &requirements_value);
338
339        let response = self
340            .client
341            .post(format!("{}/verify", self.url))
342            .headers(self.verify_headers())
343            .json(&body)
344            .send()
345            .await
346            .map_err(|e| -> SchemeError {
347                format!("Facilitator verify request failed: {e}").into()
348            })?;
349
350        let status = response.status();
351        if !status.is_success() {
352            let text = response.text().await.unwrap_or_default();
353            return Err(format!("Facilitator verify failed ({status}): {text}").into());
354        }
355
356        let result: VerifyResponse = response.json().await.map_err(|e| -> SchemeError {
357            format!("Facilitator verify response parse error: {e}").into()
358        })?;
359
360        Ok(result)
361    }
362
363    /// Internal: POST to facilitator settle endpoint.
364    async fn settle_http(
365        &self,
366        version: u32,
367        payload_value: serde_json::Value,
368        requirements_value: serde_json::Value,
369    ) -> Result<SettleResponse, SchemeError> {
370        let body = Self::build_request_body(version, &payload_value, &requirements_value);
371
372        let response = self
373            .client
374            .post(format!("{}/settle", self.url))
375            .headers(self.settle_headers())
376            .json(&body)
377            .send()
378            .await
379            .map_err(|e| -> SchemeError {
380                format!("Facilitator settle request failed: {e}").into()
381            })?;
382
383        let status = response.status();
384        if !status.is_success() {
385            let text = response.text().await.unwrap_or_default();
386            return Err(format!("Facilitator settle failed ({status}): {text}").into());
387        }
388
389        let result: SettleResponse = response.json().await.map_err(|e| -> SchemeError {
390            format!("Facilitator settle response parse error: {e}").into()
391        })?;
392
393        Ok(result)
394    }
395
396    /// Verifies a payment from raw JSON bytes.
397    ///
398    /// Operates at the network boundary — auto-detects protocol version.
399    ///
400    /// # Errors
401    ///
402    /// Returns an error on network failure, non-200 response, or parse error.
403    pub async fn verify_from_bytes(
404        &self,
405        payload_bytes: &[u8],
406        requirements_bytes: &[u8],
407    ) -> Result<VerifyResponse, SchemeError> {
408        let version = r402::proto::helpers::detect_version_bytes(payload_bytes)
409            .map_err(|e| -> SchemeError { e.to_string().into() })?;
410        let payload_value: serde_json::Value = serde_json::from_slice(payload_bytes)
411            .map_err(|e| -> SchemeError { e.to_string().into() })?;
412        let requirements_value: serde_json::Value = serde_json::from_slice(requirements_bytes)
413            .map_err(|e| -> SchemeError { e.to_string().into() })?;
414
415        self.verify_http(version, payload_value, requirements_value)
416            .await
417    }
418
419    /// Settles a payment from raw JSON bytes.
420    ///
421    /// Operates at the network boundary — auto-detects protocol version.
422    ///
423    /// # Errors
424    ///
425    /// Returns an error on network failure, non-200 response, or parse error.
426    pub async fn settle_from_bytes(
427        &self,
428        payload_bytes: &[u8],
429        requirements_bytes: &[u8],
430    ) -> Result<SettleResponse, SchemeError> {
431        let version = r402::proto::helpers::detect_version_bytes(payload_bytes)
432            .map_err(|e| -> SchemeError { e.to_string().into() })?;
433        let payload_value: serde_json::Value = serde_json::from_slice(payload_bytes)
434            .map_err(|e| -> SchemeError { e.to_string().into() })?;
435        let requirements_value: serde_json::Value = serde_json::from_slice(requirements_bytes)
436            .map_err(|e| -> SchemeError { e.to_string().into() })?;
437
438        self.settle_http(version, payload_value, requirements_value)
439            .await
440    }
441}
442
443impl std::fmt::Debug for HttpFacilitatorClient {
444    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
445        f.debug_struct("HttpFacilitatorClient")
446            .field("url", &self.url)
447            .field("identifier", &self.identifier)
448            .field("has_auth_provider", &self.auth_provider.is_some())
449            .finish_non_exhaustive()
450    }
451}
452
453impl FacilitatorClient for HttpFacilitatorClient {
454    fn verify<'a>(
455        &'a self,
456        payload: &'a PaymentPayload,
457        requirements: &'a PaymentRequirements,
458    ) -> BoxFuture<'a, Result<VerifyResponse, SchemeError>> {
459        Box::pin(async move {
460            let payload_value = serde_json::to_value(payload)
461                .map_err(|e| -> SchemeError { format!("Serialize payload: {e}").into() })?;
462            let requirements_value = serde_json::to_value(requirements)
463                .map_err(|e| -> SchemeError { format!("Serialize requirements: {e}").into() })?;
464
465            self.verify_http(2, payload_value, requirements_value).await
466        })
467    }
468
469    fn settle<'a>(
470        &'a self,
471        payload: &'a PaymentPayload,
472        requirements: &'a PaymentRequirements,
473    ) -> BoxFuture<'a, Result<SettleResponse, SchemeError>> {
474        Box::pin(async move {
475            let payload_value = serde_json::to_value(payload)
476                .map_err(|e| -> SchemeError { format!("Serialize payload: {e}").into() })?;
477            let requirements_value = serde_json::to_value(requirements)
478                .map_err(|e| -> SchemeError { format!("Serialize requirements: {e}").into() })?;
479
480            self.settle_http(2, payload_value, requirements_value).await
481        })
482    }
483
484    fn get_supported(&self) -> BoxFuture<'_, Result<SupportedResponse, SchemeError>> {
485        Box::pin(async move {
486            let response = self
487                .client
488                .get(format!("{}/supported", self.url))
489                .headers(self.supported_headers())
490                .send()
491                .await
492                .map_err(|e| -> SchemeError {
493                    format!("Facilitator get_supported request failed: {e}").into()
494                })?;
495
496            let status = response.status();
497            if !status.is_success() {
498                let text = response.text().await.unwrap_or_default();
499                return Err(format!("Facilitator get_supported failed ({status}): {text}").into());
500            }
501
502            let result: SupportedResponse = response.json().await.map_err(|e| -> SchemeError {
503                format!("Facilitator get_supported response parse error: {e}").into()
504            })?;
505
506            Ok(result)
507        })
508    }
509}