Backend metadata and manifest now match the actual running config and the frontend port mapping. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
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.mdfor the full 3-year plan. SeeCLAUDE.mdfor 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, theget_app_metadata()function referencesphotoprims.svg(missing 'h') for the PhotoPrism icon. Search forphotoprimsand replace withphotoprism. Verify the icon file exists atneode-ui/public/assets/img/app-icons/photoprism.svg. Runcargo clippy --all-targets --all-featuresincore/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.icoandindeehub.ico(typo). Deleteindeehub.ico. Convertindeedhub.icotoindeedhub.png(better format consistency). Update all references: (1)neode-ui/src/utils/dummyApps.tsline ~518 — changeindeehub.icotoindeedhub.png, (2)neode-ui/src/views/Marketplace.vueline ~913 — changeindeehub.icotoindeedhub.png, (3)core/archipelago/src/container/docker_packages.rslines ~451-454 — changeindeehub.icotoindeedhub.png. Search the entire codebase forindeehub(missing 'd') and fix all occurrences toindeedhub. Runcd neode-ui && npm run type-checkto verify. -
Audit all app icons match their references: Cross-check every icon path referenced in
docker_packages.rsget_app_metadata()against actual files inneode-ui/public/assets/img/app-icons/. Verify each app in theMarketplace.vuegetCuratedAppList()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.pngor.svgover.icoand.webpwhere 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). Runsudo podman ps -a --format '{{.Names}} {{.Status}}' | grep -i restartto 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 thearchy-netPodman 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 withsudo 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 withfree -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. Runsudo 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, runcurl -s http://localhost:5678/healthto 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 inneode-ui/src/stores/appLauncher.ts(lines 23-31, theXFRAME_BLOCKED_PORTSarray). 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 includesproxy_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 varGF_SECURITY_ALLOW_EMBEDDING=trueon the Grafana container fixes it, then remove it from the blocked list. For Nextcloud (port 8085) — check if the nginxsub_filterapproach or Nextcloud'soverwriteprotocolsetting allows embedding. For Uptime Kuma (port 3001) — it may work with the header stripped. Test each by loadinghttp://192.168.1.228/app/{id}/in a browser iframe orcurl -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.jsthat break because they resolve to the Archy root, not the app root. Inimage-recipe/configs/nginx-archipelago.conf, check IndeedHub's location block (lines 334-367) — it already usessub_filterto 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 athttp://192.168.1.228/app/{id}/, open browser dev tools Network tab, check for 404s on static assets. If assets 404, add appropriatesub_filterrules 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 athttp://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 isgit.tx1138.com/lfg2025/indeedhub:latest(fromapps/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 hardcodeshttp://localhost:8190for IndeedHub. But nginx and the frontend use port 7777. Updatedocker_packages.rsto use port 7777: changeSome("http://localhost:8190".to_string())toSome("http://localhost:7777".to_string()). Also verifyapps/indeedhub/manifest.yml— if it says port 8190, update to 7777 to match the actual deployment. Inneode-ui/src/stores/appLauncher.tsline 67, confirm'7777': '/app/indeedhub/'is correct. Deploy with./scripts/deploy-to-target.sh --liveand test. -
Verify nostr-provider.js injection works for IndeedHub iframe: The NIP-07 Nostr signer works by nginx injecting
neode-ui/public/nostr-provider.jsinto the iframe viasub_filter. Check the IndeedHub nginx location block inimage-recipe/configs/nginx-archipelago.conf(lines 334-367) includes asub_filterthat injects<script src="/nostr-provider.js"></script>into the HTML response. If missing, add:sub_filter '</head>' '<script src="/nostr-provider.js"></script></head>';withsub_filter_once on;andsub_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 forwindow.nostravailability — runJSON.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 requestswindow.nostr.getPublicKey(), the Archy parent responds with the selected identity's Nostr pubkey, (5) when IndeedHub requestswindow.nostr.signEvent(event), NostrSignConsent modal appears, (6) user approves, event is signed viaidentity.nostr-signRPC, (7) signed event returned to IndeedHub. Test each step. If NostrIdentityPicker doesn't show, checkAppSession.vueline ~302-304isIdentityAwareApp()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-Policyadjustments in the nginx location block. If internal navigation causes the iframe to navigate to a bare URL (not under/app/indeedhub/), addsub_filterrules 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 fornip44.encryptandnip44.decrypt. If any fail, check RPC handlers incore/archipelago/src/api/rpc/identity.rs— thehandle_identity_nostr_encrypt_nip04/nip44and 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 inneode-ui/src/views/MarketplaceAppDetails.vue. Note: Archy is Bitcoin only — remove any Monero or Liquid entries fromMarketplace.vuegetCuratedAppList()if present. -
Remove non-Bitcoin altcoin entries from marketplace: Search
neode-ui/src/views/Marketplace.vuefor "monero", "liquid", "litecoin", or any non-Bitcoin cryptocurrency entries in thegetCuratedAppList()function. Remove them entirely. Archy is a Bitcoin-only platform. Runcd neode-ui && npm run type-checkafter 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 withcore/archipelago/src/api/rpc/package.rslines 64-96 where backend dependency checks are defined. Ensure they match exactly. If backend checks forhas_bitcoinbefore installingelectrs, the frontend dependency list forelectrsmust showbitcoin-knotsas a prerequisite. Update the frontend to match the backend. Ideally, add an RPC methodpackage.get-dependenciesthat 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 incore/archipelago/src/state.rs. -
Fix route-to-package-key mapping divergence: In
neode-ui/src/views/AppDetails.vuelines 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:mempoolmaps tomempool-webbut backend may usearchy-mempool-web. Checkelectrsmaps tomempool-electrsorarchy-electrs. Runcd neode-ui && npm run type-checkafter changes.
Phase 6: Backend Critical Fixes
-
Fix session TTL clock bug — use SystemTime instead of Instant: Read
core/archipelago/src/session.rs. Find whereInstant::now()is used for session TTL/expiry (around line 97).Instantis monotonic but can drift on sleep/hibernate — common on NUC/Pi hardware. Replace withSystemTime::now()for absolute time comparison. TheFULL_SESSION_TTL(24 hours) andPENDING_TOTP_TTL(5 minutes) checks should useSystemTime::elapsed()or storeSystemTimetimestamps and compare withSystemTime::now(). Runcargo test --all-featuresincore/on the dev server. -
Enforce RBAC in RPC handler: Read
core/archipelago/src/auth.rs— find theUserRoleenum andcan_access()method. Then readcore/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, callrole.can_access(method_name), and return an authorization error if denied. For now, all users created via onboarding should default toAdminrole (single-user system), but this lays the groundwork for multi-user. Runcargo clippy --all-targets --all-features && cargo test --all-featureson 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.rslines ~70, 83, 88. Runcargo clippy --all-targets --all-featuresto verify no new warnings. -
Deploy and verify backend fixes: Run
./scripts/deploy-to-target.sh --live. After deploy: (1) verify login still works athttp://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 dockerodeandnpm uninstall @types/dockerodeif it exists. Search the codebase for any remaining imports:grep -r "dockerode" neode-ui/src/. Remove any dead imports found. Runnpm run type-checkto 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 -100to 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 selectorh1not 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. Runnpm run testuntil 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') }. Createneode-ui/src/views/NotFound.vue— a simple view using the existing.glass-cardclass 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). Runnpm 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
ssicrate (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, evaluatessifor 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 usesgenerateMockSignature()instead of real cryptographic proof. Replace with a call tonode.signChallengeRPC (oridentity.signif it exists). The flow should be: (1) frontend generates a random challenge string, (2) sends toidentity.signRPC with the node's default identity, (3) backend signs with Ed25519 key, (4) frontend displays the signature as proof the node controls the DID. Checkcore/archipelago/src/api/rpc/identity.rsfor existing sign handlers —handle_identity_signshould work. Ifnode.signChallengeRPC doesn't exist, theidentity.signendpoint (which takes{ id?, data }and returns{ signature }) should be sufficient. Update the Vue component to call it. Runcd 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 toidentity.exportorbackup.create-identityRPC (check what exists incore/archipelago/src/api/rpc/identity.rsandcore/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 incore/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.jsonfile. Runcargo test --all-featureson 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, thegenerate_did_document()function builds a DID Document. Verify it includes all required fields per W3C DID Core v1.0:id,verificationMethod(with correcttype: "Ed25519VerificationKey2020"),authentication,assertionMethod,keyAgreement(X25519). Check that@contextincludes["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. Runcargo test --all-features. -
Validate Verifiable Credentials against W3C VC 2.0 spec: In
core/archipelago/src/credentials.rs, verify theVerifiableCredentialstruct produces output matching W3C VC Data Model 2.0. Check: (1)@contextincludeshttps://www.w3.org/ns/credentials/v2, (2)typearray starts with"VerifiableCredential", (3)proofusesEd25519Signature2020with proper structure (type,created,verificationMethod,proofPurpose,proofValue), (4)issuanceDateis RFC 3339, (5)credentialSubjecthasidfield with holder DID. Add a test that issues a credential, serializes to JSON, and validates all required fields. Runcargo test --all-features. -
Evaluate SpruceID ssi crate for DID resolution validation: Add
ssi = "0.15"tocore/Cargo.tomlas 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'sidentity.rs, (2) parses it withssi::did::Document, (3) verifies the structure is valid per thessilibrary's validation. This is a compatibility check — ifssican parse our documents, we're spec-compliant. If it fails, note what's wrong. Do NOT makessia required dependency — this is for validation only. Runcargo test --features ssi-compaton dev server. -
Evaluate pkarr crate for did:dht enhancement: Research the
pkarrcrate (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 currentdid_dht.rsimplementation that usesmainlinedirectly. Ifpkarroffers advantages (relay fallback, caching, DNS-packet handling), document them indocs/pkarr-evaluation.md. Do NOT switch yet — just evaluate and document findings. Key question: doespkarrhandle the BEP-44 signed DNS packet encoding that Archy currently does manually indid_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. Indocs/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. Incore/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. Inneode-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, calldid_dht::create_and_publish()to re-publish, (3) log success/failure. Usetokio::spawnwithtokio::time::interval(Duration::from_secs(7200)). Only run ifconfig.nostr_discovery_enabledis 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 (theaes-gcmcrate is already a dependency). Follow the pattern used incore/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. Runcargo test --all-featureson 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 matchesdid:key:z6Mk..., (2) create Nostr key → verify npub starts withnpub1, (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. Usetempfile::tempdir()for storage. Target: 8+ new#[tokio::test]cases. Runcargo 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 indocs/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 athttp://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 onhttp://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: usefederation.inviteon .228 to generate an invite, thenfederation.joinon .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). Thesystem.factory-resetmethod should: (1) require authentication (admin only), (2) accept{ confirm: true }param as a safety check, (3) stop all running containers viaPodmanClient(iteratepodman ps -qand 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 theidentity/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 usestd::process::Command::new("sudo").args(["systemctl", "restart", "archipelago"]).spawn(). Register the handler incore/archipelago/src/api/rpc/mod.rs. Runcargo clippy --all-targets --all-features && cargo test --all-featureson 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-cardcontainer 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-buttonstyled with red text/border that says "Factory Reset", (4) on click, show a confirmation dialog (use a simplev-ifmodal with.glass-cardstyling) 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, callrpcClient.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. Runcd 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) addshowRestoreandrestoreFileandpassphraserefs, (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 thearchipelago-did-backup.jsonfile, a password input for the backup passphrase, and a "Restore".glass-button, (4) on file select, read the JSON withFileReader, (5) on Restore click, callrpcClient.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. Runcd neode-ui && npm run type-check. -
Implement backup.restore-identity RPC for DID restore: Check if
core/archipelago/src/api/rpc/backup_rpc.rshas an identity-specific restore handler. The existingbackup.restoreis for full system backups (tar archives from USB). We need a lighterbackup.restore-identitythat: (1) accepts the JSON blob fromnode.createBackup(thearchipelago-did-backup.jsonfile), (2) extracts: version, encrypted blob, (3) decrypts with Argon2 + ChaCha20-Poly1305 using the provided passphrase (reverse ofbackup::create_encrypted_backup()incore/archipelago/src/backup/identity.rs), (4) writes the decrypted 32-byte Ed25519 private key to{data_dir}/identity/node_keywith 0o600 permissions, (5) returns{ did, pubkey }of the restored identity. If thebackup/identity.rsmodule already has arestore_encrypted_backup()function, use it. If not, create one following the inverse ofcreate_encrypted_backup(). Register the handler inrpc/mod.rs. Runcargo 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), andnode.did/node.nostr-pubkeyRPCs 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) Incore/archipelago/src/main.rsorserver.rs, during startup (after loading node identity but before starting the HTTP server), check if any identities exist viaIdentityManager::list(). If the list is empty (fresh boot or factory reset), auto-create a default identity: callidentity_manager.create("Default", IdentityPurpose::Personal)— this generates Ed25519 + Nostr keypair automatically. (2) Verifyidentity_manager.rscreate()method callscreate_nostr_key()automatically — if not, add it after keypair generation. (3) This means whenOnboardingDid.vueloads, bothnode.didANDidentity.listalready 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. Runcargo test --all-featureson 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 athttp://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 requestwindow.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.jsonfile 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 whatbackup.restore-identityexpects.
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. Runnpm run test— all tests must pass. On dev server, runcd ~/archy/core && cargo clippy --all-targets --all-features— zero warnings. Runcargo 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 athttp://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.rsandcore/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 ortower-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 — usingtotp-rswith 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 bcryptcrate, DEFAULT_COSTargon2crate (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]>()+ hextower-sessionsor signed JWTs viajsonwebtokenDocument: current is fine for single-instance. JWTs add stateless verification but complexity DID signing ed25519-dalekdirect usageSpruceID ssicrateDocument: our usage is correct and minimal. ssiadds 50+ transitive deps for features we don't useBackup encryption Argon2 KDF + ChaCha20-Poly1305 agecrate (encryption tool)Document: our stack is standard and correct. ageis simpler API but less controlVC signatures Custom Ed25519Signature2020 proof SpruceID ssiVC moduleDocument: our impl handles one proof type. ssihandles many but large dep treeNostr encryption nostr-sdkNIP-04/NIP-44Direct chacha20poly1305+secp256k1Document: nostr-sdkis correct choice, actively maintainedTLS rustlsvia reqwestopensslDocument: rustlsis the right choice — pure Rust, no C deps, privacy-focusedKey storage Raw bytes in files with 0o600 perms keyringcrate or OS keychainDocument: 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, andapps/*/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? Isarchy-netproperly scoped? (8) secrets injection — how are secrets passed to containers? Env vars (visible inpodman 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 beyondauth.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 — doessanitize_error_message()strip file paths and internal details from user-facing errors? Test with a few error cases, (5) path traversal — checkfilebrowser-client.tssanitizePath()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 forv-htmlacross all.vuefiles. If found, is the content sanitized? (2) CSRF — frontend sendsX-CSRF-Tokenheader on every RPC call. Verify this inrpc-client.ts. Is the token properly scoped to the session? (3) credential storage — what's in localStorage? Search forlocalStorage.setItemacross 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.jsusespostMessage('*')for responses. Is the origin validated on incoming messages? CheckAppSession.vueandAppLauncherOverlay.vuemessage handlers — do they verifyevent.origin? (5) dependency audit — runcd neode-ui && npm auditand 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:
governorcrate ortower::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
ssicrate (v0.15.0, 146K downloads). - Verdict: Keep custom. Our code is ~300 lines, purpose-built, handles dual-key (Ed25519+secp256k1).
ssiwould add 50+ transitive deps for features we don't need. Usessionly 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
ssiVC module. - Verdict: Keep custom for issuance. Consider
ssifor 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
mainlinecrate, BEP-44 signed records, in-memory cache. - Alternative:
pkarrcrate (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.
dwncrate (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-patchcrate for RFC 6902 diffs. Frontend already hasfast-json-patch. - Verdict: Add
json-patchcrate. 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:
bollardcrate (Docker/Podman API client, 7M downloads). - Verdict: Keep custom. Our abstraction is clean and purpose-built.
bollardis 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 thedocs/refactoring-plan.md3-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.