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

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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