2026-06-11 15:43:10 +08:00
|
|
|
//! Testnet KeyPackage Registry HTTP service.
|
|
|
|
|
//!
|
|
|
|
|
//! Throwaway service for issue #110 — replaced by λLEZ in v0.3. Intentionally
|
|
|
|
|
//! self-contained: depends only on axum + sqlite + ed25519, no libchat core.
|
|
|
|
|
//!
|
|
|
|
|
//! Wire:
|
|
|
|
|
//! POST /v0/keypackage — submit a signed keypackage bundle
|
|
|
|
|
//! GET /v0/keypackage/{device_id} — fetch the latest stored keypackage bundle
|
|
|
|
|
//! POST /v0/account — upsert a signed account device-list bundle
|
|
|
|
|
//! GET /v0/account/{account_id} — fetch the account device-list bundle
|
|
|
|
|
|
|
|
|
|
mod handlers;
|
|
|
|
|
mod store;
|
|
|
|
|
|
|
|
|
|
use std::net::SocketAddr;
|
|
|
|
|
use std::path::PathBuf;
|
|
|
|
|
use std::sync::Arc;
|
|
|
|
|
use std::time::Duration;
|
|
|
|
|
|
|
|
|
|
use anyhow::{Context, Result};
|
|
|
|
|
use clap::Parser;
|
|
|
|
|
use tracing_subscriber::EnvFilter;
|
|
|
|
|
|
|
|
|
|
use store::Store;
|
|
|
|
|
|
|
|
|
|
#[derive(Parser, Debug)]
|
2026-06-11 15:47:15 +08:00
|
|
|
#[command(name = "chat-store", about = "Testnet Chat Store (KeyPackage + account directory)")]
|
2026-06-11 15:43:10 +08:00
|
|
|
struct Cli {
|
|
|
|
|
/// Address to bind the HTTP server.
|
|
|
|
|
#[arg(long, default_value = "0.0.0.0:8080")]
|
|
|
|
|
bind: SocketAddr,
|
|
|
|
|
|
|
|
|
|
/// SQLite database path.
|
2026-06-11 15:47:15 +08:00
|
|
|
#[arg(long, default_value = "chat-store.db")]
|
2026-06-11 15:43:10 +08:00
|
|
|
db: PathBuf,
|
|
|
|
|
|
|
|
|
|
/// Maximum number of bundles retained per account_id.
|
|
|
|
|
#[arg(long, default_value_t = 100)]
|
|
|
|
|
max_per_identity: usize,
|
|
|
|
|
|
|
|
|
|
/// Retention window in days; older bundles are pruned.
|
|
|
|
|
#[arg(long, default_value_t = 30)]
|
|
|
|
|
retention_days: u64,
|
|
|
|
|
|
|
|
|
|
/// How often the prune task runs.
|
|
|
|
|
#[arg(long, default_value_t = 3600)]
|
|
|
|
|
prune_interval_secs: u64,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[tokio::main]
|
|
|
|
|
async fn main() -> Result<()> {
|
|
|
|
|
tracing_subscriber::fmt()
|
|
|
|
|
.with_env_filter(
|
|
|
|
|
EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("info")),
|
|
|
|
|
)
|
|
|
|
|
.init();
|
|
|
|
|
|
|
|
|
|
let cli = Cli::parse();
|
|
|
|
|
|
2026-06-11 16:25:13 +08:00
|
|
|
let store = Arc::new(
|
|
|
|
|
Store::open(&cli.db)
|
|
|
|
|
.await
|
|
|
|
|
.context("failed to open store")?,
|
|
|
|
|
);
|
2026-06-11 15:43:10 +08:00
|
|
|
|
|
|
|
|
let prune_store = store.clone();
|
|
|
|
|
let max_per_id = cli.max_per_identity;
|
|
|
|
|
let retention = Duration::from_secs(cli.retention_days * 24 * 3600);
|
|
|
|
|
let interval = Duration::from_secs(cli.prune_interval_secs);
|
|
|
|
|
tokio::spawn(async move {
|
|
|
|
|
let mut ticker = tokio::time::interval(interval);
|
|
|
|
|
loop {
|
|
|
|
|
ticker.tick().await;
|
2026-06-11 16:25:13 +08:00
|
|
|
if let Err(e) = prune_store.prune_key_packages(max_per_id, retention).await {
|
2026-06-11 15:43:10 +08:00
|
|
|
tracing::warn!("prune (keypackages) failed: {e}");
|
|
|
|
|
}
|
2026-06-11 16:25:13 +08:00
|
|
|
if let Err(e) = prune_store.prune_accounts(retention).await {
|
2026-06-11 15:43:10 +08:00
|
|
|
tracing::warn!("prune (accounts) failed: {e}");
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
let app = handlers::router(store);
|
|
|
|
|
let listener = tokio::net::TcpListener::bind(cli.bind)
|
|
|
|
|
.await
|
|
|
|
|
.with_context(|| format!("failed to bind {}", cli.bind))?;
|
2026-06-11 15:47:15 +08:00
|
|
|
tracing::info!("chat-store listening on {}", cli.bind);
|
2026-06-11 15:43:10 +08:00
|
|
|
axum::serve(listener, app)
|
|
|
|
|
.with_graceful_shutdown(shutdown_signal())
|
|
|
|
|
.await
|
|
|
|
|
.context("server error")?;
|
|
|
|
|
Ok(())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async fn shutdown_signal() {
|
|
|
|
|
let _ = tokio::signal::ctrl_c().await;
|
|
|
|
|
tracing::info!("shutdown signal received");
|
|
|
|
|
}
|