pas-external 0.2.0

Ppoppo Accounts System (PAS) external SDK -- OAuth2 PKCE, PASETO verification, Axum middleware, session liveness
Documentation
//! [`PasAuth`] — the single public entry point that wires a PAS OAuth2 router
//! together with a matching [`SvAwareSessionResolver`].
//!
//! Construct one per application, then mount the router and share the resolver
//! with whatever custom Axum middleware needs to read the authenticated
//! context. Both halves share the same [`SessionStore`] and cookie name by
//! construction, so the "write side" (OAuth callback) and the "read side"
//! (auth middleware) cannot drift out of sync.

use std::sync::Arc;

use axum::Router;

use super::config::PasAuthConfig;
use super::routes;
use super::session::SessionResolver;
use super::state::AuthState;
use super::sv_aware::SvAwareSessionResolver;
use super::traits::{AccountResolver, SessionStore};
use crate::oauth::AuthClient;
use crate::pas_port::PasAuthPort;
use crate::session_liveness::TokenCipher;
use crate::session_version::{MemorySessionVersionCache, SessionVersionCache};

/// Bundle of the PAS auth router + a matching [`SvAwareSessionResolver`].
///
/// The `P` type parameter is the [`PasAuthPort`] adapter. In production
/// it defaults to [`AuthClient`]; the only consumers passing a different
/// `P` are the SDK's own boundary tests (using `MemoryPasAuth`).
pub struct PasAuth<S, P = AuthClient>
where
    S: SessionStore,
    P: PasAuthPort,
{
    router: Router,
    base_resolver: SessionResolver<S>,
    store: Arc<S>,
    pas: Arc<P>,
    default_cache: Arc<MemorySessionVersionCache>,
    /// Mirrored from [`PasAuthConfig`]'s `with_refresh_token_cipher`. Used by
    /// resolvers to decrypt at-rest ciphertext via [`pas_refresh`]. `None`
    /// when `with_refresh_token_cipher` was not called.
    cipher: Option<Arc<TokenCipher>>,
}

// Production constructor: P = AuthClient.
impl<S: SessionStore> PasAuth<S, AuthClient> {
    /// Assemble the PAS auth router + supporting handles.
    pub fn new<U: AccountResolver>(
        config: PasAuthConfig,
        account_resolver: Arc<U>,
        session_store: Arc<S>,
    ) -> Self {
        let cookie_name: Arc<str> = Arc::from(config.settings.session_cookie_name.as_str());
        let cipher = config
            .settings
            .refresh_token_cipher
            .as_ref()
            .cloned()
            .map(Arc::new);
        let auth_client = Arc::new(config.client);

        let state = AuthState {
            client: Arc::clone(&auth_client),
            account_resolver,
            session_store: Arc::clone(&session_store),
            settings: config.settings,
        };

        let router = routes::build_router(state);
        let base_resolver = SessionResolver::new(Arc::clone(&session_store), cookie_name);

        Self {
            router,
            base_resolver,
            store: session_store,
            pas: auth_client,
            default_cache: Arc::new(MemorySessionVersionCache::new()),
            cipher,
        }
    }
}

// Resolver-construction methods are generic over P so SDK tests can
// build a PasAuth-equivalent shape against MemoryPasAuth without going
// through PasAuthConfig.
impl<S, P> PasAuth<S, P>
where
    S: SessionStore,
    P: PasAuthPort,
{
    /// Build the default sv-aware resolver, backed by an in-process
    /// [`MemorySessionVersionCache`] (60 s TTL).
    #[must_use]
    pub fn resolver(&self) -> SvAwareSessionResolver<S, MemorySessionVersionCache, P> {
        SvAwareSessionResolver::new(
            self.base_resolver.clone(),
            Arc::clone(&self.store),
            Arc::clone(&self.pas),
            Arc::clone(&self.default_cache),
            self.cipher.as_ref().map(Arc::clone),
        )
    }

    /// Build a sv-aware resolver backed by a custom cache implementation.
    #[must_use]
    pub fn resolver_with_cache<C: SessionVersionCache>(
        &self,
        cache: C,
    ) -> SvAwareSessionResolver<S, C, P> {
        SvAwareSessionResolver::new(
            self.base_resolver.clone(),
            Arc::clone(&self.store),
            Arc::clone(&self.pas),
            Arc::new(cache),
            self.cipher.as_ref().map(Arc::clone),
        )
    }

    /// Consume the bundle and return the router.
    pub fn into_router(self) -> Router {
        self.router
    }
}