#!/usr/bin/env node /** * Archipelago Mock Backend Server * Pure Archipelago implementation - NO StartOS dependencies * Supports dev modes: setup, onboarding, existing */ import express from 'express' import cors from 'cors' import cookieParser from 'cookie-parser' import { WebSocketServer } from 'ws' import http from 'http' import { exec } from 'child_process' import { promisify } from 'util' import fs from 'fs/promises' import path from 'path' import { fileURLToPath } from 'url' import Docker from 'dockerode' const __filename = fileURLToPath(import.meta.url) const __dirname = path.dirname(__filename) const execPromise = promisify(exec) // Find container socket: Podman (macOS/Linux) or Docker import { existsSync } from 'fs' function findContainerSocket() { // DOCKER_HOST env var (set by podman machine start) if (process.env.DOCKER_HOST) { const p = process.env.DOCKER_HOST.replace('unix://', '') if (existsSync(p)) return p } // Podman machine socket (macOS) — check TMPDIR-based path if (process.env.TMPDIR) { const podmanDir = path.join(path.dirname(process.env.TMPDIR), 'podman') const sock = path.join(podmanDir, 'podman-machine-default-api.sock') if (existsSync(sock)) return sock } // Docker socket if (existsSync('/var/run/docker.sock')) return '/var/run/docker.sock' // Linux podman rootless const uid = process.getuid?.() || 1000 const linuxSock = `/run/user/${uid}/podman/podman.sock` if (existsSync(linuxSock)) return linuxSock return null } const containerSocket = findContainerSocket() const docker = containerSocket ? new Docker({ socketPath: containerSocket }) : null if (containerSocket) { console.log(`[Container] Socket: ${containerSocket}`) } else { console.log('[Container] No socket found — simulation mode (no Docker/Podman)') } const app = express() const PORT = 5959 // Dev mode from environment (setup, onboarding, existing, boot, or default) const DEV_MODE = process.env.VITE_DEV_MODE || 'default' // Boot mode: simulate server startup delay let BOOT_START_TIME = Date.now() const BOOT_DELAY_MS = 25000 // 25 seconds of simulated startup (slower for analysis) // CORS configuration const corsOptions = { credentials: true, origin: (origin, callback) => { if (!origin) return callback(null, true) callback(null, true) } } app.use(cors(corsOptions)) // Skip JSON body parsing for filebrowser upload routes (binary file bodies) app.use((req, res, next) => { if (req.path.startsWith('/app/filebrowser/api/resources') && req.method === 'POST') { return next() } express.json({ limit: '50mb' })(req, res, next) }) app.use(cookieParser()) // Mock session storage const sessions = new Map() const MOCK_PASSWORD = 'password123' // Mutable wallet state — faucet/send/receive modify these values const walletState = { onchain_sats: 2_350_000, channel_sats: 8_250_000, ecash_sats: 250_000, ecash_tokens: 12, block_height: 892451, transactions: [ { tx_hash: 'ab12cd34ef5678901234567890abcdef12345678', amount_sats: 2_000_000, direction: 'incoming', num_confirmations: 142, block_height: 892310, time_stamp: Math.floor(Date.now()/1000) - 86400, label: 'Channel funding', total_fees: 0, dest_addresses: [] }, { tx_hash: 'cd34ef5678901234567890abcdef1234567890ab', amount_sats: 250_000, direction: 'incoming', num_confirmations: 28, block_height: 892420, time_stamp: Math.floor(Date.now()/1000) - 7200, label: 'Faucet deposit', total_fees: 0, dest_addresses: [] }, { tx_hash: 'ff99ee88dd7766554433221100aabbccddeeff00', amount_sats: 100_000, direction: 'incoming', num_confirmations: 0, block_height: 0, time_stamp: Math.floor(Date.now()/1000) - 600, label: 'Incoming from faucet', total_fees: 0, dest_addresses: [] }, ], } function randomHex(bytes) { return Array.from({length: bytes}, () => Math.floor(Math.random()*256).toString(16).padStart(2,'0')).join('') } const bitcoinRelayMockState = { settings: { enabled_for_peers: true, allow_peer_requests: true, allow_http: false, allow_https: true, allow_tor: true, selected_peer_pubkey: '03d9b8a8db6b4f4d8b8d04c7a467c101f04c0ecbabc0e29e4dcb812a3b1c5f8f04', http_endpoint: '', https_endpoint: 'https://shard.tx1138.com/', tor_endpoint: 'http://btc-relay-demoabcdefghijklmnop.onion/', }, trusted_nodes: [ { pubkey: '03d9b8a8db6b4f4d8b8d04c7a467c101f04c0ecbabc0e29e4dcb812a3b1c5f8f04', onion: 'trustedalphaabcdefghijklmnop.onion', name: 'Trusted Alpha', relay_approved: true, }, { pubkey: '02f6ab6c88037cd527a92f3a016a7bd18bb2ebd91d5a3efb1161481b5cf7d9ea2a', onion: 'trustedbetabcdefghijklmnopq.onion', name: 'Trusted Beta', relay_approved: false, }, ], requests: [ { id: 'relay-demo-incoming', direction: 'incoming', status: 'pending', peer_pubkey: '02f6ab6c88037cd527a92f3a016a7bd18bb2ebd91d5a3efb1161481b5cf7d9ea2a', peer_onion: 'trustedbetabcdefghijklmnopq.onion', peer_name: 'Trusted Beta', message: 'Can I use your node for a Wasabi broadcast?', created_at: new Date(Date.now() - 12 * 60 * 1000).toISOString(), updated_at: new Date(Date.now() - 12 * 60 * 1000).toISOString(), }, ], } // User state (simulated file-based storage) let userState = { setupComplete: false, onboardingComplete: false, passwordHash: null, // In real app, this would be bcrypt hash } let mockState = { analyticsEnabled: false } // Initialize user state based on dev mode function initializeUserState() { switch (DEV_MODE) { case 'setup': // Setup mode: Original StartOS node setup - user needs to set password // This is the simple password setup, NOT the experimental onboarding userState = { setupComplete: false, // User hasn't set password yet onboardingComplete: false, // Onboarding not relevant for setup mode passwordHash: null, } break case 'onboarding': // Onboarding mode: Experimental onboarding flow // User has set password (via setup) but needs to go through experimental onboarding userState = { setupComplete: true, // Password already set onboardingComplete: false, // Needs experimental onboarding passwordHash: MOCK_PASSWORD, } break case 'existing': // Existing user: Fully set up, just needs to login userState = { setupComplete: true, onboardingComplete: true, passwordHash: MOCK_PASSWORD, } break case 'boot': // Boot mode: Simulate server startup delay (shows boot screen) // Server responds with 502 for the first 10 seconds, then works like onboarding mode userState = { setupComplete: true, onboardingComplete: false, passwordHash: MOCK_PASSWORD, } break default: // Default: Fully set up (for UI development) userState = { setupComplete: true, onboardingComplete: true, passwordHash: MOCK_PASSWORD, } } console.log(`[Auth] Dev mode: ${DEV_MODE}`) console.log(`[Auth] Setup: ${userState.setupComplete}, Onboarding: ${userState.onboardingComplete}`) } initializeUserState() // WebSocket clients for broadcasting updates const wsClients = new Set() // Helper: Broadcast data update to all WebSocket clients function broadcastUpdate(patch) { const message = JSON.stringify({ rev: Date.now(), patch: patch }) wsClients.forEach(client => { if (client.readyState === 1) { // OPEN client.send(message) } }) } // Track used ports and running containers const usedPorts = new Set([5959, 8100]) const runningContainers = new Map() // Predefined port mappings for known apps const portMappings = { 'atob': 8102, 'k484': 8103, 'amin': 8104, 'filebrowser': 8083, 'bitcoin-knots': 8332, 'electrs': 50001, 'btcpay-server': 23000, 'lnd': 8080, 'mempool': 4080, 'homeassistant': 8123, 'grafana': 3000, 'searxng': 8888, 'ollama': 11434, 'nextcloud': 8082, 'vaultwarden': 8222, 'jellyfin': 8096, 'photoprism': 2342, 'immich': 2283, 'portainer': 9443, 'uptime-kuma': 3001, 'tailscale': 41641, 'fedimint': 8175, 'thunderhub': 3010, 'nostr-rs-relay': 7000, 'syncthing': 8384, 'penpot': 9001, 'nginx-proxy-manager': 8181, 'indeedhub': 8190, 'dwn': 3000, 'tor': 9050, } // Auto-assign port for unknown apps (start at 8200, increment) let nextAutoPort = 8200 // Helper: Query real Docker containers async function getDockerContainers() { if (!docker) return {} try { const containers = await docker.listContainers({ all: true }) // Map of container names to app IDs const containerMapping = { 'archy-bitcoin': 'bitcoin', 'archy-btcpay': 'btcpay-server', 'archy-homeassistant': 'homeassistant', 'archy-grafana': 'grafana', 'archy-endurain': 'endurain', 'archy-fedimint': 'fedimint', 'archy-morphos': 'morphos-server', 'archy-lnd': 'lightning-stack', 'archy-mempool-web': 'mempool', 'mempool-electrs': 'mempool-electrs', 'archy-ollama': 'ollama', 'archy-searxng': 'searxng', 'archy-penpot-frontend': 'penpot' } const apps = {} for (const container of containers) { const name = container.Names[0].replace(/^\//, '') const appId = containerMapping[name] if (!appId) continue const isRunning = container.State === 'running' const ports = container.Ports || [] const hostPort = ports.find(p => p.PublicPort)?.PublicPort || null // Get app metadata const appMetadata = { 'bitcoin': { title: 'Bitcoin Core', icon: '/assets/img/app-icons/bitcoin.svg', description: 'Full Bitcoin node implementation' }, 'btcpay-server': { title: 'BTCPay Server', icon: '/assets/img/app-icons/btcpay-server.png', description: 'Self-hosted Bitcoin payment processor' }, 'homeassistant': { title: 'Home Assistant', icon: '/assets/img/app-icons/homeassistant.png', description: 'Open source home automation platform' }, 'grafana': { title: 'Grafana', icon: '/assets/img/grafana.png', description: 'Analytics and monitoring platform' }, 'endurain': { title: 'Endurain', icon: '/assets/img/endurain.png', description: 'Application platform' }, 'fedimint': { title: 'Fedimint', icon: '/assets/img/app-icons/fedimint.png', description: 'Federated Bitcoin mint' }, 'morphos-server': { title: 'MorphOS Server', icon: '/assets/img/morphos.png', description: 'Server platform' }, 'lightning-stack': { title: 'Lightning Stack', icon: '/assets/img/app-icons/lightning-stack.png', description: 'Lightning Network (LND)' }, 'mempool': { title: 'Mempool', icon: '/assets/img/app-icons/mempool.png', description: 'Bitcoin blockchain explorer' }, 'mempool-electrs': { title: 'Electrs', icon: '/assets/img/app-icons/electrs.svg', description: 'Electrum protocol indexer for Bitcoin' }, 'ollama': { title: 'Ollama', icon: '/assets/img/app-icons/ollama.png', description: 'Run large language models locally' }, 'searxng': { title: 'SearXNG', icon: '/assets/img/app-icons/searxng.png', description: 'Privacy-respecting metasearch engine' }, 'penpot': { title: 'Penpot', icon: '/assets/img/penpot.webp', description: 'Open-source design and prototyping' } } const metadata = appMetadata[appId] || { title: appId, icon: '/assets/icon/pwa-192x192-v2.png', description: `${appId} application` } apps[appId] = { title: metadata.title, version: '1.0.0', status: isRunning ? 'running' : 'stopped', state: isRunning ? 'running' : 'stopped', health: isRunning ? 'healthy' : null, 'static-files': { license: 'MIT', instructions: metadata.description, icon: metadata.icon }, manifest: { id: appId, title: metadata.title, version: '1.0.0', description: { short: metadata.description, long: metadata.description }, 'release-notes': 'Initial release', license: 'MIT', 'wrapper-repo': '#', 'upstream-repo': '#', 'support-site': '#', 'marketing-site': '#', 'donation-url': null, interfaces: hostPort ? { main: { name: 'Web Interface', description: `${metadata.title} web interface`, ui: true } } : {} }, installed: { 'current-dependents': {}, 'current-dependencies': {}, 'last-backup': null, 'interface-addresses': hostPort ? { main: { 'tor-address': `${appId}.onion`, 'lan-address': `http://localhost:${hostPort}` } } : {}, status: isRunning ? 'running' : 'stopped' } } } return apps } catch (error) { console.error('[Docker] Error querying containers:', error.message) return {} } } // Helper: Check if Docker/Podman is available async function isContainerRuntimeAvailable() { try { // Try Podman first (Archipelago's choice) await execPromise('podman ps') return { available: true, runtime: 'podman' } } catch { try { // Fallback to Docker await execPromise('docker ps') return { available: true, runtime: 'docker' } } catch { return { available: false, runtime: null } } } } // Marketplace metadata lookup for install (title, description, icon, version) const marketplaceMetadata = { 'bitcoin-knots': { title: 'Bitcoin Knots', shortDesc: 'Full Bitcoin node — validate and relay blocks and transactions', icon: '/assets/img/app-icons/bitcoin-knots.webp' }, 'electrs': { title: 'Electrs', shortDesc: 'Electrum protocol indexer for Bitcoin', icon: '/assets/img/app-icons/electrs.svg' }, 'btcpay-server': { title: 'BTCPay Server', shortDesc: 'Self-hosted Bitcoin payment processor', icon: '/assets/img/app-icons/btcpay-server.png' }, 'lnd': { title: 'LND', shortDesc: 'Lightning Network Daemon', icon: '/assets/img/app-icons/lnd.svg' }, 'mempool': { title: 'Mempool Explorer', shortDesc: 'Bitcoin blockchain and mempool visualizer', icon: '/assets/img/app-icons/mempool.webp' }, 'homeassistant': { title: 'Home Assistant', shortDesc: 'Open-source home automation platform', icon: '/assets/img/app-icons/homeassistant.png' }, 'grafana': { title: 'Grafana', shortDesc: 'Analytics and monitoring dashboards', icon: '/assets/img/app-icons/grafana.png' }, 'searxng': { title: 'SearXNG', shortDesc: 'Privacy-respecting metasearch engine', icon: '/assets/img/app-icons/searxng.png' }, 'ollama': { title: 'Ollama', shortDesc: 'Run large language models locally', icon: '/assets/img/app-icons/ollama.png' }, 'penpot': { title: 'Penpot', shortDesc: 'Open-source design and prototyping platform', icon: '/assets/img/app-icons/penpot.webp' }, 'nextcloud': { title: 'Nextcloud', shortDesc: 'Self-hosted cloud storage and collaboration', icon: '/assets/img/app-icons/nextcloud.webp' }, 'vaultwarden': { title: 'Vaultwarden', shortDesc: 'Self-hosted password manager (Bitwarden-compatible)', icon: '/assets/img/app-icons/vaultwarden.webp' }, 'jellyfin': { title: 'Jellyfin', shortDesc: 'Free media server for movies, music, and photos', icon: '/assets/img/app-icons/jellyfin.webp' }, 'photoprism': { title: 'PhotoPrism', shortDesc: 'AI-powered photo management', icon: '/assets/img/app-icons/photoprism.svg' }, 'immich': { title: 'Immich', shortDesc: 'High-performance photo and video backup', icon: '/assets/img/app-icons/immich.png' }, 'filebrowser': { title: 'File Browser', shortDesc: 'Web-based file manager', icon: '/assets/img/app-icons/file-browser.webp' }, 'nginx-proxy-manager': { title: 'Nginx Proxy Manager', shortDesc: 'Easy proxy management with SSL', icon: '/assets/img/app-icons/nginx.svg' }, 'portainer': { title: 'Portainer', shortDesc: 'Container management UI', icon: '/assets/img/app-icons/portainer.webp' }, 'uptime-kuma': { title: 'Uptime Kuma', shortDesc: 'Self-hosted monitoring tool', icon: '/assets/img/app-icons/uptime-kuma.webp' }, 'tailscale': { title: 'Tailscale', shortDesc: 'Zero-config VPN for secure remote access', icon: '/assets/img/app-icons/tailscale.webp' }, 'fedimint': { title: 'Fedimint', shortDesc: 'Federated Bitcoin mint with Guardian UI', icon: '/assets/img/app-icons/fedimint.png' }, 'indeedhub': { title: 'Indeehub', shortDesc: 'Bitcoin documentary streaming platform', icon: '/assets/img/app-icons/indeedhub.png' }, 'dwn': { title: 'Decentralized Web Node', shortDesc: 'Store and sync personal data with DID-based access', icon: '/assets/img/app-icons/dwn.svg' }, 'nostr-rs-relay': { title: 'Nostr Relay', shortDesc: 'Run your own Nostr relay', icon: '/assets/img/app-icons/nostr-rs-relay.svg' }, 'syncthing': { title: 'Syncthing', shortDesc: 'Peer-to-peer file synchronization', icon: '/assets/img/app-icons/syncthing.png' }, 'thunderhub': { title: 'ThunderHub', shortDesc: 'Lightning node management UI with channel management and payments', icon: '/assets/img/app-icons/thunderhub.svg' }, 'tor': { title: 'Tor', shortDesc: 'Anonymous communication over the Tor network', icon: '/assets/img/app-icons/tor.png' }, 'amin': { title: 'Amin', shortDesc: 'Administrative interface for Archipelago', icon: '/assets/icon/pwa-192x192-v2.png' }, } // Helper: Install package with container runtime (if available) or simulate async function installPackage(id, manifestUrl, opts = {}) { console.log(`[Package] đŸ“Ļ Installing ${id}...`) try { // Check if already installed if (mockData['package-data'][id]) { throw new Error(`Package ${id} is already installed`) } const version = opts.version || '0.1.0' const runtime = await isContainerRuntimeAvailable() // Get package metadata from marketplace lookup, then fallback const metadata = marketplaceMetadata[id] || { title: id.split('-').map(w => w.charAt(0).toUpperCase() + w.slice(1)).join(' '), shortDesc: `${id} application`, icon: `/assets/img/app-icons/${id}.png` } // Determine port — use known mapping, or auto-assign a unique one let assignedPort = portMappings[id] if (!assignedPort) { while (usedPorts.has(nextAutoPort)) nextAutoPort++ assignedPort = nextAutoPort++ } usedPorts.add(assignedPort) let containerMode = false let actuallyRunning = false // Try to run with container runtime if available if (runtime.available) { try { console.log(`[Package] đŸŗ ${runtime.runtime} available, attempting to run container...`) const containerName = `${id}-archipelago` const stopCmd = runtime.runtime === 'podman' ? `podman stop ${containerName} 2>/dev/null || true` : `docker stop ${containerName} 2>/dev/null || true` const rmCmd = runtime.runtime === 'podman' ? `podman rm ${containerName} 2>/dev/null || true` : `docker rm ${containerName} 2>/dev/null || true` // Stop and remove existing container if it exists await execPromise(stopCmd) await execPromise(rmCmd) // Check if image exists const imageCheckCmd = runtime.runtime === 'podman' ? `podman images -q ${id}:${version}` : `docker images -q ${id}:${version}` let { stdout } = await execPromise(imageCheckCmd) if (stdout.trim()) { // Image exists, start container const runCmd = runtime.runtime === 'podman' ? `podman run -d --name ${containerName} -p ${assignedPort}:80 ${id}:${version}` : `docker run -d --name ${containerName} -p ${assignedPort}:80 ${id}:${version}` await execPromise(runCmd) // Wait for container to be ready await new Promise(resolve => setTimeout(resolve, 2000)) // Verify container is running const statusCmd = runtime.runtime === 'podman' ? `podman ps --filter name=${containerName} --format "{{.Status}}"` : `docker ps --filter name=${containerName} --format "{{.Status}}"` const { stdout: containerStatus } = await execPromise(statusCmd) if (containerStatus.includes('Up')) { containerMode = true actuallyRunning = true runningContainers.set(id, { port: assignedPort, containerId: containerName, runtime: runtime.runtime }) console.log(`[Package] đŸŗ ${runtime.runtime} container running on port ${assignedPort}`) } } else { console.log(`[Package] â„šī¸ Container image ${id}:${version} not found, using simulation mode`) } } catch (containerError) { console.log(`[Package] âš ī¸ Container error (${containerError.message}), falling back to simulation`) } } else { console.log(`[Package] â„šī¸ Container runtime not available, using simulation mode`) } // If container didn't work, simulate installation if (!containerMode) { await new Promise(resolve => setTimeout(resolve, 1500)) runningContainers.set(id, { port: assignedPort, containerId: null, runtime: null }) } // Add to mock data using staticApp format for consistency mockData['package-data'][id] = { ...staticApp({ id, title: metadata.title, version, shortDesc: metadata.shortDesc, longDesc: metadata.shortDesc, state: 'running', lanPort: assignedPort, icon: metadata.icon, }), port: assignedPort, containerMode: containerMode, actuallyRunning: actuallyRunning, } // Broadcast update broadcastUpdate([ { op: 'add', path: `/package-data/${id}`, value: mockData['package-data'][id] } ]) if (containerMode) { console.log(`[Package] ✅ ${id} installed and RUNNING at http://localhost:${assignedPort}`) } else { console.log(`[Package] ✅ ${id} installed (simulated)`) } return { success: true, port: assignedPort, containerMode } } catch (error) { console.error(`[Package] ❌ Installation failed:`, error.message) throw error } } // Helper: Uninstall package async function uninstallPackage(id) { console.log(`[Package] đŸ—‘ī¸ Uninstalling ${id}...`) try { if (staticDevApps[id]) { throw new Error(`${id} is a demo app and cannot be uninstalled`) } if (!mockData['package-data'][id]) { throw new Error(`Package ${id} is not installed`) } // Stop container if it's running const containerInfo = runningContainers.get(id) if (containerInfo && containerInfo.containerId) { try { const runtime = containerInfo.runtime || 'docker' const stopCmd = runtime === 'podman' ? `podman stop ${containerInfo.containerId} 2>/dev/null || true` : `docker stop ${containerInfo.containerId} 2>/dev/null || true` const rmCmd = runtime === 'podman' ? `podman rm ${containerInfo.containerId} 2>/dev/null || true` : `docker rm ${containerInfo.containerId} 2>/dev/null || true` console.log(`[Package] đŸŗ Stopping container ${containerInfo.containerId}...`) await execPromise(stopCmd) await execPromise(rmCmd) console.log(`[Package] đŸŗ Container stopped`) } catch (error) { console.log(`[Package] âš ī¸ Error stopping container: ${error.message}`) } } await new Promise(resolve => setTimeout(resolve, 1000)) const port = mockData['package-data'][id].port if (port) { usedPorts.delete(port) } runningContainers.delete(id) delete mockData['package-data'][id] broadcastUpdate([ { op: 'remove', path: `/package-data/${id}` } ]) console.log(`[Package] ✅ ${id} uninstalled successfully`) return { success: true } } catch (error) { console.error(`[Package] ❌ Uninstall failed:`, error.message) throw error } } // Mock data const mockData = { 'server-info': { id: 'archipelago-demo', version: '0.1.0', name: 'Archipelago', pubkey: 'a1b2c3d4e5f6789012345678901234567890abcdef1234567890abcdef123456', 'status-info': { restarting: false, 'shutting-down': false, updated: false, 'backup-progress': null, 'update-progress': null, }, 'lan-address': 'localhost', 'tor-address': 'archydemox7k3pnw4hv5qz2jcbr6dwefys3ockqzf4mzjlvxot2ioad.onion', unread: 3, 'wifi-ssids': ['Home-5G', 'Archipelago-Mesh', 'Neighbors-Open'], 'zram-enabled': true, }, 'package-data': {}, // Will be populated from Docker + static apps ui: { name: 'Archipelago', 'ack-welcome': '0.1.0', marketplace: { 'selected-hosts': [], 'known-hosts': {}, }, theme: 'dark', }, } // Helper to build a static app entry function staticApp({ id, title, version, shortDesc, longDesc, license, state, lanPort, torHost, icon }) { return { title, version, status: state, state, 'static-files': { license: license || 'MIT', instructions: shortDesc, icon: icon || `/assets/img/app-icons/${id}.png`, }, manifest: { id, title, version, description: { short: shortDesc, long: longDesc || shortDesc }, 'release-notes': 'Latest stable release', license: license || 'MIT', 'wrapper-repo': '#', 'upstream-repo': '#', 'support-site': '#', 'marketing-site': '#', 'donation-url': null, interfaces: { main: { name: 'Web Interface', description: `${title} web interface`, ui: true }, }, }, installed: { 'current-dependents': {}, 'current-dependencies': {}, 'last-backup': null, 'interface-addresses': { main: { 'tor-address': torHost ? `${torHost}.onion` : `${id}.onion`, 'lan-address': lanPort ? `http://localhost:${lanPort}` : '', }, }, status: state, }, } } // Static dev apps (always shown in My Apps when using mock backend) const staticDevApps = { 'bitcoin-knots': staticApp({ id: 'bitcoin-knots', title: 'Bitcoin Knots', version: '27.1', shortDesc: 'Full Bitcoin node', longDesc: 'Validate and relay Bitcoin blocks and transactions with the Archipelago custom node UI.', state: 'running', lanPort: 8334, icon: '/assets/img/app-icons/bitcoin-knots.webp', }), bitcoin: staticApp({ id: 'bitcoin', title: 'Bitcoin Core', version: '27.1', shortDesc: 'Full Bitcoin node', longDesc: 'Validate every transaction and block. Full consensus enforcement — the bedrock of sovereignty.', state: 'running', lanPort: 8332, }), lnd: staticApp({ id: 'lnd', title: 'LND', version: '0.18.3', shortDesc: 'Lightning Network Daemon', longDesc: 'Instant Bitcoin payments with near-zero fees. Open channels, route payments, earn sats.', state: 'running', lanPort: 8080, }), electrs: staticApp({ id: 'electrs', title: 'Electrs', version: '0.10.6', shortDesc: 'Electrum Server in Rust', longDesc: 'Private blockchain indexing for wallet lookups. Connect Sparrow, BlueWallet, or any Electrum-compatible wallet.', state: 'running', lanPort: 50001, }), mempool: staticApp({ id: 'mempool', title: 'Mempool', version: '3.0.0', shortDesc: 'Blockchain explorer & fee estimator', longDesc: 'Real-time mempool visualization, transaction tracking, and fee estimation — all on your own node.', license: 'AGPL-3.0', state: 'running', lanPort: 4080, }), lorabell: staticApp({ id: 'lorabell', title: 'LoraBell', version: '1.0.0', shortDesc: 'LoRa doorbell', longDesc: 'Receive doorbell notifications over LoRa radio — no WiFi or internet required.', state: 'running', lanPort: null, }), meshtastic: staticApp({ id: 'meshtastic', title: 'Meshtastic', version: '2-daily-alpine', shortDesc: 'LoRa mesh networking', longDesc: 'Open-source mesh networking for LoRa radios. Create decentralized communication networks.', state: 'running', lanPort: 4403, icon: '/assets/img/app-icons/meshcore.svg', }), filebrowser: staticApp({ id: 'filebrowser', title: 'File Browser', version: '2.27.0', shortDesc: 'Web-based file manager', longDesc: 'Browse, upload, and manage files through an elegant web interface. Drag-and-drop uploads, media previews, and sharing.', state: 'running', lanPort: 8083, icon: '/assets/img/app-icons/file-browser.webp', }), thunderhub: staticApp({ id: 'thunderhub', title: 'ThunderHub', version: '0.13.31', shortDesc: 'Lightning node management UI', longDesc: 'Full Lightning node management — channels, payments, routing fees, and node health. Connect to your LND and manage everything from one dashboard.', state: 'running', lanPort: 3010, }), fedimint: staticApp({ id: 'fedimint', title: 'Fedimint', version: '0.10.0', shortDesc: 'Federated Bitcoin mint', longDesc: 'Federated Chaumian e-cash mint with Guardian UI. Community custody, private payments, and Lightning gateways.', state: 'running', lanPort: 8175, }), } function mergePackageData(dockerApps) { return { ...dockerApps, ...staticDevApps } } // Initialize package data from Docker on startup async function initializePackageData() { console.log('[Docker] Querying running containers...') const dockerApps = await getDockerContainers() mockData['package-data'] = mergePackageData(dockerApps) const appCount = Object.keys(mockData['package-data']).length const runningCount = Object.values(mockData['package-data']).filter(app => app.state === 'running').length console.log(`[Docker] Found ${appCount} containers (${runningCount} running)`) if (appCount > 0) { console.log('[Docker] Apps detected:') Object.entries(mockData['package-data']).forEach(([id, app]) => { const port = app.installed?.['interface-addresses']?.main?.['lan-address'] console.log(` - ${app.title} (${app.state})${port ? ` → ${port}` : ''}`) }) } else { console.log('[Docker] No containers found. Start docker-compose to see apps.') } } // Handle CORS preflight app.options('/rpc/v1', (req, res) => { res.status(200).end() }) app.get('/', (_req, res) => { const uiPort = process.env.VITE_DEV_SERVER_PORT || '8102' res .status(200) .type('html') .send(` Archipelago dev backend

