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-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 ,
'onlyoffice' : 8044 ,
'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-onlyoffice' : 'onlyoffice' ,
'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'
},
'onlyoffice' : {
title : 'OnlyOffice' ,
icon : '/assets/img/onlyoffice.webp' ,
description : 'Office suite and document collaboration'
},
'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' },
'onlyoffice' : { title : 'OnlyOffice' , shortDesc : 'Office suite for document collaboration' , icon : '/assets/img/app-icons/onlyoffice.webp' },
'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 = {
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-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 ()
})
// 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: ' 03 a1b2c3d4e5f6789012345678901234567890abcdef1234567890abcdef123456 ',
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: ' 02778 f4a ', capacity: 5000000, local_balance: 2450000, remote_balance: 2550000, active: true, peer_alias: ' ACINQ Signet ' },
{ chan_id: ' 840921088114689 ', remote_pubkey: ' 03 abcdef ', capacity: 2000000, local_balance: 1200000, remote_balance: 800000, active: true, peer_alias: ' WalletOfSatoshi ' },
{ chan_id: ' 840921088114690 ', remote_pubkey: ' 02 fedcba ', 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: ' 03 a1b2c3d4e5f6789012345678901234567890abcdef1234567890abcdef123456 ',
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: ' 000000000000000000000000000000000000000000000000000000000001 a2b3 ',
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 2 x in 1 h ', 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 2 x in 1 h ', 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-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 - 01 T10 : 00 : 00 Z ', isDir: true, type: '' },
{ name: ' Documents ', path: ' / Documents ', size: 0, modified: ' 2025 - 02 - 28 T14 : 30 : 00 Z ', isDir: true, type: '' },
{ name: ' Photos ', path: ' / Photos ', size: 0, modified: ' 2025 - 02 - 20 T09 : 15 : 00 Z ', isDir: true, type: '' },
{ name: ' Videos ', path: ' / Videos ', size: 0, modified: ' 2025 - 01 - 15 T18 : 00 : 00 Z ', isDir: true, type: '' },
],
' / Music ': [
{ name: ' Bad Actors Reveal . mp3 ', path: ' / Music / Bad Actors Reveal . mp3 ', size: 8_400_000, modified: ' 2025 - 01 - 10 T12 : 00 : 00 Z ', isDir: false, type: ' audio ' },
{ name: ' Architects of Tomorrow . wav ', path: ' / Music / Architects of Tomorrow . wav ', size: 42_000_000, modified: ' 2025 - 01 - 08 T15 : 00 : 00 Z ', isDir: false, type: ' audio ' },
{ name: ' Sats or Shackles . mp3 ', path: ' / Music / Sats or Shackles . mp3 ', size: 6_200_000, modified: ' 2024 - 12 - 20 T10 : 00 : 00 Z ', 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 - 15 T11 : 00 : 00 Z ', isDir: false, type: ' audio ' },
{ name: ' Inverse Dylan ( Remix ). mp3 ', path: ' / Music / Inverse Dylan ( Remix ). mp3 ', size: 5_600_000, modified: ' 2024 - 12 - 10 T16 : 00 : 00 Z ', isDir: false, type: ' audio ' },
{ name: ' Hootcoiner . mp3 ', path: ' / Music / Hootcoiner . mp3 ', size: 4_200_000, modified: ' 2024 - 11 - 28 T09 : 00 : 00 Z ', isDir: false, type: ' audio ' },
{ name: ' decentrealisation . mp3 ', path: ' / Music / decentrealisation . mp3 ', size: 5_100_000, modified: ' 2024 - 11 - 20 T14 : 00 : 00 Z ', isDir: false, type: ' audio ' },
{ name: ' neo - morality . mp3 ', path: ' / Music / neo - morality . mp3 ', size: 6_800_000, modified: ' 2024 - 11 - 15 T11 : 00 : 00 Z ', isDir: false, type: ' audio ' },
{ name: ' death is a gift . mp3 ', path: ' / Music / death is a gift . mp3 ', size: 4_500_000, modified: ' 2024 - 11 - 10 T08 : 00 : 00 Z ', isDir: false, type: ' audio ' },
{ name: ' Wash the fucking dishes . mp3 ', path: ' / Music / Wash the fucking dishes . mp3 ', size: 3_900_000, modified: ' 2024 - 11 - 05 T13 : 00 : 00 Z ', 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 - 28 T10 : 00 : 00 Z ', isDir: false, type: ' audio ' },
{ name: ' Builders not talkers . mp3 ', path: ' / Music / Builders not talkers . mp3 ', size: 4_700_000, modified: ' 2024 - 10 - 20 T15 : 00 : 00 Z ', isDir: false, type: ' audio ' },
{ name: ' SMRI . mp3 ', path: ' / Music / SMRI . mp3 ', size: 5_900_000, modified: ' 2024 - 10 - 15 T12 : 00 : 00 Z ', isDir: false, type: ' audio ' },
{ name: ' Shadrap . mp3 ', path: ' / Music / Shadrap . mp3 ', size: 3_400_000, modified: ' 2024 - 10 - 10 T09 : 00 : 00 Z ', isDir: false, type: ' audio ' },
{ name: ' The Wehrman . mp3 ', path: ' / Music / The Wehrman . mp3 ', size: 6_100_000, modified: ' 2024 - 10 - 05 T14 : 00 : 00 Z ', isDir: false, type: ' audio ' },
{ name: ' An Exploited Substrate . mp3 ', path: ' / Music / An Exploited Substrate . mp3 ', size: 4_800_000, modified: ' 2024 - 09 - 28 T11 : 00 : 00 Z ', isDir: false, type: ' audio ' },
{ name: ' Govcucks . wav ', path: ' / Music / Govcucks . wav ', size: 38_000_000, modified: ' 2024 - 09 - 20 T16 : 00 : 00 Z ', isDir: false, type: ' audio ' },
],
' / Documents ': [
{ name: ' bitcoin - whitepaper - notes . md ', path: ' / Documents / bitcoin - whitepaper - notes . md ', size: 820, modified: ' 2025 - 02 - 28 T14 : 30 : 00 Z ', isDir: false, type: ' text ' },
{ name: ' node - setup - checklist . md ', path: ' / Documents / node - setup - checklist . md ', size: 950, modified: ' 2025 - 02 - 25 T10 : 00 : 00 Z ', isDir: false, type: ' text ' },
{ name: ' lightning - channels . csv ', path: ' / Documents / lightning - channels . csv ', size: 680, modified: ' 2025 - 02 - 20 T16 : 00 : 00 Z ', isDir: false, type: ' text ' },
{ name: ' sovereignty - manifesto . txt ', path: ' / Documents / sovereignty - manifesto . txt ', size: 1100, modified: ' 2025 - 02 - 15 T12 : 00 : 00 Z ', isDir: false, type: ' text ' },
{ name: ' backup - log . json ', path: ' / Documents / backup - log . json ', size: 1450, modified: ' 2025 - 03 - 01 T02 : 00 : 00 Z ', isDir: false, type: ' text ' },
],
' / Photos ': [
{ name: ' node - rack - setup . jpg ', path: ' / Photos / node - rack - setup . jpg ', size: 2_400_000, modified: ' 2025 - 02 - 20 T09 : 15 : 00 Z ', isDir: false, type: ' image ' },
{ name: ' bitcoin - conference - 2024. jpg ', path: ' / Photos / bitcoin - conference - 2024. jpg ', size: 3_100_000, modified: ' 2024 - 12 - 15 T14 : 30 : 00 Z ', isDir: false, type: ' image ' },
{ name: ' lightning - network - visualization . png ', path: ' / Photos / lightning - network - visualization . png ', size: 1_800_000, modified: ' 2025 - 01 - 10 T11 : 00 : 00 Z ', isDir: false, type: ' image ' },
{ name: ' home - server - build . jpg ', path: ' / Photos / home - server - build . jpg ', size: 2_900_000, modified: ' 2024 - 11 - 20 T16 : 45 : 00 Z ', isDir: false, type: ' image ' },
{ name: ' sunset - from - balcony . jpg ', path: ' / Photos / sunset - from - balcony . jpg ', size: 4_200_000, modified: ' 2025 - 02 - 14 T18 : 30 : 00 Z ', isDir: false, type: ' image ' },
],
' / Videos ': [
{ name: ' node - unboxing - timelapse . mp4 ', path: ' / Videos / node - unboxing - timelapse . mp4 ', size: 85_000_000, modified: ' 2024 - 11 - 01 T10 : 00 : 00 Z ', isDir: false, type: ' video ' },
{ name: ' bitcoin - explained - 5 min . mp4 ', path: ' / Videos / bitcoin - explained - 5 min . mp4 ', size: 42_000_000, modified: ' 2024 - 10 - 15 T14 : 00 : 00 Z ', isDir: false, type: ' video ' },
{ name: ' lightning - payment - demo . mp4 ', path: ' / Videos / lightning - payment - demo . mp4 ', size: 28_000_000, modified: ' 2025 - 01 - 20 T12 : 00 : 00 Z ', 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 - 01 T02 : 00 : 00 Z ', 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 )
})
})