xdk-rs 0.1.3

Async Rust client for the X (Twitter) API: OAuth1, OAuth2 PKCE, bearer tokens, media upload, streaming. xdk is X's SDK name; this is an independent project, not affiliated with X Corp.
Documentation
//! Token CRUD operations — save, get, clear for Bearer, OAuth2, OAuth1.

use super::TokenStore;
use super::types::{OAuth1Token, OAuth2Token, Token, TokenType};
use crate::error::Result;

#[allow(dead_code)] // Public library API — used by consumers and integration tests
impl TokenStore {
    // ── Save ─────────────────────────────────────────────────────────

    /// Saves a bearer token into the resolved app.
    ///
    /// # Errors
    ///
    /// Returns an error if the store cannot be saved to disk.
    pub fn save_bearer_token(&mut self, token: &str) -> Result<()> {
        self.save_bearer_token_for_app("", token)
    }

    /// Saves a bearer token into the named app.
    ///
    /// # Errors
    ///
    /// Returns an error if the store cannot be saved to disk.
    pub fn save_bearer_token_for_app(&mut self, app_name: &str, token: &str) -> Result<()> {
        self.update(|store| {
            let app = store.resolve_app_mut(app_name);
            app.bearer_token = Some(Token {
                token_type: TokenType::Bearer,
                bearer: Some(token.to_string()),
                oauth2: None,
                oauth1: None,
            });
            Ok(())
        })
    }

    /// Saves an `OAuth2` token into the resolved app.
    ///
    /// # Errors
    ///
    /// Returns an error if the store cannot be saved to disk.
    pub fn save_oauth2_token(
        &mut self,
        username: &str,
        access_token: &str,
        refresh_token: &str,
        expiration_time: u64,
    ) -> Result<()> {
        self.save_oauth2_token_for_app("", username, access_token, refresh_token, expiration_time)
    }

    /// Saves an `OAuth2` token into the named app.
    ///
    /// # Errors
    ///
    /// Returns an error if the store cannot be saved to disk.
    pub fn save_oauth2_token_for_app(
        &mut self,
        app_name: &str,
        username: &str,
        access_token: &str,
        refresh_token: &str,
        expiration_time: u64,
    ) -> Result<()> {
        self.update(|store| {
            let app = store.resolve_app_mut(app_name);
            app.oauth2_tokens.insert(
                username.to_string(),
                Token {
                    token_type: TokenType::Oauth2,
                    bearer: None,
                    oauth2: Some(OAuth2Token {
                        access_token: access_token.to_string(),
                        refresh_token: refresh_token.to_string(),
                        expiration_time,
                    }),
                    oauth1: None,
                },
            );
            Ok(())
        })
    }

    /// Saves an `OAuth2` token into the named app's unnamed (`/me`-failed salvage) slot.
    ///
    /// Used by the refresh and exchange paths when post-token username discovery
    /// fails: the refreshed access token is still valid and is preserved here
    /// rather than discarded. Single-occupancy, last-write-wins.
    ///
    /// # Errors
    ///
    /// Returns an error if the store cannot be saved to disk.
    pub fn save_oauth2_token_unnamed_for_app(
        &mut self,
        app_name: &str,
        access_token: &str,
        refresh_token: &str,
        expiration_time: u64,
    ) -> Result<()> {
        self.update(|store| {
            let app = store.resolve_app_mut(app_name);
            app.unnamed_oauth2_token = Some(Token {
                token_type: TokenType::Oauth2,
                bearer: None,
                oauth2: Some(OAuth2Token {
                    access_token: access_token.to_string(),
                    refresh_token: refresh_token.to_string(),
                    expiration_time,
                }),
                oauth1: None,
            });
            Ok(())
        })
    }

    /// Saves `OAuth1` tokens into the resolved app.
    ///
    /// # Errors
    ///
    /// Returns an error if the store cannot be saved to disk.
    pub fn save_oauth1_tokens(
        &mut self,
        access_token: &str,
        token_secret: &str,
        consumer_key: &str,
        consumer_secret: &str,
    ) -> Result<()> {
        self.save_oauth1_tokens_for_app(
            "",
            access_token,
            token_secret,
            consumer_key,
            consumer_secret,
        )
    }

