pas-external 0.2.0

Ppoppo Accounts System (PAS) external SDK -- OAuth2 PKCE, PASETO verification, Axum middleware, session liveness
Documentation
//! `PasAuthPort` — the network boundary at PAS Authorization Server.
//!
//! Production adapter: `AuthClient` (in `crate::oauth`).
//! Test adapter: `MemoryPasAuth` (in `crate::pas_port::memory`, gated
//! behind the `test-support` Cargo feature).

use std::future::Future;

use crate::error::Error;
use crate::oauth::{TokenResponse, UserInfo};

/// Minimum surface the SDK refresh + sv flows need from PAS.
///
/// Adapters translate transport-level errors into [`PasFailure`].
/// Once a `PasFailure` is in hand, all SDK policy code is HTTP-free.
pub trait PasAuthPort: Send + Sync + 'static {
    /// `POST /oauth/token` with `grant_type=refresh_token` (RFC 6749 §6).
    fn refresh(
        &self,
        refresh_token: &str,
    ) -> impl Future<Output = Result<TokenResponse, PasFailure>> + Send;

    /// `GET /oauth/userinfo` with `Authorization: Bearer <access_token>`.
    fn userinfo(
        &self,
        access_token: &str,
    ) -> impl Future<Output = Result<UserInfo, PasFailure>> + Send;
}

/// Classified PAS-side failure. Single source of truth for the
/// HTTP-status → cause mapping; only adapters produce these.
#[non_exhaustive]
#[derive(Debug, Clone)]
pub enum PasFailure {
    /// PAS returned 4xx — credential is dead. Refresh-token expired,
    /// revoked, or user logged out elsewhere.
    Rejected { status: u16, detail: String },
    /// PAS returned 5xx — service is degraded. Retry; do not invalidate.
    ServerError { status: u16, detail: String },
    /// HTTP-level transport issue (timeout, TLS, DNS, connect) OR a
    /// 2xx whose body failed to parse (CDN/proxy garbage).
    /// Indistinguishable from a true network blip; fail-open paths
    /// serve cache, fail-closed paths reject.
    Transport { detail: String },
}

impl PasFailure {
    /// Lossy conversion to the legacy [`Error::OAuth`] shape, used by
    /// inherent `AuthClient` methods that retain the v4 signature
    /// (currently just `exchange_code`).
    #[must_use]
    pub fn into_legacy_error(self, operation: &'static str) -> Error {
        match self {
            Self::Rejected { status, detail } | Self::ServerError { status, detail } => {
                Error::OAuth { operation, status: Some(status), detail }
            }
            Self::Transport { detail } => Error::OAuth { operation, status: None, detail },
        }
    }
}