use std::path::{Component, Path};
use std::sync::Arc;
use axum::body::Body;
use axum::extract::Path as AxumPath;
use axum::http::{HeaderMap, StatusCode, header};
use axum::response::Response;
use systemprompt_config::ProfileBootstrap;
use systemprompt_identifiers::JwtToken;
use systemprompt_models::bridge::ids::PluginId;
use systemprompt_runtime::AppContext;
use super::messages::extract_credential;
use super::{bridge_data, bridge_resolved};
use crate::services::middleware::JwtContextExtractor;
type HttpError = (StatusCode, String);
pub async fn handle(
jwt_extractor: Arc<JwtContextExtractor>,
ctx: AppContext,
headers: HeaderMap,
AxumPath((plugin_id, relative_path)): AxumPath<(String, String)>,
) -> Result<Response, HttpError> {
let user = authenticate(&jwt_extractor, &headers).await?;
if !relative_path_is_safe(&relative_path) {
tracing::warn!(
plugin_id = %plugin_id,
path = %relative_path,
"bridge: rejected non-canonical plugin file path"
);
return Err((StatusCode::BAD_REQUEST, "Invalid path".to_owned()));
}
let id = PluginId::try_new(&plugin_id).map_err(|e| {
tracing::debug!(error = %e, plugin_id = %plugin_id, "bridge: malformed plugin id");
(StatusCode::NOT_FOUND, "Plugin not found".to_owned())
})?;
let services = bridge_data::load_services_config().map_err(|e| internal("services", &e))?;
let profile = ProfileBootstrap::get().map_err(|e| internal("profile", &e))?;
let resolved = bridge_resolved::resolve_for_user(&ctx, &services, profile, &user.id)
.await
.map_err(|e| internal("resolve", &e))?;
if !resolved.candidate.plugins.iter().any(|p| p.id == id) {
tracing::warn!(
plugin_id = %plugin_id,
user_id = %user.id,
"bridge: refused a plugin bundle the caller was not granted"
);
return Err((StatusCode::NOT_FOUND, "Plugin not found".to_owned()));
}
let bundle = resolved
.bundles
.get(&id)
.ok_or_else(|| (StatusCode::NOT_FOUND, "Plugin not found".to_owned()))?;
let file = bundle
.get(relative_path.as_str())
.ok_or_else(|| (StatusCode::NOT_FOUND, "File not found".to_owned()))?;
let mut response = Response::new(Body::from(file.bytes.clone()));
response.headers_mut().insert(
header::CONTENT_TYPE,
header::HeaderValue::from_static(content_type(&relative_path)),
);
Ok(response)
}
fn internal(stage: &'static str, e: &dyn std::fmt::Display) -> HttpError {
tracing::error!(error = %e, stage, "bridge: plugin bundle assembly failed");
(
StatusCode::INTERNAL_SERVER_ERROR,
"Plugin bundle unavailable".to_owned(),
)
}
async fn authenticate(
jwt_extractor: &JwtContextExtractor,
headers: &HeaderMap,
) -> Result<systemprompt_traits::AuthUser, HttpError> {
let credential = extract_credential(headers).ok_or_else(|| {
(
StatusCode::UNAUTHORIZED,
"Missing Authorization or x-api-key credential".to_owned(),
)
})?;
jwt_extractor
.decode_for_gateway(&JwtToken::new(credential))
.await
.map(|(_, user)| user)
.map_err(|e| (StatusCode::UNAUTHORIZED, e.to_string()))
}
pub fn relative_path_is_safe(relative: &str) -> bool {
!relative.is_empty()
&& Path::new(relative)
.components()
.all(|c| matches!(c, Component::Normal(_) | Component::CurDir))
}
pub fn content_type(relative_path: &str) -> &'static str {
systemprompt_models::mime::http_content_type(Path::new(relative_path))
}