Files
archy/loop/plan.md
Dorian b29f798e05 fix: correct PhotoPrism icon filename typo in backend metadata
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-15 04:01:12 +00:00

51 KiB

Overnight Plan — Archy Refactoring & App Integration Hardening

Make the Archy codebase rock-solid: fix all broken containers/iframes, perfect app installation/management/icons, get IndeedHub + Nostr signer flawless, and begin critical refactoring. No new features, no design changes. Bitcoin only. See docs/refactoring-plan.md for the full 3-year plan. See CLAUDE.md for all project rules and conventions. Deploy after every change: ./scripts/deploy-to-target.sh --live — test at http://192.168.1.228


Phase 1: Fix App Icon Consistency

  • Fix PhotoPrism icon typo in backend metadata: In core/archipelago/src/container/docker_packages.rs, the get_app_metadata() function references photoprims.svg (missing 'h') for the PhotoPrism icon. Search for photoprims and replace with photoprism. Verify the icon file exists at neode-ui/public/assets/img/app-icons/photoprism.svg. Run cargo clippy --all-targets --all-features in core/ on the dev server after the fix.

  • Fix IndeedHub duplicate icon — consolidate to indeedhub.png: Two icon files exist: neode-ui/public/assets/img/app-icons/indeedhub.ico and indeehub.ico (typo). Delete indeehub.ico. Convert indeedhub.ico to indeedhub.png (better format consistency). Update all references: (1) neode-ui/src/utils/dummyApps.ts line ~518 — change indeehub.ico to indeedhub.png, (2) neode-ui/src/views/Marketplace.vue line ~913 — change indeehub.ico to indeedhub.png, (3) core/archipelago/src/container/docker_packages.rs lines ~451-454 — change indeehub.ico to indeedhub.png. Search the entire codebase for indeehub (missing 'd') and fix all occurrences to indeedhub. Run cd neode-ui && npm run type-check to verify.

  • Audit all app icons match their references: Cross-check every icon path referenced in docker_packages.rs get_app_metadata() against actual files in neode-ui/public/assets/img/app-icons/. Verify each app in the Marketplace.vue getCuratedAppList() function has an icon that exists. If any icon is missing, check if a similar-named file exists (e.g., wrong extension). Fix all mismatches. Remove orphaned icons that no app references (e.g., atob.png, community-store.png, k484.png, lorabell.png, morphos.png — verify they're truly unused first). Standardize: prefer .png or .svg over .ico and .webp where possible without changing existing working icons.


Phase 2: Fix Container Crash Loops & Health

  • Diagnose and fix container networking DNS failures: SSH to 192.168.1.228 (sshpass -p 'EwPDR8q45l0Upx@' ssh -o StrictHostKeyChecking=no archipelago@192.168.1.228). Run sudo podman ps -a --format '{{.Names}} {{.Status}}' | grep -i restart to identify containers in crash loops. The known issue is DNS resolution failures — containers can't resolve each other by name (e.g., mempool-web can't find mempool-api). Check if the archy-net Podman network exists: sudo podman network ls. If missing, create it: sudo podman network create archy-net. Reconnect all containers that need inter-container DNS to this network. Verify with sudo podman exec archy-mempool-web ping mempool-api. Restart affected containers and monitor for 2 minutes to confirm no more crash loops.

  • Fix .198 server swap and memory: SSH to 192.168.1.198. Check current swap: free -h. If no swap configured, create a 4GB swap file: sudo fallocate -l 4G /swapfile && sudo chmod 600 /swapfile && sudo mkswap /swapfile && sudo swapon /swapfile. Add to /etc/fstab: /swapfile none swap sw 0 0. Verify with free -h. This prevents OOM kills that crash containers.

  • Stop and remove ollama container if not needed: SSH to 192.168.1.228. Check ollama status: sudo podman ps -a | grep ollama. If it's in "Created" state and never started, remove it: sudo podman rm ollama. This frees a container slot and removes clutter from the app list. If the user has ollama in their installed apps, leave it but start it: sudo podman start ollama.

  • Verify all core Bitcoin containers are healthy: SSH to 192.168.1.228. Check these containers are running and healthy: bitcoin-knots, lnd, mempool-api, archy-mempool-web, mempool-electrs, btcpay-server, archy-nbxplorer. Run sudo podman ps --format '{{.Names}}\t{{.Status}}' | grep -E "(bitcoin|lnd|mempool|btcpay|nbxplorer|electrs)". For any that are not "Up", check logs: sudo podman logs --tail 50 {container-name}. Fix the root cause (usually missing network, wrong env var, or dependency not ready). After fixes, run curl -s http://localhost:5678/health to verify the Archy backend sees them all.


