1use crate::core::registry::{StagingManager, ValidationWorker, ValidationWorkerConfig};
4use crate::core::service::{FastSkillService, ServiceConfig};
5use crate::http::handlers::{
6 claude_api, manifest, registry, registry_publish, reindex, resolve, search, skills, status,
7 AppState,
8};
9use axum::{
10 body::Body,
11 extract::Request,
12 http::{header, HeaderName, HeaderValue, Method, StatusCode},
13 response::Response,
14 routing::{delete, get, post, put},
15 Router,
16};
17use include_dir::{include_dir, Dir};
18use std::env;
19use std::net::SocketAddr;
20use std::path::PathBuf;
21use std::str::FromStr;
22use std::sync::Arc;
23use tower::ServiceBuilder;
24use tower_http::cors::{AllowOrigin, CorsLayer};
25use tower_http::{compression::CompressionLayer, trace::TraceLayer};
26use tracing::info;
27
28static EMBEDDED_STATIC: Dir<'_> = include_dir!("$CARGO_MANIFEST_DIR/src/http/static");
30
31async fn serve_embedded_static(req: Request) -> Result<Response, StatusCode> {
33 let path = req.uri().path().trim_start_matches('/');
34 let name = match path {
35 "" | "index.html" => "index.html",
36 "app.js" => "app.js",
37 "styles.css" => "styles.css",
38 _ => return Err(StatusCode::NOT_FOUND),
39 };
40 let file = EMBEDDED_STATIC
41 .get_file(name)
42 .ok_or(StatusCode::NOT_FOUND)?;
43 let body = file.contents();
44 let content_type: HeaderValue = match name {
45 "index.html" => HeaderValue::from_static("text/html; charset=utf-8"),
46 "app.js" => HeaderValue::from_static("application/javascript; charset=utf-8"),
47 "styles.css" => HeaderValue::from_static("text/css; charset=utf-8"),
48 _ => HeaderValue::from_static("application/octet-stream"),
49 };
50 Response::builder()
51 .status(StatusCode::OK)
52 .header(header::CONTENT_TYPE, content_type)
53 .body(Body::from(body))
54 .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)
55}
56
57fn validate_blob_storage_fields(blob_config: &crate::core::BlobStorageConfig) -> Vec<&'static str> {
59 let mut missing = Vec::new();
60
61 if blob_config.storage_type.is_empty() {
62 missing.push("storage_type");
63 }
64 if blob_config.bucket.is_empty() {
65 missing.push("bucket");
66 }
67 if blob_config.region.is_empty() {
68 missing.push("region");
69 }
70 if blob_config.access_key.is_empty() {
71 missing.push("access_key");
72 }
73 if blob_config.secret_key.is_empty() {
74 missing.push("secret_key");
75 }
76
77 missing
78}
79
80fn build_validation_error_message(
82 missing: &[&str],
83 incomplete_fields: &[(&str, Vec<&str>)],
84) -> String {
85 let mut error_msg =
86 String::from("Registry enabled but required configuration is missing or incomplete:\n");
87
88 if !missing.is_empty() {
89 error_msg.push_str(" Missing configuration:\n");
90 for item in missing {
91 error_msg.push_str(&format!(" - {}\n", item));
92 }
93 }
94
95 if !incomplete_fields.is_empty() {
96 error_msg.push_str(" Incomplete configuration:\n");
97 for (config_name, fields) in incomplete_fields {
98 error_msg.push_str(&format!(" {} is missing fields:\n", config_name));
99 for field in fields {
100 error_msg.push_str(&format!(" - {}\n", field));
101 }
102 }
103 }
104
105 error_msg.push_str("\nS3 configuration is required for operational registry publishing.");
106 error_msg
107}
108
109fn validate_registry_config(config: &ServiceConfig) -> Result<(), String> {
111 let mut missing = Vec::new();
112 let mut incomplete_fields = Vec::new();
113
114 match &config.registry_blob_storage {
116 None => {
117 missing.push("registry_blob_storage");
118 }
119 Some(blob_config) => {
120 let blob_missing = validate_blob_storage_fields(blob_config);
121 if !blob_missing.is_empty() {
122 incomplete_fields.push(("registry_blob_storage", blob_missing));
123 }
124 }
125 }
126
127 if missing.is_empty() && incomplete_fields.is_empty() {
128 return Ok(());
129 }
130
131 Err(build_validation_error_message(&missing, &incomplete_fields))
132}
133
134fn display_startup_banner() {
136 let _version = crate::VERSION;
138}
139
140fn get_allowed_methods() -> [Method; 5] {
142 [
143 Method::GET,
144 Method::POST,
145 Method::PUT,
146 Method::DELETE,
147 Method::OPTIONS,
148 ]
149}
150
151fn build_default_cors_layer() -> CorsLayer {
153 CorsLayer::new()
154 .allow_methods(get_allowed_methods())
155 .allow_origin(AllowOrigin::exact(HeaderValue::from_static("")))
156}
157
158fn parse_origins(origins: &[String]) -> Result<Vec<HeaderValue>, String> {
160 origins
161 .iter()
162 .map(|origin| {
163 HeaderValue::from_str(origin)
164 .map_err(|e| format!("Invalid origin header value '{}': {}", origin, e))
165 })
166 .collect()
167}
168
169fn parse_headers(headers: &[String]) -> Result<Vec<HeaderName>, String> {
171 headers
172 .iter()
173 .map(|header| {
174 HeaderName::from_str(header)
175 .map_err(|e| format!("Invalid header name '{}': {}", header, e))
176 })
177 .collect()
178}
179
180fn build_configured_cors_layer(
182 origin_header_values: Vec<HeaderValue>,
183 allowed_origins: &[String],
184 allowed_headers: &[String],
185) -> CorsLayer {
186 info!(
187 "Configuring CORS for {} origins: {}",
188 origin_header_values.len(),
189 allowed_origins.join(", ")
190 );
191
192 let header_names = match parse_headers(allowed_headers) {
193 Ok(values) => values,
194 Err(e) => {
195 tracing::error!("Failed to build CORS headers: {}", e);
196 return CorsLayer::new()
197 .allow_methods(get_allowed_methods())
198 .allow_origin(AllowOrigin::list(origin_header_values))
199 .allow_headers([header::CONTENT_TYPE, header::AUTHORIZATION]);
200 }
201 };
202
203 CorsLayer::new()
204 .allow_methods(get_allowed_methods())
205 .allow_origin(AllowOrigin::list(origin_header_values))
206 .allow_headers(header_names)
207 .allow_credentials(true)
208}
209
210pub fn build_cors_layer(config: &crate::core::service::ServiceConfig) -> CorsLayer {
212 let http_config = match config.http_server.as_ref() {
213 None => {
214 info!("No CORS configuration found - denying all origins");
215 return build_default_cors_layer();
216 }
217 Some(cfg) => cfg,
218 };
219
220 if http_config.allowed_origins.is_empty() {
221 info!("Empty CORS allowed_origins - denying all origins");
222 return build_default_cors_layer();
223 }
224
225 let origin_header_values = match parse_origins(&http_config.allowed_origins) {
226 Ok(values) => values,
227 Err(e) => {
228 tracing::error!("Failed to build CORS origins: {}", e);
229 return build_default_cors_layer();
230 }
231 };
232
233 build_configured_cors_layer(
234 origin_header_values,
235 &http_config.allowed_origins,
236 &http_config.allowed_headers,
237 )
238}
239
240pub struct FastSkillServer {
242 service: Arc<FastSkillService>,
243 addr: SocketAddr,
244}
245
246impl FastSkillServer {
247 pub fn new(service: Arc<FastSkillService>, host: &str, port: u16) -> Self {
249 let addr = match Self::parse_address(host, port) {
250 Ok(addr) => addr,
251 Err(e) => {
252 eprintln!("Invalid address: {}:{} - {}", host, port, e);
253 std::process::exit(1);
254 }
255 };
256
257 Self { service, addr }
258 }
259
260 fn parse_address(host: &str, port: u16) -> Result<SocketAddr, String> {
262 let normalized_host = Self::normalize_host(host);
264
265 let addr_str = if normalized_host.contains(':') {
267 format!("[{}]:{}", normalized_host, port)
269 } else {
270 format!("{}:{}", normalized_host, port)
272 };
273
274 addr_str.parse().map_err(|_| {
275 format!(
276 "Unable to parse address '{}'. Use IP addresses like '127.0.0.1', '0.0.0.0', '::1', or hostnames that resolve to IP addresses",
277 addr_str
278 )
279 })
280 }
281
282 fn normalize_host(host: &str) -> String {
284 match host {
285 "localhost" => "127.0.0.1".to_string(),
287 "::1" | "[::1]" => "::1".to_string(),
289 "::" | "[::]" => "::".to_string(),
291 _ => host.to_string(),
293 }
294 }
295
296 pub fn from_ref(service: &Arc<FastSkillService>, host: &str, port: u16) -> Self {
298 let service_arc = Arc::clone(service);
299
300 let addr = match Self::parse_address(host, port) {
301 Ok(addr) => addr,
302 Err(e) => {
303 eprintln!("Invalid address: {}:{} - {}", host, port, e);
304 std::process::exit(1);
305 }
306 };
307
308 Self {
309 service: service_arc,
310 addr,
311 }
312 }
313
314 fn create_skill_routes() -> Router<AppState> {
316 Router::new()
317 .route("/api/skills", get(skills::list_skills))
318 .route("/api/skills", post(skills::create_skill))
319 .route("/api/skills/:id", get(skills::get_skill))
320 .route("/api/skills/:id", put(skills::update_skill))
321 .route("/api/skills/:id", delete(skills::delete_skill))
322 .route("/api/skills/upgrade", post(skills::upgrade_skills))
323 .route("/api/project", get(manifest::get_project))
324 }
325
326 fn create_search_routes() -> Router<AppState> {
328 Router::new()
329 .route("/api/search", post(search::search_skills))
330 .route("/api/resolve", post(resolve::resolve_context))
331 .route("/api/reindex", post(reindex::reindex_all))
332 .route("/api/reindex/:id", post(reindex::reindex_skill))
333 }
334
335 fn create_status_routes() -> Router<AppState> {
337 Router::new().route("/api/status", get(status::status))
338 }
339
340 fn create_claude_api_routes() -> Router<AppState> {
342 Router::new()
343 .route("/v1/skills", post(claude_api::create_skill))
344 .route("/v1/skills", get(claude_api::list_skills))
345 .route("/v1/skills/:skill_id", get(claude_api::get_skill))
346 .route("/v1/skills/:skill_id", delete(claude_api::delete_skill))
347 .route(
348 "/v1/skills/:skill_id/versions",
349 post(claude_api::create_skill_version),
350 )
351 .route(
352 "/v1/skills/:skill_id/versions",
353 get(claude_api::list_skill_versions),
354 )
355 .route(
356 "/v1/skills/:skill_id/versions/:version",
357 get(claude_api::get_skill_version),
358 )
359 .route(
360 "/v1/skills/:skill_id/versions/:version",
361 delete(claude_api::delete_skill_version),
362 )
363 }
364
365 fn create_ui_routes() -> Router<AppState> {
367 info!("Serving UI at /");
368 Router::new()
369 .route("/dashboard", get(status::root))
370 .route("/", get(serve_embedded_static))
371 .route("/index.html", get(serve_embedded_static))
372 .route("/app.js", get(serve_embedded_static))
373 .route("/styles.css", get(serve_embedded_static))
374 }
375
376 fn create_registry_routes() -> Router<AppState> {
378 Router::new()
379 .route("/index/*skill_id", get(registry::serve_index_file))
380 .route(
381 "/api/registry/index/skills",
382 get(registry::list_index_skills),
383 )
384 .route("/api/registry/sources", get(registry::list_sources))
385 .route("/api/registry/skills", get(registry::list_all_skills))
386 .route(
387 "/api/registry/sources/:name/skills",
388 get(registry::list_source_skills),
389 )
390 .route(
391 "/api/registry/sources/:name/marketplace",
392 get(registry::get_marketplace),
393 )
394 .route("/api/registry/refresh", post(registry::refresh_sources))
395 .route(
396 "/api/registry/publish",
397 post(registry_publish::publish_package),
398 )
399 .route(
400 "/api/registry/publish/status/:job_id",
401 get(registry_publish::get_publish_status),
402 )
403 }
404
405 fn create_manifest_routes() -> Router<AppState> {
407 Router::new()
408 .route("/api/manifest/skills", get(manifest::list_manifest_skills))
409 .route(
410 "/api/manifest/skills",
411 post(manifest::add_skill_to_manifest),
412 )
413 .route(
414 "/api/manifest/skills/:id",
415 put(manifest::update_skill_in_manifest),
416 )
417 .route(
418 "/api/manifest/skills/:id",
419 delete(manifest::remove_skill_from_manifest),
420 )
421 }
422
423 fn create_router(&self) -> Result<Router, Box<dyn std::error::Error>> {
425 let current_dir = env::current_dir()?;
427 let config = crate::core::load_project_config(¤t_dir)
428 .map_err(|e| format!("Failed to load project config: {}", e))?;
429
430 let state = AppState::new(self.service.clone())?.with_project_config(
431 config.project_root,
432 config.project_file_path,
433 config.skills_directory,
434 );
435
436 let router = Router::new()
438 .merge(Self::create_skill_routes())
439 .merge(Self::create_search_routes())
440 .merge(Self::create_status_routes())
441 .merge(Self::create_claude_api_routes())
442 .merge(Self::create_ui_routes())
443 .merge(Self::create_registry_routes())
444 .merge(Self::create_manifest_routes());
445
446 Ok(router
448 .layer(
449 ServiceBuilder::new()
450 .layer(TraceLayer::new_for_http())
451 .layer(CompressionLayer::new())
452 .layer(build_cors_layer(self.service.config())),
453 )
454 .with_state(state))
455 }
456
457 pub async fn serve(self) -> Result<(), Box<dyn std::error::Error>> {
459 display_startup_banner();
461
462 let app = self.create_router()?;
463
464 let config = self.service.config();
466 if validate_registry_config(config).is_ok() {
467 let staging_dir = config
468 .staging_dir
469 .clone()
470 .unwrap_or_else(|| PathBuf::from(".staging"));
471
472 let staging_manager = StagingManager::new(staging_dir);
473 if staging_manager.initialize().is_ok() {
474 let worker_config = ValidationWorkerConfig {
475 poll_interval_secs: 5,
476 blob_storage_config: config.registry_blob_storage.clone(),
477 registry_index_path: config.registry_index_path.clone(),
478 blob_base_url: config.registry_blob_base_url.clone(),
479 };
480 let worker = ValidationWorker::new(staging_manager, worker_config);
481 worker.start();
482 info!("Validation worker started");
483 }
484 }
485
486 info!("Starting FastSkill HTTP server on {}", self.addr);
487
488 let listener = tokio::net::TcpListener::bind(self.addr).await?;
489 let actual_addr = listener.local_addr()?;
490 info!("Server bound to {}", actual_addr);
491
492 axum::serve(listener, app).await?;
493
494 Ok(())
495 }
496
497 pub fn addr(&self) -> SocketAddr {
499 self.addr
500 }
501}
502
503pub async fn serve(
505 service: Arc<FastSkillService>,
506 host: &str,
507 port: u16,
508) -> Result<(), Box<dyn std::error::Error>> {
509 let server = FastSkillServer::new(service, host, port);
510 server.serve().await
511}