Skip to main content

git_policy_cli/
standards_review_client.rs

1//! What:
2//!  The one HTTPS call of the standards-review shadow run, made in-process with a total deadline.
3//! Why:
4//!  The root `mise.toml` task `shadow-review` calls `fetch` with a 30 s signal. This port calls the same endpoint from
5//!  the wrapper itself, so no `node` or `mise` is started. The deadline covers connecting, TLS, sending and reading
6//!  the body, so the call can never hold a caller longer than the deadline. The trust roots are a parameter: production
7//!  uses the roots embedded in the binary, a test passes the fixture authority of its local TLS server.
8//!
9//! In TS you'd write (pseudocode):
10//! ```ts
11//! const response = await fetch(url, { method: 'POST', headers, body, signal: AbortSignal.timeout(30000) });
12//! ```
13
14use std::time::Duration;
15use ureq::tls::{Certificate, RootCerts, TlsConfig};
16
17/// The script's timeout.
18pub const SCRIPT_DEADLINE: Duration = Duration::from_secs(30);
19
20/// What:
21///  Where and how to call: the URL, the bearer key, the deadline, and optionally the one trusted authority (PEM).
22/// Why:
23///   Tests point `url` at a loopback server; production passes `ENDPOINT`, the script's deadline and no authority.
24///
25/// In TS you'd write (pseudocode):
26/// ```ts
27/// type Gateway = { url: string; apiKey: string; deadline: number; trustedPem?: Uint8Array };
28/// ```
29#[derive(Clone, Copy, Debug)]
30pub struct Gateway<'a> {
31    /// The decisions endpoint.
32    pub url: &'a str,
33    /// The bearer credential.
34    pub api_key: &'a str,
35    /// The total time the call may take.
36    pub deadline: Duration,
37    /// A PEM authority that replaces the embedded roots; `None` keeps them.
38    pub trusted_pem: Option<&'a [u8]>,
39}
40
41/// What:
42///  The gateway's status and body text.
43/// Why:
44///   The answer reading decides what a status means; the client only transports.
45///
46/// In TS you'd write (pseudocode):
47/// ```ts
48/// type Reply = { status: number; body: string };
49/// ```
50#[derive(Clone, Debug, Eq, PartialEq)]
51pub struct Reply {
52    /// HTTP status code.
53    pub status: u16,
54    /// Response body as text.
55    pub body: String,
56}
57
58/// What:
59///  POST `body` as JSON with the bearer key, within the deadline; any HTTP status is a reply, not an error.
60///  Redirects are not followed and no proxy variable is read.
61/// Why:
62///   The script's failure line names timeouts and transport failures; a redirect or a proxy would send the key and the
63///   repository text somewhere the endpoint constant does not name.
64///
65/// In TS you'd write (pseudocode):
66/// ```ts
67/// const response = await fetch(url, { method: 'POST', headers: { 'content-type': 'application/json', authorization: 'Bearer ' + key }, body, signal });
68/// ```
69pub fn post_decision(gateway: &Gateway, body: &str) -> Result<Reply, String> {
70    let tls: TlsConfig = match gateway.trusted_pem {
71        Some(pem) => {
72            let authority: Certificate<'static> = Certificate::from_pem(pem).map_err(|error| return format!("the trusted authority is unreadable: {error}"))?;
73            TlsConfig::builder().root_certs(RootCerts::new_with_certs(&[authority])).build()
74        }
75        None => TlsConfig::default(),
76    };
77    let agent: ureq::Agent = ureq::Agent::config_builder()
78        .timeout_global(Some(gateway.deadline))
79        .http_status_as_error(false)
80        .max_redirects(0)
81        .proxy(None)
82        .tls_config(tls)
83        .build()
84        .into();
85    let mut response = agent
86        .post(gateway.url)
87        .header("content-type", "application/json")
88        .header("authorization", format!("Bearer {}", gateway.api_key))
89        .send(body.as_bytes())
90        .map_err(describe)?;
91    let status: u16 = response.status().as_u16();
92    let text: String = response.body_mut().read_to_string().map_err(describe)?;
93    return Ok(Reply { status, body: text });
94}
95
96/// What:
97///  A failure as the log line prints it: the script's timeout wording, otherwise `fetch failed` and the cause.
98/// Why:
99///   Node prints only `fetch failed` and hides the cause; the cause is what a person needs.
100///
101/// In TS you'd write (pseudocode):
102/// ```ts
103/// error instanceof Error ? error.message : String(error)
104/// ```
105fn describe(error: ureq::Error) -> String {
106    return match error {
107        ureq::Error::Timeout(_) => String::from("The operation was aborted due to timeout"),
108        other => format!("fetch failed: {other}"),
109    };
110}
111
112/// Controls stay out of the release executable.
113#[cfg(test)]
114#[path = "standards_review_client_tests.rs"]
115mod tests;