1use tonic::{
4 metadata::{Ascii, MetadataValue},
5 service::Interceptor,
6 {Request, Status, transport::Channel},
7};
8
9use crate::client::AnytypeGrpcConfig;
10use crate::deadline::{
11 GrpcCallOptions, GrpcDeadlineError, GrpcDeadlineService, GrpcTimeoutClass, GrpcTimeoutOutcome,
12 GrpcTimeoutPolicy, with_grpc_call_options,
13};
14use crate::error::AuthError;
15use crate::{
16 anytype::ClientCommandsClient,
17 anytype::rpc::account::local_link::{
18 new_challenge::Request as LocalLinkChallengeRequest,
19 new_challenge::Response as LocalLinkChallengeResponse,
20 solve_challenge::Request as LocalLinkSolveRequest,
21 solve_challenge::Response as LocalLinkSolveResponse,
22 },
23 anytype::rpc::wallet::create_session::{
24 Request as CreateSessionRequest, Response as CreateSessionResponse, request::Auth,
25 },
26 model::account::auth::LocalApiScope,
27};
28
29#[derive(Clone)]
31pub enum SessionAuth {
32 AppKey(String),
34 AccountKey(String),
36 Mnemonic(String),
38 Token(String),
40}
41
42impl std::fmt::Debug for SessionAuth {
43 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
44 formatter.write_str(match self {
45 Self::AppKey(_) => "SessionAuth::AppKey(redacted)",
46 Self::AccountKey(_) => "SessionAuth::AccountKey(redacted)",
47 Self::Mnemonic(_) => "SessionAuth::Mnemonic(redacted)",
48 Self::Token(_) => "SessionAuth::Token(redacted)",
49 })
50 }
51}
52
53impl SessionAuth {
54 fn into_request(self) -> CreateSessionRequest {
55 let auth = match self {
56 SessionAuth::AppKey(value) => Auth::AppKey(value),
57 SessionAuth::AccountKey(value) => Auth::AccountKey(value),
58 SessionAuth::Mnemonic(value) => Auth::Mnemonic(value),
59 SessionAuth::Token(value) => Auth::Token(value),
60 };
61 CreateSessionRequest { auth: Some(auth) }
62 }
63}
64
65pub async fn create_session(
67 channel: Channel,
68 auth: SessionAuth,
69) -> Result<CreateSessionResponse, AuthError> {
70 let policy = GrpcTimeoutPolicy::resolve(None)?;
71 create_session_with_policy(channel, auth, policy).await
72}
73
74pub async fn create_session_with_policy(
76 channel: Channel,
77 auth: SessionAuth,
78 policy: GrpcTimeoutPolicy,
79) -> Result<CreateSessionResponse, AuthError> {
80 let policy = policy.validate()?;
81 let mut client = ClientCommandsClient::new(GrpcDeadlineService::new_resolved(channel, policy));
82 let request = with_grpc_call_options(
83 Request::new(auth.into_request()),
84 GrpcCallOptions::new(
85 GrpcTimeoutClass::CredentialSetup,
86 GrpcTimeoutOutcome::MutationIndeterminate,
87 ),
88 );
89 let started = std::time::Instant::now();
90 let response: tonic::Response<CreateSessionResponse> = client
91 .wallet_create_session(request)
92 .await
93 .map_err(|status| {
94 deadline_or_auth_status(
95 status,
96 GrpcTimeoutClass::CredentialSetup,
97 GrpcTimeoutOutcome::MutationIndeterminate,
98 started.elapsed(),
99 )
100 })?;
101 let response = response.into_inner();
102
103 if let Some(error) = response.error.as_ref()
104 && error.code != 0
105 {
106 return Err(AuthError::Api {
107 code: error.code,
108 description: error.description.clone(),
109 });
110 }
111
112 Ok(response)
113}
114
115pub async fn create_session_token(
117 channel: Channel,
118 auth: SessionAuth,
119) -> Result<String, AuthError> {
120 let response = create_session(channel, auth).await?;
121 if response.token.is_empty() {
122 return Err(AuthError::EmptyToken);
123 }
124 Ok(response.token)
125}
126
127pub async fn create_session_token_from_app_key(
129 channel: Channel,
130 app_key: impl AsRef<str>,
131) -> Result<String, AuthError> {
132 create_session_token(channel, SessionAuth::AppKey(app_key.as_ref().to_string())).await
133}
134
135pub async fn create_session_token_from_app_key_with_policy(
137 channel: Channel,
138 app_key: impl AsRef<str>,
139 policy: GrpcTimeoutPolicy,
140) -> Result<String, AuthError> {
141 let response = create_session_with_policy(
142 channel,
143 SessionAuth::AppKey(app_key.as_ref().to_string()),
144 policy,
145 )
146 .await?;
147 if response.token.is_empty() {
148 return Err(AuthError::EmptyToken);
149 }
150 Ok(response.token)
151}
152
153pub async fn create_session_token_from_account_key(
155 channel: Channel,
156 account_key: impl AsRef<str>,
157) -> Result<String, AuthError> {
158 create_session_token(
159 channel,
160 SessionAuth::AccountKey(account_key.as_ref().to_string()),
161 )
162 .await
163}
164
165pub async fn create_session_token_from_account_key_with_policy(
167 channel: Channel,
168 account_key: impl AsRef<str>,
169 policy: GrpcTimeoutPolicy,
170) -> Result<String, AuthError> {
171 let response = create_session_with_policy(
172 channel,
173 SessionAuth::AccountKey(account_key.as_ref().to_string()),
174 policy,
175 )
176 .await?;
177 if response.token.is_empty() {
178 return Err(AuthError::EmptyToken);
179 }
180 Ok(response.token)
181}
182
183#[derive(Clone)]
185pub struct LocalLinkCredentials {
186 pub app_key: String,
187 pub session_token: Option<String>,
188}
189
190impl std::fmt::Debug for LocalLinkCredentials {
191 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
192 formatter
193 .debug_struct("LocalLinkCredentials")
194 .field("app_key", &"redacted")
195 .field("session_token_configured", &self.session_token.is_some())
196 .finish()
197 }
198}
199
200pub async fn create_local_link_challenge(
202 channel: Channel,
203 app_name: impl Into<String>,
204 scope: LocalApiScope,
205) -> Result<String, AuthError> {
206 let policy = GrpcTimeoutPolicy::resolve(None)?;
207 create_local_link_challenge_with_policy(channel, app_name, scope, policy).await
208}
209
210pub async fn create_local_link_challenge_with_config(
212 channel: Channel,
213 app_name: impl Into<String>,
214 scope: LocalApiScope,
215 config: &AnytypeGrpcConfig,
216) -> Result<String, AuthError> {
217 let policy = config.resolved_grpc_timeouts()?;
218 create_local_link_challenge_with_policy(channel, app_name, scope, policy).await
219}
220
221pub async fn create_local_link_challenge_with_policy(
223 channel: Channel,
224 app_name: impl Into<String>,
225 scope: LocalApiScope,
226 policy: GrpcTimeoutPolicy,
227) -> Result<String, AuthError> {
228 let policy = policy.validate()?;
229 let mut client = ClientCommandsClient::new(GrpcDeadlineService::new_resolved(channel, policy));
230 let request = LocalLinkChallengeRequest {
231 app_name: app_name.into(),
232 scope: scope as i32,
233 };
234 let request = with_grpc_call_options(
235 Request::new(request),
236 GrpcCallOptions::new(
237 GrpcTimeoutClass::CredentialSetup,
238 GrpcTimeoutOutcome::MutationIndeterminate,
239 ),
240 );
241 let started = std::time::Instant::now();
242 let response: tonic::Response<LocalLinkChallengeResponse> = client
243 .account_local_link_new_challenge(request)
244 .await
245 .map_err(|status| {
246 deadline_or_auth_status(
247 status,
248 GrpcTimeoutClass::CredentialSetup,
249 GrpcTimeoutOutcome::MutationIndeterminate,
250 started.elapsed(),
251 )
252 })?;
253 let response = response.into_inner();
254 if let Some(error) = response.error.as_ref()
255 && error.code != 0
256 {
257 return Err(AuthError::Api {
258 code: error.code,
259 description: error.description.clone(),
260 });
261 }
262 Ok(response.challenge_id)
263}
264
265pub async fn solve_local_link_challenge(
267 channel: Channel,
268 challenge_id: impl Into<String>,
269 answer: impl Into<String>,
270) -> Result<LocalLinkCredentials, AuthError> {
271 let policy = GrpcTimeoutPolicy::resolve(None)?;
272 solve_local_link_challenge_with_policy(channel, challenge_id, answer, policy).await
273}
274
275pub async fn solve_local_link_challenge_with_config(
277 channel: Channel,
278 challenge_id: impl Into<String>,
279 answer: impl Into<String>,
280 config: &AnytypeGrpcConfig,
281) -> Result<LocalLinkCredentials, AuthError> {
282 let policy = config.resolved_grpc_timeouts()?;
283 solve_local_link_challenge_with_policy(channel, challenge_id, answer, policy).await
284}
285
286pub async fn solve_local_link_challenge_with_policy(
288 channel: Channel,
289 challenge_id: impl Into<String>,
290 answer: impl Into<String>,
291 policy: GrpcTimeoutPolicy,
292) -> Result<LocalLinkCredentials, AuthError> {
293 let policy = policy.validate()?;
294 let mut client = ClientCommandsClient::new(GrpcDeadlineService::new_resolved(channel, policy));
295 let request = LocalLinkSolveRequest {
296 challenge_id: challenge_id.into(),
297 answer: answer.into(),
298 };
299 let request = with_grpc_call_options(
300 Request::new(request),
301 GrpcCallOptions::new(
302 GrpcTimeoutClass::CredentialSetup,
303 GrpcTimeoutOutcome::MutationIndeterminate,
304 ),
305 );
306 let started = std::time::Instant::now();
307 let response: tonic::Response<LocalLinkSolveResponse> = client
308 .account_local_link_solve_challenge(request)
309 .await
310 .map_err(|status| {
311 deadline_or_auth_status(
312 status,
313 GrpcTimeoutClass::CredentialSetup,
314 GrpcTimeoutOutcome::MutationIndeterminate,
315 started.elapsed(),
316 )
317 })?;
318 let response = response.into_inner();
319 if let Some(error) = response.error.as_ref()
320 && error.code != 0
321 {
322 return Err(AuthError::Api {
323 code: error.code,
324 description: error.description.clone(),
325 });
326 }
327 Ok(LocalLinkCredentials {
328 app_key: response.app_key,
329 session_token: if response.session_token.is_empty() {
330 None
331 } else {
332 Some(response.session_token)
333 },
334 })
335}
336
337pub fn with_token<T>(mut request: Request<T>, token: &str) -> Result<Request<T>, AuthError> {
339 let token_value: MetadataValue<Ascii> = token.parse()?;
340 request.metadata_mut().insert("token", token_value);
341 Ok(request)
342}
343
344pub struct TokenInterceptor {
346 token: MetadataValue<Ascii>,
347}
348
349impl TokenInterceptor {
350 pub fn new(token: impl AsRef<str>) -> Result<Self, AuthError> {
351 let token_value: MetadataValue<Ascii> = token.as_ref().parse()?;
352 Ok(Self { token: token_value })
353 }
354}
355
356impl Interceptor for TokenInterceptor {
357 fn call(&mut self, mut request: Request<()>) -> Result<Request<()>, Status> {
358 request.metadata_mut().insert("token", self.token.clone());
359 Ok(request)
360 }
361}
362
363fn deadline_or_auth_status(
364 status: Status,
365 class: GrpcTimeoutClass,
366 outcome: GrpcTimeoutOutcome,
367 elapsed: std::time::Duration,
368) -> AuthError {
369 GrpcDeadlineError::from_status(&status, class, outcome, elapsed).map_or_else(
370 || AuthError::Status { source: status },
371 |source| AuthError::Deadline { source },
372 )
373}
374
375#[cfg(test)]
376mod tests {
377 use super::*;
378
379 #[test]
380 fn credential_debug_output_is_redacted() {
381 for auth in [
382 SessionAuth::AppKey("APP_KEY_SECRET".to_owned()),
383 SessionAuth::AccountKey("ACCOUNT_KEY_SECRET".to_owned()),
384 SessionAuth::Mnemonic("MNEMONIC_SECRET".to_owned()),
385 SessionAuth::Token("TOKEN_SECRET".to_owned()),
386 ] {
387 assert!(!format!("{auth:?}").contains("SECRET"));
388 }
389 let credentials = LocalLinkCredentials {
390 app_key: "APP_KEY_SECRET".to_owned(),
391 session_token: Some("TOKEN_SECRET".to_owned()),
392 };
393 assert!(!format!("{credentials:?}").contains("SECRET"));
394 }
395}