From bc6bb427456a865ce3c2efa3d3deb9b103295b23 Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Wed, 30 Jul 2025 14:57:51 -0400 Subject: [PATCH] Add `cloud_api_client` and `cloud_api_types` crates (#35357) This PR adds two new crates for interacting with Cloud: - `cloud_api_client` - The client that will be used to talk to Cloud. - `cloud_api_types` - The types for the Cloud API that are shared between Zed and Cloud. Release Notes: - N/A --- Cargo.lock | 22 ++++++ Cargo.toml | 4 + crates/client/Cargo.toml | 1 + crates/client/src/client.rs | 5 ++ crates/cloud_api_client/Cargo.toml | 21 +++++ crates/cloud_api_client/LICENSE-APACHE | 1 + .../cloud_api_client/src/cloud_api_client.rs | 76 +++++++++++++++++++ crates/cloud_api_types/Cargo.toml | 16 ++++ crates/cloud_api_types/LICENSE-APACHE | 1 + crates/cloud_api_types/src/cloud_api_types.rs | 14 ++++ crates/http_client/src/http_client.rs | 16 ++++ 11 files changed, 177 insertions(+) create mode 100644 crates/cloud_api_client/Cargo.toml create mode 120000 crates/cloud_api_client/LICENSE-APACHE create mode 100644 crates/cloud_api_client/src/cloud_api_client.rs create mode 100644 crates/cloud_api_types/Cargo.toml create mode 120000 crates/cloud_api_types/LICENSE-APACHE create mode 100644 crates/cloud_api_types/src/cloud_api_types.rs diff --git a/Cargo.lock b/Cargo.lock index 9ddd12a876..61553e7799 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2976,6 +2976,7 @@ dependencies = [ "base64 0.22.1", "chrono", "clock", + "cloud_api_client", "cloud_llm_client", "cocoa 0.26.0", "collections", @@ -3031,6 +3032,27 @@ dependencies = [ "workspace-hack", ] +[[package]] +name = "cloud_api_client" +version = "0.1.0" +dependencies = [ + "anyhow", + "cloud_api_types", + "futures 0.3.31", + "http_client", + "parking_lot", + "serde_json", + "workspace-hack", +] + +[[package]] +name = "cloud_api_types" +version = "0.1.0" +dependencies = [ + "serde", + "workspace-hack", +] + [[package]] name = "cloud_llm_client" version = "0.1.0" diff --git a/Cargo.toml b/Cargo.toml index a6428d897b..cf1ee5956f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -29,6 +29,8 @@ members = [ "crates/cli", "crates/client", "crates/clock", + "crates/cloud_api_client", + "crates/cloud_api_types", "crates/cloud_llm_client", "crates/collab", "crates/collab_ui", @@ -251,6 +253,8 @@ channel = { path = "crates/channel" } cli = { path = "crates/cli" } client = { path = "crates/client" } clock = { path = "crates/clock" } +cloud_api_client = { path = "crates/cloud_api_client" } +cloud_api_types = { path = "crates/cloud_api_types" } cloud_llm_client = { path = "crates/cloud_llm_client" } collab = { path = "crates/collab" } collab_ui = { path = "crates/collab_ui" } diff --git a/crates/client/Cargo.toml b/crates/client/Cargo.toml index dd97bd9ca4..3ff03114ea 100644 --- a/crates/client/Cargo.toml +++ b/crates/client/Cargo.toml @@ -22,6 +22,7 @@ async-tungstenite = { workspace = true, features = ["tokio", "tokio-rustls-manua base64.workspace = true chrono = { workspace = true, features = ["serde"] } clock.workspace = true +cloud_api_client.workspace = true cloud_llm_client.workspace = true collections.workspace = true credentials_provider.workspace = true diff --git a/crates/client/src/client.rs b/crates/client/src/client.rs index 07e708f11b..1b6ce70f3a 100644 --- a/crates/client/src/client.rs +++ b/crates/client/src/client.rs @@ -15,6 +15,7 @@ use async_tungstenite::tungstenite::{ }; use chrono::{DateTime, Utc}; use clock::SystemClock; +use cloud_api_client::CloudApiClient; use credentials_provider::CredentialsProvider; use futures::{ AsyncReadExt, FutureExt, SinkExt, Stream, StreamExt, TryFutureExt as _, TryStreamExt, @@ -213,6 +214,7 @@ pub struct Client { id: AtomicU64, peer: Arc, http: Arc, + cloud_client: Arc, telemetry: Arc, credentials_provider: ClientCredentialsProvider, state: RwLock, @@ -586,6 +588,7 @@ impl Client { id: AtomicU64::new(0), peer: Peer::new(0), telemetry: Telemetry::new(clock, http.clone(), cx), + cloud_client: Arc::new(CloudApiClient::new(http.clone())), http, credentials_provider: ClientCredentialsProvider::new(cx), state: Default::default(), @@ -930,6 +933,8 @@ impl Client { } let credentials = credentials.unwrap(); self.set_id(credentials.user_id); + self.cloud_client + .set_credentials(credentials.user_id as u32, credentials.access_token.clone()); if was_disconnected { self.set_status(Status::Connecting, cx); diff --git a/crates/cloud_api_client/Cargo.toml b/crates/cloud_api_client/Cargo.toml new file mode 100644 index 0000000000..d56aa94c6e --- /dev/null +++ b/crates/cloud_api_client/Cargo.toml @@ -0,0 +1,21 @@ +[package] +name = "cloud_api_client" +version = "0.1.0" +edition.workspace = true +publish.workspace = true +license = "Apache-2.0" + +[lints] +workspace = true + +[lib] +path = "src/cloud_api_client.rs" + +[dependencies] +anyhow.workspace = true +cloud_api_types.workspace = true +futures.workspace = true +http_client.workspace = true +parking_lot.workspace = true +serde_json.workspace = true +workspace-hack.workspace = true diff --git a/crates/cloud_api_client/LICENSE-APACHE b/crates/cloud_api_client/LICENSE-APACHE new file mode 120000 index 0000000000..1cd601d0a3 --- /dev/null +++ b/crates/cloud_api_client/LICENSE-APACHE @@ -0,0 +1 @@ +../../LICENSE-APACHE \ No newline at end of file diff --git a/crates/cloud_api_client/src/cloud_api_client.rs b/crates/cloud_api_client/src/cloud_api_client.rs new file mode 100644 index 0000000000..b11e954468 --- /dev/null +++ b/crates/cloud_api_client/src/cloud_api_client.rs @@ -0,0 +1,76 @@ +use std::sync::Arc; + +use anyhow::{Result, anyhow}; +pub use cloud_api_types::*; +use futures::AsyncReadExt as _; +use http_client::{AsyncBody, HttpClientWithUrl, Method, Request}; +use parking_lot::RwLock; + +struct Credentials { + user_id: u32, + access_token: String, +} + +pub struct CloudApiClient { + credentials: RwLock>, + http_client: Arc, +} + +impl CloudApiClient { + pub fn new(http_client: Arc) -> Self { + Self { + credentials: RwLock::new(None), + http_client, + } + } + + pub fn set_credentials(&self, user_id: u32, access_token: String) { + *self.credentials.write() = Some(Credentials { + user_id, + access_token, + }); + } + + fn authorization_header(&self) -> Result { + let guard = self.credentials.read(); + let credentials = guard + .as_ref() + .ok_or_else(|| anyhow!("No credentials provided"))?; + + Ok(format!( + "{} {}", + credentials.user_id, credentials.access_token + )) + } + + pub async fn get_authenticated_user(&self) -> Result { + let request = Request::builder() + .method(Method::GET) + .uri( + self.http_client + .build_zed_cloud_url("/client/users/me", &[])? + .as_ref(), + ) + .header("Content-Type", "application/json") + .header("Authorization", self.authorization_header()?) + .body(AsyncBody::default())?; + + let mut response = self.http_client.send(request).await?; + + if !response.status().is_success() { + let mut body = String::new(); + response.body_mut().read_to_string(&mut body).await?; + + anyhow::bail!( + "Failed to get authenticated user.\nStatus: {:?}\nBody: {body}", + response.status() + ) + } + + let mut body = String::new(); + response.body_mut().read_to_string(&mut body).await?; + let response: GetAuthenticatedUserResponse = serde_json::from_str(&body)?; + + Ok(response.user) + } +} diff --git a/crates/cloud_api_types/Cargo.toml b/crates/cloud_api_types/Cargo.toml new file mode 100644 index 0000000000..0fe0b1fd6a --- /dev/null +++ b/crates/cloud_api_types/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "cloud_api_types" +version = "0.1.0" +edition.workspace = true +publish.workspace = true +license = "Apache-2.0" + +[lints] +workspace = true + +[lib] +path = "src/cloud_api_types.rs" + +[dependencies] +serde.workspace = true +workspace-hack.workspace = true diff --git a/crates/cloud_api_types/LICENSE-APACHE b/crates/cloud_api_types/LICENSE-APACHE new file mode 120000 index 0000000000..1cd601d0a3 --- /dev/null +++ b/crates/cloud_api_types/LICENSE-APACHE @@ -0,0 +1 @@ +../../LICENSE-APACHE \ No newline at end of file diff --git a/crates/cloud_api_types/src/cloud_api_types.rs b/crates/cloud_api_types/src/cloud_api_types.rs new file mode 100644 index 0000000000..5c9ca7893c --- /dev/null +++ b/crates/cloud_api_types/src/cloud_api_types.rs @@ -0,0 +1,14 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Debug, PartialEq, Serialize, Deserialize)] +pub struct GetAuthenticatedUserResponse { + pub user: AuthenticatedUser, +} + +#[derive(Debug, PartialEq, Serialize, Deserialize)] +pub struct AuthenticatedUser { + pub id: i32, + pub avatar_url: String, + pub github_login: String, + pub name: Option, +} diff --git a/crates/http_client/src/http_client.rs b/crates/http_client/src/http_client.rs index 434bd74fc8..06875718d9 100644 --- a/crates/http_client/src/http_client.rs +++ b/crates/http_client/src/http_client.rs @@ -236,6 +236,22 @@ impl HttpClientWithUrl { )?) } + /// Builds a Zed Cloud URL using the given path. + pub fn build_zed_cloud_url(&self, path: &str, query: &[(&str, &str)]) -> Result { + let base_url = self.base_url(); + let base_api_url = match base_url.as_ref() { + "https://zed.dev" => "https://cloud.zed.dev", + "https://staging.zed.dev" => "https://cloud.zed.dev", + "http://localhost:3000" => "http://localhost:8787", + other => other, + }; + + Ok(Url::parse_with_params( + &format!("{}{}", base_api_url, path), + query, + )?) + } + /// Builds a Zed LLM URL using the given path. pub fn build_zed_llm_url(&self, path: &str, query: &[(&str, &str)]) -> Result { let base_url = self.base_url();