systemprompt-api 0.2.0

HTTP API server and gateway for systemprompt.io OS
Documentation
use axum::Json;
use axum::extract::State;
use axum::http::{HeaderMap, HeaderValue, StatusCode, header};
use axum::response::IntoResponse;
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, JwtSigningParams, SessionCreationService, generate_admin_jwt,
};

#[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, Serialize)]
pub struct AnonymousError {
    pub error: String,
    pub error_description: 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>,
    #[serde(default)]
    pub user_id: Option<UserId>,
    #[serde(default)]
    pub email: Option<String>,
}

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

pub async fn generate_anonymous_token(
    State(state): State<OAuthState>,
    headers: HeaderMap,
    Json(req): Json<AnonymousTokenRequest>,
) -> impl IntoResponse {
    let expires_in = systemprompt_oauth::constants::token::ANONYMOUS_TOKEN_EXPIRY_SECONDS;
    let client_id = req.client_id.clone();
    let validator = match ClientValidator::new(state.db_pool()) {
        Ok(v) => v,
        Err(e) => {
            return (
                StatusCode::INTERNAL_SERVER_ERROR,
                Json(AnonymousError {
                    error: "server_error".to_string(),
                    error_description: format!("Failed to create client validator: {e}"),
                }),
            )
                .into_response();
        },
    };

    let validation = match validator
        .validate_client(&client_id, req.redirect_uri.as_deref())
        .await
    {
        Ok(v) => v,
        Err(e) => {
            return (
                StatusCode::BAD_REQUEST,
                Json(AnonymousError {
                    error: "invalid_client".to_string(),
                    error_description: format!("Client validation failed: {e}"),
                }),
            )
                .into_response();
        },
    };

    let client_type = validation.client_type();

    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));
    }

    if let Some(ref user_id) = req.user_id {
        if req.client_id == "sp_cli" {
            let email = req.email.clone().unwrap_or_else(|| user_id.to_string());

            match session_service
                .create_authenticated_session(user_id, &headers, SessionSource::Cli)
                .await
            {
                Ok(session_id) => {
                    let jwt_secret = match systemprompt_models::SecretsBootstrap::jwt_secret() {
                        Ok(s) => s,
                        Err(e) => {
                            return (
                                StatusCode::INTERNAL_SERVER_ERROR,
                                Json(AnonymousError {
                                    error: "server_error".to_string(),
                                    error_description: format!("Failed to get JWT secret: {e}"),
                                }),
                            )
                                .into_response();
                        },
                    };
                    let config = match systemprompt_models::Config::get() {
                        Ok(c) => c,
                        Err(e) => {
                            return (
                                StatusCode::INTERNAL_SERVER_ERROR,
                                Json(AnonymousError {
                                    error: "server_error".to_string(),
                                    error_description: format!("Failed to get config: {e}"),
                                }),
                            )
                                .into_response();
                        },
                    };
                    let signing = JwtSigningParams {
                        secret: jwt_secret,
                        issuer: &config.jwt_issuer,
                    };
                    let jwt_token = match generate_admin_jwt(
                        user_id,
                        &session_id,
                        &email,
                        &client_id,
                        &signing,
                    ) {
                        Ok(token) => token,
                        Err(e) => {
                            return (
                                StatusCode::INTERNAL_SERVER_ERROR,
                                Json(AnonymousError {
                                    error: "server_error".to_string(),
                                    error_description: format!("Failed to generate JWT: {e}"),
                                }),
                            )
                                .into_response();
                        },
                    };

                    let cookie = format!(
                        "access_token={}; Path=/; HttpOnly; SameSite=Lax; Max-Age={}",
                        jwt_token, expires_in
                    );

                    let mut response = (
                        StatusCode::OK,
                        Json(AnonymousTokenResponse {
                            access_token: jwt_token,
                            token_type: TokenType::Bearer.to_string(),
                            expires_in,
                            session_id,
                            user_id: user_id.clone(),
                            client_id: client_id.clone(),
                            client_type: "cli".to_string(),
                        }),
                    )
                        .into_response();

                    if let Ok(cookie_value) = HeaderValue::from_str(&cookie) {
                        response
                            .headers_mut()
                            .insert(header::SET_COOKIE, cookie_value);
                    }

                    return response;
                },
                Err(e) => {
                    return (
                        StatusCode::INTERNAL_SERVER_ERROR,
                        Json(AnonymousError {
                            error: "server_error".to_string(),
                            error_description: format!("Failed to create CLI session: {e}"),
                        }),
                    )
                        .into_response();
                },
            }
        }
    }

    let jwt_secret = match systemprompt_models::SecretsBootstrap::jwt_secret() {
        Ok(s) => s,
        Err(e) => {
            return (
                StatusCode::INTERNAL_SERVER_ERROR,
                Json(AnonymousError {
                    error: "server_error".to_string(),
                    error_description: format!("Failed to get JWT secret: {e}"),
                }),
            )
                .into_response();
        },
    };
    let session_source = SessionSource::from_client_id(req.client_id.as_str());
    match session_service
        .create_anonymous_session(CreateAnonymousSessionInput {
            headers: &headers,
            uri: None,
            client_id: &client_id,
            jwt_secret,
            session_source,
        })
        .await
    {
        Ok(session_info) => {
            let cookie = format!(
                "access_token={}; Path=/; HttpOnly; SameSite=Lax; Max-Age={}",
                session_info.jwt_token, expires_in
            );

            let mut response = (
                StatusCode::OK,
                Json(AnonymousTokenResponse {
                    access_token: session_info.jwt_token,
                    token_type: TokenType::Bearer.to_string(),
                    expires_in,
                    session_id: session_info.session_id.clone(),
                    user_id: session_info.user_id,
                    client_id: client_id.clone(),
                    client_type: client_type.to_string(),
                }),
            )
                .into_response();

            if let Ok(cookie_value) = HeaderValue::from_str(&cookie) {
                response
                    .headers_mut()
                    .insert(header::SET_COOKIE, cookie_value);
            }

            response
        },
        Err(e) => (
            StatusCode::INTERNAL_SERVER_ERROR,
            Json(AnonymousError {
                error: "server_error".to_string(),
                error_description: format!("Failed to create session: {e}"),
            }),
        )
            .into_response(),
    }
}