systemprompt-api 0.9.2

Axum-based HTTP server and API gateway for systemprompt.io AI governance infrastructure. Exposes governed agents, MCP, A2A, and admin endpoints with rate limiting and RBAC.
Documentation
use axum::Router;
use axum::routing::get;
use std::sync::Arc;
use systemprompt_runtime::AppContext;
use systemprompt_traits::{StartupEvent, StartupEventSender};

use crate::services::middleware::{
    ContextMiddleware, JwtContextExtractor, RouterExt, site_auth_gate,
};
use crate::services::static_content::{
    StaticContentMatcher, StaticContentState, serve_homepage, smart_fallback_handler,
};

pub(super) fn build_static_router(
    ctx: &AppContext,
    public_middleware: ContextMiddleware<JwtContextExtractor>,
    events: Option<&StartupEventSender>,
) -> Router {
    let path = ctx.app_paths().system().content_config().to_path_buf();
    #[allow(clippy::option_if_let_else)]
    let content_matcher = if let Some(path_str) = path.to_str() {
        match StaticContentMatcher::from_config(path_str) {
            Ok(matcher) => Arc::new(matcher),
            Err(e) => {
                if let Some(tx) = events {
                    if tx
                        .unbounded_send(StartupEvent::Warning {
                            message: format!("Failed to load content config: {e}"),
                            context: Some("Static content matching will be disabled".to_string()),
                        })
                        .is_err()
                    {
                        tracing::debug!("Startup event receiver dropped");
                    }
                }
                Arc::new(StaticContentMatcher::empty())
            },
        }
    } else {
        if let Some(tx) = events {
            if tx
                .unbounded_send(StartupEvent::Warning {
                    message: "CONTENT_CONFIG_PATH contains invalid UTF-8".to_string(),
                    context: None,
                })
                .is_err()
            {
                tracing::debug!("Startup event receiver dropped");
            }
        }
        Arc::new(StaticContentMatcher::empty())
    };

    let static_state = StaticContentState {
        ctx: Arc::new(ctx.clone()),
        matcher: content_matcher,
        route_classifier: Arc::clone(ctx.route_classifier()),
    };

    let static_router = Router::new()
        .route("/", get(serve_homepage))
        .fallback(smart_fallback_handler)
        .with_state(static_state)
        .with_auth_middleware(public_middleware);

    let site_auth_config = ctx
        .extension_registry()
        .extensions()
        .iter()
        .find_map(|ext| ext.site_auth());

    if let Some(auth_config) = site_auth_config {
        let secret = systemprompt_config::SecretsBootstrap::jwt_secret()
            .unwrap_or_else(|e| {
                tracing::warn!(error = %e, "JWT secret not available for site auth gate");
                ""
            })
            .to_string();
        static_router.layer(axum::middleware::from_fn(move |req, next| {
            let config = auth_config;
            let secret = secret.clone();
            async move { site_auth_gate(req, next, config, secret).await }
        }))
    } else {
        static_router
    }
}