upleb.uk

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

summaryrefslogtreecommitdiff
path: root/grasp-audit/src/specs/grasp01/spec_requirements.rs
diff options
context:
space:
mode:
authorDanConwayDev <DanConwayDev@protonmail.com>2026-02-23 15:41:32 +0000
committerDanConwayDev <DanConwayDev@protonmail.com>2026-02-23 15:41:32 +0000
commitc54ce061d6d278cce8362d5af085808ca60c239b (patch)
treeec967d6195d9f7ec4f061449596611afe3a0950f /grasp-audit/src/specs/grasp01/spec_requirements.rs
parente0ad39a489b3398f8208713bf728db0cb11475b0 (diff)
parent113928aa84894ea8f65c247d9987527e792b32a9 (diff)
feat: announcement purgatory
Extends purgatory to hold repository announcements until git data arrives, preventing empty repositories from being served to clients. When an announcement is received, a bare repo is created immediately and the announcement is held in purgatory. It is only promoted and served once a git push confirms real content exists. If no push arrives before expiry, the bare repo is deleted and the announcement is silently discarded. Key behaviours: - Soft expiry: announcements are hidden from clients but kept alive while git pushes are in progress, reviving on successful push - Expiry is extended when a matching state event or git push is observed - NIP-09 deletion events remove announcements from purgatory - Purgatory state (announcements, state events, PR events, expired set) is persisted to disk on graceful shutdown and restored on startup, with elapsed downtime subtracted from expiry deadlines - Purgatory announcements drive StateOnly sync in the sync system so state events are fetched from listed relays before promotion - SyncLevel added to RepoSyncIndex to distinguish purgatory repos (StateOnly) from promoted repos (Full L2+L3 sync)
Diffstat (limited to 'grasp-audit/src/specs/grasp01/spec_requirements.rs')
-rw-r--r--grasp-audit/src/specs/grasp01/spec_requirements.rs150
1 files changed, 130 insertions, 20 deletions
diff --git a/grasp-audit/src/specs/grasp01/spec_requirements.rs b/grasp-audit/src/specs/grasp01/spec_requirements.rs
index 71b2d69..6bc961c 100644
--- a/grasp-audit/src/specs/grasp01/spec_requirements.rs
+++ b/grasp-audit/src/specs/grasp01/spec_requirements.rs
@@ -6,9 +6,36 @@
6/// GRASP spec repository commit ID that this version is based on 6/// GRASP spec repository commit ID that this version is based on
7pub const GRASP_COMMIT_ID: &str = "1fdb8f7"; 7pub const GRASP_COMMIT_ID: &str = "1fdb8f7";
8 8
9/// Reference to a specific GRASP-01 specification requirement
10#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
11pub enum SpecRef {
12 NostrRelayNip01Compliant,
13 NostrRelayRejectMissingCloneRelays,
14 NostrRelayMayRejectOtherCriteria,
15 NostrRelayMustAcceptTaggedEvents,
16 NostrRelayMayRejectSpamCuration,
17 PurgatoryAcceptUntilGitData,
18 Nip11ServeDocument,
19 Nip11ListSupportedGrasps,
20 Nip11ListRepoAcceptanceCriteria,
21 Nip11ListCurationPolicy,
22 GitServeRepository,
23 GitAcceptPushesAlignState,
24 GitSetHeadOnReceive,
25 GitAcceptRefsNostrEventId,
26 GitIncludeAllowSha1InWant,
27 GitServeWebpage,
28 CorsAllowOrigin,
29 CorsAllowMethods,
30 CorsAllowHeaders,
31 CorsOptionsResponse,
32}
33
9/// A single specification requirement 34/// A single specification requirement
10#[derive(Debug, Clone)] 35#[derive(Debug, Clone)]
11pub struct SpecRequirement { 36pub struct SpecRequirement {
37 /// Unique reference to this requirement
38 pub spec_ref: SpecRef,
12 /// Line number in the spec document 39 /// Line number in the spec document
13 pub line: u32, 40 pub line: u32,
14 /// Section name (e.g., "Nostr Relay", "Git Smart HTTP Service", "CORS Support") 41 /// Section name (e.g., "Nostr Relay", "Git Smart HTTP Service", "CORS Support")
@@ -37,121 +64,175 @@ impl std::fmt::Display for RequirementLevel {
37 } 64 }
38} 65}
39 66
67impl SpecRef {
68 /// Get the spec reference string in format "GRASP-01:section:line"
69 pub fn spec_ref_string(self) -> &'static str {
70 match self {
71 SpecRef::NostrRelayNip01Compliant => "GRASP-01:nostr-relay:7",
72 SpecRef::NostrRelayRejectMissingCloneRelays => "GRASP-01:nostr-relay:9",
73 SpecRef::NostrRelayMayRejectOtherCriteria => "GRASP-01:nostr-relay:11",
74 SpecRef::NostrRelayMustAcceptTaggedEvents => "GRASP-01:nostr-relay:13",
75 SpecRef::NostrRelayMayRejectSpamCuration => "GRASP-01:nostr-relay:18",
76 SpecRef::PurgatoryAcceptUntilGitData => "GRASP-01:purgatory:22",
77 SpecRef::Nip11ServeDocument => "GRASP-01:nip-11:26",
78 SpecRef::Nip11ListSupportedGrasps => "GRASP-01:nip-11:28",
79 SpecRef::Nip11ListRepoAcceptanceCriteria => "GRASP-01:nip-11:29",
80 SpecRef::Nip11ListCurationPolicy => "GRASP-01:nip-11:30",
81 SpecRef::GitServeRepository => "GRASP-01:git-http:34",
82 SpecRef::GitAcceptPushesAlignState => "GRASP-01:git-http:36",
83 SpecRef::GitSetHeadOnReceive => "GRASP-01:git-http:39",
84 SpecRef::GitAcceptRefsNostrEventId => "GRASP-01:git-http:45",
85 SpecRef::GitIncludeAllowSha1InWant => "GRASP-01:git-http:56",
86 SpecRef::GitServeWebpage => "GRASP-01:git-http:58",
87 SpecRef::CorsAllowOrigin => "GRASP-01:cors:64",
88 SpecRef::CorsAllowMethods => "GRASP-01:cors:65",
89 SpecRef::CorsAllowHeaders => "GRASP-01:cors:66",
90 SpecRef::CorsOptionsResponse => "GRASP-01:cors:67",
91 }
92 }
93}
94
40/// All GRASP-01 specification requirements 95/// All GRASP-01 specification requirements
41pub const GRASP_01_REQUIREMENTS: &[SpecRequirement] = &[ 96pub const GRASP_01_REQUIREMENTS: &[SpecRequirement] = &[
42 // Nostr Relay section 97 // Nostr Relay section
43 SpecRequirement { 98 SpecRequirement {
99 spec_ref: SpecRef::NostrRelayNip01Compliant,
44 line: 7, 100 line: 7,
45 section: "Nostr Relay", 101 section: "Nostr Relay",
46 text: "MUST serve a NIP-01 compliant nostr relay at `/` that accepts git repository announcements and their corresponding repo state announcements.", 102 text: "MUST serve a NIP-01 compliant nostr relay at `/` that accepts git repository announcements and their corresponding repo state announcements.",
47 level: RequirementLevel::Must, 103 level: RequirementLevel::Must,
48 }, 104 },
49 SpecRequirement { 105 SpecRequirement {
106 spec_ref: SpecRef::NostrRelayRejectMissingCloneRelays,
50 line: 9, 107 line: 9,
51 section: "Nostr Relay", 108 section: "Nostr Relay",
52 text: "MUST reject git repository announcements that do not list the service in both `clone` and `relays` tags unless implementing `GRASP-05`.", 109 text: "MUST reject git repository announcements that do not list the service in both `clone` and `relays` tags unless implementing `GRASP-05`.",
53 level: RequirementLevel::Must, 110 level: RequirementLevel::Must,
54 }, 111 },
55 SpecRequirement { 112 SpecRequirement {
113 spec_ref: SpecRef::NostrRelayMayRejectOtherCriteria,
56 line: 11, 114 line: 11,
57 section: "Nostr Relay", 115 section: "Nostr Relay",
58 text: "MAY reject git repository announcements based on other criteria such as pre-payment, quotas, WoT, whitelist, SPAM prevention, etc.", 116 text: "MAY reject git repository announcements based on other criteria such as pre-payment, quotas, WoT, whitelist, SPAM prevention, etc.",
59 level: RequirementLevel::May, 117 level: RequirementLevel::May,
60 }, 118 },
61 SpecRequirement { 119 SpecRequirement {
120 spec_ref: SpecRef::NostrRelayMustAcceptTaggedEvents,
62 line: 13, 121 line: 13,
63 section: "Nostr Relay", 122 section: "Nostr Relay",
64 text: "MUST accept other events that tag, or are tagged by, either: 1. accepted git repository announcements; or 2. accepted issues or patches", 123 text: "MUST accept other events that tag, or are tagged by, either: 1. accepted git repository announcements; or 2. accepted issues or patches",
65 level: RequirementLevel::Must, 124 level: RequirementLevel::Must,
66 }, 125 },
67 SpecRequirement { 126 SpecRequirement {
127 spec_ref: SpecRef::NostrRelayMayRejectSpamCuration,
68 line: 18, 128 line: 18,
69 section: "Nostr Relay", 129 section: "Nostr Relay",
70 text: "MAY reject or delete events for generic SPAM prevention reasons or curation eg. WoT, whitelist, user bans and banned topics.", 130 text: "MAY reject or delete events for generic SPAM prevention reasons or curation eg. WoT, whitelist, user bans and banned topics.",
71 level: RequirementLevel::May, 131 level: RequirementLevel::May,
72 }, 132 },
73 SpecRequirement { 133 SpecRequirement {
134 spec_ref: SpecRef::PurgatoryAcceptUntilGitData,
135 line: 22,
136 section: "Purgatory",
137 text: "New repository announcements, repo state announcements, PRs and PR Updates SHOULD be accepted with message \"purgatory: won't be served until git data arrives\" and kept in purgatory (not served) until the related git data arrives and otherwise discarded after 30 minutes.",
138 level: RequirementLevel::Should,
139 },
140 SpecRequirement {
141 spec_ref: SpecRef::Nip11ServeDocument,
74 line: 26, 142 line: 26,
75 section: "Nostr Relay", 143 section: "NIP-11",
76 text: "MUST serve a NIP-11 document", 144 text: "MUST serve a NIP-11 document",
77 level: RequirementLevel::Must, 145 level: RequirementLevel::Must,
78 }, 146 },
79 SpecRequirement { 147 SpecRequirement {
148 spec_ref: SpecRef::Nip11ListSupportedGrasps,
80 line: 28, 149 line: 28,
81 section: "Nostr Relay", 150 section: "NIP-11",
82 text: "MUST list each supported GRASP under `supported_grasps` in format `GRASP-XX` eg `GRASP-01` as a string array", 151 text: "MUST list each supported GRASP under `supported_grasps` in format `GRASP-XX` eg `GRASP-01` as a string array",
83 level: RequirementLevel::Must, 152 level: RequirementLevel::Must,
84 }, 153 },
85 SpecRequirement { 154 SpecRequirement {
155 spec_ref: SpecRef::Nip11ListRepoAcceptanceCriteria,
86 line: 29, 156 line: 29,
87 section: "Nostr Relay", 157 section: "NIP-11",
88 text: "MUST list repository acceptance criteria under `repo_acceptance_criteria` as a human readable string", 158 text: "MUST list repository acceptance criteria under `repo_acceptance_criteria` as a human readable string",
89 level: RequirementLevel::Must, 159 level: RequirementLevel::Must,
90 }, 160 },
91 SpecRequirement { 161 SpecRequirement {
162 spec_ref: SpecRef::Nip11ListCurationPolicy,
92 line: 30, 163 line: 30,
93 section: "Nostr Relay", 164 section: "NIP-11",
94 text: "MUST list brief summary of curation policy under `curation` if events are curated beyond generic SPAM prevention; otherwise `curation` MUST be omitted", 165 text: "MUST list brief summary of curation policy under `curation` if events are curated beyond generic SPAM prevention; otherwise `curation` MUST be omitted",
95 level: RequirementLevel::Must, 166 level: RequirementLevel::Must,
96 }, 167 },
97 // Git Smart HTTP Service section 168 // Git Smart HTTP Service section
98 SpecRequirement { 169 SpecRequirement {
170 spec_ref: SpecRef::GitServeRepository,
99 line: 34, 171 line: 34,
100 section: "Git Smart HTTP Service", 172 section: "Git Smart HTTP Service",
101 text: "MUST serve a git repository via an unauthenticated git smart http service at `/<npub>/<identifier>.git` for each accepted git repository announcement.", 173 text: "MUST serve a git repository via an unauthenticated git smart http service at `/<npub>/<identifier>.git` for each git repository announcement the relay serves or has in purgatory.",
102 level: RequirementLevel::Must, 174 level: RequirementLevel::Must,
103 }, 175 },
104 SpecRequirement { 176 SpecRequirement {
177 spec_ref: SpecRef::GitAcceptPushesAlignState,
105 line: 36, 178 line: 36,
106 section: "Git Smart HTTP Service", 179 section: "Git Smart HTTP Service",
107 text: "MUST accept pushes via this service that match the latest repo state announcement on the relay, respecting the recursive maintainer set.", 180 text: "MUST accept pushes via this service that fully align the git repository state with a repo state announcement in purgatory that is authorised for this repository, respecting the recursive maintainer set.",
108 level: RequirementLevel::Must, 181 level: RequirementLevel::Must,
109 }, 182 },
110 SpecRequirement { 183 SpecRequirement {
111 line: 38, 184 spec_ref: SpecRef::GitSetHeadOnReceive,
185 line: 39,
112 section: "Git Smart HTTP Service", 186 section: "Git Smart HTTP Service",
113 text: "MUST set repository HEAD per repo state announcement as soon as the git data related to that branch has been received.", 187 text: "As soon as the `receive-pack` is successful, the server MUST: 1. Release the event (and related repository announcement) from purgatory. 2. Align the repository HEAD with the repo state announcement. 3. Synchronize git state with other git repositories on the server for which this state event is authoritative.",
114 level: RequirementLevel::Must, 188 level: RequirementLevel::Must,
115 }, 189 },
116 SpecRequirement { 190 SpecRequirement {
117 line: 40, 191 spec_ref: SpecRef::GitAcceptRefsNostrEventId,
192 line: 45,
118 section: "Git Smart HTTP Service", 193 section: "Git Smart HTTP Service",
119 text: "MUST accept pushes via this service to `refs/nostr/<event-id>` but SHOULD reject if event exists on relay listing a different tip and MAY reject based on criteria such as size, SPAM prevention, etc. SHOULD delete and MAY garbage collect these refs if no corresponding git PR event or git PR update event, with a `c` tag that matches the ref tip, is accepted by relay within 20 minutes.", 194 text: "MUST accept pushes via this service to `refs/nostr/<event-id>` but SHOULD reject if the event exists in purgatory listing a different tip, and MAY reject based on criteria such as size, SPAM prevention, etc.",
120 level: RequirementLevel::Must, 195 level: RequirementLevel::Must,
121 }, 196 },
122 SpecRequirement { 197 SpecRequirement {
123 line: 42, 198 spec_ref: SpecRef::GitIncludeAllowSha1InWant,
199 line: 56,
124 section: "Git Smart HTTP Service", 200 section: "Git Smart HTTP Service",
125 text: "MUST include `allow-reachable-sha1-in-want` and `allow-tip-sha1-in-want` in advertisement and serve available oids.", 201 text: "MUST include `allow-reachable-sha1-in-want` and `allow-tip-sha1-in-want` in advertisement and serve available oids.",
126 level: RequirementLevel::Must, 202 level: RequirementLevel::Must,
127 }, 203 },
128 SpecRequirement { 204 SpecRequirement {
129 line: 44, 205 spec_ref: SpecRef::GitServeWebpage,
206 line: 58,
130 section: "Git Smart HTTP Service", 207 section: "Git Smart HTTP Service",
131 text: "SHOULD serve a webpage at the same endpoint linking to git nostr client(s) to browse the repository and a 404 page for repositories it doesn't host.", 208 text: "SHOULD serve a webpage at the same endpoint linking to git nostr client(s) to browse the repository and a 404 page for repositories it doesn't host.",
132 level: RequirementLevel::Should, 209 level: RequirementLevel::Should,
133 }, 210 },
134 // CORS Support section 211 // CORS Support section
135 SpecRequirement { 212 SpecRequirement {
136 line: 50, 213 spec_ref: SpecRef::CorsAllowOrigin,
214 line: 64,
137 section: "CORS Support", 215 section: "CORS Support",
138 text: "Set `Access-Control-Allow-Origin: *` on ALL responses", 216 text: "Set `Access-Control-Allow-Origin: *` on ALL responses",
139 level: RequirementLevel::Must, 217 level: RequirementLevel::Must,
140 }, 218 },
141 SpecRequirement { 219 SpecRequirement {
142 line: 51, 220 spec_ref: SpecRef::CorsAllowMethods,
221 line: 65,
143 section: "CORS Support", 222 section: "CORS Support",
144 text: "Set `Access-Control-Allow-Methods: GET, POST` on ALL responses", 223 text: "Set `Access-Control-Allow-Methods: GET, POST` on ALL responses",
145 level: RequirementLevel::Must, 224 level: RequirementLevel::Must,
146 }, 225 },
147 SpecRequirement { 226 SpecRequirement {
148 line: 52, 227 spec_ref: SpecRef::CorsAllowHeaders,
228 line: 66,
149 section: "CORS Support", 229 section: "CORS Support",
150 text: "Set `Access-Control-Allow-Headers: Content-Type` on ALL responses", 230 text: "Set `Access-Control-Allow-Headers: Content-Type` on ALL responses",
151 level: RequirementLevel::Must, 231 level: RequirementLevel::Must,
152 }, 232 },
153 SpecRequirement { 233 SpecRequirement {
154 line: 53, 234 spec_ref: SpecRef::CorsOptionsResponse,
235 line: 67,
155 section: "CORS Support", 236 section: "CORS Support",
156 text: "Respond to OPTIONS requests with 204 No Content", 237 text: "Respond to OPTIONS requests with 204 No Content",
157 level: RequirementLevel::Must, 238 level: RequirementLevel::Must,
@@ -163,6 +244,13 @@ pub fn get_requirement(line: u32) -> Option<&'static SpecRequirement> {
163 GRASP_01_REQUIREMENTS.iter().find(|r| r.line == line) 244 GRASP_01_REQUIREMENTS.iter().find(|r| r.line == line)
164} 245}
165 246
247/// Get a requirement by its SpecRef
248pub fn get_requirement_by_ref(spec_ref: SpecRef) -> Option<&'static SpecRequirement> {
249 GRASP_01_REQUIREMENTS
250 .iter()
251 .find(|r| r.spec_ref == spec_ref)
252}
253
166/// Get all requirements for a section 254/// Get all requirements for a section
167pub fn get_requirements_for_section(section: &str) -> Vec<&'static SpecRequirement> { 255pub fn get_requirements_for_section(section: &str) -> Vec<&'static SpecRequirement> {
168 GRASP_01_REQUIREMENTS 256 GRASP_01_REQUIREMENTS
@@ -194,16 +282,38 @@ mod tests {
194 } 282 }
195 283
196 #[test] 284 #[test]
285 fn test_get_requirement_by_ref() {
286 let req = get_requirement_by_ref(SpecRef::NostrRelayNip01Compliant)
287 .expect("SpecRef should exist");
288 assert_eq!(req.line, 7);
289 assert_eq!(req.spec_ref, SpecRef::NostrRelayNip01Compliant);
290 }
291
292 #[test]
197 fn test_get_sections() { 293 fn test_get_sections() {
198 let sections = get_sections(); 294 let sections = get_sections();
199 assert_eq!(sections.len(), 3); 295 assert_eq!(sections.len(), 5);
200 assert_eq!(sections[0], "Nostr Relay"); 296 assert_eq!(sections[0], "Nostr Relay");
201 assert_eq!(sections[1], "Git Smart HTTP Service"); 297 assert_eq!(sections[1], "Purgatory");
202 assert_eq!(sections[2], "CORS Support"); 298 assert_eq!(sections[2], "NIP-11");
299 assert_eq!(sections[3], "Git Smart HTTP Service");
300 assert_eq!(sections[4], "CORS Support");
203 } 301 }
204 302
205 #[test] 303 #[test]
206 fn test_requirement_count() { 304 fn test_requirement_count() {
207 assert_eq!(GRASP_01_REQUIREMENTS.len(), 19); 305 assert_eq!(GRASP_01_REQUIREMENTS.len(), 20);
306 }
307
308 #[test]
309 fn test_spec_ref_unique() {
310 let mut refs = std::collections::HashSet::new();
311 for req in GRASP_01_REQUIREMENTS {
312 assert!(
313 refs.insert(req.spec_ref),
314 "Duplicate SpecRef found: {:?}",
315 req.spec_ref
316 );
317 }
208 } 318 }
209} 319}