diff --git a/bin/fxt/src/grpc.rs b/bin/fxt/src/grpc.rs index 0c455a3a9..b1652a529 100644 --- a/bin/fxt/src/grpc.rs +++ b/bin/fxt/src/grpc.rs @@ -3,24 +3,37 @@ //! All service RPCs route through a single [`FoxhuntClient`] which holds //! one lazily-connected [`Channel`] to the API Gateway. Individual typed //! clients (trading, ML, risk, ...) are constructed on demand from the -//! shared channel. +//! shared channel, each wrapped with [`AuthInterceptor`] so every outgoing +//! RPC carries the JWT Bearer token when available. use anyhow::Result; +use tonic::service::interceptor::InterceptedService; use tonic::transport::Channel; -/// Unified gRPC client -- all service accessors share a single channel. +use crate::auth::interceptor::AuthInterceptor; +use crate::auth::token_manager::{AuthTokenManager, FileTokenStorage}; + +/// Channel wrapped with the JWT auth interceptor. +type AuthChannel = InterceptedService>; + +/// Unified gRPC client -- all service accessors share a single channel +/// and automatically inject JWT auth headers via [`AuthInterceptor`]. pub struct FoxhuntClient { channel: Channel, + interceptor: AuthInterceptor, } impl FoxhuntClient { /// Create a client that will lazily connect to the API Gateway. /// + /// Sets up the auth interceptor backed by [`FileTokenStorage`] so all + /// outgoing RPCs include the Bearer token when one is cached on disk. + /// /// The TCP handshake is deferred until the first RPC call (`connect_lazy`). /// /// # Errors /// - /// Returns an error if the URL cannot be parsed as a valid URI. + /// Returns an error if the URL cannot be parsed or token storage init fails. pub async fn connect(api_url: &str) -> Result { let mut endpoint = Channel::from_shared(api_url.to_owned()).map_err(|e| anyhow::anyhow!("{e}"))?; @@ -33,10 +46,24 @@ impl FoxhuntClient { } let channel = endpoint.connect_lazy(); - Ok(Self { channel }) + + // Auth interceptor: reads cached JWT from ~/.config/foxhunt-tli/tokens/ + // and injects "Authorization: Bearer " on every RPC. + // If no token is cached, requests proceed without auth (allows login). + let storage = FileTokenStorage::new()?; + let auth_manager = AuthTokenManager::new(storage); + let interceptor = AuthInterceptor::new(auth_manager); + + Ok(Self { + channel, + interceptor, + }) } - /// Get the underlying channel for creating typed service clients. + /// Get the underlying bare channel (no auth interceptor). + /// + /// Used by [`LoginClient`] which needs unauthenticated access for the + /// initial login RPC before any token exists. pub fn channel(&self) -> Channel { self.channel.clone() } @@ -46,83 +73,107 @@ impl FoxhuntClient { /// Trading service client (order management, positions, executions). pub fn trading( &self, - ) -> crate::proto::trading::trading_service_client::TradingServiceClient { - crate::proto::trading::trading_service_client::TradingServiceClient::new( + ) -> crate::proto::trading::trading_service_client::TradingServiceClient { + crate::proto::trading::trading_service_client::TradingServiceClient::with_interceptor( self.channel.clone(), + self.interceptor.clone(), ) } /// ML inference service client (predictions, ensemble voting). - pub fn ml(&self) -> crate::proto::ml::ml_service_client::MlServiceClient { - crate::proto::ml::ml_service_client::MlServiceClient::new(self.channel.clone()) + pub fn ml(&self) -> crate::proto::ml::ml_service_client::MlServiceClient { + crate::proto::ml::ml_service_client::MlServiceClient::with_interceptor( + self.channel.clone(), + self.interceptor.clone(), + ) } /// ML training service client (start/stop training, checkpoints). pub fn ml_training( &self, - ) -> crate::proto::ml_training::ml_training_service_client::MlTrainingServiceClient + ) -> crate::proto::ml_training::ml_training_service_client::MlTrainingServiceClient { - crate::proto::ml_training::ml_training_service_client::MlTrainingServiceClient::new( + crate::proto::ml_training::ml_training_service_client::MlTrainingServiceClient::with_interceptor( self.channel.clone(), + self.interceptor.clone(), ) } /// Broker gateway service client (account info, order routing). pub fn broker_gateway( &self, - ) -> crate::proto::broker_gateway::broker_gateway_service_client::BrokerGatewayServiceClient + ) -> crate::proto::broker_gateway::broker_gateway_service_client::BrokerGatewayServiceClient { - crate::proto::broker_gateway::broker_gateway_service_client::BrokerGatewayServiceClient::new( + crate::proto::broker_gateway::broker_gateway_service_client::BrokerGatewayServiceClient::with_interceptor( self.channel.clone(), + self.interceptor.clone(), ) } /// Trading agent service client (autonomous agent control). pub fn trading_agent( &self, - ) -> crate::proto::trading_agent::trading_agent_service_client::TradingAgentServiceClient + ) -> crate::proto::trading_agent::trading_agent_service_client::TradingAgentServiceClient { - crate::proto::trading_agent::trading_agent_service_client::TradingAgentServiceClient::new( + crate::proto::trading_agent::trading_agent_service_client::TradingAgentServiceClient::with_interceptor( self.channel.clone(), + self.interceptor.clone(), ) } /// Monitoring service client (metrics, alerts, system health). pub fn monitoring( &self, - ) -> crate::proto::monitoring::monitoring_service_client::MonitoringServiceClient { - crate::proto::monitoring::monitoring_service_client::MonitoringServiceClient::new( + ) -> crate::proto::monitoring::monitoring_service_client::MonitoringServiceClient + { + crate::proto::monitoring::monitoring_service_client::MonitoringServiceClient::with_interceptor( self.channel.clone(), + self.interceptor.clone(), ) } /// Configuration service client (trading service config -- package `config`). pub fn config( &self, - ) -> crate::proto::config::config_service_client::ConfigServiceClient { - crate::proto::config::config_service_client::ConfigServiceClient::new( + ) -> crate::proto::config::config_service_client::ConfigServiceClient { + crate::proto::config::config_service_client::ConfigServiceClient::with_interceptor( self.channel.clone(), + self.interceptor.clone(), ) } /// Data acquisition service client (market data feeds, downloads). pub fn data_acquisition( &self, - ) -> crate::proto::data_acquisition::data_acquisition_service_client::DataAcquisitionServiceClient + ) -> crate::proto::data_acquisition::data_acquisition_service_client::DataAcquisitionServiceClient { - crate::proto::data_acquisition::data_acquisition_service_client::DataAcquisitionServiceClient::new( + crate::proto::data_acquisition::data_acquisition_service_client::DataAcquisitionServiceClient::with_interceptor( self.channel.clone(), + self.interceptor.clone(), ) } /// Risk service client (VaR, position limits, circuit breakers). - pub fn risk(&self) -> crate::proto::risk::risk_service_client::RiskServiceClient { - crate::proto::risk::risk_service_client::RiskServiceClient::new(self.channel.clone()) + pub fn risk( + &self, + ) -> crate::proto::risk::risk_service_client::RiskServiceClient { + crate::proto::risk::risk_service_client::RiskServiceClient::with_interceptor( + self.channel.clone(), + self.interceptor.clone(), + ) } /// gRPC health check client (standard `grpc.health.v1.Health`). - pub fn health(&self) -> crate::proto::health::health_client::HealthClient { - crate::proto::health::health_client::HealthClient::new(self.channel.clone()) + /// + /// Health checks don't require auth, but the interceptor handles the + /// no-token case gracefully (proceeds without Bearer header). + pub fn health( + &self, + ) -> crate::proto::health::health_client::HealthClient { + crate::proto::health::health_client::HealthClient::with_interceptor( + self.channel.clone(), + self.interceptor.clone(), + ) } /// API Gateway configuration service client (package `foxhunt.config`). @@ -131,10 +182,11 @@ impl FoxhuntClient { /// configuration proto (package `config`). pub fn config_service( &self, - ) -> crate::proto::config_service::configuration_service_client::ConfigurationServiceClient + ) -> crate::proto::config_service::configuration_service_client::ConfigurationServiceClient { - crate::proto::config_service::configuration_service_client::ConfigurationServiceClient::new( + crate::proto::config_service::configuration_service_client::ConfigurationServiceClient::with_interceptor( self.channel.clone(), + self.interceptor.clone(), ) } }