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-01-27 23:06:18 +00:00
const docker = new Docker ()
2026-01-24 22:59:20 +00:00
const app = express ()
const PORT = 5959
// Dev mode from environment (setup, onboarding, existing, or default)
const DEV_MODE = process . env . VITE_DEV_MODE || 'default'
// 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'
// User state (simulated file-based storage)
let userState = {
setupComplete : false ,
onboardingComplete : false ,
passwordHash : null , // In real app, this would be bcrypt hash
}
// 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
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 ,
'fedimint' : 8174 ,
'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 () {
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' ,
'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' },
'photoprism' : { title : 'PhotoPrism' , shortDesc : 'AI-powered photo management' , icon : '/assets/img/app-icons/photoprims.svg' },
'immich' : { title : 'Immich' , shortDesc : 'High-performance photo and video backup' , icon : '/assets/img/app-icons/immich.png' },
'filebrowser' : { title : 'File Browser' , shortDesc : 'Web-based file manager' , icon : '/assets/img/app-icons/file-browser.webp' },
'nginx-proxy-manager' : { title : 'Nginx Proxy Manager' , shortDesc : 'Easy proxy management with SSL' , icon : '/assets/img/app-icons/nginx.svg' },
'portainer' : { title : 'Portainer' , shortDesc : 'Container management UI' , icon : '/assets/img/app-icons/portainer.webp' },
'uptime-kuma' : { title : 'Uptime Kuma' , shortDesc : 'Self-hosted monitoring tool' , icon : '/assets/img/app-icons/uptime-kuma.webp' },
'tailscale' : { title : 'Tailscale' , shortDesc : 'Zero-config VPN for secure remote access' , icon : '/assets/img/app-icons/tailscale.webp' },
'fedimint' : { title : 'Fedimint' , shortDesc : 'Federated Bitcoin mint with Guardian UI' , icon : '/assets/img/app-icons/fedimint.png' },
'indeedhub' : { title : 'Indeehub' , shortDesc : 'Bitcoin documentary streaming platform' , icon : '/assets/img/app-icons/indeedhub.png' },
'dwn' : { title : 'Decentralized Web Node' , shortDesc : 'Store and sync personal data with DID-based access' , icon : '/assets/img/app-icons/dwn.svg' },
'nostr-rs-relay' : { title : 'Nostr Relay' , shortDesc : 'Run your own Nostr relay' , icon : '/assets/img/app-icons/nostr-rs-relay.svg' },
'syncthing' : { title : 'Syncthing' , shortDesc : 'Peer-to-peer file synchronization' , icon : '/assets/img/app-icons/syncthing.png' },
'tor' : { title : 'Tor' , shortDesc : 'Anonymous communication over the Tor network' , icon : '/assets/img/app-icons/tor.png' },
'atob' : { title : 'A to B Bitcoin' , shortDesc : 'Bitcoin tools for seamless transactions' , icon : '/assets/img/atob.png' },
'k484' : { title : 'K484' , shortDesc : 'Point of Sale and Admin system' , icon : '/assets/img/k484.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-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 } ` )
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 } })
}
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 })
}
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.' ,
version : '0.4.3' ,
icon : '/assets/img/app-icons/fedimint.png' ,
author : 'Fedimint' ,
license : 'MIT' ,
category : 'Bitcoin' ,
},
{
id : 'vaultwarden' ,
title : 'Vaultwarden' ,
description : 'Self-hosted password manager compatible with Bitwarden clients. Keep your credentials under your own roof.' ,
version : '1.32.5' ,
icon : '/assets/img/app-icons/vaultwarden.png' ,
author : 'Vaultwarden' ,
license : 'AGPL-3.0' ,
category : 'Privacy' ,
},
{
id : 'nextcloud' ,
title : 'Nextcloud' ,
description : 'Your personal cloud — files, calendar, contacts, and collaboration. Replace Google Drive and Dropbox entirely.' ,
version : '29.0.0' ,
icon : '/assets/img/app-icons/nextcloud.png' ,
author : 'Nextcloud GmbH' ,
license : 'AGPL-3.0' ,
category : 'Cloud' ,
},
{
id : 'nostr-relay' ,
title : 'Nostr Relay' ,
description : 'Run your own Nostr relay — sovereign social networking. Publish notes, follow friends, no censorship possible.' ,
version : '0.34.0' ,
icon : '/assets/img/app-icons/nostr-relay.png' ,
author : 'nostr-rs-relay' ,
license : 'MIT' ,
category : 'Social' ,
},
{
id : 'home-assistant' ,
title : 'Home Assistant' ,
description : 'Open-source home automation — control lights, locks, cameras, and sensors. Smart home without the cloud dependency.' ,
version : '2024.12.0' ,
icon : '/assets/img/app-icons/home-assistant.png' ,
author : 'Home Assistant' ,
license : 'Apache-2.0' ,
category : 'IoT' ,
},
{
id : 'syncthing' ,
title : 'Syncthing' ,
description : 'Continuous peer-to-peer file synchronization. Sync folders across devices without any cloud service.' ,
version : '1.28.1' ,
icon : '/assets/img/app-icons/syncthing.png' ,
author : 'Syncthing Foundation' ,
license : 'MPL-2.0' ,
category : 'Cloud' ,
},
{
id : 'tor' ,
title : 'Tor' ,
description : 'Anonymous communication — route traffic through the Tor network. Access your node from anywhere, privately.' ,
version : '0.4.8.13' ,
icon : '/assets/img/app-icons/tor.png' ,
author : 'Tor Project' ,
license : 'BSD-3' ,
category : 'Privacy' ,
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 } })
}
// =========================================================================
// Nostr
// =========================================================================
case 'nostr.add-relay' : {
const { url } = params || {}
console . log ( `[Nostr] Added relay: ${ url } ` )
return res . json ({ result : { success : true } })
}
case 'nostr.remove-relay' : {
return res . json ({ result : { success : true } })
}
case 'nostr.toggle-relay' : {
return res . json ({ result : { success : true } })
}
// =========================================================================
// Content & Network
// =========================================================================
case 'content.remove' : {
return res . json ({ result : { success : true } })
}
case 'content.set-pricing' : {
return res . json ({ result : { success : true } })
}
case 'network.accept-request' : {
return res . json ({ result : { success : true } })
}
case 'network.reject-request' : {
return res . json ({ result : { success : true } })
}
// =========================================================================
// Server & Auth extras
// =========================================================================
case 'server.health' : {
return res . json ({
result : {
status : 'healthy' ,
uptime : Math . floor ( process . uptime ()),
services : {
backend : 'running' ,
nginx : 'running' ,
podman : 'running' ,
tor : 'running' ,
},
},
})
}
case 'auth.changePassword' : {
const { currentPassword } = params || {}
if ( currentPassword !== userState . passwordHash && currentPassword !== MOCK_PASSWORD ) {
return res . json ({ error : { code : - 32603 , message : 'Current password is incorrect' } })
}
userState . passwordHash = params . newPassword
console . log ( '[Auth] Password changed' )
return res . json ({ result : { success : true } })
}
// =========================================================================
// Router (port forwarding)
// =========================================================================
case 'router.add-forward' : {
console . log ( `[Router] Added forward: ${ JSON . stringify ( params ) } ` )
return res . json ({ result : { success : true , id : `fwd- ${ Date . now () } ` } })
}
case 'router.remove-forward' : {
return res . json ({ result : { success : true } })
}
// =========================================================================
// Tor & Peer Networking
// =========================================================================
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-07 22:36:45 +00:00
return res . json ({ result : [] })
}
2026-01-24 22:59:20 +00:00
default : {
2026-03-07 22:36:45 +00:00
console . log ( `[RPC] Unknown method: ${ method } ` )
2026-01-24 22:59:20 +00:00
return res . json ({
error : {
code : - 32601 ,
message : `Method not found: ${ method } ` ,
},
})
}
}
} catch ( error ) {
console . error ( '[RPC Error]' , error )
return res . json ({
error : {
code : - 32603 ,
message : error . message ,
},
})
}
})
2026-03-07 22:50:05 +00:00
// =============================================================================
// Mock FileBrowser API (for Cloud page in demo/Docker deployments)
// =============================================================================
const MOCK_FILES = {
'/' : [
{ name : 'Music' , path : '/Music' , size : 0 , modified : '2025-03-01T10:00:00Z' , isDir : true , type : '' },
{ name : 'Documents' , path : '/Documents' , size : 0 , modified : '2025-02-28T14:30:00Z' , isDir : true , type : '' },
{ name : 'Photos' , path : '/Photos' , size : 0 , modified : '2025-02-20T09:15:00Z' , isDir : true , type : '' },
{ name : 'Videos' , path : '/Videos' , size : 0 , modified : '2025-01-15T18:00:00Z' , isDir : true , type : '' },
],
'/Music' : [
{ name : 'Bad Actors Reveal.mp3' , path : '/Music/Bad Actors Reveal.mp3' , size : 8_400_000 , modified : '2025-01-10T12:00:00Z' , isDir : false , type : 'audio' },
{ name : 'Architects of Tomorrow.wav' , path : '/Music/Architects of Tomorrow.wav' , size : 42_000_000 , modified : '2025-01-08T15:00:00Z' , isDir : false , type : 'audio' },
{ name : 'Sats or Shackles.mp3' , path : '/Music/Sats or Shackles.mp3' , size : 6_200_000 , modified : '2024-12-20T10:00:00Z' , isDir : false , type : 'audio' },
{ name : 'The Four Horseman of Technocracy.mp3' , path : '/Music/The Four Horseman of Technocracy.mp3' , size : 7_800_000 , modified : '2024-12-15T11:00:00Z' , isDir : false , type : 'audio' },
{ name : 'Inverse Dylan (Remix).mp3' , path : '/Music/Inverse Dylan (Remix).mp3' , size : 5_600_000 , modified : '2024-12-10T16:00:00Z' , isDir : false , type : 'audio' },
{ name : 'Hootcoiner.mp3' , path : '/Music/Hootcoiner.mp3' , size : 4_200_000 , modified : '2024-11-28T09:00:00Z' , isDir : false , type : 'audio' },
{ name : 'decentrealisation.mp3' , path : '/Music/decentrealisation.mp3' , size : 5_100_000 , modified : '2024-11-20T14:00:00Z' , isDir : false , type : 'audio' },
{ name : 'neo-morality.mp3' , path : '/Music/neo-morality.mp3' , size : 6_800_000 , modified : '2024-11-15T11:00:00Z' , isDir : false , type : 'audio' },
{ name : 'death is a gift.mp3' , path : '/Music/death is a gift.mp3' , size : 4_500_000 , modified : '2024-11-10T08:00:00Z' , isDir : false , type : 'audio' },
{ name : 'Wash the fucking dishes.mp3' , path : '/Music/Wash the fucking dishes.mp3' , size : 3_900_000 , modified : '2024-11-05T13:00:00Z' , isDir : false , type : 'audio' },
{ name : 'All the leaves are brown.mp3' , path : '/Music/All the leaves are brown.mp3' , size : 5_300_000 , modified : '2024-10-28T10:00:00Z' , isDir : false , type : 'audio' },
{ name : 'Builders not talkers.mp3' , path : '/Music/Builders not talkers.mp3' , size : 4_700_000 , modified : '2024-10-20T15:00:00Z' , isDir : false , type : 'audio' },
{ name : 'SMRI.mp3' , path : '/Music/SMRI.mp3' , size : 5_900_000 , modified : '2024-10-15T12:00:00Z' , isDir : false , type : 'audio' },
{ name : 'Shadrap.mp3' , path : '/Music/Shadrap.mp3' , size : 3_400_000 , modified : '2024-10-10T09:00:00Z' , isDir : false , type : 'audio' },
{ name : 'The Wehrman.mp3' , path : '/Music/The Wehrman.mp3' , size : 6_100_000 , modified : '2024-10-05T14:00:00Z' , isDir : false , type : 'audio' },
{ name : 'An Exploited Substrate.mp3' , path : '/Music/An Exploited Substrate.mp3' , size : 4_800_000 , modified : '2024-09-28T11:00:00Z' , isDir : false , type : 'audio' },
{ name : 'Govcucks.wav' , path : '/Music/Govcucks.wav' , size : 38_000_000 , modified : '2024-09-20T16:00:00Z' , isDir : false , type : 'audio' },
],
'/Documents' : [
{ name : 'bitcoin-whitepaper-notes.md' , path : '/Documents/bitcoin-whitepaper-notes.md' , size : 820 , modified : '2025-02-28T14:30:00Z' , isDir : false , type : 'text' },
{ name : 'node-setup-checklist.md' , path : '/Documents/node-setup-checklist.md' , size : 950 , modified : '2025-02-25T10:00:00Z' , isDir : false , type : 'text' },
{ name : 'lightning-channels.csv' , path : '/Documents/lightning-channels.csv' , size : 680 , modified : '2025-02-20T16:00:00Z' , isDir : false , type : 'text' },
{ name : 'sovereignty-manifesto.txt' , path : '/Documents/sovereignty-manifesto.txt' , size : 1100 , modified : '2025-02-15T12:00:00Z' , isDir : false , type : 'text' },
{ name : 'backup-log.json' , path : '/Documents/backup-log.json' , size : 1450 , modified : '2025-03-01T02:00:00Z' , isDir : false , type : 'text' },
],
'/Photos' : [
{ name : 'node-rack-setup.jpg' , path : '/Photos/node-rack-setup.jpg' , size : 2_400_000 , modified : '2025-02-20T09:15:00Z' , isDir : false , type : 'image' },
{ name : 'bitcoin-conference-2024.jpg' , path : '/Photos/bitcoin-conference-2024.jpg' , size : 3_100_000 , modified : '2024-12-15T14:30:00Z' , isDir : false , type : 'image' },
{ name : 'lightning-network-visualization.png' , path : '/Photos/lightning-network-visualization.png' , size : 1_800_000 , modified : '2025-01-10T11:00:00Z' , isDir : false , type : 'image' },
{ name : 'home-server-build.jpg' , path : '/Photos/home-server-build.jpg' , size : 2_900_000 , modified : '2024-11-20T16:45:00Z' , isDir : false , type : 'image' },
{ name : 'sunset-from-balcony.jpg' , path : '/Photos/sunset-from-balcony.jpg' , size : 4_200_000 , modified : '2025-02-14T18:30:00Z' , isDir : false , type : 'image' },
],
'/Videos' : [
{ name : 'node-unboxing-timelapse.mp4' , path : '/Videos/node-unboxing-timelapse.mp4' , size : 85_000_000 , modified : '2024-11-01T10:00:00Z' , isDir : false , type : 'video' },
{ name : 'bitcoin-explained-5min.mp4' , path : '/Videos/bitcoin-explained-5min.mp4' , size : 42_000_000 , modified : '2024-10-15T14:00:00Z' , isDir : false , type : 'video' },
{ name : 'lightning-payment-demo.mp4' , path : '/Videos/lightning-payment-demo.mp4' , size : 28_000_000 , modified : '2025-01-20T12:00:00Z' , isDir : false , type : 'video' },
],
}
const MOCK_FILE_CONTENTS = {
'/Documents/bitcoin-whitepaper-notes.md' : `# Bitcoin Whitepaper Notes\n\n## Key Concepts\n\n### Peer-to-Peer Electronic Cash\n- No trusted third party needed\n- Double-spending solved via proof-of-work\n- Longest chain = truth\n\n### Proof of Work\n- SHA-256 based hashing\n- Difficulty adjusts every 2016 blocks (~2 weeks)\n- Incentive: block reward + transaction fees\n\n## My Thoughts\n- The 21M supply cap is genius - digital scarcity\n- Lightning Network solves the scaling concern\n- Self-custody is the whole point` ,
'/Documents/node-setup-checklist.md' : `# Archipelago Node Setup Checklist\n\n## Hardware\n- [x] Intel NUC / Mini PC (16GB RAM minimum)\n- [x] 2TB NVMe SSD\n- [x] USB drive for installer\n- [x] Ethernet cable\n\n## Core Apps\n- [x] Bitcoin Knots\n- [x] LND\n- [x] Mempool Explorer\n- [ ] BTCPay Server\n- [ ] Fedimint` ,
'/Documents/lightning-channels.csv' : `channel_id,peer_alias,capacity_sats,local_balance,remote_balance,status\nch_001,ACINQ,5000000,2450000,2550000,active\nch_002,WalletOfSatoshi,2000000,1200000,800000,active\nch_003,Voltage,10000000,4500000,5500000,active\nch_004,Kraken,3000000,1800000,1200000,active` ,
'/Documents/sovereignty-manifesto.txt' : `THE SOVEREIGNTY MANIFESTO\n=========================\n\nWe hold these truths to be self-evident:\n\n1. Your data belongs to you.\n2. Your money should be uncensorable.\n3. Your communications should be private.\n4. Your compute should be sovereign.\n5. Your identity should be self-issued.\n\nRun your own node. Hold your own keys. Own your own data. Be sovereign.` ,
'/Documents/backup-log.json' : JSON . stringify ({ backups : [{ id : 'bkp-2025-03-01' , timestamp : '2025-03-01T02:00:00Z' , type : 'full' , apps : [ 'bitcoin-knots' , 'lnd' , 'mempool' ], size_mb : 2340 , status : 'success' }] }, null , 2 ),
}
2026-03-09 19:32:28 +00:00
// FileBrowser UI (demo placeholder when launched directly)
app . get ( '/app/filebrowser/' , ( req , res ) => {
res . type ( 'html' ). send ( `<!DOCTYPE html><html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
<title>File Browser</title><style>*{margin:0;padding:0;box-sizing:border-box}body{background:#1a1a2e;color:#e0e0e0;font-family:system-ui,sans-serif;display:flex;align-items:center;justify-content:center;min-height:100vh}
.card{background:rgba(0,0,0,0.4);border:1px solid rgba(255,255,255,0.1);border-radius:16px;padding:48px;text-align:center;max-width:400px;backdrop-filter:blur(20px)}
h1{font-size:24px;margin-bottom:12px}p{color:rgba(255,255,255,0.6);font-size:14px;line-height:1.6}</style></head>
<body><div class="card"><h1>File Browser</h1><p>File Browser is running. Use the Cloud page in Archipelago to manage your files.</p></div></body></html>` )
})
2026-03-07 22:50:05 +00:00
// FileBrowser login - return mock JWT
app . post ( '/app/filebrowser/api/login' , ( req , res ) => {
res . send ( '"mock-filebrowser-token-demo"' )
})
// FileBrowser list resources
app . get ( '/app/filebrowser/api/resources/*' , ( req , res ) => {
const reqPath = decodeURIComponent ( req . params [ 0 ] || '/' ). replace ( /\/+$/ , '' ) || '/'
const items = MOCK_FILES [ reqPath ] || []
res . json ({
items ,
numDirs : items . filter ( i => i . isDir ). length ,
numFiles : items . filter ( i => ! i . isDir ). length ,
sorting : { by : 'name' , asc : true },
})
})
app . get ( '/app/filebrowser/api/resources' , ( req , res ) => {
const items = MOCK_FILES [ '/' ] || []
res . json ({
items ,
numDirs : items . filter ( i => i . isDir ). length ,
numFiles : items . filter ( i => ! i . isDir ). length ,
sorting : { by : 'name' , asc : true },
})
})
2026-03-09 18:12:28 +00:00
// FileBrowser upload (POST to resources path) — mock accepts and discards the body
app . post ( '/app/filebrowser/api/resources/*' , ( req , res ) => {
req . resume ()
req . on ( 'end' , () => res . sendStatus ( 200 ))
})
// FileBrowser delete
app . delete ( '/app/filebrowser/api/resources/*' , ( req , res ) => {
res . sendStatus ( 200 )
})
// FileBrowser rename
app . patch ( '/app/filebrowser/api/resources/*' , ( req , res ) => {
res . sendStatus ( 200 )
})
2026-03-07 22:50:05 +00:00
// FileBrowser raw file content (for text file reading)
app . get ( '/app/filebrowser/api/raw/*' , ( req , res ) => {
const reqPath = '/' + decodeURIComponent ( req . params [ 0 ] || '' )
const content = MOCK_FILE_CONTENTS [ reqPath ]
if ( content ) {
res . type ( 'text/plain' ). send ( content )
} else {
res . status ( 404 ). send ( 'File not found' )
}
})
2026-03-07 23:07:38 +00:00
// Claude API Proxy (reads ANTHROPIC_API_KEY from environment)
2026-03-09 13:03:53 +00:00
// Uses fetch (Node 22+) for reliable DNS resolution and streaming in Docker/Alpine
2026-03-07 23:07:38 +00:00
// =============================================================================
2026-03-09 13:03:53 +00:00
app . post ( '/aiui/api/claude/*' , async ( req , res ) => {
2026-03-07 23:07:38 +00:00
const apiKey = process . env . ANTHROPIC_API_KEY
if ( ! apiKey ) {
return res . status ( 500 ). json ({
type : 'error' ,
error : { type : 'configuration_error' , message : 'ANTHROPIC_API_KEY not configured on server' }
})
}
const apiPath = '/' + req . params [ 0 ]
2026-03-07 23:22:30 +00:00
2026-03-08 00:21:38 +00:00
// Clean request body for Anthropic API
2026-03-07 23:22:30 +00:00
const body = req . body
2026-03-08 00:21:38 +00:00
if ( body ) {
if ( ! body . max_tokens ) body . max_tokens = 4096
2026-03-08 00:27:34 +00:00
// Fix model IDs — AIUI may send short names
if ( body . model && ! body . model . includes ( '-2' )) {
const modelMap = {
'claude-haiku-4.5' : 'claude-haiku-4-5-20251001' ,
'claude-haiku-4-5' : 'claude-haiku-4-5-20251001' ,
'claude-sonnet-4-5' : 'claude-sonnet-4-5-20250514' ,
'claude-sonnet-4.5' : 'claude-sonnet-4-5-20250514' ,
}
if ( modelMap [ body . model ]) body . model = modelMap [ body . model ]
}
2026-03-08 00:21:38 +00:00
// Strip AIUI-specific fields that Anthropic API rejects
delete body . webSearch
delete body . webResults
delete body . context
2026-03-07 23:22:30 +00:00
}
const bodyStr = JSON . stringify ( body )
2026-03-09 13:03:53 +00:00
const url = `https://api.anthropic.com ${ apiPath } `
console . log ( `[Claude Proxy] → POST ${ url } ( ${ bodyStr . length } bytes, model: ${ body ? . model || 'unknown' } )` )
2026-03-07 23:07:38 +00:00
2026-03-09 13:03:53 +00:00
try {
const controller = new AbortController ()
const timeout = setTimeout (() => controller . abort (), 60000 )
2026-03-07 23:07:38 +00:00
2026-03-09 13:03:53 +00:00
const apiRes = await fetch ( url , {
method : 'POST' ,
signal : controller . signal ,
headers : {
'Content-Type' : 'application/json' ,
'x-api-key' : apiKey ,
'anthropic-version' : '2023-06-01' ,
},
body : bodyStr ,
})
2026-03-07 23:07:38 +00:00
2026-03-09 13:03:53 +00:00
clearTimeout ( timeout )
console . log ( `[Claude Proxy] ← ${ apiRes . status } ` )
// Forward status and headers
res . status ( apiRes . status )
for ( const [ key , value ] of apiRes . headers . entries ()) {
// Skip hop-by-hop headers
if ( ! [ 'transfer-encoding' , 'connection' , 'keep-alive' ]. includes ( key . toLowerCase ())) {
res . setHeader ( key , value )
}
}
// Stream the response body
if ( apiRes . body ) {
const reader = apiRes . body . getReader ()
const pump = async () => {
while ( true ) {
const { done , value } = await reader . read ()
if ( done ) { res . end (); return }
if ( ! res . writableEnded ) res . write ( value )
}
}
pump (). catch (( err ) => {
console . error ( '[Claude Proxy] Stream error:' , err . message )
if ( ! res . writableEnded ) res . end ()
})
} else {
res . end ()
}
} catch ( err ) {
const msg = err . name === 'AbortError' ? 'Request timed out (60s)' : ( err . message || 'Unknown error' )
console . error ( `[Claude Proxy] Error: ${ msg } ` )
2026-03-07 23:07:38 +00:00
if ( ! res . headersSent ) {
res . status ( 502 ). json ({
type : 'error' ,
2026-03-07 23:58:08 +00:00
error : { type : 'proxy_error' , message : msg }
2026-03-07 23:07:38 +00:00
})
}
2026-03-09 13:03:53 +00:00
}
2026-03-07 23:07:38 +00:00
})
2026-03-08 01:48:23 +00:00
// Ollama (local AI) proxy — forwards to localhost:11434
app . all ( '/aiui/api/ollama/*' , ( req , res ) => {
const ollamaPath = '/' + req . params [ 0 ]
const bodyStr = JSON . stringify ( req . body )
const options = {
hostname : '127.0.0.1' ,
port : 11434 ,
path : ollamaPath ,
method : req . method ,
headers : {
'Content-Type' : 'application/json' ,
'Content-Length' : Buffer . byteLength ( bodyStr ),
},
}
const proxyReq = http . request ( options , ( proxyRes ) => {
res . writeHead ( proxyRes . statusCode , proxyRes . headers )
proxyRes . pipe ( res )
})
proxyReq . on ( 'error' , ( err ) => {
const msg = err . message || err . code || 'Ollama not available'
console . error ( '[Ollama Proxy] Error:' , msg )
if ( ! res . headersSent ) {
res . status ( 502 ). json ({ error : msg })
}
})
if ( req . method !== 'GET' && req . method !== 'HEAD' ) {
proxyReq . write ( bodyStr )
}
proxyReq . end ()
})
// =============================================================================
// Ollama Local AI Proxy (forwards to Ollama on localhost:11434)
// =============================================================================
app . all ( '/api/ollama/*' , ( req , res ) => {
const ollamaPath = '/' + req . params [ 0 ]
const isPost = req . method === 'POST'
const bodyStr = isPost ? JSON . stringify ( req . body ) : null
const options = {
hostname : '127.0.0.1' ,
port : 11434 ,
path : ollamaPath ,
method : req . method ,
headers : { 'Content-Type' : 'application/json' },
}
const proxyReq = http . request ( options , ( proxyRes ) => {
res . writeHead ( proxyRes . statusCode , proxyRes . headers )
proxyRes . pipe ( res )
})
proxyReq . on ( 'error' , ( err ) => {
const msg = err . message || err . code || 'Ollama not available'
console . error ( '[Ollama Proxy] Error:' , msg )
if ( ! res . headersSent ) {
res . status ( 502 ). json ({ error : msg })
}
})
if ( bodyStr ) proxyReq . write ( bodyStr )
proxyReq . end ()
})
2026-03-07 23:24:27 +00:00
// Web search stub (no search engine configured in demo)
app . get ( '/api/web-search' , ( req , res ) => {
2026-03-07 23:58:08 +00:00
res . json ({ results : [] })
2026-03-07 23:24:27 +00:00
})
2026-03-07 23:58:08 +00:00
// TMDB API stub (no TMDB key in demo)
app . get ( '/api/tmdb/*' , ( req , res ) => {
res . json ({ results : [] })
})
// Catch-all for unimplemented API endpoints (return JSON, not HTML)
app . all ( '/api/*' , ( req , res ) => {
res . status ( 404 ). json ({ error : 'Not available in demo mode' })
})
2026-03-07 23:24:27 +00:00
app . all ( '/aiui/api/*' , ( req , res ) => {
2026-03-07 23:58:08 +00:00
res . status ( 404 ). json ({ error : 'Not available in demo mode' })
})
2026-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 ) } ║
║ ║
║ Container Runtime: ${ runtime . available ? `✅ ${ runtime . runtime } ` . padEnd ( 40 ) : '❌ Not available' . padEnd ( 40 ) } ║
2026-01-27 23:06:18 +00:00
║ Docker API: ✅ Connected ║
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 )
})
})