diff --git a/core/archipelago/src/api/rpc/package/config.rs b/core/archipelago/src/api/rpc/package/config.rs index 166fb3d8..50f529c3 100644 --- a/core/archipelago/src/api/rpc/package/config.rs +++ b/core/archipelago/src/api/rpc/package/config.rs @@ -124,6 +124,12 @@ pub(super) fn get_app_capabilities(app_id: &str) -> Vec { "--cap-add=DAC_OVERRIDE".to_string(), "--cap-add=NET_BIND_SERVICE".to_string(), ], + // Nostr VPN and FIPS: mesh networking daemons need TUN + NET_ADMIN + // Note: --device=/dev/net/tun is added separately in install.rs + "nostr-vpn" | "fips" => vec![ + "--cap-add=NET_ADMIN".to_string(), + "--cap-add=NET_RAW".to_string(), + ], // Default: standard capabilities for rootless podman containers // Most apps need file ownership + port binding to function correctly _ => vec![ @@ -239,6 +245,13 @@ pub(super) fn get_health_check_args(app_id: &str, _rpc_pass: &str) -> Vec ( + "curl -sf http://localhost:8000/v1/models || exit 1", + "30s", + "3", + ), + "nostr-vpn" => ("nvpn status || exit 1", "30s", "3"), + "fips" => ("fipsctl status || exit 1", "30s", "3"), _ => return vec![], }; @@ -279,6 +292,9 @@ pub(super) fn get_memory_limit(app_id: &str) -> &'static str { "dwn" => "256m", "portainer" => "256m", "nostr-rs-relay" | "nostr-relay" => "256m", + "routstr" => "512m", + "nostr-vpn" => "256m", + "fips" => "256m", "nginx-proxy-manager" => "256m", // Databases "archy-btcpay-db" | "archy-mempool-db" | "mysql-mempool" => "512m", @@ -343,6 +359,9 @@ pub(super) fn all_container_names(package_id: &str) -> Vec { "penpot-postgres".into(), "penpot-valkey".into(), "penpot-backend".into(), "penpot-exporter".into(), "penpot-frontend".into(), ], + "nostr-vpn" => vec!["nostr-vpn".into(), "archy-nostr-vpn".into()], + "fips" => vec!["fips".into(), "archy-fips".into()], + "routstr" => vec!["routstr".into(), "archy-routstr".into()], // Default: exact name + archy- prefix _ => vec![base, archy], } @@ -407,6 +426,21 @@ fn read_secret(name: &str, default: &str) -> String { .unwrap_or_else(|_| default.to_string()) } +/// Read the node-level Nostr secret key (hex) for identity-aware apps. +/// Returns empty string if not yet generated. +fn read_nostr_secret_hex() -> String { + std::fs::read_to_string("/var/lib/archipelago/identity/nostr_secret") + .map(|s| s.trim().to_string()) + .unwrap_or_default() +} + +/// Read the node-level Nostr public key (hex). +fn read_nostr_pubkey_hex() -> String { + std::fs::read_to_string("/var/lib/archipelago/identity/nostr_pub") + .map(|s| s.trim().to_string()) + .unwrap_or_default() +} + /// Get app-specific configuration /// Returns: (ports, volumes, env_vars, custom_command, custom_args) pub(super) async fn get_app_config( @@ -771,6 +805,62 @@ pub(super) async fn get_app_config( None, None, ), + "routstr" => { + let nsec = read_nostr_secret_hex(); + let mut env = vec![ + "DATABASE_URL=sqlite:///app/data/keys.db".to_string(), + ]; + if !nsec.is_empty() { + env.push(format!("NSEC={}", nsec)); + env.push(format!("NOSTR_PUBKEY={}", read_nostr_pubkey_hex())); + } + ( + vec!["8200:8000".to_string()], + vec!["/var/lib/archipelago/routstr:/app/data".to_string()], + env, + None, + None, + ) + } + "nostr-vpn" => { + let nsec = read_nostr_secret_hex(); + let mut env = vec![]; + if !nsec.is_empty() { + env.push(format!("NOSTR_SECRET={}", nsec)); + env.push(format!("NOSTR_PUBKEY={}", read_nostr_pubkey_hex())); + } + ( + vec!["51820:51820/udp".to_string()], + vec!["/var/lib/archipelago/nostr-vpn:/root/.config/nvpn".to_string()], + env, + None, + Some(vec![ + "start".to_string(), + "--daemon".to_string(), + ]), + ) + } + "fips" => { + let nsec = read_nostr_secret_hex(); + let mut env = vec![]; + if !nsec.is_empty() { + env.push(format!("FIPS_NOSTR_SECRET={}", nsec)); + env.push(format!("FIPS_NOSTR_PUBKEY={}", read_nostr_pubkey_hex())); + } + ( + vec![ + "2121:2121/udp".to_string(), + "8443:8443".to_string(), + ], + vec![ + "/var/lib/archipelago/fips/config:/etc/fips".to_string(), + "/var/lib/archipelago/fips/run:/run/fips".to_string(), + ], + env, + None, + None, + ) + } "dwn" => ( vec!["3100:3000".to_string()], vec!["/var/lib/archipelago/dwn:/dwn/data".to_string()], diff --git a/core/archipelago/src/api/rpc/package/install.rs b/core/archipelago/src/api/rpc/package/install.rs index 1b4781e0..4f80305b 100644 --- a/core/archipelago/src/api/rpc/package/install.rs +++ b/core/archipelago/src/api/rpc/package/install.rs @@ -225,6 +225,11 @@ impl RpcHandler { } } + // TUN device for mesh networking apps + if matches!(package_id, "nostr-vpn" | "fips") { + run_args.push("--device=/dev/net/tun"); + } + // Create data directories self.create_data_dirs(package_id, &volumes).await; @@ -259,6 +264,35 @@ impl RpcHandler { } } + // Pre-install: write Nostr identity key files for headless Nostr-aware apps + if matches!(package_id, "nostr-vpn" | "fips") { + let nostr_secret = std::fs::read_to_string("/var/lib/archipelago/identity/nostr_secret") + .map(|s| s.trim().to_string()) + .unwrap_or_default(); + if !nostr_secret.is_empty() { + let key_dir = match package_id { + "nostr-vpn" => "/var/lib/archipelago/nostr-vpn", + "fips" => "/var/lib/archipelago/fips/config", + _ => unreachable!(), + }; + let key_path = match package_id { + "nostr-vpn" => format!("{}/nostr_secret", key_dir), + "fips" => format!("{}/fips.key", key_dir), + _ => unreachable!(), + }; + tokio::fs::create_dir_all(key_dir).await.ok(); + tokio::fs::write(&key_path, &nostr_secret).await.ok(); + // Restrict permissions on key file + #[cfg(unix)] + { + use std::os::unix::fs::PermissionsExt; + let perms = std::fs::Permissions::from_mode(0o600); + std::fs::set_permissions(&key_path, perms).ok(); + } + info!("Wrote Nostr identity key for {}", package_id); + } + } + // Port mappings (skip for host-network containers) if !is_tailscale { for port in &ports { diff --git a/image-recipe/build-auto-installer-iso.sh b/image-recipe/build-auto-installer-iso.sh index da538ffc..18d36f20 100755 --- a/image-recipe/build-auto-installer-iso.sh +++ b/image-recipe/build-auto-installer-iso.sh @@ -66,7 +66,8 @@ fi echo "$BUILD_NUM" | sudo tee "$BUILD_COUNTER_FILE" > /dev/null 2>/dev/null || BUILD_NUM=1 GIT_SHORT=$(cd "$SCRIPT_DIR/.." && git rev-parse --short HEAD 2>/dev/null || echo "dev") # Version format: major.minor.patch-prerelease (semver) -BUILD_VERSION="1.3.0-alpha" +# Read version from Cargo.toml (single source of truth) +BUILD_VERSION=$(grep '^version' "$SCRIPT_DIR/../core/archipelago/Cargo.toml" 2>/dev/null | head -1 | sed 's/version = "//;s/"//' || echo "0.0.0") echo "Build #${BUILD_NUM} (${BUILD_VERSION}, commit ${GIT_SHORT})" # Architecture-dependent variables diff --git a/image-recipe/configs/archipelago-kiosk-launcher.sh b/image-recipe/configs/archipelago-kiosk-launcher.sh index 25599263..2683d755 100644 --- a/image-recipe/configs/archipelago-kiosk-launcher.sh +++ b/image-recipe/configs/archipelago-kiosk-launcher.sh @@ -42,12 +42,12 @@ while true; do --disable-save-password-bubble \ --disable-suggestions-service \ --disable-component-update \ - --disable-gpu \ - --disable-gpu-compositing \ - --disable-gpu-rasterization \ - --disable-software-rasterizer \ - --num-raster-threads=1 \ - --renderer-process-limit=1 \ + --enable-gpu-rasterization \ + --num-raster-threads=2 \ + --renderer-process-limit=2 \ + --window-size=9999,9999 \ + --window-position=0,0 \ + --start-fullscreen \ --disable-background-networking \ --disable-background-timer-throttling \ --disable-backgrounding-occluded-windows \ diff --git a/image-recipe/configs/nginx-archipelago.conf b/image-recipe/configs/nginx-archipelago.conf index 0308f265..d32c762a 100644 --- a/image-recipe/configs/nginx-archipelago.conf +++ b/image-recipe/configs/nginx-archipelago.conf @@ -514,6 +514,29 @@ server { default_type application/json; return 503 '{"error":{"code":"NO_WEB_UI","message":"Tailscale is managed via CLI"}}'; } + location /app/routstr/ { + proxy_pass http://127.0.0.1:8200/; + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_hide_header X-Frame-Options; + add_header X-Frame-Options "SAMEORIGIN" always; + proxy_hide_header Content-Security-Policy; + add_header X-Content-Type-Options "nosniff" always; + proxy_set_header Accept-Encoding ""; + sub_filter_once on; + sub_filter '' ''; + } + location /app/nostr-vpn/ { + default_type application/json; + return 503 '{"error":{"code":"NO_WEB_UI","message":"Nostr VPN is managed via CLI"}}'; + } + location /app/fips/ { + default_type application/json; + return 503 '{"error":{"code":"NO_WEB_UI","message":"FIPS is managed via CLI"}}'; + } location /app/ollama/ { proxy_pass http://127.0.0.1:11434/; proxy_http_version 1.1; diff --git a/image-recipe/configs/snippets/archipelago-https-app-proxies.conf b/image-recipe/configs/snippets/archipelago-https-app-proxies.conf index f1d3b4e2..b1a96acf 100644 --- a/image-recipe/configs/snippets/archipelago-https-app-proxies.conf +++ b/image-recipe/configs/snippets/archipelago-https-app-proxies.conf @@ -197,6 +197,28 @@ location /app/tailscale/ { default_type application/json; return 503 '{"error":{"code":"NO_WEB_UI","message":"Tailscale is managed via CLI"}}'; } +location /app/routstr/ { + proxy_pass http://127.0.0.1:8200/; + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_hide_header X-Frame-Options; + add_header X-Frame-Options "SAMEORIGIN" always; + proxy_hide_header Content-Security-Policy; + proxy_set_header Accept-Encoding ""; + sub_filter_once on; + sub_filter '' ''; +} +location /app/nostr-vpn/ { + default_type application/json; + return 503 '{"error":{"code":"NO_WEB_UI","message":"Nostr VPN is managed via CLI"}}'; +} +location /app/fips/ { + default_type application/json; + return 503 '{"error":{"code":"NO_WEB_UI","message":"FIPS is managed via CLI"}}'; +} location /app/ollama/ { proxy_pass http://127.0.0.1:11434/; proxy_http_version 1.1; diff --git a/neode-ui/public/assets/img/app-icons/fips.svg b/neode-ui/public/assets/img/app-icons/fips.svg new file mode 100644 index 00000000..49e3df27 --- /dev/null +++ b/neode-ui/public/assets/img/app-icons/fips.svg @@ -0,0 +1,4 @@ + + + FIPS + diff --git a/neode-ui/public/assets/img/app-icons/nostr-vpn.svg b/neode-ui/public/assets/img/app-icons/nostr-vpn.svg new file mode 100644 index 00000000..52778219 --- /dev/null +++ b/neode-ui/public/assets/img/app-icons/nostr-vpn.svg @@ -0,0 +1,4 @@ + + + NV + diff --git a/neode-ui/public/assets/img/app-icons/routstr.svg b/neode-ui/public/assets/img/app-icons/routstr.svg new file mode 100644 index 00000000..1fcfcd3e --- /dev/null +++ b/neode-ui/public/assets/img/app-icons/routstr.svg @@ -0,0 +1,4 @@ + + + R + diff --git a/neode-ui/src/views/appSession/appSessionConfig.ts b/neode-ui/src/views/appSession/appSessionConfig.ts index 48e93cf4..a2d3f02e 100644 --- a/neode-ui/src/views/appSession/appSessionConfig.ts +++ b/neode-ui/src/views/appSession/appSessionConfig.ts @@ -38,6 +38,7 @@ export const APP_PORTS: Record = { 'fedimintd': 8175, 'fedimint-gateway': 8176, 'nostr-rs-relay': 18081, + 'routstr': 8200, 'indeedhub': 7777, 'dwn': 3100, 'endurain': 8080, @@ -83,6 +84,7 @@ export const HTTPS_PROXY_PATHS: Record = { 'penpot': '/app/penpot/', 'grafana': '/app/grafana/', 'indeedhub': '/app/indeedhub/', + 'routstr': '/app/routstr/', } /** External HTTPS apps -- always loaded directly */ @@ -100,6 +102,7 @@ export const EXTERNAL_URLS: Record = { export const APP_TITLES: Record = { 'bitcoin-knots': 'Bitcoin', 'btcpay-server': 'BTCPay Server', 'indeedhub': 'Indeehub', 'botfights': 'BotFights', '484-kitchen': '484 Kitchen', 'arch-presentation': 'Presentation', + 'nostr-vpn': 'Nostr VPN', 'fips': 'FIPS', 'routstr': 'Routstr', 'homeassistant': 'Home Assistant', 'uptime-kuma': 'Uptime Kuma', 'nginx-proxy-manager': 'Nginx Proxy Manager', 'nostr-rs-relay': 'Nostr Relay', 'call-the-operator': 'Call The Operator', 'syntropy-institute': 'Syntropy Institute', @@ -120,6 +123,7 @@ export const NEW_TAB_APPS = new Set([ 'onlyoffice', 'nginx-proxy-manager', 'tailscale', + 'routstr', ]) /** Sites known to block iframes -- skip the timeout and go straight to fallback */ diff --git a/neode-ui/src/views/appSession/useAppIdentity.ts b/neode-ui/src/views/appSession/useAppIdentity.ts index ef10be85..a60f72be 100644 --- a/neode-ui/src/views/appSession/useAppIdentity.ts +++ b/neode-ui/src/views/appSession/useAppIdentity.ts @@ -15,7 +15,7 @@ export interface SelectedIdentity { } function isIdentityAwareApp(id: string): boolean { - return id === 'indeedhub' || id === 'nostrudel' + return id === 'indeedhub' || id === 'nostrudel' || id === 'routstr' } export function useAppIdentity( diff --git a/neode-ui/src/views/apps/appsConfig.ts b/neode-ui/src/views/apps/appsConfig.ts index c34c8dd0..d6018325 100644 --- a/neode-ui/src/views/apps/appsConfig.ts +++ b/neode-ui/src/views/apps/appsConfig.ts @@ -39,6 +39,7 @@ export const APP_CATEGORY_MAP: Record = { 'homeassistant': 'home', 'lorabell': 'home', 'endurain': 'home', 'searxng': 'community', 'ollama': 'community', 'grafana': 'data', 'nostr-rs-relay': 'nostr', 'nostrudel': 'nostr', + 'nostr-vpn': 'networking', 'fips': 'networking', 'routstr': 'community', 'tailscale': 'networking', 'nginx-proxy-manager': 'networking', 'portainer': 'networking', 'uptime-kuma': 'networking', 'dwn': 'data', 'botfights': 'l484', 'nwnn': 'l484', '484-kitchen': 'l484', @@ -103,7 +104,7 @@ export const WEB_ONLY_APPS: Record = { export const TAB_LAUNCH_APPS = new Set([ 'btcpay-server', 'grafana', 'photoprism', 'homeassistant', 'vaultwarden', 'nextcloud', 'uptime-kuma', 'portainer', - 'cryptpad', 'nginx-proxy-manager', 'tailscale', + 'cryptpad', 'nginx-proxy-manager', 'tailscale', 'routstr', ]) export function opensInTab(id: string): boolean { diff --git a/neode-ui/src/views/discover/curatedApps.ts b/neode-ui/src/views/discover/curatedApps.ts index d35bb4ff..970b2d91 100644 --- a/neode-ui/src/views/discover/curatedApps.ts +++ b/neode-ui/src/views/discover/curatedApps.ts @@ -29,6 +29,9 @@ export function getCuratedAppList(): MarketplaceApp[] { { id: 'nostr-rs-relay', title: 'Nostr Relay', version: '0.9.0', category: 'nostr', description: 'Your own Nostr relay. Store events locally, relay for friends, publish over Tor.', icon: '/assets/img/app-icons/nostr-rs-relay.svg', author: 'scsiblade', dockerImage: `${R}/nostr-rs-relay:0.9.0`, repoUrl: 'https://sr.ht/~gheartsfield/nostr-rs-relay/' }, { id: 'indeedhub', title: 'Indeehub', version: '0.1.0', description: 'Bitcoin documentary streaming with Nostr identity. Stream sovereignty content.', icon: '/assets/img/app-icons/indeedhub.png', author: 'Indeehub Team', dockerImage: 'localhost/indeedhub:latest', repoUrl: 'https://github.com/indeedhub/indeedhub' }, { id: 'dwn', title: 'Decentralized Web Node', version: '0.4.0', description: 'Own your data with DID-based access control. Sync across devices, sovereign.', icon: '/assets/img/app-icons/dwn.svg', author: 'TBD', dockerImage: `${R}/dwn-server:main`, repoUrl: 'https://github.com/TBD54566975/dwn-server' }, + { id: 'nostr-vpn', title: 'Nostr VPN', version: '0.3.4', category: 'networking', description: 'Tailscale-style mesh VPN with Nostr control plane. Peer discovery and key exchange over relays, WireGuard tunnels.', icon: '/assets/img/app-icons/nostr-vpn.svg', author: 'Martti Malmi', dockerImage: `${R}/nostr-vpn:v0.3.4`, repoUrl: 'https://github.com/mmalmi/nostr-vpn' }, + { id: 'fips', title: 'FIPS', version: '0.1.0', category: 'networking', description: 'Free Internetworking Peering System. Self-organizing encrypted mesh network with Nostr identity.', icon: '/assets/img/app-icons/fips.svg', author: 'Jim Corgan', dockerImage: `${R}/fips:v0.1.0`, repoUrl: 'https://github.com/jmcorgan/fips' }, + { id: 'routstr', title: 'Routstr', version: '0.4.3', category: 'community', description: 'Decentralized AI inference proxy. Pay-per-request with Cashu ecash, provider discovery via Nostr.', icon: '/assets/img/app-icons/routstr.svg', author: 'Routstr', dockerImage: `${R}/routstr:v0.4.3`, repoUrl: 'https://github.com/routstr/routstr-core' }, { id: 'nostrudel', title: 'noStrudel', version: '0.40.0', category: 'nostr', description: 'Feature-rich Nostr web client. Browse feeds, post notes, manage relays with NIP-07.', icon: '/assets/img/app-icons/nostrudel.svg', author: 'hzrd149', dockerImage: '', repoUrl: 'https://github.com/hzrd149/nostrudel', webUrl: 'https://nostrudel.ninja' }, { id: 'botfights', title: 'BotFights', version: '1.0.0', description: 'AI bot arena — build, train, and battle autonomous agents in strategy tournaments.', icon: '/assets/img/app-icons/botfights.svg', author: 'BotFights', dockerImage: '', repoUrl: 'https://botfights.net', webUrl: 'https://botfights.net' }, { id: 'nwnn', title: 'Next Web News Network', version: '1.0.0', category: 'l484', description: 'Decentralized news aggregator. Community-curated Bitcoin and sovereignty content.', icon: '/assets/img/app-icons/nwnn.png', author: 'L484', dockerImage: '', repoUrl: 'https://nwnn.l484.com', webUrl: 'https://nwnn.l484.com' }, @@ -58,6 +61,9 @@ export const INSTALLED_ALIASES: Record = { filebrowser: ['filebrowser'], tailscale: ['tailscale'], ollama: ['ollama'], + 'nostr-vpn': ['nostr-vpn'], + fips: ['fips'], + routstr: ['routstr'], } export const FEATURED_DEFINITIONS = [ diff --git a/neode-ui/src/views/marketplace/marketplaceData.ts b/neode-ui/src/views/marketplace/marketplaceData.ts index f6a67eeb..55585b63 100644 --- a/neode-ui/src/views/marketplace/marketplaceData.ts +++ b/neode-ui/src/views/marketplace/marketplaceData.ts @@ -54,6 +54,9 @@ export const INSTALLED_ALIASES: Record = { filebrowser: ['filebrowser'], tailscale: ['tailscale'], ollama: ['ollama'], + 'nostr-vpn': ['nostr-vpn'], + fips: ['fips'], + routstr: ['routstr'], } /** Get app tier classification (matches backend get_app_tier) */ @@ -402,6 +405,42 @@ export function getCuratedAppList(): MarketplaceApp[] { manifestUrl: undefined, repoUrl: 'https://github.com/TBD54566975/dwn-server' }, + { + id: 'nostr-vpn', + title: 'Nostr VPN', + version: '0.3.4', + category: 'networking', + description: 'Tailscale-style mesh VPN with Nostr control plane. Peer discovery and key exchange over relays, WireGuard tunnels.', + icon: '/assets/img/app-icons/nostr-vpn.svg', + author: 'Martti Malmi', + dockerImage: `${REGISTRY}/nostr-vpn:v0.3.4`, + manifestUrl: undefined, + repoUrl: 'https://github.com/mmalmi/nostr-vpn' + }, + { + id: 'fips', + title: 'FIPS', + version: '0.1.0', + category: 'networking', + description: 'Free Internetworking Peering System. Self-organizing encrypted mesh network with Nostr identity.', + icon: '/assets/img/app-icons/fips.svg', + author: 'Jim Corgan', + dockerImage: `${REGISTRY}/fips:v0.1.0`, + manifestUrl: undefined, + repoUrl: 'https://github.com/jmcorgan/fips' + }, + { + id: 'routstr', + title: 'Routstr', + version: '0.4.3', + category: 'community', + description: 'Decentralized AI inference proxy. Pay-per-request with Cashu ecash, provider discovery via Nostr.', + icon: '/assets/img/app-icons/routstr.svg', + author: 'Routstr', + dockerImage: `${REGISTRY}/routstr:v0.4.3`, + manifestUrl: undefined, + repoUrl: 'https://github.com/routstr/routstr-core' + }, { id: 'nostrudel', title: 'noStrudel', diff --git a/scripts/image-versions.sh b/scripts/image-versions.sh index 3d40bb0c..ad28a3e0 100644 --- a/scripts/image-versions.sh +++ b/scripts/image-versions.sh @@ -63,6 +63,11 @@ VALKEY_IMAGE="$ARCHY_REGISTRY/valkey:8.1.6" # Nostr NOSTR_RS_RELAY_IMAGE="$ARCHY_REGISTRY/nostr-rs-relay:0.9.0" STRFRY_IMAGE="$ARCHY_REGISTRY/strfry:1.0.4" +NOSTR_VPN_IMAGE="$ARCHY_REGISTRY/nostr-vpn:v0.3.4" +FIPS_IMAGE="$ARCHY_REGISTRY/fips:v0.1.0" + +# AI / Routing +ROUTSTR_IMAGE="$ARCHY_REGISTRY/routstr:v0.4.3" # IndeedHub stack (local builds use :local tag, not :latest) MINIO_IMAGE="$ARCHY_REGISTRY/minio:RELEASE.2024-11-07T00-52-20Z"