fix(maestro): stop embedding web bundle in the registry
The registry serves no UI — that is maestro-web's job (connection- orchestration spec §"UI hosting"). The rust-embed of web/dist was legacy from NAP-218, before maestro-web was extracted (NAP-241), and it made the registry (and thus the umbrella maestro binary) fail to compile whenever web/dist was absent — e.g. a fresh cargo install --git after the tracked .gitkeep was dropped. Remove web_assets.rs, the rust-embed dep, and the / + /assets routes; the registry is now JSON/WS API only. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -19,7 +19,6 @@ jsonwebtoken.workspace = true
|
||||
maestro-config.workspace = true
|
||||
maestro-protocol.workspace = true
|
||||
rand.workspace = true
|
||||
rust-embed.workspace = true
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
thiserror.workspace = true
|
||||
|
||||
@@ -18,7 +18,6 @@ use uuid::Uuid;
|
||||
use crate::events::events_ws;
|
||||
use crate::jwt::issue_attach_jwt;
|
||||
use crate::state::{Registry, RegistryError, RPC_TIMEOUT};
|
||||
use crate::web_assets;
|
||||
|
||||
/// Shared state plumbed into client-facing handlers.
|
||||
#[derive(Debug, Clone)]
|
||||
@@ -31,9 +30,9 @@ pub struct ClientApiState {
|
||||
/// Mount client-facing routes on the given router. Caller is responsible for
|
||||
/// applying the cookie middleware (we do that in [`crate::server::router`]).
|
||||
pub fn routes(state: ClientApiState) -> Router {
|
||||
// The registry serves no web UI — that's `maestro-web`'s job (see
|
||||
// connection-orchestration spec §"UI hosting"). Only the JSON/WS API here.
|
||||
Router::new()
|
||||
.route("/", get(index))
|
||||
.route("/assets/*file", get(asset))
|
||||
.route("/api/daemons", get(list_daemons))
|
||||
.route("/api/sessions", get(list_sessions).post(create_session))
|
||||
.route("/api/sessions/:id", axum::routing::delete(delete_session))
|
||||
@@ -42,14 +41,6 @@ pub fn routes(state: ClientApiState) -> Router {
|
||||
.with_state(state)
|
||||
}
|
||||
|
||||
async fn index() -> Response {
|
||||
web_assets::serve_index()
|
||||
}
|
||||
|
||||
async fn asset(Path(file): Path<String>) -> Response {
|
||||
web_assets::serve_asset(&file)
|
||||
}
|
||||
|
||||
async fn list_daemons(State(s): State<ClientApiState>) -> Json<Vec<DaemonInfo>> {
|
||||
let infos: Vec<DaemonInfo> = s
|
||||
.registry
|
||||
|
||||
@@ -21,7 +21,6 @@ pub mod run;
|
||||
pub mod server;
|
||||
pub mod state;
|
||||
pub mod token;
|
||||
pub mod web_assets;
|
||||
|
||||
pub use run::run;
|
||||
|
||||
|
||||
@@ -1,134 +0,0 @@
|
||||
//! Embedded static assets for the web client bundle.
|
||||
//!
|
||||
//! The Vite build in `web/` writes its output to `web/dist/`; `rust-embed`
|
||||
//! picks that up at `cargo build` time and embeds the files into the binary.
|
||||
//! That keeps deploy artifacts to a single executable.
|
||||
//!
|
||||
//! When the `web/dist` tree is empty (typical for a fresh checkout without
|
||||
//! `pnpm build`), the handlers below fall back to a "build the web bundle
|
||||
//! first" placeholder so the rest of the registry still works for testing.
|
||||
//!
|
||||
//! See NAP-218.
|
||||
use axum::http::{header, StatusCode};
|
||||
use axum::response::{IntoResponse, Response};
|
||||
use rust_embed::RustEmbed;
|
||||
|
||||
#[derive(RustEmbed)]
|
||||
#[folder = "../../web/dist/"]
|
||||
struct WebAssets;
|
||||
|
||||
const FALLBACK_INDEX: &str = "<!doctype html><meta charset=utf-8><title>maestro</title>\
|
||||
<h1>maestro registry</h1>\
|
||||
<p>Web bundle not built. Run <code>pnpm --dir web install</code> \
|
||||
then <code>pnpm --dir web build</code>, then rebuild the registry.</p>";
|
||||
|
||||
fn content_type_for(path: &str) -> &'static str {
|
||||
let ext = path.rsplit('.').next().unwrap_or("");
|
||||
match ext {
|
||||
"html" | "htm" => "text/html; charset=utf-8",
|
||||
"js" | "mjs" => "text/javascript; charset=utf-8",
|
||||
"css" => "text/css; charset=utf-8",
|
||||
"json" => "application/json",
|
||||
"svg" => "image/svg+xml",
|
||||
"png" => "image/png",
|
||||
"ico" => "image/x-icon",
|
||||
"woff" => "font/woff",
|
||||
"woff2" => "font/woff2",
|
||||
"map" => "application/json",
|
||||
"txt" => "text/plain; charset=utf-8",
|
||||
_ => "application/octet-stream",
|
||||
}
|
||||
}
|
||||
|
||||
fn cache_headers(path: &str) -> &'static str {
|
||||
// Vite emits hashed filenames under /assets/. Those can be cached
|
||||
// aggressively. Everything else (index.html, the favicon, etc.) should
|
||||
// not be cached.
|
||||
if path.starts_with("assets/") {
|
||||
"public, max-age=31536000, immutable"
|
||||
} else {
|
||||
"no-cache"
|
||||
}
|
||||
}
|
||||
|
||||
fn serve(path: &str) -> Option<Response> {
|
||||
let asset = WebAssets::get(path)?;
|
||||
let body = asset.data.into_owned();
|
||||
Some(
|
||||
(
|
||||
[
|
||||
(header::CONTENT_TYPE, content_type_for(path)),
|
||||
(header::CACHE_CONTROL, cache_headers(path)),
|
||||
],
|
||||
body,
|
||||
)
|
||||
.into_response(),
|
||||
)
|
||||
}
|
||||
|
||||
/// Serve `dist/index.html` from the embed; fall back to a placeholder
|
||||
/// pointing the operator at `pnpm build` when the bundle hasn't been built.
|
||||
pub fn serve_index() -> Response {
|
||||
if let Some(resp) = serve("index.html") {
|
||||
return resp;
|
||||
}
|
||||
(
|
||||
StatusCode::OK,
|
||||
[(header::CONTENT_TYPE, "text/html; charset=utf-8")],
|
||||
FALLBACK_INDEX,
|
||||
)
|
||||
.into_response()
|
||||
}
|
||||
|
||||
/// Serve a hashed asset under `dist/assets/<file>`.
|
||||
pub fn serve_asset(file: &str) -> Response {
|
||||
// Reject anything that tries to escape the `assets/` prefix. `rust-embed`
|
||||
// already won't return parent-directory paths, but we keep the explicit
|
||||
// check so the surface is obvious.
|
||||
if file.contains("..") || file.starts_with('/') {
|
||||
return StatusCode::NOT_FOUND.into_response();
|
||||
}
|
||||
let path = format!("assets/{file}");
|
||||
match serve(&path) {
|
||||
Some(resp) => resp,
|
||||
None => StatusCode::NOT_FOUND.into_response(),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn content_type_routes_common_extensions() {
|
||||
assert_eq!(content_type_for("foo.js"), "text/javascript; charset=utf-8");
|
||||
assert_eq!(content_type_for("a/b.css"), "text/css; charset=utf-8");
|
||||
assert_eq!(content_type_for("x.svg"), "image/svg+xml");
|
||||
assert_eq!(content_type_for("index.html"), "text/html; charset=utf-8");
|
||||
assert_eq!(content_type_for("noext"), "application/octet-stream");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cache_assets_immutable_index_no_cache() {
|
||||
assert!(cache_headers("assets/index-abc.js").contains("immutable"));
|
||||
assert_eq!(cache_headers("index.html"), "no-cache");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn serve_asset_rejects_traversal() {
|
||||
let resp = serve_asset("../secret");
|
||||
assert_eq!(resp.status(), StatusCode::NOT_FOUND);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn serve_index_returns_html() {
|
||||
let resp = serve_index();
|
||||
assert_eq!(resp.status(), StatusCode::OK);
|
||||
let ct = resp
|
||||
.headers()
|
||||
.get(header::CONTENT_TYPE)
|
||||
.and_then(|v| v.to_str().ok())
|
||||
.unwrap_or("");
|
||||
assert!(ct.starts_with("text/html"));
|
||||
}
|
||||
}
|
||||
@@ -666,13 +666,13 @@ async fn events_ws_pushes_snapshot_then_deltas() {
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn index_serves_placeholder() {
|
||||
async fn root_serves_no_ui() {
|
||||
// The registry serves no web UI — that's `maestro-web`'s job (see
|
||||
// connection-orchestration spec §"UI hosting"). `/` has no route.
|
||||
let h = spawn(Duration::from_secs(5)).await;
|
||||
let client = client_with_cookies();
|
||||
let resp = client.get(h.base()).send().await.unwrap();
|
||||
assert!(resp.status().is_success());
|
||||
let body = resp.text().await.unwrap();
|
||||
assert!(body.contains("maestro"));
|
||||
assert_eq!(resp.status(), reqwest::StatusCode::NOT_FOUND);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
Reference in New Issue
Block a user