Skip to main content

dbx_tools_auth/
bindings.rs

1use crate::{CredentialStore, Error, Result, StorageLock, Token};
2use std::{sync::Arc, time::Duration};
3
4#[uniffi::export(with_foreign)]
5#[async_trait::async_trait]
6/// Foreign-language persistence contract; implement every method in Node or Python.
7///
8/// UniFFI callback interfaces do not inherit Rust default method bodies. A store
9/// must explicitly implement locking and may implement `prepare_write` as a no-op
10/// when writes need no preflight. Credential JSON can contain refresh tokens.
11pub trait StorageAdapter: Send + Sync {
12    /// Load credential JSON, returning None when the key does not exist.
13    async fn load(&self, profile: String) -> BindingResult<Option<String>>;
14    /// Check write readiness before a provider rotates a refresh token.
15    async fn prepare_write(&self) -> BindingResult<()>;
16    /// Persist credential JSON without discarding unrelated keys.
17    async fn save(&self, profile: String, token: String) -> BindingResult<()>;
18    /// Delete only the specified credential.
19    async fn remove(&self, profile: String) -> BindingResult<()>;
20    /// Acquire an exclusive refresh lease or fail within the timeout.
21    async fn acquire_lock(&self, profile: String, timeout_millis: u64) -> BindingResult<String>;
22    /// Release the lease returned by `acquire_lock`.
23    async fn release_lock(&self, lease: String) -> BindingResult<()>;
24    /// Return a human-readable backend identifier.
25    fn name(&self) -> String;
26}
27
28/// Public credential result; deliberately excludes the refresh token.
29#[derive(Clone, uniffi::Record)]
30pub struct AccessToken {
31    pub access_token: String,
32    pub token_type: String,
33    pub expiry: Option<String>,
34    pub scopes: Vec<String>,
35}
36
37#[derive(Debug, thiserror::Error, uniffi::Error)]
38pub enum AuthError {
39    #[error("{message}")]
40    Failure { message: String },
41}
42
43impl From<uniffi::UnexpectedUniFFICallbackError> for AuthError {
44    fn from(error: uniffi::UnexpectedUniFFICallbackError) -> Self {
45        Self::Failure {
46            message: error.to_string(),
47        }
48    }
49}
50
51pub type BindingResult<T> = std::result::Result<T, AuthError>;
52
53pub struct ForeignStore {
54    pub storage: Arc<dyn StorageAdapter>,
55}
56
57#[derive(uniffi::Object)]
58/// Keeps storage callbacks in the native library that owns their converters.
59pub struct StorageHandle {
60    pub store: Arc<dyn CredentialStore>,
61}
62
63#[uniffi::export]
64/// Wrap custom storage before passing it into another provider's native library.
65pub fn create_storage_handle(storage: Arc<dyn StorageAdapter>) -> Arc<StorageHandle> {
66    Arc::new(StorageHandle {
67        store: Arc::new(ForeignStore { storage }),
68    })
69}
70
71struct ForeignLock {
72    storage: Arc<dyn StorageAdapter>,
73    lease: String,
74}
75
76#[async_trait::async_trait]
77impl StorageLock for ForeignLock {
78    async fn release(self: Box<Self>) -> Result<()> {
79        self.storage
80            .release_lock(self.lease)
81            .await
82            .map_err(|error| Error::Storage(error.to_string()))
83    }
84}
85
86#[async_trait::async_trait]
87impl CredentialStore for ForeignStore {
88    async fn load(&self, profile: &str) -> Result<Option<Token>> {
89        self.storage
90            .load(profile.to_owned())
91            .await
92            .map_err(|error| Error::Storage(error.to_string()))?
93            .map(|token| serde_json::from_str(&token).map_err(Into::into))
94            .transpose()
95    }
96
97    async fn prepare_write(&self) -> Result<()> {
98        self.storage
99            .prepare_write()
100            .await
101            .map_err(|error| Error::Storage(error.to_string()))
102    }
103
104    async fn save(&self, profile: &str, token: &Token) -> Result<()> {
105        self.storage
106            .save(profile.to_owned(), serde_json::to_string(token)?)
107            .await
108            .map_err(|error| Error::Storage(error.to_string()))
109    }
110
111    async fn delete(&self, profile: &str) -> Result<()> {
112        self.storage
113            .remove(profile.to_owned())
114            .await
115            .map_err(|error| Error::Storage(error.to_string()))
116    }
117
118    async fn lock(&self, profile: &str, timeout: Duration) -> Result<Box<dyn StorageLock>> {
119        let timeout_millis = u64::try_from(timeout.as_millis()).unwrap_or(u64::MAX);
120        let lease = self
121            .storage
122            .acquire_lock(profile.to_owned(), timeout_millis)
123            .await
124            .map_err(|error| Error::Storage(error.to_string()))?;
125        Ok(Box::new(ForeignLock {
126            storage: Arc::clone(&self.storage),
127            lease,
128        }))
129    }
130
131    fn name(&self) -> &'static str {
132        "custom"
133    }
134}
135
136impl From<Token> for AccessToken {
137    fn from(token: Token) -> Self {
138        Self {
139            access_token: token.access_token,
140            token_type: token.token_type,
141            expiry: token.expires_at.map(|value| value.to_string()),
142            scopes: token.scopes,
143        }
144    }
145}