    /// Saves `OAuth1` tokens into the named app.
    ///
    /// # Errors
    ///
    /// Returns an error if the store cannot be saved to disk.
    pub fn save_oauth1_tokens_for_app(
        &mut self,
        app_name: &str,
        access_token: &str,
        token_secret: &str,
        consumer_key: &str,
        consumer_secret: &str,
    ) -> Result<()> {
        self.update(|store| {
            let app = store.resolve_app_mut(app_name);
            app.oauth1_token = Some(Token {
                token_type: TokenType::Oauth1,
                bearer: None,
                oauth2: None,
                oauth1: Some(OAuth1Token {
                    access_token: access_token.to_string(),
                    token_secret: token_secret.to_string(),
                    consumer_key: consumer_key.to_string(),
                    consumer_secret: consumer_secret.to_string(),
                }),
            });
            Ok(())
        })
    }

    // ── Get ──────────────────────────────────────────────────────────

    /// Gets an `OAuth2` token for a username from the resolved app.
    #[must_use]
    pub fn get_oauth2_token(&self, username: &str) -> Option<&Token> {
        self.get_oauth2_token_for_app("", username)
    }

    /// Gets an `OAuth2` token for a username from the named app.
    #[must_use]
    pub fn get_oauth2_token_for_app(&self, app_name: &str, username: &str) -> Option<&Token> {
        let app = self.resolve_app(app_name);
        app.oauth2_tokens.get(username)
    }

    /// Gets the first `OAuth2` token from the resolved app.
    #[must_use]
    pub fn get_first_oauth2_token(&self) -> Option<&Token> {
        self.get_first_oauth2_token_for_app("")
    }

    /// Gets the default user's token, or the first `OAuth2` token from the named app.
    #[must_use]
    pub fn get_first_oauth2_token_for_app(&self, app_name: &str) -> Option<&Token> {
        let app = self.resolve_app(app_name);
        // Prefer the default user if one is set and still has a token
        if !app.default_user.is_empty()
            && let Some(token) = app.oauth2_tokens.get(&app.default_user)
        {
            return Some(token);
        }
        app.oauth2_tokens.values().next()
    }

    /// Gets the unnamed (`/me`-failed salvage) `OAuth2` token from the named app.
    ///
    /// Returns `None` when the slot is empty.
    #[must_use]
    pub fn get_oauth2_token_unnamed_for_app(&self, app_name: &str) -> Option<&Token> {
        let app = self.resolve_app(app_name);
        app.unnamed_oauth2_token.as_ref()
    }

    /// Gets `OAuth1` tokens from the resolved app.
    #[must_use]
    pub fn get_oauth1_tokens(&self) -> Option<&Token> {
        self.get_oauth1_tokens_for_app("")
    }

    /// Gets `OAuth1` tokens from the named app.
    #[must_use]
    pub fn get_oauth1_tokens_for_app(&self, app_name: &str) -> Option<&Token> {
        let app = self.resolve_app(app_name);
        app.oauth1_token.as_ref()
    }

    /// Gets the bearer token from the resolved app.
    #[must_use]
    pub fn get_bearer_token(&self) -> Option<&Token> {
        self.get_bearer_token_for_app("")
    }

    /// Gets the bearer token from the named app.
    #[must_use]
    pub fn get_bearer_token_for_app(&self, app_name: &str) -> Option<&Token> {
        let app = self.resolve_app(app_name);
        app.bearer_token.as_ref()
    }

    /// Returns the names of every app in the store that holds at least one
    /// stored credential (OAuth2 token, OAuth1 tokens, or bearer token).
    ///
    /// Iterates in `BTreeMap` key order so the result is deterministic.
    /// Used by the `get_auth_header` resolver to surface a "wrong-app"
    /// envelope when the active app is empty but the user has credentials
    /// stored under a different app.
    #[must_use]
    pub fn apps_with_credentials(&self) -> Vec<String> {
        self.apps
            .iter()
            .filter(|(_, app)| {
                !app.oauth2_tokens.is_empty()
                    || app.oauth1_token.is_some()
                    || app.bearer_token.is_some()
                    || app.unnamed_oauth2_token.is_some()
            })
            .map(|(name, _)| name.clone())
            .collect()
    }

    // ── Clear ────────────────────────────────────────────────────────

    /// Clears an `OAuth2` token for a username from the resolved app.
    ///
    /// # Errors
    ///
    /// Returns an error if the store cannot be saved to disk.
    pub fn clear_oauth2_token(&mut self, username: &str) -> Result<()> {
        self.clear_oauth2_token_for_app("", username)
    }

