refactor: update dependencies and remove unused code
- Added new dependencies: `adler2`, `crc32fast`, `flate2`, `miniz_oxide`, and `libredox`. - Updated existing dependencies: `tokio-rustls` to version 0.26.4 and `filetime` to version 0.2.27. - Removed the `backup.rs` file as it is no longer needed. - Introduced tests for configuration and credential management. - Enhanced the `identity` module to generate W3C compliant DID documents. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,188 +1,270 @@
|
||||
# CRITICAL CHANGES FOR BETA ISO BUILD
|
||||
# Beta Release Checklist (v0.5.0-beta)
|
||||
|
||||
## ⚠️ MUST-HAVE CHANGES - Without these, the beta will NOT work!
|
||||
## Pre-Build Verification
|
||||
|
||||
### 1. Backend: Podman Detection Fix
|
||||
**File:** `core/container/src/podman_client.rs`
|
||||
### Source Code
|
||||
|
||||
```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
|
||||
}
|
||||
```
|
||||
- [ ] All changes committed and pushed to `main`
|
||||
- [ ] `cargo clippy --all-targets --all-features` passes (zero warnings)
|
||||
- [ ] `cargo fmt --all` applied
|
||||
- [ ] `cd neode-ui && npm run type-check` passes (zero errors)
|
||||
- [ ] `cd neode-ui && npm test` passes (all tests green)
|
||||
- [ ] `cargo test --all-features` passes on dev server
|
||||
|
||||
**System Config:** `/etc/sudoers.d/archipelago-podman`
|
||||
```
|
||||
archipelago ALL=(ALL) NOPASSWD: /usr/bin/podman
|
||||
```
|
||||
### Critical Files
|
||||
|
||||
### 2. Backend: Bitcoin UI Container Mapping
|
||||
**File:** `core/archipelago/src/container/docker_packages.rs`
|
||||
|
||||
Add special case for bitcoin-knots (line ~95):
|
||||
```rust
|
||||
} else if app_id == "bitcoin-knots" {
|
||||
// Check if bitcoin-ui exists (maps to "bitcoin" but serves bitcoin-knots)
|
||||
if let Some(ui_address) = ui_containers.get("bitcoin") {
|
||||
debug!("Using bitcoin-ui for bitcoin-knots: {}", ui_address);
|
||||
Some(ui_address.clone())
|
||||
} else {
|
||||
extract_lan_address(&container.ports)
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Backend: IndeedHub Metadata
|
||||
**File:** `core/archipelago/src/container/docker_packages.rs`
|
||||
|
||||
Add to `get_app_metadata()` function (line ~327):
|
||||
```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(),
|
||||
},
|
||||
```
|
||||
|
||||
### 4. Frontend: Marketplace Bitcoin Knots
|
||||
**File:** `neode-ui/src/views/Marketplace.vue`
|
||||
|
||||
```javascript
|
||||
{
|
||||
id: 'bitcoin-knots', // CHANGED from 'bitcoin'
|
||||
title: 'Bitcoin Knots',
|
||||
version: '28.1.0', // UPDATED version
|
||||
dockerImage: 'docker.io/bitcoinknots/bitcoin:latest', // CHANGED image
|
||||
// ... rest of config
|
||||
}
|
||||
```
|
||||
|
||||
### 5. Auto-Installer: Profile Script Fix
|
||||
**File:** `image-recipe/build-auto-installer-iso.sh` (line ~850)
|
||||
|
||||
Remove `|| [ ! -t 0 ]` check:
|
||||
```bash
|
||||
# CORRECT:
|
||||
if [ -n "$INSTALLER_STARTED" ]; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
# WRONG (will break auto-login):
|
||||
# if [ -n "$INSTALLER_STARTED" ] || [ ! -t 0 ]; then
|
||||
```
|
||||
|
||||
### 6. Nginx Configuration
|
||||
**File:** Captured from `/etc/nginx/sites-available/default`
|
||||
|
||||
MUST include:
|
||||
- HTTPS on port 443
|
||||
- HTTP redirect to HTTPS
|
||||
- Backend proxy: `/rpc/`, `/ws/`, `/health`
|
||||
- Root: `/opt/archipelago/web-ui`
|
||||
- SSL certificates in `/etc/nginx/ssl/`
|
||||
|
||||
### 7. Bitcoin UI Files
|
||||
**Files:** `docker/bitcoin-ui/index.html` and `Dockerfile`
|
||||
|
||||
MUST be included in ISO or downloadable, so users can deploy the web UI container.
|
||||
- [ ] `core/container/src/podman_client.rs` — sudo podman
|
||||
- [ ] `core/archipelago/src/container/docker_packages.rs` — app metadata + UI mapping
|
||||
- [ ] `core/archipelago/src/api/rpc/package.rs` — app configs, capabilities, dependencies
|
||||
- [ ] `core/archipelago/src/session.rs` — session security hardening
|
||||
- [ ] `core/security/src/secrets_manager.rs` — encryption + rotation
|
||||
- [ ] `neode-ui/src/views/Marketplace.vue` — all app entries with pinned image versions
|
||||
- [ ] `neode-ui/src/api/websocket.ts` — heartbeat + reconnection
|
||||
- [ ] `image-recipe/build-auto-installer-iso.sh` — all container images captured
|
||||
- [ ] `image-recipe/configs/nginx-archipelago.conf` — all app proxies + path traversal blocks
|
||||
- [ ] All app icons present in `neode-ui/public/assets/img/app-icons/`
|
||||
|
||||
---
|
||||
|
||||
## Build Verification Before Beta Release
|
||||
## App Integration Matrix
|
||||
|
||||
Run these checks:
|
||||
Every app must be tested for install, launch, and uninstall on a fresh system.
|
||||
|
||||
### Core Bitcoin Stack
|
||||
|
||||
| App | Image | Version | Install | Launch | UI Loads | Uninstall |
|
||||
|-----|-------|---------|---------|--------|----------|-----------|
|
||||
| Bitcoin Knots | `bitcoinknots/bitcoin` | `v28.1` | [ ] | [ ] | [ ] | [ ] |
|
||||
| Electrs | `mempool/electrs` | `v0.4.1` | [ ] | [ ] | [ ] | [ ] |
|
||||
| LND | `lightninglabs/lnd` | `v0.18.4` | [ ] | [ ] | [ ] | [ ] |
|
||||
| BTCPay Server | `btcpayserver/btcpayserver` | `2.0.6` | [ ] | [ ] | [ ] | [ ] |
|
||||
| Mempool | `mempool/frontend` | `v3.0.0` | [ ] | [ ] | [ ] | [ ] |
|
||||
| Fedimint | `fedimintui/fedimint` | `0.5.0` | [ ] | [ ] | [ ] | [ ] |
|
||||
| Fedimint Gateway | `fedimintui/gateway-ui` | `0.5.0` | [ ] | [ ] | [ ] | [ ] |
|
||||
|
||||
### Storage & Media
|
||||
|
||||
| App | Image | Version | Install | Launch | UI Loads | Uninstall |
|
||||
|-----|-------|---------|---------|--------|----------|-----------|
|
||||
| File Browser | `filebrowser/filebrowser` | `v2` | [ ] | [ ] | [ ] | [ ] |
|
||||
| Immich | `ghcr.io/immich-app/immich-server` | `v1.121.0` | [ ] | [ ] | [ ] | [ ] |
|
||||
| PhotoPrism | `photoprism/photoprism` | `240915` | [ ] | [ ] | [ ] | [ ] |
|
||||
|
||||
### Productivity & Privacy
|
||||
|
||||
| App | Image | Version | Install | Launch | UI Loads | Uninstall |
|
||||
|-----|-------|---------|---------|--------|----------|-----------|
|
||||
| Penpot | `penpotapp/frontend` | `2.4` | [ ] | [ ] | [ ] | [ ] |
|
||||
| SearXNG | `searxng/searxng` | `2024.11.17-e2554de75` | [ ] | [ ] | [ ] | [ ] |
|
||||
| Ollama | `ollama/ollama` | `0.5.4` | [ ] | [ ] | [ ] | [ ] |
|
||||
|
||||
### Network & Infrastructure
|
||||
|
||||
| App | Image | Version | Install | Launch | UI Loads | Uninstall |
|
||||
|-----|-------|---------|---------|--------|----------|-----------|
|
||||
| Nostr Relay | `scsiblade/nostr-rs-relay` | `0.9.0` | [ ] | [ ] | [ ] | [ ] |
|
||||
| Nginx Proxy Manager | `jc21/nginx-proxy-manager` | `2.12.1` | [ ] | [ ] | [ ] | [ ] |
|
||||
| Tailscale | `tailscale/tailscale` | pinned | [ ] | [ ] | [ ] | [ ] |
|
||||
| Home Assistant | `homeassistant/home-assistant` | pinned | [ ] | [ ] | [ ] | [ ] |
|
||||
|
||||
### Virtual Apps (No Container)
|
||||
|
||||
| App | Behavior | Works |
|
||||
|-----|----------|-------|
|
||||
| IndeedHub | Opens external URL | [ ] |
|
||||
|
||||
---
|
||||
|
||||
## Dependency Chain Tests
|
||||
|
||||
These must be tested in order on a fresh install:
|
||||
|
||||
- [ ] Install Bitcoin Knots → starts and begins syncing
|
||||
- [ ] Install Electrs while Bitcoin running → connects to Bitcoin automatically
|
||||
- [ ] Install LND while Bitcoin running → connects to Bitcoin automatically
|
||||
- [ ] Install BTCPay while Bitcoin running → connects; Lightning available if LND present
|
||||
- [ ] Install Mempool while Bitcoin + Electrs running → shows blockchain data
|
||||
- [ ] Try installing Electrs without Bitcoin → shows clear error message
|
||||
- [ ] Try installing LND without Bitcoin → shows clear error message
|
||||
- [ ] Try installing Mempool without Bitcoin + Electrs → shows missing deps error
|
||||
- [ ] Fedimint Gateway auto-detects LND credentials when available
|
||||
|
||||
---
|
||||
|
||||
## Security Hardening Verification
|
||||
|
||||
### Session Security
|
||||
|
||||
- [ ] Sessions expire after 24 hours of inactivity
|
||||
- [ ] Password change invalidates all other sessions
|
||||
- [ ] Maximum 5 concurrent sessions (oldest evicted when exceeded)
|
||||
- [ ] Session tokens are SHA-256 hashed in memory (never stored as plaintext)
|
||||
- [ ] Login rate limiting: 5 failures per 60 seconds per IP
|
||||
|
||||
### Container Security
|
||||
|
||||
- [ ] All container images use pinned versions (no `:latest`)
|
||||
- [ ] Read-only root filesystem enabled for compatible apps
|
||||
- [ ] `--cap-drop=ALL` applied to all containers
|
||||
- [ ] `--security-opt=no-new-privileges:true` applied to all containers
|
||||
- [ ] Required capabilities added explicitly per app (e.g., CHOWN for File Browser)
|
||||
|
||||
### Secrets Management
|
||||
|
||||
- [ ] Secrets encrypted with AES-256-GCM on disk
|
||||
- [ ] Secret metadata tracked (creation date, rotation count)
|
||||
- [ ] Secret rotation generates new random values and re-encrypts
|
||||
- [ ] `security.list-expiring` RPC returns secrets older than threshold
|
||||
|
||||
### Path Traversal Prevention
|
||||
|
||||
- [ ] Nginx blocks `..` in filebrowser API paths (403 response)
|
||||
- [ ] Frontend `sanitizePath()` strips `..` and resolves paths
|
||||
- [ ] File Browser token not exposed in URLs
|
||||
|
||||
### Authentication
|
||||
|
||||
- [ ] TOTP 2FA enrollment and verification works
|
||||
- [ ] TOTP backup codes work for recovery
|
||||
- [ ] Maximum 5 TOTP attempts before session invalidation
|
||||
- [ ] Pending TOTP sessions expire after 5 minutes
|
||||
- [ ] Cookie-based auth (no tokens in query strings)
|
||||
|
||||
---
|
||||
|
||||
## WebSocket & Connectivity
|
||||
|
||||
- [ ] WebSocket connects on login and receives initial data dump
|
||||
- [ ] WebSocket reconnects after network interruption (exponential backoff, max 30s)
|
||||
- [ ] Server sends ping every 30s; client responds with pong
|
||||
- [ ] Client sends JSON ping every 30s; server responds with JSON pong
|
||||
- [ ] Server closes inactive connections after 5 minutes
|
||||
- [ ] Connection state shown in UI (connected/reconnecting/disconnected)
|
||||
- [ ] Install progress updates delivered in real-time via WebSocket
|
||||
|
||||
---
|
||||
|
||||
## Fresh Install Testing Matrix
|
||||
|
||||
### ISO Build
|
||||
|
||||
- [ ] ISO builds successfully on dev server
|
||||
- [ ] ISO size is reasonable (< 10 GB)
|
||||
- [ ] All container images captured in ISO
|
||||
|
||||
### Installation
|
||||
|
||||
- [ ] Boot from USB on x86_64 hardware
|
||||
- [ ] Auto-installer partitions disk correctly
|
||||
- [ ] Debian 12 installs without errors
|
||||
- [ ] Archipelago services start on first boot
|
||||
- [ ] Web UI accessible at server IP within 3 minutes of first boot
|
||||
|
||||
### Onboarding Flow
|
||||
|
||||
- [ ] Welcome screen displays with intro video
|
||||
- [ ] Password creation enforces minimum requirements
|
||||
- [ ] Path selection shows all 6 options
|
||||
- [ ] DID generation completes within 60 seconds
|
||||
- [ ] Identity naming is optional and skippable
|
||||
- [ ] Backup download produces valid JSON file
|
||||
- [ ] Onboarding completes and reaches Dashboard
|
||||
|
||||
### Post-Onboarding
|
||||
|
||||
- [ ] Dashboard shows all overview cards
|
||||
- [ ] App Store loads with all curated apps
|
||||
- [ ] Settings shows server name, version, DID, Tor address
|
||||
- [ ] Logout and re-login works
|
||||
- [ ] Password change works and invalidates other sessions
|
||||
|
||||
---
|
||||
|
||||
## Performance Targets
|
||||
|
||||
- [ ] Backend startup: < 3 seconds
|
||||
- [ ] Frontend initial load: < 500 KB gzipped
|
||||
- [ ] WebSocket initial data: < 1 second after connection
|
||||
- [ ] App install progress visible in UI within 5 seconds of starting
|
||||
|
||||
---
|
||||
|
||||
## Nginx Proxy Verification
|
||||
|
||||
All app proxies must work in both HTTP and HTTPS blocks:
|
||||
|
||||
- [ ] `/rpc/` → backend:5678
|
||||
- [ ] `/ws/` → backend:5678 (WebSocket upgrade)
|
||||
- [ ] `/health` → backend:5678
|
||||
- [ ] `/app/filebrowser/` → filebrowser:80
|
||||
- [ ] `/app/searxng/` → searxng:8080
|
||||
- [ ] `/app/immich/` → immich:2283
|
||||
- [ ] `/app/penpot/` → penpot-frontend:80
|
||||
- [ ] `/app/ollama/` → ollama:11434
|
||||
- [ ] `/app/photoprism/` → photoprism:2342
|
||||
- [ ] `/app/nginx-proxy-manager/` → npm:81
|
||||
- [ ] `/app/tailscale/` → tailscale:8240
|
||||
- [ ] BTCPay (port 23000) opens in new tab
|
||||
- [ ] Home Assistant (port 8123) opens in new tab
|
||||
- [ ] Tor hidden services resolve for all configured apps
|
||||
|
||||
---
|
||||
|
||||
## Rollback Procedures
|
||||
|
||||
### If Backend Fails to Start
|
||||
|
||||
```bash
|
||||
# 1. Verify all source changes are committed
|
||||
cd /Users/dorian/Projects/archy
|
||||
git status # Should show all critical files committed
|
||||
# Check logs
|
||||
sudo journalctl -u archipelago -n 50 --no-pager
|
||||
|
||||
# 2. Build ISO from live server
|
||||
cd image-recipe
|
||||
DEV_SERVER=archipelago@192.168.1.228 ./build-auto-installer-iso.sh
|
||||
|
||||
# 3. Test ISO on clean VM
|
||||
# - Boot ISO
|
||||
# - Verify auto-installer runs
|
||||
# - System should boot to login
|
||||
# - Access http://SERVER-IP
|
||||
# - Complete onboarding
|
||||
# - Install Bitcoin Knots from App Store
|
||||
# - Verify "Already Installed" shows after install
|
||||
# - Verify "Launch" button works
|
||||
# - Verify web UI loads on port 8334
|
||||
|
||||
# 4. Test all critical features
|
||||
# - Bitcoin node syncing
|
||||
# - RPC accessible
|
||||
# - Web UI functional
|
||||
# - Backend detects container
|
||||
# - App Store shows proper status
|
||||
# Restore previous binary
|
||||
sudo cp /usr/local/bin/archipelago.bak /usr/local/bin/archipelago
|
||||
sudo systemctl restart archipelago
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Critical Files Checklist
|
||||
|
||||
Before building beta ISO, ensure these files have the latest changes:
|
||||
|
||||
- [ ] `core/container/src/podman_client.rs` - sudo podman
|
||||
- [ ] `core/archipelago/src/container/docker_packages.rs` - app metadata + UI mapping
|
||||
- [ ] `neode-ui/src/views/Marketplace.vue` - bitcoin-knots ID
|
||||
- [ ] `neode-ui/src/utils/dummyApps.ts` - IndeedHub data
|
||||
- [ ] `image-recipe/build-auto-installer-iso.sh` - auto-start fix
|
||||
- [ ] `docker/bitcoin-ui/` - UI files present
|
||||
- [ ] `scripts/deploy-bitcoin-knots.sh` - deployment script
|
||||
- [ ] All assets: `neode-ui/public/assets/img/app-icons/*.png`
|
||||
|
||||
---
|
||||
|
||||
## Testing Matrix
|
||||
|
||||
| Feature | Expected | Status |
|
||||
|---------|----------|--------|
|
||||
| Bitcoin Knots container runs | Running | ✅ |
|
||||
| Bitcoin UI container runs | Running | ✅ |
|
||||
| Backend detects bitcoin-knots | Detected | ✅ |
|
||||
| Backend maps bitcoin-ui → bitcoin-knots | Port 8334 | ✅ |
|
||||
| App shows in My Apps | Listed | ✅ |
|
||||
| App Store shows "Already Installed" | Badge shown | ✅ (after ID fix) |
|
||||
| Launch button visible | Clickable | ✅ |
|
||||
| Launch opens web UI | Port 8334 | ✅ |
|
||||
| RPC accessible | Port 8332 | ✅ |
|
||||
| Blockchain syncing | Active | ✅ |
|
||||
|
||||
---
|
||||
|
||||
## Roll-Back Plan
|
||||
|
||||
If beta ISO fails:
|
||||
|
||||
1. Check `/var/log/archipelago.log` on installed system
|
||||
2. Verify containers with `sudo podman ps -a`
|
||||
3. Check nginx status: `sudo systemctl status nginx`
|
||||
4. Test backend: `curl http://localhost:5678/health`
|
||||
5. Rebuild ISO with `BUILD_FROM_SOURCE=1` if server state is corrupt
|
||||
|
||||
---
|
||||
|
||||
## Support Commands for Users
|
||||
### If Frontend is Broken
|
||||
|
||||
```bash
|
||||
# Check Bitcoin status
|
||||
sudo podman logs -f bitcoin-knots
|
||||
|
||||
# Check blockchain sync progress
|
||||
curl --user archipelago:archipelago123 \
|
||||
--data-binary '{"jsonrpc":"1.0","id":"test","method":"getblockchaininfo","params":[]}' \
|
||||
-H 'content-type: text/plain;' http://localhost:8332/ | grep blocks
|
||||
|
||||
# Restart if needed
|
||||
sudo podman restart bitcoin-knots bitcoin-ui
|
||||
|
||||
# View Archipelago backend logs
|
||||
sudo journalctl -u archipelago -f
|
||||
# Restore previous frontend build
|
||||
sudo cp -r /opt/archipelago/web-ui.bak/* /opt/archipelago/web-ui/
|
||||
sudo systemctl reload nginx
|
||||
```
|
||||
|
||||
### If Container Won't Start
|
||||
|
||||
```bash
|
||||
# Check container logs
|
||||
sudo podman logs <container-name>
|
||||
|
||||
# Remove and recreate
|
||||
sudo podman rm -f <container-name>
|
||||
# Reinstall from App Store
|
||||
```
|
||||
|
||||
### If ISO Install Fails
|
||||
|
||||
1. Boot into rescue mode from USB
|
||||
2. Check `/var/log/installer.log` on target disk
|
||||
3. Verify disk partitioning with `lsblk`
|
||||
4. Re-run installer with `INSTALLER_STARTED= /opt/installer.sh`
|
||||
|
||||
### Full System Rollback
|
||||
|
||||
If the beta is unusable:
|
||||
1. Re-flash the ISO from the last known good build
|
||||
2. Restore user data from `/var/lib/archipelago/` backup
|
||||
3. Re-import DID from backup JSON file
|
||||
|
||||
---
|
||||
|
||||
## Sign-Off
|
||||
|
||||
| Reviewer | Area | Date | Pass/Fail |
|
||||
|----------|------|------|-----------|
|
||||
| | Backend | | |
|
||||
| | Frontend | | |
|
||||
| | Security | | |
|
||||
| | ISO Build | | |
|
||||
| | Fresh Install | | |
|
||||
| | App Integrations | | |
|
||||
|
||||
32
docs/adr/001-podman-over-docker.md
Normal file
32
docs/adr/001-podman-over-docker.md
Normal file
@@ -0,0 +1,32 @@
|
||||
# ADR-001: Podman Over Docker
|
||||
|
||||
**Status**: Accepted
|
||||
**Date**: 2026-03
|
||||
|
||||
## Context
|
||||
|
||||
Archipelago needs a container runtime for running applications. Docker and Podman are the two main options.
|
||||
|
||||
## Decision
|
||||
|
||||
Use Podman as the container runtime instead of Docker.
|
||||
|
||||
## Consequences
|
||||
|
||||
### Positive
|
||||
- **Rootless by default**: Containers run without root privileges, reducing attack surface
|
||||
- **Daemonless**: No persistent daemon process; containers are managed as individual processes under systemd
|
||||
- **Docker-compatible**: Supports Docker images and most Docker CLI commands
|
||||
- **Systemd integration**: Podman containers can be managed as systemd services natively
|
||||
- **No vendor lock-in**: OCI-compliant, works with any container registry
|
||||
|
||||
### Negative
|
||||
- **Smaller ecosystem**: Some Docker-specific tools and compose features require adaptation
|
||||
- **Docker Compose differences**: Podman Compose exists but has occasional compatibility gaps
|
||||
- **Documentation**: Most container documentation assumes Docker; developers need to translate
|
||||
- **Networking**: Podman networking (CNI/netavark) differs from Docker's bridge networking
|
||||
|
||||
### Mitigation
|
||||
- Use `podman` CLI wrapper that provides Docker-compatible interface
|
||||
- Document Podman-specific commands in developer guide
|
||||
- Use `archy-net` custom network for inter-container DNS
|
||||
31
docs/adr/002-did-key-method.md
Normal file
31
docs/adr/002-did-key-method.md
Normal file
@@ -0,0 +1,31 @@
|
||||
# ADR-002: DID Key Method for Node Identity
|
||||
|
||||
**Status**: Accepted
|
||||
**Date**: 2026-03
|
||||
|
||||
## Context
|
||||
|
||||
Each Archipelago node needs a cryptographic identity for peer authentication, federation, and verifiable credentials. Multiple DID methods exist (did:web, did:ion, did:key, did:peer).
|
||||
|
||||
## Decision
|
||||
|
||||
Use `did:key` as the primary DID method.
|
||||
|
||||
## Consequences
|
||||
|
||||
### Positive
|
||||
- **Self-contained**: The DID document is derived entirely from the public key — no external resolution needed
|
||||
- **Offline-capable**: Works without internet, aligning with sovereignty principles
|
||||
- **Simple**: No registration, no blockchain, no web server required
|
||||
- **Fast**: DID resolution is a local computation, not a network request
|
||||
- **Ed25519**: Uses Ed25519 keys which are fast, compact, and well-supported
|
||||
|
||||
### Negative
|
||||
- **No key rotation**: The DID is bound to a single key; rotating requires a new DID
|
||||
- **No service endpoints in DID**: Cannot embed service URLs in the DID document itself
|
||||
- **No revocation**: Cannot revoke a did:key without out-of-band mechanisms
|
||||
|
||||
### Mitigation
|
||||
- Use federation trust lists for key management and revocation
|
||||
- Store service endpoints (onion address, pubkey) separately in federation state
|
||||
- Support migration to did:peer or did:web in future versions if key rotation is needed
|
||||
35
docs/adr/003-nostr-for-discovery.md
Normal file
35
docs/adr/003-nostr-for-discovery.md
Normal file
@@ -0,0 +1,35 @@
|
||||
# ADR-003: Nostr Relays for Node and App Discovery
|
||||
|
||||
**Status**: Accepted
|
||||
**Date**: 2026-03
|
||||
|
||||
## Context
|
||||
|
||||
Archipelago nodes need to discover peers and community apps without a central registry. Options: custom P2P protocol, DHT, BitTorrent tracker, Nostr relays, IPFS.
|
||||
|
||||
## Decision
|
||||
|
||||
Use Nostr relays (NIP-78, kind 30078) for both node discovery and marketplace app manifests.
|
||||
|
||||
## Consequences
|
||||
|
||||
### Positive
|
||||
- **Decentralized**: Multiple independent relays; no single point of failure
|
||||
- **Existing infrastructure**: Thousands of Nostr relays already running globally
|
||||
- **Censorship-resistant**: If one relay censors, others still serve events
|
||||
- **Simple protocol**: WebSocket + JSON — easy to implement without heavy dependencies
|
||||
- **Key management**: Nostr uses secp256k1, same curve as Bitcoin — natural fit
|
||||
- **NIP-33 replaceable events**: Latest event replaces previous — clean update model
|
||||
- **Tor-compatible**: WebSocket over Tor SOCKS proxy works natively
|
||||
|
||||
### Negative
|
||||
- **Relay availability varies**: Some relays may be down or rate-limited
|
||||
- **No guaranteed persistence**: Relays may prune old events
|
||||
- **Spam potential**: Open publishing means anyone can publish junk manifests
|
||||
- **Latency**: Querying multiple relays adds latency to discovery
|
||||
|
||||
### Mitigation
|
||||
- Query multiple relays in parallel; deduplicate results
|
||||
- Cache results locally with 15-minute TTL
|
||||
- Use trust scoring to rank manifests (DID verification, relay consensus, federation trust)
|
||||
- Use hashtag filtering (`archipelago-marketplace`) to narrow queries
|
||||
35
docs/adr/004-tor-for-peer-communication.md
Normal file
35
docs/adr/004-tor-for-peer-communication.md
Normal file
@@ -0,0 +1,35 @@
|
||||
# ADR-004: Tor Hidden Services for Peer Communication
|
||||
|
||||
**Status**: Accepted
|
||||
**Date**: 2026-03
|
||||
|
||||
## Context
|
||||
|
||||
Federated nodes need to communicate directly for state sync, app deployment, and peer verification. Options: direct IP, VPN tunnel, Tor hidden services, I2P.
|
||||
|
||||
## Decision
|
||||
|
||||
Use Tor hidden services (.onion addresses) for all inter-node communication.
|
||||
|
||||
## Consequences
|
||||
|
||||
### Positive
|
||||
- **NAT traversal**: Works behind any firewall or NAT without port forwarding
|
||||
- **IP privacy**: Nodes never expose their real IP addresses to each other
|
||||
- **End-to-end encryption**: Tor provides encryption without additional TLS setup
|
||||
- **Censorship resistance**: Onion routing makes traffic analysis difficult
|
||||
- **Stable addressing**: .onion addresses persist across IP changes and network migrations
|
||||
- **No central infrastructure**: No VPN server, STUN/TURN server, or relay needed
|
||||
|
||||
### Negative
|
||||
- **Latency**: Tor adds 200-500ms per hop; 3 hops per direction = noticeable delay
|
||||
- **Bandwidth**: Tor network has limited bandwidth; not suitable for bulk data transfer
|
||||
- **Reliability**: Tor circuits can break; connections may need retry logic
|
||||
- **Setup complexity**: Requires running a Tor daemon (`archy-tor` container)
|
||||
- **Blocked networks**: Some networks block Tor; bridges can help but add complexity
|
||||
|
||||
### Mitigation
|
||||
- Use Tor only for RPC/control plane; bulk data (container images) pulled from registries
|
||||
- Implement retry with backoff for Tor connections
|
||||
- Container `archy-tor` runs automatically with host networking for hidden service access
|
||||
- Federation sync interval (5 min) tolerates occasional connection failures
|
||||
32
docs/adr/005-chacha20-backup-encryption.md
Normal file
32
docs/adr/005-chacha20-backup-encryption.md
Normal file
@@ -0,0 +1,32 @@
|
||||
# ADR-005: ChaCha20-Poly1305 for Backup Encryption
|
||||
|
||||
**Status**: Accepted
|
||||
**Date**: 2026-03
|
||||
|
||||
## Context
|
||||
|
||||
Backups contain sensitive data (keys, credentials, app state) and must be encrypted at rest. Options: AES-256-GCM, ChaCha20-Poly1305, XChaCha20-Poly1305.
|
||||
|
||||
## Decision
|
||||
|
||||
Use ChaCha20-Poly1305 (AEAD) with Argon2id key derivation for backup encryption.
|
||||
|
||||
## Consequences
|
||||
|
||||
### Positive
|
||||
- **Software performance**: ChaCha20 is faster than AES on hardware without AES-NI (common on ARM/SBCs)
|
||||
- **Constant-time**: No timing side channels, unlike some AES implementations
|
||||
- **AEAD**: Authenticated encryption ensures both confidentiality and integrity
|
||||
- **Widely audited**: Used in TLS 1.3, WireGuard, and Signal Protocol
|
||||
- **Simple implementation**: No padding, no CBC/CTR mode complexity
|
||||
- **Argon2id KDF**: Memory-hard key derivation resists GPU/ASIC brute force attacks
|
||||
|
||||
### Negative
|
||||
- **96-bit nonce**: Must ensure nonce uniqueness per encryption (random generation with collision check)
|
||||
- **Not FIPS-certified**: Some enterprise environments require AES (not relevant for personal nodes)
|
||||
- **Less hardware acceleration**: AES-NI on x86 can make AES faster on desktop CPUs
|
||||
|
||||
### Mitigation
|
||||
- Generate random nonce per backup; store nonce alongside ciphertext
|
||||
- Argon2id with high memory cost (64MB) and iterations (3) for password-to-key derivation
|
||||
- Target hardware is mixed x86/ARM; ChaCha20's consistent performance is an advantage
|
||||
397
docs/api-reference.md
Normal file
397
docs/api-reference.md
Normal file
@@ -0,0 +1,397 @@
|
||||
# Archipelago API Reference
|
||||
|
||||
All endpoints use JSON-RPC over HTTP POST to `/rpc/v1`.
|
||||
|
||||
**Request format:**
|
||||
```json
|
||||
{
|
||||
"method": "namespace.action",
|
||||
"params": { ... }
|
||||
}
|
||||
```
|
||||
|
||||
**Response format:**
|
||||
```json
|
||||
{
|
||||
"result": { ... }
|
||||
}
|
||||
```
|
||||
|
||||
**Error format:**
|
||||
```json
|
||||
{
|
||||
"error": { "message": "Error description" }
|
||||
}
|
||||
```
|
||||
|
||||
**Authentication:** All endpoints require a valid session cookie (`archipelago_session`) except those marked "No Auth".
|
||||
|
||||
---
|
||||
|
||||
## Authentication
|
||||
|
||||
| Method | Params | Returns | Auth |
|
||||
|--------|--------|---------|------|
|
||||
| `auth.login` | `{ password: string }` | `{ ok: bool, totp_required?: bool }` | No Auth |
|
||||
| `auth.logout` | — | `{ ok: bool }` | Yes |
|
||||
| `auth.changePassword` | `{ current: string, new: string }` | `{ ok: bool }` | Yes |
|
||||
| `auth.isOnboardingComplete` | — | `{ complete: bool }` | No Auth |
|
||||
| `auth.onboardingComplete` | — | `{ ok: bool }` | Yes |
|
||||
| `auth.resetOnboarding` | — | `{ ok: bool }` | Yes |
|
||||
|
||||
### TOTP 2FA
|
||||
|
||||
| Method | Params | Returns | Auth |
|
||||
|--------|--------|---------|------|
|
||||
| `auth.totp.setup.begin` | `{ password: string }` | `{ secret: string, qr_uri: string, backup_codes: string[] }` | Yes |
|
||||
| `auth.totp.setup.confirm` | `{ code: string }` | `{ ok: bool }` | Yes |
|
||||
| `auth.totp.disable` | `{ password: string }` | `{ ok: bool }` | Yes |
|
||||
| `auth.totp.status` | — | `{ enabled: bool }` | Yes |
|
||||
| `auth.login.totp` | `{ code: string }` | `{ ok: bool }` | No Auth |
|
||||
| `auth.login.backup` | `{ code: string }` | `{ ok: bool }` | No Auth |
|
||||
|
||||
---
|
||||
|
||||
## Container Orchestration
|
||||
|
||||
| Method | Params | Returns | Auth |
|
||||
|--------|--------|---------|------|
|
||||
| `container-install` | `{ image: string, name?: string }` | `{ ok: bool, container_id: string }` | Yes |
|
||||
| `container-start` | `{ id: string }` | `{ ok: bool }` | Yes |
|
||||
| `container-stop` | `{ id: string }` | `{ ok: bool }` | Yes |
|
||||
| `container-remove` | `{ id: string }` | `{ ok: bool }` | Yes |
|
||||
| `container-list` | — | `{ containers: Container[] }` | Yes |
|
||||
| `container-status` | `{ id: string }` | `{ status: string, ... }` | Yes |
|
||||
| `container-logs` | `{ id: string, lines?: number }` | `{ logs: string }` | Yes |
|
||||
| `container-health` | `{ id: string }` | `{ healthy: bool }` | Yes |
|
||||
|
||||
## Package Management
|
||||
|
||||
| Method | Params | Returns | Auth |
|
||||
|--------|--------|---------|------|
|
||||
| `package.install` | `{ id: string, dockerImage?: string, url?: string, version?: string }` | `{ ok: bool }` | Yes |
|
||||
| `package.start` | `{ id: string }` | `{ ok: bool }` | Yes |
|
||||
| `package.stop` | `{ id: string }` | `{ ok: bool }` | Yes |
|
||||
| `package.restart` | `{ id: string }` | `{ ok: bool }` | Yes |
|
||||
| `package.uninstall` | `{ id: string }` | `{ ok: bool }` | Yes |
|
||||
|
||||
## Bundled Apps
|
||||
|
||||
| Method | Params | Returns | Auth |
|
||||
|--------|--------|---------|------|
|
||||
| `bundled-app-start` | `{ id: string }` | `{ ok: bool }` | Yes |
|
||||
| `bundled-app-stop` | `{ id: string }` | `{ ok: bool }` | Yes |
|
||||
|
||||
---
|
||||
|
||||
## Node Identity & P2P
|
||||
|
||||
| Method | Params | Returns | Auth |
|
||||
|--------|--------|---------|------|
|
||||
| `node.did` | — | `{ did: string }` | Yes |
|
||||
| `node.signChallenge` | `{ challenge: string }` | `{ signature: string }` | Yes |
|
||||
| `node.tor-address` | — | `{ address: string }` | Yes |
|
||||
| `node.nostr-publish` | — | `{ ok: bool, event_id: string }` | Yes |
|
||||
| `node.nostr-pubkey` | — | `{ pubkey: string }` | Yes |
|
||||
| `node-nostr-verify-revoked` | — | `{ revoked: bool, nostr_pubkey: string }` | Yes |
|
||||
| `node-nostr-discover` | — | `{ nodes: DiscoveredNode[] }` | Yes |
|
||||
| `node-add-peer` | `{ did: string, address: string }` | `{ ok: bool }` | Yes |
|
||||
| `node-list-peers` | — | `{ peers: Peer[] }` | Yes |
|
||||
| `node-remove-peer` | `{ did: string }` | `{ ok: bool }` | Yes |
|
||||
| `node-send-message` | `{ to: string, message: string }` | `{ ok: bool }` | Yes |
|
||||
| `node-check-peer` | `{ did: string }` | `{ online: bool }` | Yes |
|
||||
| `node-messages-received` | — | `{ messages: Message[] }` | Yes |
|
||||
| `node.createBackup` | `{ password: string }` | `{ path: string }` | Yes |
|
||||
|
||||
---
|
||||
|
||||
## Identity Management
|
||||
|
||||
### Multi-Identity
|
||||
|
||||
| Method | Params | Returns | Auth |
|
||||
|--------|--------|---------|------|
|
||||
| `identity.list` | `{}` | `{ identities: Identity[] }` | Yes |
|
||||
| `identity.create` | `{ label: string }` | `{ identity: Identity }` | Yes |
|
||||
| `identity.get` | `{ id: string }` | `{ identity: Identity }` | Yes |
|
||||
| `identity.delete` | `{ id: string }` | `{ ok: bool }` | Yes |
|
||||
| `identity.set-default` | `{ id: string }` | `{ ok: bool }` | Yes |
|
||||
| `identity.sign` | `{ id: string, data: string }` | `{ signature: string }` | Yes |
|
||||
| `identity.verify` | `{ id: string, data: string, signature: string }` | `{ valid: bool }` | Yes |
|
||||
| `identity.resolve-did` | `{ did: string }` | `{ document: DIDDocument }` | Yes |
|
||||
| `identity.resolve-remote-did` | `{ did: string }` | `{ document: DIDDocument }` | Yes |
|
||||
| `identity.verify-did-document` | `{ document: object }` | `{ valid: bool }` | Yes |
|
||||
|
||||
### Nostr Keys
|
||||
|
||||
| Method | Params | Returns | Auth |
|
||||
|--------|--------|---------|------|
|
||||
| `identity.create-nostr-key` | `{ id: string }` | `{ pubkey: string, npub: string }` | Yes |
|
||||
| `identity.nostr-sign` | `{ id: string, event: object }` | `{ signed_event: object }` | Yes |
|
||||
|
||||
### Bitcoin Names (NIP-05)
|
||||
|
||||
| Method | Params | Returns | Auth |
|
||||
|--------|--------|---------|------|
|
||||
| `identity.register-name` | `{ name: string, pubkey: string }` | `{ ok: bool }` | Yes |
|
||||
| `identity.remove-name` | `{ name: string }` | `{ ok: bool }` | Yes |
|
||||
| `identity.resolve-name` | `{ name: string }` | `{ pubkey: string }` | Yes |
|
||||
| `identity.list-names` | `{}` | `{ names: NameEntry[] }` | Yes |
|
||||
| `identity.link-name` | `{ name: string, identity_id: string }` | `{ ok: bool }` | Yes |
|
||||
|
||||
### Verifiable Credentials
|
||||
|
||||
| Method | Params | Returns | Auth |
|
||||
|--------|--------|---------|------|
|
||||
| `identity.issue-credential` | `{ subject: string, claims: object }` | `{ credential: VC }` | Yes |
|
||||
| `identity.verify-credential` | `{ credential: object }` | `{ valid: bool }` | Yes |
|
||||
| `identity.list-credentials` | `{ id?: string }` | `{ credentials: VC[] }` | Yes |
|
||||
| `identity.revoke-credential` | `{ credential_id: string }` | `{ ok: bool }` | Yes |
|
||||
| `identity.create-presentation` | `{ credentials: string[] }` | `{ presentation: VP }` | Yes |
|
||||
| `identity.verify-presentation` | `{ presentation: object }` | `{ valid: bool }` | Yes |
|
||||
|
||||
---
|
||||
|
||||
## Bitcoin & Lightning
|
||||
|
||||
| Method | Params | Returns | Auth |
|
||||
|--------|--------|---------|------|
|
||||
| `bitcoin.getinfo` | — | `{ blocks: number, connections: number, ... }` | Yes |
|
||||
| `lnd.getinfo` | — | `{ identity_pubkey: string, num_active_channels: number, ... }` | Yes |
|
||||
| `lnd.listchannels` | — | `{ channels: Channel[] }` | Yes |
|
||||
| `lnd.openchannel` | `{ pubkey: string, amount: number }` | `{ funding_txid: string }` | Yes |
|
||||
| `lnd.closechannel` | `{ channel_point: string }` | `{ closing_txid: string }` | Yes |
|
||||
| `lnd.newaddress` | — | `{ address: string }` | Yes |
|
||||
| `lnd.sendcoins` | `{ addr: string, amount: number }` | `{ txid: string }` | Yes |
|
||||
| `lnd.createinvoice` | `{ amount: number, memo?: string }` | `{ payment_request: string }` | Yes |
|
||||
| `lnd.payinvoice` | `{ payment_request: string }` | `{ preimage: string }` | Yes |
|
||||
| `lnd.create-psbt` | `{ outputs: object, ... }` | `{ psbt: string }` | Yes |
|
||||
| `lnd.finalize-psbt` | `{ psbt: string }` | `{ signed_psbt: string }` | Yes |
|
||||
|
||||
---
|
||||
|
||||
## Ecash Wallet
|
||||
|
||||
| Method | Params | Returns | Auth |
|
||||
|--------|--------|---------|------|
|
||||
| `wallet.ecash-balance` | — | `{ balance: number, mint_url: string }` | Yes |
|
||||
| `wallet.ecash-mint` | `{ amount: number }` | `{ ok: bool }` | Yes |
|
||||
| `wallet.ecash-melt` | `{ amount: number, invoice: string }` | `{ ok: bool }` | Yes |
|
||||
| `wallet.ecash-send` | `{ amount: number }` | `{ token: string }` | Yes |
|
||||
| `wallet.ecash-receive` | `{ token: string }` | `{ amount: number }` | Yes |
|
||||
| `wallet.ecash-history` | — | `{ transactions: EcashTx[] }` | Yes |
|
||||
| `wallet.networking-profits` | — | `{ total_sats: number, ... }` | Yes |
|
||||
|
||||
---
|
||||
|
||||
## Network
|
||||
|
||||
### Interfaces & WiFi
|
||||
|
||||
| Method | Params | Returns | Auth |
|
||||
|--------|--------|---------|------|
|
||||
| `network.list-interfaces` | — | `{ interfaces: Interface[] }` | Yes |
|
||||
| `network.scan-wifi` | — | `{ networks: WifiNetwork[] }` | Yes |
|
||||
| `network.configure-wifi` | `{ ssid: string, password: string }` | `{ ok: bool }` | Yes |
|
||||
| `network.configure-ethernet` | `{ interface: string, mode: "dhcp"\|"static", ip?: string, gateway?: string, dns?: string }` | `{ ok: bool }` | Yes |
|
||||
| `network.diagnostics` | — | `{ wan_ip: string, nat_type: string, upnp_available: bool, tor_connected: bool, wifi_count: number }` | Yes |
|
||||
|
||||
### DNS
|
||||
|
||||
| Method | Params | Returns | Auth |
|
||||
|--------|--------|---------|------|
|
||||
| `network.dns-status` | — | `{ provider: string, servers: string[], doh_enabled: bool, doh_url: string?, resolv_conf_servers: string[] }` | Yes |
|
||||
| `network.configure-dns` | `{ provider: "system"\|"cloudflare"\|"google"\|"quad9"\|"mullvad"\|"custom", servers?: string[] }` | `{ ok: bool, provider: string, servers: string[], doh_enabled: bool }` | Yes |
|
||||
|
||||
### Network Overlay
|
||||
|
||||
| Method | Params | Returns | Auth |
|
||||
|--------|--------|---------|------|
|
||||
| `network.get-visibility` | — | `{ visibility: string }` | Yes |
|
||||
| `network.set-visibility` | `{ visibility: string }` | `{ ok: bool }` | Yes |
|
||||
| `network.request-connection` | `{ target_did: string }` | `{ request_id: string }` | Yes |
|
||||
| `network.list-requests` | — | `{ requests: ConnectionRequest[] }` | Yes |
|
||||
| `network.accept-request` | `{ request_id: string }` | `{ ok: bool }` | Yes |
|
||||
| `network.reject-request` | `{ request_id: string }` | `{ ok: bool }` | Yes |
|
||||
|
||||
### Router / UPnP
|
||||
|
||||
| Method | Params | Returns | Auth |
|
||||
|--------|--------|---------|------|
|
||||
| `router.discover` | — | `{ router: RouterInfo }` | Yes |
|
||||
| `router.list-forwards` | — | `{ forwards: PortForward[] }` | Yes |
|
||||
| `router.add-forward` | `{ port: number, protocol: string, description: string }` | `{ ok: bool }` | Yes |
|
||||
| `router.remove-forward` | `{ port: number, protocol: string }` | `{ ok: bool }` | Yes |
|
||||
| `router.detect` | `{ ... }` | `{ detected: bool, ... }` | Yes |
|
||||
| `router.info` | — | `{ ... }` | Yes |
|
||||
| `router.configure` | `{ ... }` | `{ ok: bool }` | Yes |
|
||||
|
||||
---
|
||||
|
||||
## Tor
|
||||
|
||||
| Method | Params | Returns | Auth |
|
||||
|--------|--------|---------|------|
|
||||
| `tor.list-services` | — | `{ services: TorService[] }` | Yes |
|
||||
| `tor.create-service` | `{ name: string, port: number }` | `{ onion_address: string }` | Yes |
|
||||
| `tor.delete-service` | `{ name: string }` | `{ ok: bool }` | Yes |
|
||||
| `tor.get-onion-address` | `{ name: string }` | `{ address: string }` | Yes |
|
||||
|
||||
## Nostr Relays
|
||||
|
||||
| Method | Params | Returns | Auth |
|
||||
|--------|--------|---------|------|
|
||||
| `nostr.list-relays` | — | `{ relays: RelayConfig[] }` | Yes |
|
||||
| `nostr.add-relay` | `{ url: string }` | `{ ok: bool }` | Yes |
|
||||
| `nostr.remove-relay` | `{ url: string }` | `{ ok: bool }` | Yes |
|
||||
| `nostr.toggle-relay` | `{ url: string }` | `{ ok: bool }` | Yes |
|
||||
| `nostr.get-stats` | — | `{ total_relays: number, connected: number, enabled: number }` | Yes |
|
||||
|
||||
---
|
||||
|
||||
## VPN
|
||||
|
||||
| Method | Params | Returns | Auth |
|
||||
|--------|--------|---------|------|
|
||||
| `vpn.status` | — | `{ connected: bool, provider?: string, ip_address?: string, hostname?: string, peers_connected: number }` | Yes |
|
||||
| `vpn.configure` | `{ provider: "tailscale"\|"wireguard", auth_key?: string, address?: string, dns?: string, peer?: object }` | `{ ok: bool }` | Yes |
|
||||
| `vpn.disconnect` | — | `{ disconnected: bool }` | Yes |
|
||||
|
||||
## Mesh Networking
|
||||
|
||||
| Method | Params | Returns | Auth |
|
||||
|--------|--------|---------|------|
|
||||
| `mesh.status` | — | `{ enabled: bool, device: string?, nodes: MeshNode[] }` | Yes |
|
||||
| `mesh.discover` | `{ timeout_secs?: number }` | `{ nodes: MeshNode[] }` | Yes |
|
||||
| `mesh.broadcast` | — | `{ ok: bool }` | Yes |
|
||||
| `mesh.configure` | `{ enabled: bool, device?: string }` | `{ ok: bool }` | Yes |
|
||||
|
||||
---
|
||||
|
||||
## Federation
|
||||
|
||||
| Method | Params | Returns | Auth |
|
||||
|--------|--------|---------|------|
|
||||
| `federation.invite` | — | `{ code: string }` | Yes |
|
||||
| `federation.join` | `{ code: string }` | `{ ok: bool, node: FederatedNode }` | Yes |
|
||||
| `federation.list-nodes` | — | `{ nodes: FederatedNode[] }` | Yes |
|
||||
| `federation.remove-node` | `{ did: string }` | `{ ok: bool }` | Yes |
|
||||
| `federation.set-trust` | `{ did: string, trust: "trusted"\|"observer"\|"untrusted" }` | `{ ok: bool }` | Yes |
|
||||
| `federation.sync-state` | — | `{ results: SyncResult[] }` | Yes |
|
||||
| `federation.get-state` | — | `{ state: NodeStateSnapshot }` | Federation peer |
|
||||
| `federation.peer-joined` | `{ did: string, onion: string, pubkey: string }` | `{ accepted: bool }` | Federation peer |
|
||||
| `federation.deploy-app` | `{ target_did: string, app_id: string, version?: string }` | `{ ok: bool }` | Yes |
|
||||
|
||||
---
|
||||
|
||||
## Marketplace
|
||||
|
||||
| Method | Params | Returns | Auth |
|
||||
|--------|--------|---------|------|
|
||||
| `marketplace.discover` | — | `{ apps: DiscoveredApp[], relay_count: number }` | Yes |
|
||||
| `marketplace.publish` | `{ app_id, name, version, description, author, container, category, ... }` | `{ ok: bool, event_id: string }` | Yes |
|
||||
| `marketplace.get-manifest` | `{ app_id: string }` | `DiscoveredApp \| { error: string }` | Yes |
|
||||
| `marketplace.list-published` | — | `{ manifests: AppManifest[] }` | Yes |
|
||||
| `marketplace.verify` | `{ ... manifest fields ... }` | `{ valid: bool, issues: string[], trust_score: number }` | Yes |
|
||||
|
||||
---
|
||||
|
||||
## DWN (Decentralized Web Node)
|
||||
|
||||
| Method | Params | Returns | Auth |
|
||||
|--------|--------|---------|------|
|
||||
| `dwn.status` | — | `{ running: bool, message_count: number, protocol_count: number }` | Yes |
|
||||
| `dwn.sync` | — | `{ synced: number }` | Yes |
|
||||
| `dwn.register-protocol` | `{ uri: string, definition: object }` | `{ ok: bool }` | Yes |
|
||||
| `dwn.list-protocols` | — | `{ protocols: Protocol[] }` | Yes |
|
||||
| `dwn.remove-protocol` | `{ uri: string }` | `{ ok: bool }` | Yes |
|
||||
| `dwn.query-messages` | `{ protocol?: string, limit?: number }` | `{ messages: DwnMessage[] }` | Yes |
|
||||
| `dwn.write-message` | `{ protocol: string, data: object }` | `{ ok: bool, message_id: string }` | Yes |
|
||||
|
||||
---
|
||||
|
||||
## Content Catalog
|
||||
|
||||
| Method | Params | Returns | Auth |
|
||||
|--------|--------|---------|------|
|
||||
| `content.list-mine` | — | `{ items: ContentItem[] }` | Yes |
|
||||
| `content.add` | `{ title: string, type: string, data: object }` | `{ ok: bool, id: string }` | Yes |
|
||||
| `content.remove` | `{ id: string }` | `{ ok: bool }` | Yes |
|
||||
| `content.set-pricing` | `{ id: string, price_sats: number }` | `{ ok: bool }` | Yes |
|
||||
| `content.set-availability` | `{ id: string, available: bool }` | `{ ok: bool }` | Yes |
|
||||
| `content.browse-peer` | `{ peer_did: string }` | `{ items: ContentItem[] }` | Yes |
|
||||
|
||||
---
|
||||
|
||||
## System
|
||||
|
||||
### Monitoring
|
||||
|
||||
| Method | Params | Returns | Auth |
|
||||
|--------|--------|---------|------|
|
||||
| `system.stats` | — | `{ cpu_percent: number, ram_used: number, ram_total: number, disk_used: number, disk_total: number, uptime_secs: number, load_avg: number[] }` | Yes |
|
||||
| `system.processes` | — | `{ processes: Process[] }` | Yes |
|
||||
| `system.temperature` | — | `{ celsius: number? }` | Yes |
|
||||
| `system.detect-usb-devices` | — | `{ devices: UsbDevice[] }` | Yes |
|
||||
|
||||
### Updates
|
||||
|
||||
| Method | Params | Returns | Auth |
|
||||
|--------|--------|---------|------|
|
||||
| `update.check` | — | `{ available: bool, version?: string, changelog?: string }` | Yes |
|
||||
| `update.status` | — | `{ state: string, progress?: number }` | Yes |
|
||||
| `update.dismiss` | — | `{ ok: bool }` | Yes |
|
||||
| `update.download` | — | `{ ok: bool }` | Yes |
|
||||
| `update.apply` | — | `{ ok: bool }` | Yes |
|
||||
| `update.rollback` | — | `{ ok: bool }` | Yes |
|
||||
| `update.get-schedule` | — | `{ auto_check: bool, auto_install: bool, schedule: string }` | Yes |
|
||||
| `update.set-schedule` | `{ auto_check?: bool, auto_install?: bool, schedule?: string }` | `{ ok: bool }` | Yes |
|
||||
|
||||
### Backup & Restore
|
||||
|
||||
| Method | Params | Returns | Auth |
|
||||
|--------|--------|---------|------|
|
||||
| `backup.create` | `{ password: string, include?: string[] }` | `{ path: string, size: number }` | Yes |
|
||||
| `backup.list` | — | `{ backups: BackupEntry[] }` | Yes |
|
||||
| `backup.verify` | `{ path: string, password: string }` | `{ valid: bool }` | Yes |
|
||||
| `backup.restore` | `{ path: string, password: string }` | `{ ok: bool }` | Yes |
|
||||
| `backup.delete` | `{ path: string }` | `{ ok: bool }` | Yes |
|
||||
| `backup.list-drives` | — | `{ drives: UsbDrive[] }` | Yes |
|
||||
| `backup.to-usb` | `{ drive: string, password: string }` | `{ ok: bool }` | Yes |
|
||||
|
||||
### Security
|
||||
|
||||
| Method | Params | Returns | Auth |
|
||||
|--------|--------|---------|------|
|
||||
| `security.rotate-secrets` | `{ app_id?: string }` | `{ rotated: string[] }` | Yes |
|
||||
| `security.list-expiring` | `{ days?: number }` | `{ secrets: ExpiringSecret[] }` | Yes |
|
||||
|
||||
---
|
||||
|
||||
## Utility
|
||||
|
||||
| Method | Params | Returns | Auth |
|
||||
|--------|--------|---------|------|
|
||||
| `echo` | `{ message: string }` | `{ message: string }` | No Auth |
|
||||
| `server.echo` | `{ message: string }` | `{ message: string }` | No Auth |
|
||||
|
||||
---
|
||||
|
||||
## Example: cURL
|
||||
|
||||
```bash
|
||||
# Login
|
||||
curl -c cookies.txt -X POST http://192.168.1.228/rpc/v1 \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"method":"auth.login","params":{"password":"password123"}}'
|
||||
|
||||
# Get system stats (authenticated)
|
||||
curl -b cookies.txt -X POST http://192.168.1.228/rpc/v1 \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"method":"system.stats"}'
|
||||
|
||||
# Get DID
|
||||
curl -b cookies.txt -X POST http://192.168.1.228/rpc/v1 \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"method":"node.did"}'
|
||||
```
|
||||
277
docs/app-developer-guide.md
Normal file
277
docs/app-developer-guide.md
Normal file
@@ -0,0 +1,277 @@
|
||||
# Archipelago App Developer Guide
|
||||
|
||||
Build and publish containerized apps for the Archipelago ecosystem.
|
||||
|
||||
## Overview
|
||||
|
||||
Apps run as Podman containers on user nodes. You publish app manifests to Nostr relays, where nodes discover and install them through the community marketplace.
|
||||
|
||||
## App Manifest
|
||||
|
||||
Every app needs a manifest (YAML for local apps, JSON for marketplace publishing).
|
||||
|
||||
### Template Manifest
|
||||
|
||||
```yaml
|
||||
# apps/my-app/manifest.yml
|
||||
app:
|
||||
id: my-app # Unique, lowercase kebab-case
|
||||
name: My App
|
||||
version: 1.0.0 # Semantic versioning
|
||||
|
||||
container:
|
||||
image: docker.io/myorg/my-app:1.0.0 # Never use :latest
|
||||
ports:
|
||||
- container: 8080
|
||||
host: 8180
|
||||
protocol: tcp
|
||||
volumes:
|
||||
- name: data
|
||||
path: /data
|
||||
env:
|
||||
APP_MODE: production
|
||||
capabilities: [] # Only add if absolutely necessary
|
||||
readonly_root: true # Required
|
||||
no_new_privileges: true # Required
|
||||
run_as_user: 1000 # Must be >= 1000
|
||||
|
||||
metadata:
|
||||
description:
|
||||
short: "One-line description (max 120 chars)"
|
||||
long: "Detailed description of what this app does and why."
|
||||
author:
|
||||
name: "Your Name"
|
||||
did: "did:key:z6Mk..." # Your Archipelago node DID
|
||||
category: money # money | commerce | data | networking | home | community | other
|
||||
icon_url: "https://example.com/icon.png"
|
||||
repo_url: "https://github.com/myorg/my-app"
|
||||
license: MIT
|
||||
min_archipelago_version: "0.1.0"
|
||||
dependencies: [] # e.g., ["bitcoin-knots"] if this app needs Bitcoin
|
||||
```
|
||||
|
||||
### Required Fields
|
||||
|
||||
| Field | Description |
|
||||
|-------|-------------|
|
||||
| `app.id` | Unique identifier, lowercase, kebab-case only |
|
||||
| `app.name` | Human-readable name |
|
||||
| `app.version` | Semantic version (major.minor.patch) |
|
||||
| `container.image` | Full image reference with pinned version tag |
|
||||
| `metadata.description.short` | One-line description, max 120 characters |
|
||||
| `metadata.author.did` | Your node's DID (get via `node.did` RPC) |
|
||||
|
||||
## Security Requirements
|
||||
|
||||
These are enforced by the marketplace and the node. Non-compliant apps are flagged.
|
||||
|
||||
### Mandatory
|
||||
|
||||
1. **No `:latest` tag** — Pin a specific version: `myapp:1.0.0`
|
||||
2. **Read-only root filesystem** — `readonly_root: true` (use volumes for writable data)
|
||||
3. **Non-root user** — `run_as_user: 1000` or higher
|
||||
4. **No privilege escalation** — `no_new_privileges: true`
|
||||
5. **Minimal capabilities** — Drop all caps, only add required ones
|
||||
|
||||
### Allowed Capabilities
|
||||
|
||||
Only these Linux capabilities may be requested:
|
||||
|
||||
| Capability | When Needed |
|
||||
|-----------|-------------|
|
||||
| `CHOWN` | App needs to change file ownership |
|
||||
| `NET_BIND_SERVICE` | App binds to ports below 1024 |
|
||||
| `DAC_OVERRIDE` | App needs to bypass file permissions |
|
||||
| `SETUID`, `SETGID` | App manages user switching (e.g., nginx) |
|
||||
|
||||
### Forbidden
|
||||
|
||||
- `--network host` — Apps cannot share the host network
|
||||
- Mounting system paths: `/`, `/etc`, `/var`, `/usr`, `/proc`, `/sys`
|
||||
- `SYS_ADMIN`, `SYS_PTRACE`, or any privileged capability
|
||||
- Hardcoded secrets in environment variables or images
|
||||
|
||||
## Container Best Practices
|
||||
|
||||
### Volumes
|
||||
|
||||
```yaml
|
||||
volumes:
|
||||
- name: data # App data persists across restarts
|
||||
path: /data
|
||||
- name: config # Configuration files
|
||||
path: /config
|
||||
```
|
||||
|
||||
Data is stored at `/var/lib/archipelago/{app-id}/` on the host.
|
||||
|
||||
### Health Checks
|
||||
|
||||
Define a health check endpoint in your container:
|
||||
|
||||
```dockerfile
|
||||
HEALTHCHECK --interval=30s --timeout=5s --retries=3 \
|
||||
CMD curl -f http://localhost:8080/health || exit 1
|
||||
```
|
||||
|
||||
### Logging
|
||||
|
||||
- Log to stdout/stderr (Podman captures container logs)
|
||||
- Never log secrets, passwords, or keys
|
||||
- Use structured logging (JSON) for machine parsing
|
||||
|
||||
### Networking
|
||||
|
||||
Apps get their own network namespace. To connect to other Archipelago apps:
|
||||
|
||||
```yaml
|
||||
# If your app needs to talk to Bitcoin
|
||||
dependencies:
|
||||
- bitcoin-knots
|
||||
|
||||
container:
|
||||
env:
|
||||
BITCOIN_RPC_HOST: bitcoin-knots # Container DNS name on archy-net
|
||||
BITCOIN_RPC_PORT: "8332"
|
||||
```
|
||||
|
||||
The `archy-net` Podman network provides DNS resolution between containers.
|
||||
|
||||
## Publishing to the Marketplace
|
||||
|
||||
### 1. Build and Push Your Image
|
||||
|
||||
```bash
|
||||
podman build -t docker.io/myorg/my-app:1.0.0 .
|
||||
podman push docker.io/myorg/my-app:1.0.0
|
||||
```
|
||||
|
||||
### 2. Get Your Node's DID
|
||||
|
||||
```bash
|
||||
curl -b cookies.txt -X POST http://localhost/rpc/v1 \
|
||||
-d '{"method":"node.did"}'
|
||||
# Returns: {"result":{"did":"did:key:z6Mk..."}}
|
||||
```
|
||||
|
||||
### 3. Publish via RPC
|
||||
|
||||
```bash
|
||||
curl -b cookies.txt -X POST http://localhost/rpc/v1 \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"method": "marketplace.publish",
|
||||
"params": {
|
||||
"app_id": "my-app",
|
||||
"name": "My App",
|
||||
"version": "1.0.0",
|
||||
"description": {"short": "A useful tool", "long": "Detailed description..."},
|
||||
"author": {"name": "Dev Name", "did": "did:key:z6Mk...", "nostr_pubkey": ""},
|
||||
"container": {
|
||||
"image": "docker.io/myorg/my-app:1.0.0",
|
||||
"ports": [{"container": 8080, "host": 8180, "protocol": "tcp"}],
|
||||
"volumes": [],
|
||||
"env": {},
|
||||
"capabilities": [],
|
||||
"readonly_root": true,
|
||||
"no_new_privileges": true,
|
||||
"run_as_user": 1000
|
||||
},
|
||||
"category": "other",
|
||||
"icon_url": "",
|
||||
"repo_url": "https://github.com/myorg/my-app",
|
||||
"license": "MIT",
|
||||
"min_archipelago_version": "0.1.0",
|
||||
"dependencies": []
|
||||
}
|
||||
}'
|
||||
```
|
||||
|
||||
The manifest is published to all configured Nostr relays as a NIP-78 event (kind 30078).
|
||||
|
||||
### 4. Verify Discovery
|
||||
|
||||
```bash
|
||||
curl -b cookies.txt -X POST http://localhost/rpc/v1 \
|
||||
-d '{"method":"marketplace.discover"}'
|
||||
# Your app should appear in the results
|
||||
```
|
||||
|
||||
## Trust Model
|
||||
|
||||
Published apps receive trust scores (0-100) based on:
|
||||
|
||||
| Factor | Points | How to Maximize |
|
||||
|--------|--------|-----------------|
|
||||
| Valid DID in author | 30 | Always include your node's DID |
|
||||
| Found on multiple relays | 5-20 | Configure many relays in your node |
|
||||
| Developer in federation | 20 | Have federated peers who trust you |
|
||||
| Proper semver version | 10 | Use `major.minor.patch` format |
|
||||
| Repository URL present | 5 | Include your repo URL |
|
||||
| Security compliance | 15 | Meet all security requirements |
|
||||
|
||||
### Trust Tiers
|
||||
|
||||
| Score | Tier | User Experience |
|
||||
|-------|------|----------------|
|
||||
| 80-100 | Verified | One-click install |
|
||||
| 50-79 | Community | Install with confirmation |
|
||||
| 20-49 | Unverified | Install with warning |
|
||||
| 0-19 | Untrusted | Requires explicit override |
|
||||
|
||||
## Testing Your App
|
||||
|
||||
### Local Testing
|
||||
|
||||
```bash
|
||||
# Run your container locally
|
||||
podman run -d --name my-app \
|
||||
-p 8180:8080 \
|
||||
--read-only \
|
||||
--security-opt no-new-privileges \
|
||||
--user 1000:1000 \
|
||||
docker.io/myorg/my-app:1.0.0
|
||||
|
||||
# Verify it works
|
||||
curl http://localhost:8180/health
|
||||
|
||||
# Check logs
|
||||
podman logs my-app
|
||||
```
|
||||
|
||||
### On an Archipelago Node
|
||||
|
||||
1. Install via the marketplace UI or RPC:
|
||||
```bash
|
||||
curl -b cookies.txt -X POST http://192.168.1.228/rpc/v1 \
|
||||
-d '{"method":"package.install","params":{"id":"my-app","dockerImage":"docker.io/myorg/my-app:1.0.0"}}'
|
||||
```
|
||||
2. Verify the container is running:
|
||||
```bash
|
||||
curl -b cookies.txt -X POST http://192.168.1.228/rpc/v1 \
|
||||
-d '{"method":"container-list"}'
|
||||
```
|
||||
3. Check the UI at `http://192.168.1.228/app/my-app/`
|
||||
|
||||
### Validate Manifest
|
||||
|
||||
```bash
|
||||
curl -b cookies.txt -X POST http://localhost/rpc/v1 \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"method":"marketplace.verify","params":{...your manifest...}}'
|
||||
# Returns: {"result":{"valid":true,"issues":[],"trust_score":65,"trust_tier":"community"}}
|
||||
```
|
||||
|
||||
## Updating Your App
|
||||
|
||||
1. Build and push the new version: `docker.io/myorg/my-app:1.1.0`
|
||||
2. Publish an updated manifest with the new version
|
||||
3. NIP-33 replaceable events: the latest publish overwrites the previous one on relays
|
||||
4. Nodes running your app can see the update in their marketplace
|
||||
|
||||
## App Icon
|
||||
|
||||
- Provide a URL to your app icon (PNG, WebP, or SVG)
|
||||
- Recommended size: 256x256 pixels
|
||||
- Square aspect ratio
|
||||
- If no icon URL, a generic placeholder is shown in the marketplace
|
||||
114
docs/arm64-build.md
Normal file
114
docs/arm64-build.md
Normal file
@@ -0,0 +1,114 @@
|
||||
# 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 |
|
||||
21
docs/arm64-container-images.md
Normal file
21
docs/arm64-container-images.md
Normal file
@@ -0,0 +1,21 @@
|
||||
# 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
|
||||
78
docs/arm64-rpi5-testing.md
Normal file
78
docs/arm64-rpi5-testing.md
Normal file
@@ -0,0 +1,78 @@
|
||||
# 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
|
||||
69
docs/canary-deploy.md
Normal file
69
docs/canary-deploy.md
Normal file
@@ -0,0 +1,69 @@
|
||||
# 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.
|
||||
311
docs/developer-guide.md
Normal file
311
docs/developer-guide.md
Normal file
@@ -0,0 +1,311 @@
|
||||
# Archipelago Developer Guide
|
||||
|
||||
## Project Structure
|
||||
|
||||
```
|
||||
archy/
|
||||
├── core/ # Rust backend
|
||||
│ └── archipelago/
|
||||
│ ├── src/
|
||||
│ │ ├── main.rs # Entry point, module declarations
|
||||
│ │ ├── api/rpc/ # RPC endpoint handlers
|
||||
│ │ │ ├── mod.rs # Route dispatcher
|
||||
│ │ │ ├── auth.rs # Login, session, TOTP
|
||||
│ │ │ ├── container.rs # Container lifecycle
|
||||
│ │ │ ├── package.rs # Package install/remove
|
||||
│ │ │ ├── interfaces.rs # Network interfaces, WiFi, DNS
|
||||
│ │ │ ├── federation.rs # Federation management
|
||||
│ │ │ ├── marketplace.rs # Community marketplace
|
||||
│ │ │ └── ... # Other endpoint groups
|
||||
│ │ ├── auth.rs # Password hashing, sessions
|
||||
│ │ ├── config.rs # Configuration loading
|
||||
│ │ ├── server.rs # HTTP/WS server (axum)
|
||||
│ │ ├── container/ # Podman integration
|
||||
│ │ ├── network/ # Network management
|
||||
│ │ │ ├── dns.rs # DNS configuration
|
||||
│ │ │ ├── router.rs # UPnP, diagnostics
|
||||
│ │ │ └── dwn_*.rs # DWN protocol
|
||||
│ │ ├── federation.rs # Federation protocol
|
||||
│ │ ├── marketplace.rs # Marketplace discovery
|
||||
│ │ ├── identity.rs # DID key management
|
||||
│ │ ├── vpn.rs # VPN (Tailscale/WireGuard)
|
||||
│ │ ├── mesh.rs # Meshtastic mesh networking
|
||||
│ │ └── ...
|
||||
│ ├── Cargo.toml
|
||||
│ └── tests/ # Integration tests
|
||||
├── neode-ui/ # Vue 3 frontend
|
||||
│ ├── src/
|
||||
│ │ ├── api/ # RPC client, WebSocket, container client
|
||||
│ │ │ └── rpc-client.ts # Central RPC client (all backend calls)
|
||||
│ │ ├── views/ # Page components
|
||||
│ │ │ ├── Home.vue # Dashboard with system stats
|
||||
│ │ │ ├── Marketplace.vue # App store (curated + community)
|
||||
│ │ │ ├── Server.vue # Network, VPN, DNS management
|
||||
│ │ │ ├── Federation.vue # Federation dashboard
|
||||
│ │ │ ├── Settings.vue # User settings
|
||||
│ │ │ ├── Web5.vue # DID, DWN, Nostr
|
||||
│ │ │ └── ...
|
||||
│ │ ├── stores/ # Pinia state management
|
||||
│ │ ├── components/ # Reusable UI components
|
||||
│ │ ├── composables/ # Vue composables
|
||||
│ │ ├── router/ # Vue Router with guards
|
||||
│ │ ├── types/ # TypeScript type definitions
|
||||
│ │ └── style.css # Global styles + Tailwind utilities
|
||||
│ ├── vite.config.ts
|
||||
│ └── package.json
|
||||
├── scripts/ # Deployment and utility scripts
|
||||
│ ├── deploy-to-target.sh # Main deploy script
|
||||
│ ├── first-boot-containers.sh # ISO first-boot setup
|
||||
│ └── run-tests.sh # CI test runner
|
||||
├── image-recipe/ # ISO build configuration
|
||||
│ ├── build-auto-installer-iso.sh
|
||||
│ └── configs/ # Nginx, systemd configs
|
||||
├── docs/ # Documentation
|
||||
│ ├── architecture.md
|
||||
│ ├── app-manifest-spec.md
|
||||
│ ├── marketplace-protocol.md
|
||||
│ └── multi-node-architecture.md
|
||||
├── apps/ # App manifests (YAML)
|
||||
├── CLAUDE.md # AI development instructions
|
||||
└── loop/plan.md # Project roadmap
|
||||
```
|
||||
|
||||
## Development Setup
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- **macOS** (development machine): Node.js 20+, npm
|
||||
- **Linux server** (`192.168.1.228`): Rust toolchain, Podman, Nginx, Debian 12
|
||||
- SSH key: `~/.ssh/archipelago-deploy`
|
||||
|
||||
### Local Frontend Development
|
||||
|
||||
```bash
|
||||
cd neode-ui
|
||||
npm install
|
||||
npm start # Vite dev server on :8100, mock backend on :5959
|
||||
```
|
||||
|
||||
The dev server at `http://localhost:8100` uses a mock backend. Login with `password123`.
|
||||
|
||||
### Deploying Changes
|
||||
|
||||
**Never build Rust on macOS.** The deploy script rsyncs source to the Linux server and builds there.
|
||||
|
||||
```bash
|
||||
# Deploy to live server (builds backend + frontend, restarts services)
|
||||
./scripts/deploy-to-target.sh --live
|
||||
|
||||
# Deploy to both servers
|
||||
./scripts/deploy-to-target.sh --both
|
||||
```
|
||||
|
||||
The deploy script:
|
||||
1. Rsyncs source to the server
|
||||
2. Builds Rust backend on the server (`cargo build --release`)
|
||||
3. Builds Vue frontend (`npm run build`)
|
||||
4. Copies artifacts to production paths
|
||||
5. Restarts the `archipelago` systemd service
|
||||
6. Runs a health check
|
||||
|
||||
### Running Tests
|
||||
|
||||
```bash
|
||||
# Frontend tests
|
||||
cd neode-ui && npm test
|
||||
|
||||
# Backend tests (on dev server via SSH)
|
||||
ssh -i ~/.ssh/archipelago-deploy archipelago@192.168.1.228 \
|
||||
"cd ~/archy/core && cargo test --all-features"
|
||||
|
||||
# Both
|
||||
./scripts/run-tests.sh
|
||||
```
|
||||
|
||||
## Adding a New RPC Endpoint
|
||||
|
||||
### 1. Create the Handler
|
||||
|
||||
Add a handler method in the appropriate file under `core/archipelago/src/api/rpc/`. If no existing file fits, create a new one.
|
||||
|
||||
```rust
|
||||
// core/archipelago/src/api/rpc/mymodule.rs
|
||||
use super::RpcHandler;
|
||||
use anyhow::Result;
|
||||
|
||||
impl RpcHandler {
|
||||
/// mymodule.action — description of what it does.
|
||||
pub(super) async fn handle_mymodule_action(
|
||||
&self,
|
||||
params: Option<serde_json::Value>,
|
||||
) -> Result<serde_json::Value> {
|
||||
let params = params.ok_or_else(|| anyhow::anyhow!("Missing params"))?;
|
||||
let name = params
|
||||
.get("name")
|
||||
.and_then(|v| v.as_str())
|
||||
.ok_or_else(|| anyhow::anyhow!("Missing required parameter: name"))?;
|
||||
|
||||
// Your logic here
|
||||
let result = do_something(name).await?;
|
||||
|
||||
Ok(serde_json::json!({ "ok": true, "result": result }))
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Key patterns:**
|
||||
- Handlers are `pub(super)` — visible only to the RPC router
|
||||
- Accept `Option<serde_json::Value>` for params (omit for parameterless endpoints)
|
||||
- Return `Result<serde_json::Value>`
|
||||
- Use `self.config.data_dir` for data persistence
|
||||
- Use `anyhow::bail!()` for error responses
|
||||
|
||||
### 2. Register the Route
|
||||
|
||||
Add the module declaration and route in `core/archipelago/src/api/rpc/mod.rs`:
|
||||
|
||||
```rust
|
||||
// At the top:
|
||||
mod mymodule;
|
||||
|
||||
// In the match statement (handle_rpc_call):
|
||||
"mymodule.action" => self.handle_mymodule_action(params).await,
|
||||
```
|
||||
|
||||
### 3. Add Module (if new)
|
||||
|
||||
If your logic warrants a separate module:
|
||||
|
||||
```rust
|
||||
// core/archipelago/src/main.rs
|
||||
mod mymodule; // Add to module declarations
|
||||
```
|
||||
|
||||
### 4. Frontend Client
|
||||
|
||||
Add a convenience method to `neode-ui/src/api/rpc-client.ts`:
|
||||
|
||||
```typescript
|
||||
async myAction(params: { name: string }): Promise<{ ok: boolean; result: string }> {
|
||||
return this.call({
|
||||
method: 'mymodule.action',
|
||||
params,
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
### 5. Deploy and Test
|
||||
|
||||
```bash
|
||||
./scripts/deploy-to-target.sh --live
|
||||
curl -X POST http://192.168.1.228/rpc/v1 \
|
||||
-H "Content-Type: application/json" \
|
||||
-b "archipelago_session=YOUR_SESSION" \
|
||||
-d '{"method":"mymodule.action","params":{"name":"test"}}'
|
||||
```
|
||||
|
||||
## Adding a New Vue Page
|
||||
|
||||
### 1. Create the Component
|
||||
|
||||
```vue
|
||||
<!-- neode-ui/src/views/MyPage.vue -->
|
||||
<template>
|
||||
<div>
|
||||
<h1 class="text-4xl font-bold text-white mb-2">My Page</h1>
|
||||
<div class="glass-card p-6">
|
||||
<!-- Content here -->
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { rpcClient } from '@/api/rpc-client'
|
||||
|
||||
// State and logic
|
||||
</script>
|
||||
```
|
||||
|
||||
### 2. Add the Route
|
||||
|
||||
In `neode-ui/src/router/index.ts`, add inside the dashboard children:
|
||||
|
||||
```typescript
|
||||
{
|
||||
path: 'my-page',
|
||||
name: 'my-page',
|
||||
component: () => import('@/views/MyPage.vue'),
|
||||
},
|
||||
```
|
||||
|
||||
### 3. Standards
|
||||
|
||||
- Always use `<script setup lang="ts">` — never Options API
|
||||
- Use `glass-card` for containers, `bg-white/5 rounded-lg` for sub-rows
|
||||
- Create global CSS classes in `src/style.css` instead of inline Tailwind
|
||||
- Use `rpcClient` from `@/api/rpc-client.ts` for all backend calls
|
||||
- Handle loading states and errors for all async operations
|
||||
|
||||
## Writing Tests
|
||||
|
||||
### Frontend (Vitest)
|
||||
|
||||
```typescript
|
||||
// neode-ui/src/api/__tests__/my-test.test.ts
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest'
|
||||
|
||||
describe('MyFeature', () => {
|
||||
beforeEach(() => {
|
||||
vi.restoreAllMocks()
|
||||
})
|
||||
|
||||
it('should do something', async () => {
|
||||
vi.stubGlobal('fetch', vi.fn().mockResolvedValue({
|
||||
ok: true,
|
||||
json: () => Promise.resolve({ result: 'ok' }),
|
||||
}))
|
||||
|
||||
// Test your logic
|
||||
expect(true).toBe(true)
|
||||
})
|
||||
})
|
||||
```
|
||||
|
||||
### Backend (Rust)
|
||||
|
||||
```rust
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use tempfile::tempdir;
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_my_function() {
|
||||
let dir = tempdir().unwrap();
|
||||
let result = my_function(dir.path()).await.unwrap();
|
||||
assert_eq!(result, expected);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Code Quality Checklist
|
||||
|
||||
- [ ] TypeScript strict mode: no `any`, use `unknown` or proper types
|
||||
- [ ] No `unwrap()` or `expect()` in production Rust code — use `?`
|
||||
- [ ] No `console.log` — wrap in `if (import.meta.env.DEV)`
|
||||
- [ ] No empty catch blocks — log or handle errors
|
||||
- [ ] Functions under 50 lines
|
||||
- [ ] `cargo clippy` and `cargo fmt` pass
|
||||
- [ ] `npx vue-tsc --noEmit` passes
|
||||
- [ ] Security: validate all inputs, no command injection
|
||||
- [ ] Container security: readonly_root, no_new_privileges, non-root user
|
||||
|
||||
## Contributing
|
||||
|
||||
1. Create a feature branch: `git checkout -b feature/my-feature`
|
||||
2. Make changes following the standards above
|
||||
3. Test locally: `cd neode-ui && npm test`
|
||||
4. Deploy to dev server: `./scripts/deploy-to-target.sh --live`
|
||||
5. Verify at `http://192.168.1.228`
|
||||
6. Commit with conventional format: `feat: add my feature`
|
||||
179
docs/hardware-wallet-integration.md
Normal file
179
docs/hardware-wallet-integration.md
Normal file
@@ -0,0 +1,179 @@
|
||||
# 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)
|
||||
331
docs/marketplace-protocol.md
Normal file
331
docs/marketplace-protocol.md
Normal file
@@ -0,0 +1,331 @@
|
||||
# Decentralized App Marketplace Protocol
|
||||
|
||||
## Overview
|
||||
|
||||
Archipelago's community marketplace enables developers to publish app manifests to Nostr relays, where nodes discover and install them without a central app store. Trust is established through DID-signed manifests and community reputation.
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
Developer Node Nostr Relays User Node
|
||||
│ │ │
|
||||
│── Publish signed manifest ──► │ │
|
||||
│ (NIP-78, kind 30078) │ │
|
||||
│ │ ◄── Query app manifests ── │
|
||||
│ │ (filter by d-tag) │
|
||||
│ │ │
|
||||
│ │── Return signed manifests ──► │
|
||||
│ │ │
|
||||
│ │ [Verify DID signature] │
|
||||
│ │ [Check trust score] │
|
||||
│ │ [Display in marketplace] │
|
||||
│ │ │
|
||||
│ │ [User clicks Install] │
|
||||
│ │ [Pull container image] │
|
||||
│ │ [Start container] │
|
||||
```
|
||||
|
||||
## Manifest Schema
|
||||
|
||||
App manifests published to Nostr relays follow the existing `apps/{app-id}/manifest.yml` schema (see `docs/app-manifest-spec.md`), serialized as JSON within a Nostr event.
|
||||
|
||||
### Marketplace Manifest Fields
|
||||
|
||||
```json
|
||||
{
|
||||
"app_id": "my-bitcoin-tool",
|
||||
"name": "My Bitcoin Tool",
|
||||
"version": "1.2.0",
|
||||
"description": {
|
||||
"short": "A useful Bitcoin utility",
|
||||
"long": "Detailed description of what this app does..."
|
||||
},
|
||||
"author": {
|
||||
"name": "Developer Name",
|
||||
"did": "did:key:z6Mkh...",
|
||||
"nostr_pubkey": "npub1..."
|
||||
},
|
||||
"container": {
|
||||
"image": "docker.io/developer/my-bitcoin-tool:1.2.0",
|
||||
"ports": [{ "container": 8080, "host": 8180, "protocol": "tcp" }],
|
||||
"volumes": [{ "name": "data", "path": "/data" }],
|
||||
"env": {
|
||||
"NETWORK": "mainnet"
|
||||
},
|
||||
"capabilities": [],
|
||||
"readonly_root": true,
|
||||
"no_new_privileges": true,
|
||||
"run_as_user": 1000
|
||||
},
|
||||
"category": "money",
|
||||
"icon_url": "https://example.com/icon.png",
|
||||
"repo_url": "https://github.com/developer/my-bitcoin-tool",
|
||||
"license": "MIT",
|
||||
"min_archipelago_version": "0.1.0",
|
||||
"dependencies": [],
|
||||
"signatures": {
|
||||
"manifest_hash": "sha256:abc123...",
|
||||
"did_signature": "base64-encoded-signature"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Required Fields
|
||||
|
||||
| Field | Type | Description |
|
||||
|-------|------|-------------|
|
||||
| `app_id` | string | Unique identifier, lowercase kebab-case |
|
||||
| `name` | string | Human-readable display name |
|
||||
| `version` | string | Semantic version (major.minor.patch) |
|
||||
| `description.short` | string | One-line description (max 120 chars) |
|
||||
| `author.did` | string | Developer's DID (did:key method) |
|
||||
| `container.image` | string | Full container image reference with tag (never `latest`) |
|
||||
| `category` | string | One of: money, commerce, data, networking, home, community, other |
|
||||
|
||||
### Security-Required Fields
|
||||
|
||||
| Field | Default | Description |
|
||||
|-------|---------|-------------|
|
||||
| `container.readonly_root` | true | Container root filesystem is read-only |
|
||||
| `container.no_new_privileges` | true | Prevent privilege escalation |
|
||||
| `container.run_as_user` | 1000 | UID to run as (must be > 1000) |
|
||||
| `container.capabilities` | [] | Required Linux capabilities (drop all, add only needed) |
|
||||
|
||||
## Nostr Event Format
|
||||
|
||||
### Event Kind
|
||||
|
||||
App manifests use **NIP-78 application-specific data** with event kind **30078** (replaceable parameterized). This matches the existing node discovery pattern in `nostr_discovery.rs`.
|
||||
|
||||
### Event Structure
|
||||
|
||||
```json
|
||||
{
|
||||
"kind": 30078,
|
||||
"tags": [
|
||||
["d", "archipelago-app:<app_id>"],
|
||||
["t", "archipelago-marketplace"],
|
||||
["t", "category:<category>"],
|
||||
["version", "<semver>"],
|
||||
["image", "<container_image>"],
|
||||
["L", "archipelago"],
|
||||
["l", "app-manifest", "archipelago"]
|
||||
],
|
||||
"content": "<JSON-serialized manifest>",
|
||||
"created_at": 1710000000,
|
||||
"pubkey": "<developer's secp256k1 pubkey hex>",
|
||||
"sig": "<schnorr signature>"
|
||||
}
|
||||
```
|
||||
|
||||
### Tag Semantics
|
||||
|
||||
| Tag | Purpose |
|
||||
|-----|---------|
|
||||
| `d` | Unique identifier for NIP-33 replaceable events. Format: `archipelago-app:<app_id>` |
|
||||
| `t` | Searchable topic tags for relay filtering |
|
||||
| `version` | Allows version-specific queries |
|
||||
| `image` | Container image for quick display without parsing content |
|
||||
| `L`/`l` | NIP-32 labeling namespace for structured queries |
|
||||
|
||||
### Publishing a Manifest
|
||||
|
||||
1. Developer creates/updates their app manifest
|
||||
2. Serialize manifest as JSON
|
||||
3. Compute SHA-256 hash of the serialized manifest
|
||||
4. Sign the hash with the developer's DID key
|
||||
5. Embed manifest + signature in Nostr event content
|
||||
6. Sign the Nostr event with the node's secp256k1 key
|
||||
7. Publish to all configured Nostr relays
|
||||
|
||||
### Discovering Manifests
|
||||
|
||||
1. Node queries configured relays with filter:
|
||||
```json
|
||||
{
|
||||
"kinds": [30078],
|
||||
"limit": 100,
|
||||
"#t": ["archipelago-marketplace"]
|
||||
}
|
||||
```
|
||||
2. For each returned event:
|
||||
a. Verify Nostr event signature (standard NIP-01)
|
||||
b. Parse manifest JSON from content
|
||||
c. Verify DID signature on manifest hash
|
||||
d. Check manifest against security requirements
|
||||
e. Calculate trust score
|
||||
3. Return manifests sorted by trust score
|
||||
|
||||
## Trust Model
|
||||
|
||||
### Trust Score Calculation
|
||||
|
||||
Each discovered app receives a trust score (0-100) based on:
|
||||
|
||||
| Factor | Weight | Description |
|
||||
|--------|--------|-------------|
|
||||
| **DID Verification** | 30 | Manifest is signed by a valid DID key |
|
||||
| **Relay Consensus** | 20 | Manifest found on multiple independent relays |
|
||||
| **Federation Trust** | 20 | Developer's DID is in the user's federation network |
|
||||
| **Version History** | 15 | App has multiple published versions (shows maintenance) |
|
||||
| **Security Compliance** | 15 | Manifest follows all security requirements |
|
||||
|
||||
### Trust Tiers
|
||||
|
||||
| Score | Tier | UI Treatment |
|
||||
|-------|------|--------------|
|
||||
| 80-100 | Verified | Green badge, install with one click |
|
||||
| 50-79 | Community | Yellow badge, install with confirmation |
|
||||
| 20-49 | Unverified | Orange badge, install with warning dialog |
|
||||
| 0-19 | Untrusted | Red badge, requires explicit security override |
|
||||
|
||||
### Federation-Based Trust
|
||||
|
||||
When a developer's DID appears in the user's federation network (trusted peer), the app automatically receives +20 trust points. This creates organic trust propagation: if you trust a node operator, you're more likely to trust their published apps.
|
||||
|
||||
### ADR: Nostr Relays over Centralized Registry
|
||||
|
||||
**Decision**: Use Nostr relays as the app discovery layer instead of a centralized registry.
|
||||
|
||||
**Context**: A centralized app store contradicts Archipelago's sovereignty principles. Nostr relays provide censorship-resistant, decentralized event distribution.
|
||||
|
||||
**Consequences**:
|
||||
- (+) No single point of failure for app discovery
|
||||
- (+) Developers publish without permission or review gates
|
||||
- (+) Multiple relay sources increase availability
|
||||
- (+) Leverages existing Nostr infrastructure and key management
|
||||
- (-) No global content moderation (each node decides trust locally)
|
||||
- (-) Spam is possible (mitigated by DID verification and trust scoring)
|
||||
- (-) Relay availability varies (mitigated by querying multiple relays)
|
||||
|
||||
## Signing Protocol
|
||||
|
||||
### Manifest Signing (DID Layer)
|
||||
|
||||
```
|
||||
1. Serialize manifest to canonical JSON (sorted keys, no whitespace)
|
||||
2. Compute: manifest_hash = SHA-256(canonical_json)
|
||||
3. Sign: did_signature = Ed25519_Sign(did_private_key, manifest_hash)
|
||||
4. Attach to manifest:
|
||||
{
|
||||
"signatures": {
|
||||
"manifest_hash": "sha256:<hex>",
|
||||
"did_signature": "<base64>"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Event Signing (Nostr Layer)
|
||||
|
||||
Standard NIP-01 Schnorr signature over the event ID (hash of serialized event fields). This is handled by the Nostr client library.
|
||||
|
||||
### Verification Flow
|
||||
|
||||
```
|
||||
Receiving Node:
|
||||
1. Verify Nostr event signature (NIP-01) → Proves event authenticity
|
||||
2. Extract manifest JSON from event content
|
||||
3. Compute SHA-256 of manifest content
|
||||
4. Compare with manifest.signatures.manifest_hash → Proves content integrity
|
||||
5. Resolve DID document for manifest.author.did
|
||||
6. Verify did_signature with DID public key → Proves developer identity
|
||||
7. Check container.image tag is pinned (not :latest)
|
||||
8. Validate security fields meet minimums
|
||||
```
|
||||
|
||||
## RPC Endpoints
|
||||
|
||||
### Marketplace Discovery
|
||||
|
||||
| Method | Description | Auth |
|
||||
|--------|-------------|------|
|
||||
| `marketplace.discover` | Query relays for app manifests, verify, score, return sorted | Local |
|
||||
| `marketplace.publish` | Publish an app manifest to configured relays | Local |
|
||||
| `marketplace.get-manifest` | Get full manifest for a specific app by ID | Local |
|
||||
| `marketplace.verify` | Verify a manifest's signatures and security compliance | Local |
|
||||
|
||||
### Manifest Management
|
||||
|
||||
| Method | Description | Auth |
|
||||
|--------|-------------|------|
|
||||
| `marketplace.list-published` | List manifests published by this node | Local |
|
||||
| `marketplace.unpublish` | Remove a published manifest from relays | Local |
|
||||
|
||||
## Security Requirements
|
||||
|
||||
### Container Security Enforcement
|
||||
|
||||
Before installing a community app, the node validates:
|
||||
|
||||
1. **No `latest` tag**: Image must use a specific version tag
|
||||
2. **Read-only root**: `readonly_root` must be true (or explicitly overridden by user)
|
||||
3. **No root**: `run_as_user` must be > 1000
|
||||
4. **No new privileges**: `no_new_privileges` must be true
|
||||
5. **Minimal capabilities**: Only allowed capabilities are accepted (CHOWN, NET_BIND_SERVICE, etc.)
|
||||
6. **No host networking**: Apps cannot use `--network host`
|
||||
7. **Volume restrictions**: Apps cannot mount system paths (/, /etc, /var, /usr)
|
||||
|
||||
### Image Verification
|
||||
|
||||
- Container images are pulled from registries, never transferred between nodes
|
||||
- Future: Cosign signature verification for container images (leverages `core/security/`)
|
||||
- Image digest pinning recommended for production apps
|
||||
|
||||
## UI: Community Marketplace Tab
|
||||
|
||||
### Route
|
||||
|
||||
Extends existing `/dashboard/marketplace` page.
|
||||
|
||||
### Layout
|
||||
|
||||
Two tabs at the top of Marketplace.vue:
|
||||
|
||||
1. **Curated** (existing): Built-in apps maintained by Archipelago team
|
||||
2. **Community** (new): Apps discovered from Nostr relays
|
||||
|
||||
### Community Tab Components
|
||||
|
||||
1. **App Grid**: Same card layout as curated tab, with trust score badge
|
||||
2. **Search & Filter**: Category filter + text search across community apps
|
||||
3. **Trust Indicators**: Color-coded badges (Verified/Community/Unverified/Untrusted)
|
||||
4. **App Detail**: Shows full manifest, developer DID, relay sources, version history
|
||||
5. **Install Flow**: Trust-level-dependent confirmation (one-click for Verified, warning for Untrusted)
|
||||
|
||||
### Publishing UI
|
||||
|
||||
Accessible from Settings or a "Developer" section:
|
||||
1. Select a local app container to publish
|
||||
2. Fill in manifest metadata (description, category, icon)
|
||||
3. Review security compliance
|
||||
4. Sign and publish to relays
|
||||
5. View published manifests and their discovery status
|
||||
|
||||
## Data Storage
|
||||
|
||||
```
|
||||
/var/lib/archipelago/marketplace/
|
||||
├── cache/
|
||||
│ ├── manifests.json # Cached discovered manifests
|
||||
│ └── trust-scores.json # Cached trust scores
|
||||
├── published/
|
||||
│ └── <app-id>.json # Manifests published by this node
|
||||
└── config.json # Marketplace preferences (auto-refresh interval, etc.)
|
||||
```
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
### Relay Query Strategy
|
||||
|
||||
1. Query all enabled relays in parallel (from `nostr_relays.rs` config)
|
||||
2. Deduplicate manifests by `app_id` + `version`
|
||||
3. If same manifest found on multiple relays, boost trust score
|
||||
4. Cache results with 15-minute TTL
|
||||
5. Background refresh every 30 minutes
|
||||
|
||||
### Version Comparison
|
||||
|
||||
- Use semantic versioning for all version comparisons
|
||||
- When multiple versions exist for the same `app_id`, show the latest
|
||||
- Keep version history available in app detail view
|
||||
- Flag apps with versions older than 6 months as potentially unmaintained
|
||||
188
docs/multi-node-architecture.md
Normal file
188
docs/multi-node-architecture.md
Normal file
@@ -0,0 +1,188 @@
|
||||
# Multi-Node Architecture
|
||||
|
||||
## Overview
|
||||
|
||||
Archipelago supports federation — multiple nodes can form a trusted cluster to share status, deploy apps remotely, and coordinate services. This document describes the architecture for multi-node orchestration.
|
||||
|
||||
## Discovery & Trust Model
|
||||
|
||||
### Node Discovery
|
||||
|
||||
Nodes discover each other through two complementary channels:
|
||||
|
||||
1. **Nostr Relay Discovery**: Each node publishes its identity (DID, onion address, pubkey) to configured Nostr relays as a NIP-78 application-specific event. Other nodes query relays to find peers.
|
||||
|
||||
2. **Direct Invite**: A node generates an invite code containing its DID, onion address, and a one-time authentication token. The recipient node uses this code to establish a direct connection.
|
||||
|
||||
3. **Tor Hidden Services**: All inter-node communication uses Tor hidden services (.onion addresses) for privacy and NAT traversal.
|
||||
|
||||
### Trust Establishment
|
||||
|
||||
Federation uses a mutual DID verification model:
|
||||
|
||||
```
|
||||
Node A Node B
|
||||
│ │
|
||||
│── federation.invite (generates invite code) ──► │
|
||||
│ │
|
||||
│ ◄── federation.join (presents invite + DID) ── │
|
||||
│ │
|
||||
│── Verify Node B's DID Document over Tor ──────► │
|
||||
│ ◄── Verify Node A's DID Document over Tor ── │
|
||||
│ │
|
||||
│── Exchange signed challenge/response ─────────► │
|
||||
│ ◄── Exchange signed challenge/response ────── │
|
||||
│ │
|
||||
│ [Mutual trust established] │
|
||||
│ [Both nodes add each other to federation] │
|
||||
```
|
||||
|
||||
**Trust Levels**:
|
||||
- `trusted`: Full federation — can deploy apps, sync state, see all container statuses
|
||||
- `observer`: Read-only — can see status but cannot deploy or modify
|
||||
- `untrusted`: Discovered but not yet verified — pending invite acceptance
|
||||
|
||||
### ADR: Decentralized Trust over Centralized Authority
|
||||
|
||||
**Decision**: Use DID-based mutual verification instead of a central authority or PKI.
|
||||
|
||||
**Context**: Archipelago nodes are sovereign — no central server should control trust. Each node maintains its own trust list.
|
||||
|
||||
**Consequences**:
|
||||
- (+) No single point of failure for trust
|
||||
- (+) Nodes can federate without internet (direct Tor connection)
|
||||
- (+) Consistent with the DID identity model already in use
|
||||
- (-) No global revocation mechanism (each node manages its own trust)
|
||||
- (-) Trust is bilateral — A trusting B doesn't imply C trusts B
|
||||
|
||||
## Shared State Protocol
|
||||
|
||||
### State Sync
|
||||
|
||||
Federated nodes periodically sync their state. Each node exposes a state summary via its RPC endpoint, accessible only to trusted federation peers.
|
||||
|
||||
**Synced data**:
|
||||
- Container/app statuses (installed, running, stopped, version)
|
||||
- Node health (CPU, memory, disk, uptime)
|
||||
- Available storage capacity
|
||||
- Tor hidden service status
|
||||
- Lightning Network status (channels, capacity)
|
||||
|
||||
**Not synced** (privacy):
|
||||
- Credentials and secrets
|
||||
- Private keys
|
||||
- Session data
|
||||
- User passwords
|
||||
|
||||
### Sync Protocol
|
||||
|
||||
```
|
||||
Every 5 minutes (configurable):
|
||||
For each federated node:
|
||||
1. POST to peer's /rpc/ endpoint: federation.get-state
|
||||
2. Authenticate with signed challenge (DID key)
|
||||
3. Receive state snapshot
|
||||
4. Store in local federation cache
|
||||
5. Broadcast changes via WebSocket to local UI
|
||||
```
|
||||
|
||||
### State Storage
|
||||
|
||||
```
|
||||
/var/lib/archipelago/federation/
|
||||
├── nodes.json # List of federated nodes with trust levels
|
||||
├── state-cache/
|
||||
│ ├── <node-did>.json # Latest state snapshot from each peer
|
||||
│ └── ...
|
||||
└── invites/
|
||||
├── pending.json # Outgoing invites awaiting acceptance
|
||||
└── received.json # Incoming invites awaiting approval
|
||||
```
|
||||
|
||||
## RPC Endpoints
|
||||
|
||||
### Federation Management
|
||||
|
||||
| Method | Description | Auth |
|
||||
|--------|-------------|------|
|
||||
| `federation.invite` | Generate invite code for a new peer | Local |
|
||||
| `federation.join` | Accept an invite and establish federation | Local |
|
||||
| `federation.list-nodes` | List all federated nodes with status | Local |
|
||||
| `federation.remove-node` | Remove a node from federation | Local |
|
||||
| `federation.set-trust` | Change trust level for a federated node | Local |
|
||||
|
||||
### Federation Data Exchange
|
||||
|
||||
| Method | Description | Auth |
|
||||
|--------|-------------|------|
|
||||
| `federation.get-state` | Return node's state snapshot | Federation peer |
|
||||
| `federation.deploy-app` | Request remote app installation | Trusted peer |
|
||||
| `federation.sync-state` | Trigger manual state sync | Local |
|
||||
|
||||
### Authentication for Inter-Node RPC
|
||||
|
||||
Federation RPC calls between nodes use DID-based authentication:
|
||||
|
||||
1. Caller includes `X-Federation-DID` header with their DID
|
||||
2. Caller includes `X-Federation-Sig` header with a signed timestamp
|
||||
3. Receiver verifies the DID is in their trusted federation list
|
||||
4. Receiver verifies the signature using the DID's public key
|
||||
5. Timestamp must be within 5 minutes to prevent replay attacks
|
||||
|
||||
## Federated App Deployment
|
||||
|
||||
### Flow
|
||||
|
||||
```
|
||||
Local Node Remote Node
|
||||
│ │
|
||||
│── federation.deploy-app ──────► │
|
||||
│ {app_id, version, config} │
|
||||
│ │
|
||||
│ [Remote verifies trust level] │
|
||||
│ [Remote checks if app exists] │
|
||||
│ [Remote pulls container image] │
|
||||
│ [Remote starts container] │
|
||||
│ │
|
||||
│ ◄── Status update via sync ── │
|
||||
│ {app_id: "running"} │
|
||||
```
|
||||
|
||||
### Constraints
|
||||
|
||||
- Only `trusted` peers can deploy apps to each other
|
||||
- Remote node can reject deployment (insufficient resources, policy)
|
||||
- Container images are pulled from registry, not transferred between nodes
|
||||
- App configuration is sent with the deploy command
|
||||
- Remote node applies its own security policies (AppArmor, capabilities)
|
||||
|
||||
## UI: Federation Dashboard
|
||||
|
||||
**Route**: `/dashboard/server/federation`
|
||||
|
||||
**Components**:
|
||||
1. **Node List**: Table of federated nodes showing:
|
||||
- Node name (DID-derived or custom alias)
|
||||
- Status: online/offline (based on last successful sync)
|
||||
- Trust level badge (trusted/observer)
|
||||
- App count, resource usage summary
|
||||
- Last seen timestamp
|
||||
|
||||
2. **Add Node**: Form with invite code input or QR code scanner
|
||||
|
||||
3. **Node Detail Modal**: Clicking a node shows:
|
||||
- Full DID and onion address
|
||||
- Container/app list with statuses
|
||||
- Resource usage (CPU, memory, disk)
|
||||
- Deploy app button (if trusted)
|
||||
- Change trust level / remove node
|
||||
|
||||
## Security Considerations
|
||||
|
||||
1. **All federation traffic over Tor**: Prevents IP address leakage between nodes
|
||||
2. **DID-based auth**: No shared secrets; each node proves identity with its key
|
||||
3. **Replay protection**: Signed timestamps prevent replay attacks
|
||||
4. **Trust is bilateral**: Both nodes must agree to federate
|
||||
5. **App deployment is opt-in**: Remote node can refuse deployment requests
|
||||
6. **State snapshots are read-only**: A compromised peer cannot modify another node's state
|
||||
7. **Invite codes are single-use**: Once accepted, the invite token is invalidated
|
||||
111
docs/release-process.md
Normal file
111
docs/release-process.md
Normal file
@@ -0,0 +1,111 @@
|
||||
# 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
|
||||
28
docs/startos-dependency-audit.md
Normal file
28
docs/startos-dependency-audit.md
Normal file
@@ -0,0 +1,28 @@
|
||||
# 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.
|
||||
421
docs/user-guide.md
Normal file
421
docs/user-guide.md
Normal file
@@ -0,0 +1,421 @@
|
||||
# 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 (1–3 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 1–5 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 1–7 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
|
||||
Reference in New Issue
Block a user