From 067a3ed1065fde2261bf3b5fbb6fe68197da86c3 Mon Sep 17 00:00:00 2001 From: Dorian Date: Fri, 10 Apr 2026 03:10:49 -0400 Subject: [PATCH] fix: ISO boot, container installs, VPN, nginx, companion input - LUKS auto-unlock: initramfs hook + systemd service + nofail fstab - Rootfs packages: add passt, aardvark-dns, netavark, nftables for Podman 5.x - nginx: resolver + variable proxy_pass for external domains (DNS at boot) - Boot: loglevel=0 suppresses kernel warnings, serial console for QEMU - Container installs: write configs before chown, sudo chown for LUKS volumes - Container installs: build UI sidecars locally (not from registry) for auth injection - Bitcoin UI: inject RPC auth from secrets file, --no-cache rebuild - Secrets: chown to archipelago user in first-boot (backend needs read access) - Podman: image_copy_tmp_dir for read-only /var/tmp in user namespace - NostrVPN: enable service in auto-install, always include public relays - NostrVPN: read tunnel IP from nvpn status (not just config file) - VPN invite: v2 base64 no-pad format matching phone app - Companion input: relay always active, kiosk skips relay listener (prevents double input) - dev-start.sh: production build includes AIUI deployment Co-Authored-By: Claude Opus 4.6 (1M context) --- .../src/api/handler/remote_input.rs | 2 +- .../src/api/rpc/package/install.rs | 112 ++++++++++++------ core/archipelago/src/api/rpc/vpn.rs | 38 +++--- core/archipelago/src/vpn.rs | 55 +++++++-- image-recipe/build-auto-installer-iso.sh | 106 ++++++++++++++--- image-recipe/configs/nginx-archipelago.conf | 84 +++++++++++-- neode-ui/src/App.vue | 7 +- scripts/deploy-to-target.sh | 2 +- scripts/dev-start.sh | 9 ++ scripts/first-boot-containers.sh | 2 + 10 files changed, 328 insertions(+), 89 deletions(-) diff --git a/core/archipelago/src/api/handler/remote_input.rs b/core/archipelago/src/api/handler/remote_input.rs index 2f3dee7c..a7a535e8 100644 --- a/core/archipelago/src/api/handler/remote_input.rs +++ b/core/archipelago/src/api/handler/remote_input.rs @@ -185,7 +185,7 @@ impl ApiHandler { continue; // silently drop } - // Relay to connected browsers (best-effort, ignore if no receivers) + // Always relay to browser clients (remote browser sessions) let _ = relay_tx.send(text.clone()); match handle_input(&text).await { diff --git a/core/archipelago/src/api/rpc/package/install.rs b/core/archipelago/src/api/rpc/package/install.rs index bbe5617c..24e56700 100644 --- a/core/archipelago/src/api/rpc/package/install.rs +++ b/core/archipelago/src/api/rpc/package/install.rs @@ -265,15 +265,22 @@ impl RpcHandler { run_args.push("--device=/dev/net/tun"); } - // Create data directories - self.create_data_dirs(package_id, &volumes).await; + // Create data directories (mkdir only — chown happens AFTER config files are written) + for volume in &volumes { + if let Some(host_path) = volume.split(':').next() { + if host_path.starts_with("/var/lib/archipelago/") { + if let Err(e) = std::fs::create_dir_all(host_path) { + tracing::warn!("Failed to create directory {}: {}", host_path, e); + } + } + } + } - // Pre-install: bitcoin.conf with rpcauth + // Pre-install: write config files BEFORE chown (dir is still owned by archipelago user) if matches!(package_id, "bitcoin" | "bitcoin-core" | "bitcoin-knots") { self.write_bitcoin_conf(&rpc_user, &rpc_pass).await?; } - // Pre-install: lnd.conf with Bitcoin RPC credentials if package_id == "lnd" { self.write_lnd_conf(&rpc_user, &rpc_pass).await?; } @@ -328,6 +335,9 @@ impl RpcHandler { } } + // NOW chown data directories to container UID (after all config files are written) + self.create_data_dirs(package_id, &volumes).await; + // Port mappings (skip for host-network containers) if !is_tailscale { for port in &ports { @@ -637,22 +647,31 @@ impl RpcHandler { } // Set ownership to the mapped UID for rootless podman. - // This needs elevated privileges — use podman unshare to run - // chown inside the user namespace where UIDs are mapped. - let chown_result = tokio::process::Command::new("podman") - .args(["unshare", "chown", "-R", &uid_str, host_path]) + // Try sudo chown first (works on LUKS), fall back to podman unshare. + let host_uid = format!("{}:{}", uid, uid); + let sudo_result = tokio::process::Command::new("sudo") + .args(["chown", "-R", &host_uid, host_path]) .output() .await; - match chown_result { - Ok(out) if !out.status.success() => { - tracing::warn!( - "podman unshare chown failed for {}: {}", - host_path, - String::from_utf8_lossy(&out.stderr) - ); + let sudo_ok = sudo_result.as_ref().map_or(false, |o| o.status.success()); + if !sudo_ok { + // Fallback: podman unshare (works on non-LUKS ext4) + let container_uid = uid - 100000; + let container_uid_str = format!("{}:{}", container_uid, container_uid); + let chown_result = tokio::process::Command::new("podman") + .args(["unshare", "chown", "-R", &container_uid_str, host_path]) + .output() + .await; + match chown_result { + Ok(out) if !out.status.success() => { + tracing::warn!( + "chown failed for {} (both sudo and podman unshare)", + host_path, + ); + } + Err(e) => tracing::warn!("Failed to chown {}: {}", host_path, e), + _ => {} } - Err(e) => tracing::warn!("Failed to chown {}: {}", host_path, e), - _ => {} } } } @@ -917,11 +936,21 @@ autopilot.active=false\n", for dir in ["/opt/archipelago/docker/bitcoin-ui", "/home/archipelago/archy/docker/bitcoin-ui"] { let conf_path = format!("{}/nginx.conf", dir); if let Ok(content) = tokio::fs::read_to_string(&conf_path).await { - if content.contains("__BITCOIN_RPC_AUTH__") { - let updated = content.replace("__BITCOIN_RPC_AUTH__", &auth_b64); - let _ = tokio::fs::write(&conf_path, updated).await; - info!("Injected Bitcoin RPC auth into {}", conf_path); - } + // Replace placeholder or previously-injected auth (regex: Basic followed by base64 or placeholder) + let updated = content + .replace("__BITCOIN_RPC_AUTH__", &auth_b64) + .lines() + .map(|line| { + if line.contains("proxy_set_header Authorization") && line.contains("Basic") { + format!(" proxy_set_header Authorization \"Basic {}\";", auth_b64) + } else { + line.to_string() + } + }) + .collect::>() + .join("\n"); + let _ = tokio::fs::write(&conf_path, format!("{}\n", updated)).await; + info!("Injected Bitcoin RPC auth into {}", conf_path); } } } @@ -949,7 +978,14 @@ autopilot.active=false\n", for (name, ui_dir, image_base) in ui_builds { let name = name.to_string(); - let ui_dir = ui_dir.to_string(); + // Check multiple paths: /opt (production), project tree (dev) + let ui_dir = [ + ui_dir.to_string(), + format!("/home/archipelago/archy/docker/{}", image_base), + format!("/home/archipelago/Projects/archy/docker/{}", image_base), + ].into_iter() + .find(|d| std::path::Path::new(d).join("Dockerfile").exists()) + .unwrap_or_else(|| ui_dir.to_string()); let image_base = image_base.to_string(); let registry = "80.71.235.15:3000/archipelago"; let registry_image = format!("{}/{}:latest", registry, image_base); @@ -961,19 +997,13 @@ autopilot.active=false\n", .output() .await; - // Try registry image first, fall back to local build + // Build locally first (templates may have injected credentials), + // fall back to registry only if no local Dockerfile exists. let image = { - let pull = tokio::process::Command::new("podman") - .args(["pull", ®istry_image]) - .output() - .await; - if pull.map_or(false, |o| o.status.success()) { - info!("Pulled {} UI from registry", name); - registry_image.clone() - } else if std::path::Path::new(&ui_dir).exists() { - info!("Registry pull failed, building {} from {}", name, ui_dir); + if std::path::Path::new(&ui_dir).exists() { + info!("Building {} locally from {}", name, ui_dir); let build = tokio::process::Command::new("podman") - .args(["build", "-t", &local_image, &ui_dir]) + .args(["build", "--no-cache", "-t", &local_image, &ui_dir]) .output() .await; match build { @@ -989,8 +1019,18 @@ autopilot.active=false\n", } } } else { - warn!("No registry image or source for {} — skipping", name); - return; + // No local Dockerfile — try pulling from registry + let pull = tokio::process::Command::new("podman") + .args(["pull", ®istry_image]) + .output() + .await; + if pull.map_or(false, |o| o.status.success()) { + info!("Pulled {} UI from registry", name); + registry_image.clone() + } else { + warn!("No local source or registry image for {} — skipping", name); + return; + } } }; diff --git a/core/archipelago/src/api/rpc/vpn.rs b/core/archipelago/src/api/rpc/vpn.rs index aeae27f2..1e728e59 100644 --- a/core/archipelago/src/api/rpc/vpn.rs +++ b/core/archipelago/src/api/rpc/vpn.rs @@ -269,19 +269,33 @@ impl RpcHandler { let network_id = vpn::read_nvpn_config_list_entry("networks", "network_id").await .unwrap_or_else(|| "nostr-vpn".to_string()); - // Read relays from config + // Read relays from config — filter out localhost relays (unreachable from phone) let relays = vpn::read_nvpn_config_list("nostr", "relays").await; - let relay_csv = if relays.is_empty() { - "wss://relay.damus.io,wss://relay.primal.net".to_string() + let reachable: Vec = relays.iter() + .filter(|r| !r.contains("127.0.0.1") && !r.contains("localhost")) + .cloned() + .collect(); + let invite_relays = if reachable.is_empty() { + vec!["wss://relay.damus.io".to_string(), "wss://relay.primal.net".to_string()] } else { - relays.join(",") + reachable }; - // Build invite URL: nvpn://invite/?npub=&relays= - let invite_url = format!( - "nvpn://invite/{}?npub={}&relays={}", - network_id, npub, relay_csv - ); + // Build invite as base64-encoded JSON (nvpn v2 format, no padding) + use base64::Engine; + let invite_payload = serde_json::json!({ + "v": 2, + "networkName": network_id, + "networkId": network_id, + "inviterNpub": npub, + "inviterNodeName": "archipelago", + "admins": [npub], + "participants": [npub], + "relays": invite_relays, + }); + let invite_b64 = base64::engine::general_purpose::STANDARD_NO_PAD + .encode(invite_payload.to_string().as_bytes()); + let invite_url = format!("nvpn://invite/{}", invite_b64); // Generate QR code let qr = qrcode::QrCode::new(invite_url.as_bytes()) @@ -295,11 +309,7 @@ impl RpcHandler { "qr_svg": svg, "npub": npub, "network_id": network_id, - "relays": if relays.is_empty() { - vec!["wss://relay.damus.io".to_string(), "wss://relay.primal.net".to_string()] - } else { - relays - }, + "relays": invite_relays, })) } diff --git a/core/archipelago/src/vpn.rs b/core/archipelago/src/vpn.rs index 0f612346..b6b6a6df 100644 --- a/core/archipelago/src/vpn.rs +++ b/core/archipelago/src/vpn.rs @@ -322,8 +322,48 @@ async fn get_nostr_vpn_status() -> Result { anyhow::bail!("nostr-vpn service not running"); } - // Quick IP check: read from nvpn config (TOML) - let ip = read_nvpn_config_value("node", "tunnel_ip").await; + // Get tunnel IP: try nvpn status first, fall back to config, then interface + let ip = { + // Method 1: nvpn status (most accurate) + let status_ip = tokio::process::Command::new("nvpn") + .args(["status"]) + .env("HOME", "/var/lib/archipelago/nostr-vpn") + .env("XDG_CONFIG_HOME", "/var/lib/archipelago/nostr-vpn/.config") + .output() + .await + .ok() + .and_then(|o| { + let out = String::from_utf8_lossy(&o.stdout).to_string(); + out.lines() + .find(|l| l.starts_with("tunnel_ip:")) + .map(|l| l.split(':').nth(1).unwrap_or("").trim().to_string()) + }) + .filter(|s| !s.is_empty()); + + if status_ip.is_some() { + status_ip + } else { + // Method 2: config file + let cfg_ip = read_nvpn_config_value("node", "tunnel_ip").await; + if cfg_ip.is_some() { + cfg_ip + } else { + // Method 3: interface IP + tokio::process::Command::new("ip") + .args(["-4", "addr", "show", "nvpn0"]) + .output() + .await + .ok() + .and_then(|o| { + let out = String::from_utf8_lossy(&o.stdout).to_string(); + out.lines() + .find(|l| l.contains("inet ")) + .and_then(|l| l.split_whitespace().nth(1)) + .map(|s| s.to_string()) + }) + } + } + }; Ok(VpnStatus { connected: svc_state == "active", @@ -424,17 +464,18 @@ pub async fn configure_nostr_vpn(data_dir: &Path) -> Result<()> { if should_write { // Gather relay URLs for the config let (relay_onion, relay_direct) = get_relay_urls().await; - let mut relays = Vec::new(); + // Always include public relays so peers can discover each other. + // Local/onion relays are added as extras for direct connectivity. + let mut relays = vec![ + "\"wss://relay.damus.io\"".to_string(), + "\"wss://relay.primal.net\"".to_string(), + ]; if let Some(ref onion) = relay_onion { relays.push(format!("\"{}\"", onion)); } if let Some(ref direct) = relay_direct { relays.push(format!("\"{}\"", direct)); } - if relays.is_empty() { - relays.push("\"wss://relay.damus.io\"".to_string()); - relays.push("\"wss://relay.primal.net\"".to_string()); - } let config_toml = format!( "[nostr]\npublic_key = \"{npub}\"\nsecret_key = \"{nsec}\"\nrelays = [{relays}]\n\n[[networks]]\nnetwork_id = \"archipelago\"\nparticipants = []\n", diff --git a/image-recipe/build-auto-installer-iso.sh b/image-recipe/build-auto-installer-iso.sh index bb74cf61..007ffc65 100755 --- a/image-recipe/build-auto-installer-iso.sh +++ b/image-recipe/build-auto-installer-iso.sh @@ -274,6 +274,10 @@ RUN apt-get update && apt-get install -y --no-install-recommends \ podman \ uidmap \ slirp4netns \ + passt \ + aardvark-dns \ + netavark \ + nftables \ fuse-overlayfs \ tor \ python3 \ @@ -922,10 +926,20 @@ else echo " Capturing backend binary from live server..." fi -# Try to get from live server first (unless BUILD_FROM_SOURCE=1) +# Try to get backend binary: local release build → local install → remote → container build BACKEND_CAPTURED=0 -if [ "$BUILD_FROM_SOURCE" != "1" ]; then - # Direct copy from ARCHIPELAGO_BIN env, local install, or remote + +# Check for local release binary first (works for both BUILD_FROM_SOURCE and normal mode) +LOCAL_RELEASE="$(cd "$SCRIPT_DIR/.." && pwd)/core/target/release/archipelago" +if [ -f "$LOCAL_RELEASE" ]; then + cp "$LOCAL_RELEASE" "$ARCH_DIR/bin/archipelago" + chmod +x "$ARCH_DIR/bin/archipelago" + echo " ✅ Backend from local release build ($(du -h "$ARCH_DIR/bin/archipelago" | cut -f1))" + BACKEND_CAPTURED=1 +fi + +if [ "$BACKEND_CAPTURED" = "0" ] && [ "$BUILD_FROM_SOURCE" != "1" ]; then + # Direct copy from ARCHIPELAGO_BIN env or local install BIN="${ARCHIPELAGO_BIN:-/usr/local/bin/archipelago}" if [ -f "$BIN" ]; then cp "$BIN" "$ARCH_DIR/bin/archipelago" @@ -2006,12 +2020,65 @@ DATA_UUID=$(blkid -s UUID -o value "$DATA_PART") echo "# LUKS2 encrypted data — auto-unlock with key file" > /mnt/target/etc/crypttab echo "archipelago-data UUID=$DATA_UUID /root/.luks-archipelago.key luks,discard" >> /mnt/target/etc/crypttab +# Configure LUKS auto-unlock: three layers to ensure it works +# Layer 1: cryptsetup-initramfs config (tells update-initramfs to embed key) +mkdir -p /mnt/target/etc/cryptsetup-initramfs +cat > /mnt/target/etc/cryptsetup-initramfs/conf <<'CRYPTCONF' +KEYFILE_PATTERN="/root/.luks-*.key" +UMASK=0077 +CRYPTCONF + +# Layer 2: initramfs hook to force-copy key file +mkdir -p /mnt/target/etc/initramfs-tools/hooks +cat > /mnt/target/etc/initramfs-tools/hooks/archipelago-luks <<'LUKSHOOK' +#!/bin/sh +PREREQ="" +prereqs() { echo "$PREREQ"; } +case $1 in prereqs) prereqs; exit 0;; esac +. /usr/share/initramfs-tools/hook-functions +if [ -f /root/.luks-archipelago.key ]; then + mkdir -p "${DESTDIR}/root" + cp /root/.luks-archipelago.key "${DESTDIR}/root/.luks-archipelago.key" + chmod 600 "${DESTDIR}/root/.luks-archipelago.key" +fi +if [ -f /etc/crypttab ]; then + mkdir -p "${DESTDIR}/etc" + cp /etc/crypttab "${DESTDIR}/etc/crypttab" +fi +copy_exec /sbin/cryptsetup +LUKSHOOK +chmod +x /mnt/target/etc/initramfs-tools/hooks/archipelago-luks + +# Layer 3: systemd service as fallback — unlocks LUKS early if initramfs missed it +cat > /mnt/target/etc/systemd/system/archipelago-luks-unlock.service <<'LUKSUNIT' +[Unit] +Description=Unlock Archipelago LUKS data partition +DefaultDependencies=no +Before=local-fs-pre.target +After=systemd-udevd.service +Wants=systemd-udevd.service + +[Service] +Type=oneshot +RemainAfterExit=yes +ExecStart=/bin/bash -c '\ + if [ -e /dev/mapper/archipelago-data ]; then exit 0; fi; \ + DATA_DEV=$(blkid -t TYPE=crypto_LUKS -o device 2>/dev/null | head -1); \ + if [ -z "$DATA_DEV" ]; then exit 0; fi; \ + cryptsetup open --type luks2 --key-file /root/.luks-archipelago.key "$DATA_DEV" archipelago-data' + +[Install] +WantedBy=local-fs-pre.target +LUKSUNIT +chroot /mnt/target systemctl enable archipelago-luks-unlock.service 2>/dev/null || \ + ln -sf /etc/systemd/system/archipelago-luks-unlock.service /mnt/target/etc/systemd/system/local-fs-pre.target.wants/archipelago-luks-unlock.service + # Create fstab cat > /mnt/target/etc/fstab < /mnt/target/home/archipelago/.config/containers/containers.conf <<'CONTAINERSCONF' [network] network_backend = "netavark" +default_rootless_network_cmd = "pasta" + +[engine] +image_copy_tmp_dir = "/var/lib/archipelago/containers/tmp" CONTAINERSCONF + mkdir -p /mnt/target/var/lib/archipelago/containers/tmp + chown -R 1000:1000 /mnt/target/var/lib/archipelago/containers/tmp chown -R 1000:1000 /mnt/target/home/archipelago/.config/containers echo " Configured netavark backend (container DNS enabled)" else @@ -2623,15 +2696,9 @@ if [ -d "$BOOT_MEDIA/archipelago/plymouth-theme" ]; then chroot /mnt/target plymouth-set-default-theme archipelago 2>/dev/null || \ ln -sf /usr/share/plymouth/themes/archipelago/archipelago.plymouth \ /mnt/target/etc/alternatives/default.plymouth 2>/dev/null || true - # Enable splash and ACPI in GRUB - if ! grep -q "splash" /mnt/target/etc/default/grub; then - sed -i 's/GRUB_CMDLINE_LINUX_DEFAULT="\(.*\)"/GRUB_CMDLINE_LINUX_DEFAULT="\1 splash"/' \ - /mnt/target/etc/default/grub 2>/dev/null || true - fi - if ! grep -q "acpi=force" /mnt/target/etc/default/grub; then - sed -i 's/GRUB_CMDLINE_LINUX_DEFAULT="\(.*\)"/GRUB_CMDLINE_LINUX_DEFAULT="\1 acpi=force"/' \ - /mnt/target/etc/default/grub 2>/dev/null || true - fi + # Configure clean boot: splash, suppress kernel noise, hide cursor + sed -i 's/GRUB_CMDLINE_LINUX_DEFAULT=".*"/GRUB_CMDLINE_LINUX_DEFAULT="quiet splash loglevel=0 rd.systemd.show_status=false vt.global_cursor_default=0 acpi=force"/' \ + /mnt/target/etc/default/grub 2>/dev/null || true echo " Installed Archipelago Plymouth theme on target" fi @@ -2816,6 +2883,7 @@ chroot /mnt/target systemctl enable archipelago-load-images.service 2>/dev/null chroot /mnt/target systemctl enable archipelago-setup-tor.service 2>/dev/null || true chroot /mnt/target systemctl enable archipelago-first-boot-containers.service 2>/dev/null || true chroot /mnt/target systemctl enable archipelago-kiosk.service 2>/dev/null || true +chroot /mnt/target systemctl enable nostr-vpn.service 2>/dev/null || true # Enable claude-api-proxy (create symlink manually — chroot systemctl can fail) chroot /mnt/target systemctl enable claude-api-proxy.service 2>/dev/null || \ ln -sf /etc/systemd/system/claude-api-proxy.service /mnt/target/etc/systemd/system/multi-user.target.wants/claude-api-proxy.service 2>/dev/null || true @@ -3079,15 +3147,21 @@ fi set timeout=5 set default=0 +# Serial console for QEMU/headless testing +insmod serial +serial --unit=0 --speed=115200 +terminal_input serial console +terminal_output serial console + # Load font for graphical menu — fallback to text mode on hardware without gfxterm if loadfont ($root)/boot/grub/font.pf2; then set gfxmode=auto set gfxpayload=keep insmod gfxterm insmod png - terminal_output gfxterm + terminal_output gfxterm serial else - terminal_output console + terminal_output console serial fi # Archipelago GRUB theme @@ -3103,7 +3177,7 @@ else fi menuentry "Install Archipelago" --hotkey=i { - linux ($root)/live/vmlinuz boot=live components quiet splash loglevel=0 rd.systemd.show_status=false vt.global_cursor_default=0 acpi=force + linux ($root)/live/vmlinuz boot=live components quiet splash loglevel=0 rd.systemd.show_status=false vt.global_cursor_default=0 acpi=force console=ttyS0,115200 console=tty0 initrd ($root)/live/initrd.img } diff --git a/image-recipe/configs/nginx-archipelago.conf b/image-recipe/configs/nginx-archipelago.conf index bb6d9cc8..0f247a41 100644 --- a/image-recipe/configs/nginx-archipelago.conf +++ b/image-recipe/configs/nginx-archipelago.conf @@ -3,6 +3,10 @@ limit_req_zone $binary_remote_addr zone=rpc:10m rate=20r/s; limit_req_zone $binary_remote_addr zone=auth:10m rate=3r/s; limit_req_zone $binary_remote_addr zone=peer:10m rate=10r/s; +# Resolve external domains at request time (not startup) to prevent boot failures +resolver 1.1.1.1 8.8.8.8 valid=300s ipv6=off; +resolver_timeout 5s; + server { listen 80; server_name _; @@ -46,7 +50,9 @@ server { # AIUI OpenRouter API proxy (API key managed by proxy, no session gate needed) location /aiui/api/openrouter/ { - proxy_pass https://openrouter.ai/api/; + set $upstream_1 "https://openrouter.ai/api/"; + + proxy_pass $upstream_1; proxy_http_version 1.1; proxy_set_header Host openrouter.ai; proxy_ssl_server_name on; @@ -175,6 +181,20 @@ server { error_page 504 = @backend_timeout; } + # LND REST proxy — backend handles auth + CORS + location /proxy/lnd/ { + proxy_pass http://127.0.0.1:5678; + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header Cookie $http_cookie; + proxy_set_header X-Real-IP $remote_addr; + proxy_connect_timeout 10s; + proxy_read_timeout 10s; + proxy_send_timeout 5s; + error_page 502 503 = @backend_unavailable; + error_page 504 = @backend_timeout; + } + # Content sharing — peer access over Tor (no auth) location /content { limit_req zone=peer burst=20 nodelay; @@ -662,7 +682,9 @@ server { # External site proxies — strip X-Frame-Options so iframe embedding works. # add_header here prevents inheritance of server-level X-Frame-Options. location /ext/botfights/ { - proxy_pass https://botfights.net/; + set $upstream_2 "https://botfights.net/"; + + proxy_pass $upstream_2; proxy_http_version 1.1; proxy_set_header Host botfights.net; proxy_set_header Accept-Encoding ""; @@ -684,7 +706,9 @@ server { sub_filter '' ''; } location /ext/484-kitchen/ { - proxy_pass https://484.kitchen/; + set $upstream_3 "https://484.kitchen/"; + + proxy_pass $upstream_3; proxy_http_version 1.1; proxy_set_header Host 484.kitchen; proxy_set_header Accept-Encoding ""; @@ -703,7 +727,9 @@ server { sub_filter '' ''; } location /ext/arch-presentation/ { - proxy_pass https://present.l484.com/; + set $upstream_4 "https://present.l484.com/"; + + proxy_pass $upstream_4; proxy_http_version 1.1; proxy_set_header Host present.l484.com; proxy_set_header Accept-Encoding ""; @@ -722,7 +748,9 @@ server { sub_filter '' ''; } location /ext/nostrudel/ { - proxy_pass https://nostrudel.ninja/; + set $upstream_5 "https://nostrudel.ninja/"; + + proxy_pass $upstream_5; proxy_http_version 1.1; proxy_set_header Host nostrudel.ninja; proxy_set_header Accept-Encoding ""; @@ -818,7 +846,9 @@ server { proxy_send_timeout 120s; } location /aiui/api/openrouter/ { - proxy_pass https://openrouter.ai/api/; + set $upstream_6 "https://openrouter.ai/api/"; + + proxy_pass $upstream_6; proxy_http_version 1.1; proxy_set_header Host openrouter.ai; proxy_ssl_server_name on; @@ -886,6 +916,20 @@ server { error_page 504 = @backend_timeout; } + # LND REST proxy — backend handles auth + CORS + location /proxy/lnd/ { + proxy_pass http://127.0.0.1:5678; + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header Cookie $http_cookie; + proxy_set_header X-Real-IP $remote_addr; + proxy_connect_timeout 10s; + proxy_read_timeout 10s; + proxy_send_timeout 5s; + error_page 502 503 = @backend_unavailable; + error_page 504 = @backend_timeout; + } + # Content sharing — peer access over Tor (no auth) location /content { limit_req zone=peer burst=20 nodelay; @@ -1038,7 +1082,9 @@ server { # External site proxies — strip X-Frame-Options so iframe embedding works. # add_header here prevents inheritance of server-level X-Frame-Options. location /ext/botfights/ { - proxy_pass https://botfights.net/; + set $upstream_7 "https://botfights.net/"; + + proxy_pass $upstream_7; proxy_http_version 1.1; proxy_set_header Host botfights.net; proxy_set_header Accept-Encoding ""; @@ -1060,7 +1106,9 @@ server { sub_filter '' ''; } location /ext/484-kitchen/ { - proxy_pass https://484.kitchen/; + set $upstream_8 "https://484.kitchen/"; + + proxy_pass $upstream_8; proxy_http_version 1.1; proxy_set_header Host 484.kitchen; proxy_set_header Accept-Encoding ""; @@ -1079,7 +1127,9 @@ server { sub_filter '' ''; } location /ext/arch-presentation/ { - proxy_pass https://present.l484.com/; + set $upstream_9 "https://present.l484.com/"; + + proxy_pass $upstream_9; proxy_http_version 1.1; proxy_set_header Host present.l484.com; proxy_set_header Accept-Encoding ""; @@ -1098,7 +1148,9 @@ server { sub_filter '' ''; } location /ext/nostrudel/ { - proxy_pass https://nostrudel.ninja/; + set $upstream_10 "https://nostrudel.ninja/"; + + proxy_pass $upstream_10; proxy_http_version 1.1; proxy_set_header Host nostrudel.ninja; proxy_set_header Accept-Encoding ""; @@ -1139,7 +1191,9 @@ server { listen 8901; server_name _; location / { - proxy_pass https://botfights.net; + set $upstream_11 "https://botfights.net"; + + proxy_pass $upstream_11; proxy_http_version 1.1; proxy_set_header Host botfights.net; proxy_set_header Accept-Encoding ""; @@ -1164,7 +1218,9 @@ server { listen 8902; server_name _; location / { - proxy_pass https://484.kitchen; + set $upstream_12 "https://484.kitchen"; + + proxy_pass $upstream_12; proxy_http_version 1.1; proxy_set_header Host 484.kitchen; proxy_set_header Accept-Encoding ""; @@ -1185,7 +1241,9 @@ server { listen 8903; server_name _; location / { - proxy_pass https://present.l484.com; + set $upstream_13 "https://present.l484.com"; + + proxy_pass $upstream_13; proxy_http_version 1.1; proxy_set_header Host present.l484.com; proxy_set_header Accept-Encoding ""; diff --git a/neode-ui/src/App.vue b/neode-ui/src/App.vue index 8c15aa97..d84c3351 100644 --- a/neode-ui/src/App.vue +++ b/neode-ui/src/App.vue @@ -102,7 +102,12 @@ watch(() => appStore.isAuthenticated, (authenticated) => { if (authenticated) { messageToast.startPolling() screensaverStore.resetInactivityTimer() - startRemoteRelay() + // Don't start relay on kiosk — kiosk gets input via xdotool (system-level), + // relay would duplicate every keystroke/click as DOM events + const isKiosk = window.location.pathname.startsWith('/kiosk') + if (!isKiosk) { + startRemoteRelay() + } } else { messageToast.stopPolling() toastMessage.value = { show: false, text: '' } diff --git a/scripts/deploy-to-target.sh b/scripts/deploy-to-target.sh index 5e86d6f9..b9bbdb28 100755 --- a/scripts/deploy-to-target.sh +++ b/scripts/deploy-to-target.sh @@ -1350,7 +1350,7 @@ import json, os # Protocol services get direct port mapping; web apps map port 80 to their local port PROTOCOL_SERVICES = {"bitcoin", "bitcoin-knots", "electrs", "electrumx", "lnd"} -lines = ["# Auto-generated by Archipelago deploy", "SocksPort 9050", "# ControlPort disabled", ""] +lines = ["# Auto-generated by Archipelago deploy", "SocksPort 0.0.0.0:9050", "# ControlPort disabled", ""] # Try reading services config (check both paths for compatibility) cfg = None diff --git a/scripts/dev-start.sh b/scripts/dev-start.sh index 6a84a578..e87b4c48 100755 --- a/scripts/dev-start.sh +++ b/scripts/dev-start.sh @@ -331,6 +331,15 @@ case $choice in if npx vue-tsc -b --noEmit 2>&1 | tail -3; then npm run build 2>&1 | tail -3 sudo cp -r "$PROJECT_ROOT/web/dist/neode-ui/"* /opt/archipelago/web-ui/ + # Deploy AIUI (pre-built demo or source build) + if [ -d "$PROJECT_ROOT/../AIUI/packages/app/dist" ]; then + sudo cp -r "$PROJECT_ROOT/../AIUI/packages/app/dist/"* /opt/archipelago/web-ui/aiui/ + echo " AIUI deployed from source build" + elif [ -d "$PROJECT_ROOT/demo/aiui" ]; then + sudo mkdir -p /opt/archipelago/web-ui/aiui/ + sudo cp -r "$PROJECT_ROOT/demo/aiui/"* /opt/archipelago/web-ui/aiui/ + echo " AIUI deployed from demo/" + fi echo " Frontend deployed to /opt/archipelago/web-ui/" else echo " FAILED: vue-tsc type check" diff --git a/scripts/first-boot-containers.sh b/scripts/first-boot-containers.sh index c6a38d1a..88e31bdd 100644 --- a/scripts/first-boot-containers.sh +++ b/scripts/first-boot-containers.sh @@ -242,6 +242,8 @@ if [ ! -f "$SECRETS_DIR/bitcoin-rpc-password" ]; then openssl rand -hex 16 > "$SECRETS_DIR/bitcoin-rpc-password" chmod 600 "$SECRETS_DIR/bitcoin-rpc-password" fi +# Ensure archipelago user can read secrets (backend runs as archipelago, not root) +chown -R 1000:1000 "$SECRETS_DIR" BITCOIN_RPC_USER="archipelago" BITCOIN_RPC_PASS=$(cat "$SECRETS_DIR/bitcoin-rpc-password" 2>/dev/null) if [ -z "$BITCOIN_RPC_PASS" ]; then