//! OxygenHub: a forge for Oxygen repositories (`docs/spec.md`). //! //! One process: an axum router serving pages, the JSON API and the wire //! protocol; SQLite for hub state; repositories as bare Oxygen stores. pub mod api; pub mod apt; pub mod assistants; pub mod auth; pub mod config; pub mod db; pub mod events; pub mod flow; pub mod hooks; pub mod keys; pub mod levels; pub mod live; pub mod mail; pub mod maintenance; pub mod mirror; pub mod notify; pub mod oidc; pub mod protocol; pub mod render; pub mod repos; pub mod secrets; pub mod signin; pub mod social; pub mod storage; pub mod variables; pub mod web; pub mod wiki; use crate::config::Config; use crate::repos::Repos; use std::sync::Arc; /// Everything a request handler can reach. pub struct App { pub config: Config, pub db: db::Pool, pub repos: Arc, /// Wikis, one Oxygen repository per repository (hub spec §6.11). pub wikis: Arc, pub server: Arc, pub hub_key: keys::HubKey, pub cookie_key: auth::CookieKey, /// Seals AI assistants' tokens at rest (hub spec §13.4). pub assistant_key: assistants::SecretKey, /// Seals Flow secrets at rest (hub spec §13.2, #107). pub secret_key: assistants::SecretKey, /// The OIDC issuer for jobs (hub spec §13.5), made on first use. pub oidc: tokio::sync::OnceCell, /// Which assistants are answering which proposals, and recent calls /// per repository (hub spec §13.4). pub assistant_gate: std::sync::Mutex, pub events: events::Events, /// Who is resolving which conflict together (hub spec §6.10). pub rooms: Arc, /// Outgoing mail: hub.toml's `[email]`, overridden by what the admin /// page saved (#101). pub mail: std::sync::RwLock, pub started: std::time::Instant, } impl App { /// The OIDC issuer, its key loaded or made the first time it is asked for. pub async fn oidc(&self) -> anyhow::Result<&oidc::Issuer> { let keys = self.config.keys_dir(); self.oidc.get_or_try_init(|| async move { tokio::task::spawn_blocking(move || oidc::Issuer::load_or_create(&keys)).await? }).await } } pub type Shared = Arc; /// Open the data directory and build the application state. `init` must /// have run once; `serve` and the tests call this. pub async fn build(config: Config) -> anyhow::Result { std::fs::create_dir_all(config.repos_dir())?; let db = db::open(&config.db_path()).await?; let repos = Arc::new(Repos::new(config.repos_dir())); let wikis = Arc::new(Repos::new(config.data.join("wikis"))); let hub_key = keys::ensure_identity(&config.keys_dir(), &config.site.name, config.base_url())?; let cookie_key = auth::CookieKey::load_or_create(&config.keys_dir())?; let assistant_key = assistants::SecretKey::load_or_create(&config.keys_dir())?; let secret_key = assistants::SecretKey::load_or_create_named(&config.keys_dir(), "secrets.key")?; let backend = protocol::HubBackend::new( repos.clone(), wikis.clone(), db.clone(), config.auth.push_tokens.clone(), tokio::runtime::Handle::current(), ); let slot = backend.app_slot(); let server = oxy_server::Server::new(backend); let mail = mail::load(&db, &config.email).await; let app = Arc::new(App { mail: std::sync::RwLock::new(mail), config, db, repos, wikis, server, hub_key, cookie_key, assistant_key, secret_key, oidc: tokio::sync::OnceCell::new(), assistant_gate: std::sync::Mutex::new(assistants::Gate::default()), events: events::Events::default(), rooms: Arc::new(live::Rooms::default()), started: std::time::Instant::now(), }); *slot.lock().unwrap() = Some(Arc::downgrade(&app)); storage::load(&app).await?; tokio::spawn(hooks::worker(app.clone())); tokio::spawn(flow::reaper(app.clone())); tokio::spawn(flow::scheduler(app.clone())); tokio::spawn(flow::placement(app.clone())); tokio::spawn(maintenance::nightly(app.clone())); Ok(app) } /// The full router: pages, API, protocol and static assets. pub fn router(app: Shared) -> axum::Router { web::router(app) } /// Serve on the configured addresses until the process is told to stop. pub async fn serve(app: Shared) -> anyhow::Result<()> { let router = router(app.clone()); let http = app.config.bind_addr()?; let https = app.config.tls_bind_addr()?; if http.is_none() && https.is_none() { anyhow::bail!("nothing to bind: set server.bind or server.tls_bind"); } let mut tasks = tokio::task::JoinSet::new(); if let Some(addr) = http { let listener = tokio::net::TcpListener::bind(addr).await?; tracing::info!("http on {addr}"); let r = router.clone(); tasks.spawn(async move { axum::serve(listener, r.into_make_service_with_connect_info::()).await.map_err(anyhow::Error::from) }); } if let Some(addr) = https { let _ = rustls::crypto::ring::default_provider().install_default(); let tls = axum_server::tls_rustls::RustlsConfig::from_pem_file(app.config.tls_cert(), app.config.tls_key()) .await .map_err(|e| anyhow::anyhow!("loading TLS files: {e}"))?; tracing::info!("https on {addr}"); let r = router.clone(); tasks.spawn(async move { axum_server::bind_rustls(addr, tls) .serve(r.into_make_service_with_connect_info::()) .await .map_err(anyhow::Error::from) }); } tokio::select! { r = tasks.join_next() => { r.transpose()?.transpose()?; Ok(()) } _ = tokio::signal::ctrl_c() => { tracing::info!("stopping"); Ok(()) } } }