pub mod handlers;
use std::sync::Arc;
use axum::Router;
use axum::extract::{DefaultBodyLimit, Request, State};
use axum::http::{Method, StatusCode, header};
use axum::middleware::{self, Next};
use axum::response::Response;
use axum::routing::{get, post};
use mnemo_core::query::MnemoEngine;
use tower_http::cors::{AllowOrigin, CorsLayer};
pub fn router(engine: Arc<MnemoEngine>) -> Router {
let token = std::env::var("MNEMO_AUTH_TOKEN")
.ok()
.filter(|s| !s.is_empty());
router_with_auth(engine, token)
}
pub fn router_with_auth(engine: Arc<MnemoEngine>, auth_token: Option<String>) -> Router {
let cors = build_cors_layer();
let app = Router::new()
.route(
"/v1/memories",
post(handlers::remember_handler).get(handlers::recall_handler),
)
.route(
"/v1/memories/{id}",
get(handlers::get_memory_handler).delete(handlers::forget_handler),
)
.route("/v1/memories/{id}/share", post(handlers::share_handler))
.route("/v1/checkpoints", post(handlers::checkpoint_handler))
.route("/v1/consolidate", post(handlers::consolidate_handler))
.route("/v1/branches", post(handlers::branch_handler))
.route("/v1/merge", post(handlers::merge_handler))
.route("/v1/replay", post(handlers::replay_handler))
.route("/v1/verify", post(handlers::verify_handler))
.route(
"/v1/compliance/trajectory_audit",
post(handlers::trajectory_audit_handler),
)
.route("/v1/delegate", post(handlers::delegate_handler))
.route("/v1/forget_subject", post(handlers::forget_subject_handler))
.route("/v1/ingest/otlp", post(handlers::otlp_ingest_handler))
.route("/v1/health", get(handlers::health_handler))
.layer(DefaultBodyLimit::max(2 * 1024 * 1024)) .layer(cors)
.layer(tower_http::trace::TraceLayer::new_for_http());
let app = match auth_token {
Some(token) if !token.is_empty() => {
tracing::info!(
"REST bearer-token auth ENABLED (Authorization: Bearer <MNEMO_AUTH_TOKEN>)"
);
app.layer(middleware::from_fn_with_state(
Arc::new(token),
require_bearer,
))
}
_ => {
tracing::warn!(
"REST API running WITHOUT authentication — set MNEMO_AUTH_TOKEN to require a \
bearer token. Do not expose an unauthenticated memory server."
);
app
}
};
app.with_state(engine)
}
async fn require_bearer(
State(expected): State<Arc<String>>,
req: Request,
next: Next,
) -> Result<Response, StatusCode> {
if req.method() == Method::OPTIONS || req.uri().path() == "/v1/health" {
return Ok(next.run(req).await);
}
let provided = req
.headers()
.get(header::AUTHORIZATION)
.and_then(|v| v.to_str().ok());
if mnemo_core::auth::bearer_token_matches(provided, &expected) {
Ok(next.run(req).await)
} else {
Err(StatusCode::UNAUTHORIZED)
}
}
fn build_cors_layer() -> CorsLayer {
use axum::http::{HeaderName, Method};
let base = CorsLayer::new()
.allow_methods([Method::GET, Method::POST, Method::DELETE, Method::OPTIONS])
.allow_headers([
HeaderName::from_static("content-type"),
HeaderName::from_static("authorization"),
])
.max_age(std::time::Duration::from_secs(3600));
match std::env::var("MNEMO_CORS_ORIGINS") {
Ok(val) if val == "*" => base.allow_origin(AllowOrigin::any()),
Ok(val) => {
let origins: Vec<_> = val
.split(',')
.filter_map(|s| s.trim().parse().ok())
.collect();
base.allow_origin(origins)
}
Err(_) => {
let origins: Vec<_> = [
"http://localhost:3000",
"http://localhost:8080",
"http://127.0.0.1:3000",
"http://127.0.0.1:8080",
]
.iter()
.filter_map(|s| s.parse().ok())
.collect();
base.allow_origin(origins)
}
}
}