upleb.uk

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

summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-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
6 files changed, 215 insertions, 65 deletions
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!(