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
feat: v1.2.0-alpha — E2E encrypted mesh relay, steganography, relay status polling
Phase 5 mesh networking:
- E2E encrypted TX relay (X25519 + ChaCha20-Poly1305) — non-Archy nodes
relay encrypted blobs transparently via Meshcore native routing
- Steganographic encoding modes (WeatherStation, SensorNetwork) — traffic
looks like sensor data on the wire, 0xAA marker, configurable per-node
- Pre-flight Bitcoin Core health check on relay node — specific error codes
(bitcoin_unreachable, bitcoin_syncing, tx_rejected) instead of generic fails
- mesh.relay-status RPC endpoint — frontend polls for relay result every 3s
- On-Chain / Lightning tabs in Off-Grid Bitcoin panel
- Archy Peers vs Mesh Broadcast relay mode selector
- Mesh view fills viewport (no page scroll), internal panel scrolling
- Version bump to 1.2.0-alpha
Also includes: deploy hardening, container fixes, IndeedHub updates,
boot screen, dashboard improvements, MASTER_PLAN task tracking
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
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'
feat: v1.2.0-alpha — E2E encrypted mesh relay, steganography, relay status polling
Phase 5 mesh networking:
- E2E encrypted TX relay (X25519 + ChaCha20-Poly1305) — non-Archy nodes
relay encrypted blobs transparently via Meshcore native routing
- Steganographic encoding modes (WeatherStation, SensorNetwork) — traffic
looks like sensor data on the wire, 0xAA marker, configurable per-node
- Pre-flight Bitcoin Core health check on relay node — specific error codes
(bitcoin_unreachable, bitcoin_syncing, tx_rejected) instead of generic fails
- mesh.relay-status RPC endpoint — frontend polls for relay result every 3s
- On-Chain / Lightning tabs in Off-Grid Bitcoin panel
- Archy Peers vs Mesh Broadcast relay mode selector
- Mesh view fills viewport (no page scroll), internal panel scrolling
- Version bump to 1.2.0-alpha
Also includes: deploy hardening, container fixes, IndeedHub updates,
boot screen, dashboard improvements, MASTER_PLAN task tracking
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
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'
// 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
feat: v1.2.0-alpha — E2E encrypted mesh relay, steganography, relay status polling
Phase 5 mesh networking:
- E2E encrypted TX relay (X25519 + ChaCha20-Poly1305) — non-Archy nodes
relay encrypted blobs transparently via Meshcore native routing
- Steganographic encoding modes (WeatherStation, SensorNetwork) — traffic
looks like sensor data on the wire, 0xAA marker, configurable per-node
- Pre-flight Bitcoin Core health check on relay node — specific error codes
(bitcoin_unreachable, bitcoin_syncing, tx_rejected) instead of generic fails
- mesh.relay-status RPC endpoint — frontend polls for relay result every 3s
- On-Chain / Lightning tabs in Off-Grid Bitcoin panel
- Archy Peers vs Mesh Broadcast relay mode selector
- Mesh view fills viewport (no page scroll), internal panel scrolling
- Version bump to 1.2.0-alpha
Also includes: deploy hardening, container fixes, IndeedHub updates,
boot screen, dashboard improvements, MASTER_PLAN task tracking
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
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 ( ) {
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' } ,
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-06 01:11:00 +00:00
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 ( ' ' ) ,
2026-01-24 22:59:20 +00:00
shortDesc : ` ${ id } application ` ,
2026-03-09 17:09:59 +00:00
icon : ` /assets/img/app-icons/ ${ id } .png `
}
// Determine port — use known mapping, or auto-assign a unique one
let assignedPort = portMappings [ id ]
if ( ! assignedPort ) {
while ( usedPorts . has ( nextAutoPort ) ) nextAutoPort ++
assignedPort = nextAutoPort ++
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 } ` )
feat: v1.2.0-alpha — E2E encrypted mesh relay, steganography, relay status polling
Phase 5 mesh networking:
- E2E encrypted TX relay (X25519 + ChaCha20-Poly1305) — non-Archy nodes
relay encrypted blobs transparently via Meshcore native routing
- Steganographic encoding modes (WeatherStation, SensorNetwork) — traffic
looks like sensor data on the wire, 0xAA marker, configurable per-node
- Pre-flight Bitcoin Core health check on relay node — specific error codes
(bitcoin_unreachable, bitcoin_syncing, tx_rejected) instead of generic fails
- mesh.relay-status RPC endpoint — frontend polls for relay result every 3s
- On-Chain / Lightning tabs in Off-Grid Bitcoin panel
- Archy Peers vs Mesh Broadcast relay mode selector
- Mesh view fills viewport (no page scroll), internal panel scrolling
- Version bump to 1.2.0-alpha
Also includes: deploy hardening, container fixes, IndeedHub updates,
boot screen, dashboard improvements, MASTER_PLAN task tracking
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
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 } } )
}
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' ,
} ,
{
id : 'lnd' ,
title : 'LND' ,
description : 'Lightning Network Daemon — instant, low-fee Bitcoin payments. Open channels, route payments, earn routing fees.' ,
version : '0.18.3' ,
icon : '/assets/img/app-icons/lnd.png' ,
author : 'Lightning Labs' ,
license : 'MIT' ,
category : 'Bitcoin' ,
} ,
{
id : 'electrs' ,
title : 'Electrs' ,
description : 'Electrum Server in Rust — index the blockchain for fast wallet lookups. Connect your hardware wallets privately.' ,
version : '0.10.6' ,
icon : '/assets/img/app-icons/electrs.png' ,
author : 'Roman Zeyde' ,
license : 'MIT' ,
category : 'Bitcoin' ,
} ,
{
id : 'mempool' ,
title : 'Mempool' ,
description : 'Bitcoin blockchain explorer and mempool visualizer. Monitor transactions, fees, and network activity in real time.' ,
version : '3.0.0' ,
icon : '/assets/img/app-icons/mempool.png' ,
author : 'Mempool Space' ,
license : 'AGPL-3.0' ,
category : 'Bitcoin' ,
} ,
{
id : 'btcpay' ,
title : 'BTCPay Server' ,
description : 'Self-hosted Bitcoin payment processor. Accept Bitcoin payments in your store — no fees, no middlemen, no KYC.' ,
version : '2.0.4' ,
icon : '/assets/img/app-icons/btcpay.png' ,
author : 'BTCPay Server' ,
license : 'MIT' ,
category : 'Commerce' ,
} ,
{
id : 'fedimint' ,
title : 'Fedimint' ,
description : 'Federated Chaumian e-cash mint — community custody, private payments, and Lightning gateways for your tribe.' ,
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' ,
2026-01-24 22:59:20 +00:00
} ,
{
2026-03-09 13:03:53 +00:00
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' ,
2026-01-24 22:59:20 +00:00
license : 'MIT' ,
2026-03-09 13:03:53 +00:00
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' } )
}
feat: add TOTP 2FA, API key switcher, login progress bar, and alpha hardening plan
- TOTP 2FA: full setup/confirm/disable/login flow with Argon2id + ChaCha20-Poly1305
encrypted secret storage, QR code generation, and bcrypt-hashed backup codes
- API key switcher: OAuth vs personal API key toggle in AIUI chat settings with
status indicator, key validation, and help text
- Login progress bar: server startup detection with health check polling, form
disabled until server is ready
- AI quarantine docs: comprehensive HTML page documenting all 6 security layers
- Settings: AI Data Access permission toggles with per-category control
- Alpha hardening plan: 28-task overnight automation plan across 7 phases
(onboarding, login, app install, AIUI, UI polish, security, ISO build)
- Backlog: node discovery spatial map feature for alpha demo
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
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-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 = [
feat: Phase 3 Week 7 — typed message UI, session badges, rich chat cards
Frontend store (mesh.ts):
- Add typed message interfaces: InvoiceData, AlertData, CoordinateData,
SessionStatus, AlertStatus, MeshMessageTypeLabel
- New actions: sendInvoice, sendCoordinate, sendAlert, getSessionStatus,
rotatePrekeys
Mesh.vue UI:
- Typed message rendering in chat bubbles:
- Invoice: orange card with sats amount, memo, bolt11 preview, paid badge
- Alert: red card (emergency/dead_man) or blue (status), signed badge,
GPS link to OpenStreetMap
- Coordinate: blue card with lat/lng, label, OSM map link
- Block header: purple inline with chain icon
- Session badge in chat header: green shield (Double Ratchet),
yellow (static encryption), gray (none)
- Session status fetched on peer selection via mesh.session-status RPC
Mock backend:
- Messages now include message_type and typed_payload fields
- Mix of text, invoice (paid + unpaid), alert (emergency + status),
coordinate, and block_header messages for testing
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
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-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-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 \n ch_001,ACINQ,5000000,2450000,2550000,active \n ch_002,WalletOfSatoshi,2000000,1200000,800000,active \n ch_003,Voltage,10000000,4500000,5500000,active \n ch_004,Kraken,3000000,1800000,1200000,active ` ,
'/Documents/sovereignty-manifesto.txt' : ` THE SOVEREIGNTY MANIFESTO \n ========================= \n \n We hold these truths to be self-evident: \n \n 1. Your data belongs to you. \n 2. Your money should be uncensorable. \n 3. Your communications should be private. \n 4. Your compute should be sovereign. \n 5. Your identity should be self-issued. \n \n Run 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 < / t i t l e > < s t y l e > * { m a r g i n : 0 ; p a d d i n g : 0 ; b o x - s i z i n g : b o r d e r - b o x } b o d y { b a c k g r o u n d : # 1 a 1 a 2 e ; c o l o r : # e 0 e 0 e 0 ; f o n t - f a m i l y : s y s t e m - u i , s a n s - s e r i f ; d i s p l a y : f l e x ; a l i g n - i t e m s : c e n t e r ; j u s t i f y - c o n t e n t : c e n t e r ; m i n - h e i g h t : 1 0 0 v h }
. card { background : rgba ( 0 , 0 , 0 , 0.4 ) ; border : 1 px solid rgba ( 255 , 255 , 255 , 0.1 ) ; border - radius : 16 px ; padding : 48 px ; text - align : center ; max - width : 400 px ; backdrop - filter : blur ( 20 px ) }
h1 { font - size : 24 px ; margin - bottom : 12 px } p { color : rgba ( 255 , 255 , 255 , 0.6 ) ; font - size : 14 px ; line - height : 1.6 } < / s t y l e > < / h e a d >
< body > < div class = "card" > < h1 > File Browser < / h 1 > < p > F i l e B r o w s e r i s r u n n i n g . U s e t h e C l o u d p a g e i n A r c h i p e l a g o t o m a n a g e y o u r f i l e s . < / p > < / d i v > < / b o d y > < / h t m l > ` )
} )
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 )
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 } ` )
2026-03-07 23:07:38 +00:00
2026-03-09 13:03:53 +00:00
// 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 : [ ] } )
} )
// TMDB API stub (no TMDB key in demo)
app . get ( '/api/tmdb/*' , ( req , res ) => {
res . json ( { results : [ ] } )
2026-03-07 23:24:27 +00:00
} )
2026-03-07 23:58:08 +00:00
// 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 )
} )
} )