spiral-auth 0.1.7

Spiral credentials: bearer tokens and the providers that obtain them
Documentation
//! The HTTP client that credential providers share.

use std::sync::LazyLock;
use std::time::Duration;

use async_trait::async_trait;
use reqwest::{Client, Response};

use crate::tls::install_default_crypto_provider;
use crate::{CredentialError, CredentialResult};

/// The `User-Agent` of credential requests, matching the SDK's.
const USER_AGENT: &str = concat!("spiral-sdk/", env!("CARGO_PKG_VERSION"));

static SHARED_CLIENT: LazyLock<Client> = LazyLock::new(|| {
    install_default_crypto_provider();
    #[expect(
        clippy::disallowed_methods,
        reason = "credential requests do not use the server HTTP client policy"
    )]
    let builder = Client::builder();
    builder
        .user_agent(USER_AGENT)
        .connect_timeout(Duration::from_secs(10))
        .timeout(Duration::from_secs(60))
        .build()
        .expect("valid credential HTTP client")
});

/// The process-wide HTTP client of credential providers.
pub(crate) fn shared_client() -> Client {
    SHARED_CLIENT.clone()
}

/// Treats an unsuccessful response as a rejection that carries its status and body.
#[async_trait]
pub(crate) trait Accepted: Sized {
    /// The response if it succeeded, else the status and body it was rejected with.
    async fn accepted(self) -> CredentialResult<Self>;
}

#[async_trait]
impl Accepted for Response {
    async fn accepted(self) -> CredentialResult<Self> {
        let status = self.status();
        if status.is_success() {
            return Ok(self);
        }
        let url = self.url().clone();
        let body = self.text().await?;
        Err(CredentialError::Rejected { url, status, body })
    }
}