2026-01-24 22:59:20 +00:00
#!/usr/bin/env node
/ * *
* Archipelago Mock Backend Server
* Pure Archipelago implementation - NO StartOS dependencies
* Supports dev modes : setup , onboarding , existing
* /
import express from 'express'
import cors from 'cors'
import cookieParser from 'cookie-parser'
import { WebSocketServer } from 'ws'
import http from 'http'
import { exec } from 'child_process'
import { promisify } from 'util'
import fs from 'fs/promises'
import path from 'path'
import { fileURLToPath } from 'url'
2026-01-27 23:06:18 +00:00
import Docker from 'dockerode'
2026-01-24 22:59:20 +00:00
const _ _filename = fileURLToPath ( import . meta . url )
const _ _dirname = path . dirname ( _ _filename )
const execPromise = promisify ( exec )
2026-03-18 21:06:14 +00:00
// Find container socket: Podman (macOS/Linux) or Docker
import { existsSync } from 'fs'
function findContainerSocket ( ) {
// DOCKER_HOST env var (set by podman machine start)
if ( process . env . DOCKER _HOST ) {
const p = process . env . DOCKER _HOST . replace ( 'unix://' , '' )
if ( existsSync ( p ) ) return p
}
// Podman machine socket (macOS) — check TMPDIR-based path
if ( process . env . TMPDIR ) {
const podmanDir = path . join ( path . dirname ( process . env . TMPDIR ) , 'podman' )
const sock = path . join ( podmanDir , 'podman-machine-default-api.sock' )
if ( existsSync ( sock ) ) return sock
}
// Docker socket
if ( existsSync ( '/var/run/docker.sock' ) ) return '/var/run/docker.sock'
// Linux podman rootless
const uid = process . getuid ? . ( ) || 1000
const linuxSock = ` /run/user/ ${ uid } /podman/podman.sock `
if ( existsSync ( linuxSock ) ) return linuxSock
return null
}
const containerSocket = findContainerSocket ( )
const docker = containerSocket ? new Docker ( { socketPath : containerSocket } ) : null
if ( containerSocket ) {
console . log ( ` [Container] Socket: ${ containerSocket } ` )
} else {
console . log ( '[Container] No socket found — simulation mode (no Docker/Podman)' )
}
2026-01-24 22:59:20 +00:00
const app = express ( )
const PORT = 5959
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'
feat(TASK-12): beta telemetry — report endpoint + settings toggle
Backend: telemetry.report RPC builds anonymous health report with node ID
(SHA-256 hash of pubkey, truncated), version, uptime, container states,
CPU/RAM, federation peers, and recent alerts. Saves latest report to disk.
Requires analytics opt-in (existing analytics.enable/disable flow).
Frontend: "Beta Telemetry" section in Settings with enable/disable toggle.
Shows what data is and isn't collected. Mock backend handles all analytics
and telemetry RPCs.
Privacy: No wallet data, no private keys, no DIDs, no IP addresses.
Node identified by truncated hash only.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-18 23:14:47 +00:00
// Mutable wallet state — faucet/send/receive modify these values
const walletState = {
onchain _sats : 2_350_000 ,
channel _sats : 8_250_000 ,
ecash _sats : 250_000 ,
ecash _tokens : 12 ,
block _height : 892451 ,
transactions : [
{ tx _hash : 'ab12cd34ef5678901234567890abcdef12345678' , amount _sats : 2_000_000 , direction : 'incoming' , num _confirmations : 142 , block _height : 892310 , time _stamp : Math . floor ( Date . now ( ) / 1000 ) - 86400 , label : 'Channel funding' , total _fees : 0 , dest _addresses : [ ] } ,
{ tx _hash : 'cd34ef5678901234567890abcdef1234567890ab' , amount _sats : 250_000 , direction : 'incoming' , num _confirmations : 28 , block _height : 892420 , time _stamp : Math . floor ( Date . now ( ) / 1000 ) - 7200 , label : 'Faucet deposit' , total _fees : 0 , dest _addresses : [ ] } ,
{ tx _hash : 'ff99ee88dd7766554433221100aabbccddeeff00' , amount _sats : 100_000 , direction : 'incoming' , num _confirmations : 0 , block _height : 0 , time _stamp : Math . floor ( Date . now ( ) / 1000 ) - 600 , label : 'Incoming from faucet' , total _fees : 0 , dest _addresses : [ ] } ,
] ,
}
function randomHex ( bytes ) { return Array . from ( { length : bytes } , ( ) => Math . floor ( Math . random ( ) * 256 ) . toString ( 16 ) . padStart ( 2 , '0' ) ) . join ( '' ) }
2026-01-24 22:59:20 +00:00
// User state (simulated file-based storage)
let userState = {
setupComplete : false ,
onboardingComplete : false ,
passwordHash : null , // In real app, this would be bcrypt hash
}
feat(TASK-12): beta telemetry — report endpoint + settings toggle
Backend: telemetry.report RPC builds anonymous health report with node ID
(SHA-256 hash of pubkey, truncated), version, uptime, container states,
CPU/RAM, federation peers, and recent alerts. Saves latest report to disk.
Requires analytics opt-in (existing analytics.enable/disable flow).
Frontend: "Beta Telemetry" section in Settings with enable/disable toggle.
Shows what data is and isn't collected. Mock backend handles all analytics
and telemetry RPCs.
Privacy: No wallet data, no private keys, no DIDs, no IP addresses.
Node identified by truncated hash only.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-18 23:14:47 +00:00
let mockState = { analyticsEnabled : false }
2026-01-24 22:59:20 +00:00
// Initialize user state based on dev mode
function initializeUserState ( ) {
switch ( DEV _MODE ) {
case 'setup' :
// Setup mode: Original StartOS node setup - user needs to set password
// This is the simple password setup, NOT the experimental onboarding
userState = {
setupComplete : false , // User hasn't set password yet
onboardingComplete : false , // Onboarding not relevant for setup mode
passwordHash : null ,
}
break
case 'onboarding' :
// Onboarding mode: Experimental onboarding flow
// User has set password (via setup) but needs to go through experimental onboarding
userState = {
setupComplete : true , // Password already set
onboardingComplete : false , // Needs experimental onboarding
passwordHash : MOCK _PASSWORD ,
}
break
case 'existing' :
// Existing user: Fully set up, just needs to login
userState = {
setupComplete : true ,
onboardingComplete : true ,
passwordHash : MOCK _PASSWORD ,
}
break
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 ( ) {
2026-03-18 21:06:14 +00:00
if ( ! docker ) return { }
2026-01-27 23:06:18 +00:00
try {
const containers = await docker . listContainers ( { all : true } )
// Map of container names to app IDs
const containerMapping = {
'archy-bitcoin' : 'bitcoin' ,
'archy-btcpay' : 'btcpay-server' ,
'archy-homeassistant' : 'homeassistant' ,
'archy-grafana' : 'grafana' ,
'archy-endurain' : 'endurain' ,
'archy-fedimint' : 'fedimint' ,
'archy-morphos' : 'morphos-server' ,
'archy-lnd' : 'lightning-stack' ,
'archy-mempool-web' : 'mempool' ,
2026-02-17 15:03:34 +00:00
'mempool-electrs' : 'mempool-electrs' ,
2026-01-27 23:06:18 +00:00
'archy-ollama' : 'ollama' ,
'archy-searxng' : 'searxng' ,
'archy-onlyoffice' : 'onlyoffice' ,
'archy-penpot-frontend' : 'penpot'
}
const apps = { }
for ( const container of containers ) {
const name = container . Names [ 0 ] . replace ( /^\// , '' )
const appId = containerMapping [ name ]
if ( ! appId ) continue
const isRunning = container . State === 'running'
const ports = container . Ports || [ ]
const hostPort = ports . find ( p => p . PublicPort ) ? . PublicPort || null
// Get app metadata
const appMetadata = {
'bitcoin' : {
title : 'Bitcoin Core' ,
icon : '/assets/img/app-icons/bitcoin.svg' ,
description : 'Full Bitcoin node implementation'
} ,
'btcpay-server' : {
title : 'BTCPay Server' ,
icon : '/assets/img/app-icons/btcpay-server.png' ,
description : 'Self-hosted Bitcoin payment processor'
} ,
'homeassistant' : {
title : 'Home Assistant' ,
icon : '/assets/img/app-icons/homeassistant.png' ,
description : 'Open source home automation platform'
} ,
'grafana' : {
title : 'Grafana' ,
icon : '/assets/img/grafana.png' ,
description : 'Analytics and monitoring platform'
} ,
'endurain' : {
title : 'Endurain' ,
icon : '/assets/img/endurain.png' ,
description : 'Application platform'
} ,
'fedimint' : {
title : 'Fedimint' ,
2026-02-17 15:03:34 +00:00
icon : '/assets/img/app-icons/fedimint.png' ,
2026-01-27 23:06:18 +00:00
description : 'Federated Bitcoin mint'
} ,
'morphos-server' : {
title : 'MorphOS Server' ,
icon : '/assets/img/morphos.png' ,
description : 'Server platform'
} ,
'lightning-stack' : {
title : 'Lightning Stack' ,
icon : '/assets/img/app-icons/lightning-stack.png' ,
description : 'Lightning Network (LND)'
} ,
'mempool' : {
title : 'Mempool' ,
icon : '/assets/img/app-icons/mempool.png' ,
description : 'Bitcoin blockchain explorer'
} ,
2026-02-17 15:03:34 +00:00
'mempool-electrs' : {
title : 'Electrs' ,
icon : '/assets/img/app-icons/electrs.svg' ,
description : 'Electrum protocol indexer for Bitcoin'
} ,
2026-01-27 23:06:18 +00:00
'ollama' : {
title : 'Ollama' ,
2026-02-01 18:46:35 +00:00
icon : '/assets/img/app-icons/ollama.png' ,
2026-01-27 23:06:18 +00:00
description : 'Run large language models locally'
} ,
'searxng' : {
title : 'SearXNG' ,
icon : '/assets/img/app-icons/searxng.png' ,
description : 'Privacy-respecting metasearch engine'
} ,
'onlyoffice' : {
title : 'OnlyOffice' ,
icon : '/assets/img/onlyoffice.webp' ,
description : 'Office suite and document collaboration'
} ,
'penpot' : {
title : 'Penpot' ,
icon : '/assets/img/penpot.webp' ,
description : 'Open-source design and prototyping'
}
}
const metadata = appMetadata [ appId ] || {
title : appId ,
2026-03-06 01:11:00 +00:00
icon : '/assets/icon/pwa-192x192-v2.png' ,
2026-01-27 23:06:18 +00:00
description : ` ${ appId } application `
}
apps [ appId ] = {
title : metadata . title ,
version : '1.0.0' ,
status : isRunning ? 'running' : 'stopped' ,
state : isRunning ? 'running' : 'stopped' ,
security+feat: v1.3.0 — pentest remediation, container reliability, UI overhaul
Security (33 pentest findings addressed):
- CRITICAL: backend binds 127.0.0.1, path traversal in tor.rs/dwn fixed
- HIGH: federation requires signatures, XSS login redirect, RBAC viewer restricted
- HIGH: tar slip prevention, S3 SSRF validation, backup ID validation
- MEDIUM: remember-me random secret, TOTP session rotation, password re-auth
- LOW: CSP unsafe-inline removed, CORS dev-only, onion/webhook validation
Container reliability:
- Memory limits on all 37 containers (OOM prevention)
- Exited vs stopped state distinction with health-aware status badges
- Crash recovery coordination (no more restart cascade)
- User-stopped tracking survives reboots
- Tiered boot recovery (databases → core → services → apps)
UI:
- Wallet TransactionsModal, health-aware app status badges
- Restart button on containers, exited/crashed red state
- Mesh view overhaul, glass button updates, BaseModal/ToggleSwitch
- Apps sticky header removed, dev faucet, mutable mock wallet
Infrastructure:
- LND REST port 8080 exposed over Tor (LND Connect fix)
- Nginx cookie_session fix, deploy script Tor config updated
- Dev environment: podman auto-start, boot mode simulation
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-19 12:44:31 +00:00
health : isRunning ? 'healthy' : null ,
2026-01-27 23:06:18 +00:00
'static-files' : {
license : 'MIT' ,
instructions : metadata . description ,
icon : metadata . icon
} ,
manifest : {
id : appId ,
title : metadata . title ,
version : '1.0.0' ,
description : {
short : metadata . description ,
long : metadata . description
} ,
'release-notes' : 'Initial release' ,
license : 'MIT' ,
'wrapper-repo' : '#' ,
'upstream-repo' : '#' ,
'support-site' : '#' ,
'marketing-site' : '#' ,
'donation-url' : null ,
interfaces : hostPort ? {
main : {
name : 'Web Interface' ,
description : ` ${ metadata . title } web interface ` ,
ui : true
}
} : { }
} ,
installed : {
'current-dependents' : { } ,
'current-dependencies' : { } ,
'last-backup' : null ,
'interface-addresses' : hostPort ? {
main : {
'tor-address' : ` ${ appId } .onion ` ,
'lan-address' : ` http://localhost: ${ hostPort } `
}
} : { } ,
status : isRunning ? 'running' : 'stopped'
}
}
}
return apps
} catch ( error ) {
console . error ( '[Docker] Error querying containers:' , error . message )
return { }
}
}
2026-01-24 22:59:20 +00:00
// Helper: Check if Docker/Podman is available
async function isContainerRuntimeAvailable ( ) {
try {
// Try Podman first (Archipelago's choice)
await execPromise ( 'podman ps' )
return { available : true , runtime : 'podman' }
} catch {
try {
// Fallback to Docker
await execPromise ( 'docker ps' )
return { available : true , runtime : 'docker' }
} catch {
return { available : false , runtime : null }
}
}
}
2026-03-09 17:09:59 +00:00
// Marketplace metadata lookup for install (title, description, icon, version)
const marketplaceMetadata = {
'bitcoin-knots' : { title : 'Bitcoin Knots' , shortDesc : 'Full Bitcoin node — validate and relay blocks and transactions' , icon : '/assets/img/app-icons/bitcoin-knots.webp' } ,
'electrs' : { title : 'Electrs' , shortDesc : 'Electrum protocol indexer for Bitcoin' , icon : '/assets/img/app-icons/electrs.svg' } ,
'btcpay-server' : { title : 'BTCPay Server' , shortDesc : 'Self-hosted Bitcoin payment processor' , icon : '/assets/img/app-icons/btcpay-server.png' } ,
'lnd' : { title : 'LND' , shortDesc : 'Lightning Network Daemon' , icon : '/assets/img/app-icons/lnd.svg' } ,
'mempool' : { title : 'Mempool Explorer' , shortDesc : 'Bitcoin blockchain and mempool visualizer' , icon : '/assets/img/app-icons/mempool.webp' } ,
'homeassistant' : { title : 'Home Assistant' , shortDesc : 'Open-source home automation platform' , icon : '/assets/img/app-icons/homeassistant.png' } ,
'grafana' : { title : 'Grafana' , shortDesc : 'Analytics and monitoring dashboards' , icon : '/assets/img/app-icons/grafana.png' } ,
'searxng' : { title : 'SearXNG' , shortDesc : 'Privacy-respecting metasearch engine' , icon : '/assets/img/app-icons/searxng.png' } ,
'ollama' : { title : 'Ollama' , shortDesc : 'Run large language models locally' , icon : '/assets/img/app-icons/ollama.png' } ,
'onlyoffice' : { title : 'OnlyOffice' , shortDesc : 'Office suite for document collaboration' , icon : '/assets/img/app-icons/onlyoffice.webp' } ,
'penpot' : { title : 'Penpot' , shortDesc : 'Open-source design and prototyping platform' , icon : '/assets/img/app-icons/penpot.webp' } ,
'nextcloud' : { title : 'Nextcloud' , shortDesc : 'Self-hosted cloud storage and collaboration' , icon : '/assets/img/app-icons/nextcloud.webp' } ,
'vaultwarden' : { title : 'Vaultwarden' , shortDesc : 'Self-hosted password manager (Bitwarden-compatible)' , icon : '/assets/img/app-icons/vaultwarden.webp' } ,
'jellyfin' : { title : 'Jellyfin' , shortDesc : 'Free media server for movies, music, and photos' , icon : '/assets/img/app-icons/jellyfin.webp' } ,
2026-03-13 23:40:29 +00:00
'photoprism' : { title : 'PhotoPrism' , shortDesc : 'AI-powered photo management' , icon : '/assets/img/app-icons/photoprism.svg' } ,
2026-03-09 17:09:59 +00:00
'immich' : { title : 'Immich' , shortDesc : 'High-performance photo and video backup' , icon : '/assets/img/app-icons/immich.png' } ,
'filebrowser' : { title : 'File Browser' , shortDesc : 'Web-based file manager' , icon : '/assets/img/app-icons/file-browser.webp' } ,
'nginx-proxy-manager' : { title : 'Nginx Proxy Manager' , shortDesc : 'Easy proxy management with SSL' , icon : '/assets/img/app-icons/nginx.svg' } ,
'portainer' : { title : 'Portainer' , shortDesc : 'Container management UI' , icon : '/assets/img/app-icons/portainer.webp' } ,
'uptime-kuma' : { title : 'Uptime Kuma' , shortDesc : 'Self-hosted monitoring tool' , icon : '/assets/img/app-icons/uptime-kuma.webp' } ,
'tailscale' : { title : 'Tailscale' , shortDesc : 'Zero-config VPN for secure remote access' , icon : '/assets/img/app-icons/tailscale.webp' } ,
'fedimint' : { title : 'Fedimint' , shortDesc : 'Federated Bitcoin mint with Guardian UI' , icon : '/assets/img/app-icons/fedimint.png' } ,
'indeedhub' : { title : 'Indeehub' , shortDesc : 'Bitcoin documentary streaming platform' , icon : '/assets/img/app-icons/indeedhub.png' } ,
'dwn' : { title : 'Decentralized Web Node' , shortDesc : 'Store and sync personal data with DID-based access' , icon : '/assets/img/app-icons/dwn.svg' } ,
'nostr-rs-relay' : { title : 'Nostr Relay' , shortDesc : 'Run your own Nostr relay' , icon : '/assets/img/app-icons/nostr-rs-relay.svg' } ,
'syncthing' : { title : 'Syncthing' , shortDesc : 'Peer-to-peer file synchronization' , icon : '/assets/img/app-icons/syncthing.png' } ,
2026-03-18 19:24:52 +00:00
'thunderhub' : { title : 'ThunderHub' , shortDesc : 'Lightning node management UI with channel management and payments' , icon : '/assets/img/app-icons/thunderhub.svg' } ,
2026-03-09 17:09:59 +00:00
'tor' : { title : 'Tor' , shortDesc : 'Anonymous communication over the Tor network' , icon : '/assets/img/app-icons/tor.png' } ,
'amin' : { title : 'Amin' , shortDesc : 'Administrative interface for Archipelago' , icon : '/assets/icon/pwa-192x192-v2.png' } ,
}
2026-01-24 22:59:20 +00:00
// Helper: Install package with container runtime (if available) or simulate
2026-03-09 17:09:59 +00:00
async function installPackage ( id , manifestUrl , opts = { } ) {
2026-01-24 22:59:20 +00:00
console . log ( ` [Package] 📦 Installing ${ id } ... ` )
2026-03-09 17:09:59 +00:00
2026-01-24 22:59:20 +00:00
try {
// Check if already installed
if ( mockData [ 'package-data' ] [ id ] ) {
throw new Error ( ` Package ${ id } is already installed ` )
}
2026-03-09 17:09:59 +00:00
const version = opts . version || '0.1.0'
2026-01-24 22:59:20 +00:00
const runtime = await isContainerRuntimeAvailable ( )
2026-03-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 } } )
}
2026-03-22 03:30:21 +00:00
case 'identity.verify' : {
return res . json ( { result : { valid : true } } )
}
2026-03-02 08:34:13 +00:00
case 'node.createBackup' : {
const { passphrase } = params || { }
if ( ! passphrase ) {
return res . json ( { error : { code : - 32602 , message : 'Missing passphrase' } } )
}
const mockDid = 'did:key:z6MkpTHR8VNsBxYAAWHut2Geadd9jSwuBV8xRoAnwWsdvktH'
const mockPubkey = 'a1b2c3d4e5f6789012345678901234567890abcdef1234567890abcdef123456'
return res . json ( {
result : {
version : 1 ,
did : mockDid ,
pubkey : mockPubkey ,
kid : ` ${ mockDid } #key-1 ` ,
encrypted : true ,
blob : Buffer . from ( ` mock-encrypted-backup- ${ passphrase } ` ) . toString ( 'base64' ) ,
timestamp : new Date ( ) . toISOString ( ) ,
} ,
} )
}
2026-01-24 22:59:20 +00:00
case 'auth.login' : {
const { password } = params
if ( ! userState . setupComplete ) {
return res . json ( {
error : {
code : - 32603 ,
message : 'User not set up. Please complete setup first.' ,
} ,
} )
}
// Simple password check (in real app, use bcrypt)
if ( password !== userState . passwordHash && password !== MOCK _PASSWORD ) {
return res . json ( {
error : {
code : - 32603 ,
message : 'Password Incorrect' ,
} ,
} )
}
const sessionId = ` session- ${ Date . now ( ) } `
sessions . set ( sessionId , {
createdAt : new Date ( ) ,
} )
res . cookie ( 'session' , sessionId , {
httpOnly : true ,
maxAge : 24 * 60 * 60 * 1000 ,
} )
return res . json ( { result : null } )
}
case 'auth.logout' : {
const sessionId = req . cookies ? . session
if ( sessionId ) {
sessions . delete ( sessionId )
}
res . clearCookie ( 'session' )
return res . json ( { result : null } )
}
2026-03-13 23:40:29 +00:00
case 'server.set-name' : {
const name = ( params ? . name || '' ) . trim ( )
if ( ! name || name . length > 64 ) {
return res . json ( { error : { code : - 1 , message : 'Name must be 1-64 characters' } } )
}
mockData [ 'server-info' ] . name = name
broadcastUpdate ( )
return res . json ( { result : { name } } )
}
2026-01-24 22:59:20 +00:00
case 'server.echo' : {
return res . json ( { result : { message : params ? . message || 'Hello from Archipelago!' } } )
}
case 'server.time' : {
return res . json ( {
result : {
now : new Date ( ) . toISOString ( ) ,
uptime : process . uptime ( ) ,
} ,
} )
}
case 'server.metrics' : {
2026-03-09 13:03:53 +00:00
// Slightly randomize so the dashboard feels alive
2026-01-24 22:59:20 +00:00
return res . json ( {
result : {
2026-03-09 13:03:53 +00:00
cpu : + ( 12 + Math . random ( ) * 18 ) . toFixed ( 1 ) ,
memory : + ( 58 + Math . random ( ) * 8 ) . toFixed ( 1 ) ,
disk : + ( 34 + Math . random ( ) * 3 ) . toFixed ( 1 ) ,
2026-01-24 22:59:20 +00:00
} ,
} )
}
case 'marketplace.get' : {
const mockApps = [
{
id : 'bitcoin' ,
title : 'Bitcoin Core' ,
2026-03-09 13:03:53 +00:00
description : 'Full Bitcoin node — validate transactions, enforce consensus rules, and support the network. The foundation of sovereignty.' ,
version : '27.1' ,
icon : '/assets/img/app-icons/bitcoin.png' ,
author : 'Bitcoin Core' ,
2026-01-24 22:59:20 +00:00
license : 'MIT' ,
2026-03-09 13:03:53 +00:00
category : 'Bitcoin' ,
} ,
{
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 } } )
}
2026-03-22 03:30:21 +00:00
case 'identity.list' : {
return res . json ( {
result : {
identities : [
{
id : 'id-primary' ,
name : 'Primary' ,
purpose : 'Main node identity' ,
pubkey : 'a1b2c3d4e5f6789012345678901234567890abcdef1234567890abcdef123456' ,
did : 'did:key:z6MkpTHR8VNsBxYAAWHut2Geadd9jSwuBV8xRoAnwWsdvktH' ,
created _at : '2026-01-10T08:00:00Z' ,
is _default : true ,
nostr _pubkey : 'a1b2c3d4e5f6789012345678901234567890abcdef1234567890abcdef123456' ,
nostr _npub : 'npub1mockprimaryidentitypubkeyvalue0000000000000000000000000abc' ,
profile : {
display _name : 'Archipelago Node' ,
about : 'Self-sovereign Bitcoin node' ,
nip05 : 'satoshi@archipelago.local' ,
lud16 : 'satoshi@getalby.com' ,
} ,
} ,
{
id : 'id-anon' ,
name : 'Anonymous' ,
purpose : 'Privacy-focused browsing' ,
pubkey : 'f6e5d4c3b2a19876543210fedcba9876543210fedcba9876543210fedcba98' ,
did : 'did:key:z6MkvWkza1fMBWhKnYE3CgMgxHLKbN8NmbFRqHcECF4oGrwx' ,
created _at : '2026-02-14T12:30:00Z' ,
is _default : false ,
nostr _pubkey : 'f6e5d4c3b2a19876543210fedcba9876543210fedcba9876543210fedcba98' ,
nostr _npub : 'npub1mockanonidentitypubkeyvalue000000000000000000000000000xyz' ,
profile : { display _name : 'Anon' } ,
} ,
{
id : 'id-merchant' ,
name : 'Merchant' ,
purpose : 'BTCPay & commerce' ,
pubkey : '1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef' ,
did : 'did:key:z6MknGc3ocHs3zdPiJbnaaqDi58NGb4pk1Sp9WNhSwaxN21V' ,
created _at : '2026-03-01T16:00:00Z' ,
is _default : false ,
nostr _pubkey : '1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef' ,
nostr _npub : 'npub1mockmerchantidentitypubkeyvalue000000000000000000000000def' ,
profile : { display _name : 'My Shop' , website : 'https://myshop.onion' } ,
} ,
] ,
} ,
} )
}
case 'identity.get' : {
const id = params ? . id || 'id-primary'
return res . json ( {
result : {
id ,
name : 'Primary' ,
purpose : 'Main node identity' ,
pubkey : 'a1b2c3d4e5f6789012345678901234567890abcdef1234567890abcdef123456' ,
did : 'did:key:z6MkpTHR8VNsBxYAAWHut2Geadd9jSwuBV8xRoAnwWsdvktH' ,
created _at : '2026-01-10T08:00:00Z' ,
is _default : true ,
nostr _pubkey : 'a1b2c3d4e5f6789012345678901234567890abcdef1234567890abcdef123456' ,
nostr _npub : 'npub1mockprimaryidentitypubkeyvalue0000000000000000000000000abc' ,
profile : {
display _name : 'Archipelago Node' ,
about : 'Self-sovereign Bitcoin node' ,
} ,
} ,
} )
}
case 'identity.export-keys' : {
return res . json ( {
result : {
ed25519 _secret _hex : 'deadbeef' . repeat ( 8 ) ,
nostr _secret _hex : 'cafebabe' . repeat ( 8 ) ,
nostr _nsec : 'nsec1mockexportedkeyvalue00000000000000000000000000000000000abc' ,
} ,
} )
}
case 'identity.update-profile' : {
return res . json ( { result : { success : true } } )
}
case 'identity.publish-profile' : {
return res . json ( { result : { event _id : ` evt- ${ Date . now ( ) . toString ( 36 ) } ` } } )
}
case 'identity.list' : {
return res . json ( {
result : {
identities : [
{
id : 'id-primary' ,
name : 'Primary' ,
purpose : 'personal' ,
pubkey : 'a1b2c3d4e5f6789012345678901234567890abcdef1234567890abcdef123456' ,
did : 'did:key:z6MkpTHR8VNsBxYAAWHut2Geadd9jSwuBV8xRoAnwWsdvktH' ,
created _at : '2026-01-10T08:00:00Z' ,
is _default : true ,
nostr _pubkey : 'a1b2c3d4e5f6789012345678901234567890abcdef1234567890abcdef123456' ,
nostr _npub : 'npub1598eg0y7m08htfzjfmzv6zjvf5u7p00dr9w0yfamaxqhkwlryckq5dh9ee' ,
profile : {
display _name : 'Satoshi' ,
about : 'Running a sovereign Bitcoin node' ,
nip05 : 'satoshi@archipelago.local' ,
lud16 : 'satoshi@getalby.com' ,
} ,
} ,
{
id : 'id-business' ,
name : 'Business' ,
purpose : 'business' ,
pubkey : 'f6e5d4c3b2a10987654321fedcba0987654321fedcba0987654321fedcba0987' ,
did : 'did:key:z6MkhaXgBZDvotDkL5257faiztiGiC2ReMkBe4bR6XBIDNq9' ,
created _at : '2026-02-05T12:30:00Z' ,
is _default : false ,
nostr _pubkey : 'f6e5d4c3b2a10987654321fedcba0987654321fedcba0987654321fedcba0987' ,
nostr _npub : 'npub17m9mx9kk2ry8p3xfkq0070fjlph9r9l0dj0y8fn0zrlj80ekjph7jxmxfg' ,
profile : {
display _name : 'Archy Consulting' ,
about : 'Bitcoin infrastructure services' ,
} ,
} ,
{
id : 'id-anon' ,
name : 'Anonymous' ,
purpose : 'anonymous' ,
pubkey : 'deadbeef00112233445566778899aabbccddeeff00112233445566778899aabb' ,
did : 'did:key:z6MknGc3ocHs3zdPiJbnaaqDi5hjrZo4HzmQnwzaxWhAbWAs' ,
created _at : '2026-03-01T18:00:00Z' ,
is _default : false ,
profile : { } ,
} ,
] ,
} ,
} )
}
case 'identity.get' : {
const id = params ? . id || 'id-primary'
const identities = {
'id-primary' : {
id : 'id-primary' , name : 'Primary' , purpose : 'personal' ,
pubkey : 'a1b2c3d4e5f6789012345678901234567890abcdef1234567890abcdef123456' ,
did : 'did:key:z6MkpTHR8VNsBxYAAWHut2Geadd9jSwuBV8xRoAnwWsdvktH' ,
nostr _pubkey : 'a1b2c3d4e5f6789012345678901234567890abcdef1234567890abcdef123456' ,
nostr _npub : 'npub1598eg0y7m08htfzjfmzv6zjvf5u7p00dr9w0yfamaxqhkwlryckq5dh9ee' ,
is _default : true , created _at : '2026-01-10T08:00:00Z' ,
profile : { display _name : 'Satoshi' , about : 'Running a sovereign Bitcoin node' } ,
} ,
}
return res . json ( { result : identities [ id ] || identities [ 'id-primary' ] } )
}
case 'identity.export-keys' : {
const { password } = params || { }
if ( password !== MOCK _PASSWORD ) {
return res . json ( { error : { code : - 32603 , message : 'Incorrect password' } } )
}
return res . json ( {
result : {
ed25519 _secret _hex : 'mock_ed25519_secret_' + '0' . repeat ( 40 ) ,
nostr _secret _hex : 'mock_nostr_secret_' + '0' . repeat ( 44 ) ,
nostr _nsec : 'nsec1mockkeymockkeymockkeymockkeymockkeymockkeymockkeymockkeymo' ,
} ,
} )
}
case 'identity.update-profile' : {
console . log ( ` [Identity] Updated profile for: ${ params ? . id } ` )
return res . json ( { result : { success : true } } )
}
case 'identity.publish-profile' : {
console . log ( ` [Identity] Published profile for: ${ params ? . id } ` )
return res . json ( { result : { event _id : ` nostr-event- ${ Date . now ( ) . toString ( 36 ) } ` } } )
}
2026-03-09 13:03:53 +00:00
// =========================================================================
// Nostr
// =========================================================================
case 'nostr.add-relay' : {
const { url } = params || { }
console . log ( ` [Nostr] Added relay: ${ url } ` )
return res . json ( { result : { success : true } } )
}
case 'nostr.remove-relay' : {
return res . json ( { result : { success : true } } )
}
case 'nostr.toggle-relay' : {
return res . json ( { result : { success : true } } )
}
// =========================================================================
// Content & Network
// =========================================================================
2026-03-22 03:30:21 +00:00
case 'content.browse-peer' : {
const onion = params ? . onion || ''
return res . json ( {
result : {
items : [
{ id : 'peer-doc-1' , filename : 'Bitcoin Whitepaper.pdf' , mime _type : 'application/pdf' , size _bytes : 184292 , description : 'The original Bitcoin whitepaper by Satoshi Nakamoto' , access : 'free' } ,
{ id : 'peer-img-1' , filename : 'node-setup-guide.png' , mime _type : 'image/png' , size _bytes : 524800 , description : 'Visual guide for setting up a Bitcoin node' , access : 'free' } ,
{ id : 'peer-vid-1' , filename : 'Lightning Demo.mp4' , mime _type : 'video/mp4' , size _bytes : 15728640 , description : 'Lightning Network payment channel demo' , access : { paid : { price _sats : 500 } } } ,
] ,
} ,
} )
}
2026-03-09 13:03:53 +00:00
case 'content.remove' : {
return res . json ( { result : { success : true } } )
}
case 'content.set-pricing' : {
return res . json ( { result : { success : true } } )
}
2026-03-22 03:30:21 +00:00
case 'network.diagnostics' : {
return res . json ( {
result : {
wan _ip : '82.14.203.47' ,
nat _type : 'Full Cone' ,
upnp _available : true ,
tor _connected : true ,
wifi _count : 3 ,
} ,
} )
}
case 'network.list-interfaces' : {
return res . json ( {
result : {
interfaces : [
{ name : 'eth0' , type : 'ethernet' , state : 'up' , mac : 'a8:a1:59:3c:f2:10' , ipv4 : [ '192.168.1.228/24' ] } ,
{ name : 'wlan0' , type : 'wifi' , state : 'up' , mac : 'dc:a6:32:12:ab:cd' , ipv4 : [ '192.168.1.230/24' ] } ,
{ name : 'lo' , type : 'loopback' , state : 'up' , mac : '00:00:00:00:00:00' , ipv4 : [ '127.0.0.1/8' ] } ,
{ name : 'podman0' , type : 'bridge' , state : 'up' , mac : '2e:f4:8a:11:22:33' , ipv4 : [ '10.89.0.1/16' ] } ,
{ name : 'tailscale0' , type : 'tunnel' , state : 'up' , mac : '' , ipv4 : [ '100.82.97.63/32' ] } ,
] ,
} ,
} )
}
case 'network.scan-wifi' : {
return res . json ( {
result : {
networks : [
{ ssid : 'HomeNetwork' , signal : 92 , security : 'WPA2' } ,
{ ssid : 'Neighbor-5G' , signal : 45 , security : 'WPA3' } ,
{ ssid : 'CoffeeShop' , signal : 30 , security : 'Open' } ,
] ,
} ,
} )
}
case 'network.configure-wifi' : {
console . log ( ` [Network] Connecting to WiFi: ${ params ? . ssid } ` )
return res . json ( { result : { success : true } } )
}
case 'network.dns-status' : {
return res . json ( {
result : {
provider : 'system' ,
servers : [ '1.1.1.1' , '9.9.9.9' ] ,
doh _enabled : false ,
doh _url : null ,
resolv _conf _servers : [ '1.1.1.1' , '9.9.9.9' ] ,
} ,
} )
}
case 'network.configure-dns' : {
console . log ( ` [Network] DNS configured: ${ params ? . provider } ` )
return res . json ( { result : { success : true } } )
}
2026-03-09 13:03:53 +00:00
case 'network.accept-request' : {
return res . json ( { result : { success : true } } )
}
case 'network.reject-request' : {
return res . json ( { result : { success : true } } )
}
// =========================================================================
// Server & Auth extras
// =========================================================================
case 'server.health' : {
return res . json ( {
result : {
status : 'healthy' ,
uptime : Math . floor ( process . uptime ( ) ) ,
services : {
backend : 'running' ,
nginx : 'running' ,
podman : 'running' ,
tor : 'running' ,
} ,
} ,
} )
}
case 'auth.changePassword' : {
const { currentPassword } = params || { }
if ( currentPassword !== userState . passwordHash && currentPassword !== MOCK _PASSWORD ) {
return res . json ( { error : { code : - 32603 , message : 'Current password is incorrect' } } )
}
userState . passwordHash = params . newPassword
console . log ( '[Auth] Password changed' )
return res . json ( { result : { success : true } } )
}
// =========================================================================
// Router (port forwarding)
// =========================================================================
case 'router.add-forward' : {
console . log ( ` [Router] Added forward: ${ JSON . stringify ( params ) } ` )
return res . json ( { result : { success : true , id : ` fwd- ${ Date . now ( ) } ` } } )
}
case 'router.remove-forward' : {
return res . json ( { result : { success : true } } )
}
2026-03-22 03:30:21 +00:00
case 'router.list-forwards' : {
return res . json ( {
result : {
forwards : [
{ id : 'fwd-1' , name : 'Bitcoin P2P' , external _port : 8333 , internal _port : 8333 , protocol : 'tcp' , enabled : true } ,
{ id : 'fwd-2' , name : 'LND gRPC' , external _port : 10009 , internal _port : 10009 , protocol : 'tcp' , enabled : true } ,
] ,
} ,
} )
}
2026-03-09 13:03:53 +00:00
// =========================================================================
// Tor & Peer Networking
// =========================================================================
2026-03-22 03:30:21 +00:00
case 'tor.list-services' : {
return res . json ( {
result : {
tor _running : true ,
services : [
{ name : 'archipelago' , local _port : 5678 , onion _address : 'archydemox7k3pnw4hv5qz2jcbr6dwefys3ockqzf4mzjlvxot2ioad.onion' , enabled : true , unauthenticated : false , protocol : false } ,
{ name : 'bitcoin' , local _port : 8333 , onion _address : 'btcmockxj4k5pnw7hv8qz9jcbr2dwefys6ockqzf1mzjlvxot5ioad.onion' , enabled : true , unauthenticated : false , protocol : false } ,
{ name : 'lnd' , local _port : 9735 , onion _address : 'lndmockab3c4def5ghi6jkl7mno8pqr9stu0vwx1yz2ab3c4def5ghi.onion' , enabled : true , unauthenticated : false , protocol : false } ,
{ name : 'electrs' , local _port : 50001 , onion _address : 'elecmockyz9wvu8tsr7qpo6nml5kji4hgf3edc2ba1xyz9wvu8tsr7q.onion' , enabled : true , unauthenticated : true , protocol : false } ,
{ name : 'nostr-relay' , local _port : 7777 , onion _address : null , enabled : false , unauthenticated : true , protocol : false } ,
] ,
} ,
} )
}
2026-03-09 13:03:53 +00:00
case 'node.tor-address' : {
return res . json ( {
result : {
tor _address : 'archydemox7k3pnw4hv5qz2jcbr6dwefys3ockqzf4mzjlvxot2ioad.onion' ,
} ,
} )
}
2026-03-08 00:27:34 +00:00
case 'node-list-peers' : {
2026-03-09 13:03:53 +00:00
return res . json ( {
result : {
peers : [
{ onion : 'peer1abc2def3ghi4jkl5mno6pqr7stu8vwx9yz.onion' , pubkey : 'a1b2c3d4e5f6' , name : 'satoshi-node' } ,
{ onion : 'peer2xyz9wvu8tsr7qpo6nml5kji4hgf3edc2ba.onion' , pubkey : 'f6e5d4c3b2a1' , name : 'lightning-lab' } ,
{ onion : 'peer3mno6pqr7stu8vwx9yzabc2def3ghi4jkl5.onion' , pubkey : 'c3d4e5f6a1b2' , name : 'sovereign-relay' } ,
] ,
} ,
} )
}
case 'node-check-peer' : {
return res . json ( { result : { onion : params ? . onion || '' , reachable : Math . random ( ) > 0.2 } } )
}
case 'node-add-peer' : {
console . log ( ` [Peers] Added peer: ${ params ? . onion } ` )
return res . json ( { result : { success : true } } )
}
case 'node-send-message' : {
console . log ( ` [Peers] Sent message to: ${ params ? . onion } ` )
return res . json ( { result : { ok : true , sent _to : params ? . onion || '' } } )
}
case 'node-nostr-discover' : {
return res . json ( {
result : {
nodes : [
{ did : 'did:key:z6MkhaXgBZDvotDkL5257faiztiGiC2ReMkBe4bR6XBIDNq9' , onion : 'disc1abc2def3ghi4jkl5mno6pqr7stu8vwx9yz.onion' , pubkey : 'disc1pub' , node _address : '192.168.1.50' } ,
{ did : 'did:key:z6MkpTHR8VNsBxYAAWHut2Geadd9jSwuBV8xRoAnwWsdvktH' , onion : 'disc2xyz9wvu8tsr7qpo6nml5kji4hgf3edc2ba.onion' , pubkey : 'disc2pub' , node _address : '192.168.1.51' } ,
] ,
} ,
} )
}
case 'node-messages-received' :
case 'node.messages' : {
return res . json ( {
result : {
messages : [
{ from _pubkey : 'a1b2c3d4e5f6' , message : 'Hey, your relay is online! Nice uptime.' , timestamp : new Date ( Date . now ( ) - 3600000 ) . toISOString ( ) } ,
{ from _pubkey : 'f6e5d4c3b2a1' , message : 'Channel opened successfully. 500k sats capacity.' , timestamp : new Date ( Date . now ( ) - 7200000 ) . toISOString ( ) } ,
{ from _pubkey : 'c3d4e5f6a1b2' , message : 'Backup sync complete. All good on my end.' , timestamp : new Date ( Date . now ( ) - 86400000 ) . toISOString ( ) } ,
] ,
} ,
} )
}
case 'node.notifications' : {
2026-03-18 19:24:52 +00:00
return res . json ( {
result : [
{
id : 'notif-1' ,
type : 'info' ,
title : 'Bitcoin Core synced' ,
message : 'Blockchain fully synced at block 892,451. IBD complete.' ,
timestamp : new Date ( Date . now ( ) - 1800000 ) . toISOString ( ) ,
read : false ,
} ,
{
id : 'notif-2' ,
type : 'success' ,
title : 'LND channel opened' ,
message : 'Channel opened with ACINQ (500,000 sats capacity). 6 confirmations received.' ,
timestamp : new Date ( Date . now ( ) - 7200000 ) . toISOString ( ) ,
read : false ,
} ,
{
id : 'notif-3' ,
type : 'warning' ,
title : 'Disk usage above 80%' ,
message : 'Storage at 82% capacity. Consider pruning old blockchain data or expanding storage.' ,
timestamp : new Date ( Date . now ( ) - 14400000 ) . toISOString ( ) ,
read : true ,
} ,
{
id : 'notif-4' ,
type : 'info' ,
title : 'System update available' ,
message : 'Archipelago v0.1.1 is available. Review changelog before updating.' ,
timestamp : new Date ( Date . now ( ) - 86400000 ) . toISOString ( ) ,
read : true ,
} ,
{
id : 'notif-5' ,
type : 'success' ,
title : 'Fedimint guardian connected' ,
message : 'Guardian consensus achieved. Mint is operational with 3/4 guardians online.' ,
timestamp : new Date ( Date . now ( ) - 43200000 ) . toISOString ( ) ,
read : false ,
} ,
] ,
} )
2026-03-07 22:36:45 +00:00
}
2026-03-17 01:32:02 +00:00
// =====================================================================
// Federation (multi-node clusters)
// =====================================================================
case 'federation.list-nodes' : {
return res . json ( {
result : {
nodes : [
{
did : 'did:key:z6MkhaXgBZDvotDkL5257faiztiGiC2ReMkBe4bR6XBIDNq9' ,
pubkey : 'a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2' ,
onion : 'peer1abc2def3ghi4jkl5mno6pqr7stu8vwx9yz.onion' ,
trust _level : 'trusted' ,
added _at : '2026-02-15T10:30:00Z' ,
name : 'archy-198' ,
last _seen : new Date ( Date . now ( ) - 120000 ) . toISOString ( ) ,
last _state : {
timestamp : new Date ( Date . now ( ) - 120000 ) . toISOString ( ) ,
apps : [
{ id : 'bitcoin-knots' , status : 'running' , version : '27.1' } ,
{ id : 'lnd' , status : 'running' , version : '0.18.0' } ,
{ id : 'mempool' , status : 'running' , version : '3.0' } ,
{ id : 'electrs' , status : 'running' , version : '0.10.0' } ,
] ,
cpu _usage _percent : 18.3 ,
mem _used _bytes : 6_200_000_000 ,
mem _total _bytes : 16_000_000_000 ,
disk _used _bytes : 820_000_000_000 ,
disk _total _bytes : 1_800_000_000_000 ,
uptime _secs : 604800 ,
tor _active : true ,
} ,
} ,
{
did : 'did:key:z6MkpTHR8VNsBxYAAWHut2Geadd9jSwuBV8xRoAnwWsdvktH' ,
pubkey : 'f6e5d4c3b2a1f6e5d4c3b2a1f6e5d4c3b2a1f6e5d4c3b2a1f6e5d4c3b2a1f6e5' ,
onion : 'peer2xyz9wvu8tsr7qpo6nml5kji4hgf3edc2ba.onion' ,
trust _level : 'trusted' ,
added _at : '2026-03-01T14:00:00Z' ,
name : 'arch-tailscale-1' ,
last _seen : new Date ( Date . now ( ) - 300000 ) . toISOString ( ) ,
last _state : {
timestamp : new Date ( Date . now ( ) - 300000 ) . toISOString ( ) ,
apps : [
{ id : 'bitcoin-knots' , status : 'running' , version : '27.1' } ,
{ id : 'lnd' , status : 'running' , version : '0.18.0' } ,
{ id : 'nextcloud' , status : 'running' , version : '29.0' } ,
] ,
cpu _usage _percent : 42.1 ,
mem _used _bytes : 10_500_000_000 ,
mem _total _bytes : 32_000_000_000 ,
disk _used _bytes : 1_200_000_000_000 ,
disk _total _bytes : 2_000_000_000_000 ,
uptime _secs : 259200 ,
tor _active : true ,
} ,
} ,
{
did : 'did:key:z6MkrHKPxJP6tvCvXMaJKZd3rRA2Y44tyftVhR8FDCMKGFjb' ,
pubkey : 'c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4' ,
onion : 'peer3mno6pqr7stu8vwx9yzabc2def3ghi4jkl5.onion' ,
trust _level : 'observer' ,
added _at : '2026-03-10T09:15:00Z' ,
name : 'bunker-alpha' ,
last _seen : new Date ( Date . now ( ) - 3600000 ) . toISOString ( ) ,
last _state : {
timestamp : new Date ( Date . now ( ) - 3600000 ) . toISOString ( ) ,
apps : [
{ id : 'bitcoin-knots' , status : 'running' , version : '27.1' } ,
{ id : 'vaultwarden' , status : 'running' , version : '1.31.0' } ,
] ,
cpu _usage _percent : 5.8 ,
mem _used _bytes : 2_100_000_000 ,
mem _total _bytes : 8_000_000_000 ,
disk _used _bytes : 450_000_000_000 ,
disk _total _bytes : 1_000_000_000_000 ,
uptime _secs : 1209600 ,
tor _active : false ,
} ,
} ,
] ,
} ,
} )
}
case 'federation.invite' : {
const mockCode = 'fed1:' + Buffer . from ( JSON . stringify ( {
did : 'did:key:z6MkTest228NodeInvite' ,
onion : 'self228abc2def3ghi4jkl5mno6pqr7stu8vwx.onion' ,
pubkey : 'aabbccdd' ,
token : 'mock-invite-token-' + Date . now ( ) ,
} ) ) . toString ( 'base64url' )
return res . json ( {
result : {
code : mockCode ,
did : 'did:key:z6MkTest228NodeInvite' ,
onion : 'self228abc2def3ghi4jkl5mno6pqr7stu8vwx.onion' ,
} ,
} )
}
case 'federation.join' : {
console . log ( ` [Federation] Joining with code: ${ params ? . code ? . substring ( 0 , 20 ) } ... ` )
return res . json ( {
result : {
joined : true ,
node : {
did : 'did:key:z6MkNewJoinedNode' ,
onion : 'newnode123abc456def789ghi012jkl345mno6pqr.onion' ,
pubkey : 'ddeeff11' ,
trust _level : 'trusted' ,
} ,
} ,
} )
}
case 'federation.sync-state' : {
return res . json ( {
result : {
synced : 3 ,
failed : 0 ,
results : [
{ did : 'did:key:z6MkhaXgBZDvotDkL5257faiztiGiC2ReMkBe4bR6XBIDNq9' , status : 'synced' , apps : 4 } ,
{ did : 'did:key:z6MkpTHR8VNsBxYAAWHut2Geadd9jSwuBV8xRoAnwWsdvktH' , status : 'synced' , apps : 3 } ,
{ did : 'did:key:z6MkrHKPxJP6tvCvXMaJKZd3rRA2Y44tyftVhR8FDCMKGFjb' , status : 'synced' , apps : 2 } ,
] ,
} ,
} )
}
case 'federation.set-trust' : {
console . log ( ` [Federation] Set trust: ${ params ? . did } -> ${ params ? . trust _level } ` )
return res . json ( { result : { updated : true , did : params ? . did , trust _level : params ? . trust _level } } )
}
case 'federation.remove-node' : {
console . log ( ` [Federation] Remove node: ${ params ? . did } ` )
return res . json ( { result : { removed : true , nodes _remaining : 2 } } )
}
case 'federation.deploy-app' : {
console . log ( ` [Federation] Deploy app: ${ params ? . app _id } to ${ params ? . target _did } ` )
return res . json ( { result : { deployed : true , app _id : params ? . app _id } } )
}
// =====================================================================
// DWN (Decentralized Web Node)
// =====================================================================
case 'dwn.status' : {
return res . json ( {
result : {
running : true ,
protocols _registered : 3 ,
messages _stored : 47 ,
peers _synced : 2 ,
last _sync : new Date ( Date . now ( ) - 600000 ) . toISOString ( ) ,
protocols : [
{ uri : 'https://archipelago.dev/protocols/node-identity/v1' , published : true , messages : 12 } ,
{ uri : 'https://archipelago.dev/protocols/federation/v1' , published : false , messages : 28 } ,
{ uri : 'https://archipelago.dev/protocols/app-deploy/v1' , published : false , messages : 7 } ,
] ,
} ,
} )
}
case 'dwn.sync' : {
console . log ( '[DWN] Syncing with peers...' )
return res . json ( { result : { synced : true , messages _pulled : 5 , messages _pushed : 3 } } )
}
2026-03-17 00:03:08 +00:00
// =====================================================================
// Mesh Networking (LoRa radio via Meshcore)
// =====================================================================
case 'mesh.status' : {
return res . json ( {
result : {
enabled : true ,
device _type : 'Meshcore' ,
device _path : '/dev/ttyUSB0' ,
device _connected : true ,
firmware _version : '2.3.1' ,
self _node _id : 42 ,
self _advert _name : 'archy-228' ,
peer _count : 4 ,
channel _name : 'archipelago' ,
messages _sent : 23 ,
messages _received : 47 ,
detected _devices : [ '/dev/ttyUSB0' ] ,
} ,
} )
}
case 'mesh.peers' : {
return res . json ( {
result : {
peers : [
{
contact _id : 1 ,
advert _name : 'archy-198' ,
did : 'did:key:z6MkhaXgBZDvotDkL5257faiztiGiC2ReMkBe4bR6XBIDNq9' ,
pubkey _hex : 'a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2' ,
rssi : - 67 ,
snr : 9.5 ,
last _heard : new Date ( Date . now ( ) - 30000 ) . toISOString ( ) ,
hops : 0 ,
} ,
{
contact _id : 2 ,
advert _name : 'satoshi-relay' ,
did : 'did:key:z6MkpTHR8VNsBxYAAWHut2Geadd9jSwuBV8xRoAnwWsdvktH' ,
pubkey _hex : 'f6e5d4c3b2a1f6e5d4c3b2a1f6e5d4c3b2a1f6e5d4c3b2a1f6e5d4c3b2a1f6e5' ,
rssi : - 82 ,
snr : 4.2 ,
last _heard : new Date ( Date . now ( ) - 120000 ) . toISOString ( ) ,
hops : 1 ,
} ,
{
contact _id : 3 ,
advert _name : 'mountain-node' ,
did : null ,
pubkey _hex : null ,
rssi : - 95 ,
snr : 1.8 ,
last _heard : new Date ( Date . now ( ) - 600000 ) . toISOString ( ) ,
hops : 2 ,
} ,
{
contact _id : 4 ,
advert _name : 'bunker-alpha' ,
did : 'did:key:z6MkrHKPxJP6tvCvXMaJKZd3rRA2Y44tyftVhR8FDCMKGFjb' ,
pubkey _hex : 'c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4' ,
rssi : - 74 ,
snr : 7.1 ,
last _heard : new Date ( Date . now ( ) - 45000 ) . toISOString ( ) ,
hops : 0 ,
} ,
] ,
count : 4 ,
} ,
} )
}
case 'mesh.messages' : {
const limit = params ? . limit || 100
const now = Date . now ( )
const allMessages = [
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-22 03:30:21 +00:00
case 'mesh.deadman-status' : {
return res . json ( {
result : {
dead _man _enabled : false ,
dead _man _interval _secs : 21600 ,
time _remaining _secs : 21600 ,
triggered : false ,
has _gps : false ,
emergency _contacts : 2 ,
} ,
} )
}
case 'mesh.deadman-configure' : {
const { enabled , interval _secs } = params || { }
console . log ( ` [Mesh] Deadman configured: enabled= ${ enabled } , interval= ${ interval _secs } ` )
return res . json ( {
result : {
dead _man _enabled : enabled ? ? false ,
dead _man _interval _secs : interval _secs ? ? 21600 ,
time _remaining _secs : interval _secs ? ? 21600 ,
triggered : false ,
has _gps : false ,
emergency _contacts : 2 ,
} ,
} )
}
case 'mesh.deadman-checkin' : {
return res . json ( {
result : {
checked _in : true ,
time _remaining _secs : 21600 ,
} ,
} )
}
case 'mesh.block-headers' : {
return res . json ( {
result : {
headers : [
{ height : 893421 , hash : '0000000000000000000234a6b12dc03e5c4f7e891d2f34b5a678cd9012345678' , timestamp : new Date ( Date . now ( ) - 600000 ) . toISOString ( ) } ,
{ height : 893420 , hash : '00000000000000000001bc7d89ef01234567890abcdef1234567890abcdef12' , timestamp : new Date ( Date . now ( ) - 1200000 ) . toISOString ( ) } ,
{ height : 893419 , hash : '00000000000000000003ef4a56789012bcdef34567890abcdef1234567890ab' , timestamp : new Date ( Date . now ( ) - 1800000 ) . toISOString ( ) } ,
] ,
latest _height : 893421 ,
count : 3 ,
} ,
} )
}
case 'mesh.relay-tx' : {
return res . json ( {
result : {
request _id : Math . floor ( Math . random ( ) * 10000 ) ,
queued : true ,
tx _hex _len : ( params ? . tx _hex || '' ) . length ,
} ,
} )
}
case 'mesh.relay-status' : {
return res . json ( {
result : {
relayed : true ,
pending _count : 0 ,
status : 'confirmed' ,
txid : 'a1b2c3d4e5f6789012345678901234567890abcdef1234567890abcdef123456' ,
} ,
} )
}
case 'mesh.relay-lightning' : {
return res . json ( {
result : {
request _id : Math . floor ( Math . random ( ) * 10000 ) ,
queued : true ,
amount _sats : params ? . amount _sats || 0 ,
} ,
} )
}
2026-03-17 00:03:08 +00:00
// =====================================================================
// Transport Layer (unified routing: mesh > lan > tor)
// =====================================================================
case 'transport.status' : {
return res . json ( {
result : {
transports : [
{ kind : 'mesh' , available : true } ,
{ kind : 'lan' , available : true } ,
{ kind : 'tor' , available : true } ,
] ,
mesh _only : false ,
peer _count : 5 ,
} ,
} )
}
case 'transport.peers' : {
return res . json ( {
result : {
peers : [
{
did : 'did:key:z6MkhaXgBZDvotDkL5257faiztiGiC2ReMkBe4bR6XBIDNq9' ,
pubkey _hex : 'a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2' ,
name : 'archy-198' ,
trust _level : 'trusted' ,
mesh _contact _id : 1 ,
lan _address : '192.168.1.198:5678' ,
onion _address : 'peer1abc2def3ghi4jkl5mno6pqr7stu8vwx9yz.onion' ,
preferred _transport : 'lan' ,
available _transports : [ 'mesh' , 'lan' , 'tor' ] ,
last _seen : new Date ( Date . now ( ) - 30000 ) . toISOString ( ) ,
} ,
{
did : 'did:key:z6MkpTHR8VNsBxYAAWHut2Geadd9jSwuBV8xRoAnwWsdvktH' ,
pubkey _hex : 'f6e5d4c3b2a1f6e5d4c3b2a1f6e5d4c3b2a1f6e5d4c3b2a1f6e5d4c3b2a1f6e5' ,
name : 'satoshi-relay' ,
trust _level : 'trusted' ,
mesh _contact _id : 2 ,
lan _address : null ,
onion _address : 'peer2xyz9wvu8tsr7qpo6nml5kji4hgf3edc2ba.onion' ,
preferred _transport : 'mesh' ,
available _transports : [ 'mesh' , 'tor' ] ,
last _seen : new Date ( Date . now ( ) - 120000 ) . toISOString ( ) ,
} ,
{
did : 'did:key:z6MkrHKPxJP6tvCvXMaJKZd3rRA2Y44tyftVhR8FDCMKGFjb' ,
pubkey _hex : 'c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4' ,
name : 'bunker-alpha' ,
trust _level : 'observer' ,
mesh _contact _id : 4 ,
lan _address : null ,
onion _address : null ,
preferred _transport : 'mesh' ,
available _transports : [ 'mesh' ] ,
last _seen : new Date ( Date . now ( ) - 45000 ) . toISOString ( ) ,
} ,
{
did : 'did:key:z6MkjchhfUsD6mmvni8mCdXHw216Xrm9bQe2mBH1P5RDjVJG' ,
pubkey _hex : 'd4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5' ,
name : 'office-node' ,
trust _level : 'trusted' ,
mesh _contact _id : null ,
lan _address : '192.168.1.42:5678' ,
onion _address : 'peer4mno6pqr7stu8vwx9yzabc2def3ghi4jkl5.onion' ,
preferred _transport : 'lan' ,
available _transports : [ 'lan' , 'tor' ] ,
last _seen : new Date ( Date . now ( ) - 60000 ) . toISOString ( ) ,
} ,
{
did : 'did:key:z6MknGc3ocHs3zdPiJbnaaqDi58NGb4pk1Sp7NQD5EjEREWh' ,
pubkey _hex : 'e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6' ,
name : 'remote-cabin' ,
trust _level : 'trusted' ,
mesh _contact _id : null ,
lan _address : null ,
onion _address : 'peer5xyz9abc2def3ghi4jkl5mno6pqr7stu8vw.onion' ,
preferred _transport : 'tor' ,
available _transports : [ 'tor' ] ,
last _seen : new Date ( Date . now ( ) - 300000 ) . toISOString ( ) ,
} ,
] ,
} ,
} )
}
case 'transport.send' : {
const targetDid = params ? . did
console . log ( ` [Transport] Send to ${ targetDid } via best transport ` )
return res . json ( {
result : {
sent : true ,
transport _used : 'mesh' ,
did : targetDid ,
} ,
} )
}
case 'transport.set-mode' : {
const meshOnly = params ? . mesh _only ? ? false
console . log ( ` [Transport] Set mesh_only mode: ${ meshOnly } ` )
return res . json ( { result : { mesh _only : meshOnly , configured : true } } )
}
2026-03-18 21:06:14 +00:00
// =====================================================================
// LND / Lightning
// =====================================================================
case 'lnd.getinfo' : {
return res . json ( {
result : {
alias : 'archy-signet' ,
color : '#f7931a' ,
num _active _channels : 4 ,
num _inactive _channels : 1 ,
num _pending _channels : 1 ,
feat(TASK-12): beta telemetry — report endpoint + settings toggle
Backend: telemetry.report RPC builds anonymous health report with node ID
(SHA-256 hash of pubkey, truncated), version, uptime, container states,
CPU/RAM, federation peers, and recent alerts. Saves latest report to disk.
Requires analytics opt-in (existing analytics.enable/disable flow).
Frontend: "Beta Telemetry" section in Settings with enable/disable toggle.
Shows what data is and isn't collected. Mock backend handles all analytics
and telemetry RPCs.
Privacy: No wallet data, no private keys, no DIDs, no IP addresses.
Node identified by truncated hash only.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-18 23:14:47 +00:00
block _height : walletState . block _height ,
2026-03-18 21:06:14 +00:00
synced _to _chain : true ,
synced _to _graph : true ,
version : '0.17.4-beta' ,
identity _pubkey : '03a1b2c3d4e5f6789012345678901234567890abcdef1234567890abcdef123456' ,
chains : [ { chain : 'bitcoin' , network : 'signet' } ] ,
feat(TASK-12): beta telemetry — report endpoint + settings toggle
Backend: telemetry.report RPC builds anonymous health report with node ID
(SHA-256 hash of pubkey, truncated), version, uptime, container states,
CPU/RAM, federation peers, and recent alerts. Saves latest report to disk.
Requires analytics opt-in (existing analytics.enable/disable flow).
Frontend: "Beta Telemetry" section in Settings with enable/disable toggle.
Shows what data is and isn't collected. Mock backend handles all analytics
and telemetry RPCs.
Privacy: No wallet data, no private keys, no DIDs, no IP addresses.
Node identified by truncated hash only.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-18 23:14:47 +00:00
balance _sats : walletState . onchain _sats ,
channel _balance _sats : walletState . channel _sats ,
2026-03-18 21:06:14 +00:00
} ,
} )
}
case 'lnd.gettransactions' : {
feat(TASK-12): beta telemetry — report endpoint + settings toggle
Backend: telemetry.report RPC builds anonymous health report with node ID
(SHA-256 hash of pubkey, truncated), version, uptime, container states,
CPU/RAM, federation peers, and recent alerts. Saves latest report to disk.
Requires analytics opt-in (existing analytics.enable/disable flow).
Frontend: "Beta Telemetry" section in Settings with enable/disable toggle.
Shows what data is and isn't collected. Mock backend handles all analytics
and telemetry RPCs.
Privacy: No wallet data, no private keys, no DIDs, no IP addresses.
Node identified by truncated hash only.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-18 23:14:47 +00:00
const pending = walletState . transactions . filter ( tx => tx . direction === 'incoming' && tx . num _confirmations < 3 ) . length
2026-03-18 21:06:14 +00:00
return res . json ( {
result : {
feat(TASK-12): beta telemetry — report endpoint + settings toggle
Backend: telemetry.report RPC builds anonymous health report with node ID
(SHA-256 hash of pubkey, truncated), version, uptime, container states,
CPU/RAM, federation peers, and recent alerts. Saves latest report to disk.
Requires analytics opt-in (existing analytics.enable/disable flow).
Frontend: "Beta Telemetry" section in Settings with enable/disable toggle.
Shows what data is and isn't collected. Mock backend handles all analytics
and telemetry RPCs.
Privacy: No wallet data, no private keys, no DIDs, no IP addresses.
Node identified by truncated hash only.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-18 23:14:47 +00:00
transactions : walletState . transactions ,
incoming _pending _count : pending ,
2026-03-18 21:06:14 +00:00
} ,
} )
}
case 'lnd.channelbalance' : {
return res . json ( {
result : {
feat(TASK-12): beta telemetry — report endpoint + settings toggle
Backend: telemetry.report RPC builds anonymous health report with node ID
(SHA-256 hash of pubkey, truncated), version, uptime, container states,
CPU/RAM, federation peers, and recent alerts. Saves latest report to disk.
Requires analytics opt-in (existing analytics.enable/disable flow).
Frontend: "Beta Telemetry" section in Settings with enable/disable toggle.
Shows what data is and isn't collected. Mock backend handles all analytics
and telemetry RPCs.
Privacy: No wallet data, no private keys, no DIDs, no IP addresses.
Node identified by truncated hash only.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-18 23:14:47 +00:00
local _balance : { sat : walletState . channel _sats } ,
2026-03-18 21:06:14 +00:00
remote _balance : { sat : 11750000 } ,
pending _open _local _balance : { sat : 500000 } ,
} ,
} )
}
case 'lnd.walletbalance' : {
return res . json ( {
result : {
feat(TASK-12): beta telemetry — report endpoint + settings toggle
Backend: telemetry.report RPC builds anonymous health report with node ID
(SHA-256 hash of pubkey, truncated), version, uptime, container states,
CPU/RAM, federation peers, and recent alerts. Saves latest report to disk.
Requires analytics opt-in (existing analytics.enable/disable flow).
Frontend: "Beta Telemetry" section in Settings with enable/disable toggle.
Shows what data is and isn't collected. Mock backend handles all analytics
and telemetry RPCs.
Privacy: No wallet data, no private keys, no DIDs, no IP addresses.
Node identified by truncated hash only.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-18 23:14:47 +00:00
total _balance : walletState . onchain _sats + 100000 ,
confirmed _balance : walletState . onchain _sats ,
2026-03-18 21:06:14 +00:00
unconfirmed _balance : 100000 ,
} ,
} )
}
case 'lnd.listchannels' : {
return res . json ( {
result : {
channels : [
{ chan _id : '840921088114688' , remote _pubkey : '02778f4a' , capacity : 5000000 , local _balance : 2450000 , remote _balance : 2550000 , active : true , peer _alias : 'ACINQ Signet' } ,
{ chan _id : '840921088114689' , remote _pubkey : '03abcdef' , capacity : 2000000 , local _balance : 1200000 , remote _balance : 800000 , active : true , peer _alias : 'WalletOfSatoshi' } ,
{ chan _id : '840921088114690' , remote _pubkey : '02fedcba' , capacity : 10000000 , local _balance : 4500000 , remote _balance : 5500000 , active : true , peer _alias : 'Voltage' } ,
{ chan _id : '840921088114691' , remote _pubkey : '03456789' , capacity : 3000000 , local _balance : 100000 , remote _balance : 2900000 , active : true , peer _alias : 'Kraken' } ,
] ,
} ,
} )
}
case 'lnd.newaddress' : {
const addrType = params ? . type || 'p2wkh'
const mockAddr = addrType === 'p2tr'
feat(TASK-12): beta telemetry — report endpoint + settings toggle
Backend: telemetry.report RPC builds anonymous health report with node ID
(SHA-256 hash of pubkey, truncated), version, uptime, container states,
CPU/RAM, federation peers, and recent alerts. Saves latest report to disk.
Requires analytics opt-in (existing analytics.enable/disable flow).
Frontend: "Beta Telemetry" section in Settings with enable/disable toggle.
Shows what data is and isn't collected. Mock backend handles all analytics
and telemetry RPCs.
Privacy: No wallet data, no private keys, no DIDs, no IP addresses.
Node identified by truncated hash only.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-18 23:14:47 +00:00
? 'tb1p' + randomHex ( 29 )
: 'tb1q' + randomHex ( 19 )
2026-03-18 21:06:14 +00:00
return res . json ( { result : { address : mockAddr } } )
}
case 'lnd.addinvoice' :
case 'lnd.createinvoice' : {
const amt = params ? . amt || params ? . value || params ? . amount _sats || 1000
feat(TASK-12): beta telemetry — report endpoint + settings toggle
Backend: telemetry.report RPC builds anonymous health report with node ID
(SHA-256 hash of pubkey, truncated), version, uptime, container states,
CPU/RAM, federation peers, and recent alerts. Saves latest report to disk.
Requires analytics opt-in (existing analytics.enable/disable flow).
Frontend: "Beta Telemetry" section in Settings with enable/disable toggle.
Shows what data is and isn't collected. Mock backend handles all analytics
and telemetry RPCs.
Privacy: No wallet data, no private keys, no DIDs, no IP addresses.
Node identified by truncated hash only.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-18 23:14:47 +00:00
const rHash = randomHex ( 32 )
2026-03-18 21:06:14 +00:00
return res . json ( {
result : {
r _hash : rHash ,
payment _request : ` lnsb ${ amt } n1pjmock ${ Date . now ( ) . toString ( 36 ) } qqqxqyz5vqsp5mock ${ rHash . slice ( 0 , 20 ) } ` ,
add _index : Date . now ( ) ,
feat(TASK-12): beta telemetry — report endpoint + settings toggle
Backend: telemetry.report RPC builds anonymous health report with node ID
(SHA-256 hash of pubkey, truncated), version, uptime, container states,
CPU/RAM, federation peers, and recent alerts. Saves latest report to disk.
Requires analytics opt-in (existing analytics.enable/disable flow).
Frontend: "Beta Telemetry" section in Settings with enable/disable toggle.
Shows what data is and isn't collected. Mock backend handles all analytics
and telemetry RPCs.
Privacy: No wallet data, no private keys, no DIDs, no IP addresses.
Node identified by truncated hash only.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-18 23:14:47 +00:00
payment _addr : randomHex ( 32 ) ,
2026-03-18 21:06:14 +00:00
} ,
} )
}
case 'lnd.payinvoice' :
case 'lnd.sendpayment' : {
feat(TASK-12): beta telemetry — report endpoint + settings toggle
Backend: telemetry.report RPC builds anonymous health report with node ID
(SHA-256 hash of pubkey, truncated), version, uptime, container states,
CPU/RAM, federation peers, and recent alerts. Saves latest report to disk.
Requires analytics opt-in (existing analytics.enable/disable flow).
Frontend: "Beta Telemetry" section in Settings with enable/disable toggle.
Shows what data is and isn't collected. Mock backend handles all analytics
and telemetry RPCs.
Privacy: No wallet data, no private keys, no DIDs, no IP addresses.
Node identified by truncated hash only.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-18 23:14:47 +00:00
const amt = params ? . amt || params ? . amount _sats || 1000
const fee = Math . floor ( Math . random ( ) * 10 ) + 1
walletState . channel _sats = Math . max ( 0 , walletState . channel _sats - amt - fee )
2026-03-18 21:06:14 +00:00
return res . json ( {
result : {
feat(TASK-12): beta telemetry — report endpoint + settings toggle
Backend: telemetry.report RPC builds anonymous health report with node ID
(SHA-256 hash of pubkey, truncated), version, uptime, container states,
CPU/RAM, federation peers, and recent alerts. Saves latest report to disk.
Requires analytics opt-in (existing analytics.enable/disable flow).
Frontend: "Beta Telemetry" section in Settings with enable/disable toggle.
Shows what data is and isn't collected. Mock backend handles all analytics
and telemetry RPCs.
Privacy: No wallet data, no private keys, no DIDs, no IP addresses.
Node identified by truncated hash only.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-18 23:14:47 +00:00
payment _hash : randomHex ( 32 ) ,
payment _preimage : randomHex ( 32 ) ,
2026-03-18 21:06:14 +00:00
status : 'SUCCEEDED' ,
feat(TASK-12): beta telemetry — report endpoint + settings toggle
Backend: telemetry.report RPC builds anonymous health report with node ID
(SHA-256 hash of pubkey, truncated), version, uptime, container states,
CPU/RAM, federation peers, and recent alerts. Saves latest report to disk.
Requires analytics opt-in (existing analytics.enable/disable flow).
Frontend: "Beta Telemetry" section in Settings with enable/disable toggle.
Shows what data is and isn't collected. Mock backend handles all analytics
and telemetry RPCs.
Privacy: No wallet data, no private keys, no DIDs, no IP addresses.
Node identified by truncated hash only.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-18 23:14:47 +00:00
fee _sat : fee ,
value _sat : amt ,
2026-03-18 21:06:14 +00:00
} ,
} )
}
case 'lnd.sendcoins' : {
const amt = params ? . amount || params ? . amt || 50000
feat(TASK-12): beta telemetry — report endpoint + settings toggle
Backend: telemetry.report RPC builds anonymous health report with node ID
(SHA-256 hash of pubkey, truncated), version, uptime, container states,
CPU/RAM, federation peers, and recent alerts. Saves latest report to disk.
Requires analytics opt-in (existing analytics.enable/disable flow).
Frontend: "Beta Telemetry" section in Settings with enable/disable toggle.
Shows what data is and isn't collected. Mock backend handles all analytics
and telemetry RPCs.
Privacy: No wallet data, no private keys, no DIDs, no IP addresses.
Node identified by truncated hash only.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-18 23:14:47 +00:00
walletState . onchain _sats = Math . max ( 0 , walletState . onchain _sats - amt )
const txid = randomHex ( 32 )
walletState . transactions . unshift ( {
tx _hash : txid , amount _sats : - amt , direction : 'outgoing' , num _confirmations : 0 ,
block _height : 0 , time _stamp : Math . floor ( Date . now ( ) / 1000 ) , label : 'Sent on-chain' ,
total _fees : 250 , dest _addresses : [ params ? . addr || '' ] ,
} )
2026-03-18 21:06:14 +00:00
return res . json ( {
feat(TASK-12): beta telemetry — report endpoint + settings toggle
Backend: telemetry.report RPC builds anonymous health report with node ID
(SHA-256 hash of pubkey, truncated), version, uptime, container states,
CPU/RAM, federation peers, and recent alerts. Saves latest report to disk.
Requires analytics opt-in (existing analytics.enable/disable flow).
Frontend: "Beta Telemetry" section in Settings with enable/disable toggle.
Shows what data is and isn't collected. Mock backend handles all analytics
and telemetry RPCs.
Privacy: No wallet data, no private keys, no DIDs, no IP addresses.
Node identified by truncated hash only.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-18 23:14:47 +00:00
result : { txid , amount : amt } ,
2026-03-18 21:06:14 +00:00
} )
}
case 'lnd.decodepayreq' : {
return res . json ( {
result : {
destination : '03a1b2c3d4e5f6789012345678901234567890abcdef1234567890abcdef123456' ,
num _satoshis : params ? . pay _req ? . match ( /lnsb(\d+)/ ) ? . [ 1 ] || '1000' ,
description : 'Mock decoded invoice' ,
expiry : 3600 ,
timestamp : Math . floor ( Date . now ( ) / 1000 ) ,
} ,
} )
}
case 'lnd.openchannel' : {
return res . json ( {
result : {
funding _txid : Array . from ( { length : 32 } , ( ) => Math . floor ( Math . random ( ) * 256 ) . toString ( 16 ) . padStart ( 2 , '0' ) ) . join ( '' ) ,
output _index : 0 ,
} ,
} )
}
case 'lnd.closechannel' : {
return res . json ( {
result : {
closing _txid : Array . from ( { length : 32 } , ( ) => Math . floor ( Math . random ( ) * 256 ) . toString ( 16 ) . padStart ( 2 , '0' ) ) . join ( '' ) ,
} ,
} )
}
case 'lnd.listinvoices' : {
return res . json ( { result : { invoices : MOCK _LND _DATA . invoices } } )
}
case 'lnd.listpayments' : {
return res . json ( { result : { payments : MOCK _LND _DATA . payments } } )
}
case 'lnd.create-psbt' :
case 'lnd.finalize-psbt' :
case 'lnd.create-raw-tx' : {
return res . json ( {
result : {
psbt : 'cHNidP8BAH0CAAAA...mockPSBT' ,
txid : Array . from ( { length : 32 } , ( ) => Math . floor ( Math . random ( ) * 256 ) . toString ( 16 ) . padStart ( 2 , '0' ) ) . join ( '' ) ,
} ,
} )
}
// =====================================================================
// Wallet / Ecash (Fedimint)
// =====================================================================
case 'wallet.ecash-balance' : {
return res . json ( {
result : {
feat(TASK-12): beta telemetry — report endpoint + settings toggle
Backend: telemetry.report RPC builds anonymous health report with node ID
(SHA-256 hash of pubkey, truncated), version, uptime, container states,
CPU/RAM, federation peers, and recent alerts. Saves latest report to disk.
Requires analytics opt-in (existing analytics.enable/disable flow).
Frontend: "Beta Telemetry" section in Settings with enable/disable toggle.
Shows what data is and isn't collected. Mock backend handles all analytics
and telemetry RPCs.
Privacy: No wallet data, no private keys, no DIDs, no IP addresses.
Node identified by truncated hash only.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-18 23:14:47 +00:00
balance _sats : walletState . ecash _sats ,
balance _msat : walletState . ecash _sats * 1000 ,
token _count : walletState . ecash _tokens ,
2026-03-18 21:06:14 +00:00
federations : [
feat(TASK-12): beta telemetry — report endpoint + settings toggle
Backend: telemetry.report RPC builds anonymous health report with node ID
(SHA-256 hash of pubkey, truncated), version, uptime, container states,
CPU/RAM, federation peers, and recent alerts. Saves latest report to disk.
Requires analytics opt-in (existing analytics.enable/disable flow).
Frontend: "Beta Telemetry" section in Settings with enable/disable toggle.
Shows what data is and isn't collected. Mock backend handles all analytics
and telemetry RPCs.
Privacy: No wallet data, no private keys, no DIDs, no IP addresses.
Node identified by truncated hash only.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-18 23:14:47 +00:00
{ federation _id : 'fed1-demo' , name : 'Archy Signet Mint' , balance _msat : walletState . ecash _sats * 1000 , gateway _active : true } ,
2026-03-18 21:06:14 +00:00
] ,
} ,
} )
}
case 'wallet.ecash-send' : {
const amt = params ? . amount _sats || 1000
feat(TASK-12): beta telemetry — report endpoint + settings toggle
Backend: telemetry.report RPC builds anonymous health report with node ID
(SHA-256 hash of pubkey, truncated), version, uptime, container states,
CPU/RAM, federation peers, and recent alerts. Saves latest report to disk.
Requires analytics opt-in (existing analytics.enable/disable flow).
Frontend: "Beta Telemetry" section in Settings with enable/disable toggle.
Shows what data is and isn't collected. Mock backend handles all analytics
and telemetry RPCs.
Privacy: No wallet data, no private keys, no DIDs, no IP addresses.
Node identified by truncated hash only.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-18 23:14:47 +00:00
walletState . ecash _sats = Math . max ( 0 , walletState . ecash _sats - amt )
walletState . ecash _tokens = Math . max ( 0 , walletState . ecash _tokens - 1 )
2026-03-18 21:06:14 +00:00
return res . json ( {
result : {
token : ` cashuSend_mock_ ${ amt } _ ${ Date . now ( ) . toString ( 36 ) } ` ,
amount _sats : amt ,
} ,
} )
}
case 'wallet.ecash-receive' : {
feat(TASK-12): beta telemetry — report endpoint + settings toggle
Backend: telemetry.report RPC builds anonymous health report with node ID
(SHA-256 hash of pubkey, truncated), version, uptime, container states,
CPU/RAM, federation peers, and recent alerts. Saves latest report to disk.
Requires analytics opt-in (existing analytics.enable/disable flow).
Frontend: "Beta Telemetry" section in Settings with enable/disable toggle.
Shows what data is and isn't collected. Mock backend handles all analytics
and telemetry RPCs.
Privacy: No wallet data, no private keys, no DIDs, no IP addresses.
Node identified by truncated hash only.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-18 23:14:47 +00:00
const amt = 5000
walletState . ecash _sats += amt
walletState . ecash _tokens += 1
2026-03-18 21:06:14 +00:00
return res . json ( {
result : {
feat(TASK-12): beta telemetry — report endpoint + settings toggle
Backend: telemetry.report RPC builds anonymous health report with node ID
(SHA-256 hash of pubkey, truncated), version, uptime, container states,
CPU/RAM, federation peers, and recent alerts. Saves latest report to disk.
Requires analytics opt-in (existing analytics.enable/disable flow).
Frontend: "Beta Telemetry" section in Settings with enable/disable toggle.
Shows what data is and isn't collected. Mock backend handles all analytics
and telemetry RPCs.
Privacy: No wallet data, no private keys, no DIDs, no IP addresses.
Node identified by truncated hash only.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-18 23:14:47 +00:00
amount _sats : amt ,
2026-03-18 21:06:14 +00:00
federation _id : 'fed1-demo' ,
} ,
} )
}
case 'wallet.ecash-history' : {
return res . json ( {
result : {
transactions : [
{ type : 'receive' , amount _sats : 50000 , timestamp : new Date ( Date . now ( ) - 86400000 ) . toISOString ( ) , note : 'Minted from Lightning' } ,
{ type : 'send' , amount _sats : 5000 , timestamp : new Date ( Date . now ( ) - 43200000 ) . toISOString ( ) , note : 'Sent ecash token' } ,
{ type : 'receive' , amount _sats : 10000 , timestamp : new Date ( Date . now ( ) - 3600000 ) . toISOString ( ) , note : 'Redeemed token' } ,
] ,
} ,
} )
}
case 'wallet.networking-profits' : {
return res . json ( {
result : {
total _earned _sats : 42 ,
total _forwarded _sats : 35025 ,
forward _count : 3 ,
period _days : 30 ,
daily _avg _sats : 1.4 ,
} ,
} )
}
case 'dev.faucet' : {
const amount = params ? . amount _sats || 1_000_000
feat(TASK-12): beta telemetry — report endpoint + settings toggle
Backend: telemetry.report RPC builds anonymous health report with node ID
(SHA-256 hash of pubkey, truncated), version, uptime, container states,
CPU/RAM, federation peers, and recent alerts. Saves latest report to disk.
Requires analytics opt-in (existing analytics.enable/disable flow).
Frontend: "Beta Telemetry" section in Settings with enable/disable toggle.
Shows what data is and isn't collected. Mock backend handles all analytics
and telemetry RPCs.
Privacy: No wallet data, no private keys, no DIDs, no IP addresses.
Node identified by truncated hash only.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-18 23:14:47 +00:00
const ecashAmt = Math . floor ( amount / 10 )
walletState . onchain _sats += amount
walletState . channel _sats += amount
walletState . ecash _sats += ecashAmt
walletState . ecash _tokens += 1
const txid = randomHex ( 32 )
walletState . transactions . unshift ( {
tx _hash : txid , amount _sats : amount , direction : 'incoming' , num _confirmations : 0 ,
block _height : 0 , time _stamp : Math . floor ( Date . now ( ) / 1000 ) , label : 'Dev faucet' ,
total _fees : 0 , dest _addresses : [ ] ,
} )
console . log ( ` [Dev Faucet] + ${ amount } on-chain, + ${ amount } Lightning, + ${ ecashAmt } ecash ` )
2026-03-18 21:06:14 +00:00
return res . json ( {
result : {
feat(TASK-12): beta telemetry — report endpoint + settings toggle
Backend: telemetry.report RPC builds anonymous health report with node ID
(SHA-256 hash of pubkey, truncated), version, uptime, container states,
CPU/RAM, federation peers, and recent alerts. Saves latest report to disk.
Requires analytics opt-in (existing analytics.enable/disable flow).
Frontend: "Beta Telemetry" section in Settings with enable/disable toggle.
Shows what data is and isn't collected. Mock backend handles all analytics
and telemetry RPCs.
Privacy: No wallet data, no private keys, no DIDs, no IP addresses.
Node identified by truncated hash only.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-18 23:14:47 +00:00
onchain : { txid , amount _sats : amount } ,
lightning : { payment _hash : randomHex ( 32 ) , amount _sats : amount } ,
ecash : { token : ` cashuSend_faucet_ ${ amount } _ ${ Date . now ( ) . toString ( 36 ) } ` , amount _sats : ecashAmt } ,
message : ` Added ${ amount } sats on-chain, ${ amount } sats Lightning, ${ ecashAmt } sats ecash ` ,
2026-03-18 21:06:14 +00:00
} ,
} )
}
case 'bitcoin.getinfo' : {
return res . json ( {
result : {
chain : 'signet' ,
blocks : 892451 ,
headers : 892451 ,
bestblockhash : 'a1b2c3d4e5f6' + '0' . repeat ( 58 ) ,
difficulty : 0.001126515290698186 ,
mediantime : Math . floor ( Date . now ( ) / 1000 ) - 300 ,
verificationprogress : 1.0 ,
chainwork : '000000000000000000000000000000000000000000000000000000000001a2b3' ,
size _on _disk : 210_000_000 ,
pruned : false ,
network : 'signet' ,
} ,
} )
}
// =====================================================================
feat(TASK-12): beta telemetry — report endpoint + settings toggle
Backend: telemetry.report RPC builds anonymous health report with node ID
(SHA-256 hash of pubkey, truncated), version, uptime, container states,
CPU/RAM, federation peers, and recent alerts. Saves latest report to disk.
Requires analytics opt-in (existing analytics.enable/disable flow).
Frontend: "Beta Telemetry" section in Settings with enable/disable toggle.
Shows what data is and isn't collected. Mock backend handles all analytics
and telemetry RPCs.
Privacy: No wallet data, no private keys, no DIDs, no IP addresses.
Node identified by truncated hash only.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-18 23:14:47 +00:00
// Analytics / Telemetry
// =====================================================================
case 'analytics.get-status' : {
return res . json ( { result : { enabled : mockState . analyticsEnabled || false , description : 'Anonymous aggregate statistics. No personal data collected.' } } )
}
case 'analytics.enable' : {
mockState . analyticsEnabled = true
return res . json ( { result : { enabled : true } } )
}
case 'analytics.disable' : {
mockState . analyticsEnabled = false
return res . json ( { result : { enabled : false } } )
}
2026-03-22 03:30:21 +00:00
case 'telemetry.fleet-status' : {
return res . json ( {
result : {
nodes : [
{
node _id : 'archy-228' ,
version : '0.1.0' ,
uptime _secs : 604800 ,
cpu _cores : 4 ,
cpu _pct : + ( 15 + Math . random ( ) * 20 ) . toFixed ( 1 ) ,
mem _pct : + ( 38 + Math . random ( ) * 10 ) . toFixed ( 1 ) ,
disk _pct : 34.2 ,
container _count : 12 ,
running _count : 10 ,
federation _peers : 4 ,
recent _alerts : [ ] ,
containers : [
{ id : 'bitcoin' , state : 'running' , version : '27.0' } ,
{ id : 'lnd' , state : 'running' , version : '0.18.0' } ,
{ id : 'electrs' , state : 'running' , version : '0.10.6' } ,
{ id : 'mempool' , state : 'running' , version : '3.0.0' } ,
] ,
reported _at : new Date ( ) . toISOString ( ) ,
} ,
{
node _id : 'archy-198' ,
version : '0.1.0' ,
uptime _secs : 259200 ,
cpu _cores : 4 ,
cpu _pct : + ( 8 + Math . random ( ) * 12 ) . toFixed ( 1 ) ,
mem _pct : + ( 25 + Math . random ( ) * 8 ) . toFixed ( 1 ) ,
disk _pct : 22.7 ,
container _count : 8 ,
running _count : 7 ,
federation _peers : 4 ,
recent _alerts : [ { rule : 'container_crash' , message : 'electrs restarted 2x in 1h' , timestamp : new Date ( Date . now ( ) - 7200000 ) . toISOString ( ) } ] ,
containers : [
{ id : 'bitcoin' , state : 'running' , version : '27.0' } ,
{ id : 'lnd' , state : 'running' , version : '0.18.0' } ,
{ id : 'electrs' , state : 'running' , version : '0.10.6' } ,
] ,
reported _at : new Date ( Date . now ( ) - 60000 ) . toISOString ( ) ,
} ,
{
node _id : 'arch-1' ,
version : '0.1.0' ,
uptime _secs : 172800 ,
cpu _cores : 2 ,
cpu _pct : + ( 5 + Math . random ( ) * 10 ) . toFixed ( 1 ) ,
mem _pct : + ( 45 + Math . random ( ) * 15 ) . toFixed ( 1 ) ,
disk _pct : 61.3 ,
container _count : 6 ,
running _count : 6 ,
federation _peers : 4 ,
recent _alerts : [ ] ,
containers : [
{ id : 'bitcoin' , state : 'running' , version : '27.0' } ,
{ id : 'lnd' , state : 'running' , version : '0.18.0' } ,
] ,
reported _at : new Date ( Date . now ( ) - 120000 ) . toISOString ( ) ,
} ,
{
node _id : 'arch-2' ,
version : '0.1.0' ,
uptime _secs : 86400 ,
cpu _cores : 2 ,
cpu _pct : + ( 3 + Math . random ( ) * 8 ) . toFixed ( 1 ) ,
mem _pct : + ( 30 + Math . random ( ) * 10 ) . toFixed ( 1 ) ,
disk _pct : 18.9 ,
container _count : 5 ,
running _count : 5 ,
federation _peers : 4 ,
recent _alerts : [ ] ,
containers : [
{ id : 'bitcoin' , state : 'running' , version : '27.0' } ,
] ,
reported _at : new Date ( Date . now ( ) - 300000 ) . toISOString ( ) ,
} ,
{
node _id : 'arch-3' ,
version : '0.1.0' ,
uptime _secs : 43200 ,
cpu _cores : 4 ,
cpu _pct : + ( 20 + Math . random ( ) * 15 ) . toFixed ( 1 ) ,
mem _pct : + ( 55 + Math . random ( ) * 10 ) . toFixed ( 1 ) ,
disk _pct : 47.5 ,
container _count : 10 ,
running _count : 9 ,
federation _peers : 4 ,
recent _alerts : [ { rule : 'disk_warning' , message : 'Disk usage approaching 50%' , timestamp : new Date ( Date . now ( ) - 3600000 ) . toISOString ( ) } ] ,
containers : [
{ id : 'bitcoin' , state : 'running' , version : '27.0' } ,
{ id : 'lnd' , state : 'running' , version : '0.18.0' } ,
{ id : 'electrs' , state : 'running' , version : '0.10.6' } ,
{ id : 'mempool' , state : 'running' , version : '3.0.0' } ,
] ,
reported _at : new Date ( Date . now ( ) - 30000 ) . toISOString ( ) ,
} ,
] ,
} ,
} )
}
case 'telemetry.fleet-alerts' : {
return res . json ( {
result : {
alerts : [
{ node _id : 'archy-198' , rule : 'container_crash' , message : 'electrs restarted 2x in 1h' , timestamp : new Date ( Date . now ( ) - 7200000 ) . toISOString ( ) } ,
{ node _id : 'arch-3' , rule : 'disk_warning' , message : 'Disk usage approaching 50%' , timestamp : new Date ( Date . now ( ) - 3600000 ) . toISOString ( ) } ,
{ node _id : 'arch-1' , rule : 'mem_high' , message : 'Memory usage above 60%' , timestamp : new Date ( Date . now ( ) - 86400000 ) . toISOString ( ) } ,
] ,
} ,
} )
}
case 'telemetry.fleet-node-history' : {
const nodeId = params ? . node _id || 'archy-228'
const now = Date . now ( )
const history = Array . from ( { length : 24 } , ( _ , i ) => ( {
timestamp : new Date ( now - ( 23 - i ) * 3600000 ) . toISOString ( ) ,
cpu _pct : + ( 10 + Math . random ( ) * 30 ) . toFixed ( 1 ) ,
mem _pct : + ( 30 + Math . random ( ) * 20 ) . toFixed ( 1 ) ,
disk _pct : + ( 30 + Math . random ( ) * 5 ) . toFixed ( 1 ) ,
} ) )
return res . json ( { result : { history } } )
}
feat(TASK-12): beta telemetry — report endpoint + settings toggle
Backend: telemetry.report RPC builds anonymous health report with node ID
(SHA-256 hash of pubkey, truncated), version, uptime, container states,
CPU/RAM, federation peers, and recent alerts. Saves latest report to disk.
Requires analytics opt-in (existing analytics.enable/disable flow).
Frontend: "Beta Telemetry" section in Settings with enable/disable toggle.
Shows what data is and isn't collected. Mock backend handles all analytics
and telemetry RPCs.
Privacy: No wallet data, no private keys, no DIDs, no IP addresses.
Node identified by truncated hash only.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-18 23:14:47 +00:00
case 'analytics.get-snapshot' :
case 'telemetry.report' : {
return res . json ( { result : {
node _id : 'mock-dev-node' ,
version : '1.2.0-alpha' ,
uptime _secs : 86400 ,
cpu _cores : 4 ,
ram _mb : 16384 ,
container _count : 12 ,
running _count : 10 ,
federation _peers : 2 ,
recent _alerts : [ ] ,
reported _at : new Date ( ) . toISOString ( ) ,
} } )
}
2026-03-18 21:06:14 +00:00
// System / Network / Updates
// =====================================================================
case 'system.stats' : {
return res . json ( {
result : {
feat(TASK-12): beta telemetry — report endpoint + settings toggle
Backend: telemetry.report RPC builds anonymous health report with node ID
(SHA-256 hash of pubkey, truncated), version, uptime, container states,
CPU/RAM, federation peers, and recent alerts. Saves latest report to disk.
Requires analytics opt-in (existing analytics.enable/disable flow).
Frontend: "Beta Telemetry" section in Settings with enable/disable toggle.
Shows what data is and isn't collected. Mock backend handles all analytics
and telemetry RPCs.
Privacy: No wallet data, no private keys, no DIDs, no IP addresses.
Node identified by truncated hash only.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-18 23:14:47 +00:00
cpu _usage _percent : + ( 12 + Math . random ( ) * 18 ) . toFixed ( 1 ) ,
2026-03-18 21:06:14 +00:00
mem _used _bytes : 6_200_000_000 + Math . floor ( Math . random ( ) * 500_000_000 ) ,
mem _total _bytes : 16_000_000_000 ,
disk _used _bytes : 620_000_000_000 + Math . floor ( Math . random ( ) * 10_000_000_000 ) ,
disk _total _bytes : 1_800_000_000_000 ,
uptime _secs : Math . floor ( process . uptime ( ) ) + 604800 ,
load _avg : [ + ( 0.5 + Math . random ( ) * 1.5 ) . toFixed ( 2 ) , + ( 0.8 + Math . random ( ) ) . toFixed ( 2 ) , + ( 0.6 + Math . random ( ) ) . toFixed ( 2 ) ] ,
net _rx _bytes : 12_400_000_000 ,
net _tx _bytes : 8_900_000_000 ,
} ,
} )
}
case 'update.status' : {
return res . json ( {
result : {
current _version : '0.1.0' ,
latest _version : '0.1.1' ,
update _available : true ,
release _notes : 'Bug fixes and performance improvements.' ,
channel : 'stable' ,
} ,
} )
}
case 'network.list-requests' : {
return res . json ( {
result : {
requests : [
{ id : 'req-1' , from _did : 'did:key:z6MkhaXgBZDvotDkL5257faiztiGiC2ReMkBe4bR6XBIDNq9' , from _name : 'archy-198' , type : 'federation-join' , status : 'pending' , created _at : new Date ( Date . now ( ) - 3600000 ) . toISOString ( ) } ,
] ,
} ,
} )
}
2026-01-24 22:59:20 +00:00
default : {
2026-03-07 22:36:45 +00:00
console . log ( ` [RPC] Unknown method: ${ method } ` )
2026-01-24 22:59:20 +00:00
return res . json ( {
error : {
code : - 32601 ,
message : ` Method not found: ${ method } ` ,
} ,
} )
}
}
} catch ( error ) {
console . error ( '[RPC Error]' , error )
return res . json ( {
error : {
code : - 32603 ,
message : error . message ,
} ,
} )
}
} )
2026-03-07 22:50:05 +00:00
// =============================================================================
// Mock FileBrowser API (for Cloud page in demo/Docker deployments)
// =============================================================================
const MOCK _FILES = {
'/' : [
{ name : 'Music' , path : '/Music' , size : 0 , modified : '2025-03-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-03-18 21:06:14 +00:00
// =============================================================================
// Mock ThunderHub UI + Lightning API (no Docker required)
// =============================================================================
const MOCK _LND _DATA = {
info : {
alias : 'archy-signet' ,
color : '#f7931a' ,
public _key : '03a1b2c3d4e5f6789012345678901234567890abcdef1234567890abcdef123456' ,
num _active _channels : 4 ,
num _inactive _channels : 1 ,
num _pending _channels : 1 ,
block _height : 892451 ,
synced _to _chain : true ,
synced _to _graph : true ,
version : '0.17.4-beta commit=v0.17.4-beta' ,
chains : [ { chain : 'bitcoin' , network : 'signet' } ] ,
uris : [ '03a1b2c3@archy-signet.onion:9735' ] ,
} ,
balance : {
total _balance : 2_450_000 ,
confirmed _balance : 2_350_000 ,
unconfirmed _balance : 100_000 ,
} ,
channelBalance : {
local _balance : { sat : 8_250_000 } ,
remote _balance : { sat : 11_750_000 } ,
pending _open _local _balance : { sat : 500_000 } ,
pending _open _remote _balance : { sat : 0 } ,
} ,
channels : [
{ chan _id : '840921088114688' , remote _pubkey : '02778f4a4e...acinq' , capacity : 5_000_000 , local _balance : 2_450_000 , remote _balance : 2_550_000 , active : true , peer _alias : 'ACINQ Signet' , total _satoshis _sent : 850_000 , total _satoshis _received : 1_200_000 , uptime : 604800 , lifetime : 2592000 } ,
{ chan _id : '840921088114689' , remote _pubkey : '03abcdef12...wos' , capacity : 2_000_000 , local _balance : 1_200_000 , remote _balance : 800_000 , active : true , peer _alias : 'WalletOfSatoshi' , total _satoshis _sent : 350_000 , total _satoshis _received : 500_000 , uptime : 259200 , lifetime : 1296000 } ,
{ chan _id : '840921088114690' , remote _pubkey : '02fedcba98...voltage' , capacity : 10_000_000 , local _balance : 4_500_000 , remote _balance : 5_500_000 , active : true , peer _alias : 'Voltage' , total _satoshis _sent : 2_100_000 , total _satoshis _received : 1_800_000 , uptime : 518400 , lifetime : 2592000 } ,
{ chan _id : '840921088114691' , remote _pubkey : '03456789ab...kraken' , capacity : 3_000_000 , local _balance : 100_000 , remote _balance : 2_900_000 , active : true , peer _alias : 'Kraken' , total _satoshis _sent : 50_000 , total _satoshis _received : 120_000 , uptime : 86400 , lifetime : 604800 } ,
{ chan _id : '840921088114692' , remote _pubkey : '02112233aa...old' , capacity : 1_000_000 , local _balance : 0 , remote _balance : 1_000_000 , active : false , peer _alias : 'OldPeer-Offline' , total _satoshis _sent : 0 , total _satoshis _received : 0 , uptime : 0 , lifetime : 5184000 } ,
] ,
pendingChannels : {
pending _open _channels : [
{ channel : { remote _node _pub : '03ffeeddcc...newpeer' , capacity : 500_000 , local _balance : 500_000 , remote _balance : 0 } , confirmation _height : 892452 } ,
] ,
pending _closing _channels : [ ] ,
pending _force _closing _channels : [ ] ,
waiting _close _channels : [ ] ,
} ,
invoices : [
{ memo : 'Channel opening fee' , value : 50_000 , settled : true , creation _date : Math . floor ( Date . now ( ) / 1000 ) - 86400 , settle _date : Math . floor ( Date . now ( ) / 1000 ) - 85800 , payment _request : 'lnsb500000n1pjtest...truncated' , state : 'SETTLED' , amt _paid _sat : 50_000 , r _hash : Buffer . from ( 'aabbccdd01' ) . toString ( 'hex' ) } ,
{ memo : 'Test payment' , value : 1_000 , settled : true , creation _date : Math . floor ( Date . now ( ) / 1000 ) - 7200 , settle _date : Math . floor ( Date . now ( ) / 1000 ) - 7100 , payment _request : 'lnsb10000n1pjtest2...truncated' , state : 'SETTLED' , amt _paid _sat : 1_000 , r _hash : Buffer . from ( 'aabbccdd02' ) . toString ( 'hex' ) } ,
{ memo : 'Coffee payment' , value : 5_000 , settled : true , creation _date : Math . floor ( Date . now ( ) / 1000 ) - 3600 , settle _date : Math . floor ( Date . now ( ) / 1000 ) - 3500 , payment _request : 'lnsb50000n1pjtest3...truncated' , state : 'SETTLED' , amt _paid _sat : 5_000 , r _hash : Buffer . from ( 'aabbccdd03' ) . toString ( 'hex' ) } ,
{ memo : 'Donation' , value : 21_000 , settled : false , creation _date : Math . floor ( Date . now ( ) / 1000 ) - 600 , settle _date : 0 , payment _request : 'lnsb210000n1pjtest4...truncated' , state : 'OPEN' , amt _paid _sat : 0 , r _hash : Buffer . from ( 'aabbccdd04' ) . toString ( 'hex' ) } ,
] ,
payments : [
{ payment _hash : 'ff00112233' , value _sat : 10_000 , fee _sat : 3 , status : 'SUCCEEDED' , creation _date : Math . floor ( Date . now ( ) / 1000 ) - 43200 , payment _request : 'lnsb100000n1pjpay1...' , failure _reason : 'FAILURE_REASON_NONE' } ,
{ payment _hash : 'ff00112234' , value _sat : 100_000 , fee _sat : 12 , status : 'SUCCEEDED' , creation _date : Math . floor ( Date . now ( ) / 1000 ) - 21600 , payment _request : 'lnsb1000000n1pjpay2...' , failure _reason : 'FAILURE_REASON_NONE' } ,
{ payment _hash : 'ff00112235' , value _sat : 500 , fee _sat : 1 , status : 'SUCCEEDED' , creation _date : Math . floor ( Date . now ( ) / 1000 ) - 1800 , payment _request : 'lnsb5000n1pjpay3...' , failure _reason : 'FAILURE_REASON_NONE' } ,
] ,
forwarding : [
{ chan _id _in : '840921088114688' , chan _id _out : '840921088114690' , amt _in : 10_012 , amt _out : 10_000 , fee : 12 , timestamp _ns : ( Date . now ( ) - 7200000 ) * 1e6 } ,
{ chan _id _in : '840921088114690' , chan _id _out : '840921088114689' , amt _in : 5_005 , amt _out : 5_000 , fee : 5 , timestamp _ns : ( Date . now ( ) - 3600000 ) * 1e6 } ,
{ chan _id _in : '840921088114689' , chan _id _out : '840921088114691' , amt _in : 25_025 , amt _out : 25_000 , fee : 25 , timestamp _ns : ( Date . now ( ) - 1200000 ) * 1e6 } ,
] ,
}
// ThunderHub mock web UI
app . get ( '/app/thunderhub/' , ( req , res ) => {
const d = MOCK _LND _DATA
const totalCap = d . channels . reduce ( ( s , c ) => s + c . capacity , 0 )
const totalLocal = d . channels . reduce ( ( s , c ) => s + c . local _balance , 0 )
const totalRemote = d . channels . reduce ( ( s , c ) => s + c . remote _balance , 0 )
const totalFees = d . forwarding . reduce ( ( s , f ) => s + f . fee , 0 )
const channelRows = d . channels . map ( c => `
< tr >
< td > $ { c . peer _alias } < / t d >
< td > $ { ( c . capacity / 1e6 ) . toFixed ( 1 ) } M < / t d >
< td > < div style = "display:flex;gap:4px;align-items:center" > < div style = "background:#4ade80;height:8px;width:${Math.round(c.local_balance/c.capacity*100)}%;border-radius:4px" > < / d i v > < s p a n s t y l e = " f o n t - s i z e : 1 1 p x ; o p a c i t y : . 6 " > $ { ( c . l o c a l _ b a l a n c e / 1 e 3 ) . t o F i x e d ( 0 ) } k < / s p a n > < / d i v > < / t d >
< td > < div style = "display:flex;gap:4px;align-items:center" > < div style = "background:#3b82f6;height:8px;width:${Math.round(c.remote_balance/c.capacity*100)}%;border-radius:4px" > < / d i v > < s p a n s t y l e = " f o n t - s i z e : 1 1 p x ; o p a c i t y : . 6 " > $ { ( c . r e m o t e _ b a l a n c e / 1 e 3 ) . t o F i x e d ( 0 ) } k < / s p a n > < / d i v > < / t d >
< td > < span style = "color:${c.active ? '#4ade80' : '#ef4444'}" > $ { c . active ? 'Active' : 'Offline' } < / s p a n > < / t d >
< / t r > ` ) . j o i n ( ' ' )
const invoiceRows = d . invoices . slice ( ) . reverse ( ) . map ( inv => `
< tr >
< td > $ { inv . memo } < / t d >
< td > $ { inv . value . toLocaleString ( ) } sats < / t d >
< td > < span style = "color:${inv.settled ? '#4ade80' : '#fb923c'}" > $ { inv . settled ? 'Settled' : 'Open' } < / s p a n > < / t d >
< td > $ { new Date ( inv . creation _date * 1000 ) . toLocaleString ( ) } < / t d >
< / t r > ` ) . j o i n ( ' ' )
const paymentRows = d . payments . map ( p => `
< tr >
< td > $ { p . payment _hash . slice ( 0 , 12 ) } ... < / t d >
< td > $ { p . value _sat . toLocaleString ( ) } sats < / t d >
< td > $ { p . fee _sat } sats < / t d >
< td style = "color:#4ade80" > Succeeded < / t d >
< / t r > ` ) . j o i n ( ' ' )
res . type ( 'html' ) . send ( ` <!DOCTYPE html><html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
< title > ThunderHub — archy - signet < / t i t l e >
< style >
* { margin : 0 ; padding : 0 ; box - sizing : border - box }
body { background : # 0 f0f1a ; color : # e0e0e0 ; font - family : system - ui , - apple - system , sans - serif ; padding : 24 px }
h1 { font - size : 22 px ; margin - bottom : 4 px ; color : # fb923c }
. subtitle { color : rgba ( 255 , 255 , 255 , . 5 ) ; font - size : 13 px ; margin - bottom : 24 px }
. grid { display : grid ; grid - template - columns : repeat ( auto - fit , minmax ( 180 px , 1 fr ) ) ; gap : 16 px ; margin - bottom : 32 px }
. stat { background : rgba ( 255 , 255 , 255 , . 05 ) ; border : 1 px solid rgba ( 255 , 255 , 255 , . 1 ) ; border - radius : 12 px ; padding : 16 px }
. stat . label { font - size : 11 px ; text - transform : uppercase ; letter - spacing : 1 px ; color : rgba ( 255 , 255 , 255 , . 4 ) ; margin - bottom : 4 px }
. stat . value { font - size : 24 px ; font - weight : 600 }
. stat . value . green { color : # 4 ade80 } . stat . value . orange { color : # fb923c } . stat . value . blue { color : # 3 b82f6 }
h2 { font - size : 16 px ; margin : 24 px 0 12 px ; color : rgba ( 255 , 255 , 255 , . 8 ) }
table { width : 100 % ; border - collapse : collapse ; font - size : 13 px }
th { text - align : left ; padding : 8 px 12 px ; border - bottom : 1 px solid rgba ( 255 , 255 , 255 , . 1 ) ; color : rgba ( 255 , 255 , 255 , . 4 ) ; font - weight : 500 ; font - size : 11 px ; text - transform : uppercase ; letter - spacing : . 5 px }
td { padding : 8 px 12 px ; border - bottom : 1 px solid rgba ( 255 , 255 , 255 , . 05 ) }
tr : hover td { background : rgba ( 255 , 255 , 255 , . 02 ) }
. section { background : rgba ( 0 , 0 , 0 , . 3 ) ; border : 1 px solid rgba ( 255 , 255 , 255 , . 08 ) ; border - radius : 12 px ; padding : 20 px ; margin - bottom : 20 px }
. badge { display : inline - block ; background : rgba ( 247 , 147 , 26 , . 15 ) ; color : # fb923c ; padding : 2 px 8 px ; border - radius : 6 px ; font - size : 11 px ; margin - left : 8 px }
< / s t y l e > < / h e a d > < b o d y >
< h1 > ThunderHub < span class = "badge" > signet < / s p a n > < / h 1 >
< div class = "subtitle" > $ { d . info . alias } & mdash ; block $ { d . info . block _height . toLocaleString ( ) } & mdash ; $ { d . info . num _active _channels } active channels < / d i v >
< div class = "grid" >
< div class = "stat" > < div class = "label" > On - chain Balance < / d i v > < d i v c l a s s = " v a l u e o r a n g e " > $ { ( d . b a l a n c e . c o n f i r m e d _ b a l a n c e / 1 e 6 ) . t o F i x e d ( 2 ) } M s a t s < / d i v > < / d i v >
< div class = "stat" > < div class = "label" > Channel Capacity < / d i v > < d i v c l a s s = " v a l u e " > $ { ( t o t a l C a p / 1 e 6 ) . t o F i x e d ( 1 ) } M s a t s < / d i v > < / d i v >
< div class = "stat" > < div class = "label" > Local Balance < / d i v > < d i v c l a s s = " v a l u e g r e e n " > $ { ( t o t a l L o c a l / 1 e 6 ) . t o F i x e d ( 1 ) } M s a t s < / d i v > < / d i v >
< div class = "stat" > < div class = "label" > Remote Balance < / d i v > < d i v c l a s s = " v a l u e b l u e " > $ { ( t o t a l R e m o t e / 1 e 6 ) . t o F i x e d ( 1 ) } M s a t s < / d i v > < / d i v >
< div class = "stat" > < div class = "label" > Routing Fees Earned < / d i v > < d i v c l a s s = " v a l u e g r e e n " > $ { t o t a l F e e s } s a t s < / d i v > < / d i v >
< div class = "stat" > < div class = "label" > Payments Sent < / d i v > < d i v c l a s s = " v a l u e " > $ { d . p a y m e n t s . l e n g t h } < / d i v > < / d i v >
< / d i v >
< div class = "section" >
< h2 > Channels ( $ { d . channels . length } ) < / h 2 >
< table > < thead > < tr > < th > Peer < / t h > < t h > C a p a c i t y < / t h > < t h > L o c a l < / t h > < t h > R e m o t e < / t h > < t h > S t a t u s < / t h > < / t r > < / t h e a d > < t b o d y > $ { c h a n n e l R o w s } < / t b o d y > < / t a b l e >
< / d i v >
< div class = "section" >
< h2 > Recent Invoices < / h 2 >
< table > < thead > < tr > < th > Memo < / t h > < t h > A m o u n t < / t h > < t h > S t a t u s < / t h > < t h > C r e a t e d < / t h > < / t r > < / t h e a d > < t b o d y > $ { i n v o i c e R o w s } < / t b o d y > < / t a b l e >
< / d i v >
< div class = "section" >
< h2 > Recent Payments < / h 2 >
< table > < thead > < tr > < th > Hash < / t h > < t h > A m o u n t < / t h > < t h > F e e < / t h > < t h > S t a t u s < / t h > < / t r > < / t h e a d > < t b o d y > $ { p a y m e n t R o w s } < / t b o d y > < / t a b l e >
< / d i v >
< div class = "section" >
< h2 > Forwarding History < / h 2 >
< table > < thead > < tr > < th > In Channel < / t h > < t h > O u t C h a n n e l < / t h > < t h > A m o u n t < / t h > < t h > F e e < / t h > < t h > T i m e < / t h > < / t r > < / t h e a d > < t b o d y >
$ { d . forwarding . map ( f => {
const inPeer = d . channels . find ( c => c . chan _id === f . chan _id _in ) ? . peer _alias || f . chan _id _in
const outPeer = d . channels . find ( c => c . chan _id === f . chan _id _out ) ? . peer _alias || f . chan _id _out
return ` <tr><td> ${ inPeer } </td><td> ${ outPeer } </td><td> ${ f . amt _in . toLocaleString ( ) } sats</td><td> ${ f . fee } sats</td><td> ${ new Date ( f . timestamp _ns / 1e6 ) . toLocaleString ( ) } </td></tr> `
} ) . join ( '' ) }
< / t b o d y > < / t a b l e >
< / d i v >
< p style = "text-align:center;color:rgba(255,255,255,.25);font-size:12px;margin-top:32px" > Mock ThunderHub & mdash ; Archipelago Dev Mode & mdash ; No Docker Required < / p >
< / b o d y > < / h t m l > ` )
} )
// ThunderHub API stubs (for any programmatic access)
app . get ( '/app/thunderhub/api/info' , ( req , res ) => res . json ( MOCK _LND _DATA . info ) )
app . get ( '/app/thunderhub/api/balance' , ( req , res ) => res . json ( MOCK _LND _DATA . balance ) )
app . get ( '/app/thunderhub/api/channels' , ( req , res ) => res . json ( MOCK _LND _DATA . channels ) )
app . get ( '/app/thunderhub/api/invoices' , ( req , res ) => res . json ( MOCK _LND _DATA . invoices ) )
app . get ( '/app/thunderhub/api/payments' , ( req , res ) => res . json ( MOCK _LND _DATA . payments ) )
app . get ( '/app/thunderhub/api/forwards' , ( req , res ) => res . json ( MOCK _LND _DATA . forwarding ) )
2026-01-24 22:59:20 +00:00
// Health check
app . get ( '/health' , ( req , res ) => {
res . status ( 200 ) . send ( 'healthy' )
} )
// WebSocket endpoint
const server = http . createServer ( app )
const wss = new WebSocketServer ( { server , path : '/ws/db' } )
wss . on ( 'connection' , ( ws , req ) => {
console . log ( '[WebSocket] Client connected from' , req . socket . remoteAddress )
wsClients . add ( ws )
// Set up ping/pong to keep connection alive
const pingInterval = setInterval ( ( ) => {
if ( ws . readyState === 1 ) { // OPEN
try {
ws . ping ( )
} catch ( err ) {
console . error ( '[WebSocket] Ping error:' , err )
clearInterval ( pingInterval )
2026-03-07 22:36:45 +00:00
clearInterval ( heartbeatInterval )
2026-01-24 22:59:20 +00:00
}
} else {
clearInterval ( pingInterval )
2026-03-07 22:36:45 +00:00
clearInterval ( heartbeatInterval )
2026-01-24 22:59:20 +00:00
}
} , 30000 ) // Ping every 30 seconds
2026-03-07 22:36:45 +00:00
// Send periodic heartbeat data so clients don't think the connection is dead
const heartbeatInterval = setInterval ( ( ) => {
if ( ws . readyState === 1 ) {
try {
2026-03-07 23:22:30 +00:00
ws . send ( JSON . stringify ( { type : 'heartbeat' , rev : Date . now ( ) } ) )
2026-03-07 22:36:45 +00:00
} catch { /* ignore */ }
}
} , 45000 ) // Every 45s (client expects data within 60s)
2026-01-24 22:59:20 +00:00
// Send initial data immediately
try {
ws . send ( JSON . stringify ( {
type : 'initial' ,
data : mockData ,
} ) )
console . log ( '[WebSocket] Initial data sent' )
} catch ( err ) {
console . error ( '[WebSocket] Error sending initial data:' , err )
}
ws . on ( 'pong' , ( ) => {
// Client responded to ping, connection is alive
} )
ws . on ( 'message' , ( message ) => {
// Handle incoming messages if needed
try {
const data = JSON . parse ( message . toString ( ) )
console . log ( '[WebSocket] Received message:' , data )
} catch ( err ) {
console . error ( '[WebSocket] Error parsing message:' , err )
}
} )
ws . on ( 'close' , ( code , reason ) => {
console . log ( '[WebSocket] Client disconnected' , { code , reason : reason . toString ( ) } )
clearInterval ( pingInterval )
2026-03-07 22:36:45 +00:00
clearInterval ( heartbeatInterval )
2026-01-24 22:59:20 +00:00
wsClients . delete ( ws )
} )
ws . on ( 'error' , ( error ) => {
console . error ( '[WebSocket Error]' , error )
clearInterval ( pingInterval )
2026-03-07 22:36:45 +00:00
clearInterval ( heartbeatInterval )
2026-01-24 22:59:20 +00:00
wsClients . delete ( ws )
} )
} )
server . listen ( PORT , '0.0.0.0' , async ( ) => {
const runtime = await isContainerRuntimeAvailable ( )
2026-01-27 23:06:18 +00:00
// Initialize package data from Docker
await initializePackageData ( )
2026-01-24 22:59:20 +00:00
console . log ( `
╔ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ╗
║ ║
║ 🚀 Archipelago Mock Backend Server ║
║ ║
║ RPC : http : //localhost:${PORT}/rpc/v1 ║
║ WebSocket : ws : //localhost:${PORT}/ws/db ║
║ ║
║ Dev Mode : $ { DEV _MODE . padEnd ( 47 ) } ║
║ Setup : $ { userState . setupComplete ? '✅ Complete' : '❌ Not done' . padEnd ( 47 ) } ║
║ Onboarding : $ { userState . onboardingComplete ? '✅ Complete' : '❌ Not done' . padEnd ( 46 ) } ║
║ ║
║ Mock Password : $ { MOCK _PASSWORD . padEnd ( 40 ) } ║
║ ║
2026-03-18 21:06:14 +00:00
║ Container Runtime : $ { runtime . available ? ` ✅ ${ runtime . runtime } ` . padEnd ( 40 ) : '⏭️ Simulation mode' . padEnd ( 40 ) } ║
2026-03-09 13:03:53 +00:00
║ Claude API Key : $ { process . env . ANTHROPIC _API _KEY ? '✅ Set (' + process . env . ANTHROPIC _API _KEY . slice ( 0 , 12 ) + '...)' : '❌ Not set (chat disabled)' . padEnd ( 40 ) } ║
2026-01-24 22:59:20 +00:00
║ ║
╚ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ╝
` )
console . log ( 'Mock backend is running. Press Ctrl+C to stop.\n' )
2026-03-09 13:03:53 +00:00
// Pre-check Anthropic API connectivity
if ( process . env . ANTHROPIC _API _KEY ) {
try {
const dns = await import ( 'dns' )
dns . lookup ( 'api.anthropic.com' , ( err , address ) => {
if ( err ) {
console . error ( '[Claude Proxy] ⚠ DNS lookup failed for api.anthropic.com:' , err . message )
console . error ( '[Claude Proxy] Chat will fail. Check container DNS settings.' )
} else {
console . log ( '[Claude Proxy] ✅ api.anthropic.com resolves to' , address )
}
} )
} catch { /* ignore */ }
}
2026-01-27 23:06:18 +00:00
2026-02-18 08:30:12 +00:00
// Periodically update package data from Docker (merge with static dev apps)
2026-03-07 20:53:02 +00:00
// Only poll if container runtime is available (avoids log spam in demo/Docker deployments)
if ( runtime . available ) {
setInterval ( async ( ) => {
const dockerApps = await getDockerContainers ( )
mockData [ 'package-data' ] = mergePackageData ( dockerApps )
// Broadcast update to connected clients
broadcastUpdate ( [
{
op : 'replace' ,
path : '/package-data' ,
value : mockData [ 'package-data' ]
}
] )
} , 5000 ) // Update every 5 seconds
}
2026-01-24 22:59:20 +00:00
} )
process . on ( 'SIGINT' , ( ) => {
console . log ( '\n\nShutting down mock backend...' )
server . close ( ( ) => {
console . log ( 'Server stopped.' )
process . exit ( 0 )
} )
} )