Skip to content

Commit f9b4dde

Browse files
committed
feat: rework auth flow
1 parent df613b3 commit f9b4dde

File tree

26 files changed

+1814
-1437
lines changed

26 files changed

+1814
-1437
lines changed

.github/workflows/build.yaml

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,10 +62,15 @@ jobs:
6262
cp result-binaries/bin/x86_64-pc-windows-gnu/client.exe release-artifacts/client-x86_64-windows.exe
6363
cp result-binaries/bin/x86_64-pc-windows-gnu/mcp-client.exe release-artifacts/mcp-client-x86_64-windows.exe
6464
65-
# macOS binaries
65+
# macOS x86_64 binaries
6666
cp result-binaries/bin/x86_64-apple-darwin/server release-artifacts/server-x86_64-darwin
6767
cp result-binaries/bin/x86_64-apple-darwin/client release-artifacts/client-x86_64-darwin
6868
cp result-binaries/bin/x86_64-apple-darwin/mcp-client release-artifacts/mcp-client-x86_64-darwin
69+
70+
# macOS aarch64 binaries
71+
cp result-binaries/bin/aarch64-apple-darwin/server release-artifacts/server-aarch64-darwin
72+
cp result-binaries/bin/aarch64-apple-darwin/client release-artifacts/client-aarch64-darwin
73+
cp result-binaries/bin/aarch64-apple-darwin/mcp-client release-artifacts/mcp-client-aarch64-darwin
6974
- name: Create GitHub Release
7075
if: inputs.create-release
7176
uses: softprops/action-gh-release@v2

Cargo.lock

Lines changed: 16 additions & 11 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ toml = "0.8"
3232
anyhow = "1"
3333
thiserror = "1"
3434
# Utilities
35+
async-trait = "0.1"
3536
uuid = {version = "1", features = ["v4"]}
3637
rand = "0.8"
3738
tracing = "0.1"
@@ -40,7 +41,8 @@ dirs = "6"
4041
keyring = "3"
4142
hex = "0.4"
4243
open = "5"
44+
chrono = {version = "0.4", features = ["serde"]}
4345

4446
[workspace.package]
45-
version = "0.1.0"
47+
version = "0.1.1"
4648
edition = "2024"

client/Cargo.toml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,10 @@ tonic.workspace = true
1717
tracing.workspace = true
1818
tracing-subscriber.workspace = true
1919

20+
[lib]
21+
name = "client"
22+
path = "src/lib.rs"
23+
2024
[package]
2125
name = "client"
2226
edition.workspace = true

client/src/config.rs

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
//! Client-specific configuration
22
3-
use std::path::PathBuf;
3+
use std::path::{Path, PathBuf};
44

55
use anyhow::{Context, Result, bail};
66
use serde::{Deserialize, Serialize};
@@ -29,9 +29,14 @@ impl Config {
2929
config_path()
3030
}
3131

