systemprompt_api/routes/gateway/bridge_release/
mod.rs1use std::sync::Arc;
13
14use axum::Json;
15use axum::body::Body;
16use axum::extract::{Path, Query};
17use axum::http::{HeaderMap, StatusCode, header};
18use axum::response::{IntoResponse, Response};
19use serde::{Deserialize, Serialize};
20use systemprompt_identifiers::JwtToken;
21use systemprompt_loader::ServicesBootstrap;
22use systemprompt_models::services::BridgeReleasesSpec;
23
24mod github;
25
26pub use self::github::parse_sha256sums;
27
28use self::github::{asset_digest, github, resolve_release};
29
30use super::messages::extract_credential;
31use crate::services::middleware::JwtContextExtractor;
32
33
34#[derive(Debug, Deserialize)]
35pub struct LatestQuery {
36 pub platform: String,
37}
38
39#[derive(Debug, Serialize)]
44pub struct ReleaseManifest {
45 pub version: String,
46 pub sha256: String,
47 pub size: u64,
48 #[serde(skip_serializing_if = "Option::is_none")]
49 pub notes_url: Option<String>,
50}
51
52pub async fn latest(
53 jwt_extractor: Arc<JwtContextExtractor>,
54 headers: HeaderMap,
55 Query(query): Query<LatestQuery>,
56) -> Result<Json<ReleaseManifest>, (StatusCode, String)> {
57 authenticate(&jwt_extractor, &headers).await?;
58 let spec = releases_spec()?;
59
60 let asset_name = spec.assets.get(&query.platform).ok_or_else(|| {
61 (
62 StatusCode::NOT_FOUND,
63 format!("no published build for platform {}", query.platform),
64 )
65 })?;
66
67 let release = resolve_release(&spec).await?;
68 let asset = release
69 .assets
70 .iter()
71 .find(|a| a.name == *asset_name)
72 .ok_or_else(|| {
73 (
74 StatusCode::NOT_FOUND,
75 format!("release {} has no asset {asset_name}", release.tag_name),
76 )
77 })?;
78
79 let version = release
80 .tag_name
81 .strip_prefix(&spec.tag_prefix)
82 .unwrap_or(&release.tag_name)
83 .to_owned();
84 let sha256 = asset_digest(&spec, &release, asset_name).await?;
85
86 Ok(Json(ReleaseManifest {
87 version,
88 sha256,
89 size: asset.size,
90 notes_url: release.html_url.clone(),
91 }))
92}
93
94pub async fn download(
95 jwt_extractor: Arc<JwtContextExtractor>,
96 headers: HeaderMap,
97 Path(platform): Path<String>,
98) -> Result<Response, (StatusCode, String)> {
99 authenticate(&jwt_extractor, &headers).await?;
100 let spec = releases_spec()?;
101
102 let asset_name = spec.assets.get(&platform).ok_or_else(|| {
103 (
104 StatusCode::NOT_FOUND,
105 format!("no published build for platform {platform}"),
106 )
107 })?;
108
109 let release = resolve_release(&spec).await?;
110 let asset = release
111 .assets
112 .iter()
113 .find(|a| a.name == *asset_name)
114 .ok_or_else(|| {
115 (
116 StatusCode::NOT_FOUND,
117 format!("release {} has no asset {asset_name}", release.tag_name),
118 )
119 })?;
120
121 let upstream = github(&spec, &asset.url)
124 .header(header::ACCEPT, "application/octet-stream")
125 .send()
126 .await
127 .map_err(|e| (StatusCode::BAD_GATEWAY, format!("asset fetch failed: {e}")))?;
128
129 if !upstream.status().is_success() {
130 return Err((
131 StatusCode::BAD_GATEWAY,
132 format!("asset fetch returned {}", upstream.status()),
133 ));
134 }
135
136 let body = Body::from_stream(upstream.bytes_stream());
139 Ok((
140 StatusCode::OK,
141 [
142 (header::CONTENT_TYPE, "application/octet-stream".to_owned()),
143 (
144 header::CONTENT_DISPOSITION,
145 format!("attachment; filename=\"{asset_name}\""),
146 ),
147 ],
148 body,
149 )
150 .into_response())
151}
152
153async fn authenticate(
154 jwt_extractor: &Arc<JwtContextExtractor>,
155 headers: &HeaderMap,
156) -> Result<(), (StatusCode, String)> {
157 let credential = extract_credential(headers).ok_or_else(|| {
158 (
159 StatusCode::UNAUTHORIZED,
160 "Missing Authorization or x-api-key credential".to_owned(),
161 )
162 })?;
163 jwt_extractor
164 .decode_for_gateway(&JwtToken::new(credential))
165 .await
166 .map_err(|e| (StatusCode::UNAUTHORIZED, e.to_string()))?;
167 Ok(())
168}
169
170fn releases_spec() -> Result<BridgeReleasesSpec, (StatusCode, String)> {
171 let services = ServicesBootstrap::get().map_err(|e| {
172 (
173 StatusCode::SERVICE_UNAVAILABLE,
174 format!("Services config not ready: {e}"),
175 )
176 })?;
177 services
178 .gateway_config()
179 .and_then(|g| g.bridge_releases.clone())
180 .ok_or_else(|| {
181 (
182 StatusCode::NOT_FOUND,
183 "bridge releases are not configured on this gateway".to_owned(),
184 )
185 })
186}