Skip to main content

termivar_scanner/web_runtime/
api_visibility.rs

1//! Runtime-owned facade for authorized, host-paired API visibility evidence.
2
3use termivar_core::{ApiVisibilityObservation, EntityId};
4use thiserror::Error;
5
6use super::StandardWebDecisionRuntime;
7use crate::{
8    api_visibility_reviews_for_resource, ingest_api_visibility_observation,
9    ApiObservationCommitReceipt, ApiObservationError, ApiObservationReceipt,
10    ApiVisibilityReviewPage, ApiVisibilityReviewQuery,
11};
12
13mod differential;
14
15pub use differential::{
16    ApiVisibilityContextProbe, ApiVisibilityDifferentialAudit,
17    ApiVisibilityDifferentialDisposition, ApiVisibilityDifferentialRequest,
18    ApiVisibilityDifferentialRequestError, ApiVisibilityInconclusiveReason, ApiVisibilityLeg,
19    ApiVisibilityLegReceipt, RuntimeApiVisibilityExecutionError, RuntimeApiVisibilityRunReport,
20};
21
22/// Failure while using paired API visibility reasoning through a web runtime.
23///
24/// The wrapper preserves the standalone observation boundary's post-commit
25/// receipt. A failed return value therefore does not imply that an observation
26/// was rolled back.
27#[derive(Debug, Error)]
28#[non_exhaustive]
29pub enum RuntimeApiVisibilityError {
30    /// The runtime was built without API reasoning enabled.
31    #[error("API visibility reasoning is disabled for this runtime")]
32    ApiReasoningDisabled,
33
34    /// Observation validation, storage, or reasoning failed.
35    #[error(transparent)]
36    Observation(#[from] ApiObservationError),
37}
38
39impl RuntimeApiVisibilityError {
40    /// Returns the observation committed before a later reasoning failure.
41    pub fn committed_observation(&self) -> Option<&ApiObservationCommitReceipt> {
42        match self {
43            Self::Observation(source) => source.committed_observation(),
44            Self::ApiReasoningDisabled => None,
45        }
46    }
47
48    /// Takes ownership of an observation committed before a later failure.
49    pub fn into_committed_observation(self) -> Option<ApiObservationCommitReceipt> {
50        match self {
51            Self::Observation(source) => source.into_committed_observation(),
52            Self::ApiReasoningDisabled => None,
53        }
54    }
55}
56
57impl StandardWebDecisionRuntime {
58    /// Commits one authorized, host-paired API visibility observation.
59    ///
60    /// The runtime must have API reasoning enabled. Disabled runtimes reject
61    /// the call before any write. The host remains responsible for authenticating
62    /// the producer, authorizing both compared contexts, and asserting that
63    /// `expected_resource` is the same logical resource in both views.
64    ///
65    /// This boundary accepts no raw response, URL, header, credential, or
66    /// principal. It preserves the observation's isolated comparison subject
67    /// and never rewrites it to the runtime endpoint subject. Ingestion performs
68    /// no request and does not change runtime usage, experience, planning, or
69    /// decision-session state. Exact replay remains idempotent.
70    ///
71    /// # Examples
72    ///
73    /// ```rust
74    /// use url::Url;
75    /// use termivar_scanner::{
76    ///     ApiSurfaceKind, ApiVisibilityComparison, ApiVisibilityDimension,
77    ///     ApiVisibilityPairKind, ApiVisibilityResult, ApiVisibilityReviewQuery,
78    ///     ConfidenceScore, EntityId, StandardWebDecisionRuntime,
79    /// };
80    ///
81    /// let resource = EntityId::new("resource:account-42")?;
82    /// let observation = ApiVisibilityComparison::new(
83    ///     "comparison-17",
84    ///     ApiSurfaceKind::JsonHttp,
85    ///     ApiVisibilityPairKind::AuthorizationContext,
86    ///     ApiVisibilityResult::Different,
87    ///     ApiVisibilityDimension::Fields,
88    ///     "anonymous-view",
89    ///     "member-view",
90    ///     resource.as_str(),
91    /// )?
92    /// .to_observation("host.api-comparator", ConfidenceScore::MAX)?;
93    /// let mut runtime = StandardWebDecisionRuntime::builder(
94    ///     Url::parse("https://example.test/api/accounts/42")?,
95    /// )
96    /// .enable_api_reasoning()
97    /// .build()?;
98    ///
99    /// runtime.ingest_api_visibility(observation, &resource)?;
100    /// let page = runtime.api_visibility_reviews(
101    ///     &resource,
102    ///     &ApiVisibilityReviewQuery::default(),
103    /// )?;
104    /// assert_eq!(page.reviews().len(), 1);
105    /// # Ok::<(), Box<dyn std::error::Error>>(())
106    /// ```
107    pub fn ingest_api_visibility(
108        &mut self,
109        observation: ApiVisibilityObservation,
110        expected_resource: &EntityId,
111    ) -> Result<ApiObservationReceipt, RuntimeApiVisibilityError> {
112        if self.api_reasoning_installation.is_none() {
113            return Err(RuntimeApiVisibilityError::ApiReasoningDisabled);
114        }
115
116        ingest_api_visibility_observation(
117            observation,
118            expected_resource,
119            self.authority.knowledge(),
120            self.decision_loop.rules(),
121        )
122        .map_err(Into::into)
123    }
124
125    /// Returns one cursor-bounded page of paired-visibility reviews.
126    ///
127    /// Review projection is available only when API reasoning was explicitly
128    /// enabled. It reads the runtime's comparison subjects without feeding
129    /// their hypotheses into the endpoint planner or decision session.
130    pub fn api_visibility_reviews(
131        &self,
132        resource: &EntityId,
133        query: &ApiVisibilityReviewQuery,
134    ) -> Result<ApiVisibilityReviewPage, RuntimeApiVisibilityError> {
135        if self.api_reasoning_installation.is_none() {
136            return Err(RuntimeApiVisibilityError::ApiReasoningDisabled);
137        }
138
139        Ok(api_visibility_reviews_for_resource(
140            self.authority.knowledge(),
141            resource,
142            query,
143        ))
144    }
145}
146
147#[cfg(test)]
148mod tests;