use std::path::{Component, Path};
use std::sync::Arc;
use axum::body::Body;
use axum::extract::Path as AxumPath;
use axum::http::{HeaderMap, header};
use axum::response::Response;
use systemprompt_config::ProfileBootstrap;
use systemprompt_identifiers::{PluginId, UserId};
use systemprompt_marketplace::ResolvedCatalog;
use systemprompt_runtime::AppContext;
use super::bridge_error::{BridgeError, authenticate_bridge};
use super::{bridge_plugin_archive, bridge_resolved, bridge_services_cache};
use crate::error::ApiHttpError;
use crate::services::middleware::JwtContextExtractor;
pub async fn handle(
jwt_extractor: Arc<JwtContextExtractor>,
ctx: AppContext,
headers: HeaderMap,
AxumPath((plugin_id, relative_path)): AxumPath<(String, String)>,
) -> Result<Response, ApiHttpError> {
let (_claims, user) = authenticate_bridge(&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(BridgeError::InvalidPath.into());
}
let id = PluginId::try_new(&plugin_id).map_err(|e| {
tracing::debug!(error = %e, plugin_id = %plugin_id, "bridge: malformed plugin id");
BridgeError::PluginNotFound
})?;
if let Some(sha256) = bridge_plugin_archive::archive_sha(&relative_path) {
return bridge_plugin_archive::serve(&ctx, &user.id, &id, sha256).await;
}
let resolved = resolve_entitled(&ctx, &user.id, &id).await?;
let bundle = resolved
.bundles
.get(&id)
.ok_or(BridgeError::PluginNotFound)?;
let file = bundle
.get(relative_path.as_str())
.ok_or(BridgeError::FileNotFound)?;
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)
}
pub(super) async fn resolve_entitled(
ctx: &AppContext,
user_id: &UserId,
id: &PluginId,
) -> Result<Arc<ResolvedCatalog>, BridgeError> {
let grants =
|resolved: &ResolvedCatalog| resolved.candidate.plugins.iter().any(|p| &p.id == id);
if let Some(latest) = ctx.marketplace_cache().latest_resolved(user_id)
&& grants(&latest)
{
return Ok(latest);
}
let services = bridge_services_cache::load_services_config()
.map_err(|e| BridgeError::internal("plugin bundle: services config load failed", e))?;
let profile = ProfileBootstrap::get()
.map_err(|e| BridgeError::internal("plugin bundle: profile not ready", e))?;
let resolved = bridge_resolved::resolve_for_user(
ctx,
&services,
profile,
user_id,
bridge_resolved::Freshness::Memo,
)
.await
.map_err(|e| BridgeError::internal("plugin bundle: catalogue resolution failed", e))?;
if !grants(&resolved) {
tracing::warn!(
plugin_id = %id,
user_id = %user_id,
"bridge: refused a plugin bundle the caller was not granted"
);
return Err(BridgeError::PluginNotFound);
}
Ok(resolved)
}
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))
}