1use std::future::Future;
4use std::sync::Arc;
5
6use compact_str::CompactString;
7use http::{HeaderMap, Uri};
8use r402_facilitator::{Facilitator, FacilitatorClient, FacilitatorClientError};
9use r402_protocol::extension::Extension;
10use r402_protocol::network::{ChainId, ChainIdPattern};
11use r402_protocol::payment::PriceTag;
12use r402_server::{
13 PaymentFlowError, PaymentFlowName, ResourceServer, SchemeNetworkServer, schedule,
14};
15use url::Url;
16
17use super::SettlementMode;
18use super::layer::{ResourceTemplate, X402Layer};
19use super::pricing::{DynamicPriceTags, StaticPriceTags};
20
21#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
23pub enum BuildError {
24 #[error("base_url is required; call with_base_url before with_price_tag")]
28 MissingBaseUrl,
29 #[error("missing scheme {scheme} on {network}")]
31 MissingScheme {
32 scheme: CompactString,
34 network: ChainId,
36 },
37 #[error(transparent)]
39 PaymentFlow(#[from] PaymentFlowError),
40 #[error(transparent)]
42 Mode(#[from] r402_server::IncompatibleSettlementMode),
43 #[error(
45 "with_price_tags([]) requires with_auth_only first; empty static tags are not a layer bypass"
46 )]
47 EmptyPriceTags,
48 #[error("escrow scheme {scheme} is missing settle_on_cancel")]
50 MissingSettleOnCancel {
51 scheme: CompactString,
53 },
54}
55
56pub(crate) fn validate_static_layer(
58 server: &ResourceServer,
59 tags: &[PriceTag],
60 mode: SettlementMode,
61 auth_only: bool,
62) -> Result<(), BuildError> {
63 if tags.is_empty() {
64 return if auth_only {
65 Ok(())
66 } else {
67 Err(BuildError::EmptyPriceTags)
68 };
69 }
70
71 for tag in tags {
72 let requirements = &tag.requirements;
73 let Some(scheme) =
74 server.registered_scheme(requirements.scheme.as_str(), &requirements.network)
75 else {
76 return Err(BuildError::MissingScheme {
77 scheme: requirements.scheme.clone(),
78 network: requirements.network.clone(),
79 });
80 };
81
82 let flow = match server.get_payment_flow(requirements) {
83 Ok(flow) => flow,
84 Err(PaymentFlowError::UnregisteredScheme { .. }) => {
85 return Err(BuildError::MissingScheme {
86 scheme: requirements.scheme.clone(),
87 network: requirements.network.clone(),
88 });
89 }
90 Err(err) => return Err(BuildError::PaymentFlow(err)),
91 };
92
93 if flow == PaymentFlowName::Escrow && !scheme.settles_on_cancel() {
94 return Err(BuildError::MissingSettleOnCancel {
95 scheme: requirements.scheme.clone(),
96 });
97 }
98
99 schedule(flow.phases(), mode)?;
100 }
101 Ok(())
102}
103
104#[derive(Clone, Debug)]
108pub struct X402Middleware {
109 server: ResourceServer,
110 base_url: Option<Url>,
111 #[cfg(feature = "siwx")]
112 siwx: Option<Arc<super::SiwxGate>>,
113}
114
115impl X402Middleware {
116 #[must_use]
118 pub fn from_facilitator(fac: impl Facilitator + 'static) -> Self {
119 Self::from_resource_server(ResourceServer::new(Arc::new(fac)))
120 }
121
122 #[must_use]
124 pub const fn from_resource_server(server: ResourceServer) -> Self {
125 Self {
126 server,
127 base_url: None,
128 #[cfg(feature = "siwx")]
129 siwx: None,
130 }
131 }
132
133 pub fn try_new(url: &str) -> Result<Self, FacilitatorClientError> {
139 Ok(Self::from_facilitator(FacilitatorClient::try_from(url)?))
140 }
141
142 #[must_use]
144 pub fn with_scheme(
145 mut self,
146 network: ChainIdPattern,
147 scheme: impl SchemeNetworkServer + 'static,
148 ) -> Self {
149 self.server.register_scheme(network, scheme);
150 self
151 }
152
153 #[must_use]
155 pub fn with_extension(mut self, extension: impl Extension + 'static) -> Self {
156 self.server = self.server.with_extension(extension);
157 self
158 }
159
160 #[must_use]
164 pub fn with_base_url(&self, base_url: Url) -> Self {
165 let mut this = self.clone();
166 this.base_url = Some(base_url);
167 this
168 }
169
170 #[must_use]
172 pub const fn resource_server(&self) -> &ResourceServer {
173 &self.server
174 }
175
176 #[cfg(feature = "siwx")]
181 #[must_use]
182 pub fn with_siwx(&self, gate: super::SiwxGate) -> Self {
183 let mut this = self.clone();
184 this.siwx = Some(Arc::new(gate));
185 this
186 }
187
188 #[cfg(feature = "siwx")]
193 #[must_use]
194 pub fn with_auth_only(&self, gate: super::SiwxGate) -> Self {
195 self.with_siwx(gate.with_auth_only())
196 }
197
198 fn auth_only(&self) -> bool {
199 #[cfg(feature = "siwx")]
200 {
201 self.siwx.as_ref().is_some_and(|g| g.is_auth_only())
202 }
203 #[cfg(not(feature = "siwx"))]
204 {
205 let _ = self;
206 false
207 }
208 }
209
210 pub fn with_price_tag(
217 &self,
218 price_tag: PriceTag,
219 ) -> Result<X402Layer<StaticPriceTags>, BuildError> {
220 self.with_price_tags(vec![price_tag])
221 }
222
223 pub fn with_price_tags(
234 &self,
235 price_tags: Vec<PriceTag>,
236 ) -> Result<X402Layer<StaticPriceTags>, BuildError> {
237 let base_url = Arc::new(self.base_url.clone().ok_or(BuildError::MissingBaseUrl)?);
238 validate_static_layer(
239 &self.server,
240 &price_tags,
241 SettlementMode::default(),
242 self.auth_only(),
243 )?;
244 Ok(X402Layer {
245 server: self.server.clone(),
246 price_source: StaticPriceTags::new(price_tags),
247 base_url,
248 resource: Arc::new(ResourceTemplate::default()),
249 settlement_mode: SettlementMode::default(),
250 settlement_tracker: None,
251 hooks: None,
252 #[cfg(feature = "siwx")]
253 siwx: self.siwx.clone(),
254 })
255 }
256
257 pub fn with_dynamic_price<F, Fut>(
263 &self,
264 callback: F,
265 ) -> Result<X402Layer<DynamicPriceTags>, BuildError>
266 where
267 F: Fn(&HeaderMap, &Uri, &Url) -> Fut + Send + Sync + 'static,
268 Fut: Future<Output = Vec<PriceTag>> + Send + 'static,
269 {
270 Ok(X402Layer {
271 server: self.server.clone(),
272 price_source: DynamicPriceTags::new(callback),
273 base_url: Arc::new(self.base_url.clone().ok_or(BuildError::MissingBaseUrl)?),
274 resource: Arc::new(ResourceTemplate::default()),
275 settlement_mode: SettlementMode::default(),
276 settlement_tracker: None,
277 hooks: None,
278 #[cfg(feature = "siwx")]
279 siwx: self.siwx.clone(),
280 })
281 }
282}
283
284impl TryFrom<&str> for X402Middleware {
285 type Error = FacilitatorClientError;
286
287 fn try_from(value: &str) -> Result<Self, Self::Error> {
288 Self::try_new(value)
289 }
290}
291
292impl TryFrom<String> for X402Middleware {
293 type Error = FacilitatorClientError;
294
295 fn try_from(value: String) -> Result<Self, Self::Error> {
296 Self::try_new(&value)
297 }
298}