// Copyright (C) 2026 Doan Luu Duc Tai // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as // published by the Free Software Foundation, either version 3 of the // License, or (at your option) any later version. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . use crate::app::project::{Mode, Project, Settings}; use crate::app::{files, process}; use anyhow::{Context, Result, ensure}; use stack_config::Config; use std::fs; use std::net::IpAddr; use std::path::Path; use std::process::Command; pub(super) fn web_bind(configuration: &Config) -> Result { let address: IpAddr = configuration.value("WEB_BIND")?.parse()?; Ok(match address { IpAddr::V4(_) => address.to_string(), IpAddr::V6(_) => format!("[{address}]"), }) } pub(super) fn environment( command: &mut Command, configuration: &Config, settings: &Settings, ) -> Result<()> { let local = (settings.mode == Mode::Local).then_some(settings.port); let mut public = configuration.public_values(local)?; let origin = public .get("CHAT_PUBLIC_ORIGIN") .context("Missing chat gateway origin")? .clone(); public.insert("CHAT_ORIGIN".into(), origin); public.insert("CHAT_BIND".into(), "0.0.0.0:8080".into()); for (key, value) in public { command.arg("--env").arg(&key).env(key, value); } Ok(()) } pub(super) fn build(project: &Project) -> Result<()> { process::run(project.command("sh")?.arg("infra/chat/build-typescript.sh"))?; process::run(project.command("sh")?.arg("infra/chat/build-native.sh"))?; let wasm_build = if project .root .join(".tools/openssl-wasi/lib/libcrypto.a") .is_file() { "infra/chat/build-browser.sh" } else { "infra/chat/build-openssl-wasi.sh" }; process::run(project.command("sh")?.arg(wasm_build))?; process::run(project.command("sh")?.arg("infra/chat/build-source.sh"))?; package(project) } pub(super) fn package(project: &Project) -> Result<()> { let directory = project.root.join("bin/chat"); fs::create_dir_all(&directory)?; for (source, target) in [ ( "target/x86_64-unknown-linux-musl/release/stack-chat-server", "server", ), ( "target/wasm32-wasip1/release/stack-chat-crypto.wasm", "crypto.wasm", ), ("var/build/chat-source.tar.gz", "SOURCE.tar.gz"), ("infra/containers/chat/Containerfile", "Containerfile"), ( "var/build/typescript/crates/chat-crypto/browser/wasi.js", "wasi.mjs", ), ( "var/build/typescript/crates/chat-server/web/app.js", "web/app.js", ), ( "var/build/typescript/crates/chat-server/web/worker.js", "web/worker.js", ), ("web/THIRD-PARTY-LICENSES.html", "THIRD-PARTY-LICENSES.html"), ("docs/CHAT-APPLICATION.txt", "CHAT-APPLICATION.txt"), ("docs/CHAT-PRODUCTION.txt", "CHAT-PRODUCTION.txt"), ("LICENSE", "LICENSE"), ] { files::copy_tree(&project.root.join(source), &directory.join(target))?; } for name in ["index.html", "style.css", "javascript.html"] { files::copy_tree( &project.root.join("crates/chat-server/web").join(name), &directory.join("web").join(name), )?; } Ok(()) } pub(super) fn copy_artifacts(project: &Project, destination: &Path) -> Result<()> { let artifacts = project.root.join("bin/chat"); ensure!( artifacts.join("server").is_file() && artifacts.join("crypto.wasm").is_file() && artifacts.join("SOURCE.tar.gz").is_file(), "Build a complete chat release locally before export" ); files::copy_tree(&artifacts, &destination.join("bin/chat")) } pub(super) fn admin( project: &Project, init: bool, reset_owner: Option<&str>, owner_fingerprint: bool, ) -> Result<()> { let settings = project.settings()?; if init { project.initialize(&settings)?; return process::line(format_args!( "Owner setup token is available in the single root .env" )); } ensure!( reset_owner.is_some() || owner_fingerprint, "Use up --production to run chat; maintenance options are --owner-fingerprint and --reset-owner" ); let configuration = Config::load(&project.root)?; let services = project.configuration(&settings)?; let service = services .services .get("chat") .context("Chat is not packaged in this release")?; let mut command = project.command("podman")?; command .args([ "run", "--rm", "--network", "none", "--read-only", "--read-only-tmpfs=false", "--cap-drop", "ALL", "--security-opt", "no-new-privileges", "--env", "CHAT_SETUP_TOKEN", ]) .env("CHAT_SETUP_TOKEN", configuration.value("CHAT_SETUP_TOKEN")?); environment(&mut command, &configuration, &settings)?; command .arg("--volume") .arg(format!( "{}:/app/var/data/chat:rw,z", project.root.join("var/data/chat").display() )) .arg(&service.image); if let Some(fingerprint) = reset_owner { command.arg("--reset-owner").arg(fingerprint); } if owner_fingerprint { command.arg("--owner-fingerprint"); } process::run(&mut command) }