bctx-cloud-core 0.1.39

bctx-cloud-core — cloud client and server for Vault sync, dashboard API, billing
Documentation
#[cfg(not(feature = "cloud-server"))]
use anyhow::bail;
use anyhow::Result;
use serde::{Deserialize, Serialize};
use vault::fact::MemoFact;

#[derive(Debug, Serialize, Deserialize)]
pub struct SyncPushPayload {
    pub project_hash: String,
    pub facts: Vec<MemoFact>,
    pub client_version: String,
}

#[derive(Debug, Serialize, Deserialize)]
pub struct SyncPullResponse {
    pub facts: Vec<MemoFact>,
    pub server_version: u64,
}

#[derive(Debug, Serialize, Deserialize)]
pub struct SyncPushResponse {
    pub accepted: usize,
    pub merged: usize,
    pub server_version: u64,
}

/// Push CrystallizedKnowledge facts to the cloud for the given project.
#[cfg(feature = "cloud-server")]
pub async fn push_vault(
    endpoint: &str,
    token: &str,
    project_hash: &str,
    facts: Vec<MemoFact>,
) -> Result<SyncPushResponse> {
    let payload = SyncPushPayload {
        project_hash: project_hash.to_string(),
        facts,
        client_version: env!("CARGO_PKG_VERSION").to_string(),
    };
    let resp = reqwest::Client::builder()
        .timeout(std::time::Duration::from_secs(15))
        .connect_timeout(std::time::Duration::from_secs(8))
        .build()?
        .post(format!("{endpoint}/sync/vault"))
        .bearer_auth(token)
        .json(&payload)
        .send()
        .await?
        .error_for_status()?
        .json::<SyncPushResponse>()
        .await?;
    Ok(resp)
}

#[cfg(not(feature = "cloud-server"))]
pub async fn push_vault(
    _endpoint: &str,
    _token: &str,
    _project_hash: &str,
    _facts: Vec<MemoFact>,
) -> Result<SyncPushResponse> {
    bail!("cloud-server feature not enabled")
}

/// Pull CrystallizedKnowledge facts from the cloud for the given project.
#[cfg(feature = "cloud-server")]
pub async fn pull_vault(
    endpoint: &str,
    token: &str,
    project_hash: &str,
) -> Result<SyncPullResponse> {
    let resp = reqwest::Client::builder()
        .timeout(std::time::Duration::from_secs(15))
        .connect_timeout(std::time::Duration::from_secs(8))
        .build()?
        .get(format!("{endpoint}/sync/vault/{project_hash}"))
        .bearer_auth(token)
        .send()
        .await?
        .error_for_status()?
        .json::<SyncPullResponse>()
        .await?;
    Ok(resp)
}

#[cfg(not(feature = "cloud-server"))]
pub async fn pull_vault(
    _endpoint: &str,
    _token: &str,
    _project_hash: &str,
) -> Result<SyncPullResponse> {
    bail!("cloud-server feature not enabled")
}

/// Sends one daily usage snapshot. Blocking with short timeouts. Called only from the
/// detached `bctx sync --telemetry` process, never from a command a user is waiting on.
#[cfg(feature = "cloud-server")]
pub fn push_usage_snapshot(
    endpoint: &str,
    token: &str,
    snapshot: &crate::usage::UsageSnapshot,
) -> anyhow::Result<()> {
    let rt = tokio::runtime::Builder::new_current_thread()
        .enable_all()
        .build()?;
    rt.block_on(async {
        let client = reqwest::Client::builder()
            .timeout(std::time::Duration::from_secs(10))
            .connect_timeout(std::time::Duration::from_secs(5))
            .build()?;
        let resp: serde_json::Value = client
            .post(format!("{endpoint}/sync/usage"))
            .bearer_auth(token)
            .json(snapshot)
            .send()
            .await?
            .error_for_status()?
            .json()
            .await?;
        if resp["ok"] == serde_json::json!(true) {
            Ok(())
        } else {
            anyhow::bail!("snapshot rejected: {}", resp["reason"])
        }
    })
}

#[cfg(not(feature = "cloud-server"))]
pub fn push_usage_snapshot(
    _endpoint: &str,
    _token: &str,
    _snapshot: &crate::usage::UsageSnapshot,
) -> anyhow::Result<()> {
    bail!("cloud-server feature not enabled")
}

#[cfg(all(test, not(feature = "cloud-server")))]
mod stub_tests {
    #[test]
    fn without_the_cloud_client_a_snapshot_is_never_reported_as_sent() {
        let snapshot = crate::usage::UsageSnapshot {
            device_id: "11111111-1111-4111-8111-111111111111".into(),
            client_version: "test".into(),
            rows: vec![],
        };
        assert!(super::push_usage_snapshot("http://127.0.0.1:9", "t", &snapshot).is_err());
    }
}