systemprompt-cloud 0.8.0

Cloud API client, credentials, OAuth, and tenant management for systemprompt.io AI governance deployments. Remote sync and multi-tenant orchestration for the MCP governance pipeline.
Documentation
//! `CloudApiClient` constructor + accessors. Lower-level HTTP verbs
//! live in `methods.rs`; high-level endpoints in `endpoints.rs`.

use reqwest::{Client, StatusCode};
use serde::de::DeserializeOwned;
use systemprompt_models::net::{HTTP_CONNECT_TIMEOUT, HTTP_DEFAULT_TIMEOUT};

use super::types::ApiError;
use crate::error::{CloudError, CloudResult};

#[derive(Debug)]
pub struct CloudApiClient {
    pub(super) client: Client,
    pub(super) api_url: String,
    pub(super) token: String,
}

impl CloudApiClient {
    pub fn new(api_url: &str, token: &str) -> Result<Self, reqwest::Error> {
        Ok(Self {
            client: Client::builder()
                .connect_timeout(HTTP_CONNECT_TIMEOUT)
                .timeout(HTTP_DEFAULT_TIMEOUT)
                .build()?,
            api_url: api_url.to_string(),
            token: token.to_string(),
        })
    }

    #[must_use]
    pub fn api_url(&self) -> &str {
        &self.api_url
    }

    #[must_use]
    pub fn token(&self) -> &str {
        &self.token
    }

    pub(super) async fn handle_response<T: DeserializeOwned>(
        &self,
        response: reqwest::Response,
    ) -> CloudResult<T> {
        let status = response.status();

        if status == StatusCode::UNAUTHORIZED {
            return Err(CloudError::Unauthorized);
        }

        if !status.is_success() {
            return Err(parse_error_response(status, response).await);
        }

        response.json().await.map_err(CloudError::from)
    }

    pub(super) async fn handle_no_content_response(
        &self,
        response: reqwest::Response,
    ) -> CloudResult<()> {
        let status = response.status();
        if status == StatusCode::UNAUTHORIZED {
            return Err(CloudError::Unauthorized);
        }
        if status == StatusCode::NO_CONTENT || status.is_success() {
            return Ok(());
        }
        Err(parse_error_response(status, response).await)
    }
}

pub(super) async fn parse_error_response(
    status: StatusCode,
    response: reqwest::Response,
) -> CloudError {
    let error_text = match response.text().await {
        Ok(t) => t,
        Err(e) => {
            tracing::warn!(error = %e, "Failed to read error response body");
            String::from("<failed to read response body>")
        },
    };

    serde_json::from_str::<ApiError>(&error_text).map_or_else(
        |_| CloudError::HttpStatus {
            status: status.as_u16(),
            body: error_text.chars().take(500).collect(),
        },
        |parsed| CloudError::ApiError {
            message: format!("{}: {}", parsed.error.code, parsed.error.message),
        },
    )
}