arcly-http 0.1.1

Enterprise-grade NestJS-inspired web framework on axum: zero-lock DI, declarative controllers, multi-tenant data routing, transactional outbox, ABAC, and a self-documenting OpenAPI surface
Documentation
//! The single credential-extraction pipeline shared by every request boundary.
//!
//! Order of precedence:
//! 1. `Authorization: Bearer <token>` header (decoded as an **access** JWT).
//! 2. HMAC-signed JWT cookie (`CookieService`) — same `decode_access` path,
//!    so the security properties are identical to the Bearer path.
//! 3. Server-side session (`SessionManager`) — loaded independently of claims;
//!    a request can carry both a JWT identity and a mutable session.
//!
//! HTTP boundary, plugin routes, and the WebSocket handshake all call into
//! this module, so a security fix or a new credential source lands everywhere
//! at once — there is deliberately no second copy of this logic anywhere.

use std::sync::Arc;

use axum::http::HeaderMap;

use crate::auth::cookie::CookieService;
use crate::auth::jwt::{decode_bearer_token, JwtService};
use crate::auth::session::{Session, SessionManager};
use crate::core::engine::FrozenDiContainer;
use crate::web::context::Claims;

/// Everything the boundaries need to authenticate one request.
pub struct AuthExtraction {
    pub claims: Option<Arc<Claims>>,
    pub session: Option<Arc<Session>>,
}

/// Extract JWT claims only (Bearer → signed-cookie fallback).
///
/// Used directly by the WebSocket handshake, which authenticates once at
/// upgrade time and never loads a server-side session.
pub fn extract_claims(
    headers: &HeaderMap,
    container: &'static FrozenDiContainer,
) -> Option<Arc<Claims>> {
    decode_bearer_token(headers, container).or_else(|| {
        let cookie = container.try_get::<CookieService>()?;
        let jwt = container.try_get::<JwtService>()?;
        let value = cookie.extract(headers)?;
        jwt.decode_access(&value)
    })
}

/// Extract claims **and** load the server-side session.
///
/// Used by the HTTP boundary and plugin routes. Session loading is skipped
/// entirely (no store round-trip) when no `SessionManager` is registered.
pub async fn extract_auth(
    headers: &HeaderMap,
    container: &'static FrozenDiContainer,
) -> AuthExtraction {
    let claims = extract_claims(headers, container);

    let session = match container.try_get::<SessionManager>() {
        Some(sm) => sm.load_from_headers(headers).await,
        None => None,
    };

    AuthExtraction { claims, session }
}