upleb.uk

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

summaryrefslogtreecommitdiff
path: root/docs/ARCHITECTURE.md
diff options
context:
space:
mode:
Diffstat (limited to 'docs/ARCHITECTURE.md')
-rw-r--r--docs/ARCHITECTURE.md808
1 files changed, 808 insertions, 0 deletions
diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md
new file mode 100644
index 0000000..ebf7a74
--- /dev/null
+++ b/docs/ARCHITECTURE.md
@@ -0,0 +1,808 @@
1# ngit-grasp Architecture
2
3## Executive Summary
4
5`ngit-grasp` implements the GRASP protocol in Rust with **inline authorization** rather than Git hooks. The key architectural insight is that the `git-http-backend` Rust crate provides sufficient flexibility to intercept and validate Git push operations before they reach the Git repository, eliminating the need for pre-receive hooks.
6
7## Architectural Decision: Inline vs. Hook-Based Authorization
8
9### Investigation Summary
10
11After examining both the reference implementation and the `git-http-backend` Rust crate, we have two options:
12
13#### Option 1: Hook-Based (Reference Implementation Approach)
14- Use `git-http-backend` crate as-is
15- Create pre-receive and post-receive hooks
16- Hooks query the Nostr relay and validate pushes
17- **Pros**: Follows reference implementation closely
18- **Cons**: Requires hook management, harder to test, less Rust-native
19
20#### Option 2: Inline Authorization (Recommended)
21- Intercept Git receive-pack requests in the HTTP handler
22- Validate against Nostr state before spawning Git process
23- Only forward valid pushes to Git
24- **Pros**: Better error handling, easier testing, pure Rust, simpler deployment
25- **Cons**: Requires custom Git protocol handling
26
27### Decision: Inline Authorization (Option 2)
28
29**Rationale:**
30
311. **The `git-http-backend` crate is sufficiently flexible**: Examining `src/actix/git_receive_pack.rs` shows it spawns `git receive-pack` as a subprocess and streams data. We can intercept this.
32
332. **Better Developer Experience**:
34 - Validation errors can be returned as proper HTTP responses
35 - No need to parse hook stderr output
36 - Shared state between Git and Nostr components
37 - Pure Rust testing without shell scripts
38
393. **Simpler Deployment**:
40 - Single binary
41 - No hook symlinks or permissions to manage
42 - No multi-process coordination
43
444. **Performance**:
45 - Can parse incoming pack data once
46 - Avoid process spawn overhead for invalid pushes
47 - Better async integration
48
49## System Architecture
50
51```
52┌─────────────────────────────────────────────────────────────┐
53│ ngit-grasp │
54│ (Single Rust Binary) │
55├─────────────────────────────────────────────────────────────┤
56│ │
57│ ┌──────────────────┐ ┌──────────────────┐ │
58│ │ HTTP Router │ │ Nostr Relay │ │
59│ │ (actix-web) │ │ (nostr-relay- │ │
60│ │ │ │ builder) │ │
61│ └────────┬─────────┘ └────────┬─────────┘ │
62│ │ │ │
63│ │ │ │
64│ ┌────────▼──────────────────────────────────▼─────────┐ │
65│ │ Shared State & Storage │ │
66│ │ ┌──────────────┐ ┌──────────────┐ │ │
67│ │ │ Repository │ │ Event Store │ │ │
68│ │ │ Manager │ │ (LMDB/NDB) │ │ │
69│ │ └──────────────┘ └──────────────┘ │ │
70│ └─────────────────────────────────────────────────────┘ │
71│ │
72│ ┌──────────────────────────────────────────────────────┐ │
73│ │ Git Protocol Handler │ │
74│ │ │ │
75│ │ 1. Receive git-receive-pack request │ │
76│ │ 2. Parse ref updates from request │ │
77│ │ 3. Query Nostr relay for state event │ │
78│ │ 4. Validate refs against state │ │
79│ │ 5. If valid: spawn git-receive-pack │ │
80│ │ 6. If invalid: return HTTP error │ │
81│ │ │ │
82│ └──────────────────────────────────────────────────────┘ │
83│ │
84└─────────────────────────────────────────────────────────────┘
85 │ │
86 │ HTTP/Git │ WebSocket/Nostr
87 ▼ ▼
88 Git Clients Nostr Clients
89```
90
91## Component Design
92
93### 1. Main Server (`src/main.rs`)
94
95**Responsibilities:**
96- Initialize configuration from environment
97- Set up actix-web HTTP server
98- Initialize Nostr relay builder
99- Set up shared storage
100- Configure routes for both Git and Nostr endpoints
101- Handle graceful shutdown
102
103**Key Dependencies:**
104```rust
105actix-web = "4"
106tokio = { version = "1", features = ["full"] }
107nostr-relay-builder = "0.43"
108nostr-sdk = "0.43"
109```
110
111### 2. Git Module (`src/git/`)
112
113#### `handler.rs` - Git HTTP Handlers
114
115Implements actix-web handlers for Git Smart HTTP protocol:
116
117```rust
118// GET /<npub>/<identifier>.git/info/refs?service=git-upload-pack
119async fn info_refs_upload_pack(
120 req: HttpRequest,
121 state: web::Data<AppState>,
122) -> Result<HttpResponse>
123
124// POST /<npub>/<identifier>.git/git-upload-pack
125async fn git_upload_pack(
126 req: HttpRequest,
127 body: web::Payload,
128 state: web::Data<AppState>,
129) -> Result<HttpResponse>
130
131// GET /<npub>/<identifier>.git/info/refs?service=git-receive-pack
132async fn info_refs_receive_pack(
133 req: HttpRequest,
134 state: web::Data<AppState>,
135) -> Result<HttpResponse>
136
137// POST /<npub>/<identifier>.git/git-receive-pack
138// THIS IS WHERE THE MAGIC HAPPENS
139async fn git_receive_pack(
140 req: HttpRequest,
141 body: web::Payload,
142 state: web::Data<AppState>,
143) -> Result<HttpResponse>
144```
145
146#### `authorization.rs` - Push Validation
147
148**Core Logic:**
149
150```rust
151pub struct PushValidator {
152 nostr_client: Arc<Client>,
153 relay_url: String,
154}
155
156impl PushValidator {
157 /// Validate a push operation against Nostr state
158 pub async fn validate_push(
159 &self,
160 npub: &str,
161 identifier: &str,
162 ref_updates: Vec<RefUpdate>,
163 ) -> Result<ValidationResult> {
164 // 1. Fetch announcement and state events from local relay
165 let events = self.fetch_events(identifier).await?;
166
167 // 2. Extract pubkey from npub
168 let pubkey = decode_npub(npub)?;
169
170 // 3. Get recursive maintainer set
171 let maintainers = get_maintainers(&events, &pubkey, identifier);
172
173 // 4. Get latest state from maintainers
174 let state = get_state_from_maintainers(&events, &maintainers)?;
175
176 // 5. Validate each ref update
177 for ref_update in ref_updates {
178 if ref_update.ref_name.starts_with("refs/nostr/") {
179 // Allow refs/nostr/<event-id> for PRs
180 validate_pr_ref(&ref_update)?;
181 } else if ref_update.ref_name.starts_with("refs/heads/pr/") {
182 // Reject pr/* branches - should use refs/nostr/
183 return Err(Error::InvalidRef("pr/* branches must use refs/nostr/"));
184 } else {
185 // Validate against state event
186 validate_state_ref(&state, &ref_update)?;
187 }
188 }
189
190 Ok(ValidationResult::Accept)
191 }
192}
193```
194
195**Key Functions:**
196
197```rust
198/// Parse ref updates from git-receive-pack request body
199fn parse_ref_updates(body: &[u8]) -> Result<Vec<RefUpdate>>
200
201/// Recursively find all maintainers
202fn get_maintainers(
203 events: &[Event],
204 pubkey: &str,
205 identifier: &str,
206) -> Vec<String>
207
208/// Get latest state from maintainer set
209fn get_state_from_maintainers(
210 events: &[Event],
211 maintainers: &[String],
212) -> Result<RepositoryState>
213
214/// Validate a ref matches the state event
215fn validate_state_ref(
216 state: &RepositoryState,
217 ref_update: &RefUpdate,
218) -> Result<()>
219```
220
221### 3. Nostr Module (`src/nostr/`)
222
223#### `relay.rs` - Relay Configuration
224
225```rust
226pub async fn build_relay(config: &Config) -> Result<LocalRelay> {
227 let builder = RelayBuilder::default()
228 .write_policy(RepositoryAnnouncementPolicy::new(config.domain.clone()))
229 .write_policy(RelatedEventsPolicy::new())
230 .query_policy(StandardQueryPolicy::new())
231 .on_event_saved(create_repository_hook(config.git_data_path.clone()));
232
233 // Configure storage backend (LMDB or NDB)
234 let relay = LocalRelay::run(builder).await?;
235
236 Ok(relay)
237}
238```
239
240#### `events.rs` - Event Handlers
241
242```rust
243/// Hook called when events are saved
244pub fn create_repository_hook(
245 git_data_path: PathBuf,
246) -> impl Fn(&Event) -> BoxFuture<'static, ()> {
247 move |event: &Event| {
248 let git_path = git_data_path.clone();
249 Box::pin(async move {
250 if event.kind == Kind::RepositoryAnnouncement {
251 handle_repository_announcement(event, &git_path).await;
252 } else if event.kind == Kind::RepositoryState {
253 handle_repository_state(event, &git_path).await;
254 }
255 })
256 }
257}
258
259async fn handle_repository_announcement(event: &Event, git_path: &Path) {
260 // 1. Parse repository from event
261 // 2. Check if listed in clone and relays tags
262 // 3. Create empty bare Git repository
263 // 4. Configure uploadpack.allowTipSHA1InWant
264 // 5. Configure uploadpack.allowUnreachable
265 // 6. Configure http.receivepack
266}
267
268async fn handle_repository_state(event: &Event, git_path: &Path) {
269 // 1. Parse state from event
270 // 2. Update repository HEAD if needed
271 // 3. Trigger proactive sync (GRASP-02)
272}
273```
274
275**Write Policies:**
276
277```rust
278/// Accept repository announcements that list this instance
279pub struct RepositoryAnnouncementPolicy {
280 domain: String,
281}
282
283impl WritePolicy for RepositoryAnnouncementPolicy {
284 fn admit_event(&self, event: &Event, _addr: &SocketAddr)
285 -> BoxFuture<PolicyResult>
286 {
287 Box::pin(async move {
288 if event.kind != Kind::RepositoryAnnouncement {
289 return PolicyResult::Accept; // Not our concern
290 }
291
292 // Check if this instance is in clone and relays tags
293 let has_clone = event.tags.iter()
294 .any(|t| t.kind() == "clone" && t.content() == Some(&self.domain));
295 let has_relay = event.tags.iter()
296 .any(|t| t.kind() == "relays" && t.content() == Some(&self.domain));
297
298 if has_clone && has_relay {
299 PolicyResult::Accept
300 } else {
301 PolicyResult::Reject("instance not listed in clone and relays".into())
302 }
303 })
304 }
305}
306
307/// Accept events related to stored announcements/issues/patches
308pub struct RelatedEventsPolicy;
309
310impl WritePolicy for RelatedEventsPolicy {
311 fn admit_event(&self, event: &Event, _addr: &SocketAddr)
312 -> BoxFuture<PolicyResult>
313 {
314 // Accept if event tags or is tagged by stored events
315 // Implementation requires querying the event store
316 }
317}
318```
319
320### 4. Storage Module (`src/storage/`)
321
322#### `repository.rs` - Repository Management
323
324```rust
325pub struct RepositoryManager {
326 git_data_path: PathBuf,
327}
328
329impl RepositoryManager {
330 /// Create a new bare Git repository
331 pub async fn create_repository(
332 &self,
333 npub: &str,
334 identifier: &str,
335 ) -> Result<PathBuf> {
336 let repo_path = self.git_data_path
337 .join(npub)
338 .join(format!("{}.git", identifier));
339
340 // Create directory
341 tokio::fs::create_dir_all(&repo_path).await?;
342
343 // Initialize bare repo
344 Command::new("git")
345 .args(&["init", "--bare"])
346 .arg(&repo_path)
347 .output()
348 .await?;
349
350 // Configure
351 self.configure_repository(&repo_path).await?;
352
353 Ok(repo_path)
354 }
355
356 async fn configure_repository(&self, repo_path: &Path) -> Result<()> {
357 // Enable unauthenticated push (we handle auth ourselves)
358 git_config(repo_path, "http.receivepack", "true").await?;
359
360 // Enable tip SHA1 fetching (required for ngit)
361 git_config(repo_path, "uploadpack.allowTipSHA1InWant", "true").await?;
362
363 // Enable unreachable object fetching
364 git_config(repo_path, "uploadpack.allowUnreachable", "true").await?;
365
366 Ok(())
367 }
368
369 /// Check if repository exists
370 pub async fn repository_exists(
371 &self,
372 npub: &str,
373 identifier: &str,
374 ) -> bool {
375 let repo_path = self.git_data_path
376 .join(npub)
377 .join(format!("{}.git", identifier));
378
379 repo_path.join("HEAD").exists() &&
380 repo_path.join("config").exists()
381 }
382}
383```
384
385### 5. Configuration (`src/config.rs`)
386
387```rust
388pub struct Config {
389 pub domain: String,
390 pub owner_npub: String,
391 pub relay_name: String,
392 pub relay_description: String,
393 pub git_data_path: PathBuf,
394 pub relay_data_path: PathBuf,
395 pub bind_address: SocketAddr,
396 pub log_level: String,
397}
398
399impl Config {
400 pub fn from_env() -> Result<Self> {
401 Ok(Config {
402 domain: env::var("NGIT_DOMAIN")?,
403 owner_npub: env::var("NGIT_OWNER_NPUB")?,
404 relay_name: env::var("NGIT_RELAY_NAME")?,
405 relay_description: env::var("NGIT_RELAY_DESCRIPTION")?,
406 git_data_path: PathBuf::from(
407 env::var("NGIT_GIT_DATA_PATH")
408 .unwrap_or_else(|_| "./data/git".to_string())
409 ),
410 relay_data_path: PathBuf::from(
411 env::var("NGIT_RELAY_DATA_PATH")
412 .unwrap_or_else(|_| "./data/relay".to_string())
413 ),
414 bind_address: env::var("NGIT_BIND_ADDRESS")
415 .unwrap_or_else(|_| "127.0.0.1:8080".to_string())
416 .parse()?,
417 log_level: env::var("RUST_LOG")
418 .unwrap_or_else(|_| "info".to_string()),
419 })
420 }
421}
422```
423
424## Data Flow
425
426### Push Operation Flow
427
428```
4291. Git Client → POST /<npub>/<id>.git/git-receive-pack
430
4312. git_receive_pack handler receives request
432
4333. Parse ref updates from request body
434
4354. Extract npub and identifier from URL
436
4375. PushValidator::validate_push()
438 ├─ Fetch events from local Nostr relay
439 ├─ Get maintainers recursively
440 ├─ Get latest state from maintainers
441 └─ Validate each ref update
442
4436. If VALID:
444 ├─ Spawn git-receive-pack subprocess
445 ├─ Stream request body to git stdin
446 └─ Stream git stdout back to client
447
4487. If INVALID:
449 └─ Return HTTP 403 with error message
450```
451
452### Repository Announcement Flow
453
454```
4551. Nostr Client → EVENT (Kind 30317)
456
4572. Nostr relay receives event
458
4593. RepositoryAnnouncementPolicy::admit_event()
460 ├─ Check if instance in clone tags
461 ├─ Check if instance in relays tags
462 └─ Accept or reject
463
4644. If ACCEPTED:
465 ├─ Event saved to store
466 └─ on_event_saved hook triggered
467
4685. handle_repository_announcement()
469 ├─ Parse repository details
470 ├─ Create Git repository directory
471 ├─ Initialize bare Git repo
472 └─ Configure Git settings
473```
474
475## Key Implementation Details
476
477### 1. Parsing Git Receive-Pack Protocol
478
479The Git receive-pack protocol uses a pkt-line format. We need to parse:
480
481```
4820000-0000-0000-0000 0000-0000-0000-0000 refs/heads/main\0 report-status
4830000-0000-0000-0000 0000-0000-0000-0000 refs/heads/dev
484```
485
486Each line has:
487- Old SHA (40 hex chars)
488- Space
489- New SHA (40 hex chars)
490- Space
491- Ref name
492- Optional capabilities (first line only, after \0)
493
494```rust
495pub struct RefUpdate {
496 pub old_sha: String,
497 pub new_sha: String,
498 pub ref_name: String,
499}
500
501pub fn parse_ref_updates(body: &[u8]) -> Result<Vec<RefUpdate>> {
502 // Parse pkt-line format
503 // Extract ref updates
504 // Return structured data
505}
506```
507
508### 2. Maintainer Recursion
509
510The maintainer resolution must handle cycles and correctly build the set:
511
512```rust
513fn get_maintainers_recursive(
514 events: &[Event],
515 pubkey: &str,
516 identifier: &str,
517 visited: &mut HashSet<String>,
518) -> HashSet<String> {
519 if visited.contains(pubkey) {
520 return HashSet::new();
521 }
522
523 visited.insert(pubkey.to_string());
524
525 let announcement = find_announcement(events, pubkey, identifier);
526 if announcement.is_none() {
527 return HashSet::new();
528 }
529
530 let repo = parse_repository(announcement.unwrap());
531
532 for maintainer in repo.maintainers {
533 get_maintainers_recursive(events, &maintainer, identifier, visited);
534 }
535
536 visited.clone()
537}
538```
539
540### 3. State Event Validation
541
542```rust
543fn validate_state_ref(
544 state: &RepositoryState,
545 ref_update: &RefUpdate,
546) -> Result<()> {
547 if ref_update.ref_name.starts_with("refs/heads/") {
548 let branch_name = &ref_update.ref_name[11..];
549 if let Some(commit) = state.branches.get(branch_name) {
550 if commit == &ref_update.new_sha {
551 return Ok(());
552 }
553 return Err(Error::StateMismatch {
554 ref_name: ref_update.ref_name.clone(),
555 expected: commit.clone(),
556 got: ref_update.new_sha.clone(),
557 });
558 }
559 return Err(Error::RefNotInState(ref_update.ref_name.clone()));
560 }
561
562 if ref_update.ref_name.starts_with("refs/tags/") {
563 let tag_name = &ref_update.ref_name[10..];
564 if let Some(commit) = state.tags.get(tag_name) {
565 if commit == &ref_update.new_sha {
566 return Ok(());
567 }
568 return Err(Error::StateMismatch {
569 ref_name: ref_update.ref_name.clone(),
570 expected: commit.clone(),
571 got: ref_update.new_sha.clone(),
572 });
573 }
574 return Err(Error::RefNotInState(ref_update.ref_name.clone()));
575 }
576
577 Err(Error::InvalidRef(ref_update.ref_name.clone()))
578}
579```
580
581### 4. CORS Support
582
583As per GRASP-01, we must support CORS:
584
585```rust
586use actix_cors::Cors;
587
588fn configure_cors() -> Cors {
589 Cors::default()
590 .allow_any_origin()
591 .allowed_methods(vec!["GET", "POST", "OPTIONS"])
592 .allowed_headers(vec!["Content-Type"])
593 .max_age(3600)
594}
595
596// In main.rs
597App::new()
598 .wrap(configure_cors())
599 .configure(git_routes)
600 .configure(nostr_routes)
601```
602
603## Testing Strategy
604
605See [TEST_STRATEGY.md](TEST_STRATEGY.md) for comprehensive testing documentation, including:
606
607- **GRASP Compliance Testing Tool**: Reusable test suite that validates any GRASP implementation against the spec
608- **Spec-Mirrored Tests**: Test structure matches GRASP protocol documents exactly
609- **Clear Failure Messages**: Test failures cite exact spec lines (e.g., "GRASP-01:12-13")
610- **Multiple Test Levels**: Unit, integration, compliance, and end-to-end tests
611
612### Quick Overview
613
614```rust
615// Unit Tests - Individual functions
616#[test]
617fn test_parse_ref_updates() {
618 let body = b"0000... 0000... refs/heads/main\0report-status\n";
619 let updates = parse_ref_updates(body).unwrap();
620 assert_eq!(updates.len(), 1);
621 assert_eq!(updates[0].ref_name, "refs/heads/main");
622}
623
624// Integration Tests - Component interaction
625#[tokio::test]
626async fn test_full_push_flow() {
627 let app = test_app().await;
628 let (announcement, state) = app.create_repo_with_state()
629 .branch("main", "commit-123")
630 .build()
631 .await;
632
633 let result = app.git_push("main", "commit-123").await;
634 assert!(result.success);
635}
636
637// Compliance Tests - GRASP spec validation
638#[tokio::test]
639async fn test_grasp_01_compliance() {
640 use grasp_compliance_tests::{TestContext, Grasp01Spec};
641
642 let ctx = TestContext::builder()
643 .base_url(&server.url())
644 .build();
645
646 let results = Grasp01Spec::test_compliance(&ctx).await;
647 assert!(results.all_passed(), "{}", results.report());
648}
649```
650
651The compliance testing tool is designed as a **standalone crate** that can be:
652- Used by ngit-grasp for self-validation
653- Published for other GRASP implementations to use
654- Updated as new GRASP specs are released
655- Run in CI/CD for continuous compliance verification
656
657## Performance Considerations
658
659### 1. Async All The Way
660
661- Use `tokio` for all I/O
662- Non-blocking Git subprocess spawning
663- Stream large pack files without buffering
664
665### 2. Connection Pooling
666
667- Reuse Nostr relay connections
668- Connection pool for internal relay queries
669
670### 3. Caching
671
672- Cache parsed state events (with TTL)
673- Cache maintainer sets
674- Invalidate on new state events
675
676```rust
677pub struct StateCache {
678 cache: Arc<RwLock<HashMap<String, CachedState>>>,
679}
680
681struct CachedState {
682 state: RepositoryState,
683 maintainers: Vec<String>,
684 timestamp: Instant,
685}
686
687impl StateCache {
688 pub async fn get_or_fetch(
689 &self,
690 identifier: &str,
691 fetcher: impl Future<Output = Result<(RepositoryState, Vec<String>)>>,
692 ) -> Result<(RepositoryState, Vec<String>)> {
693 // Check cache
694 // Return if fresh
695 // Otherwise fetch and cache
696 }
697}
698```
699
700## Future Extensions
701
702### GRASP-02: Proactive Sync
703
704Add background tasks:
705
706```rust
707pub struct ProactiveSyncTask {
708 relay_client: Client,
709 git_manager: RepositoryManager,
710}
711
712impl ProactiveSyncTask {
713 pub async fn run(&self) {
714 loop {
715 tokio::time::sleep(Duration::from_secs(3600)).await;
716
717 // Fetch all announcements from our relay
718 let announcements = self.fetch_announcements().await;
719
720 for ann in announcements {
721 // Sync events from listed relays
722 self.sync_events(&ann).await;
723
724 // Sync git data from listed clones
725 self.sync_git_data(&ann).await;
726
727 // Fetch PR data
728 self.sync_pr_data(&ann).await;
729 }
730 }
731 }
732}
733```
734
735### GRASP-05: Archive
736
737Relax the policy:
738
739```rust
740pub struct ArchiveAnnouncementPolicy;
741
742impl WritePolicy for ArchiveAnnouncementPolicy {
743 fn admit_event(&self, event: &Event, _addr: &SocketAddr)
744 -> BoxFuture<PolicyResult>
745 {
746 // Accept all repository announcements
747 // Don't check clone/relays tags
748 PolicyResult::Accept
749 }
750}
751```
752
753## Deployment
754
755### Single Binary
756
757```bash
758cargo build --release
759./target/release/ngit-grasp
760```
761
762### Docker
763
764```dockerfile
765FROM rust:1.75 as builder
766WORKDIR /app
767COPY . .
768RUN cargo build --release
769
770FROM debian:bookworm-slim
771RUN apt-get update && apt-get install -y git && rm -rf /var/lib/apt/lists/*
772COPY --from=builder /app/target/release/ngit-grasp /usr/local/bin/
773EXPOSE 8080
774CMD ["ngit-grasp"]
775```
776
777### Systemd
778
779```ini
780[Unit]
781Description=ngit-grasp GRASP server
782After=network.target
783
784[Service]
785Type=simple
786User=git
787WorkingDirectory=/opt/ngit-grasp
788EnvironmentFile=/opt/ngit-grasp/.env
789ExecStart=/usr/local/bin/ngit-grasp
790Restart=on-failure
791
792[Install]
793WantedBy=multi-user.target
794```
795
796## Security Considerations
797
7981. **Input Validation**: All npub/identifier inputs must be validated
7992. **Path Traversal**: Prevent directory traversal in repository paths
8003. **DoS Protection**: Rate limiting on both HTTP and WebSocket
8014. **Resource Limits**: Limit pack file sizes, event sizes
8025. **Nostr Event Validation**: Strict signature verification
803
804## Conclusion
805
806The inline authorization approach provides a cleaner, more maintainable architecture than hook-based authorization while maintaining full GRASP-01 compliance. The Rust ecosystem provides excellent libraries for both Git and Nostr protocols, enabling a high-performance, type-safe implementation.
807
808The key insight is that we don't need to rely on Git's hook mechanism when we have full control over the HTTP layer that Git operates through. By intercepting at the HTTP handler level, we gain better error handling, easier testing, and tighter integration between the Git and Nostr components.