pub mod auth;
pub mod discovery;
pub mod error;
pub mod files;
pub mod harnesses;
pub mod manage;
pub mod sessions;
pub mod share;
pub mod state;
pub mod streaming;
pub mod tasks;
pub mod version;
use std::env;
use axum::middleware;
use axum::routing::{delete, get, post};
use axum::Router;
use crate::state::AppState;
pub const BASE_PATH_ENV: &str = "UHP_BASE_PATH";
pub const DEFAULT_BASE_PATH: &str = "/uhp";
pub fn base_path() -> String {
let raw = env::var(BASE_PATH_ENV).unwrap_or_else(|_| DEFAULT_BASE_PATH.to_owned());
let trimmed = raw.trim().trim_end_matches('/');
if trimmed.is_empty() {
String::new()
} else if trimmed.starts_with('/') {
trimmed.to_owned()
} else {
format!("/{trimmed}")
}
}
pub fn router(state: AppState) -> Router {
let uhp_state = state::UhpState::new(state.shared);
let public_state = uhp_state.clone();
let authenticated = Router::new()
.route("/v1/harnesses", get(harnesses::list).post(manage::create))
.route(
"/v1/harnesses/{harness_id}",
get(harnesses::get)
.put(manage::update)
.delete(manage::delete),
)
.route(
"/v1/harnesses/{harness_id}/models",
get(harnesses::harness_models),
)
.route(
"/v1/harnesses/{harness_id}/skills/{skill_name}/files",
get(manage::skill_files),
)
.route("/v1/models", get(harnesses::models))
.route("/v1/responses", post(tasks::create))
.route("/v1/responses/{response_id}", get(tasks::get))
.route("/v1/responses/{response_id}/cancel", post(tasks::cancel))
.route("/v1/sessions", get(sessions::list))
.route(
"/v1/sessions/{session_id}",
get(sessions::get).delete(sessions::delete),
)
.route("/v1/traces/{session_id}", delete(sessions::delete))
.route("/v1/sessions/{session_id}/turns", get(sessions::turns))
.route(
"/v1/sessions/{session_id}/share",
get(share::get).post(share::create).delete(share::revoke),
)
.route("/v1/sessions/{session_id}/files", get(files::session_files))
.route(
"/v1/containers/{container_id}/files/{file_id}/content",
get(files::container_file),
)
.with_state(uhp_state)
.layer(middleware::from_fn(auth::require_auth));
let public = Router::new()
.route("/share/{share_id}", get(share::view))
.with_state(public_state);
Router::new()
.route("/v1/uhp", get(discovery::handle))
.merge(public)
.merge(authenticated)
.fallback(harnesses::unknown_route)
.layer(middleware::from_fn(version::negotiate))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_base_path_is_normalised_and_can_be_emptied() {
env::set_var(BASE_PATH_ENV, "/api/harness/");
assert_eq!(base_path(), "/api/harness");
env::set_var(BASE_PATH_ENV, "harness");
assert_eq!(base_path(), "/harness");
env::set_var(BASE_PATH_ENV, "/");
assert_eq!(base_path(), "");
env::remove_var(BASE_PATH_ENV);
assert_eq!(base_path(), DEFAULT_BASE_PATH);
}
}