upleb.uk

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

summaryrefslogtreecommitdiff
path: root/src/http
diff options
context:
space:
mode:
authorDanConwayDev <DanConwayDev@protonmail.com>2025-11-21 04:44:40 +0000
committerDanConwayDev <DanConwayDev@protonmail.com>2025-11-21 04:44:40 +0000
commit7dda553918705277c7fa5b903c6a40e4b4a0aa8d (patch)
tree4f3511cd3fe56928bd2aa9a22f4ddd592f4c6b83 /src/http
parent2e799fa7ec57d284c643df8b8dc54471470f5c59 (diff)
add nip11
Diffstat (limited to 'src/http')
-rw-r--r--src/http/mod.rs27
-rw-r--r--src/http/nip11.rs146
2 files changed, 173 insertions, 0 deletions
diff --git a/src/http/mod.rs b/src/http/mod.rs
index 4690790..7c0e7bb 100644
--- a/src/http/mod.rs
+++ b/src/http/mod.rs
@@ -2,6 +2,7 @@
2/// 2///
3/// Provides hyper HTTP server with WebSocket upgrade support for the Nostr relay. 3/// Provides hyper HTTP server with WebSocket upgrade support for the Nostr relay.
4pub mod landing; 4pub mod landing;
5pub mod nip11;
5 6
6use std::future::Future; 7use std::future::Future;
7use std::net::SocketAddr; 8use std::net::SocketAddr;
@@ -46,6 +47,32 @@ impl Service<Request<Incoming>> for HttpService {
46 fn call(&self, req: Request<Incoming>) -> Self::Future { 47 fn call(&self, req: Request<Incoming>) -> Self::Future {
47 let base = Response::builder().header("server", "ngit-grasp"); 48 let base = Response::builder().header("server", "ngit-grasp");
48 49
50 // Check for NIP-11 relay information request (Accept: application/nostr+json)
51 if let Some(accept) = req.headers().get("accept") {
52 if accept
53 .to_str()
54 .map(|s| s.contains("application/nostr+json"))
55 .unwrap_or(false)
56 {
57 let doc = nip11::RelayInformationDocument::from_config(&self.config);
58 let json = doc.to_json().unwrap_or_else(|e| {
59 tracing::error!("Failed to serialize NIP-11 document: {}", e);
60 "{}".to_string()
61 });
62
63 tracing::debug!("Serving NIP-11 relay information document to {}", self.remote);
64
65 return Box::pin(async move {
66 Ok(base
67 .status(200)
68 .header("content-type", "application/nostr+json")
69 .header("access-control-allow-origin", "*")
70 .body(json)
71 .unwrap())
72 });
73 }
74 }
75
49 // Check if this is a WebSocket upgrade request 76 // Check if this is a WebSocket upgrade request
50 if let (Some(c), Some(w)) = ( 77 if let (Some(c), Some(w)) = (
51 req.headers().get("connection"), 78 req.headers().get("connection"),
diff --git a/src/http/nip11.rs b/src/http/nip11.rs
new file mode 100644
index 0000000..a93ee5f
--- /dev/null
+++ b/src/http/nip11.rs
@@ -0,0 +1,146 @@
1/// NIP-11 Relay Information Document
2///
3/// Implements NIP-11 relay information endpoint with GRASP-01 extensions.
4/// See: https://github.com/nostr-protocol/nips/blob/master/11.md
5
6use serde::{Deserialize, Serialize};
7use crate::config::Config;
8
9/// NIP-11 Relay Information Document
10///
11/// This structure represents the relay metadata served at the HTTP(S) endpoint
12/// when the client sends `Accept: application/nostr+json` header.
13#[derive(Debug, Clone, Serialize, Deserialize)]
14pub struct RelayInformationDocument {
15 /// Relay name
16 pub name: String,
17
18 /// Relay description
19 pub description: String,
20
21 /// Relay owner's public key (hex format)
22 #[serde(skip_serializing_if = "Option::is_none")]
23 pub pubkey: Option<String>,
24
25 /// Contact information for relay admin
26 #[serde(skip_serializing_if = "Option::is_none")]
27 pub contact: Option<String>,
28
29 /// List of NIPs supported by this relay
30 pub supported_nips: Vec<u16>,
31
32 /// Relay software identifier
33 pub software: String,
34
35 /// Software version
36 pub version: String,
37
38 // GRASP-01 Extensions (lines 11-14 of GRASP-01 spec)
39
40 /// List of supported GRASPs (e.g., ["GRASP-01"])
41 /// Required by GRASP-01 specification line 12
42 pub supported_grasps: Vec<String>,
43
44 /// Repository acceptance criteria description
45 /// Required by GRASP-01 specification line 13
46 pub repo_acceptance_criteria: String,
47
48 /// Curation policy (present if curated, absent otherwise)
49 /// Optional per GRASP-01 specification line 14
50 #[serde(skip_serializing_if = "Option::is_none")]
51 pub curation: Option<String>,
52}
53
54impl RelayInformationDocument {
55 /// Create NIP-11 relay information document from configuration
56 pub fn from_config(config: &Config) -> Self {
57 Self {
58 name: config.relay_name.clone(),
59 description: config.relay_description.clone(),
60 pubkey: Some(config.owner_npub.clone()),
61 contact: None, // Could be added to config if needed
62 supported_nips: vec![
63 1, // NIP-01: Basic protocol flow
64 11, // NIP-11: Relay information document (this!)
65 34, // NIP-34: Git repository announcements
66 ],
67 software: env!("CARGO_PKG_NAME").to_string(),
68 version: env!("CARGO_PKG_VERSION").to_string(),
69
70 // GRASP-01 Extensions
71 supported_grasps: vec!["GRASP-01".to_string()],
72 repo_acceptance_criteria: format!(
73 "Repositories must list this relay ({}) in both 'clone' and 'relays' tags of kind 30617 announcements. \
74 All other events must reference accepted repositories or accepted events.",
75 config.domain
76 ),
77 curation: None, // Not a curated relay - only SPAM prevention via GRASP-01 policy
78 }
79 }
80
81 /// Serialize to JSON string
82 pub fn to_json(&self) -> Result<String, serde_json::Error> {
83 serde_json::to_string_pretty(self)
84 }
85}
86
87#[cfg(test)]
88mod tests {
89 use super::*;
90
91 #[test]
92 fn test_relay_information_document_structure() {
93 let config = Config {
94 domain: "relay.example.com".to_string(),
95 owner_npub: "npub1test".to_string(),
96 relay_name: "Test Relay".to_string(),
97 relay_description: "A test relay".to_string(),
98 git_data_path: "./data/git".to_string(),
99 relay_data_path: "./data/relay".to_string(),
100 bind_address: "127.0.0.1:8080".to_string(),
101 database_backend: crate::config::DatabaseBackend::Memory,
102 };
103
104 let doc = RelayInformationDocument::from_config(&config);
105
106 assert_eq!(doc.name, "Test Relay");
107 assert_eq!(doc.description, "A test relay");
108 assert_eq!(doc.pubkey, Some("npub1test".to_string()));
109 assert!(doc.supported_nips.contains(&1));
110 assert!(doc.supported_nips.contains(&11));
111 assert!(doc.supported_nips.contains(&34));
112 assert_eq!(doc.supported_grasps, vec!["GRASP-01"]);
113 assert!(doc.repo_acceptance_criteria.contains("relay.example.com"));
114 assert!(doc.curation.is_none());
115 }
116
117 #[test]
118 fn test_relay_information_document_json() {
119 let config = Config {
120 domain: "relay.example.com".to_string(),
121 owner_npub: "npub1test".to_string(),
122 relay_name: "Test Relay".to_string(),
123 relay_description: "A test relay".to_string(),
124 git_data_path: "./data/git".to_string(),
125 relay_data_path: "./data/relay".to_string(),
126 bind_address: "127.0.0.1:8080".to_string(),
127 database_backend: crate::config::DatabaseBackend::Memory,
128 };
129
130 let doc = RelayInformationDocument::from_config(&config);
131 let json = doc.to_json().expect("Failed to serialize to JSON");
132
133 // Verify JSON contains expected fields
134 assert!(json.contains("\"name\""));
135 assert!(json.contains("\"description\""));
136 assert!(json.contains("\"supported_nips\""));
137 assert!(json.contains("\"supported_grasps\""));
138 assert!(json.contains("\"repo_acceptance_criteria\""));
139 assert!(json.contains("GRASP-01"));
140
141 // Verify it's valid JSON by parsing
142 let parsed: serde_json::Value = serde_json::from_str(&json).expect("Invalid JSON");
143 assert_eq!(parsed["name"], "Test Relay");
144 assert_eq!(parsed["supported_grasps"][0], "GRASP-01");
145 }
146} \ No newline at end of file