posemesh_compute_node/
http.rs

1use axum::{
2    http::StatusCode,
3    response::IntoResponse,
4    routing::{get, post},
5    Json, Router,
6};
7use serde::{Deserialize, Serialize};
8use tracing::{info, warn};
9
10use crate::dds::persist;
11
12async fn health() -> impl IntoResponse {
13    StatusCode::OK
14}
15
16#[derive(Debug, Deserialize)]
17struct RegistrationRequest {
18    id: String,
19    secret: String,
20    organization_id: Option<String>,
21    #[serde(rename = "lighthouses_in_domains")]
22    _lighthouses_in_domains: Option<serde_json::Value>,
23    #[serde(rename = "domains")]
24    _domains: Option<serde_json::Value>,
25}
26
27#[derive(Debug, Serialize)]
28struct RegistrationResponse {
29    ok: bool,
30}
31
32#[derive(Debug)]
33enum RegistrationError {
34    Unprocessable(&'static str),
35    Forbidden(&'static str),
36    Conflict(&'static str),
37}
38
39impl IntoResponse for RegistrationError {
40    fn into_response(self) -> axum::response::Response {
41        let (status, msg) = match self {
42            RegistrationError::Unprocessable(msg) => (StatusCode::UNPROCESSABLE_ENTITY, msg),
43            RegistrationError::Forbidden(msg) => (StatusCode::FORBIDDEN, msg),
44            RegistrationError::Conflict(msg) => (StatusCode::CONFLICT, msg),
45        };
46        (status, msg).into_response()
47    }
48}
49
50async fn register(
51    Json(payload): Json<RegistrationRequest>,
52) -> Result<Json<RegistrationResponse>, RegistrationError> {
53    if payload.id.trim().is_empty() {
54        return Err(RegistrationError::Unprocessable("missing id"));
55    }
56    if payload.secret.trim().is_empty() {
57        return Err(RegistrationError::Unprocessable("missing secret"));
58    }
59    if payload.secret.len() > 4096 {
60        return Err(RegistrationError::Forbidden("secret too large"));
61    }
62
63    let secret_len = payload.secret.len();
64    let org = payload.organization_id.as_deref().unwrap_or("");
65    info!(
66        id = %payload.id,
67        org = %org,
68        secret_len,
69        "Received registration callback"
70    );
71
72    persist::write_node_secret(&payload.secret)
73        .map_err(|_| RegistrationError::Conflict("persist failed"))?;
74
75    match persist::read_node_secret() {
76        Ok(Some(_)) => {}
77        Ok(None) => {
78            warn!(
79                id = %payload.id,
80                "persisted secret missing after write"
81            );
82            return Err(RegistrationError::Conflict("persist verify failed"));
83        }
84        Err(_) => {
85            return Err(RegistrationError::Conflict("persist verify failed"));
86        }
87    }
88
89    Ok(Json(RegistrationResponse { ok: true }))
90}
91
92/// Build the node HTTP router exposing /health and internal registration endpoint.
93pub fn router() -> Router {
94    Router::new()
95        .route("/health", get(health))
96        .route("/internal/v1/registrations", post(register))
97}