tail-fin-core 0.7.5

Public session-lifecycle abstractions for tail-fin: Site trait, SessionManager, Credentials, SessionStatus, auth errors. The stable API surface that downstream agents (Flock A2A etc.) consume.
Documentation
//! Error types for site lifecycle operations.

use night_fury_core::NightFuryError;

/// Errors specific to site lifecycle operations (refresh, validate, login).
#[derive(Debug, thiserror::Error)]
pub enum SiteError {
    #[error("refresh failed on site '{site}': {reason}")]
    RefreshFailed { site: &'static str, reason: String },

    #[error("validation failed on site '{site}': {reason}")]
    ValidationFailed { site: &'static str, reason: String },

    #[error("manual login required for site '{site}' — automated login not supported")]
    ManualLoginRequired { site: &'static str },

    #[error("login failed: {0:?}")]
    LoginFailed(AuthFailureKind),

    #[error("browser error during site operation: {0}")]
    Browser(#[from] NightFuryError),
}

/// Why a site operation failed authentication.
#[derive(Debug, Clone)]
pub enum AuthFailureKind {
    /// Cookie expired — refresh should help.
    CookieExpired,
    /// Credentials (password / token) were rejected by the server.
    CredentialsRejected,
    /// Rate limited — caller should wait and retry.
    RateLimited { retry_after: std::time::Duration },
    /// Anti-bot challenge (captcha, fingerprint check, etc.).
    AntibotBlock { challenge_type: String },
    /// Account suspended / banned.
    AccountSuspended,
    /// Other — free-form string.
    Other(String),
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn site_error_displays_with_site_id() {
        let err = SiteError::RefreshFailed {
            site: "twitter",
            reason: "auth_token missing".into(),
        };
        let msg = err.to_string();
        assert!(msg.contains("twitter"));
        assert!(msg.contains("auth_token missing"));
    }

    #[test]
    fn auth_failure_kind_debug() {
        let kind = AuthFailureKind::RateLimited {
            retry_after: std::time::Duration::from_secs(60),
        };
        let debug = format!("{kind:?}");
        assert!(debug.contains("RateLimited"));
    }

    #[test]
    fn auth_failure_kind_variants_exist() {
        let _ = AuthFailureKind::CookieExpired;
        let _ = AuthFailureKind::CredentialsRejected;
        let _ = AuthFailureKind::AccountSuspended;
        let _ = AuthFailureKind::AntibotBlock {
            challenge_type: "captcha".into(),
        };
        let _ = AuthFailureKind::Other("custom".into());
    }
}