Phase 3: Fix Iframe Embedding for All Apps

  • Audit X-Frame-Options headers for all proxied apps: SSH to 192.168.1.228. For each app with a known port, check the actual response headers: for port in 81 3000 3001 4080 7777 8080 8081 8082 8083 8085 8096 8123 8175 8176 8190 8240 8334 8888 9000 9001 9980 11434 2283 2342 23000 50002; do echo "Port $port:"; curl -sI http://localhost:$port/ 2>/dev/null | grep -i "x-frame\|content-security-policy" || echo " (no frame restrictions)"; done. Record the results. Compare against the blocking list in neode-ui/src/stores/appLauncher.ts (lines 23-31, the XFRAME_BLOCKED_PORTS array). Update the blocking list to match reality — if an app no longer sends X-Frame-Options DENY, remove it from the blocked list. If an app sends it but isn't in the list, add it.

  • Ensure nginx strips X-Frame-Options for iframe-compatible apps: In image-recipe/configs/nginx-archipelago.conf, verify every /app/{id}/ location block includes proxy_hide_header X-Frame-Options; for apps that should work in iframes. Apps that genuinely can't work in iframes (BTCPay with DENY, Home Assistant with SAMEORIGIN that rejects proxy origin) should open in new tabs. For apps like Grafana (port 3000) — check if setting the env var GF_SECURITY_ALLOW_EMBEDDING=true on the Grafana container fixes it, then remove it from the blocked list. For Nextcloud (port 8085) — check if the nginx sub_filter approach or Nextcloud's overwriteprotocol setting allows embedding. For Uptime Kuma (port 3001) — it may work with the header stripped. Test each by loading http://192.168.1.228/app/{id}/ in a browser iframe or curl -sI http://192.168.1.228/app/{id}/ | grep -i frame.

  • Fix nginx sub_filter for apps with root-relative asset paths: Apps served under /app/{id}/ may have root-relative paths like /static/main.js that break because they resolve to the Archy root, not the app root. In image-recipe/configs/nginx-archipelago.conf, check IndeedHub's location block (lines 334-367) — it already uses sub_filter to rewrite paths. Verify the same pattern exists for other Next.js/React apps that need it (Penpot on 9001, Immich on 2283, Fedimint UI on 8175). For each, test: load the app at http://192.168.1.228/app/{id}/, open browser dev tools Network tab, check for 404s on static assets. If assets 404, add appropriate sub_filter rules to their nginx location block. After changes, sync the config: scp image-recipe/configs/nginx-archipelago.conf archipelago@192.168.1.228:/tmp/ && ssh archipelago@192.168.1.228 'sudo cp /tmp/nginx-archipelago.conf /etc/nginx/sites-available/archipelago && sudo nginx -t && sudo systemctl reload nginx'.

  • Deploy and verify iframe loading for all apps: Deploy with ./scripts/deploy-to-target.sh --live. After deploy, test each app iframe by hitting the Archy UI at http://192.168.1.228, navigating to Apps, and clicking each installed app. Verify: (1) iframe apps load content (not blank white), (2) blocked apps open in new tab cleanly, (3) no mixed-content warnings in console. Log any remaining issues for the next phase.


Phase 4: IndeedHub + Nostr Signer Integration

  • Verify IndeedHub container is running and accessible: SSH to 192.168.1.228. Check: sudo podman ps | grep indeedhub. If not running, check if the image exists: sudo podman images | grep indeedhub. If no image, pull from manifest: the image is git.tx1138.com/lfg2025/indeedhub:latest (from apps/indeedhub/manifest.yml). Pull and start: sudo podman pull git.tx1138.com/lfg2025/indeedhub:latest && sudo podman run -d --name indeedhub --restart unless-stopped -p 7777:3000 --cap-drop ALL --cap-add CHOWN --cap-add SETUID --cap-add SETGID --security-opt no-new-privileges --user 1001 git.tx1138.com/lfg2025/indeedhub:latest. Verify it responds: curl -sI http://localhost:7777/. Check nginx proxy works: curl -sI http://localhost/app/indeedhub/.

  • Fix IndeedHub port mapping inconsistency: In core/archipelago/src/container/docker_packages.rs, line ~139-141 hardcodes http://localhost:8190 for IndeedHub. But nginx and the frontend use port 7777. Update docker_packages.rs to use port 7777: change Some("http://localhost:8190".to_string()) to Some("http://localhost:7777".to_string()). Also verify apps/indeedhub/manifest.yml — if it says port 8190, update to 7777 to match the actual deployment. In neode-ui/src/stores/appLauncher.ts line 67, confirm '7777': '/app/indeedhub/' is correct. Deploy with ./scripts/deploy-to-target.sh --live and test.

  • Verify nostr-provider.js injection works for IndeedHub iframe: The NIP-07 Nostr signer works by nginx injecting neode-ui/public/nostr-provider.js into the iframe via sub_filter. Check the IndeedHub nginx location block in image-recipe/configs/nginx-archipelago.conf (lines 334-367) includes a sub_filter that injects <script src="/nostr-provider.js"></script> into the HTML response. If missing, add: sub_filter '</head>' '<script src="/nostr-provider.js"></script></head>'; with sub_filter_once on; and sub_filter_types text/html;. Sync nginx config to server and reload. Verify by loading IndeedHub in the Archy iframe and checking browser dev tools console for window.nostr availability — run JSON.stringify(Object.keys(window.nostr)) in the iframe console, should show ["getPublicKey","signEvent","getRelays","nip04","nip44"].

  • Test full NIP-07 signing flow with IndeedHub: Open Archy at http://192.168.1.228, go to Apps, click IndeedHub. Expected flow: (1) NostrIdentityPicker modal appears on first launch asking which identity to use, (2) select an identity with a Nostr key, (3) IndeedHub loads in iframe, (4) when IndeedHub requests window.nostr.getPublicKey(), the Archy parent responds with the selected identity's Nostr pubkey, (5) when IndeedHub requests window.nostr.signEvent(event), NostrSignConsent modal appears, (6) user approves, event is signed via identity.nostr-sign RPC, (7) signed event returned to IndeedHub. Test each step. If NostrIdentityPicker doesn't show, check AppSession.vue line ~302-304 isIdentityAwareApp() includes 'indeedhub'. If signing fails, check RPC logs: ssh archipelago@192.168.1.228 'sudo journalctl -u archipelago --since "5 min ago" | grep -i nostr'.

  • Ensure IndeedHub content loads fully — all pages, media, navigation: After the Nostr flow works, navigate through IndeedHub's content inside the iframe. Check: (1) all pages/routes load (no blank screens), (2) media content (videos, images) loads, (3) navigation within IndeedHub works without breaking the iframe, (4) no console errors related to CORS, mixed content, or CSP. If videos don't load, check if the video hosting domain is blocked by CSP headers — may need to add Content-Security-Policy adjustments in the nginx location block. If internal navigation causes the iframe to navigate to a bare URL (not under /app/indeedhub/), add sub_filter rules to rewrite the app's internal links.

  • Test NIP-04 and NIP-44 encryption/decryption: In IndeedHub (or manually via browser console in the iframe), test the encryption methods: (1) window.nostr.nip04.encrypt(somePubkey, "test message") — should return ciphertext, (2) window.nostr.nip04.decrypt(somePubkey, ciphertext) — should return "test message", (3) same for nip44.encrypt and nip44.decrypt. If any fail, check RPC handlers in core/archipelago/src/api/rpc/identity.rs — the handle_identity_nostr_encrypt_nip04/nip44 and decrypt handlers (lines 428-496). Check that the identity manager has the required keys.


