Skip to main content

anytype_rpc/
client.rs

1use std::time::Duration;
2use tonic::transport::{Channel, Endpoint};
3
4use crate::anytype::ClientCommandsClient;
5use crate::auth::{
6    create_session_token_from_account_key_with_policy,
7    create_session_token_from_app_key_with_policy,
8};
9use crate::deadline::{GrpcDeadlineService, GrpcTimeoutPolicy};
10use crate::error::AnytypeGrpcError;
11
12// optional environment variable containing grpc endpoint
13const ANYTYPE_GRPC_ENDPOINT_ENV: &str = "ANYTYPE_GRPC_ENDPOINT";
14const ANYTYPE_GRPC_ENDPOINT: &str = "http://127.0.0.1:31010"; // headless server
15
16/// checks environment variable "ANYTYPE_GRPC_ENDPOINT", then falls back to headless cli endpoint
17pub fn default_grpc_endpoint() -> String {
18    std::env::var(ANYTYPE_GRPC_ENDPOINT_ENV).unwrap_or_else(|_| ANYTYPE_GRPC_ENDPOINT.to_string())
19}
20
21/// Configuration for connecting to Anytype gRPC.
22#[derive(Clone)]
23pub struct AnytypeGrpcConfig {
24    endpoint: String,
25    grpc_timeouts: Option<GrpcTimeoutPolicy>,
26}
27
28impl std::fmt::Debug for AnytypeGrpcConfig {
29    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
30        formatter
31            .debug_struct("AnytypeGrpcConfig")
32            .field("endpoint", &"redacted")
33            .field("grpc_timeouts", &self.grpc_timeouts)
34            .finish()
35    }
36}
37
38impl Default for AnytypeGrpcConfig {
39    fn default() -> Self {
40        Self {
41            endpoint: default_grpc_endpoint(),
42            grpc_timeouts: None,
43        }
44    }
45}
46
47impl AnytypeGrpcConfig {
48    pub fn new(endpoint: impl Into<String>) -> Self {
49        Self {
50            endpoint: endpoint.into(),
51            grpc_timeouts: None,
52        }
53    }
54
55    pub fn endpoint(&self) -> &str {
56        &self.endpoint
57    }
58
59    /// Sets an explicit logical gRPC deadline policy.
60    ///
61    /// An explicit policy ignores `ANYTYPE_GRPC_TIMEOUT_SECS`. `None` fields
62    /// inside the policy disable their individual boundaries.
63    #[must_use]
64    pub fn grpc_timeouts(mut self, policy: GrpcTimeoutPolicy) -> Self {
65        self.grpc_timeouts = Some(policy);
66        self
67    }
68
69    /// Resolves and validates the effective logical gRPC deadline policy.
70    pub fn resolved_grpc_timeouts(
71        &self,
72    ) -> Result<GrpcTimeoutPolicy, crate::deadline::GrpcTimeoutConfigError> {
73        GrpcTimeoutPolicy::resolve(self.grpc_timeouts)
74    }
75}
76
77/// Deadline-aware tonic service used by generated Anytype clients.
78pub type AnytypeGrpcService = GrpcDeadlineService<Channel>;
79
80/// gRPC client wrapper holding the connection and session token.
81#[derive(Clone)]
82pub struct AnytypeGrpcClient {
83    channel: Channel,
84    token: String,
85    endpoint: String,
86    grpc_timeouts: GrpcTimeoutPolicy,
87}
88
89impl std::fmt::Debug for AnytypeGrpcClient {
90    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
91        formatter
92            .debug_struct("AnytypeGrpcClient")
93            .field("channel", &"redacted")
94            .field("token_configured", &!self.token.is_empty())
95            .field("endpoint", &"redacted")
96            .field("grpc_timeouts", &self.grpc_timeouts)
97            .finish()
98    }
99}
100
101impl AnytypeGrpcClient {
102    /// returns the endpoint
103    pub fn get_endpoint(&self) -> &str {
104        &self.endpoint
105    }
106
107    /// Connects the raw transport after validating the configured timeout policy.
108    ///
109    /// This method only establishes the transport. Construct an
110    /// [`AnytypeGrpcClient`] to apply logical deadlines to generated RPCs.
111    pub async fn connect_channel(config: &AnytypeGrpcConfig) -> Result<Channel, AnytypeGrpcError> {
112        // Resolve before network activity so malformed process configuration
113        // cannot dispatch a connection attempt.
114        let _ = config.resolved_grpc_timeouts()?;
115        let endpoint = Endpoint::from_shared(config.endpoint.clone())?
116            .connect_timeout(Duration::from_secs(30))
117            .tcp_keepalive(Some(Duration::from_secs(60)))
118            .http2_keep_alive_interval(Duration::from_secs(30))
119            .keep_alive_timeout(Duration::from_secs(10))
120            .keep_alive_while_idle(true);
121        Ok(endpoint.connect().await?)
122    }
123
124    /// if you're using the headless client, you can generate a session token
125    /// from the account key in ~/.anytype/config.json
126    pub async fn from_account_key(
127        config: &AnytypeGrpcConfig,
128        account_key: impl AsRef<str>,
129    ) -> Result<Self, AnytypeGrpcError> {
130        let grpc_timeouts = config.resolved_grpc_timeouts()?;
131        let channel = Self::connect_channel(config).await?;
132        let token = create_session_token_from_account_key_with_policy(
133            channel.clone(),
134            account_key,
135            grpc_timeouts,
136        )
137        .await?;
138        Ok(Self {
139            channel,
140            token,
141            endpoint: config.endpoint.clone(),
142            grpc_timeouts,
143        })
144    }
145
146    // this may not work: the api may not have sufficient scope to create a grpc token
147    pub async fn from_app_key(
148        config: &AnytypeGrpcConfig,
149        app_key: impl AsRef<str>,
150    ) -> Result<Self, AnytypeGrpcError> {
151        let grpc_timeouts = config.resolved_grpc_timeouts()?;
152        let channel = Self::connect_channel(config).await?;
153        let token =
154            create_session_token_from_app_key_with_policy(channel.clone(), app_key, grpc_timeouts)
155                .await?;
156        Ok(Self {
157            channel,
158            token,
159            endpoint: config.endpoint.clone(),
160            grpc_timeouts,
161        })
162    }
163
164    pub async fn from_token(
165        config: &AnytypeGrpcConfig,
166        token: impl Into<String>,
167    ) -> Result<Self, AnytypeGrpcError> {
168        let grpc_timeouts = config.resolved_grpc_timeouts()?;
169        let channel = Self::connect_channel(config).await?;
170        Ok(Self {
171            channel,
172            token: token.into(),
173            endpoint: config.endpoint.clone(),
174            grpc_timeouts,
175        })
176    }
177
178    /// Returns generated commands over the configured deadline-aware service.
179    ///
180    /// Requests without explicit [`GrpcCallOptions`](crate::deadline::GrpcCallOptions)
181    /// use the ordinary unary profile. Existing shorter `grpc-timeout`
182    /// metadata remains authoritative.
183    pub fn client_commands(&self) -> ClientCommandsClient<AnytypeGrpcService> {
184        ClientCommandsClient::new(self.deadline_channel())
185    }
186
187    pub fn token(&self) -> &str {
188        &self.token
189    }
190
191    /// Returns the raw transport channel for compatibility with channel-generic helpers.
192    ///
193    /// Calls made directly through this channel do not receive the logical
194    /// deadline policy. Prefer [`Self::client_commands`] for generated RPCs.
195    #[must_use]
196    pub fn channel(&self) -> Channel {
197        self.channel.clone()
198    }
199
200    /// Returns a cloned deadline-aware tonic service.
201    #[must_use]
202    pub fn deadline_channel(&self) -> AnytypeGrpcService {
203        GrpcDeadlineService::new_resolved(self.channel.clone(), self.grpc_timeouts)
204    }
205
206    /// Returns the resolved logical gRPC deadline policy.
207    #[must_use]
208    pub const fn grpc_timeouts(&self) -> GrpcTimeoutPolicy {
209        self.grpc_timeouts
210    }
211}
212
213#[cfg(test)]
214mod tests {
215    use super::*;
216
217    #[tokio::test]
218    async fn debug_output_redacts_endpoint_token_and_transport() {
219        let endpoint = "http://ENDPOINT_SECRET.invalid";
220        let config = AnytypeGrpcConfig::new(endpoint);
221        let config_debug = format!("{config:?}");
222        assert!(!config_debug.contains("ENDPOINT_SECRET"));
223
224        let channel = Endpoint::from_static("http://CHANNEL_SECRET.invalid").connect_lazy();
225        let client = AnytypeGrpcClient {
226            channel,
227            token: "TOKEN_SECRET".to_owned(),
228            endpoint: endpoint.to_owned(),
229            grpc_timeouts: GrpcTimeoutPolicy::default(),
230        };
231        let client_debug = format!("{client:?}");
232        for secret in ["ENDPOINT_SECRET", "CHANNEL_SECRET", "TOKEN_SECRET"] {
233            assert!(!client_debug.contains(secret));
234        }
235    }
236}