Detailed changes
@@ -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"
@@ -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" }
@@ -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
@@ -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<Peer>,
http: Arc<HttpClientWithUrl>,
+ cloud_client: Arc<CloudApiClient>,
telemetry: Arc<Telemetry>,
credentials_provider: ClientCredentialsProvider,
state: RwLock<ClientState>,
@@ -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);
@@ -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
@@ -0,0 +1 @@
+../../LICENSE-APACHE
@@ -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<Option<Credentials>>,
+ http_client: Arc<HttpClientWithUrl>,
+}
+
+impl CloudApiClient {
+ pub fn new(http_client: Arc<HttpClientWithUrl>) -> 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<String> {
+ 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<AuthenticatedUser> {
+ 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)
+ }
+}
@@ -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
@@ -0,0 +1 @@
+../../LICENSE-APACHE
@@ -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<String>,
+}
@@ -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<Url> {
+ 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<Url> {
let base_url = self.base_url();