Phase 5: App Installation & Management Polish

  • Verify install flow for every Bitcoin-related marketplace app: In the Archy UI at http://192.168.1.228, go to Marketplace. For each Bitcoin-related app (Bitcoin Knots, LND, Mempool, BTCPay, Electrs, Fedimint), click through to the detail page. Verify: (1) icon loads correctly (not fallback logo), (2) description is accurate, (3) "Install" button appears if not installed, (4) dependency warnings show correctly (Mempool requires Bitcoin Knots + Electrs, BTCPay requires Bitcoin Knots), (5) if already installed, status shows correctly. Fix any issues found in neode-ui/src/views/MarketplaceAppDetails.vue. Note: Archy is Bitcoin only — remove any Monero or Liquid entries from Marketplace.vue getCuratedAppList() if present.

  • Remove non-Bitcoin altcoin entries from marketplace: Search neode-ui/src/views/Marketplace.vue for "monero", "liquid", "litecoin", or any non-Bitcoin cryptocurrency entries in the getCuratedAppList() function. Remove them entirely. Archy is a Bitcoin-only platform. Run cd neode-ui && npm run type-check after changes.

  • Fix dependency checks — frontend must match backend: In neode-ui/src/views/MarketplaceAppDetails.vue, find the hardcoded dependency definitions (around lines 447-456). Cross-reference with core/archipelago/src/api/rpc/package.rs lines 64-96 where backend dependency checks are defined. Ensure they match exactly. If backend checks for has_bitcoin before installing electrs, the frontend dependency list for electrs must show bitcoin-knots as a prerequisite. Update the frontend to match the backend. Ideally, add an RPC method package.get-dependencies that returns the dependency list from the backend, and have the frontend call it instead of hardcoding — but for now, just make the hardcoded lists match.

  • Verify start/stop/restart works for all installed apps: In the Archy UI, go to Apps. For each installed app, test: (1) click Stop — container stops, UI updates to "Stopped" state, (2) click Start — container starts, UI updates to "Running" state with health indicator, (3) click the app — it launches (iframe or new tab as appropriate). Check that the container store (neode-ui/src/stores/container.ts) correctly polls for status changes after start/stop actions. If status doesn't update, check the WebSocket state broadcasting in core/archipelago/src/state.rs.

  • Fix route-to-package-key mapping divergence: In neode-ui/src/views/AppDetails.vue lines 501-529, the route ID to backend container name mapping is hardcoded. Verify every mapping is correct by checking actual container names on the server: ssh archipelago@192.168.1.228 'sudo podman ps --format "{{.Names}}"'. Fix any mismatches. Known issues: mempool maps to mempool-web but backend may use archy-mempool-web. Check electrs maps to mempool-electrs or archy-electrs. Run cd neode-ui && npm run type-check after changes.


Phase 6: Backend Critical Fixes

  • Fix session TTL clock bug — use SystemTime instead of Instant: Read core/archipelago/src/session.rs. Find where Instant::now() is used for session TTL/expiry (around line 97). Instant is monotonic but can drift on sleep/hibernate — common on NUC/Pi hardware. Replace with SystemTime::now() for absolute time comparison. The FULL_SESSION_TTL (24 hours) and PENDING_TOTP_TTL (5 minutes) checks should use SystemTime::elapsed() or store SystemTime timestamps and compare with SystemTime::now(). Run cargo test --all-features in core/ on the dev server.

  • Enforce RBAC in RPC handler: Read core/archipelago/src/auth.rs — find the UserRole enum and can_access() method. Then read core/archipelago/src/api/rpc/mod.rs — find where authenticated requests are dispatched to handlers. Add a role check before dispatching: after validating the session, get the user's role, call role.can_access(method_name), and return an authorization error if denied. For now, all users created via onboarding should default to Admin role (single-user system), but this lays the groundwork for multi-user. Run cargo clippy --all-targets --all-features && cargo test --all-features on the dev server.

  • Remove dead code and #[allow(dead_code)]: Search core/ for all #[allow(dead_code)] and #[allow(unused)] annotations. For each: (1) if the code is genuinely unused and not part of a planned feature, delete it, (2) if it should be used (like RBAC — now wired up in previous task), remove the allow annotation. Key file: core/archipelago/src/auth.rs lines ~70, 83, 88. Run cargo clippy --all-targets --all-features to verify no new warnings.

  • Deploy and verify backend fixes: Run ./scripts/deploy-to-target.sh --live. After deploy: (1) verify login still works at http://192.168.1.228 (password: password123), (2) verify session persists after navigating between pages, (3) check logs for any new errors: ssh archipelago@192.168.1.228 'sudo journalctl -u archipelago --since "2 min ago" | grep -i error'.


Phase 7: Frontend Cleanup

  • Remove dead dockerode dependency: Run cd /Users/dorian/Projects/archy/neode-ui && npm uninstall dockerode and npm uninstall @types/dockerode if it exists. Search the codebase for any remaining imports: grep -r "dockerode" neode-ui/src/. Remove any dead imports found. Run npm run type-check to verify nothing breaks.

  • Fix the 10 failing frontend tests: Run cd /Users/dorian/Projects/archy/neode-ui && npm run test -- --reporter=verbose 2>&1 | head -100 to see which tests fail. Known failures: (1) src/stores/__tests__/appLauncher.test.ts — URL rewriting tests expecting different proxy behavior, (2) src/views/__tests__/settings.test.ts — heading selector h1 not finding the heading element. For each failing test, read the test file and the component/store it tests. Update test expectations to match current implementation. Do NOT change the production code to match tests — fix the tests. Run npm run test until all pass.

  • Add 404 catch-all route: In neode-ui/src/router/index.ts, add a catch-all route at the end of the routes array: { path: '/:pathMatch(.*)*', name: 'not-found', component: () => import('@/views/NotFound.vue') }. Create neode-ui/src/views/NotFound.vue — a simple view using the existing .glass-card class with "Page not found" message and a router-link back to /dashboard. Use <script setup lang="ts">, no props needed. Style with existing global classes only (.glass-card, .glass-button). Run npm run type-check.


