pas-external 4.0.1

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, RefreshTokenResolver, SessionStore};
use crate::oauth::AuthClient;
use crate::session_version::{MemorySessionVersionCache, SessionVersionCache};

/// Bundle of the PAS auth router + a matching [`SvAwareSessionResolver`].
///
/// # Example
///
/// ```rust,ignore
/// // Single concrete adapter implementing AccountResolver + SessionStore +
/// // RefreshTokenResolver — share one Arc.
/// let adapter = Arc::new(PasAdapter::new(pool));
/// let pas_auth = PasAuth::new(
///     pas_config,
///     Arc::clone(&adapter),  // AccountResolver
///     Arc::clone(&adapter),  // SessionStore
///     Arc::clone(&adapter),  // RefreshTokenResolver
/// );
///
/// let resolver = pas_auth.resolver();       // SvAwareSessionResolver
/// let router   = pas_auth.into_router();    // merge into main router
/// ```
pub struct PasAuth<S: SessionStore, R: RefreshTokenResolver> {
    router: Router,
    base_resolver: SessionResolver<S>,
    store: Arc<S>,
    refresh_resolver: Arc<R>,
    auth_client: Arc<AuthClient>,
}

impl<S: SessionStore, R: RefreshTokenResolver> PasAuth<S, R> {
    /// Assemble the PAS auth router + supporting handles.
    ///
    /// All three trait objects are taken as [`Arc`] so consumers can
    /// share a single backing adapter (typical pattern: one `PasAdapter`
    /// implements all three) or keep them independent. The router and
    /// the resolver always share the same `Arc<S>`, so the cookie name
    /// and the store they resolve against cannot drift.
    pub fn new<U: AccountResolver>(
        config: PasAuthConfig,
        account_resolver: Arc<U>,
        session_store: Arc<S>,
        refresh_resolver: Arc<R>,
    ) -> Self {
        let cookie_name: Arc<str> = Arc::from(config.settings.session_cookie_name.as_str());
        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,
            refresh_resolver,
            auth_client,
        }
    }

    /// Build the default sv-aware resolver, backed by an in-process
    /// [`MemorySessionVersionCache`] (60 s TTL).
    ///
    /// Most consumers want this. The in-memory cache is per-pod, so a
    /// break-glass on PAS converges across the consumer's pods within
    /// the cache TTL — see [`SvAwareSessionResolver`] docs and
    /// STANDARDS_AUTH_INVALIDATION §4.3.
    #[must_use]
    pub fn resolver(&self) -> SvAwareSessionResolver<S, R, MemorySessionVersionCache> {
        let cache = Arc::new(MemorySessionVersionCache::new());
        SvAwareSessionResolver::new(
            self.base_resolver.clone(),
            Arc::clone(&self.store),
            Arc::clone(&self.refresh_resolver),
            Arc::clone(&self.auth_client),
            cache,
        )
    }

    /// Build a sv-aware resolver backed by a custom cache implementation.
    ///
    /// Use this when consumer pods need a shared cache substrate (e.g.
    /// Redis, KVRocks) so a break-glass converges in `~RTT` instead of
    /// the default per-pod 60 s TTL bound. The trait surface is small
    /// — see [`SessionVersionCache`].
    #[must_use]
    pub fn resolver_with_cache<C: SessionVersionCache>(
        &self,
        cache: C,
    ) -> SvAwareSessionResolver<S, R, C> {
        SvAwareSessionResolver::new(
            self.base_resolver.clone(),
            Arc::clone(&self.store),
            Arc::clone(&self.refresh_resolver),
            Arc::clone(&self.auth_client),
            Arc::new(cache),
        )
    }

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