2026-01-24 22:59:20 +00:00
#!/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'
2026-01-27 23:06:18 +00:00
import Docker from 'dockerode'
2026-01-24 22:59:20 +00:00
const __filename = fileURLToPath ( import . meta . url )
const __dirname = path . dirname ( __filename )
const execPromise = promisify ( exec )
2026-03-18 21:06:14 +00:00
// 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)' )
}
2026-01-24 22:59:20 +00:00
const app = express ()
const PORT = 5959
2026-03-17 23:56:37 +00:00
// Dev mode from environment (setup, onboarding, existing, boot, or default)
2026-01-24 22:59:20 +00:00
const DEV_MODE = process . env . VITE_DEV_MODE || 'default'
2026-03-17 23:56:37 +00:00
// 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)
2026-01-24 22:59:20 +00:00
// CORS configuration
const corsOptions = {
credentials : true ,
origin : ( origin , callback ) => {
if ( ! origin ) return callback ( null , true )
callback ( null , true )
}
}
app . use ( cors ( corsOptions ))
2026-03-09 21:27:26 +00:00
// 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 )
})
2026-01-24 22:59:20 +00:00
app . use ( cookieParser ())
// Mock session storage
const sessions = new Map ()
const MOCK_PASSWORD = 'password123'
2026-03-18 23:14:47 +00:00
// 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 ( '' ) }
2026-06-11 00:24:40 -04:00
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 (),
},
],
}
2026-01-24 22:59:20 +00:00
// User state (simulated file-based storage)
let userState = {
setupComplete : false ,
onboardingComplete : false ,
passwordHash : null , // In real app, this would be bcrypt hash
}
2026-03-18 23:14:47 +00:00
let mockState = { analyticsEnabled : false }
2026-01-24 22:59:20 +00:00
// 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
2026-03-17 23:56:37 +00:00
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
2026-01-24 22:59:20 +00:00
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 ,
2026-03-09 17:09:59 +00:00
'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 ,
2026-03-18 19:24:52 +00:00
'fedimint' : 8175 ,
'thunderhub' : 3010 ,
2026-03-09 17:09:59 +00:00
'nostr-rs-relay' : 7000 ,
'syncthing' : 8384 ,
'penpot' : 9001 ,
'nginx-proxy-manager' : 8181 ,
'indeedhub' : 8190 ,
'dwn' : 3000 ,
'tor' : 9050 ,
2026-01-24 22:59:20 +00:00
}
2026-03-09 17:09:59 +00:00
// Auto-assign port for unknown apps (start at 8200, increment)
let nextAutoPort = 8200
2026-01-27 23:06:18 +00:00
// Helper: Query real Docker containers
async function getDockerContainers () {
2026-03-18 21:06:14 +00:00
if ( ! docker ) return {}
2026-01-27 23:06:18 +00:00
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' ,
2026-02-17 15:03:34 +00:00
'mempool-electrs' : 'mempool-electrs' ,
2026-01-27 23:06:18 +00:00
'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' ,
2026-02-17 15:03:34 +00:00
icon : '/assets/img/app-icons/fedimint.png' ,
2026-01-27 23:06:18 +00:00
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'
},
2026-02-17 15:03:34 +00:00
'mempool-electrs' : {
title : 'Electrs' ,
icon : '/assets/img/app-icons/electrs.svg' ,
description : 'Electrum protocol indexer for Bitcoin'
},
2026-01-27 23:06:18 +00:00
'ollama' : {
title : 'Ollama' ,
2026-02-01 18:46:35 +00:00
icon : '/assets/img/app-icons/ollama.png' ,
2026-01-27 23:06:18 +00:00
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 ,
2026-03-06 01:11:00 +00:00
icon : '/assets/icon/pwa-192x192-v2.png' ,
2026-01-27 23:06:18 +00:00
description : ` ${ appId } application`
}
apps [ appId ] = {
title : metadata . title ,
version : '1.0.0' ,
status : isRunning ? 'running' : 'stopped' ,
state : isRunning ? 'running' : 'stopped' ,
2026-03-19 12:44:31 +00:00
health : isRunning ? 'healthy' : null ,
2026-01-27 23:06:18 +00:00
'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 {}
}
}
2026-01-24 22:59:20 +00:00
// 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 }
}
}
}
2026-03-09 17:09:59 +00:00
// 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' },
2026-03-13 23:40:29 +00:00
'photoprism' : { title : 'PhotoPrism' , shortDesc : 'AI-powered photo management' , icon : '/assets/img/app-icons/photoprism.svg' },
2026-03-09 17:09:59 +00:00
'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' },
2026-03-18 19:24:52 +00:00
'thunderhub' : { title : 'ThunderHub' , shortDesc : 'Lightning node management UI with channel management and payments' , icon : '/assets/img/app-icons/thunderhub.svg' },
2026-03-09 17:09:59 +00:00
'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' },
}
2026-01-24 22:59:20 +00:00
// Helper: Install package with container runtime (if available) or simulate
2026-03-09 17:09:59 +00:00
async function installPackage ( id , manifestUrl , opts = {}) {
2026-01-24 22:59:20 +00:00
console . log ( `[Package] 📦 Installing ${ id } ...` )
2026-03-09 17:09:59 +00:00
2026-01-24 22:59:20 +00:00
try {
// Check if already installed
if ( mockData [ 'package-data' ][ id ]) {
throw new Error ( `Package ${ id } is already installed` )
}
2026-03-09 17:09:59 +00:00
const version = opts . version || '0.1.0'
2026-01-24 22:59:20 +00:00
const runtime = await isContainerRuntimeAvailable ()
2026-03-09 17:09:59 +00:00
// 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`
2026-01-24 22:59:20 +00:00
}
2026-03-06 01:11:00 +00:00
2026-03-09 17:09:59 +00:00
// Determine port — use known mapping, or auto-assign a unique one
let assignedPort = portMappings [ id ]
if ( ! assignedPort ) {
while ( usedPorts . has ( nextAutoPort )) nextAutoPort ++
assignedPort = nextAutoPort ++
2026-01-24 22:59:20 +00:00
}
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 })
}
2026-03-09 17:09:59 +00:00
// Add to mock data using staticApp format for consistency
2026-01-24 22:59:20 +00:00
mockData [ 'package-data' ][ id ] = {
2026-03-09 17:09:59 +00:00
... staticApp ({
id ,
title : metadata . title ,
version ,
shortDesc : metadata . shortDesc ,
longDesc : metadata . shortDesc ,
state : 'running' ,
lanPort : assignedPort ,
icon : metadata . icon ,
}),
2026-01-24 22:59:20 +00:00
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 {
2026-02-18 08:30:12 +00:00
if ( staticDevApps [ id ]) {
throw new Error ( ` ${ id } is a demo app and cannot be uninstalled` )
}
2026-01-24 22:59:20 +00:00
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' : {
2026-03-09 13:03:53 +00:00
id : 'archipelago-demo' ,
2026-01-24 22:59:20 +00:00
version : '0.1.0' ,
2026-03-09 13:03:53 +00:00
name : 'Archipelago' ,
pubkey : 'a1b2c3d4e5f6789012345678901234567890abcdef1234567890abcdef123456' ,
2026-01-24 22:59:20 +00:00
'status-info' : {
restarting : false ,
'shutting-down' : false ,
updated : false ,
'backup-progress' : null ,
'update-progress' : null ,
},
2026-03-09 18:49:20 +00:00
'lan-address' : 'localhost' ,
2026-03-09 13:03:53 +00:00
'tor-address' : 'archydemox7k3pnw4hv5qz2jcbr6dwefys3ockqzf4mzjlvxot2ioad.onion' ,
unread : 3 ,
'wifi-ssids' : [ 'Home-5G' , 'Archipelago-Mesh' , 'Neighbors-Open' ],
'zram-enabled' : true ,
2026-01-24 22:59:20 +00:00
},
2026-03-09 13:03:53 +00:00
'package-data' : {}, // Will be populated from Docker + static apps
2026-01-24 22:59:20 +00:00
ui : {
name : 'Archipelago' ,
'ack-welcome' : '0.1.0' ,
marketplace : {
'selected-hosts' : [],
'known-hosts' : {},
},
theme : 'dark' ,
},
}
2026-03-09 13:03:53 +00:00
// 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 ,
2026-02-18 08:30:12 +00:00
'static-files' : {
2026-03-09 13:03:53 +00:00
license : license || 'MIT' ,
instructions : shortDesc ,
icon : icon || `/assets/img/app-icons/ ${ id } .png` ,
2026-02-18 08:30:12 +00:00
},
manifest : {
2026-03-09 13:03:53 +00:00
id ,
title ,
version ,
description : { short : shortDesc , long : longDesc || shortDesc },
'release-notes' : 'Latest stable release' ,
license : license || 'MIT' ,
2026-02-18 08:30:12 +00:00
'wrapper-repo' : '#' ,
'upstream-repo' : '#' ,
'support-site' : '#' ,
'marketing-site' : '#' ,
'donation-url' : null ,
interfaces : {
2026-03-09 13:03:53 +00:00
main : { name : 'Web Interface' , description : ` ${ title } web interface` , ui : true },
},
2026-02-18 08:30:12 +00:00
},
installed : {
'current-dependents' : {},
'current-dependencies' : {},
'last-backup' : null ,
'interface-addresses' : {
main : {
2026-03-09 13:03:53 +00:00
'tor-address' : torHost ? ` ${ torHost } .onion` : ` ${ id } .onion` ,
2026-03-09 18:49:20 +00:00
'lan-address' : lanPort ? `http://localhost: ${ lanPort } ` : '' ,
2026-03-09 13:03:53 +00:00
},
2026-02-18 08:30:12 +00:00
},
2026-03-09 13:03:53 +00:00
status : state ,
},
2026-02-18 08:30:12 +00:00
}
}
2026-03-09 13:03:53 +00:00
// Static dev apps (always shown in My Apps when using mock backend)
const staticDevApps = {
2026-06-11 00:24:40 -04:00
'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' ,
}),
2026-03-09 13:03:53 +00:00
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 ,
}),
2026-06-11 00:24:40 -04:00
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' ,
}),
2026-03-09 17:09:59 +00:00
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' ,
}),
2026-03-18 19:24:52 +00:00
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 ,
}),
2026-03-09 13:03:53 +00:00
}
2026-02-18 08:30:12 +00:00
function mergePackageData ( dockerApps ) {
return { ... dockerApps , ... staticDevApps }
}
2026-01-27 23:06:18 +00:00
// Initialize package data from Docker on startup
async function initializePackageData () {
console . log ( '[Docker] Querying running containers...' )
const dockerApps = await getDockerContainers ()
2026-02-18 08:30:12 +00:00
mockData [ 'package-data' ] = mergePackageData ( dockerApps )
2026-01-27 23:06:18 +00:00
2026-02-18 08:30:12 +00:00
const appCount = Object . keys ( mockData [ 'package-data' ]). length
const runningCount = Object . values ( mockData [ 'package-data' ]). filter ( app => app . state === 'running' ). length
2026-01-27 23:06:18 +00:00
console . log ( `[Docker] Found ${ appCount } containers ( ${ runningCount } running)` )
if ( appCount > 0 ) {
console . log ( '[Docker] Apps detected:' )
2026-02-18 08:30:12 +00:00
Object . entries ( mockData [ 'package-data' ]). forEach (([ id , app ]) => {
2026-01-27 23:06:18 +00:00
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.' )
}
}
2026-01-24 22:59:20 +00:00
// Handle CORS preflight
app . options ( '/rpc/v1' , ( req , res ) => {
res . status ( 200 ). end ()
})
2026-06-11 00:24:40 -04:00
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 } ` } })
}
})
2026-01-24 22:59:20 +00:00
// RPC endpoint
app . post ( '/rpc/v1' , ( req , res ) => {
const { method , params } = req . body
console . log ( `[RPC] ${ method } ` )
2026-03-17 23:56:37 +00:00
// 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)` )
}
}
2026-01-24 22:59:20 +00:00
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` )
2026-02-17 15:03:34 +00:00
return res . json ({ result : true })
2026-01-24 22:59:20 +00:00
}
case 'auth.isOnboardingComplete' : {
return res . json ({ result : userState . onboardingComplete })
}
2026-03-02 08:34:13 +00:00
case 'auth.resetOnboarding' : {
userState . onboardingComplete = false
console . log ( '[Auth] Onboarding reset' )
return res . json ({ result : true })
}
2026-02-17 15:03:34 +00:00
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' } })
}
2026-03-02 08:34:13 +00:00
case 'node.signChallenge' : {
const { challenge } = params || {}
const mockSig = Buffer . from ( `mock-sig- ${ challenge || 'challenge' } ` ). toString ( 'hex' )
return res . json ({ result : { signature : mockSig } })
}
2026-03-22 03:30:21 +00:00
case 'identity.verify' : {
return res . json ({ result : { valid : true } })
}
2026-03-31 01:41:24 +01:00
// 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 } })
}
2026-03-02 08:34:13 +00:00
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 (),
},
})
}
2026-01-24 22:59:20 +00:00
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 })
}
2026-03-13 23:40:29 +00:00
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 } })
}
2026-01-24 22:59:20 +00:00
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' : {
2026-03-09 13:03:53 +00:00
// Slightly randomize so the dashboard feels alive
2026-01-24 22:59:20 +00:00
return res . json ({
result : {
2026-03-09 13:03:53 +00:00
cpu : + ( 12 + Math . random () * 18 ). toFixed ( 1 ),
memory : + ( 58 + Math . random () * 8 ). toFixed ( 1 ),
disk : + ( 34 + Math . random () * 3 ). toFixed ( 1 ),
2026-01-24 22:59:20 +00:00
},
})
}
case 'marketplace.get' : {
const mockApps = [
{
id : 'bitcoin' ,
title : 'Bitcoin Core' ,
2026-03-09 13:03:53 +00:00
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' ,
2026-01-24 22:59:20 +00:00
license : 'MIT' ,
2026-03-09 13:03:53 +00:00
category : 'Bitcoin' ,
2026-01-24 22:59:20 +00:00
},
{
2026-03-09 13:03:53 +00:00
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' ,
2026-01-24 22:59:20 +00:00
license : 'MIT' ,
2026-03-09 13:03:53 +00:00
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.' ,
2026-03-18 19:24:52 +00:00
version : '0.10.0' ,
2026-03-09 13:03:53 +00:00
icon : '/assets/img/app-icons/fedimint.png' ,
author : 'Fedimint' ,
license : 'MIT' ,
category : 'Bitcoin' ,
},
2026-03-18 19:24:52 +00:00
{
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' ,
},
2026-03-09 13:03:53 +00:00
{
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' ,
2026-01-24 22:59:20 +00:00
},
]
2026-03-09 13:03:53 +00:00
2026-01-24 22:59:20 +00:00
return res . json ({ result : mockApps })
}
case 'server.update' :
case 'server.restart' :
case 'server.shutdown' : {
return res . json ({ result : 'ok' })
}
case 'package.install' : {
2026-03-09 17:09:59 +00:00
const { id , url , dockerImage , version } = params
installPackage ( id , url , { dockerImage , version }). catch ( err => {
2026-01-24 22:59:20 +00:00
console . error ( `[RPC] Installation failed:` , err . message )
})
2026-03-09 17:09:59 +00:00
2026-01-24 22:59:20 +00:00
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' })
}
2026-03-06 12:23:57 +00:00
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 } })
}
2026-03-09 13:03:53 +00:00
// =========================================================================
// 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 } })
}
2026-03-22 03:30:21 +00:00
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 ) } ` } })
}
2026-03-09 13:03:53 +00:00
// =========================================================================
// 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
// =========================================================================
2026-03-22 03:30:21 +00:00
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 } } },
],
},
})
}
2026-03-09 13:03:53 +00:00
case 'content.remove' : {
return res . json ({ result : { success : true } })
}
case 'content.set-pricing' : {
return res . json ({ result : { success : true } })
}
2026-03-22 03:30:21 +00:00
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 } })
}
2026-03-09 13:03:53 +00:00
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 } })
}
2026-03-22 03:30:21 +00:00
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 },
],
},
})
}
2026-03-09 13:03:53 +00:00
// =========================================================================
// Tor & Peer Networking
// =========================================================================
2026-03-22 03:30:21 +00:00
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 },
],
},
})
}
2026-03-09 13:03:53 +00:00
case 'node.tor-address' : {
return res . json ({
result : {
tor_address : 'archydemox7k3pnw4hv5qz2jcbr6dwefys3ockqzf4mzjlvxot2ioad.onion' ,
},
})
}
2026-03-08 00:27:34 +00:00
case 'node-list-peers' : {
2026-03-09 13:03:53 +00:00
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' : {
2026-03-18 19:24:52 +00:00
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 ,
},
],
})
2026-03-07 22:36:45 +00:00
}
2026-03-17 01:32:02 +00:00
// =====================================================================
// 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 } })
}
2026-03-17 00:03:08 +00:00
// =====================================================================
// Mesh Networking (LoRa radio via Meshcore)
// =====================================================================
case 'mesh.status' : {
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' ],
},
})
}
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 = [
2026-03-17 02:34:37 +00:00
{ 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...' } },
2026-03-17 00:03:08 +00:00
]
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 )
return res . json ({ result : { configured : true } })
}
2026-03-17 02:23:30 +00:00
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 ,
},
})
}
2026-03-22 03:30:21 +00:00
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 ,
},
})
}
2026-03-17 00:03:08 +00:00
// =====================================================================
// 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 } })
}
2026-03-18 21:06:14 +00:00
// =====================================================================
// 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 ,
2026-03-18 23:14:47 +00:00
block_height : walletState . block_height ,
2026-03-18 21:06:14 +00:00
synced_to_chain : true ,
synced_to_graph : true ,
version : '0.17.4-beta' ,
identity_pubkey : '03a1b2c3d4e5f6789012345678901234567890abcdef1234567890abcdef123456' ,
chains : [{ chain : 'bitcoin' , network : 'signet' }],
2026-03-18 23:14:47 +00:00
balance_sats : walletState . onchain_sats ,
channel_balance_sats : walletState . channel_sats ,
2026-03-18 21:06:14 +00:00
},
})
}
case 'lnd.gettransactions' : {
2026-03-18 23:14:47 +00:00
const pending = walletState . transactions . filter ( tx => tx . direction === 'incoming' && tx . num_confirmations < 3 ). length
2026-03-18 21:06:14 +00:00
return res . json ({
result : {
2026-03-18 23:14:47 +00:00
transactions : walletState . transactions ,
incoming_pending_count : pending ,
2026-03-18 21:06:14 +00:00
},
})
}
case 'lnd.channelbalance' : {
return res . json ({
result : {
2026-03-18 23:14:47 +00:00
local_balance : { sat : walletState . channel_sats },
2026-03-18 21:06:14 +00:00
remote_balance : { sat : 11750000 },
pending_open_local_balance : { sat : 500000 },
},
})
}
case 'lnd.walletbalance' : {
return res . json ({
result : {
2026-03-18 23:14:47 +00:00
total_balance : walletState . onchain_sats + 100000 ,
confirmed_balance : walletState . onchain_sats ,
2026-03-18 21:06:14 +00:00
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'
2026-03-18 23:14:47 +00:00
? 'tb1p' + randomHex ( 29 )
: 'tb1q' + randomHex ( 19 )
2026-03-18 21:06:14 +00:00
return res . json ({ result : { address : mockAddr } })
}
case 'lnd.addinvoice' :
case 'lnd.createinvoice' : {
const amt = params ? . amt || params ? . value || params ? . amount_sats || 1000
2026-03-18 23:14:47 +00:00
const rHash = randomHex ( 32 )
2026-03-18 21:06:14 +00:00
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 (),
2026-03-18 23:14:47 +00:00
payment_addr : randomHex ( 32 ),
2026-03-18 21:06:14 +00:00
},
})
}
case 'lnd.payinvoice' :
case 'lnd.sendpayment' : {
2026-03-18 23:14:47 +00:00
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 )
2026-03-18 21:06:14 +00:00
return res . json ({
result : {
2026-03-18 23:14:47 +00:00
payment_hash : randomHex ( 32 ),
payment_preimage : randomHex ( 32 ),
2026-03-18 21:06:14 +00:00
status : 'SUCCEEDED' ,
2026-03-18 23:14:47 +00:00
fee_sat : fee ,
value_sat : amt ,
2026-03-18 21:06:14 +00:00
},
})
}
case 'lnd.sendcoins' : {
const amt = params ? . amount || params ? . amt || 50000
2026-03-18 23:14:47 +00:00
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 || '' ],
})
2026-03-18 21:06:14 +00:00
return res . json ({
2026-03-18 23:14:47 +00:00
result : { txid , amount : amt },
2026-03-18 21:06:14 +00:00
})
}
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 : {
2026-03-18 23:14:47 +00:00
balance_sats : walletState . ecash_sats ,
balance_msat : walletState . ecash_sats * 1000 ,
token_count : walletState . ecash_tokens ,
2026-03-18 21:06:14 +00:00
federations : [
2026-03-18 23:14:47 +00:00
{ federation_id : 'fed1-demo' , name : 'Archy Signet Mint' , balance_msat : walletState . ecash_sats * 1000 , gateway_active : true },
2026-03-18 21:06:14 +00:00
],
},
})
}
case 'wallet.ecash-send' : {
const amt = params ? . amount_sats || 1000
2026-03-18 23:14:47 +00:00
walletState . ecash_sats = Math . max ( 0 , walletState . ecash_sats - amt )
walletState . ecash_tokens = Math . max ( 0 , walletState . ecash_tokens - 1 )
2026-03-18 21:06:14 +00:00
return res . json ({
result : {
token : `cashuSend_mock_ ${ amt } _ ${ Date . now (). toString ( 36 ) } ` ,
amount_sats : amt ,
},
})
}
case 'wallet.ecash-receive' : {
2026-03-18 23:14:47 +00:00
const amt = 5000
walletState . ecash_sats += amt
walletState . ecash_tokens += 1
2026-03-18 21:06:14 +00:00
return res . json ({
result : {
2026-03-18 23:14:47 +00:00
amount_sats : amt ,
2026-03-18 21:06:14 +00:00
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
2026-03-18 23:14:47 +00:00
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` )
2026-03-18 21:06:14 +00:00
return res . json ({
result : {
2026-03-18 23:14:47 +00:00
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` ,
2026-03-18 21:06:14 +00:00
},
})
}
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' ,
},
})
}
// =====================================================================
2026-03-18 23:14:47 +00:00
// 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 } })
}
2026-03-22 03:30:21 +00:00
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 } })
}
2026-03-18 23:14:47 +00:00
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 (),
}})
}
2026-03-18 21:06:14 +00:00
// System / Network / Updates
// =====================================================================
case 'system.stats' : {
return res . json ({
result : {
2026-03-18 23:14:47 +00:00
cpu_usage_percent : + ( 12 + Math . random () * 18 ). toFixed ( 1 ),
2026-03-18 21:06:14 +00:00
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 '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 () },
],
},
})
}
2026-04-11 13:35:52 +01:00
// ── 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 } })
}
2026-01-24 22:59:20 +00:00
default : {
2026-03-07 22:36:45 +00:00
console . log ( `[RPC] Unknown method: ${ method } ` )
2026-01-24 22:59:20 +00:00
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 ,
},
})
}
})
2026-03-07 22:50:05 +00:00
// =============================================================================
// 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 ),
}
2026-03-09 19:32:28 +00:00
// 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>` )
})
2026-03-07 22:50:05 +00:00
// 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 },
})
})
2026-03-09 18:12:28 +00:00
// 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 )
})
2026-03-07 22:50:05 +00:00
// 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' )
}
})
2026-03-07 23:07:38 +00:00
// Claude API Proxy (reads ANTHROPIC_API_KEY from environment)
2026-03-09 13:03:53 +00:00
// Uses fetch (Node 22+) for reliable DNS resolution and streaming in Docker/Alpine
2026-03-07 23:07:38 +00:00
// =============================================================================
2026-03-09 13:03:53 +00:00
app . post ( '/aiui/api/claude/*' , async ( req , res ) => {
2026-03-07 23:07:38 +00:00
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 ]
2026-03-07 23:22:30 +00:00
2026-03-08 00:21:38 +00:00
// Clean request body for Anthropic API
2026-03-07 23:22:30 +00:00
const body = req . body
2026-03-08 00:21:38 +00:00
if ( body ) {
if ( ! body . max_tokens ) body . max_tokens = 4096
2026-03-08 00:27:34 +00:00
// 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 ]
}
2026-03-08 00:21:38 +00:00
// Strip AIUI-specific fields that Anthropic API rejects
delete body . webSearch
delete body . webResults
delete body . context
2026-03-07 23:22:30 +00:00
}
const bodyStr = JSON . stringify ( body )
2026-03-09 13:03:53 +00:00
const url = `https://api.anthropic.com ${ apiPath } `
console . log ( `[Claude Proxy] → POST ${ url } ( ${ bodyStr . length } bytes, model: ${ body ? . model || 'unknown' } )` )
2026-03-07 23:07:38 +00:00
2026-03-09 13:03:53 +00:00
try {
const controller = new AbortController ()
const timeout = setTimeout (() => controller . abort (), 60000 )
2026-03-07 23:07:38 +00:00
2026-03-09 13:03:53 +00:00
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 ,
})
2026-03-07 23:07:38 +00:00
2026-03-09 13:03:53 +00:00
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 } ` )
2026-03-07 23:07:38 +00:00
if ( ! res . headersSent ) {
res . status ( 502 ). json ({
type : 'error' ,
2026-03-07 23:58:08 +00:00
error : { type : 'proxy_error' , message : msg }
2026-03-07 23:07:38 +00:00
})
}
2026-03-09 13:03:53 +00:00
}
2026-03-07 23:07:38 +00:00
})
2026-03-08 01:48:23 +00:00
// 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 ()
})
2026-03-07 23:24:27 +00:00
// Web search stub (no search engine configured in demo)
app . get ( '/api/web-search' , ( req , res ) => {
2026-03-07 23:58:08 +00:00
res . json ({ results : [] })
2026-03-07 23:24:27 +00:00
})
2026-03-07 23:58:08 +00:00
// 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' })
})
2026-03-07 23:24:27 +00:00
app . all ( '/aiui/api/*' , ( req , res ) => {
2026-03-07 23:58:08 +00:00
res . status ( 404 ). json ({ error : 'Not available in demo mode' })
})
2026-03-18 21:06:14 +00:00
// =============================================================================
// 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 } — block ${ d . info . block_height . toLocaleString () } — ${ 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 — Archipelago Dev Mode — 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 ))
2026-01-24 22:59:20 +00:00
// 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 )
2026-03-07 22:36:45 +00:00
clearInterval ( heartbeatInterval )
2026-01-24 22:59:20 +00:00
}
} else {
clearInterval ( pingInterval )
2026-03-07 22:36:45 +00:00
clearInterval ( heartbeatInterval )
2026-01-24 22:59:20 +00:00
}
}, 30000 ) // Ping every 30 seconds
2026-03-07 22:36:45 +00:00
// Send periodic heartbeat data so clients don't think the connection is dead
const heartbeatInterval = setInterval (() => {
if ( ws . readyState === 1 ) {
try {
2026-03-07 23:22:30 +00:00
ws . send ( JSON . stringify ({ type : 'heartbeat' , rev : Date . now () }))
2026-03-07 22:36:45 +00:00
} catch { /* ignore */ }
}
}, 45000 ) // Every 45s (client expects data within 60s)
2026-01-24 22:59:20 +00:00
// 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 )
2026-03-07 22:36:45 +00:00
clearInterval ( heartbeatInterval )
2026-01-24 22:59:20 +00:00
wsClients . delete ( ws )
})
ws . on ( 'error' , ( error ) => {
console . error ( '[WebSocket Error]' , error )
clearInterval ( pingInterval )
2026-03-07 22:36:45 +00:00
clearInterval ( heartbeatInterval )
2026-01-24 22:59:20 +00:00
wsClients . delete ( ws )
})
})
server . listen ( PORT , '0.0.0.0' , async () => {
const runtime = await isContainerRuntimeAvailable ()
2026-01-27 23:06:18 +00:00
// Initialize package data from Docker
await initializePackageData ()
2026-01-24 22:59:20 +00:00
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 ) } ║
║ ║
2026-03-18 21:06:14 +00:00
║ Container Runtime: ${ runtime . available ? `✅ ${ runtime . runtime } ` . padEnd ( 40 ) : '⏭️ Simulation mode' . padEnd ( 40 ) } ║
2026-03-09 13:03:53 +00:00
║ Claude API Key: ${ process . env . ANTHROPIC_API_KEY ? '✅ Set (' + process . env . ANTHROPIC_API_KEY . slice ( 0 , 12 ) + '...)' : '❌ Not set (chat disabled)' . padEnd ( 40 ) } ║
2026-01-24 22:59:20 +00:00
║ ║
╚════════════════════════════════════════════════════════════╝
` )
console . log ( 'Mock backend is running. Press Ctrl+C to stop.\n' )
2026-03-09 13:03:53 +00:00
// 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 */ }
}
2026-01-27 23:06:18 +00:00
2026-02-18 08:30:12 +00:00
// Periodically update package data from Docker (merge with static dev apps)
2026-03-07 20:53:02 +00:00
// 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
}
2026-01-24 22:59:20 +00:00
})
process . on ( 'SIGINT' , () => {
console . log ( '\n\nShutting down mock backend...' )
server . close (() => {
console . log ( 'Server stopped.' )
process . exit ( 0 )
})
})