Phase 8: Web5 Identity & Credentials Hardening

Context: TBD/Block shut down Nov 2024 — Web5 repos donated to DIF but effectively unmaintained. Archy's custom implementations (did:key, did:dht, VCs, multi-identity) are W3C-compliant and well-tested. SpruceID ssi crate (v0.15.0, Feb 2026) is the only mature Rust DID/VC library. DWN spec is stalled — no Rust implementation exists anywhere. Strategy: keep our custom stack (it's good), fix onboarding gaps, encrypt credential storage, validate against W3C specs, evaluate ssi for external VC verification only, deprioritize DWN in favor of Nostr + federation. Do NOT adopt dead TBD SDKs.

  • Fix DID onboarding — replace mock signature with real proof-of-control: In neode-ui/src/views/OnboardingVerify.vue, the verification step uses generateMockSignature() instead of real cryptographic proof. Replace with a call to node.signChallenge RPC (or identity.sign if it exists). The flow should be: (1) frontend generates a random challenge string, (2) sends to identity.sign RPC with the node's default identity, (3) backend signs with Ed25519 key, (4) frontend displays the signature as proof the node controls the DID. Check core/archipelago/src/api/rpc/identity.rs for existing sign handlers — handle_identity_sign should work. If node.signChallenge RPC doesn't exist, the identity.sign endpoint (which takes { id?, data } and returns { signature }) should be sufficient. Update the Vue component to call it. Run cd neode-ui && npm run type-check.

  • Fix DID onboarding — real encrypted backup: In neode-ui/src/views/OnboardingBackup.vue, the backup step uses mock JSON data instead of real encrypted key material. Replace with a call to identity.export or backup.create-identity RPC (check what exists in core/archipelago/src/api/rpc/identity.rs and core/archipelago/src/api/rpc/backup_rpc.rs). The backup should contain the Ed25519 private key encrypted with the user's password via Argon2 + ChaCha20-Poly1305 (the encryption stack already exists in core/security/). If no export RPC exists, create one that: (1) derives a key from the user's password with Argon2, (2) encrypts the identity's private key with ChaCha20-Poly1305, (3) returns base64-encoded ciphertext. The frontend should offer this as a downloadable .json file. Run cargo test --all-features on the dev server.

  • Fix DID onboarding UX copy: In neode-ui/src/views/OnboardingDid.vue, the copy says "Generate DID" but actually fetches an existing DID from the server (generated at first boot). Update the button text to "View Your DID" or "Retrieve Your DID" and the description to explain that the DID was created when the node was set up. Small change but prevents user confusion. Do NOT change any styling or layout.

  • Validate DID Document structure against W3C spec: In core/archipelago/src/identity.rs, the generate_did_document() function builds a DID Document. Verify it includes all required fields per W3C DID Core v1.0: id, verificationMethod (with correct type: "Ed25519VerificationKey2020"), authentication, assertionMethod, keyAgreement (X25519). Check that @context includes ["https://www.w3.org/ns/did/v1", "https://w3id.org/security/suites/ed25519-2020/v1"]. Add a unit test that validates the document structure against these requirements. Run cargo test --all-features.

  • Validate Verifiable Credentials against W3C VC 2.0 spec: In core/archipelago/src/credentials.rs, verify the VerifiableCredential struct produces output matching W3C VC Data Model 2.0. Check: (1) @context includes https://www.w3.org/ns/credentials/v2, (2) type array starts with "VerifiableCredential", (3) proof uses Ed25519Signature2020 with proper structure (type, created, verificationMethod, proofPurpose, proofValue), (4) issuanceDate is RFC 3339, (5) credentialSubject has id field with holder DID. Add a test that issues a credential, serializes to JSON, and validates all required fields. Run cargo test --all-features.

  • Evaluate SpruceID ssi crate for DID resolution validation: Add ssi = "0.15" to core/Cargo.toml as an optional dependency ([dependencies.ssi] version = "0.15" optional = true). Create a test (behind #[cfg(feature = "ssi-compat")]) that: (1) generates a DID Document with Archy's identity.rs, (2) parses it with ssi::did::Document, (3) verifies the structure is valid per the ssi library's validation. This is a compatibility check — if ssi can parse our documents, we're spec-compliant. If it fails, note what's wrong. Do NOT make ssi a required dependency — this is for validation only. Run cargo test --features ssi-compat on dev server.

  • Evaluate pkarr crate for did:dht enhancement: Research the pkarr crate (v5.0.3, 550K downloads) by reading its documentation. It provides Ed25519-public-key-addressable resource records over the Mainline DHT — essentially did:dht but with better tooling and active maintenance. Compare with Archy's current did_dht.rs implementation that uses mainline directly. If pkarr offers advantages (relay fallback, caching, DNS-packet handling), document them in docs/pkarr-evaluation.md. Do NOT switch yet — just evaluate and document findings. Key question: does pkarr handle the BEP-44 signed DNS packet encoding that Archy currently does manually in did_dht.rs?

  • Clean up DWN — remove dead TBD references and simplify: Search the codebase for any references to TBD URLs, @tbd54566975, tbd.website, or TBD-specific terminology. Remove them. In docs/dwn-protocols.md, update the context to note that TBD is defunct and Archy's DWN is a custom implementation for peer sync, not a full DWN spec implementation. In core/archipelago/src/network/dwn_store.rs, verify the protocol definitions use Archy-specific URLs (https://archipelago.dev/protocols/...) not TBD URLs. Keep the DWN store functionality — it works for peer file catalogs and federation state — but stop calling it "Web5 DWN" in user-facing text. In neode-ui/src/views/Web5.vue, if there are references to "TBD" or "Web5 by TBD", update to just "Decentralized Identity" or "Web5 Standards".

  • Add did:dht auto-refresh background task: In core/archipelago/src/server.rs, add a background task that refreshes the did:dht publication every 2 hours. DHT records expire if not re-published. The task should: (1) check if the node has a published did:dht, (2) if yes, call did_dht::create_and_publish() to re-publish, (3) log success/failure. Use tokio::spawn with tokio::time::interval(Duration::from_secs(7200)). Only run if config.nostr_discovery_enabled is true (the same flag that gates DHT usage). Add the task alongside the existing background tasks (container scanner, peer health, etc.).

  • Encrypt credentials storage at rest: Read core/archipelago/src/credentials.rs — credentials are stored as plaintext JSON in {data_dir}/credentials/credentials.json. These may contain sensitive claims about identity holders. Fix: encrypt the file at rest using AES-256-GCM (the aes-gcm crate is already a dependency). Follow the pattern used in core/security/ for secrets encryption — derive a key from the node's master key. On read: detect if file is plaintext JSON (starts with [ or {) vs encrypted (binary/base64), decrypt if needed. On write: always encrypt. This provides a migration path — existing plaintext files get encrypted on first write. Add a test that writes credentials, reads them back, and verifies the file on disk is not plaintext. Run cargo test --all-features on dev server.

  • Add identity lifecycle integration tests: In core/archipelago/src/identity_manager.rs, add comprehensive tests for the full lifecycle: (1) create identity with default purpose → verify did:key format matches did:key:z6Mk..., (2) create Nostr key → verify npub starts with npub1, (3) sign arbitrary data → verify signature with public key, (4) issue a VC from this identity → verify the VC, (5) create a presentation wrapping the VC → verify the presentation, (6) delete identity → verify it's gone and default shifts. Use tempfile::tempdir() for storage. Target: 8+ new #[tokio::test] cases. Run cargo test --all-features.

  • Write ADR for DWN deprioritization: Create docs/adr/011-dwn-deprioritization.md. Document: (1) TBD/Block shut down Nov 2024, donated code to DIF, (2) no maintained Rust DWN SDK exists, (3) DWN spec losing momentum without TBD's backing, (4) Archy's federation over Tor + Nostr relays already serve the peer data sync use case, (5) DWN store code stays in codebase but is not actively developed, (6) re-evaluate if DIF produces a viable Rust SDK. Follow existing ADR format in docs/adr/. This is documentation only — no code changes.

  • Deploy to both nodes and test Web5 features: Deploy with ./scripts/deploy-to-target.sh --both. Test at http://192.168.1.228: (1) navigate to Web5 page — DID displays correctly, (2) click "Publish to DHT" if available — should publish and show status, (3) go to Credentials page — issue a test credential to self, verify it shows in list. Repeat on http://192.168.1.198. Check logs on both: ssh archipelago@192.168.1.228 'sudo journalctl -u archipelago --since "5 min ago" | grep -iE "(did|credential|dwn|identity)"' and same for .198.

  • Test cross-node DID resolution between .228 and .198: From .228's Web5 page, get its DID (did:key). SSH to .198 and test resolving .228's DID: curl -s http://localhost:5678/rpc/v1 -H 'Content-Type: application/json' -d '{"method":"identity.resolve-remote-did","params":{"did":"<.228-did>","onion_address":"<.228-onion>"}}'. The response should return .228's full DID Document. Test the reverse direction (resolve .198's DID from .228). If resolution fails, check: (1) Tor is running on both nodes (sudo podman ps | grep tor), (2) onion addresses are valid (cat /var/lib/archipelago/tor/*/hostname), (3) RPC is accessible over Tor. Fix any issues found.

  • Test cross-node credential issuance and verification: From .228, issue a Verifiable Credential where .228 is the issuer and .198's DID is the subject. Use the Credentials UI or RPC: curl -s http://localhost:5678/rpc/v1 -H 'Content-Type: application/json' -d '{"method":"identity.issue-credential","params":{"subject_did":"<.198-did>","credential_type":"FederationMember","claims":{"role":"peer","joined":"2026-03-15"}}}'. Copy the credential ID. From .198, verify the credential: curl -s http://localhost:5678/rpc/v1 -H 'Content-Type: application/json' -d '{"method":"identity.verify-credential","params":{"credential_id":"<id>"}}'. If .198 can't verify (it needs .228's public key), test the resolution chain: .198 resolves .228's DID → extracts public key → verifies signature. Fix any issues in the verification flow.

  • Test federation trust via DIDs between .228 and .198: Verify the federation between the two nodes uses DID-based identity. SSH to .228: curl -s http://localhost:5678/rpc/v1 -H 'Content-Type: application/json' -d '{"method":"federation.list-nodes"}'. Check that .198 appears as a peer with its DID. SSH to .198 and verify .228 appears similarly. If federation is not set up between them, establish it: use federation.invite on .228 to generate an invite, then federation.join on .198. After joining, verify: (1) both nodes see each other in their peer lists, (2) both nodes have each other's DIDs, (3) peer health checks pass between them. Check logs for federation errors: sudo journalctl -u archipelago --since "10 min ago" | grep -i federation.

  • Test DWN sync between .228 and .198: Even though DWN is deprioritized, test the existing sync functionality. On .228, write a test DWN message: curl -s http://localhost:5678/rpc/v1 -H 'Content-Type: application/json' -d '{"method":"dwn.write","params":{"protocol":"https://archipelago.dev/protocols/file-catalog/v1","data":{"filename":"test.txt","size":1024}}}'. Check DWN status on both nodes: curl -s http://localhost:5678/rpc/v1 -d '{"method":"dwn.status"}'. If sync is working, the message should appear on .198 after a sync cycle. If sync is not working, document what fails and where — this informs whether to invest more or formally pause DWN development. Don't spend more than 15 minutes debugging — document findings either way.


Phase 9: Factory Reset, Restore, & End-to-End Onboarding Test

Goal: Be able to factory reset the node, go through onboarding (DID + Nostr key created together), keys loaded into identity management, sign into IndeedHub with native Nostr signer, content loads. Also: restore from backup on the very first screen.

  • Implement system.factory-reset RPC endpoint: Create a new RPC handler in core/archipelago/src/api/rpc/system.rs (or add to an existing system module). The system.factory-reset method should: (1) require authentication (admin only), (2) accept { confirm: true } param as a safety check, (3) stop all running containers via PodmanClient (iterate podman ps -q and stop each), (4) delete user data: remove {data_dir}/user.json, {data_dir}/onboarding.json, {data_dir}/identities/ directory, {data_dir}/credentials/ directory, {data_dir}/peers.json, {data_dir}/did-cache/ directory, {data_dir}/dwn/ directory, (5) keep container images (don't re-download), keep the identity/node_key (node identity persists — it's the hardware identity), keep nginx and systemd configs, (6) clear all sessions from the session store, (7) restart the Archipelago service: sd_notify::notify(false, &[sd_notify::NotifyState::Reloading]) then exit the process (systemd will restart it), or alternatively use std::process::Command::new("sudo").args(["systemctl", "restart", "archipelago"]).spawn(). Register the handler in core/archipelago/src/api/rpc/mod.rs. Run cargo clippy --all-targets --all-features && cargo test --all-features on the dev server.

  • Add factory reset button to Settings.vue: In neode-ui/src/views/Settings.vue, add a "Factory Reset" section at the very bottom of the page (after all other settings). Use a .path-option-card container with a red-tinted warning. Include: (1) heading "Factory Reset", (2) description "Wipe all user data, identities, and credentials. Container images are preserved. The node will restart and show the onboarding screen.", (3) a .glass-button styled with red text/border that says "Factory Reset", (4) on click, show a confirmation dialog (use a simple v-if modal with .glass-card styling) asking "Are you sure? This will delete all identities, credentials, and settings. This cannot be undone." with Cancel and "Yes, Reset" buttons, (5) on confirm, call rpcClient.call({ method: 'system.factory-reset', params: { confirm: true } }), (6) on success, clear all localStorage (localStorage.clear()), redirect to /onboarding/intro. Use existing glass styles only — no new CSS classes. Run cd neode-ui && npm run type-check.

  • Add "Restore from Backup" button to OnboardingIntro.vue (first screen): In neode-ui/src/views/OnboardingIntro.vue, this is the very first screen a user sees after a fresh install or factory reset. Currently it just has a "Unlock your sovereignty →" button. Add a "Restore from Backup" link below it. Implementation: (1) add showRestore and restoreFile and passphrase refs, (2) below the main CTA button, add a subtle text link "Restore from backup" (style: text-white/50 hover:text-white/80 underline text-sm cursor-pointer mt-4 block text-center), (3) clicking it toggles a restore panel (use .glass-card) with: a file input (<input type="file" accept=".json">) for the archipelago-did-backup.json file, a password input for the backup passphrase, and a "Restore" .glass-button, (4) on file select, read the JSON with FileReader, (5) on Restore click, call rpcClient.call({ method: 'backup.restore-identity', params: { backup: parsedJson, passphrase: password } }), (6) on success, show "Identity restored successfully" message, then navigate to /onboarding/did — the DID step will now show the restored DID instead of generating a new one. Run cd neode-ui && npm run type-check.

  • Implement backup.restore-identity RPC for DID restore: Check if core/archipelago/src/api/rpc/backup_rpc.rs has an identity-specific restore handler. The existing backup.restore is for full system backups (tar archives from USB). We need a lighter backup.restore-identity that: (1) accepts the JSON blob from node.createBackup (the archipelago-did-backup.json file), (2) extracts: version, encrypted blob, (3) decrypts with Argon2 + ChaCha20-Poly1305 using the provided passphrase (reverse of backup::create_encrypted_backup() in core/archipelago/src/backup/identity.rs), (4) writes the decrypted 32-byte Ed25519 private key to {data_dir}/identity/node_key with 0o600 permissions, (5) returns { did, pubkey } of the restored identity. If the backup/identity.rs module already has a restore_encrypted_backup() function, use it. If not, create one following the inverse of create_encrypted_backup(). Register the handler in rpc/mod.rs. Run cargo clippy --all-targets --all-features && cargo test --all-features.

  • Ensure DID + Nostr keypair exist immediately from boot / factory reset: The node's Ed25519 key is auto-generated at first boot (stored in identity/node_key), and node.did / node.nostr-pubkey RPCs derive from it. But user identities with Nostr keys are only created when the user reaches the Identity step in onboarding. Fix this so keys are available from the very start: (1) In core/archipelago/src/main.rs or server.rs, during startup (after loading node identity but before starting the HTTP server), check if any identities exist via IdentityManager::list(). If the list is empty (fresh boot or factory reset), auto-create a default identity: call identity_manager.create("Default", IdentityPurpose::Personal) — this generates Ed25519 + Nostr keypair automatically. (2) Verify identity_manager.rs create() method calls create_nostr_key() automatically — if not, add it after keypair generation. (3) This means when OnboardingDid.vue loads, both node.did AND identity.list already return data with Nostr npub populated. The identity step in onboarding can then let the user rename or create additional identities, but the default is already there. (4) After factory reset (which deletes {data_dir}/identities/), the next boot auto-creates the default identity again. Run cargo test --all-features on the dev server.

  • Deploy factory reset + restore and test the full cycle: Deploy with ./scripts/deploy-to-target.sh --live. Then run the end-to-end test on .228: (1) Login at http://192.168.1.228, go to Settings, scroll to bottom, click "Factory Reset", confirm, (2) node restarts — wait 10-15 seconds, refresh browser, (3) should see the onboarding intro screen, (4) go through: Intro → Path → DID (should show new or existing DID + Nostr npub) → Identity (create "Personal" identity) → Backup (download backup file) → Verify (signature verified) → Done → Login, (5) set password, login, (6) navigate to Web5/Identity page — DID and Nostr npub should display, (7) go to Apps → click IndeedHub, (8) NostrIdentityPicker should appear — select the identity just created, (9) IndeedHub should load in iframe, (10) IndeedHub should request window.nostr.getPublicKey() — Archy returns the identity's Nostr pubkey, (11) if IndeedHub requires signing, NostrSignConsent appears, approve it, (12) IndeedHub content should load from their API (videos, pages). Check logs: ssh archipelago@192.168.1.228 'sudo journalctl -u archipelago --since "5 min ago" | grep -iE "(factory|reset|onboard|identity|nostr|indeedhub)"'.

  • Test restore from backup on fresh state: After the previous test, do another factory reset on .228. This time: (1) when the first screen appears (Login.vue in setup mode), click "Restore from Backup", (2) select the archipelago-did-backup.json file downloaded in the previous test, (3) enter the backup passphrase, (4) click Restore, (5) should see success message, (6) continue onboarding — the DID step should show the SAME DID as before (restored from backup), (7) create identity, complete onboarding, (8) login and verify: same DID, identity management has the restored keys, (9) go to IndeedHub — Nostr signing should work with the restored identity. If any step fails, check: backend logs for restore errors, frontend console for RPC failures, verify the backup file format matches what backup.restore-identity expects.


Phase 10: Final Verification & Deploy

  • Full type-check and lint pass: Run cd /Users/dorian/Projects/archy/neode-ui && npm run type-check — must pass with zero errors. Run npm run test — all tests must pass. On dev server, run cd ~/archy/core && cargo clippy --all-targets --all-features — zero warnings. Run cargo test --all-features — all tests pass.

  • Final deploy and complete smoke test: Run ./scripts/deploy-to-target.sh --live. After deploy, test the full user flow at http://192.168.1.228: (1) login works, (2) dashboard loads with app list, (3) click each installed app — loads in iframe or new tab correctly, (4) go to Marketplace — all icons load, no broken images, no altcoins, (5) open IndeedHub — identity picker shows, select identity, app loads, Nostr signing works, content from their API loads, (6) start/stop an app — status updates correctly, (7) navigate to a fake URL like /dashboard/nonexistent — shows 404 page with back link, (8) Web5 page shows DID + Nostr npub correctly, credentials can be issued and verified, (9) Settings page has Factory Reset at the bottom, (10) factory reset works — node restarts, onboarding appears, (11) restore from backup works on first screen, (12) check server logs for errors: ssh archipelago@192.168.1.228 'sudo journalctl -u archipelago --since "5 min ago" | grep -i error'.


Phase 11: Security & Code Quality Audit Report

Generate a comprehensive written report at docs/security-code-audit-2026-03.md. This phase is research and documentation only — no code changes. The report should be honest about strengths and weaknesses so we know exactly where we stand.

  • Audit authentication & session security: Review core/archipelago/src/auth.rs and core/archipelago/src/session.rs. Document: (1) password hashing — bcrypt with what cost factor? Is Argon2id a better choice for new installs? Compare bcrypt (current) vs argon2id (already a dependency for backup encryption) — pros/cons, (2) session token generation — is 32-byte random hex sufficient entropy? How does it compare to using a CSPRNG-backed JWT or tower-sessions? (3) session storage — in-memory only, lost on restart (unless SQLite was added in earlier phases). Rate the risk, (4) CSRF — 32-byte hex token per login, validated on every request. Is this sufficient? (5) rate limiting — per-method in-memory counters. Document coverage gaps (which endpoints lack rate limiting), (6) TOTP — using totp-rs with encrypted secret storage. Rate the implementation quality. Write findings to a "Session & Auth" section of the report.

  • Audit cryptographic implementations: Review all crypto code across the codebase. For each, compare our implementation against what a library would provide:

    Component Our Implementation Library Alternative Verdict
    Password hashing bcrypt crate, DEFAULT_COST argon2 crate (already a dep) Document: argon2id is newer, memory-hard, better against GPU attacks. bcrypt is battle-tested. Recommendation?
    Session tokens rand::thread_rng().gen::<[u8; 32]>() + hex tower-sessions or signed JWTs via jsonwebtoken Document: current is fine for single-instance. JWTs add stateless verification but complexity
    DID signing ed25519-dalek direct usage SpruceID ssi crate Document: our usage is correct and minimal. ssi adds 50+ transitive deps for features we don't use
    Backup encryption Argon2 KDF + ChaCha20-Poly1305 age crate (encryption tool) Document: our stack is standard and correct. age is simpler API but less control
    VC signatures Custom Ed25519Signature2020 proof SpruceID ssi VC module Document: our impl handles one proof type. ssi handles many but large dep tree
    Nostr encryption nostr-sdk NIP-04/NIP-44 Direct chacha20poly1305 + secp256k1 Document: nostr-sdk is correct choice, actively maintained
    TLS rustls via reqwest openssl Document: rustls is the right choice — pure Rust, no C deps, privacy-focused
    Key storage Raw bytes in files with 0o600 perms keyring crate or OS keychain Document: file-based is correct for headless server. OS keychain not available

    Write findings to a "Cryptographic Review" section. For each row, state: is our code correct? Is it secure? Would a library be better? Why or why not?

  • Audit container security: Review container security across core/container/src/podman_client.rs, core/archipelago/src/api/rpc/package.rs, and apps/*/manifest.yml. Document: (1) are all containers running with --cap-drop ALL + only required caps added back? Check each app manifest, (2) readonly_root: true — which apps have it, which don't and why, (3) no-new-privileges — is it set for all containers? (4) user namespace — are containers running as non-root (UID > 1000)? Check for any running as root, (5) image pinning — are images pinned to specific digests/versions or using :latest? List offenders, (6) cosign verification — still a TODO? Document the gap, (7) network isolation — which containers share networks? Is archy-net properly scoped? (8) secrets injection — how are secrets passed to containers? Env vars (visible in podman inspect) vs mounted files? Write findings to a "Container Security" section.

  • Audit RPC endpoint security: Review core/archipelago/src/api/rpc/mod.rs — the main RPC dispatcher. Document: (1) which endpoints require authentication and which don't? List any unauthenticated endpoints beyond auth.login, auth.setup, auth.isSetup, auth.isOnboardingComplete, (2) RBAC enforcement — was it wired up in Phase 6? If yes, verify it works. If no, document the gap and risk, (3) input validation — pick 5 critical endpoints (login, install package, factory reset, backup restore, identity create) and trace the input from RPC params to handler. Are inputs validated? Are there injection risks? (4) error message sanitization — does sanitize_error_message() strip file paths and internal details from user-facing errors? Test with a few error cases, (5) path traversal — check filebrowser-client.ts sanitizePath() and any backend file operations. Can a crafted path escape the data directory? Write findings to an "RPC Security" section.

  • Audit frontend security: Review the Vue frontend for common web vulnerabilities. Document: (1) XSS — are any user inputs rendered with v-html? Search for v-html across all .vue files. If found, is the content sanitized? (2) CSRF — frontend sends X-CSRF-Token header on every RPC call. Verify this in rpc-client.ts. Is the token properly scoped to the session? (3) credential storage — what's in localStorage? Search for localStorage.setItem across all files. Are any secrets (passwords, keys, tokens) stored client-side? They shouldn't be — only session flags and UI preferences, (4) iframe security — nostr-provider.js uses postMessage('*') for responses. Is the origin validated on incoming messages? Check AppSession.vue and AppLauncherOverlay.vue message handlers — do they verify event.origin? (5) dependency audit — run cd neode-ui && npm audit and document findings. Write findings to a "Frontend Security" section.

  • Assess custom code quality vs library alternatives — full comparison: This is the core of the report. For each major custom module, write a comparison:

    1. HTTP Server (custom hyper 0.14 handler.rs — 813 lines)

    • Quality: Hand-rolled routing, middleware, CORS, WebSocket upgrade. Works but brittle.
    • Alternative: axum (tokio team, built on hyper 1.x). Typed extractors, middleware stack, tower integration.
    • Verdict: Migrate. hyper 0.14 is EOL. axum reduces handler.rs from 813 lines to ~200.
    • Risk: Medium — RPC logic unchanged, only HTTP glue changes.

    2. Session Management (custom session.rs — 200 lines)

    • Quality: In-memory token store, TTL-based expiry, max 5 concurrent sessions, zeroize on drop.
    • Alternative: tower-sessions + tower-sessions-sqlx-store (SQLite backend).
    • Verdict: If SQLite is added, migrate. If not, keep custom — it's simple and correct for single-instance.

    3. Rate Limiting (custom in rpc/mod.rs)

    • Quality: Per-method in-memory counters. Simple, works, not configurable.
    • Alternative: governor crate or tower::limit::RateLimitLayer.
    • Verdict: Low priority swap. Current works fine for single-instance appliance.

    4. DID Implementation (custom identity.rs — ~300 lines)

    • Quality: Clean did:key generation, proper W3C DID Document, good test coverage.
    • Alternative: SpruceID ssi crate (v0.15.0, 146K downloads).
    • Verdict: Keep custom. Our code is ~300 lines, purpose-built, handles dual-key (Ed25519+secp256k1). ssi would add 50+ transitive deps for features we don't need. Use ssi only for external VC verification if needed.

    5. Verifiable Credentials (custom credentials.rs — ~400 lines)

    • Quality: W3C VC 2.0 compliant, issue/verify/revoke/present all working, good test coverage.
    • Alternative: SpruceID ssi VC module.
    • Verdict: Keep custom for issuance. Consider ssi for verification of external VCs (more proof types). Our code handles Ed25519Signature2020 only — sufficient for node-to-node but not for arbitrary external VCs.

    6. did:dht (custom did_dht.rs — ~200 lines)

    • Quality: Works via mainline crate, BEP-44 signed records, in-memory cache.
    • Alternative: pkarr crate (v5.0.3, 550K downloads) — higher-level abstraction over mainline.
    • Verdict: Evaluate pkarr. If it handles BEP-44 encoding we do manually, switch. Otherwise keep custom — it's small and works.

    7. DWN Store (custom dwn_store.rs — ~300 lines)

    • Quality: Basic CRUD, filesystem-backed, protocol registration. Skeletal.
    • Alternative: None production-ready in Rust. dwn crate (unavi-xyz) is v0.4.0, 323 downloads.
    • Verdict: Keep custom. No alternative exists. Deprioritize per ADR-011.

    8. WebSocket State Broadcasting (custom state.rs — ~200 lines)

    • Quality: tokio broadcast channel, full model resync on every change. Functional but inefficient.
    • Alternative: json-patch crate for RFC 6902 diffs. Frontend already has fast-json-patch.
    • Verdict: Add json-patch crate. One of the highest-impact improvements — reduces bandwidth dramatically.

    9. Form Validation (manual inline in Vue components)

    • Quality: Scattered, inconsistent, error-prone as forms grow.
    • Alternative: zod (TypeScript-first schema validation, 40M weekly npm downloads).
    • Verdict: Add zod. Centralize schemas in src/types/schemas.ts. Critical for onboarding where bad input can break key generation.

    10. Container Runtime Abstraction (custom runtime.rs + podman_client.rs — ~600 lines)

    • Quality: Clean trait abstraction (PodmanRuntime, DockerRuntime, AutoRuntime). Well-designed.
    • Alternative: bollard crate (Docker/Podman API client, 7M downloads).
    • Verdict: Keep custom. Our abstraction is clean and purpose-built. bollard is Docker-first and would need wrapping anyway for our manifest-based approach.

    Write all comparisons to a "Custom Code vs Libraries" section with a summary table.

  • Write executive summary and next steps: At the top of docs/security-code-audit-2026-03.md, write an executive summary covering: (1) overall security posture (1-10 rating with justification), (2) top 5 risks ranked by severity, (3) top 5 strengths, (4) recommended next actions (ordered by impact). Reference the docs/refactoring-plan.md 3-year plan for longer-term items. End with a "What to do next" section listing the 3 most impactful changes from this audit. Commit the report.