feat: architecture review fixes, self-update system, CI pipeline, supply chain hardening
Architecture review (all P0+P1 issues now fixed): - Add 10s timeout to 6 bare Nostr client.connect() calls - Pin all 12 crypto deps to exact versions from Cargo.lock - Pin all 15 floating container image tags to exact patch versions - Add CI pipeline (cargo fmt + clippy + tests, frontend type-check + build) Self-update system (git.tx1138.com): - scripts/self-update.sh: pull, build, install, restart with rollback - systemd timer checks daily at 3 AM - update.check RPC does git-based checks when repo is present - update.git-apply RPC triggers self-update from UI - Default update URL changed from GitHub to git.tx1138.com - Git added to ISO package list for fresh installs Documentation: - CHANGELOG v1.3.1 with all changes - README updated (version, update system section) - BETA-PROGRESS session #6 logged - architecture-review.html: 4 issues marked FIXED, 8/12 refactoring done Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -41,15 +41,15 @@ archipelago-parmanode = { path = "../parmanode" }
|
||||
|
||||
# Authentication
|
||||
bcrypt = "0.15"
|
||||
sha2 = "0.10"
|
||||
hmac = "0.12"
|
||||
sha2 = "0.10.9"
|
||||
hmac = "0.12.1"
|
||||
uuid = { version = "1.0", features = ["v4"] }
|
||||
regex = "1.10"
|
||||
|
||||
# Node identity (Ed25519 + X25519 key agreement)
|
||||
ed25519-dalek = { version = "2.1", features = ["rand_core"] }
|
||||
curve25519-dalek = "4"
|
||||
rand = "0.8"
|
||||
ed25519-dalek = { version = "2.2.0", features = ["rand_core"] }
|
||||
curve25519-dalek = "4.1.3"
|
||||
rand = "0.8.5"
|
||||
hex = "0.4"
|
||||
bs58 = "0.5"
|
||||
chrono = "0.4"
|
||||
@@ -66,8 +66,8 @@ reqwest = { version = "0.11", default-features = false, features = ["json", "soc
|
||||
nostr-sdk = { version = "0.44", features = ["nip04", "nip44"] }
|
||||
|
||||
# Backup encryption (DID identity export) + TOTP 2FA encryption
|
||||
argon2 = "0.5"
|
||||
chacha20poly1305 = "0.10"
|
||||
argon2 = "0.5.3"
|
||||
chacha20poly1305 = "0.10.1"
|
||||
base64 = "0.21"
|
||||
|
||||
# Full system backup (tar archive + gzip compression)
|
||||
@@ -78,7 +78,7 @@ flate2 = "1.0"
|
||||
totp-rs = { version = "5.7", features = ["otpauth", "gen_secret"] }
|
||||
qrcode = "0.14"
|
||||
data-encoding = "2.6"
|
||||
zeroize = { version = "1.7", features = ["derive"] }
|
||||
zeroize = { version = "1.8.2", features = ["derive"] }
|
||||
|
||||
# Mainline DHT (did:dht — BitTorrent DHT for decentralized identity)
|
||||
mainline = "2"
|
||||
@@ -89,7 +89,7 @@ bytes = "1"
|
||||
serial2-tokio = "0.1"
|
||||
|
||||
# Double Ratchet key derivation (Phase 3: encrypted mesh messaging)
|
||||
hkdf = "0.12"
|
||||
hkdf = "0.12.4"
|
||||
|
||||
# Transport abstraction (Phase 2: mesh as federation transport)
|
||||
ciborium = "0.2.2"
|
||||
|
||||
@@ -308,6 +308,7 @@ impl RpcHandler {
|
||||
"update.dismiss" => self.handle_update_dismiss().await,
|
||||
"update.download" => self.handle_update_download().await,
|
||||
"update.apply" => self.handle_update_apply().await,
|
||||
"update.git-apply" => self.handle_update_git_apply().await,
|
||||
"update.rollback" => self.handle_update_rollback().await,
|
||||
"update.get-schedule" => self.handle_update_get_schedule().await,
|
||||
"update.set-schedule" => {
|
||||
|
||||
@@ -1,10 +1,23 @@
|
||||
use super::RpcHandler;
|
||||
use crate::update;
|
||||
use anyhow::Result;
|
||||
use anyhow::{Context, Result};
|
||||
|
||||
impl RpcHandler {
|
||||
/// Check for available system updates.
|
||||
/// Tries git-based check first (if repo exists), falls back to manifest-based.
|
||||
pub(super) async fn handle_update_check(&self) -> Result<serde_json::Value> {
|
||||
// Try git-based check first (preferred for beta nodes)
|
||||
let repo_dir = std::path::PathBuf::from(
|
||||
std::env::var("HOME").unwrap_or_else(|_| "/home/archipelago".to_string()),
|
||||
)
|
||||
.join("archy");
|
||||
if repo_dir.join(".git").exists() {
|
||||
if let Ok(git_status) = self.git_check_update(&repo_dir).await {
|
||||
return Ok(git_status);
|
||||
}
|
||||
}
|
||||
|
||||
// Fall back to manifest-based check
|
||||
let state = update::check_for_updates(&self.config.data_dir).await?;
|
||||
|
||||
let update_info = state.available_update.as_ref().map(|u| {
|
||||
@@ -24,6 +37,108 @@ impl RpcHandler {
|
||||
}))
|
||||
}
|
||||
|
||||
/// Git-based update check: runs `git fetch` and compares HEAD to origin/main.
|
||||
async fn git_check_update(&self, repo_dir: &std::path::Path) -> Result<serde_json::Value> {
|
||||
let repo_str = repo_dir.to_string_lossy().to_string();
|
||||
|
||||
// git fetch origin main
|
||||
let fetch = tokio::process::Command::new("git")
|
||||
.args(["fetch", "origin", "main", "--quiet"])
|
||||
.current_dir(&repo_str)
|
||||
.output()
|
||||
.await
|
||||
.context("git fetch failed")?;
|
||||
|
||||
if !fetch.status.success() {
|
||||
anyhow::bail!("git fetch failed: {}", String::from_utf8_lossy(&fetch.stderr));
|
||||
}
|
||||
|
||||
// Get local and remote HEADs
|
||||
let local = tokio::process::Command::new("git")
|
||||
.args(["rev-parse", "--short", "HEAD"])
|
||||
.current_dir(&repo_str)
|
||||
.output()
|
||||
.await?;
|
||||
let local_hash = String::from_utf8_lossy(&local.stdout).trim().to_string();
|
||||
|
||||
let remote = tokio::process::Command::new("git")
|
||||
.args(["rev-parse", "--short", "origin/main"])
|
||||
.current_dir(&repo_str)
|
||||
.output()
|
||||
.await?;
|
||||
let remote_hash = String::from_utf8_lossy(&remote.stdout).trim().to_string();
|
||||
|
||||
let update_available = local_hash != remote_hash;
|
||||
|
||||
// Get commit count and changelog if update available
|
||||
let mut changelog = Vec::new();
|
||||
let mut commits_behind = 0u64;
|
||||
if update_available {
|
||||
let count = tokio::process::Command::new("git")
|
||||
.args(["rev-list", "HEAD..origin/main", "--count"])
|
||||
.current_dir(&repo_str)
|
||||
.output()
|
||||
.await?;
|
||||
commits_behind = String::from_utf8_lossy(&count.stdout)
|
||||
.trim()
|
||||
.parse()
|
||||
.unwrap_or(0);
|
||||
|
||||
let log = tokio::process::Command::new("git")
|
||||
.args(["log", "HEAD..origin/main", "--oneline", "--no-merges", "-20"])
|
||||
.current_dir(&repo_str)
|
||||
.output()
|
||||
.await?;
|
||||
changelog = String::from_utf8_lossy(&log.stdout)
|
||||
.lines()
|
||||
.map(|l| l.to_string())
|
||||
.collect();
|
||||
}
|
||||
|
||||
let now = chrono::Utc::now().to_rfc3339();
|
||||
|
||||
Ok(serde_json::json!({
|
||||
"current_version": local_hash,
|
||||
"last_check": now,
|
||||
"update_available": update_available,
|
||||
"update_method": "git",
|
||||
"update": if update_available {
|
||||
Some(serde_json::json!({
|
||||
"version": remote_hash,
|
||||
"commits_behind": commits_behind,
|
||||
"changelog": changelog,
|
||||
}))
|
||||
} else { None },
|
||||
}))
|
||||
}
|
||||
|
||||
/// Apply git-based update: runs self-update.sh which pulls, builds, and restarts.
|
||||
pub(super) async fn handle_update_git_apply(&self) -> Result<serde_json::Value> {
|
||||
let script = std::path::PathBuf::from(
|
||||
std::env::var("HOME").unwrap_or_else(|_| "/home/archipelago".to_string()),
|
||||
)
|
||||
.join("archy/scripts/self-update.sh");
|
||||
|
||||
if !script.exists() {
|
||||
anyhow::bail!("self-update.sh not found at {}", script.display());
|
||||
}
|
||||
|
||||
// Spawn the update script in the background (it will restart the service)
|
||||
let child = tokio::process::Command::new("bash")
|
||||
.arg(&script)
|
||||
.stdout(std::process::Stdio::null())
|
||||
.stderr(std::process::Stdio::null())
|
||||
.spawn()
|
||||
.context("Failed to spawn self-update.sh")?;
|
||||
|
||||
tracing::info!(pid = child.id(), "Self-update script spawned");
|
||||
|
||||
Ok(serde_json::json!({
|
||||
"started": true,
|
||||
"message": "Update started. The service will restart when complete.",
|
||||
}))
|
||||
}
|
||||
|
||||
/// Get update status without checking remote.
|
||||
pub(super) async fn handle_update_status(&self) -> Result<serde_json::Value> {
|
||||
let state = update::get_status(&self.config.data_dir).await?;
|
||||
|
||||
@@ -7,6 +7,7 @@ use ed25519_dalek::{Signature, Signer, SigningKey, Verifier, VerifyingKey};
|
||||
use rand::rngs::OsRng;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::time::Duration;
|
||||
use tokio::fs;
|
||||
|
||||
use crate::identity::did_key_from_pubkey_hex;
|
||||
@@ -406,7 +407,9 @@ impl IdentityManager {
|
||||
|
||||
let client = nostr_sdk::Client::new(keys);
|
||||
client.add_relay(relay_url).await.context("Failed to add relay")?;
|
||||
client.connect().await;
|
||||
if tokio::time::timeout(Duration::from_secs(10), client.connect()).await.is_err() {
|
||||
tracing::warn!("Nostr relay connection timed out after 10s, continuing anyway");
|
||||
}
|
||||
|
||||
let builder = nostr_sdk::prelude::EventBuilder::new(
|
||||
nostr_sdk::prelude::Kind::Metadata,
|
||||
|
||||
@@ -7,6 +7,7 @@ use anyhow::{Context, Result};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::HashMap;
|
||||
use std::path::Path;
|
||||
use std::time::Duration;
|
||||
use tokio::fs;
|
||||
use tracing::{debug, info, warn};
|
||||
|
||||
@@ -295,7 +296,9 @@ pub async fn discover(
|
||||
for url in relays {
|
||||
let _ = client.add_relay(url).await;
|
||||
}
|
||||
client.connect().await;
|
||||
if tokio::time::timeout(Duration::from_secs(10), client.connect()).await.is_err() {
|
||||
tracing::warn!("Nostr relay connection timed out after 10s, continuing anyway");
|
||||
}
|
||||
|
||||
let filter = nostr_sdk::prelude::Filter::new()
|
||||
.kind(nostr_sdk::prelude::Kind::Custom(ARCHIPELAGO_KIND as u16))
|
||||
@@ -403,7 +406,9 @@ pub async fn publish(
|
||||
for url in relays {
|
||||
let _ = client.add_relay(url).await;
|
||||
}
|
||||
client.connect().await;
|
||||
if tokio::time::timeout(Duration::from_secs(10), client.connect()).await.is_err() {
|
||||
tracing::warn!("Nostr relay connection timed out after 10s, continuing anyway");
|
||||
}
|
||||
|
||||
let builder = nostr_sdk::prelude::EventBuilder::new(
|
||||
nostr_sdk::prelude::Kind::Custom(ARCHIPELAGO_KIND as u16),
|
||||
|
||||
@@ -9,6 +9,7 @@ use anyhow::{Context, Result};
|
||||
use nostr_sdk::prelude::*;
|
||||
use std::net::SocketAddr;
|
||||
use std::path::Path;
|
||||
use std::time::Duration;
|
||||
use tokio::fs;
|
||||
|
||||
/// Parse "host:port" to SocketAddr. Returns None if invalid.
|
||||
@@ -110,7 +111,9 @@ pub async fn publish_node_revocation(
|
||||
for url in LEGACY_RELAYS {
|
||||
let _ = client.add_relay(*url).await;
|
||||
}
|
||||
client.connect().await;
|
||||
if tokio::time::timeout(Duration::from_secs(10), client.connect()).await.is_err() {
|
||||
tracing::warn!("Nostr relay connection timed out after 10s, continuing anyway");
|
||||
}
|
||||
|
||||
// NIP-33 replaceable: empty content overwrites previous event
|
||||
let builder = EventBuilder::new(Kind::Custom(ARCHIPELAGO_KIND as u16), "{}")
|
||||
@@ -197,7 +200,9 @@ pub async fn verify_revocation(
|
||||
for url in LEGACY_RELAYS {
|
||||
let _ = client.add_relay(*url).await;
|
||||
}
|
||||
client.connect().await;
|
||||
if tokio::time::timeout(Duration::from_secs(10), client.connect()).await.is_err() {
|
||||
tracing::warn!("Nostr relay connection timed out after 10s, continuing anyway");
|
||||
}
|
||||
|
||||
let filter = Filter::new()
|
||||
.kind(Kind::Custom(ARCHIPELAGO_KIND as u16))
|
||||
@@ -261,7 +266,9 @@ pub async fn discover_archipelago_nodes(
|
||||
for url in relays {
|
||||
let _ = client.add_relay(url).await;
|
||||
}
|
||||
client.connect().await;
|
||||
if tokio::time::timeout(Duration::from_secs(10), client.connect()).await.is_err() {
|
||||
tracing::warn!("Nostr relay connection timed out after 10s, continuing anyway");
|
||||
}
|
||||
|
||||
let filter = Filter::new()
|
||||
.kind(Kind::Custom(ARCHIPELAGO_KIND as u16))
|
||||
|
||||
@@ -8,7 +8,7 @@ use tokio::fs;
|
||||
use tracing::{debug, info};
|
||||
|
||||
const DEFAULT_UPDATE_MANIFEST_URL: &str =
|
||||
"https://raw.githubusercontent.com/archipelago-os/releases/main/manifest.json";
|
||||
"https://git.tx1138.com/lfg2025/archy/raw/branch/main/releases/manifest.json";
|
||||
const UPDATE_STATE_FILE: &str = "update_state.json";
|
||||
|
||||
fn update_manifest_url() -> String {
|
||||
|
||||
@@ -13,10 +13,10 @@ tracing = "0.1"
|
||||
uuid = { version = "1.0", features = ["v4"] }
|
||||
chrono = { version = "0.4", features = ["serde"] }
|
||||
serde_json = "1.0"
|
||||
aes-gcm = "0.10"
|
||||
rand = "0.8"
|
||||
aes-gcm = "0.10.3"
|
||||
rand = "0.8.5"
|
||||
hex = "0.4"
|
||||
zeroize = { version = "1", features = ["derive"] }
|
||||
zeroize = { version = "1.8.2", features = ["derive"] }
|
||||
|
||||
[dev-dependencies]
|
||||
tempfile = "3"
|
||||
|
||||
Reference in New Issue
Block a user