upleb.uk

Public git repos — served from a NIP-34 GRASP relay at git.upleb.uk

summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorYour Name <you@example.com>2026-05-27 15:32:38 +0530
committerYour Name <you@example.com>2026-05-27 15:32:38 +0530
commitd889b890f9e63815c178853ed98a1e31f6cec7f8 (patch)
tree3eda17e8edaf61cc6dba640ae12a24e90c9d8b8c
parentd60fa03de6edae0667a93ac36be4206e76255a2c (diff)
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
-rw-r--r--NIP46-PLAN.md111
-rw-r--r--PLAN.md86
-rw-r--r--src/git_mirror.rs12
-rw-r--r--src/main.rs2
-rw-r--r--src/nip46.rs4
5 files changed, 176 insertions, 39 deletions
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 @@
1# GRASP Mirror NIP-46 Remote Signing — Implementation Plan
2
3## Goal
4
5Build 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.
6
7## Architecture
8
9```
10 ┌─────────────┐
11 │ Amber │
12 │ (phone) │
13 └──────┬──────┘
14 │ NIP-46 (kind:24133)
15 │ NIP-04 encrypted
16 ┌──────┴──────┐
17 │ Nostr Relay │
18 │ (relay/ngit)│
19 └──────┬──────┘
20
21 ┌──────┴──────┐
22 │ grasp-mirror│
23 │ daemon │
24 │ (VPS) │
25 └──────┬──────┘
26 │ git push + kind:30618
27 ┌──────┴──────┐
28 │ GRASP │
29 │ servers │
30 └─────────────┘
31```
32
33- Daemon generates client keypairs per npub, persists in SQLite
34- Produces `nostrconnect://` pairing URIs shown in health endpoint
35- User opens URI in Amber → NIP-46 session established
36- Before git push, daemon builds unsigned `kind:30618` from repo refs
37- Sends `sign_event` request to signer via NIP-46 relay protocol
38- Amber signs on phone → daemon receives signed event → publishes + pushes
39
40## Checklist
41
42### Phase 1: Fix compile errors locally
43
44- [ ] Fix `nip46.rs` — `notification.as_ref()` → match on owned `RelayPoolNotification`
45- [ ] Fix `nip46.rs` — ensure `message.id()` borrow is cloned before consuming message
46- [ ] Fix `git_mirror.rs` — accept shared `nostr_sdk::Client` instead of creating throwaway per server
47- [ ] Fix `main.rs` — pass `nostr_client` to `git_mirror.mirror_repo_to_servers`
48- [ ] Verify `db.rs` — `bool` FromRow works with sqlx 0.8 sqlite INTEGER
49- [ ] Pin Rust 1.95.0 in Ansible role `tasks/main.yml`
50- [ ] Commit and push to GRASP origin
51
52### Phase 2: Build on VPS
53
54- [ ] GRASP server running on target VPS (dependency: migration session)
55- [ ] Push SSH key to VPS for Ansible key-based auth
56- [ ] Run `ansible-playbook playbooks/30-grasp-mirror.yml -v`
57- [ ] Fix any remaining compile errors iteratively
58- [ ] Binary installed at `/usr/local/bin/grasp-mirror`
59- [ ] Systemd service `grasp-mirror` running
60
61### Phase 3: Verify and pair
62
63- [ ] Health endpoint responds: `curl http://localhost:7335/health`
64- [ ] Response includes `nip46` array with session statuses
65- [ ] Each unpaired npub shows `pairing_uri`
66- [ ] Pair npub 1 (`npub12m5...`) via Amber
67- [ ] Pair npub 2 (`npub19jx...`) via Amber
68- [ ] Pair npub 3 (`npub1c03...`) via Amber
69- [ ] Health endpoint shows all 3 sessions `connected: true`
70- [ ] Test signing: daemon builds and signs a `kind:30618` event
71- [ ] Test push: git data pushes to a target GRASP server
72
73### Phase 4: Document roadmap
74
75- [ ] Add QEMU/OpenWRT orchestration roadmap to PLAN.md or PROGRESS.md
76
77## Key Files
78
79| File | Role |
80|---|---|
81| `src/nip46.rs` | NIP-46 client: sessions, NIP-04 encrypt/decrypt, relay listener, `sign_event()` |
82| `src/db.rs` | `nip46_sessions` table + CRUD methods |
83| `src/config.rs` | `Nip46Config` with relays + signing_timeout_secs |
84| `src/git_mirror.rs` | Builds unsigned `kind:30618`, signs via NIP-46, publishes before push |
85| `src/http_health.rs` | NIP-46 session status in `/health` JSON |
86| `src/main.rs` | Wires NIP-46 init + listener into daemon startup |
87| `ansible/roles/grasp_mirror/tasks/main.yml` | Ansible deploy tasks (build + install) |
88| `ansible/roles/grasp_mirror/defaults/main.yml` | NIP-46 relay + timeout vars |
89| `ansible/roles/grasp_mirror/templates/config.toml.j2` | Config with `[nip46]` section |
90
91## Constraints
92
93- No `any` type in TypeScript (N/A — Rust project)
94- No comments in code unless requested
95- Use `pnpm` for JS, `cargo` for Rust
96- Rust 1.95.0 required (nostr-sdk 0.39 doesn't compile on 1.85)
97- NIP-04 encryption for NIP-46 messages (not NIP-44 — simpler, Amber supports it)
98- Signing timeout: 7 days (604800 seconds) — queue-and-wait model
99- GRASP push auth requires signed `kind:30618` by maintainer, not HTTP auth
100
101## Future Roadmap: QEMU/OpenWRT Test Orchestration
102
103After migration, repurpose old VPS (2 vCPU, 8GB RAM, 99GB disk) as dedicated OpenWRT test runner:
104
1051. **Deploy loom-worker** on old VPS — decentralized compute marketplace via Nostr
1062. **Write QEMU execution adapter** for loom — Unix domain socket that spins up OpenWRT instances, runs tests, streams results
1073. **Use existing act-runner** custom pipeline to submit loom jobs from CI
1084. **Do NOT build custom orchestration** — loom already has standardized event kinds (5001/5100/5101), Cashu payments, Blossom result storage, encrypted communication
1095. **Evaluate loom from budabit** (`nostr://npub1hw6amg8p24ne08c9gdq8hhpqx0t0pwanpae9z25crn7m9uy7yarse465gr/relay.ngit.dev/loom-worker`) before building anything custom
110
111Loom'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 @@
1# grasp-mirror Bugfix Plan 1# grasp-mirror Plan
2 2
3## Bugs Identified from VPS Logs 3## Completed
4 4
5### Bug 1: `remote 'push_target' already exists` 5- [x] Fix `push_mirror` in `src/git_mirror.rs` — reuse existing `push_target` remote via `remote_set_url`
6- **Location**: `src/git_mirror.rs:137` 6- [x] Fix `mirror_cycle` in `src/main.rs` — make per-repo errors non-fatal
7- **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)`. 7- [x] Add HTTP health endpoint (`/health`, `/api/mirror-health`) on port 7335
8- **Fix**: Use `repo.find_remote("push_target")` first. If it exists, call `repo.remote_set_url()` to update the URL. If not, create it. 8- [x] Mirror repos from 3 npubs
9 9- [x] Ansible role, playbook, systemd unit, config templates
10### Bug 2: Single repo failure aborts entire cycle 10- [x] Dashboard integration on `services.orangesync.tech`
11- **Location**: `src/main.rs:mirror_cycle()` — the `mirror.mirror_repo_to_servers()` call uses `?` which propagates the error and aborts the loop. 11- [x] Watchdog integration
12- **Cause**: When one repo (e.g. `market`) fails to clone from any server, the `?` operator returns early, skipping the remaining ~87 repos. 12
13- **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). 13## Phase 3: NIP-46 Remote Signing via Amber
14 14
15## Checklist 15### Problem
16 16GRASP 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.
17- [ ] Fix `push_mirror` in `src/git_mirror.rs` — reuse existing `push_target` remote 17
18- [ ] Fix `mirror_cycle` in `src/main.rs` — make per-repo errors non-fatal 18### Solution
19- [ ] Rebuild release binary (`cargo build --release`) 19NIP-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.
20- [ ] Push updated source to all 9 GRASP servers (`git push origin master`) 20
21- [ ] Redeploy via Ansible (`ansible-playbook playbooks/30-grasp-mirror.yml`) 21### Architecture
22- [ ] Verify: `grasp-mirror status` shows repos tracked and sync records 22```
23- [ ] Verify: `journalctl -u grasp-mirror` shows successful mirror cycles without `already exists` errors 23VPS: grasp-mirror ←→ relays ←→ Amber (phone)
24- [ ] Verify: spot-check `git ls-remote` against a few GRASP servers for repos from all 3 npubs 24 - discovers repos - NIP-46 kind:24133
25- [ ] Verify: `https://git.orangesync.tech/api/mirror-health` returns `"status": "ok"` 25 - builds state events - sign_event:30618
26 - queues signing - user approves
27 - publishes event - returns signature
28 - git push
29
30services.orangesync.tech
31 - pairing UI + QR codes
32 - status display
33```
34
35### Config
36```toml
37[nip46]
38relays = ["wss://relay.orangesync.tech", "wss://ngit.orangesync.tech"]
39signing_timeout_secs = 604800 # 7 days
40```
41
42### Checklist
43
44- [ ] Add `nip46_sessions` and `signing_queue` tables to `src/db.rs`
45- [ ] Build `src/nip46.rs` — NIP-46 client with session management
46 - Generate client keypair per npub, persist to SQLite
47 - Build `nostrconnect://` URIs with `perms=sign_event:30618`
48 - Listen for `kind:24133` events on configured relays
49 - Handle connect/get_public_key/sign_event handshake
50 - `sign_state_event(npub, state_event) -> Result<SignedEvent>` method
51 - Auto-reconnect on session drop
52- [ ] Wire NIP-46 into `src/main.rs` daemon startup — connect sessions for all 3 npubs
53- [ ] Modify `src/git_mirror.rs` — before push, build `kind:30618` state event, request signature via NIP-46
54- [ ] Add pairing URI + NIP-46 status to health endpoint (`/api/mirror-health`)
55- [ ] Update dashboard HTML (`static/services/index.html`) with pairing UI + QR codes
56- [ ] Update Ansible config template with `[nip46]` section
57- [ ] Add `health_port` to `[storage]` in config template (already done)
58- [ ] Rebuild release binary
59- [ ] Push to all 9 GRASP servers
60- [ ] Redeploy via Ansible
61- [ ] 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 {
31 repo: &DiscoveredRepo, 31 repo: &DiscoveredRepo,
32 target_servers: &[GraspServer], 32 target_servers: &[GraspServer],
33 nip46_client: Option<&Arc<Nip46Client>>, 33 nip46_client: Option<&Arc<Nip46Client>>,
34 nostr_client: &nostr_sdk::Client,
34 ) -> Result<()> { 35 ) -> Result<()> {
35 if target_servers.is_empty() { 36 if target_servers.is_empty() {
36 tracing::debug!( 37 tracing::debug!(
@@ -80,17 +81,6 @@ impl GitMirror {
80 let relay_url = server.relay_url(); 81 let relay_url = server.relay_url();
81 if let Ok(url) = RelayUrl::parse(&relay_url) { 82 if let Ok(url) = RelayUrl::parse(&relay_url) {
82 let urls = vec![url]; 83 let urls = vec![url];
83 if let Err(e) = nip46_client
84 .map_or(Ok(()), |_| {
85 Err(anyhow::anyhow!("need nostr client to send state event"))
86 })
87 {
88 let _ = e;
89 }
90
91 let nostr_client = nostr_sdk::Client::default();
92 let _ = nostr_client.add_relay(&relay_url).await;
93 nostr_client.connect().await;
94 if let Err(e) = nostr_client.send_event_to(urls, event.clone()).await { 84 if let Err(e) = nostr_client.send_event_to(urls, event.clone()).await {
95 tracing::warn!( 85 tracing::warn!(
96 server = %server.domain, 86 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(
231 "mirroring to missing servers" 231 "mirroring to missing servers"
232 ); 232 );
233 233
234 if let Err(e) = mirror.mirror_repo_to_servers(db, repo, &missing, nip46_client).await { 234 if let Err(e) = mirror.mirror_repo_to_servers(db, repo, &missing, nip46_client, nostr_client).await {
235 tracing::error!(identifier = %repo.identifier, error = %e, "git mirror failed for repo, continuing"); 235 tracing::error!(identifier = %repo.identifier, error = %e, "git mirror failed for repo, continuing");
236 } 236 }
237 237
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 {
156 let db = db.clone(); 156 let db = db.clone();
157 157
158 async move { 158 async move {
159 if let RelayPoolNotification::Event { event, .. } = notification.as_ref() { 159 if let RelayPoolNotification::Event { event, .. } = notification {
160 if event.kind == Kind::Custom(24133) { 160 if event.kind == Kind::Custom(24133) {
161 let _ = 161 let _ =
162 Self::handle_response(&sessions, &pending, &db, event).await; 162 Self::handle_response(&sessions, &pending, &db, &event).await;
163 } 163 }
164 } 164 }
165 Ok(false) 165 Ok(false)