32-
/// Load configuration from file
32+
/// Load configuration from the default path
3333
pub fn load() -> Result<Self> {
34-
let path = Self::config_path()?;
34+
Self::load_from(Self::config_path()?)
35+
}
36+
37+
/// Load configuration from a specific path
38+
pub fn load_from<P: AsRef<Path>>(path: P) -> Result<Self> {
39+
let path = path.as_ref();
3540

3641
if !path.exists() {
3742
bail!(
@@ -40,7 +45,7 @@ impl Config {
4045
);
4146
}
4247

43-
let contents = std::fs::read_to_string(&path)
48+
let contents = std::fs::read_to_string(path)
4449
.context("Failed to read config file")?;
4550

4651
toml::from_str(&contents)

client/src/grpc.rs

Lines changed: 18 additions & 92 deletions
Original file line numberDiff line numberDiff line change
@@ -1,120 +1,46 @@
1-
use anyhow::{Context, Result};
2-
use api::{
3-
GetConfigRequest, HttpResponse,
4-
relay_service_client::RelayServiceClient,
5-
};
6-
use tokio_stream::StreamExt;
7-
use tonic::{
8-
Request,
9-
metadata::MetadataValue,
10-
transport::Channel,
11-
};
1+
use api::HttpResponse;
2+
use common::RelayClient;
123

134
use crate::proxy::Proxy;
145

156
pub struct GrpcClient {
16-
channel: Channel,
17-
access_token: String,
7+
relay_client: RelayClient,
188
}
199

2010
impl GrpcClient {
21-
pub async fn connect(server_address: &str, access_token: String) -> Result<Self> {
22-
let channel = Channel::from_shared(server_address.to_string())?
23-
.connect()
24-
.await
25-
.context("Failed to connect to server")?;
26-
27-
Ok(Self {
28-
channel,
29-
access_token,
30-
})
31-
}
32-
33-
fn create_client(&self) -> RelayServiceClient<Channel> {
34-
RelayServiceClient::new(self.channel.clone())
11+
pub async fn connect(server_address: &str, access_token: String) -> anyhow::Result<Self> {
12+
let relay_client = RelayClient::connect(server_address, access_token).await?;
13+
Ok(Self { relay_client })
3514
}
36-
37-
fn add_auth<T>(&self, request: &mut Request<T>) -> Result<()> {
38-
let token: MetadataValue<_> = format!("Bearer {}", self.access_token)
39-
.parse()
40-
.context("Invalid token format")?;
41-
request.metadata_mut().insert("authorization", token);
42-
Ok(())
43-
}
44-
45-
pub async fn get_config(&self) -> Result<api::ClientConfig> {
46-
let mut client = self.create_client();
47-
let mut request = Request::new(GetConfigRequest {});
48-
self.add_auth(&mut request)?;
49-
50-
let response = client.get_config(request).await
51-
.context("GetConfig RPC failed")?;
52-
53-
response.into_inner().config
54-
.context("Server returned empty config")
15+
16+
pub async fn get_config(&mut self) -> anyhow::Result<api::ClientConfig> {
17+
self.relay_client.get_config().await
5518
}
56-
57-
pub async fn run_webhook_stream(&self, proxy: Proxy) -> Result<()> {
58-
let mut client = self.create_client();
59-
60-
// Create channel for sending responses
61-
let (response_tx, response_rx) = tokio::sync::mpsc::channel::<HttpResponse>(32);
62-
let response_stream = tokio_stream::wrappers::ReceiverStream::new(response_rx);
63-
64-
let mut request = Request::new(response_stream);
65-
self.add_auth(&mut request)?;
66-
67-
let response = client.do_webhook(request).await
68-
.context("DoWebhook RPC failed")?;
69-
70-
let mut request_stream = response.into_inner();
71-
72-
tracing::info!("Connected to webhook stream");
73-
74-
while let Some(result) = request_stream.next().await {
75-
match result {
76-
Ok(http_request) => {
19+
20+
pub async fn run_webhook_stream(&self, proxy: Proxy) -> anyhow::Result<()> {
21+
self.relay_client
22+
.run_webhook_loop(move |http_request| {
23+
let proxy = proxy.clone();
24+
async move {
7725
let request_id = http_request.request_id.clone();
78-
tracing::info!(
79-
request_id = %request_id,
80-
method = %http_request.method,
81-
path = %http_request.path,
82-
"Received webhook request"
83-
);
84-
85-
// Forward to local endpoint
86-
let response = match proxy.forward(http_request).await {
26+
match proxy.forward(http_request).await {
8727
Ok(resp) => resp,
8828
Err(e) => {
8929
tracing::error!(
9030
request_id = %request_id,
9131
error = %e,
9232
"Failed to forward request"
9333
);
94-
// Return error response
9534
HttpResponse {
9635
request_id,
9736
status_code: 502,
9837
headers: Default::default(),
9938
body: format!("Failed to forward request: {}", e).into_bytes(),
10039
}
10140
}
102-
};
103-
104-
// Send response back to server
105-
if response_tx.send(response).await.is_err() {
106-
tracing::error!("Failed to send response to server stream");
107-
break;
10841
}
10942
}
110-
Err(e) => {
111-
tracing::error!(error = %e, "Error receiving from server");
112-
break;
113-
}
114-
}
115-
}
116-
117-
tracing::info!("Webhook stream ended");
118-
Ok(())
43+
})
44+
.await
11945
}
12046
}

client/src/lib.rs

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
//! Webhook relay client library
2+
//!
3+
//! This module provides the core client functionality for connecting to a webhook
4+
//! relay server and forwarding requests to a local endpoint.
5+
6+
mod grpc;
7+
mod proxy;
8+
9+
pub use common::AuthProvider;
10+
pub use proxy::Proxy;
11+
12+
use std::sync::Arc;
13+
14+
use anyhow::Result;
15+
use grpc::GrpcClient;
16+
17+
/// Configuration for running the client
18+
pub struct ClientConfig<A: AuthProvider> {
19+
/// gRPC server address (e.g., "http://localhost:50051")
20+
pub server_address: String,
21+
/// Authentication provider for obtaining access tokens
22+
pub auth_provider: A,
23+
/// Local endpoint to forward webhooks to (e.g., "http://localhost:3000")
24+
pub local_endpoint: String,
25+
}
26+
27+
/// Result of starting the client - contains the endpoint URL and a handle to stop it
28+
pub struct ClientHandle {
29+
/// The webhook endpoint URL assigned by the server
30+
pub endpoint: String,
31+
/// Handle to abort the client task
32+
abort_handle: tokio::task::AbortHandle,
33+
}
34+
35+
impl ClientHandle {
36+
/// Stop the client
37+
pub fn stop(&self) {
38+
self.abort_handle.abort();
39+
}
40+
}
41+
42+
/// Start the webhook relay client.
43+
///
44+
/// This connects to the relay server, gets the assigned endpoint, and starts
45+
/// forwarding webhooks to the local endpoint in the background.
46+
///
47+
/// Returns a handle containing the endpoint URL and a way to stop the client.
48+
pub async fn run_client<A: AuthProvider + 'static>(config: ClientConfig<A>) -> Result<ClientHandle> {
49+
let auth_provider = Arc::new(config.auth_provider);
50+
51+
// Get initial access token
52+
let access_token = auth_provider.get_access_token().await?;
53+
54+
// Connect to server
55+
let mut grpc_client = GrpcClient::connect(&config.server_address, access_token).await?;
56+
57+
// Get config (establishes session, returns endpoint)
58+
let client_config = grpc_client.get_config().await?;
59+
let endpoint = client_config.endpoint.clone();
60+
61+
tracing::info!(endpoint = %endpoint, "Client connected, starting webhook stream");
62+
63+
// Create proxy
64+
let proxy = Proxy::new(config.local_endpoint);
65+
66+
// Spawn the webhook stream handler
67+
let join_handle = tokio::spawn(async move {
68+
if let Err(e) = grpc_client.run_webhook_stream(proxy).await {
69+
tracing::error!(error = %e, "Webhook stream error");
70+
}
71+
});
72+
73+
Ok(ClientHandle {
74+
endpoint,
75+
abort_handle: join_handle.abort_handle(),
76+
})
77+
}

0 commit comments

Comments
 (0)