This is the mock JSON-RPC backend. Open the dashboard at http://localhost:${uiPort}/.

`) }) app.get('/rpc/v1', (_req, res) => { const uiPort = process.env.VITE_DEV_SERVER_PORT || '8102' res .status(405) .type('text/plain') .send(`JSON-RPC is available at /rpc/v1 for POST requests only.\nOpen the dashboard at http://localhost:${uiPort}/.\n`) }) function mockBitcoinBlockchainInfo() { return { chain: 'main', blocks: 902418, headers: 902418, bestblockhash: randomHex(32), difficulty: 126_984_812_384_099.3, verificationprogress: 0.999998, initialblockdownload: false, size_on_disk: 678_000_000_000, pruned: false, chainwork: '00000000000000000000000000000000000000008d4b42d8b9b0000000000000', } } function mockBitcoinNetworkInfo() { return { version: 270100, subversion: '/Satoshi:27.1/Knots:20240513/', protocolversion: 70016, localservices: '0000000000000409', localrelay: true, timeoffset: 0, networkactive: true, connections: 18, networks: [ { name: 'ipv4', limited: false, reachable: true, proxy: '', proxy_randomize_credentials: false }, { name: 'onion', limited: false, reachable: true, proxy: '127.0.0.1:9050', proxy_randomize_credentials: true }, ], } } function bitcoinRelayStatusPayload() { return { settings: bitcoinRelayMockState.settings, trusted_nodes: bitcoinRelayMockState.trusted_nodes, requests: bitcoinRelayMockState.requests, local_node: { synced: true, blocks: 902418, headers: 902418, chain: 'main', status_ok: true, status_stale: false, error: null, }, credentials: { username: 'txrelay', available: true, password_available: true, rpcauth_available: true, client_env_available: true, client_env_path: '/var/lib/archipelago/secrets/bitcoin-rpc-txrelay-client.env', restart_hint: 'If this was just generated, restart Bitcoin Core/Knots so bitcoind loads the txrelay rpcauth whitelist.', }, } } app.get('/app/bitcoin-ui/', async (_req, res) => { try { const html = await fs.readFile(path.join(__dirname, '..', 'docker', 'bitcoin-ui', 'index.html'), 'utf8') res.type('html').send(html) } catch (error) { res.status(500).type('text/plain').send(`Unable to load Bitcoin UI mock: ${error.message}`) } }) app.get('/app/bitcoin-ui/bitcoin-status', (_req, res) => { res.json({ ok: true, stale: false, updated_at_ms: Date.now(), error: null, blockchain_info: mockBitcoinBlockchainInfo(), network_info: mockBitcoinNetworkInfo(), index_info: { txindex: { synced: true, best_block_height: 902418 }, blockfilterindex: { synced: true, best_block_height: 902418 }, }, zmq_notifications: [ { type: 'pubhashblock', address: 'tcp://127.0.0.1:28332', hwm: 1000 }, { type: 'pubrawtx', address: 'tcp://127.0.0.1:28333', hwm: 1000 }, ], }) }) app.post('/app/bitcoin-ui/bitcoin-rpc/', (req, res) => { const { id = 'bitcoin-ui', method } = req.body || {} const results = { getblockchaininfo: mockBitcoinBlockchainInfo(), getnetworkinfo: mockBitcoinNetworkInfo(), getindexinfo: { txindex: { synced: true, best_block_height: 902418 }, blockfilterindex: { synced: true, best_block_height: 902418 }, }, getzmqnotifications: [ { type: 'pubhashblock', address: 'tcp://127.0.0.1:28332', hwm: 1000 }, { type: 'pubrawtx', address: 'tcp://127.0.0.1:28333', hwm: 1000 }, ], getmempoolinfo: { loaded: true, size: 18452, bytes: 62400000, usage: 143000000, maxmempool: 300000000 }, getpeerinfo: Array.from({ length: 18 }, (_, index) => ({ id: index, addr: `198.51.100.${index + 10}:8333`, inbound: index % 3 === 0 })), } if (!Object.prototype.hasOwnProperty.call(results, method)) { return res.json({ id, result: null, error: { code: -32601, message: `Method not found: ${method}` } }) } res.json({ id, result: results[method], error: null }) }) app.post('/app/bitcoin-ui/rpc/v1', (req, res) => { const { method, params = {} } = req.body || {} const now = new Date().toISOString() switch (method) { case 'bitcoin.relay-status': return res.json({ result: bitcoinRelayStatusPayload(), error: null }) case 'bitcoin.relay-update-settings': bitcoinRelayMockState.settings = { ...bitcoinRelayMockState.settings, ...params, } return res.json({ result: bitcoinRelayStatusPayload(), error: null }) case 'bitcoin.relay-request-peer': { const peer = bitcoinRelayMockState.trusted_nodes.find(p => p.pubkey === params.peer_pubkey) if (!peer) return res.status(400).json({ error: { message: 'Peer is not in trusted nodes' } }) const request = { id: `relay-demo-${Date.now()}`, direction: 'outbound', status: 'pending', peer_pubkey: peer.pubkey, peer_onion: peer.onion, peer_name: peer.name, message: params.message || '', created_at: now, updated_at: now, } bitcoinRelayMockState.requests.push(request) return res.json({ result: { ok: true, request_id: request.id }, error: null }) } case 'bitcoin.relay-approve-request': case 'bitcoin.relay-reject-request': { const requestId = params.id || params.request_id const request = bitcoinRelayMockState.requests.find(r => r.id === requestId) if (!request) return res.status(404).json({ error: { message: `Request not found: ${requestId}` } }) request.status = method.endsWith('approve-request') ? 'approved' : 'rejected' request.updated_at = now if (request.status === 'approved') { request.approved_endpoint = bitcoinRelayMockState.settings.https_endpoint || bitcoinRelayMockState.settings.tor_endpoint || bitcoinRelayMockState.settings.http_endpoint request.credential_secret_path = '/var/lib/archipelago/secrets/bitcoin-relay-peer-demo.env' } return res.json({ result: { ok: true, request_id: request.id }, error: null }) } case 'bitcoin.relay-create-tor-service': bitcoinRelayMockState.settings.allow_tor = true bitcoinRelayMockState.settings.tor_endpoint = 'http://btc-relay-demoabcdefghijklmnop.onion/' return res.json({ result: { created: true, name: 'bitcoin-rpc', onion_address: 'btc-relay-demoabcdefghijklmnop.onion', }, error: null, }) default: return res.status(404).json({ error: { message: `Unknown mock Bitcoin UI RPC method: ${method}` } }) } }) // RPC endpoint app.post('/rpc/v1', (req, res) => { const { method, params } = req.body console.log(`[RPC] ${method}`) // Boot mode: return 502 during simulated startup delay if (DEV_MODE === 'boot') { // Reset boot timer when browser does a fresh page load (server.echo with 'boot' message) if (method === 'server.echo' && params?.message === 'boot-reset') { BOOT_START_TIME = Date.now() console.log(`[Boot] Timer RESET — simulating ${BOOT_DELAY_MS / 1000}s startup`) return res.status(502).json({ error: 'Server starting up (reset)' }) } const elapsed = Date.now() - BOOT_START_TIME if (elapsed < BOOT_DELAY_MS) { const secs = Math.round(elapsed / 1000) const total = Math.round(BOOT_DELAY_MS / 1000) console.log(`[Boot] Server starting... ${secs}s / ${total}s`) return res.status(502).json({ error: 'Server starting up' }) } if (elapsed < BOOT_DELAY_MS + 2000) { console.log(`[Boot] Server is now READY (took ${Math.round(elapsed / 1000)}s)`) } } try { switch (method) { // Authentication endpoints case 'auth.setup': { const { password } = params if (!password || password.length < 8) { return res.json({ error: { code: -32602, message: 'Password must be at least 8 characters', }, }) } // Set up user userState.setupComplete = true userState.passwordHash = password // In real app, bcrypt hash console.log(`[Auth] User setup completed`) return res.json({ result: { success: true } }) } case 'auth.isSetup': { return res.json({ result: userState.setupComplete }) } case 'auth.onboardingComplete': { userState.onboardingComplete = true console.log(`[Auth] Onboarding completed`) return res.json({ result: true }) } case 'auth.isOnboardingComplete': { return res.json({ result: userState.onboardingComplete }) } case 'auth.resetOnboarding': { userState.onboardingComplete = false console.log('[Auth] Onboarding reset') return res.json({ result: true }) } case 'node.did': { const mockDid = 'did:key:z6MkpTHR8VNsBxYAAWHut2Geadd9jSwuBV8xRoAnwWsdvktH' const mockPubkey = 'a1b2c3d4e5f6789012345678901234567890abcdef1234567890abcdef123456' return res.json({ result: { did: mockDid, pubkey: mockPubkey } }) } case 'node.nostr-publish': { return res.json({ result: { event_id: 'mock-event-id', success: 2, failed: 0 } }) } case 'node.nostr-pubkey': { return res.json({ result: { nostr_pubkey: 'mock-nostr-pubkey-hex' } }) } case 'node.signChallenge': { const { challenge } = params || {} const mockSig = Buffer.from(`mock-sig-${challenge || 'challenge'}`).toString('hex') return res.json({ result: { signature: mockSig } }) } case 'identity.verify': { return res.json({ result: { valid: true } }) } // BIP-39 seed management (mock for dev mode) case 'seed.generate': { const mockWords = [ 'abandon', 'ability', 'able', 'about', 'above', 'absent', 'absorb', 'abstract', 'absurd', 'abuse', 'access', 'accident', 'account', 'accuse', 'achieve', 'acid', 'acoustic', 'acquire', 'across', 'act', 'action', 'actor', 'actress', 'actual' ] return res.json({ result: { words: mockWords } }) } case 'seed.verify': { const mockDid = 'did:key:z6MkpTHR8VNsBxYAAWHut2Geadd9jSwuBV8xRoAnwWsdvktH' return res.json({ result: { verified: true, did: mockDid, nostr_npub: 'npub1mock...' } }) } case 'seed.restore': { const mockDid = 'did:key:z6MkpTHR8VNsBxYAAWHut2Geadd9jSwuBV8xRoAnwWsdvktH' return res.json({ result: { did: mockDid, nostr_npub: 'npub1mock...', restored: true } }) } case 'seed.save-encrypted': { return res.json({ result: { saved: true } }) } case 'seed.status': { return res.json({ result: { has_seed: true, is_legacy: false, identity_count: 1, next_index: 1 } }) } case 'node.createBackup': { const { passphrase } = params || {} if (!passphrase) { return res.json({ error: { code: -32602, message: 'Missing passphrase' } }) } const mockDid = 'did:key:z6MkpTHR8VNsBxYAAWHut2Geadd9jSwuBV8xRoAnwWsdvktH' const mockPubkey = 'a1b2c3d4e5f6789012345678901234567890abcdef1234567890abcdef123456' return res.json({ result: { version: 1, did: mockDid, pubkey: mockPubkey, kid: `${mockDid}#key-1`, encrypted: true, blob: Buffer.from(`mock-encrypted-backup-${passphrase}`).toString('base64'), timestamp: new Date().toISOString(), }, }) } case 'auth.login': { const { password } = params if (!userState.setupComplete) { return res.json({ error: { code: -32603, message: 'User not set up. Please complete setup first.', }, }) } // Simple password check (in real app, use bcrypt) if (password !== userState.passwordHash && password !== MOCK_PASSWORD) { return res.json({ error: { code: -32603, message: 'Password Incorrect', }, }) } const sessionId = `session-${Date.now()}` sessions.set(sessionId, { createdAt: new Date(), }) res.cookie('session', sessionId, { httpOnly: true, maxAge: 24 * 60 * 60 * 1000, }) return res.json({ result: null }) } case 'auth.logout': { const sessionId = req.cookies?.session if (sessionId) { sessions.delete(sessionId) } res.clearCookie('session') return res.json({ result: null }) } case 'server.set-name': { const name = (params?.name || '').trim() if (!name || name.length > 64) { return res.json({ error: { code: -1, message: 'Name must be 1-64 characters' } }) } mockData['server-info'].name = name broadcastUpdate() return res.json({ result: { name } }) } case 'server.echo': { return res.json({ result: { message: params?.message || 'Hello from Archipelago!' } }) } case 'server.time': { return res.json({ result: { now: new Date().toISOString(), uptime: process.uptime(), }, }) } case 'server.metrics': { // Slightly randomize so the dashboard feels alive return res.json({ result: { cpu: +(12 + Math.random() * 18).toFixed(1), memory: +(58 + Math.random() * 8).toFixed(1), disk: +(34 + Math.random() * 3).toFixed(1), }, }) } case 'marketplace.get': { const mockApps = [ { id: 'bitcoin', title: 'Bitcoin Core', description: 'Full Bitcoin node — validate transactions, enforce consensus rules, and support the network. The foundation of sovereignty.', version: '27.1', icon: '/assets/img/app-icons/bitcoin.png', author: 'Bitcoin Core', license: 'MIT', category: 'Bitcoin', }, { id: 'lnd', title: 'LND', description: 'Lightning Network Daemon — instant, low-fee Bitcoin payments. Open channels, route payments, earn routing fees.', version: '0.18.3', icon: '/assets/img/app-icons/lnd.png', author: 'Lightning Labs', license: 'MIT', category: 'Bitcoin', }, { id: 'electrs', title: 'Electrs', description: 'Electrum Server in Rust — index the blockchain for fast wallet lookups. Connect your hardware wallets privately.', version: '0.10.6', icon: '/assets/img/app-icons/electrs.png', author: 'Roman Zeyde', license: 'MIT', category: 'Bitcoin', }, { id: 'mempool', title: 'Mempool', description: 'Bitcoin blockchain explorer and mempool visualizer. Monitor transactions, fees, and network activity in real time.', version: '3.0.0', icon: '/assets/img/app-icons/mempool.png', author: 'Mempool Space', license: 'AGPL-3.0', category: 'Bitcoin', }, { id: 'btcpay', title: 'BTCPay Server', description: 'Self-hosted Bitcoin payment processor. Accept Bitcoin payments in your store — no fees, no middlemen, no KYC.', version: '2.0.4', icon: '/assets/img/app-icons/btcpay.png', author: 'BTCPay Server', license: 'MIT', category: 'Commerce', }, { id: 'fedimint', title: 'Fedimint', description: 'Federated Chaumian e-cash mint — community custody, private payments, and Lightning gateways for your tribe.', version: '0.10.0', icon: '/assets/img/app-icons/fedimint.png', author: 'Fedimint', license: 'MIT', category: 'Bitcoin', }, { id: 'thunderhub', title: 'ThunderHub', description: 'Lightning node management UI — manage channels, send and receive payments, view routing fees, and monitor your node health.', version: '0.13.31', icon: '/assets/img/app-icons/thunderhub.svg', author: 'Anthony Potdevin', license: 'MIT', category: 'Bitcoin', }, { id: 'vaultwarden', title: 'Vaultwarden', description: 'Self-hosted password manager compatible with Bitwarden clients. Keep your credentials under your own roof.', version: '1.32.5', icon: '/assets/img/app-icons/vaultwarden.png', author: 'Vaultwarden', license: 'AGPL-3.0', category: 'Privacy', }, { id: 'nextcloud', title: 'Nextcloud', description: 'Your personal cloud — files, calendar, contacts, and collaboration. Replace Google Drive and Dropbox entirely.', version: '29.0.0', icon: '/assets/img/app-icons/nextcloud.png', author: 'Nextcloud GmbH', license: 'AGPL-3.0', category: 'Cloud', }, { id: 'nostr-relay', title: 'Nostr Relay', description: 'Run your own Nostr relay — sovereign social networking. Publish notes, follow friends, no censorship possible.', version: '0.34.0', icon: '/assets/img/app-icons/nostr-relay.png', author: 'nostr-rs-relay', license: 'MIT', category: 'Social', }, { id: 'home-assistant', title: 'Home Assistant', description: 'Open-source home automation — control lights, locks, cameras, and sensors. Smart home without the cloud dependency.', version: '2024.12.0', icon: '/assets/img/app-icons/home-assistant.png', author: 'Home Assistant', license: 'Apache-2.0', category: 'IoT', }, { id: 'syncthing', title: 'Syncthing', description: 'Continuous peer-to-peer file synchronization. Sync folders across devices without any cloud service.', version: '1.28.1', icon: '/assets/img/app-icons/syncthing.png', author: 'Syncthing Foundation', license: 'MPL-2.0', category: 'Cloud', }, { id: 'tor', title: 'Tor', description: 'Anonymous communication — route traffic through the Tor network. Access your node from anywhere, privately.', version: '0.4.8.13', icon: '/assets/img/app-icons/tor.png', author: 'Tor Project', license: 'BSD-3', category: 'Privacy', }, ] return res.json({ result: mockApps }) } case 'server.update': case 'server.restart': case 'server.shutdown': { return res.json({ result: 'ok' }) } case 'package.install': { const { id, url, dockerImage, version } = params installPackage(id, url, { dockerImage, version }).catch(err => { console.error(`[RPC] Installation failed:`, err.message) }) return res.json({ result: `job-${Date.now()}` }) } case 'package.uninstall': { const { id } = params uninstallPackage(id).catch(err => { console.error(`[RPC] Uninstall failed:`, err.message) }) return res.json({ result: 'ok' }) } case 'package.start': case 'package.stop': case 'package.restart': { return res.json({ result: 'ok' }) } case 'auth.totp.status': { return res.json({ result: { enabled: false } }) } case 'auth.totp.setup.begin': { return res.json({ result: { qr_svg: 'Mock QR Code', secret_base32: 'JBSWY3DPEHPK3PXP', pending_token: 'mock-pending-token', }, }) } case 'auth.totp.setup.confirm': { return res.json({ result: { enabled: true, backup_codes: ['ABCD-EFGH', 'JKLM-NPQR', 'STUV-WXYZ', '2345-6789', 'ABCD-2345', 'EFGH-6789', 'JKLM-STUV', 'NPQR-WXYZ'], }, }) } case 'auth.totp.disable': { return res.json({ result: { disabled: true } }) } case 'auth.login.totp': case 'auth.login.backup': { return res.json({ result: { success: true } }) } // ========================================================================= // Identity & Onboarding // ========================================================================= case 'identity.create': { const { name, purpose } = params || {} console.log(`[Identity] Created identity: "${name || 'Personal'}" (${purpose || 'personal'})`) return res.json({ result: { success: true, id: `identity-${Date.now()}` } }) } case 'identity.register-name': { const { name } = params || {} console.log(`[Identity] Registered name: ${name}`) return res.json({ result: { success: true, id: `name-${Date.now()}` } }) } case 'identity.remove-name': { return res.json({ result: { success: true } }) } case 'identity.set-default': { return res.json({ result: { success: true } }) } case 'identity.delete': { return res.json({ result: { success: true } }) } case 'identity.issue-credential': { return res.json({ result: { success: true, credential_id: `cred-${Date.now()}` } }) } case 'identity.revoke-credential': { return res.json({ result: { success: true } }) } case 'identity.list': { return res.json({ result: { identities: [ { id: 'id-primary', name: 'Primary', purpose: 'Main node identity', pubkey: 'a1b2c3d4e5f6789012345678901234567890abcdef1234567890abcdef123456', did: 'did:key:z6MkpTHR8VNsBxYAAWHut2Geadd9jSwuBV8xRoAnwWsdvktH', created_at: '2026-01-10T08:00:00Z', is_default: true, nostr_pubkey: 'a1b2c3d4e5f6789012345678901234567890abcdef1234567890abcdef123456', nostr_npub: 'npub1mockprimaryidentitypubkeyvalue0000000000000000000000000abc', profile: { display_name: 'Archipelago Node', about: 'Self-sovereign Bitcoin node', nip05: 'satoshi@archipelago.local', lud16: 'satoshi@getalby.com', }, }, { id: 'id-anon', name: 'Anonymous', purpose: 'Privacy-focused browsing', pubkey: 'f6e5d4c3b2a19876543210fedcba9876543210fedcba9876543210fedcba98', did: 'did:key:z6MkvWkza1fMBWhKnYE3CgMgxHLKbN8NmbFRqHcECF4oGrwx', created_at: '2026-02-14T12:30:00Z', is_default: false, nostr_pubkey: 'f6e5d4c3b2a19876543210fedcba9876543210fedcba9876543210fedcba98', nostr_npub: 'npub1mockanonidentitypubkeyvalue000000000000000000000000000xyz', profile: { display_name: 'Anon' }, }, { id: 'id-merchant', name: 'Merchant', purpose: 'BTCPay & commerce', pubkey: '1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef', did: 'did:key:z6MknGc3ocHs3zdPiJbnaaqDi58NGb4pk1Sp9WNhSwaxN21V', created_at: '2026-03-01T16:00:00Z', is_default: false, nostr_pubkey: '1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef', nostr_npub: 'npub1mockmerchantidentitypubkeyvalue000000000000000000000000def', profile: { display_name: 'My Shop', website: 'https://myshop.onion' }, }, ], }, }) } case 'identity.get': { const id = params?.id || 'id-primary' return res.json({ result: { id, name: 'Primary', purpose: 'Main node identity', pubkey: 'a1b2c3d4e5f6789012345678901234567890abcdef1234567890abcdef123456', did: 'did:key:z6MkpTHR8VNsBxYAAWHut2Geadd9jSwuBV8xRoAnwWsdvktH', created_at: '2026-01-10T08:00:00Z', is_default: true, nostr_pubkey: 'a1b2c3d4e5f6789012345678901234567890abcdef1234567890abcdef123456', nostr_npub: 'npub1mockprimaryidentitypubkeyvalue0000000000000000000000000abc', profile: { display_name: 'Archipelago Node', about: 'Self-sovereign Bitcoin node', }, }, }) } case 'identity.export-keys': { return res.json({ result: { ed25519_secret_hex: 'deadbeef'.repeat(8), nostr_secret_hex: 'cafebabe'.repeat(8), nostr_nsec: 'nsec1mockexportedkeyvalue00000000000000000000000000000000000abc', }, }) } case 'identity.update-profile': { return res.json({ result: { success: true } }) } case 'identity.publish-profile': { return res.json({ result: { event_id: `evt-${Date.now().toString(36)}` } }) } case 'identity.list': { return res.json({ result: { identities: [ { id: 'id-primary', name: 'Primary', purpose: 'personal', pubkey: 'a1b2c3d4e5f6789012345678901234567890abcdef1234567890abcdef123456', did: 'did:key:z6MkpTHR8VNsBxYAAWHut2Geadd9jSwuBV8xRoAnwWsdvktH', created_at: '2026-01-10T08:00:00Z', is_default: true, nostr_pubkey: 'a1b2c3d4e5f6789012345678901234567890abcdef1234567890abcdef123456', nostr_npub: 'npub1598eg0y7m08htfzjfmzv6zjvf5u7p00dr9w0yfamaxqhkwlryckq5dh9ee', profile: { display_name: 'Satoshi', about: 'Running a sovereign Bitcoin node', nip05: 'satoshi@archipelago.local', lud16: 'satoshi@getalby.com', }, }, { id: 'id-business', name: 'Business', purpose: 'business', pubkey: 'f6e5d4c3b2a10987654321fedcba0987654321fedcba0987654321fedcba0987', did: 'did:key:z6MkhaXgBZDvotDkL5257faiztiGiC2ReMkBe4bR6XBIDNq9', created_at: '2026-02-05T12:30:00Z', is_default: false, nostr_pubkey: 'f6e5d4c3b2a10987654321fedcba0987654321fedcba0987654321fedcba0987', nostr_npub: 'npub17m9mx9kk2ry8p3xfkq0070fjlph9r9l0dj0y8fn0zrlj80ekjph7jxmxfg', profile: { display_name: 'Archy Consulting', about: 'Bitcoin infrastructure services', }, }, { id: 'id-anon', name: 'Anonymous', purpose: 'anonymous', pubkey: 'deadbeef00112233445566778899aabbccddeeff00112233445566778899aabb', did: 'did:key:z6MknGc3ocHs3zdPiJbnaaqDi5hjrZo4HzmQnwzaxWhAbWAs', created_at: '2026-03-01T18:00:00Z', is_default: false, profile: {}, }, ], }, }) } case 'identity.get': { const id = params?.id || 'id-primary' const identities = { 'id-primary': { id: 'id-primary', name: 'Primary', purpose: 'personal', pubkey: 'a1b2c3d4e5f6789012345678901234567890abcdef1234567890abcdef123456', did: 'did:key:z6MkpTHR8VNsBxYAAWHut2Geadd9jSwuBV8xRoAnwWsdvktH', nostr_pubkey: 'a1b2c3d4e5f6789012345678901234567890abcdef1234567890abcdef123456', nostr_npub: 'npub1598eg0y7m08htfzjfmzv6zjvf5u7p00dr9w0yfamaxqhkwlryckq5dh9ee', is_default: true, created_at: '2026-01-10T08:00:00Z', profile: { display_name: 'Satoshi', about: 'Running a sovereign Bitcoin node' }, }, } return res.json({ result: identities[id] || identities['id-primary'] }) } case 'identity.export-keys': { const { password } = params || {} if (password !== MOCK_PASSWORD) { return res.json({ error: { code: -32603, message: 'Incorrect password' } }) } return res.json({ result: { ed25519_secret_hex: 'mock_ed25519_secret_' + '0'.repeat(40), nostr_secret_hex: 'mock_nostr_secret_' + '0'.repeat(44), nostr_nsec: 'nsec1mockkeymockkeymockkeymockkeymockkeymockkeymockkeymockkeymo', }, }) } case 'identity.update-profile': { console.log(`[Identity] Updated profile for: ${params?.id}`) return res.json({ result: { success: true } }) } case 'identity.publish-profile': { console.log(`[Identity] Published profile for: ${params?.id}`) return res.json({ result: { event_id: `nostr-event-${Date.now().toString(36)}` } }) } // ========================================================================= // Nostr // ========================================================================= case 'nostr.add-relay': { const { url } = params || {} console.log(`[Nostr] Added relay: ${url}`) return res.json({ result: { success: true } }) } case 'nostr.remove-relay': { return res.json({ result: { success: true } }) } case 'nostr.toggle-relay': { return res.json({ result: { success: true } }) } // ========================================================================= // Content & Network // ========================================================================= case 'content.browse-peer': { const onion = params?.onion || '' return res.json({ result: { items: [ { id: 'peer-doc-1', filename: 'Bitcoin Whitepaper.pdf', mime_type: 'application/pdf', size_bytes: 184292, description: 'The original Bitcoin whitepaper by Satoshi Nakamoto', access: 'free' }, { id: 'peer-img-1', filename: 'node-setup-guide.png', mime_type: 'image/png', size_bytes: 524800, description: 'Visual guide for setting up a Bitcoin node', access: 'free' }, { id: 'peer-vid-1', filename: 'Lightning Demo.mp4', mime_type: 'video/mp4', size_bytes: 15728640, description: 'Lightning Network payment channel demo', access: { paid: { price_sats: 500 } } }, ], }, }) } case 'content.remove': { return res.json({ result: { success: true } }) } case 'content.set-pricing': { return res.json({ result: { success: true } }) } case 'network.diagnostics': { return res.json({ result: { wan_ip: '82.14.203.47', nat_type: 'Full Cone', upnp_available: true, tor_connected: true, wifi_count: 3, }, }) } case 'network.list-interfaces': { return res.json({ result: { interfaces: [ { name: 'eth0', type: 'ethernet', state: 'up', mac: 'a8:a1:59:3c:f2:10', ipv4: ['192.168.1.228/24'] }, { name: 'wlan0', type: 'wifi', state: 'up', mac: 'dc:a6:32:12:ab:cd', ipv4: ['192.168.1.230/24'] }, { name: 'lo', type: 'loopback', state: 'up', mac: '00:00:00:00:00:00', ipv4: ['127.0.0.1/8'] }, { name: 'podman0', type: 'bridge', state: 'up', mac: '2e:f4:8a:11:22:33', ipv4: ['10.89.0.1/16'] }, { name: 'tailscale0', type: 'tunnel', state: 'up', mac: '', ipv4: ['100.82.97.63/32'] }, ], }, }) } case 'network.scan-wifi': { return res.json({ result: { networks: [ { ssid: 'HomeNetwork', signal: 92, security: 'WPA2' }, { ssid: 'Neighbor-5G', signal: 45, security: 'WPA3' }, { ssid: 'CoffeeShop', signal: 30, security: 'Open' }, ], }, }) } case 'network.configure-wifi': { console.log(`[Network] Connecting to WiFi: ${params?.ssid}`) return res.json({ result: { success: true } }) } case 'network.dns-status': { return res.json({ result: { provider: 'system', servers: ['1.1.1.1', '9.9.9.9'], doh_enabled: false, doh_url: null, resolv_conf_servers: ['1.1.1.1', '9.9.9.9'], }, }) } case 'network.configure-dns': { console.log(`[Network] DNS configured: ${params?.provider}`) return res.json({ result: { success: true } }) } case 'network.accept-request': { return res.json({ result: { success: true } }) } case 'network.reject-request': { return res.json({ result: { success: true } }) } // ========================================================================= // Server & Auth extras // ========================================================================= case 'server.health': { return res.json({ result: { status: 'healthy', uptime: Math.floor(process.uptime()), services: { backend: 'running', nginx: 'running', podman: 'running', tor: 'running', }, }, }) } case 'auth.changePassword': { const { currentPassword } = params || {} if (currentPassword !== userState.passwordHash && currentPassword !== MOCK_PASSWORD) { return res.json({ error: { code: -32603, message: 'Current password is incorrect' } }) } userState.passwordHash = params.newPassword console.log('[Auth] Password changed') return res.json({ result: { success: true } }) } // ========================================================================= // Router (port forwarding) // ========================================================================= case 'router.add-forward': { console.log(`[Router] Added forward: ${JSON.stringify(params)}`) return res.json({ result: { success: true, id: `fwd-${Date.now()}` } }) } case 'router.remove-forward': { return res.json({ result: { success: true } }) } case 'router.list-forwards': { return res.json({ result: { forwards: [ { id: 'fwd-1', name: 'Bitcoin P2P', external_port: 8333, internal_port: 8333, protocol: 'tcp', enabled: true }, { id: 'fwd-2', name: 'LND gRPC', external_port: 10009, internal_port: 10009, protocol: 'tcp', enabled: true }, ], }, }) } // ========================================================================= // Tor & Peer Networking // ========================================================================= case 'tor.list-services': { return res.json({ result: { tor_running: true, services: [ { name: 'archipelago', local_port: 5678, onion_address: 'archydemox7k3pnw4hv5qz2jcbr6dwefys3ockqzf4mzjlvxot2ioad.onion', enabled: true, unauthenticated: false, protocol: false }, { name: 'bitcoin', local_port: 8333, onion_address: 'btcmockxj4k5pnw7hv8qz9jcbr2dwefys6ockqzf1mzjlvxot5ioad.onion', enabled: true, unauthenticated: false, protocol: false }, { name: 'lnd', local_port: 9735, onion_address: 'lndmockab3c4def5ghi6jkl7mno8pqr9stu0vwx1yz2ab3c4def5ghi.onion', enabled: true, unauthenticated: false, protocol: false }, { name: 'electrs', local_port: 50001, onion_address: 'elecmockyz9wvu8tsr7qpo6nml5kji4hgf3edc2ba1xyz9wvu8tsr7q.onion', enabled: true, unauthenticated: true, protocol: false }, { name: 'nostr-relay', local_port: 7777, onion_address: null, enabled: false, unauthenticated: true, protocol: false }, ], }, }) } case 'node.tor-address': { return res.json({ result: { tor_address: 'archydemox7k3pnw4hv5qz2jcbr6dwefys3ockqzf4mzjlvxot2ioad.onion', }, }) } case 'node-list-peers': { return res.json({ result: { peers: [ { onion: 'peer1abc2def3ghi4jkl5mno6pqr7stu8vwx9yz.onion', pubkey: 'a1b2c3d4e5f6', name: 'satoshi-node' }, { onion: 'peer2xyz9wvu8tsr7qpo6nml5kji4hgf3edc2ba.onion', pubkey: 'f6e5d4c3b2a1', name: 'lightning-lab' }, { onion: 'peer3mno6pqr7stu8vwx9yzabc2def3ghi4jkl5.onion', pubkey: 'c3d4e5f6a1b2', name: 'sovereign-relay' }, ], }, }) } case 'node-check-peer': { return res.json({ result: { onion: params?.onion || '', reachable: Math.random() > 0.2 } }) } case 'node-add-peer': { console.log(`[Peers] Added peer: ${params?.onion}`) return res.json({ result: { success: true } }) } case 'node-send-message': { console.log(`[Peers] Sent message to: ${params?.onion}`) return res.json({ result: { ok: true, sent_to: params?.onion || '' } }) } case 'node-nostr-discover': { return res.json({ result: { nodes: [ { did: 'did:key:z6MkhaXgBZDvotDkL5257faiztiGiC2ReMkBe4bR6XBIDNq9', onion: 'disc1abc2def3ghi4jkl5mno6pqr7stu8vwx9yz.onion', pubkey: 'disc1pub', node_address: '192.168.1.50' }, { did: 'did:key:z6MkpTHR8VNsBxYAAWHut2Geadd9jSwuBV8xRoAnwWsdvktH', onion: 'disc2xyz9wvu8tsr7qpo6nml5kji4hgf3edc2ba.onion', pubkey: 'disc2pub', node_address: '192.168.1.51' }, ], }, }) } case 'node-messages-received': case 'node.messages': { return res.json({ result: { messages: [ { from_pubkey: 'a1b2c3d4e5f6', message: 'Hey, your relay is online! Nice uptime.', timestamp: new Date(Date.now() - 3600000).toISOString() }, { from_pubkey: 'f6e5d4c3b2a1', message: 'Channel opened successfully. 500k sats capacity.', timestamp: new Date(Date.now() - 7200000).toISOString() }, { from_pubkey: 'c3d4e5f6a1b2', message: 'Backup sync complete. All good on my end.', timestamp: new Date(Date.now() - 86400000).toISOString() }, ], }, }) } case 'node.notifications': { return res.json({ result: [ { id: 'notif-1', type: 'info', title: 'Bitcoin Core synced', message: 'Blockchain fully synced at block 892,451. IBD complete.', timestamp: new Date(Date.now() - 1800000).toISOString(), read: false, }, { id: 'notif-2', type: 'success', title: 'LND channel opened', message: 'Channel opened with ACINQ (500,000 sats capacity). 6 confirmations received.', timestamp: new Date(Date.now() - 7200000).toISOString(), read: false, }, { id: 'notif-3', type: 'warning', title: 'Disk usage above 80%', message: 'Storage at 82% capacity. Consider pruning old blockchain data or expanding storage.', timestamp: new Date(Date.now() - 14400000).toISOString(), read: true, }, { id: 'notif-4', type: 'info', title: 'System update available', message: 'Archipelago v0.1.1 is available. Review changelog before updating.', timestamp: new Date(Date.now() - 86400000).toISOString(), read: true, }, { id: 'notif-5', type: 'success', title: 'Fedimint guardian connected', message: 'Guardian consensus achieved. Mint is operational with 3/4 guardians online.', timestamp: new Date(Date.now() - 43200000).toISOString(), read: false, }, ], }) } // ===================================================================== // Federation (multi-node clusters) // ===================================================================== case 'federation.list-nodes': { return res.json({ result: { nodes: [ { did: 'did:key:z6MkhaXgBZDvotDkL5257faiztiGiC2ReMkBe4bR6XBIDNq9', pubkey: 'a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2', onion: 'peer1abc2def3ghi4jkl5mno6pqr7stu8vwx9yz.onion', trust_level: 'trusted', added_at: '2026-02-15T10:30:00Z', name: 'archy-198', last_seen: new Date(Date.now() - 120000).toISOString(), last_state: { timestamp: new Date(Date.now() - 120000).toISOString(), apps: [ { id: 'bitcoin-knots', status: 'running', version: '27.1' }, { id: 'lnd', status: 'running', version: '0.18.0' }, { id: 'mempool', status: 'running', version: '3.0' }, { id: 'electrs', status: 'running', version: '0.10.0' }, ], cpu_usage_percent: 18.3, mem_used_bytes: 6_200_000_000, mem_total_bytes: 16_000_000_000, disk_used_bytes: 820_000_000_000, disk_total_bytes: 1_800_000_000_000, uptime_secs: 604800, tor_active: true, }, }, { did: 'did:key:z6MkpTHR8VNsBxYAAWHut2Geadd9jSwuBV8xRoAnwWsdvktH', pubkey: 'f6e5d4c3b2a1f6e5d4c3b2a1f6e5d4c3b2a1f6e5d4c3b2a1f6e5d4c3b2a1f6e5', onion: 'peer2xyz9wvu8tsr7qpo6nml5kji4hgf3edc2ba.onion', trust_level: 'trusted', added_at: '2026-03-01T14:00:00Z', name: 'arch-tailscale-1', last_seen: new Date(Date.now() - 300000).toISOString(), last_state: { timestamp: new Date(Date.now() - 300000).toISOString(), apps: [ { id: 'bitcoin-knots', status: 'running', version: '27.1' }, { id: 'lnd', status: 'running', version: '0.18.0' }, { id: 'nextcloud', status: 'running', version: '29.0' }, ], cpu_usage_percent: 42.1, mem_used_bytes: 10_500_000_000, mem_total_bytes: 32_000_000_000, disk_used_bytes: 1_200_000_000_000, disk_total_bytes: 2_000_000_000_000, uptime_secs: 259200, tor_active: true, }, }, { did: 'did:key:z6MkrHKPxJP6tvCvXMaJKZd3rRA2Y44tyftVhR8FDCMKGFjb', pubkey: 'c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4', onion: 'peer3mno6pqr7stu8vwx9yzabc2def3ghi4jkl5.onion', trust_level: 'observer', added_at: '2026-03-10T09:15:00Z', name: 'bunker-alpha', last_seen: new Date(Date.now() - 3600000).toISOString(), last_state: { timestamp: new Date(Date.now() - 3600000).toISOString(), apps: [ { id: 'bitcoin-knots', status: 'running', version: '27.1' }, { id: 'vaultwarden', status: 'running', version: '1.31.0' }, ], cpu_usage_percent: 5.8, mem_used_bytes: 2_100_000_000, mem_total_bytes: 8_000_000_000, disk_used_bytes: 450_000_000_000, disk_total_bytes: 1_000_000_000_000, uptime_secs: 1209600, tor_active: false, }, }, ], }, }) } case 'federation.invite': { const mockCode = 'fed1:' + Buffer.from(JSON.stringify({ did: 'did:key:z6MkTest228NodeInvite', onion: 'self228abc2def3ghi4jkl5mno6pqr7stu8vwx.onion', pubkey: 'aabbccdd', token: 'mock-invite-token-' + Date.now(), })).toString('base64url') return res.json({ result: { code: mockCode, did: 'did:key:z6MkTest228NodeInvite', onion: 'self228abc2def3ghi4jkl5mno6pqr7stu8vwx.onion', }, }) } case 'federation.join': { console.log(`[Federation] Joining with code: ${params?.code?.substring(0, 20)}...`) return res.json({ result: { joined: true, node: { did: 'did:key:z6MkNewJoinedNode', onion: 'newnode123abc456def789ghi012jkl345mno6pqr.onion', pubkey: 'ddeeff11', trust_level: 'trusted', }, }, }) } case 'federation.sync-state': { return res.json({ result: { synced: 3, failed: 0, results: [ { did: 'did:key:z6MkhaXgBZDvotDkL5257faiztiGiC2ReMkBe4bR6XBIDNq9', status: 'synced', apps: 4 }, { did: 'did:key:z6MkpTHR8VNsBxYAAWHut2Geadd9jSwuBV8xRoAnwWsdvktH', status: 'synced', apps: 3 }, { did: 'did:key:z6MkrHKPxJP6tvCvXMaJKZd3rRA2Y44tyftVhR8FDCMKGFjb', status: 'synced', apps: 2 }, ], }, }) } case 'federation.set-trust': { console.log(`[Federation] Set trust: ${params?.did} -> ${params?.trust_level}`) return res.json({ result: { updated: true, did: params?.did, trust_level: params?.trust_level } }) } case 'federation.remove-node': { console.log(`[Federation] Remove node: ${params?.did}`) return res.json({ result: { removed: true, nodes_remaining: 2 } }) } case 'federation.deploy-app': { console.log(`[Federation] Deploy app: ${params?.app_id} to ${params?.target_did}`) return res.json({ result: { deployed: true, app_id: params?.app_id } }) } // ===================================================================== // DWN (Decentralized Web Node) // ===================================================================== case 'dwn.status': { return res.json({ result: { running: true, protocols_registered: 3, messages_stored: 47, peers_synced: 2, last_sync: new Date(Date.now() - 600000).toISOString(), protocols: [ { uri: 'https://archipelago.dev/protocols/node-identity/v1', published: true, messages: 12 }, { uri: 'https://archipelago.dev/protocols/federation/v1', published: false, messages: 28 }, { uri: 'https://archipelago.dev/protocols/app-deploy/v1', published: false, messages: 7 }, ], }, }) } case 'dwn.sync': { console.log('[DWN] Syncing with peers...') return res.json({ result: { synced: true, messages_pulled: 5, messages_pushed: 3 } }) } // ===================================================================== // Mesh Networking (LoRa radio via Meshcore) // ===================================================================== case 'mesh.status': { globalThis.__meshHeaders ||= { announce_block_headers: false, receive_block_headers: true } return res.json({ result: { enabled: true, device_type: 'Meshcore', device_path: '/dev/ttyUSB0', device_connected: true, firmware_version: '2.3.1', self_node_id: 42, self_advert_name: 'archy-228', peer_count: 4, channel_name: 'archipelago', messages_sent: 23, messages_received: 47, detected_devices: ['/dev/ttyUSB0'], announce_block_headers: globalThis.__meshHeaders.announce_block_headers, receive_block_headers: globalThis.__meshHeaders.receive_block_headers, }, }) } case 'mesh.peers': { return res.json({ result: { peers: [ { contact_id: 1, advert_name: 'archy-198', did: 'did:key:z6MkhaXgBZDvotDkL5257faiztiGiC2ReMkBe4bR6XBIDNq9', pubkey_hex: 'a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2', rssi: -67, snr: 9.5, last_heard: new Date(Date.now() - 30000).toISOString(), hops: 0, }, { contact_id: 2, advert_name: 'satoshi-relay', did: 'did:key:z6MkpTHR8VNsBxYAAWHut2Geadd9jSwuBV8xRoAnwWsdvktH', pubkey_hex: 'f6e5d4c3b2a1f6e5d4c3b2a1f6e5d4c3b2a1f6e5d4c3b2a1f6e5d4c3b2a1f6e5', rssi: -82, snr: 4.2, last_heard: new Date(Date.now() - 120000).toISOString(), hops: 1, }, { contact_id: 3, advert_name: 'mountain-node', did: null, pubkey_hex: null, rssi: -95, snr: 1.8, last_heard: new Date(Date.now() - 600000).toISOString(), hops: 2, }, { contact_id: 4, advert_name: 'bunker-alpha', did: 'did:key:z6MkrHKPxJP6tvCvXMaJKZd3rRA2Y44tyftVhR8FDCMKGFjb', pubkey_hex: 'c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4', rssi: -74, snr: 7.1, last_heard: new Date(Date.now() - 45000).toISOString(), hops: 0, }, ], count: 4, }, }) } case 'mesh.messages': { const limit = params?.limit || 100 const now = Date.now() const allMessages = [ { id: 1, direction: 'received', peer_contact_id: 1, peer_name: 'archy-198', plaintext: 'Node online. Bitcoin Knots synced to tip.', timestamp: new Date(now - 3600000).toISOString(), delivered: true, encrypted: true, message_type: 'text' }, { id: 2, direction: 'sent', peer_contact_id: 1, peer_name: 'archy-198', plaintext: 'Good. Electrs index at 98%. Channel capacity 2.5M sats.', timestamp: new Date(now - 3540000).toISOString(), delivered: true, encrypted: true, message_type: 'text' }, { id: 3, direction: 'received', peer_contact_id: 2, peer_name: 'satoshi-relay', plaintext: 'Block #890,413 relayed. Fees avg 12 sat/vB.', timestamp: new Date(now - 3000000).toISOString(), delivered: true, encrypted: true, message_type: 'block_header', typed_payload: { alert_type: 'block_header', message: 'Block #890,413 — 2,847 txs, 12 sat/vB avg fee', signed: true } }, { id: 4, direction: 'received', peer_contact_id: 1, peer_name: 'archy-198', plaintext: 'Invoice: 50,000 sats — Channel opening fee', timestamp: new Date(now - 1800000).toISOString(), delivered: true, encrypted: true, message_type: 'invoice', typed_payload: { bolt11: 'lnbc500000n1pjmesh...truncated...', amount_sats: 50000, memo: 'Channel opening fee', paid: false } }, { id: 5, direction: 'sent', peer_contact_id: 4, peer_name: 'bunker-alpha', plaintext: 'Running mesh-only mode. No internet for 48h. All good.', timestamp: new Date(now - 900000).toISOString(), delivered: true, encrypted: true, message_type: 'text' }, { id: 6, direction: 'received', peer_contact_id: 4, peer_name: 'bunker-alpha', plaintext: 'Copy. Block height 890,412 via compact headers.', timestamp: new Date(now - 840000).toISOString(), delivered: true, encrypted: true, message_type: 'text' }, { id: 7, direction: 'received', peer_contact_id: 3, peer_name: 'mountain-node', plaintext: 'EMERGENCY: Solar array failure. Running on battery reserve.', timestamp: new Date(now - 600000).toISOString(), delivered: true, encrypted: false, message_type: 'alert', typed_payload: { alert_type: 'emergency', message: 'Solar array failure. Running on battery reserve. ETA 4h before shutdown.', coordinate: { lat: 39507400, lng: -106042800, label: 'Mountain relay site' }, signed: true } }, { id: 8, direction: 'sent', peer_contact_id: 1, peer_name: 'archy-198', plaintext: 'Opening 1M sat channel to your node. Approve?', timestamp: new Date(now - 300000).toISOString(), delivered: true, encrypted: true, message_type: 'text' }, { id: 9, direction: 'received', peer_contact_id: 1, peer_name: 'archy-198', plaintext: 'Approved. Waiting for funding tx confirmation.', timestamp: new Date(now - 240000).toISOString(), delivered: true, encrypted: true, message_type: 'text' }, { id: 10, direction: 'sent', peer_contact_id: 3, peer_name: 'mountain-node', plaintext: 'Location shared', timestamp: new Date(now - 120000).toISOString(), delivered: true, encrypted: false, message_type: 'coordinate', typed_payload: { lat: 30267200, lng: -97743100, label: 'Supply drop point' } }, { id: 11, direction: 'received', peer_contact_id: 4, peer_name: 'bunker-alpha', plaintext: 'Dead man switch check-in. All systems nominal. Battery 78%.', timestamp: new Date(now - 60000).toISOString(), delivered: true, encrypted: true, message_type: 'alert', typed_payload: { alert_type: 'status', message: 'All systems nominal. Battery 78%. Mesh uptime 14d.', signed: true } }, { id: 12, direction: 'received', peer_contact_id: 1, peer_name: 'archy-198', plaintext: 'Invoice paid: 50,000 sats', timestamp: new Date(now - 30000).toISOString(), delivered: true, encrypted: true, message_type: 'invoice', typed_payload: { bolt11: 'lnbc500000n1pjmesh...truncated...', amount_sats: 50000, memo: 'Channel opening fee', paid: true, payment_hash: 'a1b2c3d4e5f6...' } }, ] return res.json({ result: { messages: allMessages.slice(0, limit), count: allMessages.length, }, }) } case 'mesh.send': { const contactId = params?.contact_id const message = params?.message || '' const peer = [ { id: 1, name: 'archy-198', encrypted: true }, { id: 2, name: 'satoshi-relay', encrypted: true }, { id: 3, name: 'mountain-node', encrypted: false }, { id: 4, name: 'bunker-alpha', encrypted: true }, ].find(p => p.id === contactId) console.log(`[Mesh] Send to ${peer?.name || contactId}: ${message}`) return res.json({ result: { sent: true, message_id: Math.floor(Math.random() * 10000) + 100, encrypted: peer?.encrypted ?? false, }, }) } case 'mesh.broadcast': { console.log('[Mesh] Broadcasting identity over LoRa') return res.json({ result: { broadcast: true } }) } case 'mesh.configure': { console.log(`[Mesh] Configure:`, params) globalThis.__meshHeaders ||= { announce_block_headers: false, receive_block_headers: true } if (params && typeof params.announce_block_headers === 'boolean') globalThis.__meshHeaders.announce_block_headers = params.announce_block_headers if (params && typeof params.receive_block_headers === 'boolean') globalThis.__meshHeaders.receive_block_headers = params.receive_block_headers return res.json({ result: { configured: true, ...globalThis.__meshHeaders } }) } case 'mesh.send-invoice': { console.log(`[Mesh] Send invoice: ${params?.amount_sats} sats to contact ${params?.contact_id}`) return res.json({ result: { sent: true, message_id: Math.floor(Math.random() * 10000) + 200, amount_sats: params?.amount_sats, bolt11: `lnbc${params?.amount_sats}n1pjmesh...`, }, }) } case 'mesh.send-coordinate': { console.log(`[Mesh] Send coordinate: ${params?.lat}, ${params?.lng} to contact ${params?.contact_id}`) return res.json({ result: { sent: true, message_id: Math.floor(Math.random() * 10000) + 300, lat: Math.round((params?.lat || 0) * 1000000), lng: Math.round((params?.lng || 0) * 1000000), }, }) } case 'mesh.send-alert': { console.log(`[Mesh] Send alert: ${params?.alert_type} — ${params?.message}`) return res.json({ result: { sent: true, alert_type: params?.alert_type || 'status', signed: true, }, }) } case 'mesh.outbox': { return res.json({ result: { messages: [ { id: 1, dest_did: 'did:key:z6MkrHKPxJP6tvCvXMaJKZd3rRA2Y44tyftVhR8FDCMKGFjb', from_did: 'did:key:z6MkSelf', created_at: new Date(Date.now() - 1800000).toISOString(), ttl_secs: 86400, retry_count: 3, relay_hops: 0, expired: false, }, { id: 2, dest_did: 'did:key:z6MknGc3ocHs3zdPiJbnaaqDi58NGb4pk1Sp7NQD5EjEREWh', from_did: 'did:key:z6MkSelf', created_at: new Date(Date.now() - 7200000).toISOString(), ttl_secs: 86400, retry_count: 8, relay_hops: 1, expired: false, }, ], count: 2, }, }) } case 'mesh.session-status': { const hasSess = (params?.contact_id === 1 || params?.contact_id === 4) return res.json({ result: { has_session: hasSess, forward_secrecy: hasSess, message_count: hasSess ? 23 : 0, ratchet_generation: hasSess ? 7 : 0, peer_did: hasSess ? 'did:key:z6MkhaXgBZDvotDkL5257faiztiGiC2ReMkBe4bR6XBIDNq9' : null, }, }) } case 'mesh.rotate-prekeys': { console.log('[Mesh] Rotating prekeys...') return res.json({ result: { rotated: true, signed_prekey_id: Math.floor(Math.random() * 1000000), one_time_prekeys: 10, }, }) } case 'mesh.deadman-status': { return res.json({ result: { dead_man_enabled: false, dead_man_interval_secs: 21600, time_remaining_secs: 21600, triggered: false, has_gps: false, emergency_contacts: 2, }, }) } case 'mesh.deadman-configure': { const { enabled, interval_secs } = params || {} console.log(`[Mesh] Deadman configured: enabled=${enabled}, interval=${interval_secs}`) return res.json({ result: { dead_man_enabled: enabled ?? false, dead_man_interval_secs: interval_secs ?? 21600, time_remaining_secs: interval_secs ?? 21600, triggered: false, has_gps: false, emergency_contacts: 2, }, }) } case 'mesh.deadman-checkin': { return res.json({ result: { checked_in: true, time_remaining_secs: 21600, }, }) } case 'mesh.block-headers': { return res.json({ result: { headers: [ { height: 893421, hash: '0000000000000000000234a6b12dc03e5c4f7e891d2f34b5a678cd9012345678', timestamp: new Date(Date.now() - 600000).toISOString() }, { height: 893420, hash: '00000000000000000001bc7d89ef01234567890abcdef1234567890abcdef12', timestamp: new Date(Date.now() - 1200000).toISOString() }, { height: 893419, hash: '00000000000000000003ef4a56789012bcdef34567890abcdef1234567890ab', timestamp: new Date(Date.now() - 1800000).toISOString() }, ], latest_height: 893421, count: 3, }, }) } case 'mesh.relay-tx': { return res.json({ result: { request_id: Math.floor(Math.random() * 10000), queued: true, tx_hex_len: (params?.tx_hex || '').length, }, }) } case 'mesh.relay-status': { return res.json({ result: { relayed: true, pending_count: 0, status: 'confirmed', txid: 'a1b2c3d4e5f6789012345678901234567890abcdef1234567890abcdef123456', }, }) } case 'mesh.relay-lightning': { return res.json({ result: { request_id: Math.floor(Math.random() * 10000), queued: true, amount_sats: params?.amount_sats || 0, }, }) } // ===================================================================== // Transport Layer (unified routing: mesh > lan > tor) // ===================================================================== case 'transport.status': { return res.json({ result: { transports: [ { kind: 'mesh', available: true }, { kind: 'lan', available: true }, { kind: 'tor', available: true }, ], mesh_only: false, peer_count: 5, }, }) } case 'transport.peers': { return res.json({ result: { peers: [ { did: 'did:key:z6MkhaXgBZDvotDkL5257faiztiGiC2ReMkBe4bR6XBIDNq9', pubkey_hex: 'a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2', name: 'archy-198', trust_level: 'trusted', mesh_contact_id: 1, lan_address: '192.168.1.198:5678', onion_address: 'peer1abc2def3ghi4jkl5mno6pqr7stu8vwx9yz.onion', preferred_transport: 'lan', available_transports: ['mesh', 'lan', 'tor'], last_seen: new Date(Date.now() - 30000).toISOString(), }, { did: 'did:key:z6MkpTHR8VNsBxYAAWHut2Geadd9jSwuBV8xRoAnwWsdvktH', pubkey_hex: 'f6e5d4c3b2a1f6e5d4c3b2a1f6e5d4c3b2a1f6e5d4c3b2a1f6e5d4c3b2a1f6e5', name: 'satoshi-relay', trust_level: 'trusted', mesh_contact_id: 2, lan_address: null, onion_address: 'peer2xyz9wvu8tsr7qpo6nml5kji4hgf3edc2ba.onion', preferred_transport: 'mesh', available_transports: ['mesh', 'tor'], last_seen: new Date(Date.now() - 120000).toISOString(), }, { did: 'did:key:z6MkrHKPxJP6tvCvXMaJKZd3rRA2Y44tyftVhR8FDCMKGFjb', pubkey_hex: 'c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4', name: 'bunker-alpha', trust_level: 'observer', mesh_contact_id: 4, lan_address: null, onion_address: null, preferred_transport: 'mesh', available_transports: ['mesh'], last_seen: new Date(Date.now() - 45000).toISOString(), }, { did: 'did:key:z6MkjchhfUsD6mmvni8mCdXHw216Xrm9bQe2mBH1P5RDjVJG', pubkey_hex: 'd4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5', name: 'office-node', trust_level: 'trusted', mesh_contact_id: null, lan_address: '192.168.1.42:5678', onion_address: 'peer4mno6pqr7stu8vwx9yzabc2def3ghi4jkl5.onion', preferred_transport: 'lan', available_transports: ['lan', 'tor'], last_seen: new Date(Date.now() - 60000).toISOString(), }, { did: 'did:key:z6MknGc3ocHs3zdPiJbnaaqDi58NGb4pk1Sp7NQD5EjEREWh', pubkey_hex: 'e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6', name: 'remote-cabin', trust_level: 'trusted', mesh_contact_id: null, lan_address: null, onion_address: 'peer5xyz9abc2def3ghi4jkl5mno6pqr7stu8vw.onion', preferred_transport: 'tor', available_transports: ['tor'], last_seen: new Date(Date.now() - 300000).toISOString(), }, ], }, }) } case 'transport.send': { const targetDid = params?.did console.log(`[Transport] Send to ${targetDid} via best transport`) return res.json({ result: { sent: true, transport_used: 'mesh', did: targetDid, }, }) } case 'transport.set-mode': { const meshOnly = params?.mesh_only ?? false console.log(`[Transport] Set mesh_only mode: ${meshOnly}`) return res.json({ result: { mesh_only: meshOnly, configured: true } }) } // ===================================================================== // LND / Lightning // ===================================================================== case 'lnd.getinfo': { return res.json({ result: { alias: 'archy-signet', color: '#f7931a', num_active_channels: 4, num_inactive_channels: 1, num_pending_channels: 1, block_height: walletState.block_height, synced_to_chain: true, synced_to_graph: true, version: '0.17.4-beta', identity_pubkey: '03a1b2c3d4e5f6789012345678901234567890abcdef1234567890abcdef123456', chains: [{ chain: 'bitcoin', network: 'signet' }], balance_sats: walletState.onchain_sats, channel_balance_sats: walletState.channel_sats, }, }) } case 'lnd.gettransactions': { const pending = walletState.transactions.filter(tx => tx.direction === 'incoming' && tx.num_confirmations < 3).length return res.json({ result: { transactions: walletState.transactions, incoming_pending_count: pending, }, }) } case 'lnd.channelbalance': { return res.json({ result: { local_balance: { sat: walletState.channel_sats }, remote_balance: { sat: 11750000 }, pending_open_local_balance: { sat: 500000 }, }, }) } case 'lnd.walletbalance': { return res.json({ result: { total_balance: walletState.onchain_sats + 100000, confirmed_balance: walletState.onchain_sats, unconfirmed_balance: 100000, }, }) } case 'lnd.listchannels': { return res.json({ result: { channels: [ { chan_id: '840921088114688', remote_pubkey: '02778f4a', capacity: 5000000, local_balance: 2450000, remote_balance: 2550000, active: true, peer_alias: 'ACINQ Signet' }, { chan_id: '840921088114689', remote_pubkey: '03abcdef', capacity: 2000000, local_balance: 1200000, remote_balance: 800000, active: true, peer_alias: 'WalletOfSatoshi' }, { chan_id: '840921088114690', remote_pubkey: '02fedcba', capacity: 10000000, local_balance: 4500000, remote_balance: 5500000, active: true, peer_alias: 'Voltage' }, { chan_id: '840921088114691', remote_pubkey: '03456789', capacity: 3000000, local_balance: 100000, remote_balance: 2900000, active: true, peer_alias: 'Kraken' }, ], }, }) } case 'lnd.newaddress': { const addrType = params?.type || 'p2wkh' const mockAddr = addrType === 'p2tr' ? 'tb1p' + randomHex(29) : 'tb1q' + randomHex(19) return res.json({ result: { address: mockAddr } }) } case 'lnd.addinvoice': case 'lnd.createinvoice': { const amt = params?.amt || params?.value || params?.amount_sats || 1000 const rHash = randomHex(32) return res.json({ result: { r_hash: rHash, payment_request: `lnsb${amt}n1pjmock${Date.now().toString(36)}qqqxqyz5vqsp5mock${rHash.slice(0,20)}`, add_index: Date.now(), payment_addr: randomHex(32), }, }) } case 'lnd.payinvoice': case 'lnd.sendpayment': { const amt = params?.amt || params?.amount_sats || 1000 const fee = Math.floor(Math.random() * 10) + 1 walletState.channel_sats = Math.max(0, walletState.channel_sats - amt - fee) return res.json({ result: { payment_hash: randomHex(32), payment_preimage: randomHex(32), status: 'SUCCEEDED', fee_sat: fee, value_sat: amt, }, }) } case 'lnd.sendcoins': { const amt = params?.amount || params?.amt || 50000 walletState.onchain_sats = Math.max(0, walletState.onchain_sats - amt) const txid = randomHex(32) walletState.transactions.unshift({ tx_hash: txid, amount_sats: -amt, direction: 'outgoing', num_confirmations: 0, block_height: 0, time_stamp: Math.floor(Date.now()/1000), label: 'Sent on-chain', total_fees: 250, dest_addresses: [params?.addr || ''], }) return res.json({ result: { txid, amount: amt }, }) } case 'lnd.decodepayreq': { return res.json({ result: { destination: '03a1b2c3d4e5f6789012345678901234567890abcdef1234567890abcdef123456', num_satoshis: params?.pay_req?.match(/lnsb(\d+)/)?.[1] || '1000', description: 'Mock decoded invoice', expiry: 3600, timestamp: Math.floor(Date.now() / 1000), }, }) } case 'lnd.openchannel': { return res.json({ result: { funding_txid: Array.from({length: 32}, () => Math.floor(Math.random()*256).toString(16).padStart(2,'0')).join(''), output_index: 0, }, }) } case 'lnd.closechannel': { return res.json({ result: { closing_txid: Array.from({length: 32}, () => Math.floor(Math.random()*256).toString(16).padStart(2,'0')).join(''), }, }) } case 'lnd.listinvoices': { return res.json({ result: { invoices: MOCK_LND_DATA.invoices } }) } case 'lnd.listpayments': { return res.json({ result: { payments: MOCK_LND_DATA.payments } }) } case 'lnd.create-psbt': case 'lnd.finalize-psbt': case 'lnd.create-raw-tx': { return res.json({ result: { psbt: 'cHNidP8BAH0CAAAA...mockPSBT', txid: Array.from({length: 32}, () => Math.floor(Math.random()*256).toString(16).padStart(2,'0')).join(''), }, }) } // ===================================================================== // Wallet / Ecash (Fedimint) // ===================================================================== case 'wallet.ecash-balance': { return res.json({ result: { balance_sats: walletState.ecash_sats, balance_msat: walletState.ecash_sats * 1000, token_count: walletState.ecash_tokens, federations: [ { federation_id: 'fed1-demo', name: 'Archy Signet Mint', balance_msat: walletState.ecash_sats * 1000, gateway_active: true }, ], }, }) } case 'wallet.ecash-send': { const amt = params?.amount_sats || 1000 walletState.ecash_sats = Math.max(0, walletState.ecash_sats - amt) walletState.ecash_tokens = Math.max(0, walletState.ecash_tokens - 1) return res.json({ result: { token: `cashuSend_mock_${amt}_${Date.now().toString(36)}`, amount_sats: amt, }, }) } case 'wallet.ecash-receive': { const amt = 5000 walletState.ecash_sats += amt walletState.ecash_tokens += 1 return res.json({ result: { amount_sats: amt, federation_id: 'fed1-demo', }, }) } case 'wallet.ecash-history': { return res.json({ result: { transactions: [ { type: 'receive', amount_sats: 50000, timestamp: new Date(Date.now() - 86400000).toISOString(), note: 'Minted from Lightning' }, { type: 'send', amount_sats: 5000, timestamp: new Date(Date.now() - 43200000).toISOString(), note: 'Sent ecash token' }, { type: 'receive', amount_sats: 10000, timestamp: new Date(Date.now() - 3600000).toISOString(), note: 'Redeemed token' }, ], }, }) } case 'wallet.networking-profits': { return res.json({ result: { total_earned_sats: 42, total_forwarded_sats: 35025, forward_count: 3, period_days: 30, daily_avg_sats: 1.4, }, }) } case 'dev.faucet': { const amount = params?.amount_sats || 1_000_000 const ecashAmt = Math.floor(amount / 10) walletState.onchain_sats += amount walletState.channel_sats += amount walletState.ecash_sats += ecashAmt walletState.ecash_tokens += 1 const txid = randomHex(32) walletState.transactions.unshift({ tx_hash: txid, amount_sats: amount, direction: 'incoming', num_confirmations: 0, block_height: 0, time_stamp: Math.floor(Date.now()/1000), label: 'Dev faucet', total_fees: 0, dest_addresses: [], }) console.log(`[Dev Faucet] +${amount} on-chain, +${amount} Lightning, +${ecashAmt} ecash`) return res.json({ result: { onchain: { txid, amount_sats: amount }, lightning: { payment_hash: randomHex(32), amount_sats: amount }, ecash: { token: `cashuSend_faucet_${amount}_${Date.now().toString(36)}`, amount_sats: ecashAmt }, message: `Added ${amount} sats on-chain, ${amount} sats Lightning, ${ecashAmt} sats ecash`, }, }) } case 'bitcoin.getinfo': { return res.json({ result: { chain: 'signet', blocks: 892451, headers: 892451, bestblockhash: 'a1b2c3d4e5f6' + '0'.repeat(58), difficulty: 0.001126515290698186, mediantime: Math.floor(Date.now() / 1000) - 300, verificationprogress: 1.0, chainwork: '000000000000000000000000000000000000000000000000000000000001a2b3', size_on_disk: 210_000_000, pruned: false, network: 'signet', }, }) } // ===================================================================== // Analytics / Telemetry // ===================================================================== case 'analytics.get-status': { return res.json({ result: { enabled: mockState.analyticsEnabled || false, description: 'Anonymous aggregate statistics. No personal data collected.' } }) } case 'analytics.enable': { mockState.analyticsEnabled = true return res.json({ result: { enabled: true } }) } case 'analytics.disable': { mockState.analyticsEnabled = false return res.json({ result: { enabled: false } }) } case 'telemetry.fleet-status': { return res.json({ result: { nodes: [ { node_id: 'archy-228', version: '0.1.0', uptime_secs: 604800, cpu_cores: 4, cpu_pct: +(15 + Math.random() * 20).toFixed(1), mem_pct: +(38 + Math.random() * 10).toFixed(1), disk_pct: 34.2, container_count: 12, running_count: 10, federation_peers: 4, recent_alerts: [], containers: [ { id: 'bitcoin', state: 'running', version: '27.0' }, { id: 'lnd', state: 'running', version: '0.18.0' }, { id: 'electrs', state: 'running', version: '0.10.6' }, { id: 'mempool', state: 'running', version: '3.0.0' }, ], reported_at: new Date().toISOString(), }, { node_id: 'archy-198', version: '0.1.0', uptime_secs: 259200, cpu_cores: 4, cpu_pct: +(8 + Math.random() * 12).toFixed(1), mem_pct: +(25 + Math.random() * 8).toFixed(1), disk_pct: 22.7, container_count: 8, running_count: 7, federation_peers: 4, recent_alerts: [{ rule: 'container_crash', message: 'electrs restarted 2x in 1h', timestamp: new Date(Date.now() - 7200000).toISOString() }], containers: [ { id: 'bitcoin', state: 'running', version: '27.0' }, { id: 'lnd', state: 'running', version: '0.18.0' }, { id: 'electrs', state: 'running', version: '0.10.6' }, ], reported_at: new Date(Date.now() - 60000).toISOString(), }, { node_id: 'arch-1', version: '0.1.0', uptime_secs: 172800, cpu_cores: 2, cpu_pct: +(5 + Math.random() * 10).toFixed(1), mem_pct: +(45 + Math.random() * 15).toFixed(1), disk_pct: 61.3, container_count: 6, running_count: 6, federation_peers: 4, recent_alerts: [], containers: [ { id: 'bitcoin', state: 'running', version: '27.0' }, { id: 'lnd', state: 'running', version: '0.18.0' }, ], reported_at: new Date(Date.now() - 120000).toISOString(), }, { node_id: 'arch-2', version: '0.1.0', uptime_secs: 86400, cpu_cores: 2, cpu_pct: +(3 + Math.random() * 8).toFixed(1), mem_pct: +(30 + Math.random() * 10).toFixed(1), disk_pct: 18.9, container_count: 5, running_count: 5, federation_peers: 4, recent_alerts: [], containers: [ { id: 'bitcoin', state: 'running', version: '27.0' }, ], reported_at: new Date(Date.now() - 300000).toISOString(), }, { node_id: 'arch-3', version: '0.1.0', uptime_secs: 43200, cpu_cores: 4, cpu_pct: +(20 + Math.random() * 15).toFixed(1), mem_pct: +(55 + Math.random() * 10).toFixed(1), disk_pct: 47.5, container_count: 10, running_count: 9, federation_peers: 4, recent_alerts: [{ rule: 'disk_warning', message: 'Disk usage approaching 50%', timestamp: new Date(Date.now() - 3600000).toISOString() }], containers: [ { id: 'bitcoin', state: 'running', version: '27.0' }, { id: 'lnd', state: 'running', version: '0.18.0' }, { id: 'electrs', state: 'running', version: '0.10.6' }, { id: 'mempool', state: 'running', version: '3.0.0' }, ], reported_at: new Date(Date.now() - 30000).toISOString(), }, ], }, }) } case 'telemetry.fleet-alerts': { return res.json({ result: { alerts: [ { node_id: 'archy-198', rule: 'container_crash', message: 'electrs restarted 2x in 1h', timestamp: new Date(Date.now() - 7200000).toISOString() }, { node_id: 'arch-3', rule: 'disk_warning', message: 'Disk usage approaching 50%', timestamp: new Date(Date.now() - 3600000).toISOString() }, { node_id: 'arch-1', rule: 'mem_high', message: 'Memory usage above 60%', timestamp: new Date(Date.now() - 86400000).toISOString() }, ], }, }) } case 'telemetry.fleet-node-history': { const nodeId = params?.node_id || 'archy-228' const now = Date.now() const history = Array.from({ length: 24 }, (_, i) => ({ timestamp: new Date(now - (23 - i) * 3600000).toISOString(), cpu_pct: +(10 + Math.random() * 30).toFixed(1), mem_pct: +(30 + Math.random() * 20).toFixed(1), disk_pct: +(30 + Math.random() * 5).toFixed(1), })) return res.json({ result: { history } }) } case 'analytics.get-snapshot': case 'telemetry.report': { return res.json({ result: { node_id: 'mock-dev-node', version: '1.2.0-alpha', uptime_secs: 86400, cpu_cores: 4, ram_mb: 16384, container_count: 12, running_count: 10, federation_peers: 2, recent_alerts: [], reported_at: new Date().toISOString(), }}) } // System / Network / Updates // ===================================================================== case 'system.stats': { return res.json({ result: { cpu_usage_percent: +(12 + Math.random() * 18).toFixed(1), mem_used_bytes: 6_200_000_000 + Math.floor(Math.random() * 500_000_000), mem_total_bytes: 16_000_000_000, disk_used_bytes: 620_000_000_000 + Math.floor(Math.random() * 10_000_000_000), disk_total_bytes: 1_800_000_000_000, uptime_secs: Math.floor(process.uptime()) + 604800, load_avg: [+(0.5 + Math.random() * 1.5).toFixed(2), +(0.8 + Math.random()).toFixed(2), +(0.6 + Math.random()).toFixed(2)], net_rx_bytes: 12_400_000_000, net_tx_bytes: 8_900_000_000, }, }) } case 'update.status': { return res.json({ result: { current_version: '0.1.0', latest_version: '0.1.1', update_available: true, release_notes: 'Bug fixes and performance improvements.', channel: 'stable', }, }) } case 'seed.reveal': { if (!params || !params.password) { return res.json({ error: { code: -32000, message: 'Password is required to reveal the recovery phrase' } }) } // Demo gate: accept any non-empty password; reject "wrong" to exercise the error path. if (params.password === 'wrong') { return res.json({ error: { code: -32000, message: 'Incorrect password' } }) } const demo = 'legal winner thank year wave sausage worth useful legal winner thank yellow able cabin dad debris during dose talent layer crater proud drift movie'.split(' ') return res.json({ result: { words: demo, word_count: demo.length } }) } case 'update.list-mirrors': { globalThis.__mockMirrors ||= [ { url: 'http://146.59.87.168:3000/lfg2025/archy/raw/branch/main/releases/manifest.json', label: 'Origin (vps2)' }, ] return res.json({ result: { mirrors: globalThis.__mockMirrors } }) } case 'update.get-source': { globalThis.__swarmPrefs ||= { source: 'origin', provide_dht: true } return res.json({ result: { source: globalThis.__swarmPrefs.source, provide_dht: globalThis.__swarmPrefs.provide_dht, swarm_available: false, // default build has no iroh-swarm feature swarm_enabled: false, }, }) } case 'update.set-source': { globalThis.__swarmPrefs ||= { source: 'origin', provide_dht: true } if (params && (params.source === 'origin' || params.source === 'swarm')) { globalThis.__swarmPrefs.source = params.source } if (params && typeof params.provide === 'boolean') { globalThis.__swarmPrefs.provide_dht = params.provide } return res.json({ result: { source: globalThis.__swarmPrefs.source, provide_dht: globalThis.__swarmPrefs.provide_dht, swarm_available: false, swarm_enabled: false, }, }) } case 'network.list-requests': { return res.json({ result: { requests: [ { id: 'req-1', from_did: 'did:key:z6MkhaXgBZDvotDkL5257faiztiGiC2ReMkBe4bR6XBIDNq9', from_name: 'archy-198', type: 'federation-join', status: 'pending', created_at: new Date(Date.now() - 3600000).toISOString() }, ], }, }) } // ── Monitoring ────────────────────────────────────────────── case 'monitoring.current': { return res.json({ result: { system: { cpu_percent: +(12 + Math.random() * 18).toFixed(1), load_avg_1: +(0.5 + Math.random() * 1.5).toFixed(2), load_avg_5: +(0.8 + Math.random()).toFixed(2), load_avg_15: +(0.6 + Math.random()).toFixed(2), mem_used_bytes: 6_200_000_000 + Math.floor(Math.random() * 500_000_000), mem_total_bytes: 16_000_000_000, disk_used_bytes: 620_000_000_000, disk_total_bytes: 1_800_000_000_000, net_rx_bytes: 12_400_000_000 + Math.floor(Math.random() * 100_000_000), net_tx_bytes: 8_900_000_000 + Math.floor(Math.random() * 50_000_000), uptime_secs: Math.floor(process.uptime()) + 604800, }, rpc: { avg_latency_ms: +(2 + Math.random() * 8).toFixed(1), requests_per_minute: Math.floor(30 + Math.random() * 40) }, }, }) } case 'monitoring.history': { const points = 60 const history = [] for (let i = 0; i < points; i++) { history.push({ timestamp: new Date(Date.now() - (points - i) * 60000).toISOString(), system: { cpu_percent: +(10 + Math.random() * 25).toFixed(1), mem_used_bytes: 5_800_000_000 + Math.floor(Math.random() * 1_000_000_000), mem_total_bytes: 16_000_000_000, net_rx_bytes: Math.floor(Math.random() * 5_000_000), net_tx_bytes: Math.floor(Math.random() * 3_000_000), }, rpc: { avg_latency_ms: +(1 + Math.random() * 10).toFixed(1) }, }) } return res.json({ result: { history } }) } case 'monitoring.alerts': { return res.json({ result: { alerts: [ { id: 'a1', kind: 'cpu_high', message: 'CPU usage exceeded 80% for 5 minutes', timestamp: new Date(Date.now() - 7200000).toISOString(), acknowledged: false }, { id: 'a2', kind: 'disk_high', message: 'Disk usage at 85%', timestamp: new Date(Date.now() - 86400000).toISOString(), acknowledged: true }, ], }, }) } case 'monitoring.alert-rules': { return res.json({ result: { rules: [ { kind: 'cpu_high', enabled: true, threshold: 80, description: 'Alert when CPU usage exceeds threshold' }, { kind: 'mem_high', enabled: true, threshold: 85, description: 'Alert when memory usage exceeds threshold' }, { kind: 'disk_high', enabled: true, threshold: 90, description: 'Alert when disk usage exceeds threshold' }, { kind: 'backend_error_spike', enabled: false, threshold: 500, description: 'Alert when RPC latency exceeds threshold' }, ], }, }) } case 'monitoring.configure-alert': case 'monitoring.acknowledge-alert': { return res.json({ result: { success: true } }) } case 'monitoring.export': { return res.json({ result: { data: 'timestamp,cpu,mem\n2026-04-10T12:00:00Z,15.2,38.5\n' } }) } case 'monitoring.container-metrics': { return res.json({ result: { containers: [ { name: 'bitcoin-knots', cpu_percent: 8.2, mem_used_bytes: 1_200_000_000, net_rx_bytes: 5_000_000, net_tx_bytes: 3_200_000 }, { name: 'lnd', cpu_percent: 3.1, mem_used_bytes: 480_000_000, net_rx_bytes: 2_100_000, net_tx_bytes: 1_800_000 }, { name: 'electrs', cpu_percent: 12.4, mem_used_bytes: 890_000_000, net_rx_bytes: 800_000, net_tx_bytes: 600_000 }, { name: 'mempool', cpu_percent: 5.6, mem_used_bytes: 320_000_000, net_rx_bytes: 1_500_000, net_tx_bytes: 900_000 }, { name: 'filebrowser', cpu_percent: 0.8, mem_used_bytes: 45_000_000, net_rx_bytes: 200_000, net_tx_bytes: 150_000 }, ], }, }) } // ── Fleet / Telemetry ───────────────────────────────────── case 'telemetry.fleet-status': { return res.json({ result: { nodes: [ { id: 'node-1', name: 'archy-main', did: 'did:key:z6MkhaXgBZDvotDkL5257faiztiGiC2ReMkBe4bR6XBIDNq9', status: 'online', last_seen: new Date().toISOString(), version: '1.3.0', uptime_secs: 604800, system: { cpu_percent: 15.2, mem_used_bytes: 6_200_000_000, mem_total_bytes: 16_000_000_000, disk_used_bytes: 620_000_000_000, disk_total_bytes: 1_800_000_000_000 }, apps: ['bitcoin-knots', 'lnd', 'electrs', 'mempool', 'filebrowser'], tor_connected: true, }, { id: 'node-2', name: 'archy-198', did: 'did:key:z6Mkp2z3PJbJHbQ95fGk3CqYVNEPE3VNnFGA7yUYkQjXoZTL', status: 'online', last_seen: new Date(Date.now() - 120000).toISOString(), version: '1.2.1', uptime_secs: 259200, system: { cpu_percent: 8.7, mem_used_bytes: 3_100_000_000, mem_total_bytes: 8_000_000_000, disk_used_bytes: 180_000_000_000, disk_total_bytes: 500_000_000_000 }, apps: ['bitcoin-knots', 'lnd', 'mempool'], tor_connected: true, }, { id: 'node-3', name: 'archy-vps', did: 'did:key:z6MknGc3ocHs3zdPiJbnaaqDi5ER7eLYwBqPR4NkDhCUD7Li', status: 'offline', last_seen: new Date(Date.now() - 3600000).toISOString(), version: '1.3.0', uptime_secs: 0, system: { cpu_percent: 0, mem_used_bytes: 0, mem_total_bytes: 4_000_000_000, disk_used_bytes: 45_000_000_000, disk_total_bytes: 80_000_000_000 }, apps: ['lnd'], tor_connected: false, }, ], }, }) } case 'telemetry.fleet-alerts': { return res.json({ result: { alerts: [ { id: 'fa1', node_id: 'node-3', node_name: 'archy-vps', kind: 'node_offline', message: 'Node went offline', timestamp: new Date(Date.now() - 3600000).toISOString(), acknowledged: false }, { id: 'fa2', node_id: 'node-2', node_name: 'archy-198', kind: 'disk_high', message: 'Disk usage at 36%', timestamp: new Date(Date.now() - 86400000).toISOString(), acknowledged: true }, ], }, }) } case 'telemetry.fleet-node-history': { const nodeId = params?.node_id || 'node-1' const points = 60 const history = [] for (let i = 0; i < points; i++) { history.push({ timestamp: new Date(Date.now() - (points - i) * 60000).toISOString(), cpu_percent: +(8 + Math.random() * 20).toFixed(1), mem_used_bytes: 5_000_000_000 + Math.floor(Math.random() * 2_000_000_000), }) } return res.json({ result: { node_id: nodeId, history } }) } default: { console.log(`[RPC] Unknown method: ${method}`) return res.json({ error: { code: -32601, message: `Method not found: ${method}`, }, }) } } } catch (error) { console.error('[RPC Error]', error) return res.json({ error: { code: -32603, message: error.message, }, }) } }) // ============================================================================= // Mock FileBrowser API (for Cloud page in demo/Docker deployments) // ============================================================================= const MOCK_FILES = { '/': [ { name: 'Music', path: '/Music', size: 0, modified: '2025-03-01T10:00:00Z', isDir: true, type: '' }, { name: 'Documents', path: '/Documents', size: 0, modified: '2025-02-28T14:30:00Z', isDir: true, type: '' }, { name: 'Photos', path: '/Photos', size: 0, modified: '2025-02-20T09:15:00Z', isDir: true, type: '' }, { name: 'Videos', path: '/Videos', size: 0, modified: '2025-01-15T18:00:00Z', isDir: true, type: '' }, ], '/Music': [ { name: 'Bad Actors Reveal.mp3', path: '/Music/Bad Actors Reveal.mp3', size: 8_400_000, modified: '2025-01-10T12:00:00Z', isDir: false, type: 'audio' }, { name: 'Architects of Tomorrow.wav', path: '/Music/Architects of Tomorrow.wav', size: 42_000_000, modified: '2025-01-08T15:00:00Z', isDir: false, type: 'audio' }, { name: 'Sats or Shackles.mp3', path: '/Music/Sats or Shackles.mp3', size: 6_200_000, modified: '2024-12-20T10:00:00Z', isDir: false, type: 'audio' }, { name: 'The Four Horseman of Technocracy.mp3', path: '/Music/The Four Horseman of Technocracy.mp3', size: 7_800_000, modified: '2024-12-15T11:00:00Z', isDir: false, type: 'audio' }, { name: 'Inverse Dylan (Remix).mp3', path: '/Music/Inverse Dylan (Remix).mp3', size: 5_600_000, modified: '2024-12-10T16:00:00Z', isDir: false, type: 'audio' }, { name: 'Hootcoiner.mp3', path: '/Music/Hootcoiner.mp3', size: 4_200_000, modified: '2024-11-28T09:00:00Z', isDir: false, type: 'audio' }, { name: 'decentrealisation.mp3', path: '/Music/decentrealisation.mp3', size: 5_100_000, modified: '2024-11-20T14:00:00Z', isDir: false, type: 'audio' }, { name: 'neo-morality.mp3', path: '/Music/neo-morality.mp3', size: 6_800_000, modified: '2024-11-15T11:00:00Z', isDir: false, type: 'audio' }, { name: 'death is a gift.mp3', path: '/Music/death is a gift.mp3', size: 4_500_000, modified: '2024-11-10T08:00:00Z', isDir: false, type: 'audio' }, { name: 'Wash the fucking dishes.mp3', path: '/Music/Wash the fucking dishes.mp3', size: 3_900_000, modified: '2024-11-05T13:00:00Z', isDir: false, type: 'audio' }, { name: 'All the leaves are brown.mp3', path: '/Music/All the leaves are brown.mp3', size: 5_300_000, modified: '2024-10-28T10:00:00Z', isDir: false, type: 'audio' }, { name: 'Builders not talkers.mp3', path: '/Music/Builders not talkers.mp3', size: 4_700_000, modified: '2024-10-20T15:00:00Z', isDir: false, type: 'audio' }, { name: 'SMRI.mp3', path: '/Music/SMRI.mp3', size: 5_900_000, modified: '2024-10-15T12:00:00Z', isDir: false, type: 'audio' }, { name: 'Shadrap.mp3', path: '/Music/Shadrap.mp3', size: 3_400_000, modified: '2024-10-10T09:00:00Z', isDir: false, type: 'audio' }, { name: 'The Wehrman.mp3', path: '/Music/The Wehrman.mp3', size: 6_100_000, modified: '2024-10-05T14:00:00Z', isDir: false, type: 'audio' }, { name: 'An Exploited Substrate.mp3', path: '/Music/An Exploited Substrate.mp3', size: 4_800_000, modified: '2024-09-28T11:00:00Z', isDir: false, type: 'audio' }, { name: 'Govcucks.wav', path: '/Music/Govcucks.wav', size: 38_000_000, modified: '2024-09-20T16:00:00Z', isDir: false, type: 'audio' }, ], '/Documents': [ { name: 'bitcoin-whitepaper-notes.md', path: '/Documents/bitcoin-whitepaper-notes.md', size: 820, modified: '2025-02-28T14:30:00Z', isDir: false, type: 'text' }, { name: 'node-setup-checklist.md', path: '/Documents/node-setup-checklist.md', size: 950, modified: '2025-02-25T10:00:00Z', isDir: false, type: 'text' }, { name: 'lightning-channels.csv', path: '/Documents/lightning-channels.csv', size: 680, modified: '2025-02-20T16:00:00Z', isDir: false, type: 'text' }, { name: 'sovereignty-manifesto.txt', path: '/Documents/sovereignty-manifesto.txt', size: 1100, modified: '2025-02-15T12:00:00Z', isDir: false, type: 'text' }, { name: 'backup-log.json', path: '/Documents/backup-log.json', size: 1450, modified: '2025-03-01T02:00:00Z', isDir: false, type: 'text' }, ], '/Photos': [ { name: 'node-rack-setup.jpg', path: '/Photos/node-rack-setup.jpg', size: 2_400_000, modified: '2025-02-20T09:15:00Z', isDir: false, type: 'image' }, { name: 'bitcoin-conference-2024.jpg', path: '/Photos/bitcoin-conference-2024.jpg', size: 3_100_000, modified: '2024-12-15T14:30:00Z', isDir: false, type: 'image' }, { name: 'lightning-network-visualization.png', path: '/Photos/lightning-network-visualization.png', size: 1_800_000, modified: '2025-01-10T11:00:00Z', isDir: false, type: 'image' }, { name: 'home-server-build.jpg', path: '/Photos/home-server-build.jpg', size: 2_900_000, modified: '2024-11-20T16:45:00Z', isDir: false, type: 'image' }, { name: 'sunset-from-balcony.jpg', path: '/Photos/sunset-from-balcony.jpg', size: 4_200_000, modified: '2025-02-14T18:30:00Z', isDir: false, type: 'image' }, ], '/Videos': [ { name: 'node-unboxing-timelapse.mp4', path: '/Videos/node-unboxing-timelapse.mp4', size: 85_000_000, modified: '2024-11-01T10:00:00Z', isDir: false, type: 'video' }, { name: 'bitcoin-explained-5min.mp4', path: '/Videos/bitcoin-explained-5min.mp4', size: 42_000_000, modified: '2024-10-15T14:00:00Z', isDir: false, type: 'video' }, { name: 'lightning-payment-demo.mp4', path: '/Videos/lightning-payment-demo.mp4', size: 28_000_000, modified: '2025-01-20T12:00:00Z', isDir: false, type: 'video' }, ], } const MOCK_FILE_CONTENTS = { '/Documents/bitcoin-whitepaper-notes.md': `# Bitcoin Whitepaper Notes\n\n## Key Concepts\n\n### Peer-to-Peer Electronic Cash\n- No trusted third party needed\n- Double-spending solved via proof-of-work\n- Longest chain = truth\n\n### Proof of Work\n- SHA-256 based hashing\n- Difficulty adjusts every 2016 blocks (~2 weeks)\n- Incentive: block reward + transaction fees\n\n## My Thoughts\n- The 21M supply cap is genius - digital scarcity\n- Lightning Network solves the scaling concern\n- Self-custody is the whole point`, '/Documents/node-setup-checklist.md': `# Archipelago Node Setup Checklist\n\n## Hardware\n- [x] Intel NUC / Mini PC (16GB RAM minimum)\n- [x] 2TB NVMe SSD\n- [x] USB drive for installer\n- [x] Ethernet cable\n\n## Core Apps\n- [x] Bitcoin Knots\n- [x] LND\n- [x] Mempool Explorer\n- [ ] BTCPay Server\n- [ ] Fedimint`, '/Documents/lightning-channels.csv': `channel_id,peer_alias,capacity_sats,local_balance,remote_balance,status\nch_001,ACINQ,5000000,2450000,2550000,active\nch_002,WalletOfSatoshi,2000000,1200000,800000,active\nch_003,Voltage,10000000,4500000,5500000,active\nch_004,Kraken,3000000,1800000,1200000,active`, '/Documents/sovereignty-manifesto.txt': `THE SOVEREIGNTY MANIFESTO\n=========================\n\nWe hold these truths to be self-evident:\n\n1. Your data belongs to you.\n2. Your money should be uncensorable.\n3. Your communications should be private.\n4. Your compute should be sovereign.\n5. Your identity should be self-issued.\n\nRun your own node. Hold your own keys. Own your own data. Be sovereign.`, '/Documents/backup-log.json': JSON.stringify({ backups: [{ id: 'bkp-2025-03-01', timestamp: '2025-03-01T02:00:00Z', type: 'full', apps: ['bitcoin-knots', 'lnd', 'mempool'], size_mb: 2340, status: 'success' }] }, null, 2), } // FileBrowser UI (demo placeholder when launched directly) app.get('/app/filebrowser/', (req, res) => { res.type('html').send(` File Browser

