Skip to main content

anytype_rpc/
auth.rs

1//! Authentication helpers for Anytype gRPC clients.
2
3use tonic::{
4    metadata::{Ascii, MetadataValue},
5    service::Interceptor,
6    {Request, Status, transport::Channel},
7};
8
9use crate::error::AuthError;
10use crate::{
11    anytype::ClientCommandsClient,
12    anytype::rpc::account::local_link::{
13        new_challenge::Request as LocalLinkChallengeRequest,
14        new_challenge::Response as LocalLinkChallengeResponse,
15        solve_challenge::Request as LocalLinkSolveRequest,
16        solve_challenge::Response as LocalLinkSolveResponse,
17    },
18    anytype::rpc::wallet::create_session::{
19        Request as CreateSessionRequest, Response as CreateSessionResponse, request::Auth,
20    },
21    model::account::auth::LocalApiScope,
22};
23
24/// Authentication options for `WalletCreateSession`.
25#[derive(Debug, Clone)]
26pub enum SessionAuth {
27    /// Local app key created via LocalLink (limited scope).
28    AppKey(String),
29    /// Account key from the headless CLI (full scope).
30    AccountKey(String),
31    /// Mnemonic phrase (full scope).
32    Mnemonic(String),
33    /// Existing session token to refresh.
34    Token(String),
35}
36
37impl SessionAuth {
38    fn into_request(self) -> CreateSessionRequest {
39        let auth = match self {
40            SessionAuth::AppKey(value) => Auth::AppKey(value),
41            SessionAuth::AccountKey(value) => Auth::AccountKey(value),
42            SessionAuth::Mnemonic(value) => Auth::Mnemonic(value),
43            SessionAuth::Token(value) => Auth::Token(value),
44        };
45        CreateSessionRequest { auth: Some(auth) }
46    }
47}
48
49/// Create a session and return the full response for additional fields (like `app_token`).
50pub async fn create_session(
51    channel: Channel,
52    auth: SessionAuth,
53) -> Result<CreateSessionResponse, AuthError> {
54    let mut client = ClientCommandsClient::new(channel);
55    let request = auth.into_request();
56    let response: tonic::Response<CreateSessionResponse> =
57        client.wallet_create_session(request).await?;
58    let response = response.into_inner();
59
60    if let Some(error) = response.error.as_ref()
61        && error.code != 0
62    {
63        return Err(AuthError::Api {
64            code: error.code,
65            description: error.description.clone(),
66        });
67    }
68
69    Ok(response)
70}
71
72/// Create a session and return just the session token.
73pub async fn create_session_token(
74    channel: Channel,
75    auth: SessionAuth,
76) -> Result<String, AuthError> {
77    let response = create_session(channel, auth).await?;
78    if response.token.is_empty() {
79        return Err(AuthError::EmptyToken);
80    }
81    Ok(response.token)
82}
83
84/// Create a session token from a LocalLink app key.
85pub async fn create_session_token_from_app_key(
86    channel: Channel,
87    app_key: impl AsRef<str>,
88) -> Result<String, AuthError> {
89    create_session_token(channel, SessionAuth::AppKey(app_key.as_ref().to_string())).await
90}
91
92/// Create a session token from a headless account key.
93pub async fn create_session_token_from_account_key(
94    channel: Channel,
95    account_key: impl AsRef<str>,
96) -> Result<String, AuthError> {
97    create_session_token(
98        channel,
99        SessionAuth::AccountKey(account_key.as_ref().to_string()),
100    )
101    .await
102}
103
104/// Response from LocalLink SolveChallenge.
105#[derive(Debug, Clone)]
106pub struct LocalLinkCredentials {
107    pub app_key: String,
108    pub session_token: Option<String>,
109}
110
111/// Create a LocalLink challenge for the given app name and scope.
112pub async fn create_local_link_challenge(
113    channel: Channel,
114    app_name: impl Into<String>,
115    scope: LocalApiScope,
116) -> Result<String, AuthError> {
117    let mut client = ClientCommandsClient::new(channel);
118    let request = LocalLinkChallengeRequest {
119        app_name: app_name.into(),
120        scope: scope as i32,
121    };
122    let response: tonic::Response<LocalLinkChallengeResponse> =
123        client.account_local_link_new_challenge(request).await?;
124    let response = response.into_inner();
125    if let Some(error) = response.error.as_ref()
126        && error.code != 0
127    {
128        return Err(AuthError::Api {
129            code: error.code,
130            description: error.description.clone(),
131        });
132    }
133    Ok(response.challenge_id)
134}
135
136/// Solve a LocalLink challenge and return the app key.
137pub async fn solve_local_link_challenge(
138    channel: Channel,
139    challenge_id: impl Into<String>,
140    answer: impl Into<String>,
141) -> Result<LocalLinkCredentials, AuthError> {
142    let mut client = ClientCommandsClient::new(channel);
143    let request = LocalLinkSolveRequest {
144        challenge_id: challenge_id.into(),
145        answer: answer.into(),
146    };
147    let response: tonic::Response<LocalLinkSolveResponse> =
148        client.account_local_link_solve_challenge(request).await?;
149    let response = response.into_inner();
150    if let Some(error) = response.error.as_ref()
151        && error.code != 0
152    {
153        return Err(AuthError::Api {
154            code: error.code,
155            description: error.description.clone(),
156        });
157    }
158    Ok(LocalLinkCredentials {
159        app_key: response.app_key,
160        session_token: if response.session_token.is_empty() {
161            None
162        } else {
163            Some(response.session_token)
164        },
165    })
166}
167
168/// Convenience helper to add the `token` metadata to a request.
169pub fn with_token<T>(mut request: Request<T>, token: &str) -> Result<Request<T>, AuthError> {
170    let token_value: MetadataValue<Ascii> = token.parse()?;
171    request.metadata_mut().insert("token", token_value);
172    Ok(request)
173}
174
175/// gRPC interceptor that injects a static session token.
176pub struct TokenInterceptor {
177    token: MetadataValue<Ascii>,
178}
179
180impl TokenInterceptor {
181    pub fn new(token: impl AsRef<str>) -> Result<Self, AuthError> {
182        let token_value: MetadataValue<Ascii> = token.as_ref().parse()?;
183        Ok(Self { token: token_value })
184    }
185}
186
187impl Interceptor for TokenInterceptor {
188    fn call(&mut self, mut request: Request<()>) -> Result<Request<()>, Status> {
189        request.metadata_mut().insert("token", self.token.clone());
190        Ok(request)
191    }
192}