archy/neode-ui/mock-backend.js
archipelago 87769cbfbf feat(ui): dual-ecash wallet settings, buy-peer-files, seed backup, assorted fixes
- Tabbed Wallet Settings modal (Cashu + Fedimint) and dual-balance wallet card
- Buy a peer's paid file (ecash / node Lightning / on-chain / external QR)
- Recovery-phrase reveal + backup section; onboarding seed retry resilience
- NetBird HTTPS launch, remote-control two-finger scroll + external-open
- Shared BackButton, single-v version label, mesh Bitcoin header toggles

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 19:21:42 -04:00

3862 lines
155 KiB
JavaScript
Executable File
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/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(`<!doctype html>
<html>
<head>
<meta charset="utf-8">
<meta http-equiv="refresh" content="0; url=http://localhost:${uiPort}/">
<title>Archipelago dev backend</title>
</head>
<body>
<p>This is the mock JSON-RPC backend. Open the dashboard at
<a href="http://localhost:${uiPort}/">http://localhost:${uiPort}/</a>.
</p>
</body>
</html>`)
})
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: '<svg xmlns="http://www.w3.org/2000/svg" width="200" height="200"><rect width="200" height="200" fill="#fff"/><text x="100" y="100" text-anchor="middle" font-size="12" fill="#333">Mock QR Code</text></svg>',
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(`<!DOCTYPE html><html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
<title>File Browser</title><style>*{margin:0;padding:0;box-sizing:border-box}body{background:#1a1a2e;color:#e0e0e0;font-family:system-ui,sans-serif;display:flex;align-items:center;justify-content:center;min-height:100vh}
.card{background:rgba(0,0,0,0.4);border:1px solid rgba(255,255,255,0.1);border-radius:16px;padding:48px;text-align:center;max-width:400px;backdrop-filter:blur(20px)}
h1{font-size:24px;margin-bottom:12px}p{color:rgba(255,255,255,0.6);font-size:14px;line-height:1.6}</style></head>
<body><div class="card"><h1>File Browser</h1><p>File Browser is running. Use the Cloud page in Archipelago to manage your files.</p></div></body></html>`)
})
// 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 => `
<tr>
<td>${c.peer_alias}</td>
<td>${(c.capacity/1e6).toFixed(1)}M</td>
<td><div style="display:flex;gap:4px;align-items:center"><div style="background:#4ade80;height:8px;width:${Math.round(c.local_balance/c.capacity*100)}%;border-radius:4px"></div><span style="font-size:11px;opacity:.6">${(c.local_balance/1e3).toFixed(0)}k</span></div></td>
<td><div style="display:flex;gap:4px;align-items:center"><div style="background:#3b82f6;height:8px;width:${Math.round(c.remote_balance/c.capacity*100)}%;border-radius:4px"></div><span style="font-size:11px;opacity:.6">${(c.remote_balance/1e3).toFixed(0)}k</span></div></td>
<td><span style="color:${c.active ? '#4ade80' : '#ef4444'}">${c.active ? 'Active' : 'Offline'}</span></td>
</tr>`).join('')
const invoiceRows = d.invoices.slice().reverse().map(inv => `
<tr>
<td>${inv.memo}</td>
<td>${inv.value.toLocaleString()} sats</td>
<td><span style="color:${inv.settled ? '#4ade80' : '#fb923c'}">${inv.settled ? 'Settled' : 'Open'}</span></td>
<td>${new Date(inv.creation_date * 1000).toLocaleString()}</td>
</tr>`).join('')
const paymentRows = d.payments.map(p => `
<tr>
<td>${p.payment_hash.slice(0,12)}...</td>
<td>${p.value_sat.toLocaleString()} sats</td>
<td>${p.fee_sat} sats</td>
<td style="color:#4ade80">Succeeded</td>
</tr>`).join('')
res.type('html').send(`<!DOCTYPE html><html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
<title>ThunderHub — archy-signet</title>
<style>
*{margin:0;padding:0;box-sizing:border-box}
body{background:#0f0f1a;color:#e0e0e0;font-family:system-ui,-apple-system,sans-serif;padding:24px}
h1{font-size:22px;margin-bottom:4px;color:#fb923c}
.subtitle{color:rgba(255,255,255,.5);font-size:13px;margin-bottom:24px}
.grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:16px;margin-bottom:32px}
.stat{background:rgba(255,255,255,.05);border:1px solid rgba(255,255,255,.1);border-radius:12px;padding:16px}
.stat .label{font-size:11px;text-transform:uppercase;letter-spacing:1px;color:rgba(255,255,255,.4);margin-bottom:4px}
.stat .value{font-size:24px;font-weight:600}
.stat .value.green{color:#4ade80}.stat .value.orange{color:#fb923c}.stat .value.blue{color:#3b82f6}
h2{font-size:16px;margin:24px 0 12px;color:rgba(255,255,255,.8)}
table{width:100%;border-collapse:collapse;font-size:13px}
th{text-align:left;padding:8px 12px;border-bottom:1px solid rgba(255,255,255,.1);color:rgba(255,255,255,.4);font-weight:500;font-size:11px;text-transform:uppercase;letter-spacing:.5px}
td{padding:8px 12px;border-bottom:1px solid rgba(255,255,255,.05)}
tr:hover td{background:rgba(255,255,255,.02)}
.section{background:rgba(0,0,0,.3);border:1px solid rgba(255,255,255,.08);border-radius:12px;padding:20px;margin-bottom:20px}
.badge{display:inline-block;background:rgba(247,147,26,.15);color:#fb923c;padding:2px 8px;border-radius:6px;font-size:11px;margin-left:8px}
</style></head><body>
<h1>ThunderHub<span class="badge">signet</span></h1>
<div class="subtitle">${d.info.alias} &mdash; block ${d.info.block_height.toLocaleString()} &mdash; ${d.info.num_active_channels} active channels</div>
<div class="grid">
<div class="stat"><div class="label">On-chain Balance</div><div class="value orange">${(d.balance.confirmed_balance/1e6).toFixed(2)}M sats</div></div>
<div class="stat"><div class="label">Channel Capacity</div><div class="value">${(totalCap/1e6).toFixed(1)}M sats</div></div>
<div class="stat"><div class="label">Local Balance</div><div class="value green">${(totalLocal/1e6).toFixed(1)}M sats</div></div>
<div class="stat"><div class="label">Remote Balance</div><div class="value blue">${(totalRemote/1e6).toFixed(1)}M sats</div></div>
<div class="stat"><div class="label">Routing Fees Earned</div><div class="value green">${totalFees} sats</div></div>
<div class="stat"><div class="label">Payments Sent</div><div class="value">${d.payments.length}</div></div>
</div>
<div class="section">
<h2>Channels (${d.channels.length})</h2>
<table><thead><tr><th>Peer</th><th>Capacity</th><th>Local</th><th>Remote</th><th>Status</th></tr></thead><tbody>${channelRows}</tbody></table>
</div>
<div class="section">
<h2>Recent Invoices</h2>
<table><thead><tr><th>Memo</th><th>Amount</th><th>Status</th><th>Created</th></tr></thead><tbody>${invoiceRows}</tbody></table>
</div>
<div class="section">
<h2>Recent Payments</h2>
<table><thead><tr><th>Hash</th><th>Amount</th><th>Fee</th><th>Status</th></tr></thead><tbody>${paymentRows}</tbody></table>
</div>
<div class="section">
<h2>Forwarding History</h2>
<table><thead><tr><th>In Channel</th><th>Out Channel</th><th>Amount</th><th>Fee</th><th>Time</th></tr></thead><tbody>
${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 `<tr><td>${inPeer}</td><td>${outPeer}</td><td>${f.amt_in.toLocaleString()} sats</td><td>${f.fee} sats</td><td>${new Date(f.timestamp_ns/1e6).toLocaleString()}</td></tr>`
}).join('')}
</tbody></table>
</div>
<p style="text-align:center;color:rgba(255,255,255,.25);font-size:12px;margin-top:32px">Mock ThunderHub &mdash; Archipelago Dev Mode &mdash; No Docker Required</p>
</body></html>`)
})
// 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)
})
})