    /// Name of the app a clear should act on, or `None` when it does not
    /// exist.
    ///
    /// Clearing must not be the operation that materializes an app: a clear
    /// against a store with nothing in it has nothing to do, and creating a
    /// placeholder to empty it would put back a phantom app an empty store
    /// does not carry.
    fn existing_app_name(&self, app_name: &str) -> Option<String> {
        let name = self.get_active_app_name(app_name).to_string();
        self.apps.contains_key(&name).then_some(name)
    }

    /// Clears an `OAuth2` token for a username from the named app.
    ///
    /// # Errors
    ///
    /// Returns an error if the store cannot be saved to disk.
    pub fn clear_oauth2_token_for_app(&mut self, app_name: &str, username: &str) -> Result<()> {
        self.clear_in_app(app_name, |app| {
            app.oauth2_tokens.remove(username);
        })
    }

    /// Applies `clear` to the app a clear should act on, under the lock, and
    /// does nothing when that app does not exist.
    fn clear_in_app(&mut self, app_name: &str, clear: impl FnOnce(&mut super::App)) -> Result<()> {
        if self.existing_app_name(app_name).is_none() {
            return Ok(());
        }
        self.update(|store| {
            if let Some(name) = store.existing_app_name(app_name)
                && let Some(app) = store.apps.get_mut(&name)
            {
                clear(app);
            }
            Ok(())
        })
    }

    /// Clears `OAuth1` tokens from the resolved app.
    ///
    /// # Errors
    ///
    /// Returns an error if the store cannot be saved to disk.
    pub fn clear_oauth1_tokens(&mut self) -> Result<()> {
        self.clear_oauth1_tokens_for_app("")
    }

    /// Clears `OAuth1` tokens from the named app.
    ///
    /// # Errors
    ///
    /// Returns an error if the store cannot be saved to disk.
    pub fn clear_oauth1_tokens_for_app(&mut self, app_name: &str) -> Result<()> {
        self.clear_in_app(app_name, |app| app.oauth1_token = None)
    }

    /// Clears the bearer token from the resolved app.
    ///
    /// # Errors
    ///
    /// Returns an error if the store cannot be saved to disk.
    pub fn clear_bearer_token(&mut self) -> Result<()> {
        self.clear_bearer_token_for_app("")
    }

    /// Clears the bearer token from the named app.
    ///
    /// # Errors
    ///
    /// Returns an error if the store cannot be saved to disk.
    pub fn clear_bearer_token_for_app(&mut self, app_name: &str) -> Result<()> {
        self.clear_in_app(app_name, |app| app.bearer_token = None)
    }

    /// Clears all tokens from the resolved app.
    ///
    /// # Errors
    ///
    /// Returns an error if the store cannot be saved to disk.
    pub fn clear_all(&mut self) -> Result<()> {
        self.clear_all_for_app("")
    }

    /// Clears all tokens from the named app.
    ///
    /// # Errors
    ///
    /// Returns an error if the store cannot be saved to disk.
    pub fn clear_all_for_app(&mut self, app_name: &str) -> Result<()> {
        self.clear_in_app(app_name, |app| {
            app.oauth2_tokens.clear();
            app.oauth1_token = None;
            app.bearer_token = None;
            app.unnamed_oauth2_token = None;
        })
    }

    // ── Query ────────────────────────────────────────────────────────

    /// Gets all `OAuth2` usernames from the resolved app.
    #[must_use]
    pub fn get_oauth2_usernames(&self) -> Vec<String> {
        self.get_oauth2_usernames_for_app("")
    }

    /// Gets all `OAuth2` usernames from the named app.
    #[must_use]
    pub fn get_oauth2_usernames_for_app(&self, app_name: &str) -> Vec<String> {
        let app = self.resolve_app(app_name);
        app.oauth2_tokens.keys().cloned().collect()
    }

    /// Checks if `OAuth1` tokens exist in the resolved app.
    #[must_use]
    pub fn has_oauth1_tokens(&self) -> bool {
        self.active_app()
            .is_some_and(|app| app.oauth1_token.is_some())
    }

    /// Checks if a bearer token exists in the resolved app.
    #[must_use]
    pub fn has_bearer_token(&self) -> bool {
        self.active_app()
            .is_some_and(|app| app.bearer_token.is_some())
    }
}