2026-07-28 19:37:56 +08:00
|
|
|
//! Testnet KeyPackage Registry service.
|
2026-06-11 16:55:32 +08:00
|
|
|
//!
|
2026-07-28 19:37:56 +08:00
|
|
|
//! Throwaway service for issue #110 — replaced by λLEZ in v0.3. No libchat-core
|
|
|
|
|
//! dependency; the embedded logos-delivery node comes from libchat's transport
|
|
|
|
|
//! crate, pulled in as a pinned git dependency.
|
2026-06-11 16:55:32 +08:00
|
|
|
//!
|
2026-07-28 19:37:56 +08:00
|
|
|
//! Submissions arrive on either write path — both feed the same verification +
|
|
|
|
|
//! storage pipeline (`bundle`):
|
|
|
|
|
//! - HTTP POST (below), synchronous and acknowledged;
|
|
|
|
|
//! - logos-delivery subscription (`delivery`, disable with `--no-delivery`):
|
|
|
|
|
//! clients publish protobuf submissions on the store content topics.
|
|
|
|
|
//!
|
|
|
|
|
//! HTTP wire:
|
2026-06-11 16:55:32 +08:00
|
|
|
//! 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_pub} — fetch the account device-list bundle
|
|
|
|
|
|
2026-07-28 19:37:56 +08:00
|
|
|
mod bundle;
|
|
|
|
|
mod delivery;
|
2026-06-11 16:55:32 +08:00
|
|
|
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-07-28 19:37:56 +08:00
|
|
|
#[command(
|
|
|
|
|
name = "chat-store",
|
|
|
|
|
about = "Testnet Chat Store (KeyPackage + account directory)"
|
|
|
|
|
)]
|
2026-06-11 16:55:32 +08:00
|
|
|
struct Cli {
|
|
|
|
|
/// Address to bind the HTTP server.
|
|
|
|
|
#[arg(long, default_value = "0.0.0.0:8080")]
|
|
|
|
|
bind: SocketAddr,
|
|
|
|
|
|
|
|
|
|
/// SQLite database path.
|
|
|
|
|
#[arg(long, default_value = "chat-store.db")]
|
|
|
|
|
db: PathBuf,
|
|
|
|
|
|
|
|
|
|
/// Maximum number of keypackage bundles retained per device_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,
|
2026-07-28 19:37:56 +08:00
|
|
|
|
|
|
|
|
/// Disable the logos-delivery subscriber; submissions then arrive over
|
|
|
|
|
/// HTTP POST only.
|
|
|
|
|
#[arg(long)]
|
|
|
|
|
no_delivery: bool,
|
|
|
|
|
|
|
|
|
|
/// logos-delivery network preset the subscriber joins.
|
|
|
|
|
#[arg(long, default_value = "logos.dev")]
|
|
|
|
|
preset: String,
|
|
|
|
|
|
|
|
|
|
/// TCP + discv5 UDP port for the embedded logos-delivery node
|
|
|
|
|
/// (0 = OS-assigned).
|
|
|
|
|
#[arg(long, default_value_t = 0)]
|
|
|
|
|
p2p_port: u16,
|
2026-06-11 16:55:32 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[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-07-28 19:37:56 +08:00
|
|
|
let store = Arc::new(Store::open(&cli.db).await.context("failed to open store")?);
|
2026-06-11 16:55:32 +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;
|
|
|
|
|
if let Err(e) = prune_store.prune_key_packages(max_per_id, retention).await {
|
|
|
|
|
tracing::warn!("prune (keypackages) failed: {e}");
|
|
|
|
|
}
|
|
|
|
|
if let Err(e) = prune_store.prune_accounts(retention).await {
|
|
|
|
|
tracing::warn!("prune (accounts) failed: {e}");
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
2026-07-28 19:37:56 +08:00
|
|
|
if !cli.no_delivery {
|
|
|
|
|
// Blocks for a few seconds while the node starts and finds peers;
|
|
|
|
|
// deliberately before serving HTTP so a subscriber failure is a
|
|
|
|
|
// startup error, not a silent half-running store.
|
|
|
|
|
delivery::start(
|
|
|
|
|
store.clone(),
|
|
|
|
|
delivery::P2pConfig {
|
|
|
|
|
preset: cli.preset.clone(),
|
|
|
|
|
port: cli.p2p_port,
|
|
|
|
|
log_level: "ERROR".into(),
|
|
|
|
|
},
|
|
|
|
|
tokio::runtime::Handle::current(),
|
|
|
|
|
)
|
|
|
|
|
.context("failed to start logos-delivery ingestion")?;
|
|
|
|
|
tracing::info!("logos-delivery ingestion running (preset={})", cli.preset);
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-11 16:55:32 +08:00
|
|
|
let app = handlers::router(store);
|
|
|
|
|
let listener = tokio::net::TcpListener::bind(cli.bind)
|
|
|
|
|
.await
|
|
|
|
|
.with_context(|| format!("failed to bind {}", cli.bind))?;
|
|
|
|
|
tracing::info!("chat-store listening on {}", cli.bind);
|
|
|
|
|
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");
|
|
|
|
|
}
|