Skip to main content

systemprompt_api/services/server/
builder.rs

1//! Full API router construction and the global middleware stack.
2//!
3//! [`setup_api_server`] composes the route tree and applies the global layers
4//! (body limit, analytics, context, session, CORS, trailing-slash, trace and
5//! served-by headers, content negotiation, security headers) in the order they
6//! must run. Binding and serving live in [`super::startup`], which binds the
7//! listener before this router exists and swaps it in once bootstrap
8//! completes.
9//!
10//! Copyright (c) systemprompt.io — Business Source License 1.1.
11//! See <https://systemprompt.io> for licensing details.
12
13use anyhow::Result;
14use axum::Router;
15use axum::extract::DefaultBodyLimit;
16use systemprompt_runtime::AppContext;
17use systemprompt_traits::{StartupEventExt, StartupEventSender};
18
19use super::routes::configure_routes;
20use crate::services::middleware::{
21    AnalyticsMiddleware, CorsMiddleware, PublicContextMiddleware, SessionMiddleware,
22    inject_security_headers, inject_served_by, inject_trace_header, remove_trailing_slash,
23};
24
25pub use super::discovery::*;
26pub use super::health::handle_health;
27
28pub fn setup_api_server(ctx: &AppContext, events: Option<&StartupEventSender>) -> Result<Router> {
29    super::state_dirs::ensure_writable(&[ctx.app_paths().storage().data()])?;
30
31    let rate_config = &ctx.config().rate_limits;
32
33    if rate_config.disabled
34        && let Some(tx) = events
35    {
36        tx.warning("Rate limiting disabled - development mode only");
37    }
38
39    register_artifact_scanner(ctx);
40    let router = configure_routes(ctx, events)?;
41    apply_global_middleware(router, ctx)
42}
43
44// Why: the ingest is built by the runtime, which cannot see the gateway's
45// scanner registry; the composition root closes the loop here so every tool
46// result — from any vantage point — is scanned by the installation's policy.
47fn register_artifact_scanner(ctx: &AppContext) {
48    let resolver = crate::services::gateway::policy::PolicyResolver::from_repository(
49        ctx.ai_repositories().gateway_policies.clone(),
50    );
51    ctx.artifact_ingest().register_scanner(std::sync::Arc::new(
52        crate::services::gateway::GatewayArtifactScanner::new(resolver),
53    ));
54}
55
56fn apply_global_middleware(router: Router, ctx: &AppContext) -> Result<Router> {
57    let mut router = router;
58
59    router = router.layer(DefaultBodyLimit::max(2 * 1024 * 1024));
60
61    let analytics_middleware = AnalyticsMiddleware::new(ctx)?;
62    router = router.layer(axum::middleware::from_fn({
63        let middleware = analytics_middleware;
64        move |req, next| {
65            let middleware = middleware.clone();
66            async move { middleware.track_request(req, next).await }
67        }
68    }));
69
70    let global_context_middleware = PublicContextMiddleware::new();
71    router = router.layer(axum::middleware::from_fn({
72        let middleware = global_context_middleware;
73        move |req, next| async move { middleware.seed(req, next).await }
74    }));
75
76    let session_middleware = SessionMiddleware::new(ctx);
77    router = router.layer(axum::middleware::from_fn({
78        let middleware = session_middleware;
79        move |req, next| {
80            let middleware = middleware.clone();
81            async move { middleware.handle(req, next).await }
82        }
83    }));
84
85    let cors = CorsMiddleware::build_layer(ctx.config())?;
86    router = router.layer(cors);
87
88    router = router.layer(axum::middleware::from_fn(remove_trailing_slash));
89
90    router = router.layer(axum::middleware::from_fn(inject_trace_header));
91
92    router = router.layer(axum::middleware::from_fn(inject_served_by));
93
94    if ctx.config().content_negotiation.enabled {
95        router = router.layer(axum::middleware::from_fn(
96            crate::services::middleware::content_negotiation_middleware,
97        ));
98    }
99
100    if ctx.config().security_headers.enabled {
101        let security_config = ctx.config().security_headers.clone();
102        router = router.layer(axum::middleware::from_fn(move |req, next| {
103            let config = security_config.clone();
104            inject_security_headers(config, req, next)
105        }));
106    }
107
108    Ok(router)
109}