hop-relay 2.0.0

Hop-Corr relay (batch apply + optional HTTP endpoint)
Documentation
#[cfg(not(feature = "net"))]
fn main() {
    println!("hop-relay: compiled without the `net` feature — HTTP frontend disabled.");
}

#[cfg(feature = "net")]
#[tokio::main]
async fn main() {
    use std::path::PathBuf;
    use std::process;

    use clap::Parser;
    use tracing_subscriber::EnvFilter;

    fn init_tracing() {
        let filter = EnvFilter::try_from_env("HOP_LOG")
            .or_else(|_| EnvFilter::try_from_default_env())
            .unwrap_or_else(|_| EnvFilter::new("info"));

        let json = std::env::var("HOP_LOG_JSON")
            .ok()
            .is_some_and(|v| matches!(v.as_str(), "1" | "true" | "TRUE" | "yes" | "YES"));

        let base = tracing_subscriber::fmt()
            .with_env_filter(filter)
            .with_target(true);

        if json {
            base.json().init();
        } else {
            base.init();
        }
    }

    #[derive(Parser, Debug)]
    #[command(name = "hop-relay", about = "Hop-Corr relay HTTP server")]
    struct Args {
        /// Address to bind, e.g. 127.0.0.1:8787
        #[arg(long, env = "HOP_LISTEN", default_value = "127.0.0.1:8787")]
        listen: String,

        /// Server storage root (vaults, keys, snapshots)
        #[arg(long, env = "HOP_DATA_DIR", default_value = "./data")]
        data_dir: PathBuf,

        /// If set, enable authentication.
        ///
        /// Admin endpoints require `Authorization: Bearer <admin token>`.
        /// Vault-scoped endpoints accept either the admin token or a per-vault capability token.
        #[arg(long, env = "HOP_AUTH_TOKEN")]
        auth_token: Option<String>,

        /// Maximum accepted JSON body size for POST endpoints.
        #[arg(long, env = "HOP_MAX_BODY_BYTES", default_value_t = 1 * 1024 * 1024)]
        max_body_bytes: u64,

        /// Maximum length of `coeffs` accepted by endpoints that bootstrap channels/vaults.
        #[arg(long, env = "HOP_MAX_COEFFS_LEN", default_value_t = 1024)]
        max_coeffs_len: usize,

        /// Maximum number of actions accepted in a single request.
        #[arg(long, env = "HOP_MAX_ACTIONS_PER_REQUEST", default_value_t = 4096)]
        max_actions_per_request: usize,

        /// Maximum estimated field operations in one action bundle.
        #[arg(long, env = "HOP_MAX_ACTION_WORK", default_value_t = 10_000_000)]
        max_action_work: u64,

        /// Maximum number of transcript entries returned by `/vault/prove`.
        #[arg(long, env = "HOP_MAX_PROVE_ENTRIES", default_value_t = 10_000)]
        max_prove_entries: usize,

        /// Maximum number of in-flight requests (excluding GET /health and OPTIONS).
        #[arg(long, env = "HOP_MAX_IN_FLIGHT", default_value_t = 128)]
        max_in_flight_requests: usize,

        /// Per-request processing timeout in milliseconds (0 disables).
        #[arg(long, env = "HOP_REQUEST_TIMEOUT_MS", default_value_t = 30_000)]
        request_timeout_ms: u64,

        /// Global rate limit in requests/second (0 disables).
        #[arg(long, env = "HOP_RATE_LIMIT_RPS", default_value_t = 0)]
        rate_limit_rps: u32,

        /// Global rate limit burst capacity (0 disables).
        #[arg(long, env = "HOP_RATE_LIMIT_BURST", default_value_t = 0)]
        rate_limit_burst: u32,

        /// Maximum time to wait for in-flight requests after SIGINT/SIGTERM (0 waits forever).
        #[arg(long, env = "HOP_SHUTDOWN_GRACE_MS", default_value_t = 10_000)]
        shutdown_grace_ms: u64,

        /// Dev-only: allow any origin via CORS (not recommended for production).
        #[arg(long, env = "HOP_CORS_ANY", default_value_t = false)]
        cors_any: bool,

        /// Allow these exact origins via CORS. Repeatable.
        #[arg(long = "cors-origin", env = "HOP_CORS_ORIGINS", value_delimiter = ',')]
        cors_origins: Vec<String>,
    }

    init_tracing();

    let args = Args::parse();

    if args.auth_token.is_none() {
        tracing::warn!("no auth token configured; relay endpoints are unauthenticated");
    }
    if args.cors_any {
        tracing::warn!("CORS any-origin enabled; do not use this in production");
    }
    if args.rate_limit_rps > 0 && args.rate_limit_burst == 0 {
        tracing::warn!("rate limiting enabled but burst is 0; rate limiting will be disabled");
    }
    if args.rate_limit_burst > 0 && args.rate_limit_rps == 0 {
        tracing::warn!("rate limit burst set but rps is 0; rate limiting will be disabled");
    }

    let cfg = hop_relay::net::ServerConfig {
        data_dir: args.data_dir,
        auth_token: args.auth_token,
        max_body_bytes: args.max_body_bytes,
        max_coeffs_len: args.max_coeffs_len,
        max_actions_per_request: args.max_actions_per_request,
        max_action_work: args.max_action_work,
        max_prove_entries: args.max_prove_entries,
        max_in_flight_requests: args.max_in_flight_requests,
        request_timeout_ms: args.request_timeout_ms,
        rate_limit_rps: args.rate_limit_rps,
        rate_limit_burst: args.rate_limit_burst,
        cors_allow_any_origin: args.cors_any,
        cors_allow_origins: args.cors_origins,
        shutdown_grace_ms: args.shutdown_grace_ms,
    };

    tracing::info!(listen = %args.listen, "starting hop-relay HTTP server");
    if let Err(err) = hop_relay::net::serve(&args.listen, cfg).await {
        tracing::error!(error = %err, "hop-relay server error");
        process::exit(1);
    }
}