mod extension_mount;
mod protocol;
mod static_setup;
use axum::Router;
use std::sync::Arc;
use systemprompt_extension::LoaderError;
use systemprompt_runtime::AppContext;
use systemprompt_traits::{AppContext as AppContextTrait, StartupEventSender};
use crate::services::middleware::authz::AuthzPolicy;
use crate::services::middleware::{
A2AContextMiddleware, JtiRevocationChecker, JwtContextExtractor, McpContextMiddleware,
PublicContextMiddleware, RateLimitState, RouterExt, UserOnlyContextMiddleware,
ip_ban_middleware,
};
pub(super) fn configure_routes(
ctx: &AppContext,
events: Option<&StartupEventSender>,
) -> Result<Router, LoaderError> {
let mut router = Router::new();
super::metrics::install_recorder(&ctx.config().instance_id).map_err(|e| {
LoaderError::InitializationFailed {
extension: "prometheus_metrics".to_owned(),
message: e.to_string(),
}
})?;
let jwt_extractor = build_jwt_extractor(ctx)?;
let limits = RateLimitState::from_context(ctx)?;
let public_middleware = PublicContextMiddleware::new();
let user_middleware = UserOnlyContextMiddleware::new(jwt_extractor.clone());
let a2a_middleware = A2AContextMiddleware::new(jwt_extractor.clone());
let mcp_middleware = McpContextMiddleware::new(jwt_extractor);
let mount = protocol::MountCtx {
ctx,
limits: &limits,
public_middleware: &public_middleware,
user_middleware: &user_middleware,
};
router = protocol::mount_oauth(router, &mount)?;
router = protocol::mount_agent(router, &mount, a2a_middleware)?;
router = protocol::mount_mcp_and_stream(router, &mount, mcp_middleware)?;
router = protocol::mount_content_and_misc(router, &mount)?;
router = protocol::mount_messaging(router, &mount)?;
router = extension_mount::mount_extension_routes(router, ctx, &user_middleware, events)?;
router =
router.merge(discovery_router(ctx).with_auth(public_middleware, AuthzPolicy::public()));
router = router.merge(
authenticated_discovery_router(ctx)
.with_auth(user_middleware, AuthzPolicy::authenticated()),
);
router =
router.merge(wellknown_router(ctx)?.with_auth(public_middleware, AuthzPolicy::public()));
let rate_config = &ctx.config().rate_limits;
router = router.merge(
Router::new()
.route(
"/auth/link-passkey",
axum::routing::get(crate::routes::oauth::webauthn::link::link_passkey_page),
)
.with_rate_limit(&limits, rate_config.oauth_public_per_second, "oauth_public")?
.with_auth(public_middleware, AuthzPolicy::public()),
);
let banned_ip_repo = crate::repository::banned_ips(ctx.db_pool()).map_err(|e| {
LoaderError::InitializationFailed {
extension: "ip_ban_middleware".to_owned(),
message: e.to_string(),
}
})?;
let trusted_proxies = Arc::new(ctx.config().trusted_proxies.clone());
router = router.layer(axum::middleware::from_fn(move |req, next| {
let repo = Arc::clone(&banned_ip_repo);
let proxies = Arc::clone(&trusted_proxies);
async move { ip_ban_middleware(req, next, repo, proxies).await }
}));
router = router.merge(static_setup::build_static_router(
ctx,
public_middleware,
events,
));
Ok(router.layer(axum::middleware::from_fn(super::metrics::track_metrics)))
}
fn build_jwt_extractor(ctx: &AppContext) -> Result<JwtContextExtractor, LoaderError> {
let analytics = ctx
.analytics_provider()
.ok_or_else(|| LoaderError::InitializationFailed {
extension: "jwt".to_owned(),
message: "AnalyticsProvider is required for JWT session enforcement".to_owned(),
})?;
let user_provider = ctx
.user_provider()
.ok_or_else(|| LoaderError::InitializationFailed {
extension: "jwt".to_owned(),
message: "UserProvider is required for JWT validation".to_owned(),
})?;
let jti_revocation =
JtiRevocationChecker::from_repository(ctx.oauth_repositories().oauth.clone());
Ok(JwtContextExtractor::new(
analytics,
user_provider,
jti_revocation,
))
}
fn discovery_router(ctx: &AppContext) -> Router {
super::builder::discovery_router(ctx)
}
fn authenticated_discovery_router(ctx: &AppContext) -> Router {
super::builder::authenticated_discovery_router(ctx)
}
fn wellknown_router(ctx: &AppContext) -> Result<Router, LoaderError> {
Ok(crate::routes::oauth::wellknown_routes(ctx).merge(crate::routes::wellknown_router(ctx)?))
}