Skip to main content

r402_server/
required.rs

1//! 402 body construction: scheme enrich, extension advertise, payment-flow extra.
2
3use compact_str::CompactString;
4use r402_protocol::error::FacilitatorError;
5use r402_protocol::extension::{AdvertiseContext, ExtensionRegistry};
6use r402_protocol::network::ChainId;
7use r402_protocol::payment::{
8    Extensions, PaymentRequired, PaymentRequirements, ResourceInfo, SupportedResponse,
9};
10
11use crate::hooks::{
12    WirePaymentPayload, assert_accepts_additive_extra_after_scheme_enrich,
13    assert_accepts_allowlisted_after_extension_enrich, snapshot_payment_requirements_list,
14};
15use crate::payment_flow::apply_payment_flow_wire_extra;
16use crate::resource::ResourceServer;
17use crate::scheme::SchemePaymentRequiredContext;
18
19/// Inputs for [`ResourceServer::create_payment_required_response`].
20#[derive(Debug, Clone)]
21pub struct PaymentRequiredBuildContext {
22    /// Resource metadata placed on the 402 body.
23    pub resource: ResourceInfo,
24    /// Optional error string (`PaymentRequired.error`).
25    pub error: Option<CompactString>,
26    /// Declared extension entries copied onto the 402 body before advertise.
27    pub extensions: Extensions,
28    /// Facilitator `/supported` snapshot (scheme enrich reads this later).
29    pub supported: SupportedResponse,
30    /// Failed payment payload, when building a 402 after a paid attempt.
31    pub payment_payload: Option<WirePaymentPayload>,
32}
33
34impl ResourceServer {
35    /// One 402 body used for both unpaid challenges and paid matching.
36    ///
37    /// After scheme enrich and extension advertise, each accept is resolved
38    /// through the registered scheme table and written with
39    /// [`apply_payment_flow_wire_extra`]. Unregistered schemes fail closed.
40    ///
41    /// # Errors
42    ///
43    /// [`FacilitatorError::Internal`] when hook policy, unregistered scheme,
44    /// or payment-flow resolution / wire extra application fails.
45    pub async fn create_payment_required_response(
46        &self,
47        accepts: Vec<PaymentRequirements>,
48        ctx: PaymentRequiredBuildContext,
49    ) -> Result<PaymentRequired, FacilitatorError> {
50        let PaymentRequiredBuildContext {
51            resource,
52            error,
53            extensions,
54            supported,
55            payment_payload,
56        } = ctx;
57        let mut response = PaymentRequired::new(resource)
58            .with_accepts(accepts)
59            .with_extensions(extensions);
60        if let Some(error) = error {
61            response = response.with_error(error);
62        }
63        self.apply_scheme_payment_required_enrich(
64            &mut response,
65            &supported,
66            payment_payload.as_ref(),
67        )
68        .await?;
69        advertise_registered_extensions(&self.extensions, &mut response).await?;
70        self.apply_payment_flow_extras(&mut response.accepts)?;
71        Ok(response)
72    }
73
74    /// Scheme-table resolve, then [`apply_payment_flow_wire_extra`].
75    fn apply_payment_flow_extras(
76        &self,
77        accepts: &mut [PaymentRequirements],
78    ) -> Result<(), FacilitatorError> {
79        for accept in accepts {
80            let resolved = self
81                .resolved_payment_flow(accept)
82                .map_err(FacilitatorError::internal)?;
83            let next = apply_payment_flow_wire_extra(accept.extra.as_ref(), &resolved);
84            accept.extra = if next.is_empty() {
85                None
86            } else {
87                Some(serde_json::Value::Object(next))
88            };
89        }
90        Ok(())
91    }
92
93    /// Per-accept scheme enrich, then additive-extra hook policy.
94    async fn apply_scheme_payment_required_enrich(
95        &self,
96        response: &mut PaymentRequired,
97        supported: &SupportedResponse,
98        payment_payload: Option<&WirePaymentPayload>,
99    ) -> Result<(), FacilitatorError> {
100        let targets: Vec<(CompactString, ChainId)> = response
101            .accepts
102            .iter()
103            .map(|accept| (accept.scheme.clone(), accept.network.clone()))
104            .collect();
105        let mut baseline = snapshot_payment_requirements_list(&response.accepts);
106        for (scheme_name, network) in targets {
107            let Some(scheme) = self.registered_scheme(scheme_name.as_str(), &network) else {
108                continue;
109            };
110            let network_label = network.to_string();
111            let enriched = {
112                let ctx = SchemePaymentRequiredContext {
113                    requirements: &response.accepts,
114                    payment_payload,
115                    resource: &response.resource,
116                    error: response.error.as_deref(),
117                    payment_required_response: response,
118                    supported,
119                    network: &network,
120                };
121                scheme.enrich_payment_required_response(&ctx).await
122            };
123            if let Some(accepts) = enriched {
124                response.accepts = accepts;
125            }
126            assert_accepts_additive_extra_after_scheme_enrich(
127                &baseline,
128                &response.accepts,
129                scheme_name.as_str(),
130                network_label.as_str(),
131            )
132            .map_err(FacilitatorError::internal)?;
133            baseline = snapshot_payment_requirements_list(&response.accepts);
134        }
135        Ok(())
136    }
137}
138
139async fn advertise_registered_extensions(
140    registry: &ExtensionRegistry,
141    response: &mut PaymentRequired,
142) -> Result<(), FacilitatorError> {
143    for ext in registry.iter() {
144        let baseline = snapshot_payment_requirements_list(&response.accepts);
145        let existing = response.extensions.get(ext.id()).cloned();
146        let ctx = AdvertiseContext::for_payment_required(
147            &response.resource,
148            &response.accepts,
149            existing.as_ref(),
150        );
151        if existing.is_none()
152            && let Some(entry) = ext.advertise(&ctx)
153        {
154            response.extensions.insert(ext.id(), entry);
155        }
156        if let Some(entry) = ext.enrich_payment_required(&ctx).await {
157            response.extensions.insert(ext.id(), entry);
158        }
159        assert_accepts_allowlisted_after_extension_enrich(&baseline, &response.accepts, ext.id())
160            .map_err(FacilitatorError::internal)?;
161    }
162    Ok(())
163}