File Browser

File Browser is running. Use the Cloud page in Archipelago to manage your files.

`) }) // FileBrowser login - return mock JWT app.post('/app/filebrowser/api/login', (req, res) => { res.send('"mock-filebrowser-token-demo"') }) // FileBrowser list resources app.get('/app/filebrowser/api/resources/*', (req, res) => { const reqPath = decodeURIComponent(req.params[0] || '/').replace(/\/+$/, '') || '/' const items = MOCK_FILES[reqPath] || [] res.json({ items, numDirs: items.filter(i => i.isDir).length, numFiles: items.filter(i => !i.isDir).length, sorting: { by: 'name', asc: true }, }) }) app.get('/app/filebrowser/api/resources', (req, res) => { const items = MOCK_FILES['/'] || [] res.json({ items, numDirs: items.filter(i => i.isDir).length, numFiles: items.filter(i => !i.isDir).length, sorting: { by: 'name', asc: true }, }) }) // FileBrowser upload (POST to resources path) — mock accepts and discards the body app.post('/app/filebrowser/api/resources/*', (req, res) => { req.resume() req.on('end', () => res.sendStatus(200)) }) // FileBrowser delete app.delete('/app/filebrowser/api/resources/*', (req, res) => { res.sendStatus(200) }) // FileBrowser rename app.patch('/app/filebrowser/api/resources/*', (req, res) => { res.sendStatus(200) }) // FileBrowser raw file content (for text file reading) app.get('/app/filebrowser/api/raw/*', (req, res) => { const reqPath = '/' + decodeURIComponent(req.params[0] || '') const content = MOCK_FILE_CONTENTS[reqPath] if (content) { res.type('text/plain').send(content) } else { res.status(404).send('File not found') } }) // Claude API Proxy (reads ANTHROPIC_API_KEY from environment) // Uses fetch (Node 22+) for reliable DNS resolution and streaming in Docker/Alpine // ============================================================================= app.post('/aiui/api/claude/*', async (req, res) => { const apiKey = process.env.ANTHROPIC_API_KEY if (!apiKey) { return res.status(500).json({ type: 'error', error: { type: 'configuration_error', message: 'ANTHROPIC_API_KEY not configured on server' } }) } const apiPath = '/' + req.params[0] // Clean request body for Anthropic API const body = req.body if (body) { if (!body.max_tokens) body.max_tokens = 4096 // Fix model IDs — AIUI may send short names if (body.model && !body.model.includes('-2')) { const modelMap = { 'claude-haiku-4.5': 'claude-haiku-4-5-20251001', 'claude-haiku-4-5': 'claude-haiku-4-5-20251001', 'claude-sonnet-4-5': 'claude-sonnet-4-5-20250514', 'claude-sonnet-4.5': 'claude-sonnet-4-5-20250514', } if (modelMap[body.model]) body.model = modelMap[body.model] } // Strip AIUI-specific fields that Anthropic API rejects delete body.webSearch delete body.webResults delete body.context } const bodyStr = JSON.stringify(body) const url = `https://api.anthropic.com${apiPath}` console.log(`[Claude Proxy] → POST ${url} (${bodyStr.length} bytes, model: ${body?.model || 'unknown'})`) try { const controller = new AbortController() const timeout = setTimeout(() => controller.abort(), 60000) const apiRes = await fetch(url, { method: 'POST', signal: controller.signal, headers: { 'Content-Type': 'application/json', 'x-api-key': apiKey, 'anthropic-version': '2023-06-01', }, body: bodyStr, }) clearTimeout(timeout) console.log(`[Claude Proxy] ← ${apiRes.status}`) // Forward status and headers res.status(apiRes.status) for (const [key, value] of apiRes.headers.entries()) { // Skip hop-by-hop headers if (!['transfer-encoding', 'connection', 'keep-alive'].includes(key.toLowerCase())) { res.setHeader(key, value) } } // Stream the response body if (apiRes.body) { const reader = apiRes.body.getReader() const pump = async () => { while (true) { const { done, value } = await reader.read() if (done) { res.end(); return } if (!res.writableEnded) res.write(value) } } pump().catch((err) => { console.error('[Claude Proxy] Stream error:', err.message) if (!res.writableEnded) res.end() }) } else { res.end() } } catch (err) { const msg = err.name === 'AbortError' ? 'Request timed out (60s)' : (err.message || 'Unknown error') console.error(`[Claude Proxy] Error: ${msg}`) if (!res.headersSent) { res.status(502).json({ type: 'error', error: { type: 'proxy_error', message: msg } }) } } }) // Ollama (local AI) proxy — forwards to localhost:11434 app.all('/aiui/api/ollama/*', (req, res) => { const ollamaPath = '/' + req.params[0] const bodyStr = JSON.stringify(req.body) const options = { hostname: '127.0.0.1', port: 11434, path: ollamaPath, method: req.method, headers: { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(bodyStr), }, } const proxyReq = http.request(options, (proxyRes) => { res.writeHead(proxyRes.statusCode, proxyRes.headers) proxyRes.pipe(res) }) proxyReq.on('error', (err) => { const msg = err.message || err.code || 'Ollama not available' console.error('[Ollama Proxy] Error:', msg) if (!res.headersSent) { res.status(502).json({ error: msg }) } }) if (req.method !== 'GET' && req.method !== 'HEAD') { proxyReq.write(bodyStr) } proxyReq.end() }) // ============================================================================= // Ollama Local AI Proxy (forwards to Ollama on localhost:11434) // ============================================================================= app.all('/api/ollama/*', (req, res) => { const ollamaPath = '/' + req.params[0] const isPost = req.method === 'POST' const bodyStr = isPost ? JSON.stringify(req.body) : null const options = { hostname: '127.0.0.1', port: 11434, path: ollamaPath, method: req.method, headers: { 'Content-Type': 'application/json' }, } const proxyReq = http.request(options, (proxyRes) => { res.writeHead(proxyRes.statusCode, proxyRes.headers) proxyRes.pipe(res) }) proxyReq.on('error', (err) => { const msg = err.message || err.code || 'Ollama not available' console.error('[Ollama Proxy] Error:', msg) if (!res.headersSent) { res.status(502).json({ error: msg }) } }) if (bodyStr) proxyReq.write(bodyStr) proxyReq.end() }) // Web search stub (no search engine configured in demo) app.get('/api/web-search', (req, res) => { res.json({ results: [] }) }) // TMDB API stub (no TMDB key in demo) app.get('/api/tmdb/*', (req, res) => { res.json({ results: [] }) }) // Catch-all for unimplemented API endpoints (return JSON, not HTML) app.all('/api/*', (req, res) => { res.status(404).json({ error: 'Not available in demo mode' }) }) app.all('/aiui/api/*', (req, res) => { res.status(404).json({ error: 'Not available in demo mode' }) }) // ============================================================================= // Mock ThunderHub UI + Lightning API (no Docker required) // ============================================================================= const MOCK_LND_DATA = { info: { alias: 'archy-signet', color: '#f7931a', public_key: '03a1b2c3d4e5f6789012345678901234567890abcdef1234567890abcdef123456', num_active_channels: 4, num_inactive_channels: 1, num_pending_channels: 1, block_height: 892451, synced_to_chain: true, synced_to_graph: true, version: '0.17.4-beta commit=v0.17.4-beta', chains: [{ chain: 'bitcoin', network: 'signet' }], uris: ['03a1b2c3@archy-signet.onion:9735'], }, balance: { total_balance: 2_450_000, confirmed_balance: 2_350_000, unconfirmed_balance: 100_000, }, channelBalance: { local_balance: { sat: 8_250_000 }, remote_balance: { sat: 11_750_000 }, pending_open_local_balance: { sat: 500_000 }, pending_open_remote_balance: { sat: 0 }, }, channels: [ { chan_id: '840921088114688', remote_pubkey: '02778f4a4e...acinq', capacity: 5_000_000, local_balance: 2_450_000, remote_balance: 2_550_000, active: true, peer_alias: 'ACINQ Signet', total_satoshis_sent: 850_000, total_satoshis_received: 1_200_000, uptime: 604800, lifetime: 2592000 }, { chan_id: '840921088114689', remote_pubkey: '03abcdef12...wos', capacity: 2_000_000, local_balance: 1_200_000, remote_balance: 800_000, active: true, peer_alias: 'WalletOfSatoshi', total_satoshis_sent: 350_000, total_satoshis_received: 500_000, uptime: 259200, lifetime: 1296000 }, { chan_id: '840921088114690', remote_pubkey: '02fedcba98...voltage', capacity: 10_000_000, local_balance: 4_500_000, remote_balance: 5_500_000, active: true, peer_alias: 'Voltage', total_satoshis_sent: 2_100_000, total_satoshis_received: 1_800_000, uptime: 518400, lifetime: 2592000 }, { chan_id: '840921088114691', remote_pubkey: '03456789ab...kraken', capacity: 3_000_000, local_balance: 100_000, remote_balance: 2_900_000, active: true, peer_alias: 'Kraken', total_satoshis_sent: 50_000, total_satoshis_received: 120_000, uptime: 86400, lifetime: 604800 }, { chan_id: '840921088114692', remote_pubkey: '02112233aa...old', capacity: 1_000_000, local_balance: 0, remote_balance: 1_000_000, active: false, peer_alias: 'OldPeer-Offline', total_satoshis_sent: 0, total_satoshis_received: 0, uptime: 0, lifetime: 5184000 }, ], pendingChannels: { pending_open_channels: [ { channel: { remote_node_pub: '03ffeeddcc...newpeer', capacity: 500_000, local_balance: 500_000, remote_balance: 0 }, confirmation_height: 892452 }, ], pending_closing_channels: [], pending_force_closing_channels: [], waiting_close_channels: [], }, invoices: [ { memo: 'Channel opening fee', value: 50_000, settled: true, creation_date: Math.floor(Date.now()/1000) - 86400, settle_date: Math.floor(Date.now()/1000) - 85800, payment_request: 'lnsb500000n1pjtest...truncated', state: 'SETTLED', amt_paid_sat: 50_000, r_hash: Buffer.from('aabbccdd01').toString('hex') }, { memo: 'Test payment', value: 1_000, settled: true, creation_date: Math.floor(Date.now()/1000) - 7200, settle_date: Math.floor(Date.now()/1000) - 7100, payment_request: 'lnsb10000n1pjtest2...truncated', state: 'SETTLED', amt_paid_sat: 1_000, r_hash: Buffer.from('aabbccdd02').toString('hex') }, { memo: 'Coffee payment', value: 5_000, settled: true, creation_date: Math.floor(Date.now()/1000) - 3600, settle_date: Math.floor(Date.now()/1000) - 3500, payment_request: 'lnsb50000n1pjtest3...truncated', state: 'SETTLED', amt_paid_sat: 5_000, r_hash: Buffer.from('aabbccdd03').toString('hex') }, { memo: 'Donation', value: 21_000, settled: false, creation_date: Math.floor(Date.now()/1000) - 600, settle_date: 0, payment_request: 'lnsb210000n1pjtest4...truncated', state: 'OPEN', amt_paid_sat: 0, r_hash: Buffer.from('aabbccdd04').toString('hex') }, ], payments: [ { payment_hash: 'ff00112233', value_sat: 10_000, fee_sat: 3, status: 'SUCCEEDED', creation_date: Math.floor(Date.now()/1000) - 43200, payment_request: 'lnsb100000n1pjpay1...', failure_reason: 'FAILURE_REASON_NONE' }, { payment_hash: 'ff00112234', value_sat: 100_000, fee_sat: 12, status: 'SUCCEEDED', creation_date: Math.floor(Date.now()/1000) - 21600, payment_request: 'lnsb1000000n1pjpay2...', failure_reason: 'FAILURE_REASON_NONE' }, { payment_hash: 'ff00112235', value_sat: 500, fee_sat: 1, status: 'SUCCEEDED', creation_date: Math.floor(Date.now()/1000) - 1800, payment_request: 'lnsb5000n1pjpay3...', failure_reason: 'FAILURE_REASON_NONE' }, ], forwarding: [ { chan_id_in: '840921088114688', chan_id_out: '840921088114690', amt_in: 10_012, amt_out: 10_000, fee: 12, timestamp_ns: (Date.now() - 7200000) * 1e6 }, { chan_id_in: '840921088114690', chan_id_out: '840921088114689', amt_in: 5_005, amt_out: 5_000, fee: 5, timestamp_ns: (Date.now() - 3600000) * 1e6 }, { chan_id_in: '840921088114689', chan_id_out: '840921088114691', amt_in: 25_025, amt_out: 25_000, fee: 25, timestamp_ns: (Date.now() - 1200000) * 1e6 }, ], } // ThunderHub mock web UI app.get('/app/thunderhub/', (req, res) => { const d = MOCK_LND_DATA const totalCap = d.channels.reduce((s, c) => s + c.capacity, 0) const totalLocal = d.channels.reduce((s, c) => s + c.local_balance, 0) const totalRemote = d.channels.reduce((s, c) => s + c.remote_balance, 0) const totalFees = d.forwarding.reduce((s, f) => s + f.fee, 0) const channelRows = d.channels.map(c => ` ${c.peer_alias} ${(c.capacity/1e6).toFixed(1)}M
${(c.local_balance/1e3).toFixed(0)}k
${(c.remote_balance/1e3).toFixed(0)}k
${c.active ? 'Active' : 'Offline'} `).join('') const invoiceRows = d.invoices.slice().reverse().map(inv => ` ${inv.memo} ${inv.value.toLocaleString()} sats ${inv.settled ? 'Settled' : 'Open'} ${new Date(inv.creation_date * 1000).toLocaleString()} `).join('') const paymentRows = d.payments.map(p => ` ${p.payment_hash.slice(0,12)}... ${p.value_sat.toLocaleString()} sats ${p.fee_sat} sats Succeeded `).join('') res.type('html').send(` ThunderHub — archy-signet

