From d889b890f9e63815c178853ed98a1e31f6cec7f8 Mon Sep 17 00:00:00 2001 From: Your Name Date: Wed, 27 May 2026 15:32:38 +0530 Subject: Fix compile issues for NIP-46 build - nip46.rs: match on owned RelayPoolNotification (not .as_ref()) - git_mirror.rs: accept shared nostr_sdk::Client for state event publishing - main.rs: pass nostr_client to mirror_repo_to_servers - NIP46-PLAN.md: full implementation checklist and roadmap - PLAN.md: updated --- NIP46-PLAN.md | 111 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ PLAN.md | 86 ++++++++++++++++++++++++++++++------------ src/git_mirror.rs | 12 +----- src/main.rs | 2 +- src/nip46.rs | 4 +- 5 files changed, 176 insertions(+), 39 deletions(-) create mode 100644 NIP46-PLAN.md diff --git a/NIP46-PLAN.md b/NIP46-PLAN.md new file mode 100644 index 0000000..8d8d56b --- /dev/null +++ b/NIP46-PLAN.md @@ -0,0 +1,111 @@ +# GRASP Mirror NIP-46 Remote Signing — Implementation Plan + +## Goal + +Build and deploy the grasp-mirror daemon with NIP-46 remote signing so it can publish authenticated `kind:30618` state events and push git data to GRASP servers, without storing nsec keys on the VPS. + +## Architecture + +``` + ┌─────────────┐ + │ Amber │ + │ (phone) │ + └──────┬──────┘ + │ NIP-46 (kind:24133) + │ NIP-04 encrypted + ┌──────┴──────┐ + │ Nostr Relay │ + │ (relay/ngit)│ + └──────┬──────┘ + │ + ┌──────┴──────┐ + │ grasp-mirror│ + │ daemon │ + │ (VPS) │ + └──────┬──────┘ + │ git push + kind:30618 + ┌──────┴──────┐ + │ GRASP │ + │ servers │ + └─────────────┘ +``` + +- Daemon generates client keypairs per npub, persists in SQLite +- Produces `nostrconnect://` pairing URIs shown in health endpoint +- User opens URI in Amber → NIP-46 session established +- Before git push, daemon builds unsigned `kind:30618` from repo refs +- Sends `sign_event` request to signer via NIP-46 relay protocol +- Amber signs on phone → daemon receives signed event → publishes + pushes + +## Checklist + +### Phase 1: Fix compile errors locally + +- [ ] Fix `nip46.rs` — `notification.as_ref()` → match on owned `RelayPoolNotification` +- [ ] Fix `nip46.rs` — ensure `message.id()` borrow is cloned before consuming message +- [ ] Fix `git_mirror.rs` — accept shared `nostr_sdk::Client` instead of creating throwaway per server +- [ ] Fix `main.rs` — pass `nostr_client` to `git_mirror.mirror_repo_to_servers` +- [ ] Verify `db.rs` — `bool` FromRow works with sqlx 0.8 sqlite INTEGER +- [ ] Pin Rust 1.95.0 in Ansible role `tasks/main.yml` +- [ ] Commit and push to GRASP origin + +### Phase 2: Build on VPS + +- [ ] GRASP server running on target VPS (dependency: migration session) +- [ ] Push SSH key to VPS for Ansible key-based auth +- [ ] Run `ansible-playbook playbooks/30-grasp-mirror.yml -v` +- [ ] Fix any remaining compile errors iteratively +- [ ] Binary installed at `/usr/local/bin/grasp-mirror` +- [ ] Systemd service `grasp-mirror` running + +### Phase 3: Verify and pair + +- [ ] Health endpoint responds: `curl http://localhost:7335/health` +- [ ] Response includes `nip46` array with session statuses +- [ ] Each unpaired npub shows `pairing_uri` +- [ ] Pair npub 1 (`npub12m5...`) via Amber +- [ ] Pair npub 2 (`npub19jx...`) via Amber +- [ ] Pair npub 3 (`npub1c03...`) via Amber +- [ ] Health endpoint shows all 3 sessions `connected: true` +- [ ] Test signing: daemon builds and signs a `kind:30618` event +- [ ] Test push: git data pushes to a target GRASP server + +### Phase 4: Document roadmap + +- [ ] Add QEMU/OpenWRT orchestration roadmap to PLAN.md or PROGRESS.md + +## Key Files + +| File | Role | +|---|---| +| `src/nip46.rs` | NIP-46 client: sessions, NIP-04 encrypt/decrypt, relay listener, `sign_event()` | +| `src/db.rs` | `nip46_sessions` table + CRUD methods | +| `src/config.rs` | `Nip46Config` with relays + signing_timeout_secs | +| `src/git_mirror.rs` | Builds unsigned `kind:30618`, signs via NIP-46, publishes before push | +| `src/http_health.rs` | NIP-46 session status in `/health` JSON | +| `src/main.rs` | Wires NIP-46 init + listener into daemon startup | +| `ansible/roles/grasp_mirror/tasks/main.yml` | Ansible deploy tasks (build + install) | +| `ansible/roles/grasp_mirror/defaults/main.yml` | NIP-46 relay + timeout vars | +| `ansible/roles/grasp_mirror/templates/config.toml.j2` | Config with `[nip46]` section | + +## Constraints + +- No `any` type in TypeScript (N/A — Rust project) +- No comments in code unless requested +- Use `pnpm` for JS, `cargo` for Rust +- Rust 1.95.0 required (nostr-sdk 0.39 doesn't compile on 1.85) +- NIP-04 encryption for NIP-46 messages (not NIP-44 — simpler, Amber supports it) +- Signing timeout: 7 days (604800 seconds) — queue-and-wait model +- GRASP push auth requires signed `kind:30618` by maintainer, not HTTP auth + +## Future Roadmap: QEMU/OpenWRT Test Orchestration + +After migration, repurpose old VPS (2 vCPU, 8GB RAM, 99GB disk) as dedicated OpenWRT test runner: + +1. **Deploy loom-worker** on old VPS — decentralized compute marketplace via Nostr +2. **Write QEMU execution adapter** for loom — Unix domain socket that spins up OpenWRT instances, runs tests, streams results +3. **Use existing act-runner** custom pipeline to submit loom jobs from CI +4. **Do NOT build custom orchestration** — loom already has standardized event kinds (5001/5100/5101), Cashu payments, Blossom result storage, encrypted communication +5. **Evaluate loom from budabit** (`nostr://npub1hw6amg8p24ne08c9gdq8hhpqx0t0pwanpae9z25crn7m9uy7yarse465gr/relay.ngit.dev/loom-worker`) before building anything custom + +Loom's pluggable adapter architecture means we write ~200 lines of Deno/TypeScript for the QEMU adapter and get the entire Nostr job queue, payment, and result streaming for free. diff --git a/PLAN.md b/PLAN.md index ab1ea9f..1911a20 100644 --- a/PLAN.md +++ b/PLAN.md @@ -1,25 +1,61 @@ -# grasp-mirror Bugfix Plan - -## Bugs Identified from VPS Logs - -### Bug 1: `remote 'push_target' already exists` -- **Location**: `src/git_mirror.rs:137` -- **Cause**: `repo.remote("push_target", target_url)` creates a new remote every cycle. On subsequent cycles, the bare repo already has a `push_target` remote, so `git2` returns `Exists (-4)`. -- **Fix**: Use `repo.find_remote("push_target")` first. If it exists, call `repo.remote_set_url()` to update the URL. If not, create it. - -### Bug 2: Single repo failure aborts entire cycle -- **Location**: `src/main.rs:mirror_cycle()` — the `mirror.mirror_repo_to_servers()` call uses `?` which propagates the error and aborts the loop. -- **Cause**: When one repo (e.g. `market`) fails to clone from any server, the `?` operator returns early, skipping the remaining ~87 repos. -- **Fix**: Replace `?` with a `match` that logs the error and continues to the next repo. Only the `discover_repos_from_relays` and `persist_discovered_repos` calls should be fatal (if relay queries fail, nothing works). - -## Checklist - -- [ ] Fix `push_mirror` in `src/git_mirror.rs` — reuse existing `push_target` remote -- [ ] Fix `mirror_cycle` in `src/main.rs` — make per-repo errors non-fatal -- [ ] Rebuild release binary (`cargo build --release`) -- [ ] Push updated source to all 9 GRASP servers (`git push origin master`) -- [ ] Redeploy via Ansible (`ansible-playbook playbooks/30-grasp-mirror.yml`) -- [ ] Verify: `grasp-mirror status` shows repos tracked and sync records -- [ ] Verify: `journalctl -u grasp-mirror` shows successful mirror cycles without `already exists` errors -- [ ] Verify: spot-check `git ls-remote` against a few GRASP servers for repos from all 3 npubs -- [ ] Verify: `https://git.orangesync.tech/api/mirror-health` returns `"status": "ok"` +# grasp-mirror Plan + +## Completed + +- [x] Fix `push_mirror` in `src/git_mirror.rs` — reuse existing `push_target` remote via `remote_set_url` +- [x] Fix `mirror_cycle` in `src/main.rs` — make per-repo errors non-fatal +- [x] Add HTTP health endpoint (`/health`, `/api/mirror-health`) on port 7335 +- [x] Mirror repos from 3 npubs +- [x] Ansible role, playbook, systemd unit, config templates +- [x] Dashboard integration on `services.orangesync.tech` +- [x] Watchdog integration + +## Phase 3: NIP-46 Remote Signing via Amber + +### Problem +GRASP servers reject unauthenticated git pushes. Authorization requires a `kind:30618` state event signed by a maintainer's key. The daemon needs to sign these events but should never hold raw nsec keys. + +### Solution +NIP-46 (Nostr Connect) remote signing via Amber on the user's phone. The daemon acts as a NIP-46 client, sends `sign_event:30618` requests to Amber, and waits for approval before pushing. + +### Architecture +``` +VPS: grasp-mirror ←→ relays ←→ Amber (phone) + - discovers repos - NIP-46 kind:24133 + - builds state events - sign_event:30618 + - queues signing - user approves + - publishes event - returns signature + - git push + +services.orangesync.tech + - pairing UI + QR codes + - status display +``` + +### Config +```toml +[nip46] +relays = ["wss://relay.orangesync.tech", "wss://ngit.orangesync.tech"] +signing_timeout_secs = 604800 # 7 days +``` + +### Checklist + +- [ ] Add `nip46_sessions` and `signing_queue` tables to `src/db.rs` +- [ ] Build `src/nip46.rs` — NIP-46 client with session management + - Generate client keypair per npub, persist to SQLite + - Build `nostrconnect://` URIs with `perms=sign_event:30618` + - Listen for `kind:24133` events on configured relays + - Handle connect/get_public_key/sign_event handshake + - `sign_state_event(npub, state_event) -> Result` method + - Auto-reconnect on session drop +- [ ] Wire NIP-46 into `src/main.rs` daemon startup — connect sessions for all 3 npubs +- [ ] Modify `src/git_mirror.rs` — before push, build `kind:30618` state event, request signature via NIP-46 +- [ ] Add pairing URI + NIP-46 status to health endpoint (`/api/mirror-health`) +- [ ] Update dashboard HTML (`static/services/index.html`) with pairing UI + QR codes +- [ ] Update Ansible config template with `[nip46]` section +- [ ] Add `health_port` to `[storage]` in config template (already done) +- [ ] Rebuild release binary +- [ ] Push to all 9 GRASP servers +- [ ] Redeploy via Ansible +- [ ] End-to-end test: pair npub via Amber, verify signing flow works diff --git a/src/git_mirror.rs b/src/git_mirror.rs index c486887..cdf853e 100644 --- a/src/git_mirror.rs +++ b/src/git_mirror.rs @@ -31,6 +31,7 @@ impl GitMirror { repo: &DiscoveredRepo, target_servers: &[GraspServer], nip46_client: Option<&Arc>, + nostr_client: &nostr_sdk::Client, ) -> Result<()> { if target_servers.is_empty() { tracing::debug!( @@ -80,17 +81,6 @@ impl GitMirror { let relay_url = server.relay_url(); if let Ok(url) = RelayUrl::parse(&relay_url) { let urls = vec![url]; - if let Err(e) = nip46_client - .map_or(Ok(()), |_| { - Err(anyhow::anyhow!("need nostr client to send state event")) - }) - { - let _ = e; - } - - let nostr_client = nostr_sdk::Client::default(); - let _ = nostr_client.add_relay(&relay_url).await; - nostr_client.connect().await; if let Err(e) = nostr_client.send_event_to(urls, event.clone()).await { tracing::warn!( server = %server.domain, diff --git a/src/main.rs b/src/main.rs index 8e1383e..974898f 100644 --- a/src/main.rs +++ b/src/main.rs @@ -231,7 +231,7 @@ async fn mirror_cycle( "mirroring to missing servers" ); - if let Err(e) = mirror.mirror_repo_to_servers(db, repo, &missing, nip46_client).await { + if let Err(e) = mirror.mirror_repo_to_servers(db, repo, &missing, nip46_client, nostr_client).await { tracing::error!(identifier = %repo.identifier, error = %e, "git mirror failed for repo, continuing"); } diff --git a/src/nip46.rs b/src/nip46.rs index bedc814..c3b1e86 100644 --- a/src/nip46.rs +++ b/src/nip46.rs @@ -156,10 +156,10 @@ impl Nip46Client { let db = db.clone(); async move { - if let RelayPoolNotification::Event { event, .. } = notification.as_ref() { + if let RelayPoolNotification::Event { event, .. } = notification { if event.kind == Kind::Custom(24133) { let _ = - Self::handle_response(&sessions, &pending, &db, event).await; + Self::handle_response(&sessions, &pending, &db, &event).await; } } Ok(false) -- cgit v1.2.3