upleb.uk

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

summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.env.example101
-rw-r--r--Cargo.lock1
-rw-r--r--Cargo.toml1
-rw-r--r--README.md74
-rw-r--r--docs/GETTING_STARTED.md437
-rw-r--r--src/config.rs223
-rw-r--r--src/http/landing.rs8
-rw-r--r--src/http/mod.rs4
-rw-r--r--src/http/nip11.rs12
-rw-r--r--src/main.rs18
-rw-r--r--src/nostr/builder.rs15
11 files changed, 361 insertions, 533 deletions
diff --git a/.env.example b/.env.example
index 796415e..0a93b1f 100644
--- a/.env.example
+++ b/.env.example
@@ -1,34 +1,91 @@
1# ngit-grasp Configuration 1# ngit-grasp Configuration
2#
3# Configuration Priority (highest to lowest):
4# 1. CLI flags (e.g., --domain example.com)
5# 2. Environment variables (e.g., NGIT_DOMAIN=example.com)
6# 3. This .env file
7# 4. Built-in defaults
8#
9# Run `ngit-grasp --help` for all CLI options
2 10
3# Domain where this instance is hosted (used in GRASP validation) 11# ============================================================================
4NGIT_DOMAIN=gitnostr.com 12# REQUIRED
13# ============================================================================
5 14
6# Owner's npub (for relay info) 15# Domain where this instance is hosted (required, used in GRASP validation)
7NGIT_OWNER_NPUB=npub1... 16# CLI: --domain <domain>
17# No default - must be set
18# NGIT_DOMAIN=
8 19
9# Relay information (NIP-11) 20# ============================================================================
10NGIT_RELAY_NAME=My GRASP Relay 21# SERVER CONFIGURATION
11NGIT_RELAY_DESCRIPTION=A GRASP-compliant Git relay with Nostr authorization 22# ============================================================================
12 23
13# Storage paths 24# Server bind address (IP:PORT)
14NGIT_GIT_DATA_PATH=./data/git 25# CLI: --bind-address <address>
15NGIT_RELAY_DATA_PATH=./data/relay 26# Default: 127.0.0.1:8080
27# NGIT_BIND_ADDRESS=127.0.0.1:8080
16 28
17# Database backend (memory, nostrdb, lmdb) 29# ============================================================================
18# - memory: In-memory database (default, fastest, no persistence) 30# RELAY INFORMATION (NIP-11)
19# - nostrdb: NostrDB backend (persistent, optimized for Nostr) [Not yet implemented] 31# ============================================================================
20# - lmdb: LMDB backend (persistent, general purpose)
21NGIT_DATABASE_BACKEND=memory
22 32
23# Server configuration 33# Owner's npub (optional, for relay info in NIP-11)
24NGIT_BIND_ADDRESS=127.0.0.1:8080 34# CLI: --owner-npub <npub>
35# Default: (none)
36# NGIT_OWNER_NPUB=npub1...
25 37
26# Logging 38# Relay name shown in NIP-11 information document
27RUST_LOG=info 39# CLI: --relay-name <name>
40# Default: ${domain} grasp relay (e.g., "gitnostr.com grasp relay")
41# NGIT_RELAY_NAME=My GRASP Relay
28 42
29# Optional: Proactive sync settings (GRASP-02) 43# Relay description shown in NIP-11 information document
44# CLI: --relay-description <description>
45# Default: Git Nostr Relay - a grasp implementation
46# NGIT_RELAY_DESCRIPTION="A GRASP-compliant Git relay with Nostr authorization"
47
48# ============================================================================
49# STORAGE
50# ============================================================================
51
52# Path to store Git repositories
53# CLI: --git-data-path <path>
54# Default: ./data/git
55# NGIT_GIT_DATA_PATH=./data/git
56
57# Path to store Nostr relay data
58# CLI: --relay-data-path <path>
59# Default: ./data/relay
60# NGIT_RELAY_DATA_PATH=./data/relay
61
62# Database backend for Nostr events
63# CLI: --database-backend <backend>
64# Options: lmdb, memory, nostrdb
65# Default: lmdb
66# - lmdb: LMDB backend (persistent, general purpose) - RECOMMENDED
67# - memory: In-memory database (fastest, no persistence, uses temp dirs)
68# - nostrdb: NostrDB backend (persistent, Nostr-optimized) [Not yet implemented]
69#
70# Note: When using 'memory' backend, git_data_path and relay_data_path
71# are automatically set to temporary directories for ephemeral testing.
72# NGIT_DATABASE_BACKEND=lmdb
73
74# ============================================================================
75# LOGGING
76# ============================================================================
77
78# Rust log level (not a ngit-grasp config, but useful for debugging)
79# Options: error, warn, info, debug, trace
80# RUST_LOG=info
81
82# ============================================================================
83# FUTURE/PLANNED OPTIONS (not yet implemented)
84# ============================================================================
85
86# Proactive sync settings (GRASP-02)
30# NGIT_PROACTIVE_SYNC_ENABLED=true 87# NGIT_PROACTIVE_SYNC_ENABLED=true
31# NGIT_PROACTIVE_SYNC_INTERVAL_SECS=3600 88# NGIT_PROACTIVE_SYNC_INTERVAL_SECS=3600
32 89
33# Optional: Archive mode (GRASP-05) 90# Archive mode (GRASP-05)
34# NGIT_ARCHIVE_MODE=false 91# NGIT_ARCHIVE_MODE=false \ No newline at end of file
diff --git a/Cargo.lock b/Cargo.lock
index 6fcb65f..2935855 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -1352,6 +1352,7 @@ version = "0.1.0"
1352dependencies = [ 1352dependencies = [
1353 "anyhow", 1353 "anyhow",
1354 "base64 0.22.1", 1354 "base64 0.22.1",
1355 "clap",
1355 "dotenvy", 1356 "dotenvy",
1356 "flate2", 1357 "flate2",
1357 "futures-util", 1358 "futures-util",
diff --git a/Cargo.toml b/Cargo.toml
index 02c66ac..a42125d 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -38,6 +38,7 @@ tracing-subscriber = { version = "0.3", features = ["env-filter"] }
38 38
39# Configuration 39# Configuration
40dotenvy = "0.15" 40dotenvy = "0.15"
41clap = { version = "4.5", features = ["derive", "env"] }
41 42
42# Error handling 43# Error handling
43anyhow = "1.0" 44anyhow = "1.0"
diff --git a/README.md b/README.md
index d2b2629..7dede9b 100644
--- a/README.md
+++ b/README.md
@@ -100,15 +100,71 @@ nix develop -c cargo test --lib
100 100
101## Configuration 101## Configuration
102 102
103Environment variables (see `.env.example`): 103Configuration is loaded with the following priority (highest to lowest):
104 104
105- `NGIT_DOMAIN`: Your domain (e.g., `gitnostr.com`) 1051. **CLI flags** (e.g., `--domain example.com`)
106- `NGIT_OWNER_NPUB`: Relay owner's npub 1062. **Environment variables** (e.g., `NGIT_DOMAIN=example.com`)
107- `NGIT_RELAY_NAME`: Relay name for NIP-11 1073. **.env file** (loaded automatically if present)
108- `NGIT_RELAY_DESCRIPTION`: Relay description 1084. **Built-in defaults**
109- `NGIT_GIT_DATA_PATH`: Path to store Git repositories 109
110- `NGIT_RELAY_DATA_PATH`: Path to store Nostr events 110This means CLI flags always take precedence over environment variables, which take precedence over `.env` file values.
111- `NGIT_BIND_ADDRESS`: Server bind address (default: `127.0.0.1:8080`) 111
112### CLI Usage
113
114```bash
115# View all options with defaults
116ngit-grasp --help
117
118# Run with CLI flags (override everything else)
119ngit-grasp --domain relay.example.com --owner-npub npub1... --bind-address 0.0.0.0:8080
120
121# Mix CLI flags with environment variables
122NGIT_OWNER_NPUB=npub1... ngit-grasp --domain relay.example.com
123```
124
125### Configuration Options
126
127| Option | CLI Flag | Environment Variable | Default |
128| ----------------- | --------------------- | ------------------------ | -------------------------------------------- |
129| Domain | `--domain` | `NGIT_DOMAIN` | (required) |
130| Owner npub | `--owner-npub` | `NGIT_OWNER_NPUB` | (optional) |
131| Relay name | `--relay-name` | `NGIT_RELAY_NAME` | `${domain} grasp relay` |
132| Relay description | `--relay-description` | `NGIT_RELAY_DESCRIPTION` | `Git Nostr Relay - a grasp implementation` |
133| Git data path | `--git-data-path` | `NGIT_GIT_DATA_PATH` | `./data/git` (temp dir for memory backend) |
134| Relay data path | `--relay-data-path` | `NGIT_RELAY_DATA_PATH` | `./data/relay` (temp dir for memory backend) |
135| Bind address | `--bind-address` | `NGIT_BIND_ADDRESS` | `127.0.0.1:8080` |
136| Database backend | `--database-backend` | `NGIT_DATABASE_BACKEND` | `lmdb` |
137
138### Database Backends
139
140- `lmdb`: LMDB backend (default, persistent, general purpose)
141- `memory`: In-memory database (fastest, no persistence - uses temp directories)
142- `nostrdb`: NostrDB backend (persistent, optimized for Nostr) [Not yet implemented]
143
144> **Note:** When using the `memory` backend, git data are automatically stored in temporary directories for ephemeral testing. This is useful for development and CI/CD pipelines.
145
146### Example: Production Deployment
147
148```bash
149# Using environment variables (recommended for production)
150export NGIT_DOMAIN=gitnostr.com
151export NGIT_OWNER_NPUB=npub1...
152export NGIT_BIND_ADDRESS=0.0.0.0:8080
153export NGIT_DATABASE_BACKEND=lmdb
154ngit-grasp
155```
156
157### Example: Development
158
159```bash
160# Using .env file
161cp .env.example .env
162# Edit .env with your settings
163ngit-grasp
164
165# Or override specific values with CLI flags
166ngit-grasp --domain localhost:3000 --bind-address 127.0.0.1:3000
167```
112 168
113## Documentation 169## Documentation
114 170
diff --git a/docs/GETTING_STARTED.md b/docs/GETTING_STARTED.md
deleted file mode 100644
index 7fea590..0000000
--- a/docs/GETTING_STARTED.md
+++ /dev/null
@@ -1,437 +0,0 @@
1# Getting Started with Implementation
2
3This guide helps you start implementing ngit-grasp based on the architecture design.
4
5## Prerequisites
6
7- Rust 1.75 or later
8- Git 2.x
9- Basic understanding of async Rust (tokio)
10- Familiarity with actix-web (helpful)
11- Understanding of Nostr basics (helpful)
12
13## Step 1: Initialize Cargo Project
14
15```bash
16# Create new binary project
17cargo init --name ngit-grasp
18
19# Or if already created:
20cargo build
21```
22
23## Step 2: Add Dependencies
24
25Edit `Cargo.toml`:
26
27```toml
28[package]
29name = "ngit-grasp"
30version = "0.1.0"
31edition = "2021"
32rust-version = "1.75"
33
34[dependencies]
35# HTTP Server
36actix-web = "4"
37actix-cors = "0.7"
38
39# Async Runtime
40tokio = { version = "1", features = ["full"] }
41
42# Git Protocol
43git-http-backend = "0.1.3"
44
45# Nostr
46nostr-sdk = { version = "0.43", features = ["all-nips"] }
47nostr-relay-builder = "0.43"
48
49# Serialization
50serde = { version = "1", features = ["derive"] }
51serde_json = "1"
52
53# Error Handling
54anyhow = "1"
55thiserror = "1"
56
57# Logging
58tracing = "0.1"
59tracing-subscriber = { version = "0.3", features = ["env-filter"] }
60
61# Environment
62dotenv = "0.15"
63
64# Utilities
65async-trait = "0.1"
66futures = "0.3"
67bytes = "1"
68
69[dev-dependencies]
70tokio-test = "0.4"
71```
72
73## Step 3: Project Structure
74
75Create the directory structure:
76
77```bash
78mkdir -p src/{git,nostr,storage}
79mkdir -p tests/{integration,fixtures}
80mkdir -p data/{git,relay}
81```
82
83## Step 4: Configuration Module
84
85Create `src/config.rs`:
86
87```rust
88use anyhow::Result;
89use std::env;
90use std::net::SocketAddr;
91use std::path::PathBuf;
92
93#[derive(Debug, Clone)]
94pub struct Config {
95 pub domain: String,
96 pub owner_npub: String,
97 pub relay_name: String,
98 pub relay_description: String,
99 pub git_data_path: PathBuf,
100 pub relay_data_path: PathBuf,
101 pub bind_address: SocketAddr,
102}
103
104impl Config {
105 pub fn from_env() -> Result<Self> {
106 dotenv::dotenv().ok();
107
108 Ok(Config {
109 domain: env::var("NGIT_DOMAIN")?,
110 owner_npub: env::var("NGIT_OWNER_NPUB")?,
111 relay_name: env::var("NGIT_RELAY_NAME")?,
112 relay_description: env::var("NGIT_RELAY_DESCRIPTION")?,
113 git_data_path: PathBuf::from(
114 env::var("NGIT_GIT_DATA_PATH")
115 .unwrap_or_else(|_| "./data/git".to_string())
116 ),
117 relay_data_path: PathBuf::from(
118 env::var("NGIT_RELAY_DATA_PATH")
119 .unwrap_or_else(|_| "./data/relay".to_string())
120 ),
121 bind_address: env::var("NGIT_BIND_ADDRESS")
122 .unwrap_or_else(|_| "127.0.0.1:8080".to_string())
123 .parse()?,
124 })
125 }
126}
127```
128
129## Step 5: Core Types
130
131Create `src/git/types.rs`:
132
133```rust
134use serde::{Deserialize, Serialize};
135
136#[derive(Debug, Clone, Serialize, Deserialize)]
137pub struct RefUpdate {
138 pub old_oid: String,
139 pub new_oid: String,
140 pub ref_name: String,
141}
142
143impl RefUpdate {
144 pub fn is_create(&self) -> bool {
145 self.old_oid == "0000000000000000000000000000000000000000"
146 }
147
148 pub fn is_delete(&self) -> bool {
149 self.new_oid == "0000000000000000000000000000000000000000"
150 }
151
152 pub fn is_update(&self) -> bool {
153 !self.is_create() && !self.is_delete()
154 }
155}
156
157#[derive(Debug, thiserror::Error)]
158pub enum GitError {
159 #[error("Invalid pkt-line format")]
160 InvalidPktLine,
161
162 #[error("Invalid ref update format")]
163 InvalidRefUpdate,
164
165 #[error("Repository not found: {0}")]
166 RepositoryNotFound(String),
167
168 #[error("Invalid repository path")]
169 InvalidPath,
170}
171```
172
173## Step 6: Main Application State
174
175Create `src/main.rs`:
176
177```rust
178use actix_web::{web, App, HttpServer};
179use anyhow::Result;
180use std::sync::Arc;
181use tracing::info;
182
183mod config;
184mod git;
185mod nostr;
186mod storage;
187
188use config::Config;
189
190#[derive(Clone)]
191pub struct AppState {
192 pub config: Arc<Config>,
193 // TODO: Add NostrClient, RepositoryManager, etc.
194}
195
196#[actix_web::main]
197async fn main() -> Result<()> {
198 // Initialize logging
199 tracing_subscriber::fmt()
200 .with_env_filter(
201 tracing_subscriber::EnvFilter::from_default_env()
202 )
203 .init();
204
205 // Load configuration
206 let config = Config::from_env()?;
207 info!("Starting ngit-grasp on {}", config.bind_address);
208
209 // Create application state
210 let state = AppState {
211 config: Arc::new(config.clone()),
212 };
213
214 // Start HTTP server
215 HttpServer::new(move || {
216 App::new()
217 .app_data(web::Data::new(state.clone()))
218 .configure(git::routes::configure)
219 .configure(nostr::routes::configure)
220 })
221 .bind(config.bind_address)?
222 .run()
223 .await?;
224
225 Ok(())
226}
227```
228
229## Step 7: Git Module Skeleton
230
231Create `src/git/mod.rs`:
232
233```rust
234pub mod routes;
235pub mod handler;
236pub mod parser;
237pub mod authorization;
238pub mod types;
239
240pub use types::{RefUpdate, GitError};
241```
242
243Create `src/git/routes.rs`:
244
245```rust
246use actix_web::web;
247
248pub fn configure(cfg: &mut web::ServiceConfig) {
249 cfg.service(
250 web::scope("/{npub}/{identifier}.git")
251 .route("/info/refs", web::get().to(super::handler::info_refs))
252 .route("/git-upload-pack", web::post().to(super::handler::git_upload_pack))
253 .route("/git-receive-pack", web::post().to(super::handler::git_receive_pack))
254 );
255}
256```
257
258## Step 8: First Test
259
260Create `tests/integration/basic_test.rs`:
261
262```rust
263use actix_web::{test, App};
264
265#[actix_web::test]
266async fn test_server_starts() {
267 // TODO: Initialize test app
268 // TODO: Make test request
269 assert!(true);
270}
271```
272
273Run tests:
274
275```bash
276cargo test
277```
278
279## Step 9: Implementation Order
280
281Follow this order for implementation:
282
283### Phase 1: Basic Infrastructure (Week 1)
2841. ✅ Config module
2852. ✅ Main server setup
2863. ✅ Core types
2874. ⏭️ Git pkt-line parser
2885. ⏭️ Ref update parser
2896. ⏭️ Parser tests
290
291### Phase 2: Git Protocol (Week 2)
2921. ⏭️ Git upload-pack handler (read-only)
2932. ⏭️ Repository manager
2943. ⏭️ Path validation and security
2954. ⏭️ Integration tests for cloning
296
297### Phase 3: Nostr Relay (Week 2-3)
2981. ⏭️ Nostr relay setup with nostr-relay-builder
2992. ⏭️ Repository announcement policy
3003. ⏭️ Event hooks for repo creation
3014. ⏭️ NIP-11 configuration
302
303### Phase 4: Authorization (Week 3-4)
3041. ⏭️ Maintainer resolution logic
3052. ⏭️ State validation logic
3063. ⏭️ Git receive-pack with inline validation
3074. ⏭️ Integration tests for pushing
308
309### Phase 5: Polish (Week 4-6)
3101. ⏭️ Error handling improvements
3112. ⏭️ Logging and observability
3123. ⏭️ Performance optimization
3134. ⏭️ GRASP-01 compliance testing
3145. ⏭️ Documentation updates
315
316## Development Workflow
317
318### Running Locally
319
320```bash
321# Copy environment template
322cp .env.example .env
323
324# Edit configuration
325vim .env
326
327# Run in development mode
328cargo run
329
330# With debug logging
331RUST_LOG=debug cargo run
332```
333
334### Testing
335
336```bash
337# Run all tests
338cargo test
339
340# Run with output
341cargo test -- --nocapture
342
343# Run specific test
344cargo test test_parse_ref_updates
345
346# Run integration tests only
347cargo test --test '*'
348```
349
350### Code Quality
351
352```bash
353# Format code
354cargo fmt
355
356# Check formatting
357cargo fmt --check
358
359# Lint
360cargo clippy
361
362# Lint with all features
363cargo clippy --all-features -- -D warnings
364```
365
366## Debugging Tips
367
368### Enable Detailed Logging
369
370```bash
371RUST_LOG=trace cargo run
372```
373
374### Test with Real Git Client
375
376```bash
377# In another terminal, after server is running
378mkdir test-repo && cd test-repo
379git init
380echo "test" > README.md
381git add . && git commit -m "test"
382
383# Try to push (will fail without Nostr setup)
384git remote add origin http://localhost:8080/npub.../test.git
385git push origin main
386```
387
388### Use curl for HTTP Testing
389
390```bash
391# Test info/refs endpoint
392curl -v http://localhost:8080/npub.../test.git/info/refs?service=git-upload-pack
393```
394
395## Common Issues
396
397### "Repository not found"
398- Check that repository announcement was sent to Nostr relay
399- Verify repository was created in git_data_path
400- Check logs for repo creation
401
402### "Push rejected"
403- Verify state event exists on relay
404- Check state event matches push refs
405- Verify maintainer list includes pusher
406
407### "Cannot connect to relay"
408- Check relay is running
409- Verify WebSocket endpoint
410- Check firewall/network settings
411
412## Next Steps
413
414After basic setup:
415
4161. Implement pkt-line parser (see [GIT_PROTOCOL.md](GIT_PROTOCOL.md))
4172. Add comprehensive tests
4183. Implement Nostr relay policies
4194. Add authorization logic
4205. Test with ngit CLI
421
422## Resources
423
424- [ARCHITECTURE.md](ARCHITECTURE.md) - Detailed design
425- [GIT_PROTOCOL.md](GIT_PROTOCOL.md) - Git protocol reference
426- [actix-web docs](https://actix.rs/docs/)
427- [nostr-sdk docs](https://docs.rs/nostr-sdk/)
428- [tokio docs](https://docs.rs/tokio/)
429
430## Getting Help
431
432- Check existing documentation in `docs/`
433- Review reference implementation at `../ngit-relay`
434- Open an issue for questions
435- Read GRASP protocol spec
436
437Good luck! 🚀
diff --git a/src/config.rs b/src/config.rs
index 9b0d0b8..d095178 100644
--- a/src/config.rs
+++ b/src/config.rs
@@ -1,74 +1,205 @@
1use anyhow::{Context, Result}; 1use anyhow::Result;
2use clap::{Parser, ValueEnum};
2use serde::{Deserialize, Serialize}; 3use serde::{Deserialize, Serialize};
3use std::env;
4 4
5/// Database backend type for the relay 5/// Database backend type for the relay
6#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] 6#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Default, ValueEnum)]
7#[serde(rename_all = "lowercase")] 7#[serde(rename_all = "lowercase")]
8#[derive(Default)]
9pub enum DatabaseBackend { 8pub enum DatabaseBackend {
10 /// In-memory database (default, fastest, no persistence) 9 /// LMDB backend (persistent, general purpose)
11 #[default] 10 #[default]
12 Memory, 11 Lmdb,
13 /// NostrDB backend (persistent, optimized for Nostr) 12 /// NostrDB backend (persistent, optimized for Nostr)
14 NostrDb, 13 NostrDb,
15 /// LMDB backend (persistent, general purpose) 14 /// In-memory database (fastest, no persistence - uses temp directory for git data)
16 Lmdb, 15 Memory,
17} 16}
18 17
19impl std::str::FromStr for DatabaseBackend { 18impl std::fmt::Display for DatabaseBackend {
20 type Err = anyhow::Error; 19 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
21 20 match self {
22 fn from_str(s: &str) -> Result<Self> { 21 Self::Memory => write!(f, "memory"),
23 match s.to_lowercase().as_str() { 22 Self::NostrDb => write!(f, "nostrdb"),
24 "memory" => Ok(Self::Memory), 23 Self::Lmdb => write!(f, "lmdb"),
25 "nostrdb" => Ok(Self::NostrDb),
26 "lmdb" => Ok(Self::Lmdb),
27 _ => Err(anyhow::anyhow!(
28 "Invalid database backend: {}. Valid options: memory, nostrdb, lmdb",
29 s
30 )),
31 } 24 }
32 } 25 }
33} 26}
34 27
35#[derive(Debug, Clone, Serialize, Deserialize)] 28/// ngit-grasp - A GRASP (Git Relays Authorized via Signed-Nostr Proofs) implementation
29///
30/// Configuration is loaded with the following priority (highest to lowest):
31/// 1. CLI flags (e.g., --domain example.com)
32/// 2. Environment variables (e.g., NGIT_DOMAIN=example.com)
33/// 3. .env file (loaded automatically if present)
34/// 4. Built-in defaults
35#[derive(Debug, Clone, Serialize, Deserialize, Parser)]
36#[command(author, version, about, long_about = None)]
37#[command(propagate_version = true)]
36pub struct Config { 38pub struct Config {
39 /// Domain where this instance is hosted (required, used in GRASP validation)
40 #[arg(long, env = "NGIT_DOMAIN")]
37 pub domain: String, 41 pub domain: String,
38 pub owner_npub: String, 42
39 pub relay_name: String, 43 /// Owner's npub (optional, for relay info in NIP-11)
44 #[arg(long, env = "NGIT_OWNER_NPUB")]
45 pub owner_npub: Option<String>,
46
47 /// Relay name for NIP-11 information document (defaults to "${domain} grasp relay")
48 #[arg(long = "relay-name", env = "NGIT_RELAY_NAME")]
49 pub relay_name_override: Option<String>,
50
51 /// Relay description for NIP-11 information document
52 #[arg(
53 long,
54 env = "NGIT_RELAY_DESCRIPTION",
55 default_value = "Git Nostr Relay - a grasp implementation"
56 )]
40 pub relay_description: String, 57 pub relay_description: String,
58
59 /// Path to store Git repositories
60 #[arg(long, env = "NGIT_GIT_DATA_PATH", default_value = "./data/git")]
41 pub git_data_path: String, 61 pub git_data_path: String,
62
63 /// Path to store Nostr relay data
64 #[arg(long, env = "NGIT_RELAY_DATA_PATH", default_value = "./data/relay")]
42 pub relay_data_path: String, 65 pub relay_data_path: String,
66
67 /// Server bind address (IP:PORT)
68 #[arg(long, env = "NGIT_BIND_ADDRESS", default_value = "127.0.0.1:8080")]
43 pub bind_address: String, 69 pub bind_address: String,
70
71 /// Database backend type
72 #[arg(long, env = "NGIT_DATABASE_BACKEND", value_enum, default_value_t = DatabaseBackend::Lmdb)]
44 pub database_backend: DatabaseBackend, 73 pub database_backend: DatabaseBackend,
45} 74}
46 75
47impl Config { 76impl Config {
48 pub fn from_env() -> Result<Self> { 77 /// Load configuration from CLI args, environment variables, and defaults.
49 // Load .env file if present 78 ///
79 /// Priority (highest to lowest):
80 /// 1. CLI flags
81 /// 2. Environment variables
82 /// 3. .env file
83 /// 4. Built-in defaults
84 pub fn load() -> Result<Self> {
85 // Load .env file if present (before clap parses, so env vars are available)
50 dotenvy::dotenv().ok(); 86 dotenvy::dotenv().ok();
51 87
52 // Parse database backend from environment 88 // Parse CLI args (clap automatically handles env var fallback)
53 let database_backend = env::var("NGIT_DATABASE_BACKEND") 89 let config = Self::parse();
54 .ok() 90
55 .and_then(|s| s.parse().ok()) 91 Ok(config)
56 .unwrap_or_default(); 92 }
57 93
58 Ok(Config { 94 /// Get relay name (defaults to "${domain} grasp relay" if not set)
59 domain: env::var("NGIT_DOMAIN").unwrap_or_else(|_| "localhost:8080".to_string()), 95 pub fn relay_name(&self) -> String {
60 owner_npub: env::var("NGIT_OWNER_NPUB").context("NGIT_OWNER_NPUB must be set")?, 96 self.relay_name_override
61 relay_name: env::var("NGIT_RELAY_NAME") 97 .clone()
62 .unwrap_or_else(|_| "ngit-grasp relay".to_string()), 98 .unwrap_or_else(|| format!("{} grasp relay", self.domain))
63 relay_description: env::var("NGIT_RELAY_DESCRIPTION") 99 }
64 .unwrap_or_else(|_| "A GRASP-compliant Nostr relay for Git".to_string()), 100
65 git_data_path: env::var("NGIT_GIT_DATA_PATH") 101 /// Get effective git data path
66 .unwrap_or_else(|_| "./data/git".to_string()), 102 /// Returns a temp directory when using memory backend, otherwise the configured path
67 relay_data_path: env::var("NGIT_RELAY_DATA_PATH") 103 pub fn effective_git_data_path(&self) -> String {
68 .unwrap_or_else(|_| "./data/relay".to_string()), 104 if self.database_backend == DatabaseBackend::Memory {
69 bind_address: env::var("NGIT_BIND_ADDRESS") 105 std::env::temp_dir()
70 .unwrap_or_else(|_| "127.0.0.1:8080".to_string()), 106 .join("ngit-grasp-git")
71 database_backend, 107 .to_string_lossy()
72 }) 108 .into_owned()
109 } else {
110 self.git_data_path.clone()
111 }
112 }
113
114 /// Create config for testing
115 #[cfg(test)]
116 pub fn for_testing() -> Self {
117 Self {
118 domain: "localhost:8080".to_string(),
119 owner_npub: Some("npub1test".to_string()),
120 relay_name_override: Some("test relay".to_string()),
121 relay_description: "test description".to_string(),
122 git_data_path: "./test_data/git".to_string(),
123 relay_data_path: "./test_data/relay".to_string(),
124 bind_address: "127.0.0.1:8080".to_string(),
125 database_backend: DatabaseBackend::Memory,
126 }
127 }
128}
129
130#[cfg(test)]
131mod tests {
132 use super::*;
133
134 #[test]
135 fn test_default_values() {
136 let config = Config::for_testing();
137 assert_eq!(config.domain, "localhost:8080");
138 assert_eq!(config.bind_address, "127.0.0.1:8080");
139 // for_testing() uses Memory, but the actual default is Lmdb
140 assert_eq!(config.database_backend, DatabaseBackend::Memory);
141 }
142
143 #[test]
144 fn test_lmdb_is_default() {
145 // Verify the actual default via the enum's Default trait
146 assert_eq!(DatabaseBackend::default(), DatabaseBackend::Lmdb);
147 }
148
149 #[test]
150 fn test_memory_backend_uses_temp_dir() {
151 let config = Config {
152 database_backend: DatabaseBackend::Memory,
153 ..Config::for_testing()
154 };
155 let git_path = config.effective_git_data_path();
156 assert!(git_path.contains("ngit-grasp-git"));
157 }
158
159 #[test]
160 fn test_lmdb_backend_uses_configured_path() {
161 let config = Config {
162 database_backend: DatabaseBackend::Lmdb,
163 git_data_path: "./my/git/path".to_string(),
164 relay_data_path: "./my/relay/path".to_string(),
165 ..Config::for_testing()
166 };
167 assert_eq!(config.effective_git_data_path(), "./my/git/path");
168 }
169
170 #[test]
171 fn test_database_backend_display() {
172 assert_eq!(DatabaseBackend::Memory.to_string(), "memory");
173 assert_eq!(DatabaseBackend::NostrDb.to_string(), "nostrdb");
174 assert_eq!(DatabaseBackend::Lmdb.to_string(), "lmdb");
175 }
176
177 #[test]
178 fn test_relay_name_default() {
179 let config = Config {
180 domain: "example.com".to_string(),
181 relay_name_override: None,
182 ..Config::for_testing()
183 };
184 assert_eq!(config.relay_name(), "example.com grasp relay");
185 }
186
187 #[test]
188 fn test_relay_name_override() {
189 let config = Config {
190 domain: "example.com".to_string(),
191 relay_name_override: Some("My Custom Relay".to_string()),
192 ..Config::for_testing()
193 };
194 assert_eq!(config.relay_name(), "My Custom Relay");
195 }
196
197 #[test]
198 fn test_owner_npub_optional() {
199 let config = Config {
200 owner_npub: None,
201 ..Config::for_testing()
202 };
203 assert!(config.owner_npub.is_none());
73 } 204 }
74} 205}
diff --git a/src/http/landing.rs b/src/http/landing.rs
index b978851..f9fca5b 100644
--- a/src/http/landing.rs
+++ b/src/http/landing.rs
@@ -282,7 +282,7 @@ pub fn get_html(config: &Config) -> String {
282 format!( 282 format!(
283 include_str!("../../templates/landing.html"), 283 include_str!("../../templates/landing.html"),
284 base_css = get_base_css(), 284 base_css = get_base_css(),
285 relay_name = config.relay_name, 285 relay_name = config.relay_name(),
286 relay_description = config.relay_description, 286 relay_description = config.relay_description,
287 version = get_version(), 287 version = get_version(),
288 curation = curation, 288 curation = curation,
@@ -357,7 +357,7 @@ pub fn get_generic_404_html(config: &Config, path: &str) -> String {
357</body> 357</body>
358</html>"##, 358</html>"##,
359 base_css = get_base_css(), 359 base_css = get_base_css(),
360 relay_name = config.relay_name, 360 relay_name = config.relay_name(),
361 path = path, 361 path = path,
362 version = get_version(), 362 version = get_version(),
363 footer_script = get_footer_script(), 363 footer_script = get_footer_script(),
@@ -456,7 +456,7 @@ pub fn get_404_html(config: &Config, npub: &str, identifier: &str) -> String {
456</body> 456</body>
457</html>"##, 457</html>"##,
458 base_css = get_base_css(), 458 base_css = get_base_css(),
459 relay_name = config.relay_name, 459 relay_name = config.relay_name(),
460 npub = npub, 460 npub = npub,
461 identifier = identifier, 461 identifier = identifier,
462 version = get_version(), 462 version = get_version(),
@@ -598,7 +598,7 @@ pub fn get_repo_html(config: &Config, npub: &str, identifier: &str) -> String {
598</body> 598</body>
599</html>"##, 599</html>"##,
600 base_css = get_base_css(), 600 base_css = get_base_css(),
601 relay_name = config.relay_name, 601 relay_name = config.relay_name(),
602 npub = npub, 602 npub = npub,
603 identifier = identifier, 603 identifier = identifier,
604 version = get_version(), 604 version = get_version(),
diff --git a/src/http/mod.rs b/src/http/mod.rs
index 4665281..8b1f687 100644
--- a/src/http/mod.rs
+++ b/src/http/mod.rs
@@ -118,7 +118,7 @@ impl Service<Request<Incoming>> for HttpService {
118 let path = req.uri().path().to_string(); 118 let path = req.uri().path().to_string();
119 let query = req.uri().query().map(|s| s.to_string()); 119 let query = req.uri().query().map(|s| s.to_string());
120 let method = req.method().clone(); 120 let method = req.method().clone();
121 let git_data_path = self.config.git_data_path.clone(); 121 let git_data_path = self.config.effective_git_data_path();
122 let database = self.database.clone(); 122 let database = self.database.clone();
123 123
124 // Handle OPTIONS preflight requests (CORS) 124 // Handle OPTIONS preflight requests (CORS)
@@ -427,7 +427,7 @@ pub async fn run_server(
427 let bind_addr: SocketAddr = config.bind_address.parse()?; 427 let bind_addr: SocketAddr = config.bind_address.parse()?;
428 428
429 tracing::info!("Starting HTTP server on {}", bind_addr); 429 tracing::info!("Starting HTTP server on {}", bind_addr);
430 tracing::info!("Relay name: {}", config.relay_name); 430 tracing::info!("Relay name: {}", config.relay_name());
431 tracing::info!("Domain: {}", config.domain); 431 tracing::info!("Domain: {}", config.domain);
432 432
433 let listener = TcpListener::bind(&bind_addr).await?; 433 let listener = TcpListener::bind(&bind_addr).await?;
diff --git a/src/http/nip11.rs b/src/http/nip11.rs
index ecb9769..e6a1e46 100644
--- a/src/http/nip11.rs
+++ b/src/http/nip11.rs
@@ -57,9 +57,9 @@ impl RelayInformationDocument {
57 /// Create NIP-11 relay information document from configuration 57 /// Create NIP-11 relay information document from configuration
58 pub fn from_config(config: &Config) -> Self { 58 pub fn from_config(config: &Config) -> Self {
59 Self { 59 Self {
60 name: config.relay_name.clone(), 60 name: config.relay_name(),
61 description: config.relay_description.clone(), 61 description: config.relay_description.clone(),
62 pubkey: Some(config.owner_npub.clone()), 62 pubkey: config.owner_npub.clone(),
63 contact: None, // Could be added to config if needed 63 contact: None, // Could be added to config if needed
64 supported_nips: vec![ 64 supported_nips: vec![
65 1, // NIP-01: Basic protocol flow 65 1, // NIP-01: Basic protocol flow
@@ -98,8 +98,8 @@ mod tests {
98 fn test_relay_information_document_structure() { 98 fn test_relay_information_document_structure() {
99 let config = Config { 99 let config = Config {
100 domain: "relay.example.com".to_string(), 100 domain: "relay.example.com".to_string(),
101 owner_npub: "npub1test".to_string(), 101 owner_npub: Some("npub1test".to_string()),
102 relay_name: "Test Relay".to_string(), 102 relay_name_override: Some("Test Relay".to_string()),
103 relay_description: "A test relay".to_string(), 103 relay_description: "A test relay".to_string(),
104 git_data_path: "./data/git".to_string(), 104 git_data_path: "./data/git".to_string(),
105 relay_data_path: "./data/relay".to_string(), 105 relay_data_path: "./data/relay".to_string(),
@@ -128,8 +128,8 @@ mod tests {
128 fn test_relay_information_document_json() { 128 fn test_relay_information_document_json() {
129 let config = Config { 129 let config = Config {
130 domain: "relay.example.com".to_string(), 130 domain: "relay.example.com".to_string(),
131 owner_npub: "npub1test".to_string(), 131 owner_npub: Some("npub1test".to_string()),
132 relay_name: "Test Relay".to_string(), 132 relay_name_override: Some("Test Relay".to_string()),
133 relay_description: "A test relay".to_string(), 133 relay_description: "A test relay".to_string(),
134 git_data_path: "./data/git".to_string(), 134 git_data_path: "./data/git".to_string(),
135 relay_data_path: "./data/relay".to_string(), 135 relay_data_path: "./data/relay".to_string(),
diff --git a/src/main.rs b/src/main.rs
index 1f18ab2..f80e920 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -2,7 +2,10 @@ use anyhow::Result;
2use tracing::{info, Level}; 2use tracing::{info, Level};
3use tracing_subscriber::FmtSubscriber; 3use tracing_subscriber::FmtSubscriber;
4 4
5use ngit_grasp::{config::Config, http, nostr}; 5use ngit_grasp::{
6 config::{Config, DatabaseBackend},
7 http, nostr,
8};
6 9
7#[tokio::main] 10#[tokio::main]
8async fn main() -> Result<()> { 11async fn main() -> Result<()> {
@@ -14,10 +17,17 @@ async fn main() -> Result<()> {
14 17
15 info!("Starting ngit-grasp with nostr-relay-builder..."); 18 info!("Starting ngit-grasp with nostr-relay-builder...");
16 19
17 // Load configuration 20 // Load configuration (priority: CLI flags > env vars > .env file > defaults)
18 let config = Config::from_env()?; 21 let config = Config::load()?;
22
19 info!("Configuration loaded: {}", config.bind_address); 23 info!("Configuration loaded: {}", config.bind_address);
20 info!("Git data directory: {}", config.git_data_path); 24 info!("Domain: {}", config.domain);
25 info!("Relay name: {}", config.relay_name());
26 info!("Git data directory: {}", config.effective_git_data_path());
27 if config.database_backend != DatabaseBackend::Memory {
28 info!("Relay data directory: {}", config.relay_data_path);
29 }
30 info!("Database backend: {}", config.database_backend);
21 31
22 // Create Nostr relay with NIP-34 validation 32 // Create Nostr relay with NIP-34 validation
23 // Returns both the relay and database for direct queries in handlers 33 // Returns both the relay and database for direct queries in handlers
diff --git a/src/nostr/builder.rs b/src/nostr/builder.rs
index eabb38f..904cba4 100644
--- a/src/nostr/builder.rs
+++ b/src/nostr/builder.rs
@@ -1203,22 +1203,31 @@ pub fn create_relay(config: &Config) -> Result<RelayWithDatabase> {
1203 tracing::info!("Using LMDB backend at: {}", db_path.display()); 1203 tracing::info!("Using LMDB backend at: {}", db_path.display());
1204 // Ensure the database directory exists 1204 // Ensure the database directory exists
1205 std::fs::create_dir_all(db_path).map_err(|e| { 1205 std::fs::create_dir_all(db_path).map_err(|e| {
1206 anyhow::anyhow!("Failed to create LMDB directory {}: {}", db_path.display(), e) 1206 anyhow::anyhow!(
1207 "Failed to create LMDB directory {}: {}",
1208 db_path.display(),
1209 e
1210 )
1207 })?; 1211 })?;
1208 Arc::new(NostrLMDB::open(db_path).map_err(|e| { 1212 Arc::new(NostrLMDB::open(db_path).map_err(|e| {
1209 anyhow::anyhow!("Failed to open LMDB database at {}: {}", db_path.display(), e) 1213 anyhow::anyhow!(
1214 "Failed to open LMDB database at {}: {}",
1215 db_path.display(),
1216 e
1217 )
1210 })?) 1218 })?)
1211 } 1219 }
1212 }; 1220 };
1213 1221
1214 // Build relay with GRASP-01 validation 1222 // Build relay with GRASP-01 validation
1215 // Clone Arc for the write policy so both relay and policy can access the database 1223 // Clone Arc for the write policy so both relay and policy can access the database
1224 let git_data_path = config.effective_git_data_path();
1216 let builder = RelayBuilder::default() 1225 let builder = RelayBuilder::default()
1217 .database(database.clone()) 1226 .database(database.clone())
1218 .write_policy(Nip34WritePolicy::new( 1227 .write_policy(Nip34WritePolicy::new(
1219 &config.domain, 1228 &config.domain,
1220 database.clone(), 1229 database.clone(),
1221 &config.git_data_path, 1230 &git_data_path,
1222 )); 1231 ));
1223 1232
1224 tracing::info!( 1233 tracing::info!(