preparing to build grasp-audit against git-relay

This commit is contained in:
DanConwayDev
2025-11-05 06:37:21 +00:00
parent 9ae69e7f68
commit 5cd47079ee
16 changed files with 6624 additions and 217 deletions
@@ -0,0 +1,299 @@
╔═══════════════════════════════════════════════════════════════════════════════╗
║ GIT PUSH AUTHORIZATION FLOW (INLINE) ║
╚═══════════════════════════════════════════════════════════════════════════════╝
┌─────────────────────────────────────────────────────────────────────────────┐
│ CLIENT: git push │
└────────────────────────────────┬────────────────────────────────────────────┘
│
│ HTTP POST
│ /npub/repo.git/git-receive-pack
▼
┌─────────────────────────────────────────────────────────────────────────────┐
│ ACTIX-WEB ROUTER (git-http-backend) │
│ │
│ Route: /{namespace}/{repo}/git-receive-pack │
│ Handler: git_receive_pack() │
└────────────────────────────────┬────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────────────────┐
│ STEP 1: RESOLVE REPOSITORY PATH │
│ │
│ GitConfig::rewrite("/npub/repo") → /data/git/npub/repo.git │
└────────────────────────────────┬────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────────────────┐
│ STEP 2: VALIDATE REPOSITORY EXISTS │
│ │
│ ✓ Check HEAD exists │
│ ✓ Check config exists │
│ ✓ Check bare = true │
│ │
│ ❌ If not: Return 400 "Repository not found" │
└────────────────────────────────┬────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────────────────┐
│ STEP 3: READ REQUEST BODY (MODIFIED) │
│ │
│ • Read full request body into memory │
│ • Decode gzip if Content-Encoding: gzip │
│ • Store in body_data: Vec<u8> │
└────────────────────────────────┬────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────────────────┐
│ STEP 4: PARSE REF UPDATES (NEW!) │
│ │
│ parse_receive_pack_request(&body_data) → Vec<RefUpdate> │
│ │
│ Git Pack Protocol: │
│ ┌──────────────────────────────────────────────────────────────┐ │
│ │ 0000000000000000000000000000000000000000 a1b2c3d4e5f6... │ │
│ │ refs/heads/main\0 report-status\n │ │
│ │ │ │
│ │ old_oid: 0000... (new branch) │ │
│ │ new_oid: a1b2c3d4e5f6... │ │
│ │ ref_name: refs/heads/main │ │
│ └──────────────────────────────────────────────────────────────┘ │
│ │
│ Result: RefUpdate { │
│ old_oid: "0000...", │
│ new_oid: "a1b2c3d4e5f6...", │
│ ref_name: "refs/heads/main" │
│ } │
└────────────────────────────────┬────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────────────────┐
│ STEP 5: VALIDATE AUTHORIZATION (NEW!) │
│ │
│ validator.validate_push(npub, identifier, &ref_updates).await │
│ │
│ ┌────────────────────────────────────────────────────────────┐ │
│ │ PushValidator::validate_push() │ │
│ │ │ │
│ │ 1. Get latest state event from Nostr relay │ │
│ │ • Query: kind=30618, d=identifier, author=npub │ │
│ │ • Extract refs from state event │ │
│ │ │ │
│ │ 2. For each ref update: │ │
│ │ • If refs/heads/* or refs/tags/*: │ │
│ │ - Check state event has matching ref │ │
│ │ - Check new_oid matches state event oid │ │
│ │ - ❌ Reject if mismatch │ │
│ │ • If refs/nostr/*: │ │
│ │ - ✅ Always allow (PRs) │ │
│ │ │ │
│ │ 3. Get maintainers (recursive) │ │
│ │ • Extract maintainers from announcement │ │
│ │ • Recursively resolve maintainer sets │ │
│ │ • Check if pusher is in maintainer list │ │
│ │ • ❌ Reject if not maintainer │ │
│ │ │ │
│ │ 4. Return Ok(()) or Err(message) │ │
│ └────────────────────────────────────────────────────────────┘ │
│ │
│ ❌ If validation fails: │
│ Return 403 Forbidden │
│ { │
│ "error": "unauthorized", │
│ "message": "Push rejected: refs/heads/main points to ..., state has..."│
│ } │
└────────────────────────────────┬────────────────────────────────────────────┘
│
│ ✅ AUTHORIZED
▼
┌─────────────────────────────────────────────────────────────────────────────┐
│ STEP 6: SPAWN GIT RECEIVE-PACK (EXISTING) │
│ │
│ Command::new("git") │
│ .arg("receive-pack") │
│ .arg("--stateless-rpc") │
│ .arg(".") │
│ .current_dir(&repo_path) │
│ .spawn() │
│ │
│ • Write body_data to git stdin │
│ • Stream git stdout back to client │
└────────────────────────────────┬────────────────────────────────────────────┘
│
│ Stream response
▼
┌─────────────────────────────────────────────────────────────────────────────┐
│ CLIENT: git push success │
└─────────────────────────────────────────────────────────────────────────────┘
╔═══════════════════════════════════════════════════════════════════════════════╗
║ COMPARISON: BEFORE vs AFTER ║
╚═══════════════════════════════════════════════════════════════════════════════╝
┌─────────────────────────────────┬─────────────────────────────────────────┐
│ BEFORE (git-http-backend) │ AFTER (our fork) │
├─────────────────────────────────┼─────────────────────────────────────────┤
│ 1. Resolve path │ 1. Resolve path │
│ 2. Check bare repo │ 2. Check bare repo │
│ 3. Read request body │ 3. Read request body │
│ 4. Spawn git immediately ❌ │ 4. Parse ref updates ← NEW │
│ 5. Stream response │ 5. Validate authorization ← NEW │
│ │ 6. Spawn git (if authorized) │
│ │ 7. Stream response │
└─────────────────────────────────┴─────────────────────────────────────────┘
┌─────────────────────────────────┬─────────────────────────────────────────┐
│ AUTHORIZATION │ METHOD │
├─────────────────────────────────┼─────────────────────────────────────────┤
│ ❌ None │ No validation │
│ ⚠️ Git hooks (pre-receive) │ After git accepts push │
│ ✅ Inline (our approach) │ Before git touches repository │
└─────────────────────────────────┴─────────────────────────────────────────┘
╔═══════════════════════════════════════════════════════════════════════════════╗
║ KEY MODIFICATIONS NEEDED ║
╚═══════════════════════════════════════════════════════════════════════════════╝
┌─────────────────────────────────────────────────────────────────────────────┐
│ FILE: src/actix/git_receive_pack.rs │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ CHANGE 1: Add validator parameter │
│ ──────────────────────────────────────────────────────────────────────── │
│ pub async fn git_receive_pack( │
│ request: HttpRequest, │
│ mut payload: Payload, │
│ service: web::Data<impl GitConfig>, │
│ + validator: web::Data<PushValidator>, // ← ADD THIS │
│ ) -> impl Responder { │
│ │
│ CHANGE 2: Parse ref updates after reading body │
│ ──────────────────────────────────────────────────────────────────────── │
│ // Read and decode body (existing) │
│ let body_data = read_and_decode_body(&mut payload, &request).await?; │
│ │
│ + // Parse ref updates (NEW) │
│ + let ref_updates = parse_receive_pack_request(&body_data)?; │
│ │
│ CHANGE 3: Validate before spawning git │
│ ──────────────────────────────────────────────────────────────────────── │
│ + // Extract repo info from path │
│ + let (npub, identifier) = extract_repo_info(&request.uri().path())?; │
│ + │
│ + // Validate authorization │
│ + if let Err(e) = validator.validate_push(&npub, &identifier, │
│ + &ref_updates).await { │
│ + return HttpResponse::Forbidden() │
│ + .json(json!({ │
│ + "error": "unauthorized", │
│ + "message": e.to_string(), │
│ + })); │
│ + } │
│ │
│ // Spawn git (existing, unchanged) │
│ let mut cmd = Command::new("git"); │
│ // ... rest of existing code ... │
└─────────────────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────────────┐
│ NEW FILE: src/git/protocol.rs │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ pub struct RefUpdate { │
│ pub old_oid: String, │
│ pub new_oid: String, │
│ pub ref_name: String, │
│ } │
│ │
│ pub fn parse_receive_pack_request(body: &[u8]) -> Result<Vec<RefUpdate>> { │
│ // Parse git pack protocol │
│ // Extract ref updates from pkt-line format │
│ } │
└─────────────────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────────────┐
│ NEW FILE: src/git/authorization.rs │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ pub struct PushValidator { │
│ storage: Storage, │
│ } │
│ │
│ impl PushValidator { │
│ pub async fn validate_push( │
│ &self, │
│ npub: &str, │
│ identifier: &str, │
│ updates: &[RefUpdate], │
│ ) -> Result<()> { │
│ // Query Nostr relay for state event │
│ // Validate each ref update │
│ // Check maintainer permissions │
│ } │
│ } │
└─────────────────────────────────────────────────────────────────────────────┘
╔═══════════════════════════════════════════════════════════════════════════════╗
║ BENEFITS OF INLINE AUTH ║
╚═══════════════════════════════════════════════════════════════════════════════╝
✅ BETTER ERROR MESSAGES
• Return 403 with JSON error details
• Show exactly which ref failed validation
• Show expected vs. actual commit
• Better developer experience
✅ SIMPLER DEPLOYMENT
• No git hooks to manage
• No symlinks or hook installation
• Single binary handles everything
• Easier to test
✅ TIGHTER INTEGRATION
• Direct access to Nostr relay state
• Shared storage layer
• No IPC between components
• Atomic validation
✅ EASIER TESTING
• Pure Rust unit tests
• Mock validator for testing
• No subprocess coordination
• Deterministic behavior
✅ SECURITY
• Validation before git touches repo
• Can't bypass by manipulating hooks
• Centralized authorization logic
• Audit trail in application logs
╔═══════════════════════════════════════════════════════════════════════════════╗
║ TIMELINE ║
╚═══════════════════════════════════════════════════════════════════════════════╝
Week 1: Foundation
├─ Day 1-2: Fork git-http-backend, set up integration
├─ Day 3-4: Add git2, implement GitRepository
└─ Day 5: Add protocol parsing module
Week 2: Authorization
├─ Day 1-2: Implement PushValidator
├─ Day 3-4: Modify git_receive_pack handler
└─ Day 5: Integration tests
Week 3: Polish
├─ Day 1-2: Add CORS support
├─ Day 3-4: Error handling improvements
└─ Day 5: E2E tests with real git
Week 4: Compliance
├─ Day 1-3: GRASP-01 compliance testing
├─ Day 4: Performance testing
└─ Day 5: Documentation
Status: ✅ Analysis complete, ready to implement
@@ -0,0 +1,714 @@
**ARCHIVED: 2025-11-04**
**Reason:** Analysis complete, crate validated
**Outcome:** Confirmed suitable for use (with fork for authorization)
---
# git-http-backend Crate Deep Dive
**Date:** 2025-11-04
**Status:** ✅ ARCHIVED - Analysis Complete
**Purpose:** Validate the recommendation in `work/current_status.md` regarding git-http-backend crate
---
## Executive Summary
**Recommendation Status:** ✅ **VALIDATED WITH CAVEATS**
The `git-http-backend` crate (v0.1.3) is a **good foundation** but requires significant customization for our inline authorization needs. The hybrid approach recommended in `current_status.md` is sound, but we'll need to:
1. **Fork or vendor** the crate for customization
2. **Add interception points** for authorization
3. **Enhance error handling** for better push rejection messages
4. **Add CORS support** (missing from current implementation)
---
## Crate Overview
### Basic Info
- **Name:** `git-http-backend`
- **Version:** 0.1.3
- **Author:** lazhenyi
- **License:** MIT
- **Repository:** https://github.com/lazhenyi/git-http-backend
- **Documentation:** https://docs.rs/git-http-backend/0.1.3
### Dependencies
```toml
tokio = { version = "1", features = ["sync","macros","rt", "rt-multi-thread","net"] }
actix-web = { version = "4.9.0", features = ["default"] }
actix-files = { version = "0.6.6", features = ["actix-server"] }
futures-util = { version = "0.3.31", features = ["futures-channel"] }
flate2 = "1.0.35" # Gzip compression
async-stream = "0.3.6" # Streaming responses
async-trait = "0.1.83" # Async trait support
```
**Good news:** Already uses actix-web 4.9.0 (same as we plan to use)
---
## Architecture Analysis
### Core Design
The crate provides:
1. **GitConfig Trait** - Path rewriting abstraction
2. **Actix Router** - Pre-configured routes for Git Smart HTTP
3. **Protocol Handlers** - Upload-pack, receive-pack, info/refs
4. **System Git Integration** - Spawns `git` subprocess
### URL Structure
```
/{namespace}/{repo}/info/refs?service=git-upload-pack
/{namespace}/{repo}/git-upload-pack
/{namespace}/{repo}/git-receive-pack
/{namespace}/{repo}/HEAD
/{namespace}/{repo}/objects/info/packs
/{namespace}/{repo}/objects/pack/{pack}
```
**Perfect match** for our `/{npub}/{identifier}.git/` structure!
### Request Flow
```
HTTP Request
↓
Actix Router → Handler Function
↓
GitConfig::rewrite() → Path resolution
↓
Spawn git subprocess (upload-pack/receive-pack)
↓
Stream response back to client
```
---
## Key Handlers Analysis
### 1. info/refs Handler (refs.rs)
**Purpose:** Advertise repository refs (clone/fetch discovery)
**Flow:**
1. Parse `service` query param (upload-pack or receive-pack)
2. Resolve repository path via `GitConfig::rewrite()`
3. Spawn `git upload-pack --advertise-refs --stateless-rpc .`
4. Return with proper content-type header
**Code:**
```rust
pub async fn info_refs(request: HttpRequest, service: web::Data<impl GitConfig>) -> impl Responder {
let uri = request.uri();
let path = uri.path().to_string().replace("/info/refs", "");
let path = service.rewrite(path).await;
// Parse service from query
let service = query.split('=').map(|x| x.to_string()).collect::<Vec<_>>()[1].clone();
// Spawn git
let mut cmd = Command::new("git");
cmd.arg(service_name.clone());
cmd.arg("--stateless-rpc");
cmd.arg("--advertise-refs");
cmd.arg(".");
cmd.current_dir(path);
// Return response with proper headers
resp.append_header(("Content-Type", format!("application/x-git-{}-advertisement", service_name)));
resp.append_header(("Cache-Control", "no-cache, max-age=0, must-revalidate"));
}
```
**Good:**
- ✅ Proper content-type headers
- ✅ Cache control headers
- ✅ Git protocol version support (Git-Protocol header)
**Issues:**
- ❌ No CORS headers
- ❌ No error handling for missing repos
- ❌ Query parsing is fragile (will panic on malformed input)
### 2. git-upload-pack Handler (git_upload_pack.rs)
**Purpose:** Handle clone/fetch operations (read-only)
**Flow:**
1. Resolve repository path
2. Read request body (may be gzipped)
3. Spawn `git upload-pack --stateless-rpc .`
4. Stream response back
**Code:**
```rust
pub async fn git_upload_pack(
request: HttpRequest,
mut payload: Payload,
service: web::Data<impl GitConfig>,
) -> impl Responder {
// Resolve path
let path = service.rewrite(path).await;
// Spawn git
let mut cmd = Command::new("git");
cmd.arg("upload-pack");
cmd.arg("--stateless-rpc");
cmd.arg(".");
cmd.current_dir(path);
let mut span = cmd.spawn()?;
let mut stdin = span.stdin.take().unwrap();
let mut stdout = span.stdout.take().unwrap();
// Read request body
let mut bytes = web::BytesMut::new();
while let Some(chunk) = payload.next().await {
bytes.extend_from_slice(&data);
}
// Handle gzip
let body_data = match encoding {
Some("gzip") => decode_gzip(bytes),
_ => bytes.to_vec(),
};
// Write to git stdin
stdin.write_all(&body_data)?;
drop(stdin);
// Stream response
let body_stream = actix_web::body::BodyStream::new(async_stream::stream! {
let mut buffer = [0; 8192];
loop {
match stdout.read(&mut buffer) {
Ok(0) => break,
Ok(n) => yield Ok(web::Bytes::copy_from_slice(&buffer[..n])),
Err(e) => break,
}
}
});
resp.body(body_stream)
}
```
**Good:**
- ✅ Handles gzip compression
- ✅ Streams response (efficient for large repos)
- ✅ Proper content-type headers
**Issues:**
- ❌ No CORS headers
- ❌ No repository existence check
- ❌ Error handling uses eprintln! (not tracing)
**For our use:** Upload-pack is read-only, so we can use as-is (just add CORS)
### 3. git-receive-pack Handler (git_receive_pack.rs) ⚠️
**Purpose:** Handle push operations (write)
**This is the critical handler for inline authorization!**
**Current Flow:**
1. Resolve repository path
2. **Check if bare repository** (good!)
3. Read request body (may be gzipped)
4. Spawn `git receive-pack --stateless-rpc .`
5. Stream response back
**Code:**
```rust
pub async fn git_receive_pack(
request: HttpRequest,
mut payload: Payload,
service: web::Data<impl GitConfig>,
) -> impl Responder {
let path = service.rewrite(path).await;
// Check repository exists
if !path.join("HEAD").exists() || !path.join("config").exists() {
return HttpResponse::BadRequest().body("Repository not found or invalid.");
}
// Check if bare
let is_bare_repo = match std::fs::read_to_string(path.join("config")) {
Ok(config) => config.contains("bare = true"),
Err(_) => false,
};
if !is_bare_repo {
return HttpResponse::BadRequest().body("Push operation requires a bare repository.");
}
// Spawn git receive-pack
let mut cmd = Command::new("git");
cmd.arg("receive-pack");
cmd.arg("--stateless-rpc");
cmd.arg(".");
cmd.current_dir(&path);
let mut git_process = cmd.spawn()?;
let mut stdin = git_process.stdin.take().unwrap();
let mut stdout = git_process.stdout.take().unwrap();
// Read request body
let mut bytes = web::BytesMut::new();
while let Some(chunk) = payload.next().await {
bytes.extend_from_slice(&data);
}
// Decode if gzipped
let body_data = match encoding {
Some(encoding) if encoding.contains("gzip") => decode_gzip(bytes),
_ => bytes.to_vec(),
};
// Write to git stdin
stdin.write_all(&body_data)?;
drop(stdin);
// Stream response
let body_stream = /* stream stdout */;
resp.body(body_stream)
}
```
**Good:**
- ✅ Validates repository exists
- ✅ Validates bare repository
- ✅ Handles gzip compression
- ✅ Streams response
**Critical Issues for Our Use:**
- ❌ **No authorization hook!** Spawns git immediately
- ❌ **No way to inspect push data** before spawning git
- ❌ **No CORS headers**
- ❌ **Can't reject unauthorized pushes** with custom error
**This is where we need customization!**
---
## Customization Requirements
### 1. Authorization Interception Point
**Need to add BEFORE spawning git:**
```rust
pub async fn git_receive_pack(
request: HttpRequest,
mut payload: Payload,
service: web::Data<impl GitConfig>,
validator: web::Data<PushValidator>, // ← ADD THIS
) -> impl Responder {
let path = service.rewrite(path).await;
// Existing checks...
// Read request body
let body_data = read_and_decode_body(&mut payload, &request).await?;
// ← ADD AUTHORIZATION HERE
let ref_updates = parse_receive_pack_request(&body_data)?;
// Extract npub and identifier from path
let (npub, identifier) = extract_repo_info(&request.uri().path())?;
// Validate against Nostr state
if let Err(e) = validator.validate_push(&npub, &identifier, &ref_updates).await {
return HttpResponse::Forbidden()
.json(json!({
"error": "unauthorized",
"message": e.to_string(),
"ref_updates": ref_updates,
}));
}
// Only spawn git if authorized
let mut cmd = Command::new("git");
// ... rest of existing code
}
```
### 2. Parse Git Protocol
**Need to add protocol parsing:**
```rust
// src/git/protocol.rs
pub struct RefUpdate {
pub old_oid: String,
pub new_oid: String,
pub ref_name: String,
}
pub fn parse_receive_pack_request(body: &[u8]) -> Result<Vec<RefUpdate>> {
// Parse git pack protocol
// Format: <old-oid> <new-oid> <ref-name>\0<capabilities>\n
// Example: 0000000000000000000000000000000000000000 a1b2c3d4... refs/heads/main\0 report-status\n
let mut updates = Vec::new();
let lines = body.split(|&b| b == b'\n');
for line in lines {
if line.is_empty() {
continue;
}
// Parse pkt-line format
// First 4 bytes are hex length
let pkt_len = parse_pkt_len(&line[0..4])?;
if pkt_len == 0 {
continue; // flush packet
}
let data = &line[4..pkt_len];
let parts: Vec<&[u8]> = data.splitn(3, |&b| b == b' ').collect();
if parts.len() >= 3 {
let old_oid = String::from_utf8_lossy(parts[0]).to_string();
let new_oid = String::from_utf8_lossy(parts[1]).to_string();
// Ref name may have capabilities after \0
let ref_data = parts[2];
let ref_name = if let Some(null_pos) = ref_data.iter().position(|&b| b == b'\0') {
String::from_utf8_lossy(&ref_data[..null_pos]).to_string()
} else {
String::from_utf8_lossy(ref_data).to_string()
};
updates.push(RefUpdate {
old_oid,
new_oid,
ref_name,
});
}
}
Ok(updates)
}
```
**Note:** Git pack protocol is complex. We may want to use a library for this:
- `git2` crate has protocol parsing
- Or we can implement minimal parsing for our needs
### 3. Add CORS Support
**Need to add to all handlers:**
```rust
// Add CORS middleware or headers to all responses
resp.append_header(("Access-Control-Allow-Origin", "*"));
resp.append_header(("Access-Control-Allow-Methods", "GET, POST, OPTIONS"));
resp.append_header(("Access-Control-Allow-Headers", "Content-Type, Git-Protocol"));
```
### 4. Better Error Handling
**Replace eprintln! with tracing:**
```rust
use tracing::{error, info, debug};
// Instead of:
eprintln!("Error running command: {}", e);
// Use:
error!(error = ?e, "Failed to spawn git process");
```
---
## Integration Strategy
### Option A: Fork the Crate ✅ RECOMMENDED
**Pros:**
- Full control over authorization logic
- Can add CORS, error handling, protocol parsing
- Can publish as `ngit-grasp-git-http-backend`
- Keep upstream changes visible
**Cons:**
- Need to maintain fork
- Diverges from upstream
**Implementation:**
1. Fork https://github.com/lazhenyi/git-http-backend
2. Add to our workspace as git submodule or copy
3. Modify `git_receive_pack.rs` to add authorization
4. Add protocol parsing module
5. Add CORS support
6. Improve error handling
### Option B: Vendor the Code
**Pros:**
- Complete control
- No external dependency
- Can heavily customize
**Cons:**
- Lose upstream updates
- More code to maintain
**Implementation:**
1. Copy source into `src/git/http_backend/`
2. Modify as needed
3. No external dependency
### Option C: Wrap the Crate
**Pros:**
- Keep upstream crate
- Add authorization via middleware
**Cons:**
- ❌ **Can't intercept before git spawns!**
- Would need to parse response, too late
- Complex to inject validator
**Not recommended** - can't achieve inline authorization
---
## Recommended Approach
### Use Forked git-http-backend + git2 + System Git
**Architecture:**
```
HTTP Request
↓
Actix Router (from forked git-http-backend)
↓
Custom GitConfig Implementation
↓
git_receive_pack Handler (MODIFIED)
↓
┌─────────────────────────────────┐
│ 1. Read request body │
│ 2. Parse ref updates (protocol) │ ← ADD THIS
│ 3. Validate via PushValidator │ ← ADD THIS
│ ├─ Query Nostr relay │
│ ├─ Check state event │
│ └─ Validate maintainers │
│ 4. If authorized: │
│ └─ Spawn git receive-pack │ ← EXISTING
│ 5. If unauthorized: │
│ └─ Return 403 with error │ ← ADD THIS
└─────────────────────────────────┘
↓
Stream response to client
```
**Dependencies:**
```toml
[dependencies]
# Fork of git-http-backend (or vendored code)
git-http-backend = { git = "https://github.com/our-org/git-http-backend", branch = "ngit-grasp" }
# Or vendor it:
# (no dependency, code in src/git/http_backend/)
# Git operations
git2 = "0.20" # For repository management, ref queries
# Already have:
actix-web = "4.9"
tokio = { version = "1", features = ["full"] }
nostr-sdk = "0.43"
```
**Implementation Plan:**
1. **Phase 1: Fork & Setup**
- Fork git-http-backend
- Add to our project (git submodule or copy)
- Verify existing functionality works
2. **Phase 2: Protocol Parsing**
- Add `src/git/protocol.rs`
- Implement `parse_receive_pack_request()`
- Unit tests for protocol parsing
3. **Phase 3: Authorization Integration**
- Modify `git_receive_pack.rs`
- Add `PushValidator` parameter
- Call validator before spawning git
- Return 403 on unauthorized
4. **Phase 4: CORS & Polish**
- Add CORS headers to all handlers
- Improve error messages
- Add tracing instead of eprintln!
5. **Phase 5: Testing**
- Unit tests for authorization
- Integration tests with real git
- GRASP-01 compliance tests
---
## Validation of current_status.md Recommendations
### Hybrid Approach ✅ VALIDATED
**Original recommendation:**
> 1. **git-http-backend** - HTTP protocol handling
> 2. **git2-rs** - Repository management, ref validation
> 3. **System git** - Actual pack operations (upload-pack/receive-pack)
**Analysis:**
- ✅ **git-http-backend** - Good foundation, needs customization
- ✅ **git2** - Perfect for repo management (init, refs, validation)
- ✅ **System git** - Proven pack protocol implementation
**Verdict:** Sound approach, but need to fork/vendor git-http-backend
### Tool Selection ✅ CORRECT
**Original analysis:**
- git2 for repository management ✅
- System git for pack operations ✅
- git-http-backend for HTTP layer ✅ (with modifications)
**Additional findings:**
- Need protocol parsing (can use git2 or implement minimal)
- Need CORS support (add to fork)
- Need better error handling (add to fork)
### Inline Authorization ✅ ACHIEVABLE
**Original goal:**
> We intercept the `git-receive-pack` operation before spawning the Git process
**Analysis:**
- ✅ Possible by modifying `git_receive_pack.rs`
- ✅ Can parse request body before spawning git
- ✅ Can return 403 before git touches repository
**Requirement:**
- Must fork or vendor git-http-backend
- Can't achieve with unmodified crate
---
## Updated Implementation Plan
### Week 1: Foundation (UPDATED)
1. ✅ Add git2 dependency
2. **Fork git-http-backend** (NEW)
3. **Add protocol parsing** (NEW)
4. Implement GitRepository (Phase 1)
5. Write unit tests for repository operations
6. Test repository creation from announcements
### Week 2: Protocol & Authorization
1. Implement protocol parsing (Phase 2)
2. Implement authorization logic (Phase 3)
3. **Modify git_receive_pack handler** (NEW)
4. Write unit tests for both
5. Integration tests for validation
### Week 3: HTTP & Integration
1. **Add CORS support to fork** (NEW)
2. Implement HTTP handlers (Phase 4)
3. Integrate with Nostr events (Phase 5)
4. Integration tests for full flow
5. Error handling improvements
### Week 4: E2E & Polish
1. E2E tests with real git (Phase 6)
2. Performance testing
3. GRASP-01 compliance testing
4. Documentation and examples
---
## Risks & Mitigations
### Risk 1: Fork Maintenance
**Risk:** Fork diverges from upstream, miss updates
**Mitigation:**
- Keep fork minimal (only modify git_receive_pack.rs)
- Document all changes clearly
- Consider upstreaming authorization hooks
- Monitor upstream for security fixes
### Risk 2: Protocol Parsing Complexity
**Risk:** Git pack protocol is complex, may miss edge cases
**Mitigation:**
- Use git2 for protocol parsing if available
- Implement minimal parsing (just ref updates)
- Extensive testing with real git clients
- Refer to Git protocol documentation
### Risk 3: Performance
**Risk:** Authorization adds latency to push operations
**Mitigation:**
- Keep validation logic fast (< 100ms target)
- Cache state events in memory
- Async validation (don't block)
- Profile and optimize
---
## Conclusion
### Summary
The **hybrid approach** recommended in `current_status.md` is **sound and validated**, with these adjustments:
1. **Fork or vendor git-http-backend** - Can't use unmodified crate
2. **Add protocol parsing** - Need to parse ref updates from request
3. **Modify git_receive_pack handler** - Add authorization before spawning git
4. **Add CORS support** - Missing from current implementation
5. **Improve error handling** - Better messages for push rejections
### Next Steps
1. ✅ **Review this analysis** - Confirm approach
2. **Fork git-http-backend** - Set up fork/vendor
3. **Start Phase 1** - Add git2, implement GitRepository
4. **Add protocol parsing** - Parse ref updates from pack protocol
5. **Modify receive-pack handler** - Add authorization logic
### Questions for Review
1. **Fork vs. Vendor?** Fork allows upstream tracking, vendor gives full control
2. **Protocol parsing?** Use git2 or implement minimal parser?
3. **CORS scope?** Support all origins or restrict?
4. **Error detail?** How much info to expose in 403 responses?
5. **Performance target?** Is < 100ms for auth validation reasonable?
---
**Status:** ✅ Analysis complete, ready to proceed with implementation
**Recommendation:** Fork git-http-backend, add authorization to git_receive_pack, use git2 for repo management
---
*Analysis Date: November 4, 2025*
@@ -0,0 +1,247 @@
**ARCHIVED: 2025-11-04**
**Reason:** Analysis complete, validated hybrid approach
**Outcome:** Will use git-http-backend (forked) + git2 + system git
---
# Analysis Summary: git-http-backend Validation
**Date:** 2025-11-04
**Status:** ✅ ARCHIVED - Analysis Complete
---
## TL;DR
✅ **VALIDATED:** The hybrid approach in `current_status.md` is sound
⚠️ **CAVEAT:** Must fork/vendor `git-http-backend` crate for inline authorization
✅ **READY:** Can proceed with implementation
---
## Key Findings
### 1. git-http-backend Crate (v0.1.3)
**What it provides:**
- ✅ Actix-web based Git Smart HTTP handlers
- ✅ Upload-pack (clone/fetch) - works as-is
- ✅ Receive-pack (push) - **needs modification**
- ✅ Info/refs advertisement
- ✅ Gzip compression support
- ✅ Streaming responses
**What it lacks:**
- ❌ Authorization hooks (spawns git immediately)
- ❌ CORS headers (needed for web clients)
- ❌ Protocol parsing (can't inspect push data)
- ❌ Proper error handling (uses eprintln!)
### 2. Critical Handler: git_receive_pack
**Current flow:**
```
Request → Validate bare repo → Spawn git → Stream response
```
**What we need:**
```
Request → Validate bare repo → Parse ref updates → Validate against Nostr state → Spawn git (if authorized) → Stream response
↑
ADD THIS
```
**Can't achieve with unmodified crate!**
### 3. Recommended Solution
**Fork the crate and modify `git_receive_pack.rs`:**
```rust
pub async fn git_receive_pack(
request: HttpRequest,
mut payload: Payload,
service: web::Data<impl GitConfig>,
validator: web::Data<PushValidator>, // ← ADD
) -> impl Responder {
// ... existing path resolution and bare check ...
// Read request body
let body_data = read_and_decode_body(&mut payload, &request).await?;
// ← ADD: Parse ref updates
let ref_updates = parse_receive_pack_request(&body_data)?;
// ← ADD: Validate authorization
let (npub, identifier) = extract_repo_info(&request.uri().path())?;
if let Err(e) = validator.validate_push(&npub, &identifier, &ref_updates).await {
return HttpResponse::Forbidden()
.json(json!({
"error": "unauthorized",
"message": e.to_string(),
}));
}
// Only spawn git if authorized
let mut cmd = Command::new("git");
cmd.arg("receive-pack");
// ... rest of existing code ...
}
```
---
## Updated Implementation Plan
### Phase 0: Setup (NEW)
1. Fork git-http-backend repository
2. Add as git submodule or vendor code
3. Verify existing functionality works
4. Add to Cargo.toml
### Phase 1: Foundation
1. Add git2 dependency
2. Implement GitRepository (repo management)
3. Add protocol parsing module
4. Unit tests for both
### Phase 2: Authorization
1. Modify git_receive_pack handler
2. Implement PushValidator
3. Integration tests for validation
4. Test unauthorized rejection
### Phase 3: Polish
1. Add CORS headers to all handlers
2. Improve error messages
3. Add tracing instead of eprintln!
4. E2E tests with real git
---
## Dependencies
```toml
[dependencies]
# Forked git-http-backend with authorization support
git-http-backend = { git = "https://github.com/our-org/git-http-backend", branch = "ngit-grasp" }
# Git repository management
git2 = "0.20"
# Already have:
actix-web = "4.9"
tokio = { version = "1", features = ["full"] }
nostr-sdk = "0.43"
```
---
## Validation of current_status.md
### ✅ Hybrid Approach - CONFIRMED
- git-http-backend for HTTP layer ✅ (with fork)
- git2 for repository management ✅
- System git for pack operations ✅
### ✅ Inline Authorization - ACHIEVABLE
- Can intercept before spawning git ✅
- Can parse ref updates ✅
- Can validate against Nostr state ✅
- Can return 403 with error message ✅
### ⚠️ Additional Requirements
- Must fork/vendor git-http-backend
- Must implement protocol parsing
- Must add CORS support
- Must improve error handling
---
## Risks & Mitigations
| Risk | Impact | Mitigation |
|------|--------|------------|
| Fork maintenance | Medium | Keep changes minimal, document well |
| Protocol parsing complexity | Medium | Use git2 or implement minimal parser |
| Performance overhead | Low | Keep validation fast (< 100ms), cache state |
| Missing edge cases | Medium | Extensive testing with real git clients |
---
## Next Steps
1. **Decision:** Fork vs. vendor git-http-backend?
- Fork: Keep upstream tracking, easier updates
- Vendor: Full control, no external dependency
- **Recommendation:** Fork (easier to contribute back)
2. **Start Phase 0:** Set up fork
- Fork https://github.com/lazhenyi/git-http-backend
- Create branch `ngit-grasp`
- Add as git submodule
3. **Start Phase 1:** Add git2, implement GitRepository
- Write tests first (TDD)
- Focus on bare repo creation, ref management
4. **Add protocol parsing:** Parse ref updates from pack protocol
- Research: Can git2 help?
- Or implement minimal parser
- Unit tests for various push scenarios
5. **Modify receive-pack:** Add authorization logic
- Integration tests for validation
- Test rejection scenarios
---
## Questions for Review
1. **Fork vs. Vendor?**
- Recommendation: Fork (can contribute back, easier updates)
2. **Protocol parsing?**
- Option A: Use git2 if it provides parsing
- Option B: Implement minimal parser (just ref updates)
- Recommendation: Research git2 first, then decide
3. **CORS policy?**
- Allow all origins (`*`) for now?
- Or restrict to configured domains?
- Recommendation: Start with `*`, make configurable later
4. **Error detail?**
- How much info in 403 responses?
- Show ref updates that failed?
- Show expected vs. actual commit?
- Recommendation: Detailed errors for better DX
5. **Performance target?**
- < 100ms for auth validation?
- Cache state events?
- Recommendation: Yes to both
---
## Conclusion
✅ **The hybrid approach is validated and sound**
⚠️ **Must fork git-http-backend for inline authorization**
✅ **Ready to proceed with implementation**
**Confidence Level:** High (95%)
The crate provides exactly what we need as a foundation. The modifications required are straightforward and well-scoped. The main work is:
1. Fork setup
2. Protocol parsing
3. Authorization integration
4. CORS and polish
All achievable within the 4-week timeline.
---
**Next:** Review this analysis, make fork vs. vendor decision, then start Phase 0.
@@ -0,0 +1,590 @@
**ARCHIVED: 2025-11-04**
**Reason:** Decided to validate grasp-audit against ngit-relay first
**See:** docs/archive/2025-11-04-test-strategy-decision.md for rationale
---
# TDD Plan for GRASP-01 Git Backend
**Date:** 2025-11-04
**Status:** ARCHIVED - Superseded by test-first approach
**Goal:** Implement Git Smart HTTP backend with inline authorization using TDD
---
## Current State
### What We Have
- ✅ NIP-01 compliant Nostr relay (working, tested)
- ✅ NIP-34 event handling (announcements accepted)
- ✅ Storage layer (in-memory + disk paths configured)
- ✅ Test infrastructure (integration tests with auto relay management)
- ✅ grasp-audit compliance testing library
### What We Need
- ❌ Git Smart HTTP protocol handler
- ❌ Git repository management (init, receive-pack, upload-pack)
- ❌ Push authorization (validate against Nostr state events)
- ❌ Integration with existing Nostr relay
---
## Tool Selection Analysis
### Option 1: git2-rs (libgit2 bindings)
**Pros:**
- ✅ Pure Rust bindings to battle-tested libgit2
- ✅ Full Git functionality (init, push, pull, refs, objects)
- ✅ Thread-safe, well-maintained
- ✅ Used by cargo, widely deployed
- ✅ Can intercept and validate operations programmatically
**Cons:**
- ❌ Requires libgit2 system dependency
- ❌ Higher-level API - may be overkill for our needs
- ❌ Harder to intercept low-level protocol for inline auth
**Use Cases:**
- Repository initialization
- Reading/writing refs
- Object storage queries
- Validation of commits/trees
### Option 2: Standard git in subprocess
**Pros:**
- ✅ Uses system git (already available)
- ✅ No additional dependencies
- ✅ Battle-tested Git implementation
- ✅ Easy to spawn for upload-pack/receive-pack
**Cons:**
- ❌ Harder to intercept for inline authorization
- ❌ Must parse git protocol to validate pushes
- ❌ Subprocess overhead
- ❌ Complex error handling
**Use Cases:**
- git-upload-pack (clone, fetch)
- git-receive-pack (push) - but need to intercept
### Option 3: git-http-backend crate
**Pros:**
- ✅ Purpose-built for Git Smart HTTP
- ✅ Handles protocol parsing
- ✅ Works with system git
- ✅ Can intercept receive-pack before spawning git
**Cons:**
- ❌ Less mature (but we're already planning to use it per README)
- ❌ Still need to parse pack protocol for validation
**Use Cases:**
- HTTP endpoint handling
- Protocol negotiation
- Spawning git processes
### Option 4: Hybrid Approach (RECOMMENDED)
**Combination:**
1. **git-http-backend** - HTTP protocol handling
2. **git2-rs** - Repository management, ref validation
3. **System git** - Actual pack operations (upload-pack/receive-pack)
**Why Hybrid:**
- git-http-backend handles HTTP → Git protocol translation
- git2 for safe repository operations (init, refs, validation)
- System git for pack operations (proven, fast)
- We intercept at the HTTP layer before spawning git
**Architecture:**
```
HTTP Request → git-http-backend → Our Auth Layer → git2/system git
↓
Nostr Relay
(state validation)
```
---
## Recommended Approach: Hybrid
### Dependencies to Add
```toml
[dependencies]
# Git operations
git2 = "0.20" # Repository management, refs
# git-http-backend - TBD (research if available, or implement minimal)
[dev-dependencies]
tempfile = "3.8" # Temporary repos for testing
```
### Why This Works
1. **git2 for Repository Management:**
- Initialize bare repos when announcements arrive
- Read/write refs safely
- Query repository state
- Validate commits exist
2. **System git for Pack Operations:**
- Spawn `git-upload-pack` for clones/fetches (read-only, safe)
- Spawn `git-receive-pack` ONLY after auth passes
- Leverage proven pack protocol implementation
3. **Inline Authorization:**
- Parse HTTP request to extract ref updates
- Query Nostr relay for latest state event
- Validate push matches state
- Only spawn git-receive-pack if authorized
---
## TDD Implementation Plan
### Phase 1: Repository Management (git2)
**Goal:** Create and manage bare Git repositories
**Tests:**
1. ✅ Create bare repository when announcement received
2. ✅ Initialize with proper config (bare, shared)
3. ✅ Set HEAD from state event
4. ✅ Read refs from repository
5. ✅ Write refs to repository
6. ✅ Query if commit exists in repository
**Implementation:**
```rust
// src/git/repository.rs
pub struct GitRepository {
path: PathBuf,
repo: git2::Repository,
}
impl GitRepository {
pub fn init_bare(path: PathBuf) -> Result<Self>;
pub fn set_head(ref_name: &str) -> Result<()>;
pub fn get_ref(&self, name: &str) -> Result<Option<String>>;
pub fn set_ref(&self, name: &str, oid: &str) -> Result<()>;
pub fn has_commit(&self, oid: &str) -> Result<bool>;
}
```
**Test Example:**
```rust
#[test]
fn test_init_bare_repository() {
let temp = TempDir::new().unwrap();
let repo = GitRepository::init_bare(temp.path().to_path_buf()).unwrap();
assert!(temp.path().join("HEAD").exists());
assert!(temp.path().join("config").exists());
assert!(temp.path().join("objects").exists());
assert!(temp.path().join("refs").exists());
}
```
### Phase 2: Git Protocol Parsing
**Goal:** Parse Git Smart HTTP protocol for authorization
**Tests:**
1. ✅ Parse info/refs request
2. ✅ Parse upload-pack request (clone/fetch)
3. ✅ Parse receive-pack request (push)
4. ✅ Extract ref updates from receive-pack
5. ✅ Extract capabilities from request
**Implementation:**
```rust
// src/git/protocol.rs
pub struct RefUpdate {
pub old_oid: String,
pub new_oid: String,
pub ref_name: String,
}
pub fn parse_receive_pack_request(body: &[u8]) -> Result<Vec<RefUpdate>>;
pub fn parse_capabilities(body: &[u8]) -> Result<Vec<String>>;
```
**Test Example:**
```rust
#[test]
fn test_parse_receive_pack_single_ref() {
let body = b"0000000000000000000000000000000000000000 \
a1b2c3d4e5f6789012345678901234567890abcd \
refs/heads/main\0 report-status\n";
let updates = parse_receive_pack_request(body).unwrap();
assert_eq!(updates.len(), 1);
assert_eq!(updates[0].ref_name, "refs/heads/main");
assert_eq!(updates[0].new_oid, "a1b2c3d4e5f6789012345678901234567890abcd");
}
```
### Phase 3: Authorization Logic
**Goal:** Validate pushes against Nostr state events
**Tests:**
1. ✅ Get maintainers from announcement
2. ✅ Get maintainers recursively
3. ✅ Handle circular maintainer references
4. ✅ Validate ref update matches state
5. ✅ Validate branch push matches state
6. ✅ Validate tag push matches state
7. ✅ Accept push to refs/nostr/*
8. ✅ Reject push not matching state
9. ✅ Reject push from non-maintainer
**Implementation:**
```rust
// src/git/authorization.rs
pub struct PushValidator {
storage: Storage,
}
impl PushValidator {
pub async fn validate_push(
&self,
npub: &str,
identifier: &str,
updates: &[RefUpdate],
) -> Result<()>;
async fn get_maintainers(&self, npub: &str, identifier: &str) -> Vec<String>;
async fn get_latest_state(&self, npub: &str, identifier: &str) -> Option<StateEvent>;
fn validate_ref_update(&self, state: &StateEvent, update: &RefUpdate) -> Result<()>;
}
```
**Test Example:**
```rust
#[tokio::test]
async fn test_validate_matching_push() {
let storage = test_storage().await;
// Create announcement and state
let announcement = create_announcement("alice", "repo1");
let state = create_state("alice", "repo1")
.branch("main", "a1b2c3d4...");
storage.store_event(announcement).await.unwrap();
storage.store_event(state).await.unwrap();
// Validate matching push
let validator = PushValidator::new(storage);
let update = RefUpdate {
old_oid: "0000...".into(),
new_oid: "a1b2c3d4...".into(),
ref_name: "refs/heads/main".into(),
};
let result = validator.validate_push("alice", "repo1", &[update]).await;
assert!(result.is_ok());
}
#[tokio::test]
async fn test_reject_mismatched_push() {
let storage = test_storage().await;
// State points to commit A
let state = create_state("alice", "repo1")
.branch("main", "aaaa1111...");
storage.store_event(state).await.unwrap();
// Try to push commit B
let validator = PushValidator::new(storage);
let update = RefUpdate {
old_oid: "0000...".into(),
new_oid: "bbbb2222...".into(), // Different!
ref_name: "refs/heads/main".into(),
};
let result = validator.validate_push("alice", "repo1", &[update]).await;
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("state"));
}
```
### Phase 4: HTTP Handlers
**Goal:** Serve Git Smart HTTP protocol
**Tests:**
1. ✅ GET /npub/repo.git/info/refs?service=git-upload-pack
2. ✅ POST /npub/repo.git/git-upload-pack (clone/fetch)
3. ✅ POST /npub/repo.git/git-receive-pack (push with auth)
4. ✅ Return 403 for unauthorized push
5. ✅ Return 404 for non-existent repository
6. ✅ Set correct content-type headers
7. ✅ Include CORS headers
**Implementation:**
```rust
// src/git/handler.rs
pub async fn handle_info_refs(
npub: String,
identifier: String,
service: String,
) -> Result<Response>;
pub async fn handle_upload_pack(
npub: String,
identifier: String,
body: Bytes,
) -> Result<Response>;
pub async fn handle_receive_pack(
npub: String,
identifier: String,
body: Bytes,
validator: PushValidator,
) -> Result<Response>;
```
**Test Example:**
```rust
#[tokio::test]
async fn test_info_refs_returns_correct_headers() {
let app = test_app().await;
// Create repository
app.create_repo("alice", "test-repo").await;
// Request info/refs
let response = app.get("/alice-npub/test-repo.git/info/refs?service=git-upload-pack").await;
assert_eq!(response.status(), 200);
assert_eq!(
response.headers().get("content-type").unwrap(),
"application/x-git-upload-pack-advertisement"
);
assert_eq!(
response.headers().get("access-control-allow-origin").unwrap(),
"*"
);
}
#[tokio::test]
async fn test_receive_pack_rejects_unauthorized() {
let app = test_app().await;
// Create repo with state
let (announcement, state) = app.create_repo_with_state()
.branch("main", "aaaa1111...")
.build()
.await;
// Try to push different commit
let body = create_receive_pack_request()
.ref_update("refs/heads/main", "0000...", "bbbb2222...")
.build();
let response = app.post(
&format!("/{}/repo.git/git-receive-pack", announcement.author_npub()),
body
).await;
assert_eq!(response.status(), 403);
}
```
### Phase 5: Integration with Nostr Events
**Goal:** Automatic repository creation and state updates
**Tests:**
1. ✅ Create repository when announcement received
2. ✅ Update HEAD when state event received
3. ✅ Handle announcement updates (maintainers change)
4. ✅ Clean up orphaned refs/nostr/* refs
**Implementation:**
```rust
// src/nostr/events.rs (extend existing)
async fn handle_repository_announcement(event: &Event, storage: &Storage) -> Result<()> {
// Extract npub and identifier
// Create bare repository
// Store event
}
async fn handle_repository_state(event: &Event, storage: &Storage) -> Result<()> {
// Find repository
// Update refs to match state
// Update HEAD
}
```
**Test Example:**
```rust
#[tokio::test]
async fn test_repository_created_on_announcement() {
let app = test_app().await;
let announcement = create_announcement("alice", "test-repo")
.with_clone_tag(app.domain())
.build();
app.send_event(announcement).await.unwrap();
// Wait for async processing
tokio::time::sleep(Duration::from_millis(100)).await;
// Verify repository exists
let repo_path = app.git_data_path()
.join("alice-npub")
.join("test-repo.git");
assert!(repo_path.exists());
assert!(repo_path.join("HEAD").exists());
}
```
### Phase 6: End-to-End Testing
**Goal:** Test with real Git client
**Tests:**
1. ✅ Clone repository with git client
2. ✅ Fetch from repository
3. ✅ Push to repository (authorized)
4. ✅ Push rejected (unauthorized)
5. ✅ Multiple concurrent operations
**Implementation:**
```rust
// tests/e2e/git_client.rs
#[tokio::test]
async fn test_real_git_clone() {
let app = test_app().await;
// Setup repository with content
let (announcement, _) = app.create_repo_with_commits()
.commit("Initial", "README.md", "# Test")
.build()
.await;
// Clone with real git
let temp = TempDir::new().unwrap();
let url = format!(
"http://{}/{}/{}.git",
app.domain(),
announcement.author_npub(),
announcement.identifier()
);
let output = Command::new("git")
.args(&["clone", &url])
.current_dir(&temp)
.output()
.await
.unwrap();
assert!(output.status.success());
let cloned_path = temp.path().join(announcement.identifier());
assert!(cloned_path.exists());
assert!(cloned_path.join("README.md").exists());
}
```
---
## Testing Strategy
### Unit Tests (40%)
- Git repository operations (git2)
- Protocol parsing
- Authorization logic
- Pure functions, no I/O
### Integration Tests (30%)
- HTTP handlers with test server
- Repository + Nostr event interaction
- Multi-maintainer flows
- State validation
### Compliance Tests (20%)
- GRASP-01 Git requirements
- Use grasp-audit library
- Spec-driven assertions
### E2E Tests (10%)
- Real git client operations
- End-to-end workflows
- Performance testing
---
## Implementation Order
### Week 1: Foundation
1. Add git2 dependency
2. Implement GitRepository (Phase 1)
3. Write unit tests for repository operations
4. Test repository creation from announcements
### Week 2: Protocol & Authorization
1. Implement protocol parsing (Phase 2)
2. Implement authorization logic (Phase 3)
3. Write unit tests for both
4. Integration tests for validation
### Week 3: HTTP & Integration
1. Implement HTTP handlers (Phase 4)
2. Integrate with Nostr events (Phase 5)
3. Integration tests for full flow
4. CORS and error handling
### Week 4: E2E & Polish
1. E2E tests with real git (Phase 6)
2. Performance testing
3. GRASP-01 compliance testing
4. Documentation and examples
---
## Success Criteria
### Functional
- ✅ Clone repository via HTTP
- ✅ Push authorized commits
- ✅ Reject unauthorized pushes
- ✅ Support multi-maintainer
- ✅ Support refs/nostr/* for PRs
### Quality
- ✅ >80% unit test coverage
- ✅ All integration tests pass
- ✅ GRASP-01 compliance 100%
- ✅ E2E tests with real git
### Performance
- ✅ Clone 1MB repo < 1s
- ✅ Push validation < 100ms
- ✅ 100 concurrent ops without errors
---
## Next Steps
1. **Review this plan** - Does the hybrid approach make sense?
2. **Start Phase 1** - Add git2, implement GitRepository
3. **Write first test** - test_init_bare_repository
4. **Iterate with TDD** - Red → Green → Refactor
---
## Questions for Review
1. **Hybrid approach?** git2 + system git + HTTP layer - good balance?
2. **git-http-backend crate?** Worth using or implement minimal HTTP layer?
3. **Authorization granularity?** Validate per-ref or entire push?
4. **Error messages?** How detailed for push rejections?
5. **Testing scope?** Is 6 phases reasonable for first iteration?
---
**Ready to proceed?** Let me know if this plan looks good, or if you'd like to adjust the approach!
+314
View File
@@ -0,0 +1,314 @@
# Work Directory Index
**Last Updated:** November 4, 2025
**Status:** Ready for Implementation
---
## 📁 Quick Navigation
### 🚀 START HERE
**[NEXT_SESSION_START_HERE.md](NEXT_SESSION_START_HERE.md)** - Begin next session with this
### 📊 Status & Progress
- **[STATUS.txt](STATUS.txt)** - Visual status summary (quick reference)
- **[current_status.md](current_status.md)** - Detailed project status
- **[session-summary.md](session-summary.md)** - What we accomplished this session
### 📖 Understanding & Planning
- **[review-summary.md](review-summary.md)** - GRASP protocol review findings
- **[architecture-diagram.md](architecture-diagram.md)** - Visual architecture reference
- **[implementation-checklist.md](implementation-checklist.md)** - Detailed task checklist
### 📋 Reference
- **[README.md](README.md)** - Work directory purpose and guidelines
---
## 📚 Document Purposes
### NEXT_SESSION_START_HERE.md
**Purpose:** Step-by-step implementation guide
**When to use:** Starting next coding session
**Contains:**
- Immediate goal (actix-web integration)
- Critical architecture understanding
- 8-step implementation plan with code examples
- Verification steps for each step
- Common issues and solutions
- Success criteria
**Read this:** When you're ready to start coding
---
### STATUS.txt
**Purpose:** Quick visual status overview
**When to use:** Quick check of where we are
**Contains:**
- Current compliance percentages
- Critical discoveries
- Next session plan
- Key references
- Success criteria
**Read this:** When you need a quick reminder
---
### current_status.md
**Purpose:** Comprehensive project status
**When to use:** Understanding overall context
**Contains:**
- Complete GRASP-01 requirements checklist
- Architecture understanding
- Current implementation status
- Known issues and blockers
- Progress summary
- Key references
**Read this:** When you need detailed context
---
### session-summary.md
**Purpose:** Summary of this review session
**When to use:** Remembering what we did
**Contains:**
- Session goals and accomplishments
- Key discoveries (7 major findings)
- Documents created
- Lessons learned
- Next steps
**Read this:** When you want to know what happened this session
---
### review-summary.md
**Purpose:** GRASP protocol review findings
**When to use:** Understanding why we're making changes
**Contains:**
- 10 critical discoveries from GRASP spec
- Evidence for each finding
- Impact and action items
- Compliance status
- Immediate next steps
**Read this:** When you need to justify architectural decisions
---
### architecture-diagram.md
**Purpose:** Visual architecture reference
**When to use:** During implementation for reference
**Contains:**
- Current vs. target architecture diagrams
- Request flow examples (5 scenarios)
- Component responsibilities
- Configuration flow
- Test architecture
- File structure
- Comparison with ngit-relay
**Read this:** When you need to visualize the system
---
### implementation-checklist.md
**Purpose:** Detailed task checklist
**When to use:** Tracking implementation progress
**Contains:**
- 5 phases with detailed tasks
- Verification steps for each task
- Manual testing procedures
- Automated testing commands
- Acceptance criteria
- Known issues to watch for
- Reference commands
**Read this:** While implementing to track progress
---
## 🎯 Recommended Reading Order
### For Next Session (Implementation)
1. **STATUS.txt** (1 min)
- Quick reminder of where we are
2. **NEXT_SESSION_START_HERE.md** (10 min)
- Understand the immediate goal
- Review the 8-step plan
3. **architecture-diagram.md** (5 min)
- Visual reference for what we're building
4. **implementation-checklist.md** (ongoing)
- Check off tasks as you complete them
### For Understanding Context
1. **session-summary.md** (5 min)
- What we accomplished this session
2. **current_status.md** (10 min)
- Overall project status
3. **review-summary.md** (15 min)
- Why we're making these changes
### For Reference During Coding
- **architecture-diagram.md** - Visual reference
- **implementation-checklist.md** - Task tracking
- **NEXT_SESSION_START_HERE.md** - Step-by-step guide
---
## 🔍 Finding Specific Information
### "How do I implement X?"
→ **NEXT_SESSION_START_HERE.md** (Step-by-step with code)
### "Why are we doing X?"
→ **review-summary.md** (Findings from GRASP review)
### "What's the overall status?"
→ **current_status.md** or **STATUS.txt**
### "What did we do this session?"
→ **session-summary.md**
### "How does the architecture work?"
→ **architecture-diagram.md**
### "What tasks are left?"
→ **implementation-checklist.md**
### "What are the requirements?"
→ **current_status.md** (GRASP-01 checklist)
### "How do I test X?"
→ **implementation-checklist.md** (Testing section)
---
## 📊 Document Relationships
```
STATUS.txt
↓ (quick overview)
current_status.md
↓ (detailed status)
session-summary.md
↓ (what we did)
review-summary.md
↓ (why we're doing this)
architecture-diagram.md
↓ (visual reference)
NEXT_SESSION_START_HERE.md
↓ (how to implement)
implementation-checklist.md
↓ (track progress)
```
---
## 🗂️ File Lifecycle
### Active (Use These)
- ✅ NEXT_SESSION_START_HERE.md - Update for each phase
- ✅ current_status.md - Update as we progress
- ✅ implementation-checklist.md - Check off as we go
- ✅ STATUS.txt - Update after each phase
### Reference (Keep These)
- 📖 architecture-diagram.md - Permanent reference
- 📖 review-summary.md - Permanent reference
- 📖 session-summary.md - Historical record
### Archive (After Implementation)
- 📦 implementation-checklist.md → Delete when phase complete
- 📦 NEXT_SESSION_START_HERE.md → Update for next phase
- 📦 session-summary.md → Move to docs/archive/
---
## ✨ Quick Tips
### Starting a New Session
1. Read STATUS.txt (1 min)
2. Read NEXT_SESSION_START_HERE.md (10 min)
3. Open implementation-checklist.md to track progress
4. Start coding!
### When Stuck
1. Check architecture-diagram.md for visual reference
2. Check NEXT_SESSION_START_HERE.md for step details
3. Check review-summary.md for why we're doing it
4. Check ../grasp/01.md for requirements
### Ending a Session
1. Update current_status.md with progress
2. Update STATUS.txt with new status
3. Check off completed tasks in implementation-checklist.md
4. Update NEXT_SESSION_START_HERE.md if needed
### After Phase Complete
1. Archive implementation-checklist.md
2. Update NEXT_SESSION_START_HERE.md for next phase
3. Update current_status.md with new status
4. Create new session-summary.md if needed
---
## 📈 Progress Tracking
Use these documents to track progress:
### Daily
- [ ] Check STATUS.txt
- [ ] Update implementation-checklist.md
- [ ] Follow NEXT_SESSION_START_HERE.md
### Weekly
- [ ] Update current_status.md
- [ ] Update STATUS.txt
- [ ] Review progress against checklist
### After Each Phase
- [ ] Update current_status.md
- [ ] Create new session-summary.md
- [ ] Update NEXT_SESSION_START_HERE.md
- [ ] Archive completed documents
---
## 🎯 Current Phase
**Phase:** actix-web Integration
**Status:** Ready to Start
**Start With:** NEXT_SESSION_START_HERE.md
**Track With:** implementation-checklist.md
**Reference:** architecture-diagram.md
---
## 📞 Quick Reference
| Need | Document |
|------|----------|
| Start coding | NEXT_SESSION_START_HERE.md |
| Quick status | STATUS.txt |
| Detailed status | current_status.md |
| Why we're doing this | review-summary.md |
| How it works | architecture-diagram.md |
| Task list | implementation-checklist.md |
| What we did | session-summary.md |
---
**Last Updated:** November 4, 2025
**Next Update:** After actix-web integration complete
@@ -0,0 +1,650 @@
# Next Session Start Here
**Date:** November 4, 2025
**Purpose:** Quick start guide for next development session
**Status:** Ready for actix-web integration
---
## 🎯 Immediate Goal
**Integrate actix-web to serve both Nostr relay (WebSocket) and Git HTTP on the SAME PORT.**
This is the critical architectural fix needed to match GRASP-01 requirements and ngit-relay's design.
---
## 🚨 Critical Understanding
### Single Port Architecture (from ../ngit-relay)
```
┌─────────────────────────────────────┐
│ Single Port (8080) │
│ │
│ ┌─────────────────────────────┐ │
│ │ HTTP/WebSocket Router │ │
│ │ (nginx in ngit-relay) │ │
│ │ (actix-web in ngit-grasp) │ │
│ └──────────┬──────────────────┘ │
│ │ │
│ ┌──────┴──────┐ │
│ ↓ ↓ │
│ Git HTTP Nostr Relay │
│ /<npub>/ / (WebSocket) │
│ <id>.git │
└─────────────────────────────────────┘
```
**Key Points:**
1. ONE port listens for all traffic
2. Router inspects path and decides where to send request
3. Git paths go to Git handler
4. Everything else goes to Nostr relay (with WebSocket upgrade)
5. CORS headers on ALL responses
---
## 📋 Step-by-Step Implementation Plan
### Step 1: Add actix-web Dependencies
**File:** `Cargo.toml`
```toml
[dependencies]
# Existing...
actix-web = "4"
actix-cors = "0.7"
actix-ws = "0.3" # For WebSocket support
# Git HTTP backend
git-http-backend = "0.2" # Check latest version
```
**Why:**
- `actix-web` - HTTP framework with routing
- `actix-cors` - Easy CORS middleware
- `actix-ws` - WebSocket support
- `git-http-backend` - Git Smart HTTP protocol
### Step 2: Create HTTP Router Module
**File:** `src/http/mod.rs` (NEW)
```rust
//! HTTP server with routing for Git and Nostr
use actix_web::{web, App, HttpServer};
use actix_cors::Cors;
pub mod git;
pub mod nostr;
pub async fn run_server(config: Config, storage: Storage) -> Result<()> {
let bind_addr = config.bind_address.clone();
HttpServer::new(move || {
App::new()
// CORS middleware (GRASP-01 requirement)
.wrap(
Cors::default()
.allow_any_origin()
.allowed_methods(vec!["GET", "POST"])
.allowed_headers(vec!["Content-Type"])
.max_age(3600)
)
// Git HTTP routes
.service(
web::scope("/{npub}/{repo}")
.guard(guard::fn_guard(|ctx| {
// Only match *.git paths
ctx.head().uri.path().ends_with(".git")
}))
.route("", web::get().to(git::handle_git_request))
.route("/{tail:.*}", web::to(git::handle_git_request))
)
// Nostr relay (WebSocket at /)
.route("/", web::get().to(nostr::handle_websocket))
// Static files (optional)
.route("/", web::get().to(nostr::handle_http_root))
})
.bind(bind_addr)?
.run()
.await?;
Ok(())
}
```
**Why:**
- Single HTTP server listening on one port
- Routes by URL path pattern
- CORS applied to all routes
- Git paths (*.git) go to Git handler
- Root path (/) handles WebSocket upgrade for Nostr
### Step 3: Create Git HTTP Handler
**File:** `src/http/git.rs` (NEW)
```rust
//! Git Smart HTTP handler
use actix_web::{web, HttpRequest, HttpResponse, Result};
use git_http_backend::{GitHttpBackend, Method};
pub async fn handle_git_request(
req: HttpRequest,
body: web::Bytes,
path: web::Path<(String, String)>,
) -> Result<HttpResponse> {
let (npub, repo) = path.into_inner();
// Construct repository path
let repo_path = format!("{}/{}/{}",
config.git_data_path, npub, repo);
// Check if repository exists
if !std::path::Path::new(&repo_path).exists() {
return Ok(HttpResponse::NotFound()
.body("Repository not found"));
}
// Parse Git HTTP request
let method = match *req.method() {
actix_web::http::Method::GET => Method::Get,
actix_web::http::Method::POST => Method::Post,
_ => return Ok(HttpResponse::MethodNotAllowed().finish()),
};
// Use git-http-backend to handle request
let backend = GitHttpBackend::new(&repo_path);
let response = backend.handle(method, req.path(), &body)?;
// Convert to actix HttpResponse
Ok(HttpResponse::Ok()
.content_type(response.content_type)
.body(response.body))
}
```
**Why:**
- Handles Git Smart HTTP protocol
- Serves from `{GIT_DATA_PATH}/{npub}/{repo}.git`
- Uses `git-http-backend` crate for protocol details
- Returns 404 if repo doesn't exist
### Step 4: Create Nostr WebSocket Handler
**File:** `src/http/nostr.rs` (NEW)
```rust
//! Nostr relay WebSocket handler
use actix_web::{web, HttpRequest, HttpResponse, Result};
use actix_ws::Message;
pub async fn handle_websocket(
req: HttpRequest,
stream: web::Payload,
storage: web::Data<Storage>,
) -> Result<HttpResponse> {
// Upgrade to WebSocket
let (response, mut session, mut msg_stream) = actix_ws::handle(&req, stream)?;
// Spawn task to handle WebSocket messages
actix_web::rt::spawn(async move {
while let Some(Ok(msg)) = msg_stream.next().await {
match msg {
Message::Text(text) => {
// Handle Nostr message (EVENT, REQ, CLOSE)
let responses = handle_nostr_message(&text, &storage).await;
for response in responses {
session.text(response).await.ok();
}
}
Message::Ping(bytes) => {
session.pong(&bytes).await.ok();
}
Message::Close(_) => break,
_ => {}
}
}
});
Ok(response)
}
pub async fn handle_http_root() -> Result<HttpResponse> {
// Serve static HTML for browsers
Ok(HttpResponse::Ok()
.content_type("text/html")
.body("<html><body><h1>ngit-grasp</h1><p>Nostr relay at ws://</p></body></html>"))
}
```
**Why:**
- Handles WebSocket upgrade at `/`
- Reuses existing Nostr message handling logic
- Returns HTML for browsers (non-WebSocket requests)
### Step 5: Update main.rs
**File:** `src/main.rs`
```rust
use anyhow::Result;
use tracing::{info, Level};
use tracing_subscriber::FmtSubscriber;
mod config;
mod http; // NEW
mod nostr;
mod storage;
use config::Config;
#[tokio::main]
async fn main() -> Result<()> {
// Initialize tracing
let subscriber = FmtSubscriber::builder()
.with_max_level(Level::DEBUG)
.finish();
tracing::subscriber::set_global_default(subscriber)?;
info!("Starting ngit-grasp...");
// Load configuration
let config = Config::from_env()?;
info!("Configuration: {}", config.bind_address);
// Initialize storage
let storage = storage::Storage::new(&config)?;
info!("Storage initialized at: {}", config.relay_data_path);
// Start HTTP server (Git + Nostr on same port)
info!("Starting server on {}", config.bind_address);
http::run_server(config, storage).await?;
Ok(())
}
```
**Why:**
- Replaces separate relay with unified HTTP server
- Single entry point for all services
- Simpler architecture
### Step 6: Update Configuration
**File:** `src/config.rs`
Add field for Git data path:
```rust
pub struct Config {
pub bind_address: String,
pub domain: String,
pub relay_data_path: String,
pub git_data_path: String, // NEW
// ... other fields
}
impl Config {
pub fn from_env() -> Result<Self> {
Ok(Config {
bind_address: env::var("NGIT_BIND_ADDRESS")
.unwrap_or_else(|_| "127.0.0.1:8080".to_string()),
domain: env::var("NGIT_DOMAIN")?,
relay_data_path: env::var("NGIT_RELAY_DATA_PATH")
.unwrap_or_else(|_| "./data/relay".to_string()),
git_data_path: env::var("NGIT_GIT_DATA_PATH") // NEW
.unwrap_or_else(|_| "./data/repos".to_string()),
// ...
})
}
}
```
**File:** `.env.example`
```bash
# Service Configuration
NGIT_DOMAIN=example.com
NGIT_BIND_ADDRESS=127.0.0.1:8080
# Relay Information
NGIT_RELAY_NAME="ngit-grasp instance"
NGIT_RELAY_DESCRIPTION="Rust GRASP implementation"
NGIT_OWNER_NPUB="npub1..."
# Storage Paths
NGIT_GIT_DATA_PATH=./data/repos
NGIT_RELAY_DATA_PATH=./data/relay
# Logging
NGIT_LOG_LEVEL=INFO
RUST_LOG=info
```
### Step 7: Update Tests
**File:** `tests/common/relay.rs`
Update `start_with_port` to pass domain correctly:
```rust
pub async fn start_with_port(port: u16) -> Self {
let bind_address = format!("127.0.0.1:{}", port);
let domain = format!("127.0.0.1:{}", port); // NEW
let url = format!("ws://{}", domain);
let process = Command::new(&binary_path)
.env("NGIT_BIND_ADDRESS", &bind_address)
.env("NGIT_DOMAIN", &domain) // UPDATED
.env("NGIT_GIT_DATA_PATH", "./test-data/repos") // NEW
.env("NGIT_RELAY_DATA_PATH", "./test-data/relay") // NEW
.env("RUST_LOG", "warn")
.stdout(Stdio::null())
.stderr(Stdio::null())
.spawn()
.expect("Failed to start relay process");
// ... rest of method
}
```
**Why:**
- Domain must match bind address for announcement validation
- Separate test data directories
- Clean up test data after tests
### Step 8: Add Git HTTP Tests
**File:** `tests/grasp01_git_http.rs` (NEW)
```rust
//! GRASP-01 Git HTTP Integration Tests
//!
//! Reference: ../grasp/01.md lines 15-40
//!
//! These tests verify Git Smart HTTP service requirements:
//! - Serve repos at /<npub>/<identifier>.git
//! - Accept pushes matching state announcements
//! - CORS support
mod common;
use common::TestRelay;
use std::process::Command;
#[tokio::test]
async fn test_git_clone_basic() {
// Reference: ../grasp/01.md line 15
// MUST serve git repository via unauthenticated git smart http service
let relay = TestRelay::start().await;
let domain = relay.domain();
// TODO: Create test repository announcement
// TODO: Clone via git clone http://{domain}/{npub}/{id}.git
relay.stop().await;
}
#[tokio::test]
async fn test_cors_headers() {
// Reference: ../grasp/01.md lines 32-40
// MUST include CORS headers on all responses
let relay = TestRelay::start().await;
let url = format!("http://{}/", relay.domain());
let response = reqwest::get(&url).await.unwrap();
// Check CORS headers
assert_eq!(
response.headers().get("access-control-allow-origin"),
Some(&"*".parse().unwrap())
);
relay.stop().await;
}
```
**Why:**
- Tests reference GRASP protocol line numbers
- Verifies Git HTTP functionality
- Checks CORS compliance
---
## 🔍 Verification Steps
After implementing the above:
### 1. Build and Run
```bash
# Build
cargo build
# Run server
NGIT_DOMAIN=localhost:8080 \
NGIT_BIND_ADDRESS=127.0.0.1:8080 \
NGIT_GIT_DATA_PATH=./data/repos \
NGIT_RELAY_DATA_PATH=./data/relay \
cargo run
```
### 2. Test Nostr Relay (WebSocket)
```bash
# In another terminal
cd grasp-audit
cargo run -- --url ws://localhost:8080
```
**Expected:** NIP-01 smoke tests should pass
### 3. Test Git HTTP (Manual)
```bash
# Create test repository
mkdir -p ./data/repos/npub1test/test-repo.git
cd ./data/repos/npub1test/test-repo.git
git init --bare
# Try to clone
git clone http://localhost:8080/npub1test/test-repo.git
```
**Expected:** Should clone successfully (even if empty)
### 4. Test CORS
```bash
curl -v http://localhost:8080/ -H "Origin: https://example.com"
```
**Expected:** Response should include:
```
access-control-allow-origin: *
access-control-allow-methods: GET, POST
access-control-allow-headers: Content-Type
```
### 5. Run Integration Tests
```bash
# All tests
cargo test
# Just NIP-01
cargo test --test nip01_compliance
# Just Git HTTP (when implemented)
cargo test --test grasp01_git_http
```
**Expected:** All tests pass
---
## 🐛 Common Issues & Solutions
### Issue: Port Already in Use
**Symptom:** "Address already in use" error
**Solution:**
```bash
# Find process using port
lsof -i :8080
# Kill it
kill -9 <PID>
# Or use different port
NGIT_BIND_ADDRESS=127.0.0.1:8081 cargo run
```
### Issue: WebSocket Upgrade Fails
**Symptom:** WebSocket connection refused
**Solution:**
- Check actix-web WebSocket handling
- Verify `Upgrade: websocket` header is present
- Check actix-ws is properly configured
### Issue: Git Clone Fails
**Symptom:** "repository not found" or protocol error
**Solution:**
- Verify repository exists at correct path
- Check git-http-backend configuration
- Ensure repository is bare (`git init --bare`)
- Check file permissions
### Issue: CORS Headers Missing
**Symptom:** Browser console shows CORS error
**Solution:**
- Verify CORS middleware is applied
- Check middleware order (CORS should be first)
- Test with curl to see actual headers
---
## 📚 Key Resources
### GRASP Protocol
- `../grasp/01.md` - **THE SPEC** - Read this first!
- Lines 1-14: Nostr relay requirements
- Lines 15-31: Git HTTP service requirements
- Lines 32-40: CORS requirements
### Reference Implementation
- `../ngit-relay/src/nginx.conf` - **ROUTING PATTERN**
- Lines 8-13: Single port listener
- Lines 15-48: Git HTTP routing
- Lines 50-94: Nostr relay routing
- `../ngit-relay/docker-compose.yml` - Port configuration
- `../ngit-relay/.env.example` - Environment variables
### actix-web Documentation
- [Routing](https://actix.rs/docs/url-dispatch/)
- [WebSocket](https://actix.rs/docs/websockets/)
- [CORS](https://docs.rs/actix-cors/)
### git-http-backend Crate
- [Docs](https://docs.rs/git-http-backend/)
- [Examples](https://github.com/w4/git-http-backend/tree/master/examples)
---
## ✅ Success Criteria
You'll know this step is complete when:
1. ✅ Server starts on single port
2. ✅ WebSocket connects at `ws://localhost:8080/`
3. ✅ NIP-01 smoke tests pass
4. ✅ Can clone Git repo at `http://localhost:8080/npub.../repo.git`
5. ✅ CORS headers present on all responses
6. ✅ OPTIONS requests return 204
7. ✅ All integration tests pass
---
## 🎯 After This Step
Once actix-web integration is complete:
1. **Repository Provisioning**
- Create repos when announcements received
- Initialize bare repositories
- Set up directory structure
2. **Push Authorization**
- Intercept git-receive-pack
- Validate against state announcements
- Handle maintainer sets
3. **Full GRASP-01 Compliance**
- All tests passing
- Ready for production testing
---
## 💡 Tips
1. **Start Simple**
- Get basic HTTP routing working first
- Add WebSocket support second
- Add Git HTTP last
2. **Test Incrementally**
- Test each component as you add it
- Don't wait until everything is done
3. **Use curl for Debugging**
```bash
# Test HTTP
curl -v http://localhost:8080/
# Test CORS
curl -v http://localhost:8080/ -H "Origin: https://example.com"
# Test Git info/refs
curl http://localhost:8080/npub.../repo.git/info/refs?service=git-upload-pack
```
4. **Check ngit-relay for Patterns**
- nginx.conf shows exact routing logic
- Copy the pattern, not the implementation
5. **Keep Tests Running**
```bash
# In one terminal
cargo watch -x 'test --test nip01_compliance'
# Make changes, tests auto-run
```
---
**Ready to Start?** Begin with Step 1 (Add Dependencies)
**Questions?** Check `work/current_status.md` for context
**Stuck?** Review `../ngit-relay/src/nginx.conf` for routing pattern
---
**Last Updated:** November 4, 2025
**Next Update:** After actix-web integration complete
@@ -0,0 +1,78 @@
╔════════════════════════════════════════════════════════════════╗
║ NGIT-GRASP STATUS ║
║ November 4, 2025 ║
╚════════════════════════════════════════════════════════════════╝
📊 OVERALL STATUS: Planning Complete, Ready for Implementation
┌────────────────────────────────────────────────────────────────┐
│ CRITICAL DISCOVERY: Architecture Was Wrong! │
├────────────────────────────────────────────────────────────────┤
│ ❌ Previous: Nostr on :8080, Git on :8081 (WRONG!) │
│ ✅ Correct: BOTH on :8080, routed by path (REQUIRED!) │
│ │
│ Fix: Integrate actix-web for HTTP routing │
└────────────────────────────────────────────────────────────────┘
┌────────────────────────────────────────────────────────────────┐
│ COMPLIANCE STATUS │
├────────────────────────────────────────────────────────────────┤
│ NIP-01 (Nostr Relay): ████████░░ 80% (needs NIP-11 fix) │
│ NIP-34 (Git Announce): ████░░░░░░ 40% (needs validation) │
│ GRASP-01 (Core): ██░░░░░░░░ 20% (needs Git HTTP) │
│ │
│ Target After Next Phase: ████████░░ 80% (Git HTTP working) │
└────────────────────────────────────────────────────────────────┘
┌────────────────────────────────────────────────────────────────┐
│ WORK DOCUMENTS CREATED │
├────────────────────────────────────────────────────────────────┤
│ ✅ current_status.md - Overall project status │
│ ✅ NEXT_SESSION_START_HERE.md - Implementation guide (START!) │
│ ✅ review-summary.md - GRASP protocol findings │
│ ✅ architecture-diagram.md - Visual reference │
│ ✅ implementation-checklist.md - Detailed task list │
│ ✅ session-summary.md - What we accomplished │
└────────────────────────────────────────────────────────────────┘
┌────────────────────────────────────────────────────────────────┐
│ NEXT SESSION: Implement actix-web Integration │
├────────────────────────────────────────────────────────────────┤
│ 1. Add dependencies (actix-web, actix-cors, git-http-backend) │
│ 2. Create src/http/mod.rs (HTTP server + routing) │
│ 3. Create src/http/git.rs (Git Smart HTTP handler) │
│ 4. Create src/http/nostr.rs (WebSocket handler) │
│ 5. Update src/main.rs (use new HTTP server) │
│ 6. Update tests (verify single-port works) │
│ 7. Manual testing (clone, WebSocket, CORS) │
│ 8. Automated testing (all tests pass) │
│ │
│ Estimated Time: 3-6 hours │
│ Confidence: HIGH ✅ │
└────────────────────────────────────────────────────────────────┘
┌────────────────────────────────────────────────────────────────┐
│ KEY REFERENCES │
├────────────────────────────────────────────────────────────────┤
│ 📖 ../grasp/01.md - THE SPEC (lines 1-40) │
│ 📖 ../ngit-relay/src/nginx.conf - Routing pattern reference │
│ 📖 work/NEXT_SESSION_START_HERE.md - Implementation guide │
│ 📖 work/architecture-diagram.md - Visual architecture │
│ 📖 work/implementation-checklist.md - Task checklist │
└────────────────────────────────────────────────────────────────┘
┌────────────────────────────────────────────────────────────────┐
│ SUCCESS CRITERIA (Next Phase) │
├────────────────────────────────────────────────────────────────┤
│ ✅ Server starts on single port (8080) │
│ ✅ WebSocket connects at ws://localhost:8080/ │
│ ✅ NIP-01 smoke tests pass │
│ ✅ Can clone Git repo via http://localhost:8080/npub.../x.git │
│ ✅ CORS headers present on all responses │
│ ✅ OPTIONS requests return 204 No Content │
│ ✅ All integration tests pass │
└────────────────────────────────────────────────────────────────┘
╔════════════════════════════════════════════════════════════════╗
║ 🚀 READY TO IMPLEMENT - Start with NEXT_SESSION_START_HERE.md ║
╚════════════════════════════════════════════════════════════════╝
@@ -0,0 +1,442 @@
# ngit-grasp Architecture Diagram
**Date:** November 4, 2025
**Purpose:** Visual reference for single-port architecture
---
## Current Architecture (WRONG ❌)
```
┌─────────────────────────────────────────┐
│ Port 8080 │
│ ┌───────────────────────────────────┐ │
│ │ Nostr Relay (WebSocket) │ │
│ │ - NIP-01 protocol │ │
│ │ - Event storage │ │
│ │ - Subscriptions │ │
│ └───────────────────────────────────┘ │
└─────────────────────────────────────────┘
┌─────────────────────────────────────────┐
│ Port 8081 (WRONG!) │
│ ┌───────────────────────────────────┐ │
│ │ Git HTTP Server │ │
│ │ - Not implemented │ │
│ └───────────────────────────────────┘ │
└─────────────────────────────────────────┘
```
**Problem:** GRASP-01 requires single port!
---
## Target Architecture (CORRECT ✅)
```
┌─────────────────────────────────────────────────────────────┐
│ Single Port (8080) │
│ │
│ ┌───────────────────────────────────────────────────────┐ │
│ │ actix-web HTTP Server │ │
│ │ │ │
│ │ ┌──────────────────────────────────────────────────┐ │ │
│ │ │ CORS Middleware (ALL requests) │ │ │
│ │ │ - Access-Control-Allow-Origin: * │ │ │
│ │ │ - Access-Control-Allow-Methods: GET, POST │ │ │
│ │ │ - Access-Control-Allow-Headers: Content-Type │ │ │
│ │ └──────────────────────────────────────────────────┘ │ │
│ │ │ │
│ │ ┌──────────────────────────────────────────────────┐ │ │
│ │ │ HTTP Router │ │ │
│ │ │ │ │ │
│ │ │ Path Pattern Matching: │ │ │
│ │ │ - /<npub>/<identifier>.git/* → Git Handler │ │ │
│ │ │ - /* → Nostr Handler │ │ │
│ │ └──────────────────────────────────────────────────┘ │ │
│ │ │ │
│ │ ┌────────────────────┐ ┌─────────────────────────┐ │ │
│ │ │ Git HTTP Handler │ │ Nostr Relay Handler │ │ │
│ │ │ │ │ │ │ │
│ │ │ ┌──────────────┐ │ │ ┌──────────────────┐ │ │ │
│ │ │ │ git-http- │ │ │ │ WebSocket Upgrade│ │ │ │
│ │ │ │ backend │ │ │ │ │ │ │ │
│ │ │ │ │ │ │ │ ┌──────────────┐ │ │ │ │
│ │ │ │ - info/refs │ │ │ │ │ NIP-01 │ │ │ │ │
│ │ │ │ - upload-pack│ │ │ │ │ - EVENT │ │ │ │ │
│ │ │ │ - receive-pack │ │ │ │ - REQ │ │ │ │ │
│ │ │ │ │ │ │ │ │ - CLOSE │ │ │ │ │
│ │ │ │ Authorization: │ │ │ └──────────────┘ │ │ │ │
│ │ │ │ - Query state│ │ │ │ │ │ │ │
│ │ │ │ - Validate │ │ │ │ ┌──────────────┐ │ │ │ │
│ │ │ │ - Accept/ │ │ │ │ │ NIP-11 │ │ │ │ │
│ │ │ │ Reject │ │ │ │ │ - GRASP fields│ │ │ │ │
│ │ │ └──────────────┘ │ │ │ └──────────────┘ │ │ │ │
│ │ │ │ │ │ │ │ │ │
│ │ │ Repository: │ │ │ ┌──────────────┐ │ │ │ │
│ │ │ {GIT_DATA_PATH}/ │ │ │ │ NIP-34 │ │ │ │ │
│ │ │ {npub}/ │ │ │ │ - Announce │ │ │ │ │
│ │ │ {identifier}.git │ │ │ │ - State │ │ │ │ │
│ │ │ │ │ │ │ - Validate │ │ │ │ │
│ │ └────────────────────┘ │ │ └──────────────┘ │ │ │ │
│ │ │ │ │ │ │ │
│ │ │ │ HTTP Root: │ │ │ │
│ │ │ │ - Serve HTML │ │ │ │
│ │ │ │ - NIP-11 JSON │ │ │ │
│ │ │ └──────────────────┘ │ │ │
│ │ │ │ │ │
│ └───────────────────────────┴─────────────────────────┘ │ │
│ │
│ ┌───────────────────────────────────────────────────────┐ │
│ │ Storage Layer │ │
│ │ │ │
│ │ ┌──────────────────────┐ ┌──────────────────────┐ │ │
│ │ │ Git Repositories │ │ Nostr Events DB │ │ │
│ │ │ │ │ │ │ │
│ │ │ {GIT_DATA_PATH}/ │ │ {RELAY_DATA_PATH}/ │ │ │
│ │ │ ├── npub1.../ │ │ - Announcements │ │ │
│ │ │ │ ├── repo1.git/ │ │ - State events │ │ │
│ │ │ │ └── repo2.git/ │ │ - Issues/Patches │ │ │
│ │ │ └── npub2.../ │ │ - Other events │ │ │
│ │ │ └── repo3.git/ │ │ │ │ │
│ │ └──────────────────────┘ └──────────────────────┘ │ │
│ └───────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
```
---
## Request Flow Examples
### Example 1: Git Clone
```
Client: git clone http://localhost:8080/npub1abc.../my-repo.git
↓
actix-web receives HTTP GET request
↓
CORS middleware adds headers
↓
Router matches path: /npub1abc.../my-repo.git
↓
Git Handler receives request
↓
git-http-backend processes:
- GET /npub1abc.../my-repo.git/info/refs?service=git-upload-pack
↓
Response includes:
- CORS headers
- Git protocol data
- Capabilities: allow-reachable-sha1-in-want, allow-tip-sha1-in-want
↓
Client receives data and clones repository
```
### Example 2: Git Push
```
Client: git push http://localhost:8080/npub1abc.../my-repo.git main
↓
actix-web receives HTTP POST request
↓
CORS middleware adds headers
↓
Router matches path: /npub1abc.../my-repo.git
↓
Git Handler receives request
↓
BEFORE spawning git-receive-pack:
1. Parse ref updates from request body
2. Query latest state announcement from relay
3. Validate pusher in maintainer set
4. Validate ref updates match state
↓
If validation passes:
- Spawn git-receive-pack
- Stream response back to client
↓
If validation fails:
- Return HTTP 403 Forbidden
- Include error message
↓
Client receives success/failure
```
### Example 3: WebSocket Connection (Nostr)
```
Client: new WebSocket('ws://localhost:8080/')
↓
actix-web receives HTTP GET with Upgrade: websocket
↓
CORS middleware adds headers
↓
Router matches path: /
↓
Nostr Handler receives request
↓
Upgrade to WebSocket
↓
Client sends: ["EVENT", {...}]
↓
Nostr Handler processes EVENT
↓
If kind 30617 (announcement):
- Validate clone/relays tags
- Provision Git repository
- Store event
↓
Response: ["OK", event_id, true, ""]
↓
Client receives confirmation
```
### Example 4: NIP-11 Request
```
Client: fetch('http://localhost:8080/', {
headers: { 'Accept': 'application/nostr+json' }
})
↓
actix-web receives HTTP GET with Accept header
↓
CORS middleware adds headers
↓
Router matches path: /
↓
Nostr Handler checks Accept header
↓
Returns NIP-11 JSON:
{
"name": "ngit-grasp instance",
"description": "Rust GRASP implementation",
"supported_nips": [1, 11, 34],
"supported_grasps": ["GRASP-01"],
"repo_acceptance_criteria": "Must list this service in clone and relays tags",
"curation": "Basic spam prevention"
}
↓
Client receives relay information
```
### Example 5: CORS Preflight (OPTIONS)
```
Browser: OPTIONS http://localhost:8080/
Headers:
- Origin: https://example.com
- Access-Control-Request-Method: POST
↓
actix-web receives OPTIONS request
↓
CORS middleware handles preflight
↓
Returns 204 No Content with headers:
- Access-Control-Allow-Origin: *
- Access-Control-Allow-Methods: GET, POST
- Access-Control-Allow-Headers: Content-Type
- Access-Control-Max-Age: 3600
↓
Browser caches preflight response
↓
Browser proceeds with actual request
```
---
## Component Responsibilities
### actix-web HTTP Server
- Listen on single port
- Route requests by path
- Handle WebSocket upgrades
- Apply CORS to all requests
### CORS Middleware
- Add headers to ALL responses
- Handle OPTIONS preflight
- Allow any origin (GRASP-01 requirement)
### HTTP Router
- Match `/npub.../repo.git` → Git Handler
- Match `/` → Nostr Handler
- Pass through to appropriate handler
### Git Handler
- Serve Git Smart HTTP protocol
- Read from `{GIT_DATA_PATH}/{npub}/{id}.git`
- Validate pushes before accepting
- Return 404 for missing repos
### Nostr Handler
- Upgrade HTTP to WebSocket
- Process NIP-01 messages
- Store/query events
- Serve NIP-11 for HTTP requests
- Provision repos from announcements
### Storage Layer
- Git repositories (bare)
- Nostr events (database)
- Separate paths for each
---
## Configuration Flow
```
Environment Variables
↓
.env file (optional)
↓
Config::from_env()
↓
Config struct:
- bind_address: "127.0.0.1:8080"
- domain: "example.com"
- git_data_path: "./data/repos"
- relay_data_path: "./data/relay"
- relay_name: "..."
- relay_description: "..."
- owner_npub: "..."
↓
Passed to:
- HTTP server (bind address)
- Git handler (git_data_path, domain)
- Nostr handler (relay_data_path, domain, NIP-11 info)
- Storage layer (both paths)
```
---
## Test Architecture
```
Integration Test
↓
TestRelay::start()
↓
Spawns ngit-grasp process:
- NGIT_BIND_ADDRESS=127.0.0.1:{random_port}
- NGIT_DOMAIN=127.0.0.1:{random_port}
- NGIT_GIT_DATA_PATH=./test-data/repos
- NGIT_RELAY_DATA_PATH=./test-data/relay
↓
Process starts:
- actix-web listens on random port
- Both Git and Nostr available
↓
Test runs:
- Uses grasp-audit library
- Connects to ws://127.0.0.1:{port}/
- Runs compliance tests
↓
TestRelay::stop()
↓
Process killed
↓
Test data cleaned up
```
---
## File Structure
```
ngit-grasp/
├── src/
│ ├── main.rs # Entry point
│ ├── config.rs # Configuration
│ ├── http/ # NEW - HTTP server
│ │ ├── mod.rs # Server setup
│ │ ├── git.rs # Git HTTP handler
│ │ └── nostr.rs # Nostr WebSocket handler
│ ├── nostr/
│ │ ├── mod.rs
│ │ ├── relay.rs # Relay logic (reused)
│ │ └── events.rs # Event handling
│ └── storage/
│ ├── mod.rs
│ └── repository.rs # Git repo management
├── tests/
│ ├── common/
│ │ ├── mod.rs
│ │ └── relay.rs # TestRelay fixture
│ ├── nip01_compliance.rs # NIP-01 tests
│ ├── nip34_announcements.rs # NIP-34 tests
│ └── grasp01_git_http.rs # NEW - GRASP-01 Git tests
├── data/ # Runtime data (gitignored)
│ ├── repos/ # Git repositories
│ └── relay/ # Nostr events
└── test-data/ # Test data (gitignored)
├── repos/
└── relay/
```
---
## Comparison: ngit-relay vs ngit-grasp
### ngit-relay (Go + nginx)
```
nginx (Port 8081)
├── Git HTTP → fcgiwrap → git-http-backend
│ ↓
│ pre-receive hook (Go)
│ ↓
│ Khatru relay (HTTP API)
│
└── Nostr → proxy → Khatru relay (Port 3334)
↓
on_event hook (Go)
↓
provision repos
```
**Components:**
- nginx (routing)
- fcgiwrap (CGI wrapper)
- git-http-backend (Git protocol)
- pre-receive hook (Go, validates pushes)
- post-receive hook (Go, updates HEAD)
- Khatru relay (Go, Nostr protocol)
- on_event hook (Go, provisions repos)
- supervisord (process management)
### ngit-grasp (Rust)
```
actix-web (Port 8080)
├── Git HTTP → git-http-backend crate
│ ↓
│ inline authorization
│ ↓
│ Storage (query state)
│
└── Nostr → WebSocket upgrade
↓
nostr-sdk relay
↓
on_event (provision repos)
↓
Storage (store events)
```
**Components:**
- actix-web (routing + HTTP + WebSocket)
- git-http-backend crate (Git protocol)
- nostr-sdk (Nostr protocol)
- Storage (unified storage layer)
**Advantages:**
- Single binary
- No external processes
- Inline authorization (better errors)
- Pure Rust (memory safety)
- Easier testing
---
**Last Updated:** November 4, 2025
**Purpose:** Reference for implementation
@@ -0,0 +1,443 @@
# Current Status - ngit-grasp Implementation
**Date:** November 4, 2025
**Status:** In Development - GRASP-01 Core Requirements
---
## 🎯 Project Goal
Implement a **GRASP-01 compliant** Git relay service in Rust that:
- Serves a NIP-01 Nostr relay at `/` (WebSocket)
- Serves Git repositories via Git Smart HTTP at `/<npub>/<identifier>.git`
- **Both on the SAME PORT** (critical requirement!)
- Validates pushes against Nostr state events
- Passes all compliance tests from grasp-audit
---
## 📋 GRASP-01 Requirements (from ../grasp/01.md)
### 1. Nostr Relay Requirements
**MUST:**
- ✅ Serve NIP-01 compliant relay at `/` (WebSocket)
- ✅ Accept NIP-34 repository announcements (kind 30617)
- ✅ Accept NIP-34 state announcements (kind 30618)
- ⏳ Reject announcements that don't list this service in `clone` and `relays` tags
- ⏳ Accept events that tag accepted announcements
- ✅ Serve NIP-11 relay information document
- ⏳ Include `supported_grasps`, `repo_acceptance_criteria`, `curation` in NIP-11
**Current Implementation:**
- Basic WebSocket relay working
- Event storage and querying functional
- NIP-11 basic implementation exists
- **Missing:** Announcement validation against service URL
- **Missing:** Event acceptance policy based on announcements
### 2. Git Smart HTTP Service Requirements
**MUST:**
- ❌ Serve Git repos at `/<npub>/<identifier>.git` via unauthenticated Git Smart HTTP
- ❌ Accept pushes matching latest state announcement (respecting maintainer set)
- ❌ Set repository HEAD per state announcement
- ❌ Accept pushes to `refs/nostr/<event-id>` for PRs
- ❌ Include `allow-reachable-sha1-in-want` and `allow-tip-sha1-in-want`
- ❌ Serve webpage at repo endpoint for browsers
**Current Implementation:**
- **NOT STARTED** - Git HTTP backend not integrated
- No Git repository management
- No push validation
### 3. CORS Support Requirements
**MUST:**
- ❌ Set `Access-Control-Allow-Origin: *` on ALL responses
- ❌ Set `Access-Control-Allow-Methods: GET, POST` on ALL responses
- ❌ Set `Access-Control-Allow-Headers: Content-Type` on ALL responses
- ❌ Respond to OPTIONS requests with 204 No Content
**Current Implementation:**
- **NOT STARTED** - No CORS headers
---
## 🏗️ Architecture Understanding (from ngit-relay)
### Critical Architecture Insight: SINGLE PORT
From `../ngit-relay/docker-compose.yml`:
```yaml
ports:
- "8081:8081" # Single port for EVERYTHING
```
From `../ngit-relay/src/nginx.conf`:
```nginx
server {
listen 8081; # Single listener
# Git repos at /<npub>/<identifier>.git
location ~ ^/npub1([a-z0-9]+)/([^/]+\.git)(/.*)?$ {
# ... git-http-backend via fcgiwrap
}
# Nostr relay at /
location / {
# ... proxy to khatru on localhost:3334
}
}
```
**Key Points:**
1. **nginx listens on ONE port (8081)**
2. **nginx routes by URL path:**
- `/<npub>/<identifier>.git/*` → git-http-backend (fcgiwrap)
- Everything else → Khatru relay (localhost:3334)
3. **Khatru relay runs on INTERNAL port 3334**
4. **git-http-backend runs via fcgiwrap socket**
### Our Rust Implementation Strategy
We need to replicate nginx's routing in Rust:
```
HTTP/WebSocket Request on port 8080
↓
actix-web router
↓
┌────┴────┐
↓ ↓
Git Path Other Path
/<npub>/ /
<id>.git
↓ ↓
git-http Nostr Relay
backend (WebSocket upgrade)
handler
```
**Implementation Options:**
**Option A: actix-web (HTTP framework)**
- Handle HTTP/WebSocket on same port
- Route by path pattern
- Use `git-http-backend` crate for Git protocol
- Native WebSocket support for Nostr relay
**Option B: Direct TCP + Manual Routing**
- Accept TCP connections
- Parse HTTP headers to determine route
- More complex but more control
**Recommendation: Option A (actix-web)**
- Well-tested HTTP/WebSocket handling
- Easy routing by path
- Good async performance
- Already in our dependencies
---
## 🧪 Test Strategy
### Current Test Structure
```
tests/
├── common/
│ ├── mod.rs # Test utilities
│ └── relay.rs # TestRelay fixture
├── nip01_compliance.rs # NIP-01 smoke tests
└── nip34_announcements.rs # NIP-34 tests (TODO)
```
### Test Approach
**Integration Tests (tests/*):**
- Use `TestRelay` fixture to start/stop relay
- Use `grasp-audit` library to run compliance tests
- Tests reference GRASP protocol line numbers
- Automatic relay lifecycle management
**Example Test Structure:**
```rust
#[tokio::test]
async fn test_grasp01_git_http_basic() {
// Reference: ../grasp/01.md lines 15-17
// Requirement: MUST serve git repository via unauthenticated git smart http
let relay = TestRelay::start().await;
let config = AuditConfig::ci();
let client = AuditClient::new(relay.url(), config).await.unwrap();
// Run GRASP-01 git HTTP tests
let results = specs::Grasp01GitHttp::run_all(&client).await;
relay.stop().await;
assert!(results.all_passed());
}
```
### Test Coverage Needed
**NIP-01 (Nostr Relay):**
- ✅ WebSocket connection
- ✅ Send/receive events
- ✅ Subscriptions (REQ/CLOSE)
- ✅ Event validation (signatures, IDs)
- ⏳ NIP-11 relay info document
**NIP-34 (Git Announcements):**
- ⏳ Accept valid repository announcements (kind 30617)
- ⏳ Accept valid state announcements (kind 30618)
- ⏳ Reject announcements without service in clone/relays
- ⏳ Validate maintainer sets
- ⏳ Handle related events (issues, patches)
**GRASP-01 (Git HTTP):**
- ❌ Serve Git repo at `/<npub>/<id>.git`
- ❌ Clone repository via HTTP
- ❌ Push matching state announcement
- ❌ Reject push not matching state
- ❌ Handle `refs/nostr/<event-id>` for PRs
- ❌ CORS headers on all responses
- ❌ OPTIONS request handling
---
## 📝 Implementation Plan
### Phase 1: Fix Current Relay (In Progress)
**Goal:** Make NIP-01 relay fully compliant
**Tasks:**
- [x] Basic WebSocket relay working
- [x] Event storage and querying
- [ ] NIP-11 relay info with GRASP fields
- [ ] Add `supported_grasps: ["GRASP-01"]`
- [ ] Add `repo_acceptance_criteria`
- [ ] Add `curation` policy
- [ ] Announcement validation
- [ ] Check `clone` tag includes our domain
- [ ] Check `relays` tag includes our domain
- [ ] Reject if not listed (unless GRASP-05)
- [ ] Event acceptance policy
- [ ] Accept events tagging accepted announcements
- [ ] Accept events tagged by accepted announcements
**Test Coverage:**
- [x] NIP-01 smoke tests passing
- [ ] NIP-11 compliance tests
- [ ] NIP-34 announcement tests
### Phase 2: Add Git HTTP Backend (Next)
**Goal:** Serve Git repositories via HTTP on same port as relay
**Tasks:**
1. **Integrate actix-web**
- [ ] Replace raw WebSocket with actix-web
- [ ] Add HTTP routing
- [ ] Preserve WebSocket upgrade for `/`
- [ ] Add Git HTTP route for `/<npub>/<id>.git`
2. **Integrate git-http-backend crate**
- [ ] Add dependency on `git-http-backend`
- [ ] Create Git handler for `/<npub>/<id>.git`
- [ ] Serve `git-upload-pack` (clone/fetch)
- [ ] Serve `git-receive-pack` (push)
3. **Repository Management**
- [ ] Auto-provision repos from announcements
- [ ] Store repos at `{GIT_DATA_PATH}/<npub>/<id>.git`
- [ ] Initialize bare repositories
- [ ] Set HEAD from state announcements
4. **CORS Support**
- [ ] Add CORS middleware to actix-web
- [ ] Set required headers on all responses
- [ ] Handle OPTIONS requests
**Test Coverage:**
- [ ] Can clone repository via HTTP
- [ ] Can fetch from repository
- [ ] Repository provisioned from announcement
- [ ] HEAD set correctly from state
- [ ] CORS headers present
- [ ] OPTIONS requests handled
### Phase 3: Push Authorization (Final)
**Goal:** Validate pushes against Nostr state announcements
**Tasks:**
1. **Inline Authorization**
- [ ] Intercept `git-receive-pack` before Git process
- [ ] Parse ref updates from request
- [ ] Query latest state announcement from relay
- [ ] Validate push matches state
- [ ] Handle maintainer sets (recursive)
- [ ] Return HTTP error if validation fails
2. **PR Support**
- [ ] Accept pushes to `refs/nostr/<event-id>`
- [ ] Validate PR event exists on relay
- [ ] Validate ref tip matches PR event `c` tag
- [ ] Implement 20-minute timeout for PR refs
- [ ] Garbage collect orphaned PR refs
3. **State Synchronization**
- [ ] Update HEAD when state announcement received
- [ ] Handle state updates for existing repos
- [ ] Handle multi-maintainer scenarios
**Test Coverage:**
- [ ] Push matching state succeeds
- [ ] Push not matching state fails
- [ ] Multi-maintainer push validation
- [ ] PR ref push/validation
- [ ] PR ref garbage collection
- [ ] State update triggers HEAD change
---
## 🐛 Known Issues
### 1. Architecture Mismatch
**Issue:** Tests assume relay on one port, Git on another
**Fix:** Both must be on same port (like ngit-relay)
**Impact:** Need to refactor server architecture
### 2. Missing Git Implementation
**Issue:** No Git HTTP backend integrated
**Fix:** Add actix-web + git-http-backend
**Impact:** Core GRASP-01 requirement not met
### 3. No Announcement Validation
**Issue:** Relay accepts all announcements
**Fix:** Validate `clone` and `relays` tags
**Impact:** Not GRASP-01 compliant
### 4. No CORS Support
**Issue:** No CORS headers on responses
**Fix:** Add CORS middleware
**Impact:** Web clients can't access relay
---
## 🔧 Environment Configuration
From `../ngit-relay/.env.example`, we need:
```bash
# Service Configuration
NGIT_DOMAIN=example.com # Used for announcement validation
NGIT_BIND_ADDRESS=127.0.0.1:8080 # Single port for HTTP/WS/Git
# Relay Information (NIP-11)
NGIT_RELAY_NAME="ngit-grasp instance"
NGIT_RELAY_DESCRIPTION="Rust GRASP implementation"
NGIT_OWNER_NPUB="npub1..." # Relay owner
# Storage Paths
NGIT_GIT_DATA_PATH=/srv/ngit-grasp/repos # Git repositories
NGIT_RELAY_DATA_PATH=/srv/ngit-grasp/relay-db # Nostr events
# Features (Future)
NGIT_PROACTIVE_SYNC_GIT=false # GRASP-02
NGIT_PROACTIVE_SYNC_NOSTR=false # GRASP-02
# Logging
NGIT_LOG_LEVEL=INFO
```
**Current .env.example status:**
- ⏳ Needs update with all required fields
- ⏳ Add GRASP-specific configuration
- ⏳ Document which fields are used where
---
## 📊 Progress Summary
### Completed ✅
- Basic Nostr relay (WebSocket)
- Event storage and querying
- NIP-01 smoke tests
- Test infrastructure (TestRelay fixture)
- Integration with grasp-audit library
### In Progress ⏳
- NIP-11 relay information
- NIP-34 announcement handling
- Event acceptance policies
### Not Started ❌
- Git HTTP backend
- Repository provisioning
- Push authorization
- CORS support
- actix-web integration
### Compliance Status
- **NIP-01:** ~60% (basic relay works, missing some features)
- **NIP-34:** ~20% (can store events, no validation)
- **GRASP-01:** ~30% (relay works, Git HTTP not started)
---
## 🎯 Next Session Priorities
1. **Fix Architecture** (CRITICAL)
- Integrate actix-web for HTTP/WebSocket routing
- Single port for all services
- Preserve existing relay functionality
2. **Add Git HTTP** (HIGH)
- Integrate `git-http-backend` crate
- Basic clone/fetch support
- Repository provisioning from announcements
3. **Update Tests** (HIGH)
- Add GRASP-01 Git HTTP tests
- Reference protocol line numbers
- Verify single-port architecture
4. **Fix NIP-11** (MEDIUM)
- Add GRASP-specific fields
- Document compliance level
- Include in tests
---
## 📚 Key References
**GRASP Protocol:**
- `../grasp/README.md` - Overview
- `../grasp/01.md` - GRASP-01 Core Requirements (THE SPEC)
- `../grasp/02.md` - GRASP-02 Proactive Sync
- `../grasp/05.md` - GRASP-05 Archive
**Reference Implementation:**
- `../ngit-relay/README.md` - Architecture overview
- `../ngit-relay/src/nginx.conf` - **CRITICAL: Shows single-port routing**
- `../ngit-relay/docker-compose.yml` - **CRITICAL: Shows port config**
- `../ngit-relay/.env.example` - Configuration template
**Nostr Specs:**
- [NIP-01](https://nips.nostr.com/1) - Basic protocol
- [NIP-11](https://nips.nostr.com/11) - Relay information
- [NIP-34](https://nips.nostr.com/34) - Git stuff
**Our Code:**
- `tests/nip01_compliance.rs` - Current test approach
- `tests/common/relay.rs` - TestRelay fixture
- `grasp-audit/src/specs/nip01_smoke.rs` - Test specs
---
**Last Updated:** November 4, 2025
**Next Review:** After actix-web integration
@@ -0,0 +1,720 @@
# Implementation Checklist
**Date:** November 4, 2025
**Purpose:** Step-by-step checklist for actix-web integration
---
## ✅ Pre-Implementation (DONE)
- [x] Review GRASP-01 specification
- [x] Review ngit-relay reference implementation
- [x] Understand single-port architecture
- [x] Document architecture in work/architecture-diagram.md
- [x] Create detailed plan in work/NEXT_SESSION_START_HERE.md
- [x] Update work/current_status.md
---
## 📦 Phase 1: Dependencies & Setup
### 1.1 Update Cargo.toml
- [ ] Add `actix-web = "4"`
- [ ] Add `actix-cors = "0.7"`
- [ ] Add `actix-ws = "0.3"` (or use actix-web-actors)
- [ ] Add `git-http-backend = "0.2"` (check latest version)
- [ ] Run `cargo check` to verify dependencies
**Verification:**
```bash
cargo tree | grep actix
cargo tree | grep git-http-backend
```
### 1.2 Update .env.example (if needed)
- [x] Already has all required fields
- [x] NGIT_DOMAIN
- [x] NGIT_BIND_ADDRESS
- [x] NGIT_GIT_DATA_PATH
- [x] NGIT_RELAY_DATA_PATH
**Verification:**
```bash
cat .env.example
```
---
## 🏗️ Phase 2: HTTP Server Module
### 2.1 Create src/http/mod.rs
- [ ] Create module structure
- [ ] Add `pub mod git;`
- [ ] Add `pub mod nostr;`
- [ ] Create `run_server()` function
- [ ] Set up actix-web HttpServer
- [ ] Add CORS middleware
- [ ] Add routing for Git and Nostr
**Verification:**
```bash
cargo check
# Should compile without errors
```
**Test:**
```rust
// In src/http/mod.rs
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_module_exists() {
// Just verify module structure
assert!(true);
}
}
```
### 2.2 Create src/http/git.rs
- [ ] Create `handle_git_request()` function
- [ ] Parse npub and repo from path
- [ ] Construct repository path
- [ ] Check if repository exists (return 404 if not)
- [ ] Use git-http-backend crate
- [ ] Handle GET (clone/fetch)
- [ ] Handle POST (push)
- [ ] Return proper HTTP responses
**Verification:**
```bash
cargo check
# Should compile
```
**Test:**
```rust
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_repo_path() {
// Test path parsing logic
let path = "/npub1abc.../my-repo.git";
// ... verify parsing
}
}
```
### 2.3 Create src/http/nostr.rs
- [ ] Create `handle_websocket()` function
- [ ] Handle WebSocket upgrade
- [ ] Reuse existing Nostr message handling
- [ ] Create `handle_http_root()` function
- [ ] Serve HTML for browsers
- [ ] Serve NIP-11 JSON for Accept: application/nostr+json
**Verification:**
```bash
cargo check
```
**Test:**
```rust
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_nip11_response() {
// Test NIP-11 JSON generation
// ...
}
}
```
---
## 🔧 Phase 3: Update Existing Code
### 3.1 Update src/config.rs
- [x] Already has `git_data_path` field
- [x] Already has `from_env()` implementation
- [ ] Verify all fields are present
- [ ] Add any missing validation
**Verification:**
```bash
cargo check
```
### 3.2 Update src/main.rs
- [ ] Remove direct relay start
- [ ] Import `http` module
- [ ] Call `http::run_server(config, storage).await`
- [ ] Update logging messages
**Verification:**
```bash
cargo build
# Should build successfully
```
**Test:**
```bash
# Run server
NGIT_DOMAIN=localhost:8080 \
NGIT_BIND_ADDRESS=127.0.0.1:8080 \
cargo run
# In another terminal, check it's listening
curl -v http://localhost:8080/
```
### 3.3 Move Relay Logic to Library
- [ ] Extract relay logic from src/nostr/relay.rs
- [ ] Make it reusable by WebSocket handler
- [ ] Keep message handling separate from transport
- [ ] Create `handle_nostr_message()` function
**Structure:**
```rust
// src/nostr/relay.rs
pub async fn handle_nostr_message(
message: &str,
storage: &Storage,
) -> Result<Vec<String>> {
// Parse message
// Handle EVENT, REQ, CLOSE
// Return response messages
}
```
**Verification:**
```bash
cargo check
cargo test --lib
```
---
## 🧪 Phase 4: Update Tests
### 4.1 Update tests/common/relay.rs
- [ ] Verify NGIT_DOMAIN is set correctly
- [ ] Add NGIT_GIT_DATA_PATH env var
- [ ] Add NGIT_RELAY_DATA_PATH env var
- [ ] Use test-specific directories
- [ ] Clean up test data after tests
**Current Status:** Already sets NGIT_DOMAIN correctly!
**Add:**
```rust
.env("NGIT_GIT_DATA_PATH", "./test-data/repos")
.env("NGIT_RELAY_DATA_PATH", "./test-data/relay")
```
**Verification:**
```bash
cargo test --test nip01_compliance
# Should still pass
```
### 4.2 Create tests/grasp01_git_http.rs
- [ ] Create new test file
- [ ] Add basic Git clone test
- [ ] Add CORS headers test
- [ ] Add OPTIONS request test
- [ ] Add repository not found test
- [ ] Reference GRASP-01 line numbers in comments
**Template:**
```rust
//! GRASP-01 Git HTTP Integration Tests
//!
//! Reference: ../grasp/01.md lines 15-40
mod common;
use common::TestRelay;
#[tokio::test]
async fn test_git_http_basic() {
// Reference: ../grasp/01.md line 15
// MUST serve git repository via unauthenticated git smart http
let relay = TestRelay::start().await;
// TODO: Create test repo
// TODO: Try to clone it
relay.stop().await;
}
```
**Verification:**
```bash
cargo test --test grasp01_git_http
```
### 4.3 Update tests/nip01_compliance.rs
- [ ] Verify tests still pass with new architecture
- [ ] Update any broken tests
- [ ] Add comments referencing GRASP-01 where relevant
**Verification:**
```bash
cargo test --test nip01_compliance
```
---
## 🔍 Phase 5: Integration & Testing
### 5.1 Manual Testing
**Test 1: Server Starts**
```bash
cargo build
NGIT_DOMAIN=localhost:8080 \
NGIT_BIND_ADDRESS=127.0.0.1:8080 \
cargo run
```
**Expected:** Server starts without errors
---
**Test 2: WebSocket Connection**
```bash
# In grasp-audit directory
cargo run -- --url ws://localhost:8080
```
**Expected:** NIP-01 smoke tests pass
---
**Test 3: HTTP Root**
```bash
curl -v http://localhost:8080/
```
**Expected:**
- Status: 200 OK
- Content-Type: text/html
- CORS headers present
- HTML content
---
**Test 4: NIP-11**
```bash
curl -v http://localhost:8080/ \
-H "Accept: application/nostr+json"
```
**Expected:**
- Status: 200 OK
- Content-Type: application/json
- CORS headers present
- JSON with `supported_grasps` field
---
**Test 5: Git Repository (404)**
```bash
curl -v http://localhost:8080/npub1test/test-repo.git/info/refs?service=git-upload-pack
```
**Expected:**
- Status: 404 Not Found
- CORS headers present
---
**Test 6: Git Repository (Success)**
```bash
# Create test repo
mkdir -p ./data/repos/npub1test
cd ./data/repos/npub1test
git init --bare test-repo.git
# Try to access it
curl -v http://localhost:8080/npub1test/test-repo.git/info/refs?service=git-upload-pack
```
**Expected:**
- Status: 200 OK
- Content-Type: application/x-git-upload-pack-advertisement
- CORS headers present
- Git protocol data
---
**Test 7: Git Clone**
```bash
git clone http://localhost:8080/npub1test/test-repo.git /tmp/test-clone
```
**Expected:**
- Clone succeeds (even if empty repo)
- No errors
---
**Test 8: CORS Preflight**
```bash
curl -v -X OPTIONS http://localhost:8080/ \
-H "Origin: https://example.com" \
-H "Access-Control-Request-Method: POST"
```
**Expected:**
- Status: 204 No Content
- Access-Control-Allow-Origin: *
- Access-Control-Allow-Methods: GET, POST
- Access-Control-Allow-Headers: Content-Type
- Access-Control-Max-Age: 3600
---
### 5.2 Automated Testing
**Run All Tests:**
```bash
# Build first
cargo build
# Run all tests
cargo test
# Run specific test suites
cargo test --test nip01_compliance
cargo test --test grasp01_git_http
# With output
cargo test -- --nocapture
```
**Expected:** All tests pass
---
### 5.3 Performance Testing
**Test Concurrent Connections:**
```bash
# Start server
cargo run &
# Run multiple clients
for i in {1..10}; do
(cd grasp-audit && cargo run -- --url ws://localhost:8080) &
done
# Wait for all to complete
wait
```
**Expected:** All clients connect and pass tests
---
### 5.4 Error Handling Testing
**Test 1: Invalid Repository Path**
```bash
curl -v http://localhost:8080/invalid/path
```
**Expected:** 404 or appropriate error
---
**Test 2: Invalid WebSocket Message**
```bash
# Use websocat or similar
echo "invalid json" | websocat ws://localhost:8080/
```
**Expected:** NOTICE message with error
---
**Test 3: Large Git Push**
```bash
# Create repo with large files
# Try to push
# Verify it works or fails gracefully
```
---
## 📋 Acceptance Criteria
### Must Have (MVP)
- [ ] Server starts on single port
- [ ] WebSocket connects at `/`
- [ ] NIP-01 smoke tests pass
- [ ] Can access Git repo at `/<npub>/<id>.git`
- [ ] Returns 404 for missing repos
- [ ] CORS headers on all responses
- [ ] OPTIONS requests return 204
- [ ] Can clone existing Git repository
- [ ] All integration tests pass
### Should Have (Before Production)
- [ ] Can push to repository (basic, no auth yet)
- [ ] Repository provisioned from announcement
- [ ] NIP-11 includes GRASP fields
- [ ] Proper error messages
- [ ] Logging works correctly
- [ ] Clean shutdown
- [ ] Test data cleanup
### Could Have (Future)
- [ ] Push authorization
- [ ] Maintainer set validation
- [ ] PR ref support
- [ ] State synchronization
- [ ] Proactive sync (GRASP-02)
---
## 🐛 Known Issues to Watch For
### Issue 1: WebSocket Upgrade Timing
**Symptom:** WebSocket upgrade fails intermittently
**Debug:**
```bash
RUST_LOG=debug cargo run
# Check for upgrade-related logs
```
**Solution:** Ensure actix-ws is configured correctly
---
### Issue 2: Git HTTP Protocol Errors
**Symptom:** Git clone fails with protocol error
**Debug:**
```bash
GIT_TRACE_PACKET=1 git clone http://localhost:8080/...
# Shows Git protocol messages
```
**Solution:** Check git-http-backend configuration
---
### Issue 3: CORS Not Applied
**Symptom:** Browser shows CORS error
**Debug:**
```bash
curl -v http://localhost:8080/ -H "Origin: https://example.com"
# Check response headers
```
**Solution:** Verify CORS middleware is first in chain
---
### Issue 4: Port Already in Use
**Symptom:** "Address already in use" error
**Debug:**
```bash
lsof -i :8080
# Find process using port
```
**Solution:**
```bash
kill -9 <PID>
# Or use different port
```
---
### Issue 5: Test Relay Won't Start
**Symptom:** Integration tests fail to start relay
**Debug:**
```bash
# Run test with output
cargo test --test nip01_compliance -- --nocapture
# Check binary exists
ls -la target/debug/ngit-grasp
```
**Solution:** Run `cargo build` before tests
---
## 📚 Reference Commands
### Development
```bash
# Build
cargo build
# Run
cargo run
# Run with logging
RUST_LOG=debug cargo run
# Check without building
cargo check
# Format code
cargo fmt
# Lint
cargo clippy
```
### Testing
```bash
# All tests
cargo test
# Specific test file
cargo test --test nip01_compliance
# Specific test
cargo test --test nip01_compliance test_nip01_smoke
# With output
cargo test -- --nocapture
# With logging
RUST_LOG=debug cargo test -- --nocapture
```
### Debugging
```bash
# Check dependencies
cargo tree
# Check for unused dependencies
cargo +nightly udeps
# Check for outdated dependencies
cargo outdated
# Audit for security issues
cargo audit
```
### Git Testing
```bash
# Create test repo
mkdir -p ./data/repos/npub1test
cd ./data/repos/npub1test
git init --bare test-repo.git
# Clone it
git clone http://localhost:8080/npub1test/test-repo.git /tmp/test
# Push to it
cd /tmp/test
echo "test" > README.md
git add .
git commit -m "test"
git push origin main
```
---
## ✅ Completion Checklist
When all items are checked, Phase 1 (actix-web integration) is complete:
### Code
- [ ] Dependencies added to Cargo.toml
- [ ] src/http/mod.rs created
- [ ] src/http/git.rs created
- [ ] src/http/nostr.rs created
- [ ] src/main.rs updated
- [ ] src/config.rs verified
- [ ] Relay logic refactored
### Tests
- [ ] tests/common/relay.rs updated
- [ ] tests/grasp01_git_http.rs created
- [ ] tests/nip01_compliance.rs still passes
- [ ] All tests pass
### Manual Testing
- [ ] Server starts successfully
- [ ] WebSocket connects
- [ ] NIP-01 smoke tests pass
- [ ] Can access Git repos
- [ ] 404 for missing repos
- [ ] CORS headers present
- [ ] OPTIONS requests work
- [ ] Can clone repository
### Documentation
- [ ] Update README.md status
- [ ] Update work/current_status.md
- [ ] Document any issues found
- [ ] Update NEXT_SESSION_START_HERE.md for next phase
---
## 🎯 Next Phase Preview
After actix-web integration is complete, next phase will be:
**Phase 2: Repository Provisioning**
- Listen for NIP-34 repository announcements
- Create Git repositories automatically
- Initialize bare repositories
- Set up directory structure
- Handle repository deletion
**Estimated Time:** 2-3 hours
**Prerequisites:** Phase 1 complete
---
**Last Updated:** November 4, 2025
**Status:** Ready to begin Phase 1
@@ -0,0 +1,513 @@
# GRASP Protocol Review Summary
**Date:** November 4, 2025
**Purpose:** Document key findings from reviewing GRASP protocol and ngit-relay
---
## 🎯 Critical Discoveries
### 1. Single Port Architecture (CRITICAL!)
**Finding:** Git server and Nostr relay MUST run on the SAME port.
**Evidence:**
```yaml
# ../ngit-relay/docker-compose.yml
ports:
- "8081:8081" # Single port only!
```
```nginx
# ../ngit-relay/src/nginx.conf
server {
listen 8081; # One listener for everything
location ~ ^/npub1([a-z0-9]+)/([^/]+\.git)(/.*)?$ {
# Git HTTP via fcgiwrap
}
location / {
# Nostr relay via proxy to localhost:3334
}
}
```
**Impact:**
- Our current architecture is WRONG
- We assumed separate ports (relay on 8080, git on 8081)
- Must use HTTP router to split traffic by path
- actix-web can handle this
**Action Required:**
- Integrate actix-web for HTTP routing
- Route `/<npub>/<id>.git` to Git handler
- Route `/` to Nostr relay (WebSocket upgrade)
- Apply CORS to ALL routes
---
### 2. GRASP-01 Test Requirements
**Finding:** Tests must closely map to GRASP protocol specification.
**GRASP-01 Requirements (from ../grasp/01.md):**
#### Nostr Relay (Lines 1-14)
- ✅ Serve NIP-01 relay at `/` (WebSocket)
- ⏳ Accept NIP-34 repository announcements (kind 30617)
- ⏳ Accept NIP-34 state announcements (kind 30618)
- ⏳ Reject announcements without service in `clone` and `relays` tags
- ⏳ Accept events that tag accepted announcements
- ✅ Serve NIP-11 relay information
- ⏳ Include `supported_grasps`, `repo_acceptance_criteria`, `curation` in NIP-11
#### Git Smart HTTP (Lines 15-31)
- ❌ Serve repos at `/<npub>/<identifier>.git`
- ❌ Accept pushes matching state announcements
- ❌ Respect recursive maintainer sets
- ❌ Set HEAD per state announcement
- ❌ Accept pushes to `refs/nostr/<event-id>` for PRs
- ❌ Include `allow-reachable-sha1-in-want` and `allow-tip-sha1-in-want`
- ❌ Serve webpage for browsers
#### CORS Support (Lines 32-40)
- ❌ `Access-Control-Allow-Origin: *` on ALL responses
- ❌ `Access-Control-Allow-Methods: GET, POST` on ALL responses
- ❌ `Access-Control-Allow-Headers: Content-Type` on ALL responses
- ❌ Respond to OPTIONS with 204 No Content
**Action Required:**
- Create test for each requirement
- Reference GRASP-01 line numbers in test comments
- Example:
```rust
#[tokio::test]
async fn test_git_http_basic() {
// Reference: ../grasp/01.md line 15
// MUST serve git repository via unauthenticated git smart http
// ...
}
```
---
### 3. Environment Variables
**Finding:** ngit-relay uses specific environment variables we should match.
**From ../ngit-relay/.env.example:**
```bash
# Service Configuration
NGIT_DOMAIN=example.com # For announcement validation
NGIT_INTERNAL_RELAY_PORT_FOR_SSL_PROXY=8081 # We don't need this
# Relay Information (NIP-11)
NGIT_RELAY_NAME="..."
NGIT_RELAY_DESCRIPTION="..."
NGIT_OWNER_NPUB="..."
# Features
NGIT_PROACTIVE_SYNC_GIT=true # GRASP-02 (future)
NGIT_PROACTIVE_SYNC_BLOSSOM=true # Not in GRASP
NGIT_PROACTIVE_SYNC_NOSTR=true # GRASP-02 (future)
# Blossom Settings
NGIT_BLOSSOM_MAX_FILE_SIZE_MB=100 # Not in GRASP
NGIT_BLOSSOM_MAX_CAPACITY_GB=50 # Not in GRASP
# Logging
NGIT_LOG_DIR=/var/log/ngit-relay
NGIT_LOG_LEVEL=INFO
NGIT_LOG_MAX_SIZE_MB=20
NGIT_LOG_MAX_BACKUPS=10
NGIT_LOG_MAX_AGE_DAYS=30
```
**Our Environment Variables:**
```bash
# Service Configuration
NGIT_DOMAIN=example.com # REQUIRED - for announcement validation
NGIT_BIND_ADDRESS=127.0.0.1:8080 # REQUIRED - single port
# Relay Information (NIP-11)
NGIT_RELAY_NAME="ngit-grasp instance"
NGIT_RELAY_DESCRIPTION="Rust GRASP implementation"
NGIT_OWNER_NPUB="npub1..."
# Storage Paths
NGIT_GIT_DATA_PATH=./data/repos # REQUIRED - where to store Git repos
NGIT_RELAY_DATA_PATH=./data/relay # REQUIRED - where to store events
# Logging
NGIT_LOG_LEVEL=INFO
RUST_LOG=info # Standard Rust logging
```
**Action Required:**
- Update `.env.example` with all required fields
- Add `NGIT_GIT_DATA_PATH` to config
- Document which fields are required vs. optional
---
### 4. Repository Path Structure
**Finding:** Repository storage follows specific pattern.
**Pattern:** `{GIT_DATA_PATH}/{npub}/{identifier}.git`
**Example:**
```
./data/repos/
├── npub1abc.../
│ ├── my-project.git/
│ │ ├── HEAD
│ │ ├── config
│ │ ├── objects/
│ │ └── refs/
│ └── another-repo.git/
└── npub1xyz.../
└── their-project.git/
```
**Action Required:**
- Create repository directory structure
- Initialize bare repositories (`git init --bare`)
- Set ownership/permissions correctly
- Clean up on repository deletion
---
### 5. NIP-11 GRASP Fields
**Finding:** NIP-11 relay information must include GRASP-specific fields.
**From ../grasp/01.md lines 11-14:**
```json
{
"name": "ngit-grasp instance",
"description": "Rust GRASP implementation",
"pubkey": "...",
"contact": "...",
"supported_nips": [1, 11, 34],
"supported_grasps": ["GRASP-01"], // NEW - array of strings
"repo_acceptance_criteria": "...", // NEW - human readable
"curation": "WoT-based spam prevention" // NEW - optional
}
```
**Action Required:**
- Add `supported_grasps` field to NIP-11 response
- Add `repo_acceptance_criteria` field
- Add `curation` field (optional)
- Update NIP-11 tests to verify these fields
---
### 6. Announcement Validation
**Finding:** Relay must validate announcements list this service.
**From ../grasp/01.md lines 3-5:**
> MUST reject [git repository announcements] that do not list the service
> in both `clone` and `relays` tags unless implementing `GRASP-05`.
**NIP-34 Repository Announcement (kind 30617):**
```json
{
"kind": 30617,
"tags": [
["d", "my-project"], // identifier
["name", "My Project"],
["clone", "https://example.com/npub.../my-project.git"],
["clone", "https://github.com/user/my-project"],
["relays", "wss://example.com"],
["relays", "wss://relay.nostr.band"]
]
}
```
**Validation Logic:**
```rust
fn validate_announcement(event: &Event, our_domain: &str) -> Result<()> {
// Check for clone tag with our domain
let has_clone = event.tags.iter().any(|tag| {
tag.kind() == TagKind::Custom("clone".into()) &&
tag.content().map(|c| c.contains(our_domain)).unwrap_or(false)
});
// Check for relays tag with our domain
let has_relay = event.tags.iter().any(|tag| {
tag.kind() == TagKind::Custom("relays".into()) &&
tag.content().map(|c| c.contains(our_domain)).unwrap_or(false)
});
if !has_clone || !has_relay {
return Err(anyhow!("Announcement must list this service in both clone and relays tags"));
}
Ok(())
}
```
**Action Required:**
- Implement announcement validation
- Check both `clone` and `relays` tags
- Reject if service not listed
- Add tests for validation
---
### 7. State Announcement Handling
**Finding:** State announcements control repository state.
**NIP-34 Repository State (kind 30618):**
```json
{
"kind": 30618,
"tags": [
["d", "my-project"], // identifier
["refs/heads/main", "abc123..."], // branch → commit
["refs/heads/develop", "def456..."],
["HEAD", "ref: refs/heads/main"], // symbolic ref
["maintainers", "npub1...", "npub2..."] // maintainer set
]
}
```
**State Handling:**
1. When state announcement received:
- Update repository HEAD if needed
- Store state for push validation
- Handle maintainer set
2. When push received:
- Query latest state announcement
- Validate pusher is in maintainer set (recursive)
- Validate ref updates match state
- Accept or reject push
**Action Required:**
- Parse state announcements
- Update repository HEAD
- Implement push validation
- Handle recursive maintainer sets
---
### 8. PR Ref Handling
**Finding:** Special handling for PR refs.
**From ../grasp/01.md lines 22-23:**
> 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 with 20 minutes.
**PR Ref Lifecycle:**
1. Push to `refs/nostr/<event-id>`
2. Verify PR event exists on relay
3. Verify ref tip matches PR event `c` tag
4. Accept push
5. After 20 minutes, check if PR event still exists
6. If not, delete ref and garbage collect
**Action Required:**
- Accept pushes to `refs/nostr/<event-id>`
- Validate against PR events
- Implement 20-minute timeout
- Implement garbage collection
---
### 9. Git HTTP Protocol Details
**Finding:** Must support specific Git protocol features.
**From ../grasp/01.md lines 25-26:**
> MUST include `allow-reachable-sha1-in-want` and `allow-tip-sha1-in-want`
> in advertisement and serve available oids.
**Git Capabilities:**
```
# info/refs response must include:
allow-reachable-sha1-in-want
allow-tip-sha1-in-want
```
**Action Required:**
- Configure git-http-backend to advertise these capabilities
- Ensure Git process is configured correctly
- Test with actual Git client
---
### 10. CORS Requirements
**Finding:** CORS must be on ALL responses, not just some.
**From ../grasp/01.md lines 32-40:**
```
1. Set `Access-Control-Allow-Origin: *` on ALL responses
2. Set `Access-Control-Allow-Methods: GET, POST` on ALL responses
3. Set `Access-Control-Allow-Headers: Content-Type` on ALL responses
4. Respond to OPTIONS requests with 204 No Content
```
**Implementation:**
```rust
// In actix-web
App::new()
.wrap(
Cors::default()
.allow_any_origin()
.allowed_methods(vec!["GET", "POST"])
.allowed_headers(vec!["Content-Type"])
.max_age(3600)
)
// ... routes
```
**Action Required:**
- Add CORS middleware to actix-web
- Verify headers on all responses
- Handle OPTIONS requests
- Test with browser
---
## 📊 Compliance Status
### NIP-01 (Nostr Relay)
- ✅ WebSocket connection
- ✅ EVENT message handling
- ✅ REQ subscription
- ✅ CLOSE subscription
- ✅ Event validation
- ⏳ NIP-11 with GRASP fields
**Status:** ~80% complete
### NIP-34 (Git Announcements)
- ✅ Store announcements (kind 30617)
- ✅ Store state events (kind 30618)
- ⏳ Validate announcements list this service
- ⏳ Handle maintainer sets
- ⏳ Accept related events
**Status:** ~40% complete
### GRASP-01 (Core Requirements)
- ✅ Nostr relay at `/`
- ❌ Git HTTP at `/<npub>/<id>.git`
- ❌ Push validation
- ❌ Repository provisioning
- ❌ CORS support
**Status:** ~20% complete
---
## 🎯 Immediate Next Steps
### 1. Fix Architecture (CRITICAL)
- [ ] Add actix-web dependencies
- [ ] Create HTTP router module
- [ ] Route Git paths to Git handler
- [ ] Route `/` to Nostr relay (WebSocket)
- [ ] Apply CORS to all routes
**Estimated Time:** 2-4 hours
**Priority:** CRITICAL
**Blocker:** Nothing else can proceed without this
### 2. Add Git HTTP Backend
- [ ] Integrate git-http-backend crate
- [ ] Create Git request handler
- [ ] Serve from `{GIT_DATA_PATH}/{npub}/{id}.git`
- [ ] Return 404 for missing repos
- [ ] Test with `git clone`
**Estimated Time:** 2-3 hours
**Priority:** HIGH
**Blocker:** Requires architecture fix
### 3. Repository Provisioning
- [ ] Create repos when announcements received
- [ ] Initialize bare repositories
- [ ] Set up directory structure
- [ ] Handle repository deletion
**Estimated Time:** 1-2 hours
**Priority:** HIGH
**Blocker:** Requires Git HTTP backend
### 4. Update Tests
- [ ] Add GRASP-01 line number references
- [ ] Create Git HTTP tests
- [ ] Create CORS tests
- [ ] Update NIP-11 tests
**Estimated Time:** 2-3 hours
**Priority:** MEDIUM
**Blocker:** None (can start now)
---
## 📚 Key Files to Reference
### GRASP Protocol
- `../grasp/01.md` - **THE SPEC** - Lines 1-40
- `../grasp/README.md` - Overview
- `../grasp/02.md` - GRASP-02 (future)
- `../grasp/05.md` - GRASP-05 (future)
### Reference Implementation
- `../ngit-relay/src/nginx.conf` - **ROUTING PATTERN** - Lines 8-94
- `../ngit-relay/docker-compose.yml` - Port configuration
- `../ngit-relay/.env.example` - Environment variables
- `../ngit-relay/README.md` - Architecture overview
### Our Code
- `tests/nip01_compliance.rs` - Current test approach
- `tests/common/relay.rs` - TestRelay fixture (already correct!)
- `src/nostr/relay.rs` - Current relay implementation
- `src/config.rs` - Configuration (needs Git path)
---
## ✅ Checklist for Next Session
Before starting implementation:
- [x] Read GRASP-01 specification (../grasp/01.md)
- [x] Review ngit-relay nginx.conf routing
- [x] Understand single-port architecture
- [x] Review environment variables
- [x] Understand repository path structure
Ready to implement:
- [ ] Add actix-web dependencies to Cargo.toml
- [ ] Create src/http/mod.rs module
- [ ] Create src/http/git.rs handler
- [ ] Create src/http/nostr.rs handler
- [ ] Update src/main.rs
- [ ] Update src/config.rs
- [ ] Update .env.example
- [ ] Update tests/common/relay.rs
- [ ] Create tests/grasp01_git_http.rs
---
**Last Updated:** November 4, 2025
**Next Review:** After actix-web integration
@@ -0,0 +1,121 @@
# Session Complete: Ready for Test Validation Phase
**Date:** 2025-11-04
**Status:** ✅ READY TO BEGIN
---
## ✅ What We Did
### 1. Strategic Planning
- Analyzed test-first vs TDD parallel approaches
- Decided to validate grasp-audit against ngit-relay first
- Validated hybrid architecture (git2 + git-http-backend + system git)
### 2. Documentation
- Archived all planning documents to `docs/archive/2025-11-04-*`
- Created fresh `work/current_status.md` for test validation phase
- Documented strategic decision and rationale
### 3. Preparation
- Identified ngit-relay location: `../ngit-relay/`
- Confirmed Docker image: `ghcr.io/danconwaydev/ngit-relay:latest`
- Outlined complete test validation plan
---
## 📋 Current State
```
work/
├── README.md ✅ (gitignored, explains work/)
└── current_status.md ✅ (test validation plan)
docs/archive/
├── 2025-11-04-session-summary.md ✅ (this session)
├── 2025-11-04-ngit-grasp-implementation-plan.md ✅ (for later)
├── 2025-11-04-git-http-backend-validation.md ✅ (architecture)
├── 2025-11-04-test-strategy-decision.md ✅ (rationale)
├── 2025-11-04-git-http-backend-deep-dive.md ✅ (crate analysis)
└── 2025-11-04-authorization-flow-diagram.txt ✅ (visual ref)
```
---
## 🎯 Next Session: Start Here
### Quick Start Command
```bash
# 1. Read the plan
cat work/current_status.md
# 2. Start ngit-relay
cd ../ngit-relay
docker-compose up -d
# 3. Verify it's working
curl http://localhost:8080 # Nostr relay
curl http://localhost:3000 # Git server
# 4. Begin building tests
cd ../ngit-grasp/grasp-audit
nix develop
# Create src/specs/grasp01_git.rs
```
### Timeline
- **Phase 1:** Setup ngit-relay (30 min)
- **Phase 2:** Build GRASP-01 Git tests (1 day)
- **Phase 3:** Validate against ngit-relay (1 day)
- **Phase 4:** Document findings (2 hours)
- **Total:** ~2 days
---
## 📚 Key Documents
### For This Phase (Test Validation)
- **Plan:** `work/current_status.md` ← START HERE
- **Rationale:** `docs/archive/2025-11-04-test-strategy-decision.md`
- **Reference:** `../ngit-relay/README.md`
### For Later (Implementation)
- **Implementation Plan:** `docs/archive/2025-11-04-ngit-grasp-implementation-plan.md`
- **Architecture:** `docs/archive/2025-11-04-git-http-backend-validation.md`
- **Flow Diagram:** `docs/archive/2025-11-04-authorization-flow-diagram.txt`
---
## 🚀 The Goal
**By end of next session:**
- ✅ grasp-audit has complete GRASP-01 Git test suite
- ✅ All tests pass against ngit-relay reference implementation
- ✅ Reference behavior documented
- ✅ Confident test suite ready for ngit-grasp implementation
**Then we can implement ngit-grasp knowing our tests are correct!**
---
## 💡 Why This Approach?
**Question:** Why not just start implementing ngit-grasp?
**Answer:**
- Testing against reference validates our test suite first
- Eliminates "is it the test or the code?" debugging
- Only 1-2 day investment for weeks of confidence
- Same total timeline but much lower risk
**See:** `docs/archive/2025-11-04-test-strategy-decision.md` for full analysis
---
## ✅ Ready!
**Status:** All planning complete, ready to begin test validation
**First Step:** `cd ../ngit-relay && docker-compose up -d`
**Reference:** `work/current_status.md`
Let's build a rock-solid test suite! 🚀
@@ -0,0 +1,487 @@
# Session Summary - GRASP Protocol Review
**Date:** November 4, 2025
**Duration:** ~2 hours
**Status:** ✅ Complete - Ready for implementation
---
## 🎯 Session Goals
1. ✅ Review GRASP protocol specification
2. ✅ Review ngit-relay reference implementation
3. ✅ Understand architecture requirements
4. ✅ Update work documents with accurate plan
5. ✅ Fix mistakes in previous understanding
---
## 🔍 Key Discoveries
### 1. Single Port Architecture (CRITICAL!)
**Previous Understanding (WRONG):**
- Nostr relay on port 8080
- Git server on port 8081
- Separate services
**Correct Understanding:**
- **BOTH services on SAME port** (e.g., 8080)
- HTTP router splits traffic by path:
- `/<npub>/<id>.git` → Git handler
- `/` → Nostr relay (WebSocket)
- This is a GRASP-01 requirement!
**Evidence:**
- `../ngit-relay/docker-compose.yml` - Single port (8081)
- `../ngit-relay/src/nginx.conf` - nginx routes by path on one listener
**Impact:**
- Complete architecture redesign needed
- Must use actix-web for HTTP routing
- All previous assumptions about ports were wrong
---
### 2. Test Requirements Must Map to Protocol
**Discovery:** Tests must reference GRASP protocol line numbers.
**Example:**
```rust
#[tokio::test]
async fn test_git_http_basic() {
// Reference: ../grasp/01.md line 15
// MUST serve git repository via unauthenticated git smart http service
// at /<npub>/<identifier>.git
// Test implementation...
}
```
**Why:**
- Makes tests traceable to requirements
- Easy to verify compliance
- Documents what we're testing
- Helps reviewers understand intent
**Action:**
- Update all test files with protocol references
- Create new tests for missing requirements
- Organize tests by GRASP-01 sections
---
### 3. Environment Variables
**Discovery:** ngit-relay uses specific env var naming we should match.
**Critical Variables:**
- `NGIT_DOMAIN` - Used for announcement validation (REQUIRED)
- `NGIT_BIND_ADDRESS` - Single port for all services (REQUIRED)
- `NGIT_GIT_DATA_PATH` - Where to store Git repos (REQUIRED)
- `NGIT_RELAY_DATA_PATH` - Where to store events (REQUIRED)
**Our .env.example:**
- ✅ Already has all required fields
- ✅ Follows ngit-relay naming convention
- ✅ No changes needed
---
### 4. NIP-11 GRASP Fields
**Discovery:** NIP-11 must include GRASP-specific fields.
**Required Fields:**
```json
{
"supported_grasps": ["GRASP-01"],
"repo_acceptance_criteria": "Must list service in clone and relays tags",
"curation": "Basic spam prevention" // optional
}
```
**Action:**
- Add fields to NIP-11 response
- Update tests to verify fields
- Document in code
---
### 5. Repository Path Structure
**Discovery:** Repos follow specific path pattern.
**Pattern:** `{GIT_DATA_PATH}/{npub}/{identifier}.git`
**Example:**
```
./data/repos/
├── npub1abc.../
│ └── my-project.git/
└── npub1xyz.../
└── their-repo.git/
```
**Action:**
- Create directory structure on repo provision
- Initialize bare repositories
- Handle cleanup on deletion
---
### 6. CORS Requirements
**Discovery:** CORS must be on ALL responses, not optional.
**Requirements (GRASP-01 lines 32-40):**
1. `Access-Control-Allow-Origin: *` on ALL responses
2. `Access-Control-Allow-Methods: GET, POST` on ALL responses
3. `Access-Control-Allow-Headers: Content-Type` on ALL responses
4. Respond to OPTIONS with 204 No Content
**Implementation:**
- Use actix-cors middleware
- Apply to all routes
- Test with browser
---
### 7. Announcement Validation
**Discovery:** Must validate announcements list this service.
**Rule (GRASP-01 lines 3-5):**
> MUST reject announcements that do not list the service in both
> `clone` and `relays` tags unless implementing GRASP-05.
**Validation Logic:**
```rust
fn validate_announcement(event: &Event, domain: &str) -> Result<()> {
let has_clone = event.tags.iter().any(|tag|
tag.is_clone() && tag.content().contains(domain)
);
let has_relay = event.tags.iter().any(|tag|
tag.is_relay() && tag.content().contains(domain)
);
if !has_clone || !has_relay {
return Err("Must list service in clone and relays");
}
Ok(())
}
```
**Action:**
- Implement validation in event handler
- Reject invalid announcements
- Add tests
---
## 📄 Documents Created
### 1. work/current_status.md
**Purpose:** Comprehensive status of implementation
**Contents:**
- GRASP-01 requirements checklist
- Architecture understanding
- Current implementation status
- Known issues
- Next priorities
- Key references
**Use:** Reference for overall project status
---
### 2. work/NEXT_SESSION_START_HERE.md
**Purpose:** Step-by-step implementation guide
**Contents:**
- Immediate goal (actix-web integration)
- Critical architecture understanding
- 8-step implementation plan with code examples
- Verification steps
- Common issues & solutions
- Success criteria
**Use:** Start here next session for implementation
---
### 3. work/review-summary.md
**Purpose:** Document findings from GRASP/ngit-relay review
**Contents:**
- 10 critical discoveries
- Evidence for each
- Action items
- Compliance status
- Next steps
**Use:** Reference for why we're making changes
---
### 4. work/architecture-diagram.md
**Purpose:** Visual reference for architecture
**Contents:**
- Current vs. target architecture diagrams
- Request flow examples
- Component responsibilities
- File structure
- Comparison with ngit-relay
**Use:** Visual reference during implementation
---
### 5. work/implementation-checklist.md
**Purpose:** Detailed checklist for implementation
**Contents:**
- 5 phases with detailed tasks
- Verification steps for each task
- Manual testing procedures
- Automated testing commands
- Acceptance criteria
- Known issues to watch for
- Reference commands
**Use:** Track progress during implementation
---
### 6. work/session-summary.md (this file)
**Purpose:** Summary of this review session
**Contents:**
- What we accomplished
- Key discoveries
- Documents created
- Next steps
**Use:** Remember what we did this session
---
## 📊 Compliance Status
### Before This Session
- **Understanding:** Incorrect (separate ports)
- **NIP-01:** ~60% (relay works)
- **NIP-34:** ~20% (basic storage)
- **GRASP-01:** ~10% (wrong architecture)
### After This Session
- **Understanding:** ✅ Correct (single port, routing)
- **NIP-01:** ~60% (no change, but plan to improve)
- **NIP-34:** ~20% (no change, but plan ready)
- **GRASP-01:** ~20% (plan ready, architecture understood)
### Target After Next Session
- **Understanding:** ✅ Complete
- **NIP-01:** ~80% (with actix-web)
- **NIP-34:** ~40% (with announcement validation)
- **GRASP-01:** ~60% (with Git HTTP working)
---
## 🎯 Next Session Plan
### Immediate Goal
**Integrate actix-web for single-port HTTP/WebSocket/Git routing**
### Steps (from NEXT_SESSION_START_HERE.md)
1. Add dependencies (actix-web, actix-cors, actix-ws, git-http-backend)
2. Create src/http/mod.rs (HTTP server)
3. Create src/http/git.rs (Git handler)
4. Create src/http/nostr.rs (WebSocket handler)
5. Update src/main.rs
6. Update tests
7. Manual testing
8. Automated testing
### Success Criteria
- ✅ Server starts on single port
- ✅ WebSocket connects at `/`
- ✅ NIP-01 smoke tests pass
- ✅ Can clone Git repo
- ✅ CORS headers present
- ✅ All tests pass
### Estimated Time
2-4 hours for core implementation
1-2 hours for testing and debugging
**Total: 3-6 hours**
---
## 📚 Key References for Next Session
### Must Read Before Starting
1. `work/NEXT_SESSION_START_HERE.md` - Implementation guide
2. `work/architecture-diagram.md` - Visual reference
3. `../grasp/01.md` - THE SPEC (lines 1-40)
4. `../ngit-relay/src/nginx.conf` - Routing pattern
### Reference During Implementation
1. `work/implementation-checklist.md` - Track progress
2. `work/current_status.md` - Overall context
3. [actix-web docs](https://actix.rs/docs/) - Framework reference
4. [git-http-backend docs](https://docs.rs/git-http-backend/) - Git protocol
### Reference for Testing
1. `tests/common/relay.rs` - TestRelay fixture
2. `grasp-audit/src/specs/nip01_smoke.rs` - Test specs
3. `work/implementation-checklist.md` - Testing procedures
---
## ✅ Accomplishments
### Understanding
- ✅ Fully understand GRASP-01 requirements
- ✅ Understand ngit-relay architecture
- ✅ Identified critical mistake (separate ports)
- ✅ Understand how to fix it (actix-web routing)
### Documentation
- ✅ Created comprehensive status document
- ✅ Created step-by-step implementation guide
- ✅ Created architecture diagrams
- ✅ Created detailed checklist
- ✅ Created review summary
- ✅ Created session summary
### Planning
- ✅ Detailed 8-step implementation plan
- ✅ Identified all required changes
- ✅ Created acceptance criteria
- ✅ Prepared verification steps
- ✅ Listed common issues to watch for
### Preparation
- ✅ Verified .env.example is correct
- ✅ Verified TestRelay is correct
- ✅ Identified which files need changes
- ✅ Created code templates for new files
---
## 🚀 Ready for Implementation
### What's Ready
- ✅ Complete understanding of requirements
- ✅ Detailed implementation plan
- ✅ Code templates prepared
- ✅ Test strategy defined
- ✅ Verification procedures documented
### What's Needed
- [ ] Time to implement (~4 hours)
- [ ] Focus for coding
- [ ] Testing as we go
- [ ] Patience for debugging
### Confidence Level
**HIGH** - We have:
- Clear understanding of problem
- Detailed solution plan
- Reference implementation to follow
- Good test coverage strategy
- Comprehensive documentation
---
## 💡 Key Insights
### 1. Architecture Matters
The single-port architecture is not just a detail - it's fundamental to GRASP-01 compliance. Getting this wrong means the whole implementation is wrong.
### 2. Reference Implementation is Gold
ngit-relay's nginx.conf showed us EXACTLY how to route traffic. We don't need to guess - we can copy the pattern.
### 3. Tests Must Map to Spec
Having tests that reference protocol line numbers makes verification trivial. We can see exactly which requirements we've met.
### 4. Documentation Saves Time
Taking time to document our understanding and plan saves hours of confused implementation. We know exactly what to do.
### 5. Incremental Progress
We can implement in phases:
1. HTTP routing (this phase)
2. Repository provisioning
3. Push authorization
4. Full compliance
Each phase is testable and valuable on its own.
---
## 🎓 Lessons Learned
### What Went Well
- Thorough review of GRASP protocol
- Found critical architecture issue early
- Created comprehensive documentation
- Have clear path forward
### What Could Be Better
- Could have reviewed GRASP spec earlier
- Could have checked ngit-relay architecture first
- Could have validated assumptions sooner
### For Next Time
- Always check reference implementation first
- Read the spec thoroughly before coding
- Validate architecture assumptions early
- Document understanding before implementing
---
## 📝 Notes for Future
### When to Revisit This
- Before starting implementation (read NEXT_SESSION_START_HERE.md)
- When confused about architecture (read architecture-diagram.md)
- When stuck on a requirement (read review-summary.md)
- When tracking progress (read implementation-checklist.md)
### What to Archive Later
- This session-summary.md → docs/archive/2025-11-04-grasp-review.md
- implementation-checklist.md → Delete after implementation complete
- NEXT_SESSION_START_HERE.md → Update for next phase
### What to Keep
- current_status.md → Update as we progress
- architecture-diagram.md → Reference documentation
- review-summary.md → Reference for decisions
---
## ✨ Final Thoughts
This session was highly productive. We:
1. Identified a critical architectural flaw
2. Fully understood the correct architecture
3. Created a detailed implementation plan
4. Prepared everything needed for next session
**We're ready to build this right.**
The next session will be focused implementation - we have everything we need to succeed.
---
**Session End:** November 4, 2025
**Next Session:** Implementation of actix-web integration
**Confidence:** HIGH ✅
---
**Remember:** Start with `work/NEXT_SESSION_START_HERE.md`
+139 -217
View File
@@ -1,254 +1,176 @@
# Session Summary - November 4, 2025
## Objective
Fix compilation errors in the `grasp-audit` crate and upgrade to latest nostr-sdk.
## Status: ✅ COMPLETE - Upgraded to nostr-sdk 0.43
**ARCHIVED: 2025-11-04**
**Session:** Strategic Planning & Test Validation Prep
**Outcome:** Decided to validate grasp-audit against ngit-relay first
---
## What We Did
# Session Summary: Strategic Planning
### 1. Identified Compilation Errors (nostr-sdk 0.35)
Started by attempting to build the project and discovered 9 compilation errors caused by API changes in `nostr-sdk` v0.35.
### 2. Fixed Errors for 0.35
Systematically fixed each error for nostr-sdk 0.35:
### 3. Discovered Version Gap
Realized the project was using nostr-sdk **0.35** when the latest is **0.43** - **8 minor versions behind**!
### 4. Upgraded to nostr-sdk 0.43
Completely upgraded to the latest version, fixing all new breaking changes:
1. **EventBuilder::new()** - Removed tags parameter, use `.tags()` method instead
2. **EventBuilder::to_event()** → **sign_with_keys()** - Renamed method
3. **Client::new()** - Takes ownership of keys (clone instead of reference)
4. **Relay::is_connected()** - No longer async (remove `.await`)
5. **Client::get_events_of()** → **fetch_events()** - Complete API redesign
6. **EventSource** - Removed entirely
7. **Filter::custom_tag()** - Takes single value instead of array
8. **Client::send_event()** - Takes reference instead of ownership
9. **Multiple filters** - Loop and combine instead of vec parameter
10. **Events type** - New return type, convert to `Vec<Event>` with `.into_iter().collect()`
### 5. Verified Build Success
- ✅ Clean build with no errors
- ✅ All 12 unit tests passing
- ✅ CLI binary builds successfully
- ✅ Example builds successfully
**Date:** 2025-11-04
**Duration:** ~3 hours
**Status:** ✅ Complete - Ready for implementation
---
## Results
## What We Accomplished
### Build Output
```
Finished `dev` profile [unoptimized + debuginfo] target(s) in 2.65s
```
### 1. Strategic Analysis
- ✅ Analyzed two approaches: TDD parallel vs. test-first
- ✅ Evaluated git-http-backend crate for inline authorization
- ✅ Validated hybrid architecture (git2 + git-http-backend + system git)
- ✅ Decided to test ngit-relay first (1-2 day investment)
### Test Results
```
running 13 tests
test audit::tests::test_production_config ... ok
test audit::tests::test_ci_config ... ok
test audit::tests::test_audit_tags ... ok
test isolation::tests::test_generate_prod_run_id ... ok
test isolation::tests::test_generate_ci_run_id ... ok
test result::tests::test_audit_result ... ok
test specs::nip01_smoke::tests::test_smoke_tests_against_relay ... ignored
test isolation::tests::test_generate_test_id ... ok
test result::tests::test_result_fail ... ok
test result::tests::test_result_pass ... ok
test client::tests::test_event_builder ... ok
test audit::tests::test_audit_event_builder ... ok
test client::tests::test_client_creation ... ok
### 2. Documentation Created
- ✅ `current_status.md` - TDD implementation plan for ngit-grasp
- ✅ `analysis-summary.md` - git-http-backend validation
- ✅ `strategic-recommendation.md` - Test strategy decision
- ✅ `git-http-backend-analysis.md` - Deep dive into crate
- ✅ `authorization-flow.txt` - Visual flow diagram
test result: ok. 12 passed; 0 failed; 1 ignored; 0 measured; 0 filtered out
```
### 3. Documentation Archived
All planning docs moved to `docs/archive/2025-11-04-*`:
- `ngit-grasp-implementation-plan.md` - Full TDD plan (for later)
- `git-http-backend-validation.md` - Crate analysis
- `test-strategy-decision.md` - Why test-first approach
- `git-http-backend-deep-dive.md` - Detailed crate analysis
- `authorization-flow-diagram.txt` - Visual reference
### CLI Verification
### 4. New Current Status
Created fresh `work/current_status.md` for Phase 1:
- **Goal:** Validate grasp-audit against ngit-relay
- **Timeline:** 2 days
- **Phases:** Setup → Build tests → Validate → Document
- **Ready to begin immediately**
---
## Key Decisions
### ✅ Test ngit-relay First
**Decision:** Build and validate grasp-audit test suite against reference implementation before implementing ngit-grasp
**Rationale:**
- Only 1-2 day investment
- Eliminates "is it the test or the code?" debugging
- Provides reference behavior documentation
- Same total timeline but higher confidence
- Lower risk of wasted implementation effort
**Alternative Rejected:** TDD parallel development (higher risk, same timeline)
### ✅ Hybrid Architecture Validated
**Decision:** Use git-http-backend (forked) + git2 + system git
**Components:**
- `git-http-backend` - HTTP protocol handling (will fork for inline auth)
- `git2` - Repository management, ref operations
- System git - Pack operations (upload-pack, receive-pack)
**Why:** Best balance of control, reliability, and implementation effort
---
## Resources Available
### Reference Implementation
- **Location:** `../ngit-relay/`
- **Docker:** `ghcr.io/danconwaydev/ngit-relay:latest`
- **Endpoints:**
- Nostr: `ws://localhost:8080`
- Git: `http://localhost:3000`
### Test Suite
- **Location:** `grasp-audit/`
- **Status:** Basic structure, NIP-01 smoke test working
- **Next:** Add GRASP-01 Git compliance tests
### Documentation
- **GRASP Spec:** https://gitworkshop.dev/danconwaydev.com/grasp
- **NIP-34:** https://nips.nostr.com/34
- **Archived Plans:** `docs/archive/2025-11-04-*`
---
## Next Session Goals
### Phase 1: Setup (30 min)
```bash
$ ./target/debug/grasp-audit --help
GRASP audit and compliance testing tool
Usage: grasp-audit <COMMAND>
Commands:
audit Run audit tests against a server
help Print this message or the help of the given subcommand(s)
Options:
-h, --help Print help
cd ../ngit-relay
docker-compose up -d
# Verify services running
```
---
### Phase 2: Build Tests (1 day)
- Create `grasp-audit/src/specs/grasp01_git.rs`
- Create `grasp-audit/src/git.rs` (test helpers)
- Add git2 dependency
- Implement all GRASP-01 Git tests
## Files Modified
### Phase 3: Validate (1 day)
- Run tests against ngit-relay
- Fix test bugs (not ngit-relay)
- Document reference behavior
- Iterate until all pass
1. **Cargo.toml**
- Updated `nostr-sdk = "0.35"` → `nostr-sdk = "0.43"`
2. **src/audit.rs**
- Changed `EventBuilder::new()` to not take tags parameter
- Changed `.to_event(keys)` → `.tags(tags).sign_with_keys(keys)`
3. **src/client.rs**
- Changed `Client::new(&keys)` → `Client::new(keys.clone())`
- Changed `is_connected()` to not await (no longer async)
- Changed `get_events_of()` → `fetch_events()`
- Removed `EventSource::relays()` usage
- Changed `Filter::custom_tag()` to use single values
- Changed `send_event(event)` → `send_event(&event)`
- Updated `subscribe()` to loop over filters
4. **src/specs/nip01_smoke.rs**
- Changed `EventBuilder::new()` to not take tags parameter
- Changed `.to_event(keys)` → `.tags(tags).sign_with_keys(keys)`
### Phase 4: Document (2 hours)
- Test suite documentation
- Reference behavior guide
- Prepare for ngit-grasp implementation
---
## Documentation Created
## Files to Reference
1. **NOSTR_SDK_0.43_UPGRADE.md** - Comprehensive upgrade guide
2. **COMPILATION_FIXES.md** - Original 0.35 fixes (now obsolete)
3. **SESSION_2025_11_04_SUMMARY.md** - This file
4. Updated **NEXT_SESSION_QUICKSTART.md** - Marked completed items
### For Implementation (Later)
- `docs/archive/2025-11-04-ngit-grasp-implementation-plan.md` - Full TDD plan
- `docs/archive/2025-11-04-git-http-backend-validation.md` - Crate details
- `docs/archive/2025-11-04-authorization-flow-diagram.txt` - Visual reference
### For Current Phase
- `work/current_status.md` - Test validation plan
- `docs/archive/2025-11-04-test-strategy-decision.md` - Why this approach
- `../ngit-relay/README.md` - Reference implementation docs
---
## Next Steps
## Metrics
### Ready for Integration Testing
### Time Investment
- Planning & Analysis: ~3 hours
- Next Phase (Test Validation): ~2 days
- Future Phase (Implementation): ~3 weeks
The code is now ready for integration testing. To proceed:
#### Option 1: Run Integration Tests
```bash
# Terminal 1: Start test relay
docker run -p 7000:7000 scsibug/nostr-rs-relay
# Terminal 2: Run tests
cd grasp-audit
nix develop --command cargo test --ignored
```
#### Option 2: Run CLI Audit
```bash
# Terminal 1: Start test relay
docker run -p 7000:7000 scsibug/nostr-rs-relay
# Terminal 2: Run audit
cd grasp-audit
nix develop --command cargo run -- audit --relay ws://localhost:7000 --mode ci --spec nip01-smoke
```
#### Option 3: Continue Development
- Implement GRASP-01 compliance tests
- Start building the ngit-grasp relay
- Add more test specifications
### Confidence Level
- Test-first approach: 95% confident this is right path
- Architecture decisions: 90% confident (validated)
- Timeline estimates: 80% confident (reasonable)
---
## Time Spent
## Lessons Learned
- **Problem Identification (0.35):** 5 minutes
- **Fixing 0.35 Errors:** 25 minutes
- **Discovering Version Gap:** 5 minutes
- **Upgrading to 0.43:** 30 minutes
- **Testing & Verification:** 10 minutes
- **Documentation:** 15 minutes
- **Total:** ~90 minutes
### 1. Test Validation is Critical
Having a reference implementation to test against is a huge advantage. Use it!
### 2. Upfront Planning Pays Off
The 3 hours of analysis and planning will save weeks of implementation time.
### 3. Documentation Structure Matters
Archiving session work keeps things clean and makes it easy to reference later.
### 4. Strategic Thinking > Speed
Taking 2 days to validate tests is smarter than rushing into implementation.
---
## Key Learnings
## Ready for Next Session
### nostr-sdk v0.43 Breaking Changes
**Status:** ✅ Ready to begin Phase 1
**First Command:** `cd ../ngit-relay && docker-compose up -d`
**Reference:** `work/current_status.md`
The main API changes from 0.35 → 0.43:
1. **EventBuilder Redesign** - Builder pattern for tags, explicit signing with `sign_with_keys()`
2. **Client Ownership** - Client takes ownership of signer (use `.clone()`)
3. **Sync Relay Status** - `is_connected()` is no longer async
4. **Query API Redesign** - `fetch_events()` instead of `get_events_of()`, single filter
5. **Events Type** - New collection type instead of `Vec<Event>`
6. **Simplified Filters** - `custom_tag()` takes single value
7. **Reference Passing** - `send_event()` takes reference for efficiency
8. **Removed EventSource** - Simpler API without source parameter
### Best Practices Applied
1. **Incremental Fixing** - Fixed one error at a time, testing after each fix
2. **Understanding Root Causes** - Identified API changes rather than just patching symptoms
3. **Proper Testing** - Verified unit tests after all fixes
4. **Documentation** - Created comprehensive documentation of all changes
**Goal:** By end of next session (2 days), have a validated GRASP-01 Git test suite that we can confidently use to implement ngit-grasp.
---
## Project Health
| Metric | Status | Notes |
|--------|--------|-------|
| Build | ✅ Success | Clean build, no warnings |
| Unit Tests | ✅ 12/12 Pass | All tests passing |
| Integration Tests | ⏳ Pending | Need relay to run |
| Documentation | ✅ Complete | All changes documented |
| Code Quality | ✅ Good | No clippy warnings |
---
## Commands for Next Session
### Quick Start
```bash
# Enter dev environment and build
cd grasp-audit
nix develop --command cargo build
# Run unit tests
cargo test --lib
# Build CLI
cargo build --bin grasp-audit
# Show help
./target/debug/grasp-audit --help
```
### Integration Testing
```bash
# In one terminal, start relay:
docker run -p 7000:7000 scsibug/nostr-rs-relay
# In another terminal, run tests:
cd grasp-audit
nix develop --command cargo test --ignored
# Or run CLI:
nix develop --command cargo run -- audit --relay ws://localhost:7000
```
---
## Success Metrics
✅ **All compilation errors fixed**
✅ **Clean build with no warnings**
✅ **All unit tests passing (12/12)**
✅ **CLI builds and shows help correctly**
✅ **Example builds successfully**
✅ **Comprehensive documentation created**
---
## Conclusion
The grasp-audit crate has been successfully upgraded to **nostr-sdk 0.43** (latest stable). All compilation errors have been resolved, the code builds cleanly with the modern API, and all unit tests pass. The upgrade brings:
- **Better APIs** - Cleaner, more intuitive interfaces
- **Performance improvements** - Reference passing, sync operations where appropriate
- **Future compatibility** - On latest stable, ready for new features
- **8 versions of bug fixes** - All improvements from 0.35 → 0.43
**Status:** Ready for integration testing with latest nostr-sdk.
*Session complete. All work archived. Ready to proceed with test validation phase.*
@@ -0,0 +1,290 @@
**ARCHIVED: 2025-11-04**
**Decision:** Test ngit-relay first (Option 1)
**Rationale:** Validate test suite before implementation (1-2 day investment)
---
# Strategic Recommendation: Test-First vs TDD Approach
**Date:** 2025-11-04
**Status:** ✅ ARCHIVED - Decision Made
**Context:** We have ngit-relay reference implementation available with Docker
---
## The Question
Should we:
1. **Test ngit-relay first** - Build grasp-audit against working reference, then apply to ngit-grasp
2. **TDD approach** - Build grasp-audit and ngit-grasp in parallel, test-driven
---
## Option 1: Test ngit-relay First (RECOMMENDED)
### Approach
```
Phase 1: Validate Test Suite (1-2 days)
├── Run ngit-relay Docker image
├── Build grasp-audit GRASP-01 tests
├── Test against ngit-relay
└── Fix grasp-audit until all tests pass
Phase 2: Apply to ngit-grasp (2-3 weeks)
├── Implement ngit-grasp features
├── Run same grasp-audit tests
├── Fix ngit-grasp until tests pass
└── Know tests are reliable (validated against reference)
```
### Pros
✅ **Validates test suite first** - Know tests work before implementing
✅ **Clear success criteria** - Tests pass against reference = tests are correct
✅ **Faster feedback** - Catch test bugs early, not during implementation
✅ **Reference behavior** - See how ngit-relay handles edge cases
✅ **Confidence** - When ngit-grasp passes, we know it's compliant
✅ **Documentation** - Tests become living spec examples
✅ **Lower risk** - Don't waste time implementing against broken tests
### Cons
❌ **Sequential** - Can't start ngit-grasp until tests validated (but only 1-2 days)
❌ **Docker dependency** - Need Docker to run ngit-relay (already have)
❌ **Different tech stack** - ngit-relay is Go, might have quirks
### Timeline
- **Phase 1:** 1-2 days (build + validate grasp-audit)
- **Phase 2:** 2-3 weeks (implement ngit-grasp)
- **Total:** ~3 weeks
### Risk Level
🟢 **LOW** - Tests validated before implementation
---
## Option 2: TDD Parallel Development
### Approach
```
Parallel Development
├── Write grasp-audit test
├── Run against ngit-grasp (fails - not implemented)
├── Implement ngit-grasp feature
├── Run test again (should pass)
└── Repeat for each feature
```
### Pros
✅ **True TDD** - Red → Green → Refactor cycle
✅ **Parallel work** - No waiting for test validation
✅ **Faster start** - Begin implementation immediately
✅ **Integrated learning** - Discover test issues during implementation
### Cons
❌ **Test uncertainty** - Don't know if test failures are test bugs or implementation bugs
❌ **Debugging complexity** - Two moving targets (tests + implementation)
❌ **Wasted effort** - Might implement wrong thing if test is wrong
❌ **No reference** - Can't verify expected behavior
❌ **Higher risk** - Could build to wrong spec
### Timeline
- **Parallel:** 2-3 weeks (but with more debugging)
- **Total:** ~3 weeks (but less confidence)
### Risk Level
🟡 **MEDIUM** - Could implement to wrong spec
---
## Comparison
| Aspect | Test ngit-relay First | TDD Parallel |
|--------|----------------------|--------------|
| **Confidence** | High (tests validated) | Medium (tests unproven) |
| **Speed to start** | 1-2 day delay | Immediate |
| **Debugging complexity** | Low (one target) | High (two targets) |
| **Risk of rework** | Low | Medium-High |
| **Learning** | See reference behavior | Discover as you go |
| **Total time** | ~3 weeks | ~3 weeks |
| **Quality** | Higher | Lower |
---
## Real-World Analogy
**Option 1 (Test First):**
- Like calibrating a measuring tape against a known standard before measuring
- Build the test rig, validate it, then use it
- Science lab approach: calibrate instruments first
**Option 2 (TDD Parallel):**
- Like building a measuring tape and the thing you're measuring at the same time
- Hope the tape is accurate while measuring
- Risky if tape is wrong
---
## Recommendation: TEST NGIT-RELAY FIRST
### Why?
1. **We already have the reference** - ngit-relay Docker image is available
2. **Low time cost** - Only 1-2 days to validate tests
3. **High confidence gain** - Know tests are correct before implementing
4. **Better debugging** - One variable at a time (test bugs, then implementation bugs)
5. **Living documentation** - Tests show how reference implementation behaves
6. **Risk mitigation** - Don't waste weeks implementing to broken tests
### Concrete Plan
#### Step 1: Setup ngit-relay (30 minutes)
```bash
# Pull and run ngit-relay
docker pull ngitrelay/ngit-relay:latest
docker run -d -p 8080:8080 -p 3000:3000 ngitrelay/ngit-relay
# Verify it's running
curl http://localhost:8080 # Nostr relay
curl http://localhost:3000 # Git HTTP backend
```
#### Step 2: Build grasp-audit GRASP-01 tests (1 day)
```bash
cd grasp-audit
# Add GRASP-01 Git tests
# - Repository creation on announcement
# - Clone via HTTP
# - Push with valid state (should succeed)
# - Push without state (should fail)
# - Push with wrong state (should fail)
# - Multi-maintainer validation
# - refs/nostr/* support
nix develop -c cargo test
```
#### Step 3: Test against ngit-relay (1 day)
```bash
# Run compliance tests
cd grasp-audit
nix develop -c cargo run -- --url ws://localhost:8080 --git-url http://localhost:3000
# Fix test bugs until all pass
# Document any ngit-relay quirks
# Create test fixtures
```
#### Step 4: Apply to ngit-grasp (2-3 weeks)
```bash
# Now implement ngit-grasp with confidence
cd ../
# Implement features
# Run grasp-audit tests
# Fix ngit-grasp until tests pass
```
---
## What We Learn from ngit-relay
By testing against the reference, we learn:
1. **Expected behavior** - How should authorization work exactly?
2. **Error messages** - What does a proper rejection look like?
3. **Edge cases** - How does it handle:
- Empty repositories
- Multiple refs in one push
- Tag vs branch pushes
- refs/nostr/* special handling
- Concurrent pushes
- Invalid state events
- Circular maintainer references
4. **Protocol details** - Git Smart HTTP quirks
5. **Performance** - What's reasonable for validation time?
---
## Migration Path
### Phase 1: Validate Tests (Days 1-2)
- [ ] Setup ngit-relay Docker
- [ ] Build grasp-audit Git tests
- [ ] Test against ngit-relay
- [ ] Fix test bugs
- [ ] Document reference behavior
### Phase 2: Implement ngit-grasp (Weeks 1-3)
- [ ] Follow current_status.md plan
- [ ] Run grasp-audit after each phase
- [ ] Fix implementation bugs
- [ ] Achieve parity with ngit-relay
### Phase 3: Exceed Reference (Week 4+)
- [ ] Add Rust-specific optimizations
- [ ] Better error messages
- [ ] Inline authorization benefits
- [ ] Performance improvements
---
## Decision Criteria
Choose **Test ngit-relay First** if:
- ✅ We value confidence over speed to start
- ✅ We want to minimize rework risk
- ✅ We can spare 1-2 days upfront
- ✅ We want tests as living documentation
Choose **TDD Parallel** if:
- ❌ We can't run ngit-relay (Docker issues, etc.)
- ❌ We need to start implementation TODAY
- ❌ We're comfortable with higher debugging complexity
- ❌ We're okay with potential rework
---
## My Recommendation
**🎯 Test ngit-relay first**
**Reasoning:**
1. Only 1-2 days upfront investment
2. Massively reduces risk of wasted effort
3. Provides living documentation
4. Gives confidence in test suite
5. We already have Docker and ngit-relay available
6. Total timeline is same (~3 weeks) but with higher quality
**The 1-2 day investment in test validation will save us days or weeks of debugging "is it the test or the implementation?"**
---
## Next Steps
If you agree with this recommendation:
1. **Today:** Setup ngit-relay Docker
2. **Tomorrow:** Build GRASP-01 Git tests in grasp-audit
3. **Day 3:** Validate tests against ngit-relay
4. **Week 2-4:** Implement ngit-grasp with confidence
If you prefer TDD parallel:
1. **Today:** Start implementing ngit-grasp Git backend
2. **Ongoing:** Write tests alongside implementation
3. **Risk:** Accept higher debugging complexity
---
## Questions?
- Is Docker available for ngit-relay?
- Any blockers to testing against reference?
- Time constraints that require immediate implementation?
- Other considerations I'm missing?
---
**Recommendation:** 🎯 **Test ngit-relay first** (1-2 day investment, weeks of confidence)
**Confidence Level:** 95% - This is the right approach
@@ -0,0 +1,577 @@
//! GRASP-01 Nostr Relay Tests
//!
//! Tests for GRASP-01 Nostr relay requirements (lines 1-14 of ../grasp/01.md)
//!
//! These tests validate that a GRASP-01 compliant relay:
//! - Accepts valid NIP-34 repository announcements and state announcements
//! - Rejects announcements that don't list the service
//! - Accepts related events (issues, patches, PRs)
//! - Serves proper NIP-11 relay information document
use crate::{AuditClient, AuditResult, TestResult};
use nostr_sdk::prelude::*;
pub struct Grasp01NostrRelayTests;
impl Grasp01NostrRelayTests {
/// Run all GRASP-01 Nostr relay tests
pub async fn run_all(client: &AuditClient) -> AuditResult {
let mut results = AuditResult::new("GRASP-01 Nostr Relay Tests");
// Repository announcement acceptance tests
results.add(Self::test_accept_valid_repo_announcement(client).await);
results.add(Self::test_reject_repo_announcement_missing_clone_tag(client).await);
results.add(Self::test_reject_repo_announcement_missing_relays_tag(client).await);
// Repository state announcement tests
results.add(Self::test_accept_valid_repo_state_announcement(client).await);
results.add(Self::test_accept_state_announcement_multiple_refs(client).await);
results.add(Self::test_accept_state_announcement_no_refs(client).await);
// Related event acceptance tests
results.add(Self::test_accept_event_tagging_repo_announcement(client).await);
results.add(Self::test_accept_event_tagged_by_repo(client).await);
results.add(Self::test_accept_patch_for_repo(client).await);
results.add(Self::test_accept_pull_request_for_repo(client).await);
results.add(Self::test_accept_issue_for_repo(client).await);
results.add(Self::test_accept_reply_to_issue(client).await);
// NIP-11 relay information tests
results.add(Self::test_nip11_document_exists(client).await);
results.add(Self::test_nip11_supported_grasps_field(client).await);
results.add(Self::test_nip11_repo_acceptance_criteria_field(client).await);
results.add(Self::test_nip11_curation_field(client).await);
// Policy tests (document behavior)
results.add(Self::test_custom_rejection_allowed(client).await);
results.add(Self::test_spam_prevention_allowed(client).await);
results
}
// =========================================================================
// Repository Announcement Acceptance Tests
// =========================================================================
/// Test: Accept valid repository announcements
///
/// Spec: Lines 3-5 of ../grasp/01.md
/// Requirement: MUST accept repo announcements listing service in clone & relays tags
async fn test_accept_valid_repo_announcement(client: &AuditClient) -> TestResult {
TestResult::new(
"accept_valid_repo_announcement",
"GRASP-01:nostr-relay:3-5",
"Accept valid repository announcements with service in clone and relays tags",
)
.run(|| async {
// TODO: Implementation
// 1. Get service URL from client config
// 2. Create kind 30617 event with:
// - d tag: "test-repo-{timestamp}"
// - name tag: "Test Repository"
// - description tag: "Test repository for GRASP-01 compliance"
// - clone tag: "{service_url}/{npub}/test-repo.git"
// - relays tag: "{service_ws_url}"
// 3. Send event to relay
// 4. Verify OK response (not rejected)
// 5. Query back with filter on kind 30617, author, d tag
// 6. Verify event is stored and matches what we sent
Err("Not implemented yet".to_string())
})
.await
}
/// Test: Reject repo announcements not listing service in clone tag
///
/// Spec: Line 5 of ../grasp/01.md
/// Requirement: MUST reject announcements not listing service (unless GRASP-05)
async fn test_reject_repo_announcement_missing_clone_tag(client: &AuditClient) -> TestResult {
TestResult::new(
"reject_repo_announcement_missing_clone_tag",
"GRASP-01:nostr-relay:5",
"Reject repository announcements without service in clone tag",
)
.run(|| async {
// TODO: Implementation
// 1. Create kind 30617 event with:
// - d tag: "test-repo-no-clone"
// - clone tag: "https://github.com/user/repo.git" (NOT this service)
// - relays tag: "{service_url}" (correct)
// 2. Send event to relay
// 3. Verify rejection (error in OK response or event not stored)
// 4. Query to confirm event is NOT in relay
// 5. Document expected error message if available
Err("Not implemented yet".to_string())
})
.await
}
/// Test: Reject repo announcements not listing service in relays tag
///
/// Spec: Line 5 of ../grasp/01.md
/// Requirement: MUST reject announcements not listing service in relays
async fn test_reject_repo_announcement_missing_relays_tag(client: &AuditClient) -> TestResult {
TestResult::new(
"reject_repo_announcement_missing_relays_tag",
"GRASP-01:nostr-relay:5",
"Reject repository announcements without service in relays tag",
)
.run(|| async {
// TODO: Implementation
// 1. Create kind 30617 event with:
// - d tag: "test-repo-no-relays"
// - clone tag: "{service_url}/{npub}/test-repo.git" (correct)
// - relays tag: "wss://relay.damus.io" (NOT this service)
// 2. Send event to relay
// 3. Verify rejection
// 4. Query to confirm event is NOT in relay
Err("Not implemented yet".to_string())
})
.await
}
// =========================================================================
// Repository State Announcement Tests
// =========================================================================
/// Test: Accept valid repository state announcements
///
/// Spec: Line 3 of ../grasp/01.md
/// Requirement: MUST accept repo state announcements
async fn test_accept_valid_repo_state_announcement(client: &AuditClient) -> TestResult {
TestResult::new(
"accept_valid_repo_state_announcement",
"GRASP-01:nostr-relay:3",
"Accept valid repository state announcements",
)
.run(|| async {
// TODO: Implementation
// 1. First send valid kind 30617 (repo announcement) - prerequisite
// 2. Create kind 30618 event with:
// - d tag: same as repo announcement
// - refs/heads/main tag: "{commit-sha}"
// - HEAD tag: "ref: refs/heads/main"
// 3. Send state announcement
// 4. Verify acceptance
// 5. Query back to confirm stored
// 6. Verify all tags are preserved
Err("Not implemented yet".to_string())
})
.await
}
/// Test: Accept state announcement with multiple refs
///
/// Spec: Line 3 of ../grasp/01.md
/// Requirement: MUST accept state announcements with multiple refs
async fn test_accept_state_announcement_multiple_refs(client: &AuditClient) -> TestResult {
TestResult::new(
"accept_state_announcement_multiple_refs",
"GRASP-01:nostr-relay:3",
"Accept state announcements with multiple branch and tag refs",
)
.run(|| async {
// TODO: Implementation
// 1. Send valid kind 30617 repo announcement
// 2. Create kind 30618 with multiple refs:
// - refs/heads/main: "{commit-sha-1}"
// - refs/heads/develop: "{commit-sha-2}"
// - refs/tags/v1.0.0: "{commit-sha-3}"
// - refs/tags/v2.0.0: "{commit-sha-4}"
// - HEAD: "ref: refs/heads/main"
// 3. Send and verify acceptance
// 4. Query back and verify all refs are stored
Err("Not implemented yet".to_string())
})
.await
}
/// Test: Accept state announcement with no refs (stop tracking)
///
/// Spec: NIP-34 repository state announcements
/// Requirement: Support stopping state tracking by sending event with no refs
async fn test_accept_state_announcement_no_refs(client: &AuditClient) -> TestResult {
TestResult::new(
"accept_state_announcement_no_refs",
"GRASP-01:nostr-relay:3",
"Accept state announcements with no refs (stop tracking)",
)
.run(|| async {
// TODO: Implementation
// 1. Send valid kind 30617 repo announcement
// 2. Send kind 30618 with refs (establish state)
// 3. Send kind 30618 with ONLY d tag (no refs)
// 4. Verify acceptance (allows author to stop tracking)
// 5. Query to confirm latest state has no refs
Err("Not implemented yet".to_string())
})
.await
}
// =========================================================================
// Related Event Acceptance Tests
// =========================================================================
/// Test: Accept events tagging accepted repo announcements
///
/// Spec: Lines 7-9 of ../grasp/01.md
/// Requirement: MUST accept events that tag accepted repo announcements
async fn test_accept_event_tagging_repo_announcement(client: &AuditClient) -> TestResult {
TestResult::new(
"accept_event_tagging_repo_announcement",
"GRASP-01:nostr-relay:7-9",
"Accept events that tag accepted repository announcements",
)
.run(|| async {
// TODO: Implementation
// 1. Create and send kind 30617 repo announcement
// 2. Create kind 1621 (issue) event with:
// - a tag: "30617:{pubkey}:{d-tag}"
// - p tag: repo owner pubkey
// - subject tag: "Test Issue"
// - content: "This is a test issue"
// 3. Send issue event
// 4. Verify acceptance
// 5. Query to confirm issue is stored
Err("Not implemented yet".to_string())
})
.await
}
/// Test: Accept events tagged by repo announcements
///
/// Spec: Lines 7-9 of ../grasp/01.md
/// Requirement: MUST accept events tagged by accepted announcements
async fn test_accept_event_tagged_by_repo(client: &AuditClient) -> TestResult {
TestResult::new(
"accept_event_tagged_by_repo",
"GRASP-01:nostr-relay:7-9",
"Accept events that are tagged by accepted repository announcements",
)
.run(|| async {
// TODO: Implementation
// 1. Create kind 1 note event (regular note)
// 2. Send the note
// 3. Create kind 30617 repo announcement that tags the note
// - Include e tag pointing to note event ID
// 4. Send repo announcement
// 5. Verify both events are stored
// 6. This tests that related events are retained
Err("Not implemented yet".to_string())
})
.await
}
/// Test: Accept patches (kind 1617) for accepted repos
///
/// Spec: Lines 8-9 of ../grasp/01.md
/// Requirement: MUST accept patches for accepted repos
async fn test_accept_patch_for_repo(client: &AuditClient) -> TestResult {
TestResult::new(
"accept_patch_for_repo",
"GRASP-01:nostr-relay:8-9",
"Accept patch events (kind 1617) for accepted repositories",
)
.run(|| async {
// TODO: Implementation
// 1. Create and send kind 30617 repo announcement
// 2. Create kind 1617 patch event with:
// - a tag: "30617:{pubkey}:{d-tag}"
// - p tag: repo owner
// - r tag: earliest-unique-commit-id
// - t tag: "root" (first patch in series)
// - content: actual git format-patch output
// 3. Send patch event
// 4. Verify acceptance
// 5. Query to confirm patch is stored
Err("Not implemented yet".to_string())
})
.await
}
/// Test: Accept pull requests (kind 1618) for accepted repos
///
/// Spec: Lines 8-9 of ../grasp/01.md
/// Requirement: MUST accept PRs for accepted repos
async fn test_accept_pull_request_for_repo(client: &AuditClient) -> TestResult {
TestResult::new(
"accept_pull_request_for_repo",
"GRASP-01:nostr-relay:8-9",
"Accept pull request events (kind 1618) for accepted repositories",
)
.run(|| async {
// TODO: Implementation
// 1. Create and send kind 30617 repo announcement
// 2. Create kind 1618 PR event with:
// - a tag: "30617:{pubkey}:{d-tag}"
// - p tag: repo owner
// - r tag: earliest-unique-commit-id
// - subject tag: "Add feature X"
// - c tag: commit SHA of PR tip
// - clone tag: URL where commit can be fetched
// - content: PR description
// 3. Send PR event
// 4. Verify acceptance
// 5. Query to confirm PR is stored
Err("Not implemented yet".to_string())
})
.await
}
/// Test: Accept issues (kind 1621) for accepted repos
///
/// Spec: Lines 8-9 of ../grasp/01.md
/// Requirement: MUST accept issues for accepted repos
async fn test_accept_issue_for_repo(client: &AuditClient) -> TestResult {
TestResult::new(
"accept_issue_for_repo",
"GRASP-01:nostr-relay:8-9",
"Accept issue events (kind 1621) for accepted repositories",
)
.run(|| async {
// TODO: Implementation
// 1. Create and send kind 30617 repo announcement
// 2. Create kind 1621 issue event with:
// - a tag: "30617:{pubkey}:{d-tag}"
// - p tag: repo owner
// - subject tag: "Bug: Something is broken"
// - t tag: "bug" (label)
// - content: issue description
// 3. Send issue event
// 4. Verify acceptance
// 5. Query to confirm issue is stored
Err("Not implemented yet".to_string())
})
.await
}
/// Test: Accept replies to accepted patches/PRs/issues
///
/// Spec: Lines 8-9 of ../grasp/01.md
/// Requirement: MUST accept replies to accepted events
async fn test_accept_reply_to_issue(client: &AuditClient) -> TestResult {
TestResult::new(
"accept_reply_to_issue",
"GRASP-01:nostr-relay:8-9",
"Accept reply events to accepted issues/patches/PRs",
)
.run(|| async {
// TODO: Implementation
// 1. Create and send kind 30617 repo announcement
// 2. Create and send kind 1621 issue
// 3. Create NIP-22 comment (kind 1111) replying to issue:
// - E tag: issue event ID
// - P tag: issue author
// - content: reply text
// 4. Send reply event
// 5. Verify acceptance
// 6. Query to confirm reply is stored
Err("Not implemented yet".to_string())
})
.await
}
// =========================================================================
// NIP-11 Relay Information Tests
// =========================================================================
/// Test: Serve NIP-11 document
///
/// Spec: Line 11 of ../grasp/01.md
/// Requirement: MUST serve NIP-11 document
async fn test_nip11_document_exists(client: &AuditClient) -> TestResult {
TestResult::new(
"nip11_document_exists",
"GRASP-01:nostr-relay:11",
"Serve NIP-11 relay information document",
)
.run(|| async {
// TODO: Implementation
// 1. Extract HTTP(S) URL from client's WebSocket URL
// - ws://localhost:8081 -> http://localhost:8081
// - wss://relay.example.com -> https://relay.example.com
// 2. HTTP GET to base URL with header:
// - Accept: application/nostr+json
// 3. Verify 200 OK response
// 4. Verify response is valid JSON
// 5. Parse as NIP-11 document
// 6. Verify has required fields (name, description, etc.)
Err("Not implemented yet".to_string())
})
.await
}
/// Test: NIP-11 includes supported_grasps field
///
/// Spec: Line 12 of ../grasp/01.md
/// Requirement: MUST list supported GRASPs as string array
async fn test_nip11_supported_grasps_field(client: &AuditClient) -> TestResult {
TestResult::new(
"nip11_supported_grasps_field",
"GRASP-01:nostr-relay:12",
"NIP-11 document includes supported_grasps field with GRASP-01",
)
.run(|| async {
// TODO: Implementation
// 1. Fetch NIP-11 document (same as above)
// 2. Verify `supported_grasps` field exists
// 3. Verify it's a JSON array of strings
// 4. Verify array includes "GRASP-01"
// 5. Verify format: each entry matches pattern "GRASP-\d{2}"
// 6. Document other GRASPs found (for info)
Err("Not implemented yet".to_string())
})
.await
}
/// Test: NIP-11 includes repo_acceptance_criteria field
///
/// Spec: Line 13 of ../grasp/01.md
/// Requirement: MUST list repository acceptance criteria
async fn test_nip11_repo_acceptance_criteria_field(client: &AuditClient) -> TestResult {
TestResult::new(
"nip11_repo_acceptance_criteria_field",
"GRASP-01:nostr-relay:13",
"NIP-11 document includes repo_acceptance_criteria field",
)
.run(|| async {
// TODO: Implementation
// 1. Fetch NIP-11 document
// 2. Verify `repo_acceptance_criteria` field exists
// 3. Verify it's a string (human-readable)
// 4. Verify non-empty
// 5. Document the criteria (for info)
// Examples: "Must list this relay in clone and relays tags"
// "Pre-payment required via Lightning invoice"
Err("Not implemented yet".to_string())
})
.await
}
/// Test: NIP-11 curation field handling
///
/// Spec: Line 14 of ../grasp/01.md
/// Requirement: MUST include curation if curated, omit otherwise
async fn test_nip11_curation_field(client: &AuditClient) -> TestResult {
TestResult::new(
"nip11_curation_field",
"GRASP-01:nostr-relay:14",
"NIP-11 curation field present if curated, absent otherwise",
)
.run(|| async {
// TODO: Implementation
// 1. Fetch NIP-11 document
// 2. Check if `curation` field exists
// 3. If present:
// - Verify it's a non-empty string
// - Document the curation policy
// 4. If absent:
// - Document that no curation beyond SPAM prevention
// 5. Both cases are valid per spec
Err("Not implemented yet".to_string())
})
.await
}
// =========================================================================
// Policy Tests (Document Allowed Behavior)
// =========================================================================
/// Test: Custom rejection criteria allowed
///
/// Spec: Line 6 of ../grasp/01.md
/// Requirement: MAY reject based on custom criteria (document behavior)
async fn test_custom_rejection_allowed(client: &AuditClient) -> TestResult {
TestResult::new(
"custom_rejection_allowed",
"GRASP-01:nostr-relay:6",
"Document that custom rejection criteria are allowed",
)
.run(|| async {
// TODO: Implementation
// This is a policy test, not a functional test
//
// The spec says relay MAY reject based on:
// - Pre-payment
// - Quotas
// - WoT (Web of Trust)
// - Whitelist
// - SPAM prevention
// - etc.
//
// This test should:
// 1. Document that such rejections are allowed
// 2. Check NIP-11 repo_acceptance_criteria for policy
// 3. Optionally test if relay enforces any criteria
// 4. Mark as PASS (this is permissive, not mandatory)
Ok(()) // This is always allowed
})
.await
}
/// Test: SPAM prevention allowed
///
/// Spec: Line 10 of ../grasp/01.md
/// Requirement: MAY reject/delete for SPAM prevention
async fn test_spam_prevention_allowed(client: &AuditClient) -> TestResult {
TestResult::new(
"spam_prevention_allowed",
"GRASP-01:nostr-relay:10",
"Document that SPAM prevention is allowed",
)
.run(|| async {
// TODO: Implementation
// Similar to above - this is permissive
//
// The spec says relay MAY reject or delete events for:
// - Generic SPAM prevention
// - Curation (WoT, whitelist, user bans, banned topics)
//
// This test should:
// 1. Document that SPAM prevention is allowed
// 2. Check NIP-11 curation field for policy
// 3. Mark as PASS (this is implementation-specific)
Ok(()) // This is always allowed
})
.await
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::AuditConfig;
#[tokio::test]
#[ignore] // Requires running relay
async fn test_grasp01_nostr_relay_against_relay() {
let config = AuditConfig::ci();
let client = AuditClient::new("ws://localhost:8081", config)
.await
.expect("Failed to connect to relay");
let results = Grasp01NostrRelayTests::run_all(&client).await;
results.print_report();
// Don't assert all passed yet - tests not implemented
// assert!(results.all_passed(), "Some GRASP-01 Nostr relay tests failed");
}
}