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;