summaryrefslogtreecommitdiff
path: root/src/chat.rs
diff options
context:
space:
mode:
authorDoan Luu Duc Tai <doan@luuduct.ai>2026-10-31 07:39:18 -0500
committerDoan Luu Duc Tai <doan@luuduct.ai>2026-10-31 07:39:18 -0500
commitd809d86182ab802472ae7d5e74cab66ba7bfa397 (patch)
tree36718e8d91b13b2714cbfe7877353ac7625f2f44 /src/chat.rs
parentdd1d5863ef865ebe29dc93d03ca6abc0aaf48bee (diff)
Package private chat for HTTPS and verify portable encrypted recovery
Diffstat (limited to 'src/chat.rs')
-rw-r--r--src/chat.rs163
1 files changed, 163 insertions, 0 deletions
diff --git a/src/chat.rs b/src/chat.rs
new file mode 100644
index 0000000..be852d5
--- /dev/null
+++ b/src/chat.rs
@@ -0,0 +1,163 @@
+// Copyright (C) 2026 Doan Luu Duc Tai <doan@luuduct.ai>
+//
+// 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 <https://www.gnu.org/licenses/>.
+
+use crate::project::{Mode, Project, Settings};
+use crate::{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(crate) fn web_bind(configuration: &Config) -> Result<String> {
+ let address: IpAddr = configuration.value("WEB_BIND")?.parse()?;
+ Ok(match address {
+ IpAddr::V4(_) => address.to_string(),
+ IpAddr::V6(_) => format!("[{address}]"),
+ })
+}
+
+pub(crate) 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(crate) fn build(project: &Project) -> Result<()> {
+ 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(crate) 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"),
+ ("crates/chat-crypto/browser/wasi.mjs", "wasi.mjs"),
+ ("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))?;
+ }
+ files::copy_tree(
+ &project.root.join("crates/chat-server/web"),
+ &directory.join("web"),
+ )?;
+ Ok(())
+}
+
+pub(crate) 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(crate) 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)
+}