systemprompt-api 0.3.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::extract::{Extension, Path, State};
use axum::http::StatusCode;
use axum::response::IntoResponse;
use axum::routing::{delete, post};
use axum::{Json, Router};
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use systemprompt_identifiers::{ApiKeyId, UserId};
use systemprompt_models::RequestContext;
use systemprompt_models::api::ApiError;
use systemprompt_runtime::AppContext;
use systemprompt_users::{ApiKeyService, IssueApiKeyParams, UserApiKey};

pub fn router() -> Router<AppContext> {
    Router::new()
        .route("/", post(issue_key).get(list_keys))
        .route("/{key_id}", delete(revoke_key))
}

#[derive(Debug, Deserialize)]
pub struct IssueApiKeyRequest {
    pub name: String,
    #[serde(default)]
    pub target_user_id: Option<String>,
    #[serde(default)]
    pub expires_at: Option<DateTime<Utc>>,
}

#[derive(Debug, Serialize)]
pub struct IssueApiKeyResponse {
    pub id: String,
    pub name: String,
    pub key_prefix: String,
    pub secret: String,
    pub created_at: Option<DateTime<Utc>>,
    pub expires_at: Option<DateTime<Utc>>,
}

#[derive(Debug, Serialize)]
pub struct ApiKeyView {
    pub id: String,
    pub name: String,
    pub key_prefix: String,
    pub created_at: Option<DateTime<Utc>>,
    pub last_used_at: Option<DateTime<Utc>>,
    pub expires_at: Option<DateTime<Utc>>,
    pub revoked_at: Option<DateTime<Utc>>,
}

impl From<UserApiKey> for ApiKeyView {
    fn from(k: UserApiKey) -> Self {
        Self {
            id: k.id.as_str().to_string(),
            name: k.name,
            key_prefix: k.key_prefix,
            created_at: k.created_at,
            last_used_at: k.last_used_at,
            expires_at: k.expires_at,
            revoked_at: k.revoked_at,
        }
    }
}

async fn issue_key(
    State(ctx): State<AppContext>,
    Extension(req_ctx): Extension<RequestContext>,
    Json(body): Json<IssueApiKeyRequest>,
) -> Result<impl IntoResponse, ApiError> {
    let target_user = resolve_target_user(&req_ctx, body.target_user_id.as_deref())?;
    let service = ApiKeyService::new(ctx.db_pool())
        .map_err(|e| ApiError::internal_error(format!("API key service error: {e}")))?;

    let issued = service
        .issue(IssueApiKeyParams {
            user_id: &target_user,
            name: &body.name,
            expires_at: body.expires_at,
        })
        .await
        .map_err(|e| ApiError::internal_error(format!("Failed to issue API key: {e}")))?;

    Ok((
        StatusCode::CREATED,
        Json(IssueApiKeyResponse {
            id: issued.record.id.as_str().to_string(),
            name: issued.record.name,
            key_prefix: issued.record.key_prefix,
            secret: issued.secret,
            created_at: issued.record.created_at,
            expires_at: issued.record.expires_at,
        }),
    ))
}

async fn list_keys(
    State(ctx): State<AppContext>,
    Extension(req_ctx): Extension<RequestContext>,
) -> Result<Json<Vec<ApiKeyView>>, ApiError> {
    let service = ApiKeyService::new(ctx.db_pool())
        .map_err(|e| ApiError::internal_error(format!("API key service error: {e}")))?;

    let keys = service
        .list_for_user(req_ctx.user_id())
        .await
        .map_err(|e| ApiError::internal_error(format!("Failed to list API keys: {e}")))?;

    Ok(Json(keys.into_iter().map(ApiKeyView::from).collect()))
}

async fn revoke_key(
    State(ctx): State<AppContext>,
    Extension(req_ctx): Extension<RequestContext>,
    Path(key_id): Path<String>,
) -> Result<StatusCode, ApiError> {
    let service = ApiKeyService::new(ctx.db_pool())
        .map_err(|e| ApiError::internal_error(format!("API key service error: {e}")))?;

    let id = ApiKeyId::new(key_id);
    let revoked = service
        .revoke(&id, req_ctx.user_id())
        .await
        .map_err(|e| ApiError::internal_error(format!("Failed to revoke API key: {e}")))?;

    if revoked {
        Ok(StatusCode::NO_CONTENT)
    } else {
        Err(ApiError::not_found("API key not found"))
    }
}

#[allow(clippy::result_large_err)]
fn resolve_target_user(
    req_ctx: &RequestContext,
    override_user_id: Option<&str>,
) -> Result<UserId, ApiError> {
    match override_user_id {
        Some(value) if !value.is_empty() => {
            if req_ctx.user_type() != systemprompt_models::auth::UserType::Admin {
                return Err(ApiError::forbidden(
                    "Only admins can issue keys for other users",
                ));
            }
            Ok(UserId::new(value.to_string()))
        },
        _ => Ok(req_ctx.user_id().clone()),
    }
}