ThunderHubsignet

${d.info.alias} — block ${d.info.block_height.toLocaleString()} — ${d.info.num_active_channels} active channels
On-chain Balance
${(d.balance.confirmed_balance/1e6).toFixed(2)}M sats
Channel Capacity
${(totalCap/1e6).toFixed(1)}M sats
Local Balance
${(totalLocal/1e6).toFixed(1)}M sats
Remote Balance
${(totalRemote/1e6).toFixed(1)}M sats
Routing Fees Earned
${totalFees} sats
Payments Sent
${d.payments.length}

Channels (${d.channels.length})

${channelRows}
PeerCapacityLocalRemoteStatus

Recent Invoices

${invoiceRows}
MemoAmountStatusCreated

Recent Payments

${paymentRows}
HashAmountFeeStatus

Forwarding History

${d.forwarding.map(f => { const inPeer = d.channels.find(c => c.chan_id === f.chan_id_in)?.peer_alias || f.chan_id_in const outPeer = d.channels.find(c => c.chan_id === f.chan_id_out)?.peer_alias || f.chan_id_out return `` }).join('')}
In ChannelOut ChannelAmountFeeTime
${inPeer}${outPeer}${f.amt_in.toLocaleString()} sats${f.fee} sats${new Date(f.timestamp_ns/1e6).toLocaleString()}

