systemprompt-api 0.15.0

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::Json;
use axum::extract::State;
use axum::http::{HeaderMap, HeaderValue, StatusCode, header};
use axum::response::{IntoResponse, Response};
use serde::{Deserialize, Serialize};
use std::sync::Arc;

use systemprompt_identifiers::{ClientId, SessionId, SessionSource, UserId};
use systemprompt_models::auth::TokenType;
use systemprompt_oauth::OAuthState;
use systemprompt_oauth::services::cimd::ClientValidator;
use systemprompt_oauth::services::{CreateAnonymousSessionInput, SessionCreationService};

use crate::routes::oauth::OAuthHttpError;

#[derive(Debug, Serialize)]
pub struct AnonymousTokenResponse {
    pub access_token: String,
    pub token_type: String,
    pub expires_in: i64,
    pub session_id: SessionId,
    pub user_id: UserId,
    pub client_id: ClientId,
    pub client_type: String,
}

#[derive(Debug, Deserialize)]
pub struct AnonymousTokenRequest {
    #[serde(default = "default_client_id")]
    pub client_id: ClientId,
    #[serde(default)]
    pub redirect_uri: Option<String>,
    #[serde(default)]
    pub metadata: Option<serde_json::Value>,
}

fn default_client_id() -> ClientId {
    ClientId::new("sp_web")
}

fn token_response(body: AnonymousTokenResponse, jwt_token: &str, expires_in: i64) -> Response {
    let cookie = format!(
        "access_token={jwt_token}; Path=/; HttpOnly; SameSite=Strict; Max-Age={expires_in}"
    );
    let mut response = (StatusCode::OK, Json(body)).into_response();
    if let Ok(cookie_value) = HeaderValue::from_str(&cookie) {
        response
            .headers_mut()
            .insert(header::SET_COOKIE, cookie_value);
    }
    response
}

async fn issue_anonymous_session(
    session_service: &SessionCreationService,
    req: &AnonymousTokenRequest,
    headers: &HeaderMap,
    client_type: String,
    expires_in: i64,
) -> Result<Response, OAuthHttpError> {
    let client_id = req.client_id.clone();
    let session_source = SessionSource::from_client_id(&req.client_id);

    let session_info = session_service
        .create_anonymous_session(CreateAnonymousSessionInput {
            headers,
            uri: None,
            client_id: &client_id,
            session_source,
        })
        .await
        .map_err(|e| OAuthHttpError::server_error(format!("Failed to create session: {e}")))?;

    let jwt_token = session_info.jwt_token;
    let body = AnonymousTokenResponse {
        access_token: jwt_token.clone(),
        token_type: TokenType::Bearer.to_string(),
        expires_in,
        session_id: session_info.session_id,
        user_id: session_info.user_id,
        client_id,
        client_type,
    };
    Ok(token_response(body, &jwt_token, expires_in))
}

fn build_session_service(state: &OAuthState) -> SessionCreationService {
    let mut session_service = SessionCreationService::new(
        Arc::clone(state.analytics_provider()),
        Arc::clone(state.user_provider()),
    );
    if let Some(fp_provider) = state.fingerprint_provider() {
        session_service = session_service.with_fingerprint_provider(Arc::clone(fp_provider));
    }
    if let Some(event_publisher) = state.event_publisher() {
        session_service = session_service.with_event_publisher(Arc::clone(event_publisher));
    }
    session_service
}

pub async fn generate_anonymous_token(
    State(state): State<OAuthState>,
    headers: HeaderMap,
    Json(req): Json<AnonymousTokenRequest>,
) -> Result<Response, OAuthHttpError> {
    let expires_in = systemprompt_oauth::constants::token::ANONYMOUS_TOKEN_EXPIRY_SECONDS;
    let client_id = req.client_id.clone();

    let validator = ClientValidator::new(state.db_pool()).map_err(|e| {
        OAuthHttpError::server_error(format!("Failed to create client validator: {e}"))
    })?;

    let validation = validator
        .validate_client(&client_id, req.redirect_uri.as_deref())
        .await
        .map_err(|e| OAuthHttpError::invalid_client(format!("Client validation failed: {e}")))?;
    let client_type = validation.client_type();

    let session_service = build_session_service(&state);

    issue_anonymous_session(
        &session_service,
        &req,
        &headers,
        client_type.to_string(),
        expires_in,
    )
    .await
}