Mock ThunderHub — Archipelago Dev Mode — No Docker Required

`) }) // ThunderHub API stubs (for any programmatic access) app.get('/app/thunderhub/api/info', (req, res) => res.json(MOCK_LND_DATA.info)) app.get('/app/thunderhub/api/balance', (req, res) => res.json(MOCK_LND_DATA.balance)) app.get('/app/thunderhub/api/channels', (req, res) => res.json(MOCK_LND_DATA.channels)) app.get('/app/thunderhub/api/invoices', (req, res) => res.json(MOCK_LND_DATA.invoices)) app.get('/app/thunderhub/api/payments', (req, res) => res.json(MOCK_LND_DATA.payments)) app.get('/app/thunderhub/api/forwards', (req, res) => res.json(MOCK_LND_DATA.forwarding)) // Health check app.get('/health', (req, res) => { res.status(200).send('healthy') }) // WebSocket endpoint const server = http.createServer(app) const wss = new WebSocketServer({ server, path: '/ws/db' }) wss.on('connection', (ws, req) => { console.log('[WebSocket] Client connected from', req.socket.remoteAddress) wsClients.add(ws) // Set up ping/pong to keep connection alive const pingInterval = setInterval(() => { if (ws.readyState === 1) { // OPEN try { ws.ping() } catch (err) { console.error('[WebSocket] Ping error:', err) clearInterval(pingInterval) clearInterval(heartbeatInterval) } } else { clearInterval(pingInterval) clearInterval(heartbeatInterval) } }, 30000) // Ping every 30 seconds // Send periodic heartbeat data so clients don't think the connection is dead const heartbeatInterval = setInterval(() => { if (ws.readyState === 1) { try { ws.send(JSON.stringify({ type: 'heartbeat', rev: Date.now() })) } catch { /* ignore */ } } }, 45000) // Every 45s (client expects data within 60s) // Send initial data immediately try { ws.send(JSON.stringify({ type: 'initial', data: mockData, })) console.log('[WebSocket] Initial data sent') } catch (err) { console.error('[WebSocket] Error sending initial data:', err) } ws.on('pong', () => { // Client responded to ping, connection is alive }) ws.on('message', (message) => { // Handle incoming messages if needed try { const data = JSON.parse(message.toString()) console.log('[WebSocket] Received message:', data) } catch (err) { console.error('[WebSocket] Error parsing message:', err) } }) ws.on('close', (code, reason) => { console.log('[WebSocket] Client disconnected', { code, reason: reason.toString() }) clearInterval(pingInterval) clearInterval(heartbeatInterval) wsClients.delete(ws) }) ws.on('error', (error) => { console.error('[WebSocket Error]', error) clearInterval(pingInterval) clearInterval(heartbeatInterval) wsClients.delete(ws) }) }) server.listen(PORT, '0.0.0.0', async () => { const runtime = await isContainerRuntimeAvailable() // Initialize package data from Docker await initializePackageData() console.log(` ╔════════════════════════════════════════════════════════════╗ ║ ║ ║ 🚀 Archipelago Mock Backend Server ║ ║ ║ ║ RPC: http://localhost:${PORT}/rpc/v1 ║ ║ WebSocket: ws://localhost:${PORT}/ws/db ║ ║ ║ ║ Dev Mode: ${DEV_MODE.padEnd(47)}║ ║ Setup: ${userState.setupComplete ? '✅ Complete' : '❌ Not done'.padEnd(47)}║ ║ Onboarding: ${userState.onboardingComplete ? '✅ Complete' : '❌ Not done'.padEnd(46)}║ ║ ║ ║ Mock Password: ${MOCK_PASSWORD.padEnd(40)}║ ║ ║ ║ Container Runtime: ${runtime.available ? `✅ ${runtime.runtime}`.padEnd(40) : 'â­ī¸ Simulation mode'.padEnd(40)}║ ║ Claude API Key: ${process.env.ANTHROPIC_API_KEY ? '✅ Set (' + process.env.ANTHROPIC_API_KEY.slice(0, 12) + '...)' : '❌ Not set (chat disabled)'.padEnd(40)}║ ║ ║ ╚════════════════════════════════════════════════════════════╝ `) console.log('Mock backend is running. Press Ctrl+C to stop.\n') // Pre-check Anthropic API connectivity if (process.env.ANTHROPIC_API_KEY) { try { const dns = await import('dns') dns.lookup('api.anthropic.com', (err, address) => { if (err) { console.error('[Claude Proxy] ⚠ DNS lookup failed for api.anthropic.com:', err.message) console.error('[Claude Proxy] Chat will fail. Check container DNS settings.') } else { console.log('[Claude Proxy] ✅ api.anthropic.com resolves to', address) } }) } catch { /* ignore */ } } // Periodically update package data from Docker (merge with static dev apps) // Only poll if container runtime is available (avoids log spam in demo/Docker deployments) if (runtime.available) { setInterval(async () => { const dockerApps = await getDockerContainers() mockData['package-data'] = mergePackageData(dockerApps) // Broadcast update to connected clients broadcastUpdate([ { op: 'replace', path: '/package-data', value: mockData['package-data'] } ]) }, 5000) // Update every 5 seconds } }) process.on('SIGINT', () => { console.log('\n\nShutting down mock backend...') server.close(() => { console.log('Server stopped.') process.exit(0) }) })