From 3c50e6afe3fdb78763df0f19edcfa69f316d820f Mon Sep 17 00:00:00 2001 From: Gravirei Date: Thu, 2 Jul 2026 08:38:40 +0600 Subject: [PATCH 01/11] fix(node): carry full owner DID on ref-update wire event (#144) --- crates/gitlawb-node/src/api/peers.rs | 5 +++++ crates/gitlawb-node/src/api/repos.rs | 1 + crates/gitlawb-node/src/db/mod.rs | 13 ++++++++++--- crates/gitlawb-node/src/p2p/mod.rs | 6 ++++++ 4 files changed, 22 insertions(+), 3 deletions(-) diff --git a/crates/gitlawb-node/src/api/peers.rs b/crates/gitlawb-node/src/api/peers.rs index a125c313..5c535f0c 100644 --- a/crates/gitlawb-node/src/api/peers.rs +++ b/crates/gitlawb-node/src/api/peers.rs @@ -347,6 +347,10 @@ pub struct NotifyRequest { pub timestamp: Option, #[serde(default)] pub cert_id: Option, + /// Full owner DID — added in #144 for DID-aware feed gating. + /// Optional for backward compat with older senders. + #[serde(default)] + pub owner_did: Option, } pub async fn notify_sync( @@ -391,6 +395,7 @@ pub async fn notify_sync( node_did: req.node_did.clone(), pusher_did: req.pusher_did.clone().unwrap_or_default(), repo: req.repo.clone(), + owner_did: req.owner_did.clone(), ref_name: req.ref_name.clone(), old_sha: req.old_sha.clone().unwrap_or_default(), new_sha: req.new_sha.clone(), diff --git a/crates/gitlawb-node/src/api/repos.rs b/crates/gitlawb-node/src/api/repos.rs index b0cca430..61d90d05 100644 --- a/crates/gitlawb-node/src/api/repos.rs +++ b/crates/gitlawb-node/src/api/repos.rs @@ -1261,6 +1261,7 @@ pub async fn git_receive_pack( node_did: node_did_str.clone(), pusher_did: pusher_did_clone.clone(), repo: repo_slug.clone(), + owner_did: Some(record.owner_did.clone()), ref_name: ref_name.clone(), old_sha: old_sha.clone(), new_sha: new_sha.clone(), diff --git a/crates/gitlawb-node/src/db/mod.rs b/crates/gitlawb-node/src/db/mod.rs index 2095bf1f..5c919689 100644 --- a/crates/gitlawb-node/src/db/mod.rs +++ b/crates/gitlawb-node/src/db/mod.rs @@ -175,6 +175,9 @@ pub struct ReceivedRefUpdate { pub cert_id: Option, pub received_at: String, pub from_peer: String, + /// Full owner DID — populated by new peers; None for events from older + /// peers that predate the wire-format change (#144). + pub owner_did: Option, } #[derive(Debug, Clone, Serialize, Deserialize)] @@ -566,8 +569,10 @@ const MIGRATIONS: &[Migration] = &[ received_at TEXT NOT NULL, from_peer TEXT NOT NULL )"#, + "ALTER TABLE received_ref_updates ADD COLUMN IF NOT EXISTS owner_did TEXT", "CREATE INDEX IF NOT EXISTS idx_ref_updates_repo ON received_ref_updates(repo)", "CREATE INDEX IF NOT EXISTS idx_ref_updates_ts ON received_ref_updates(timestamp DESC)", + "CREATE INDEX IF NOT EXISTS idx_ref_updates_owner ON received_ref_updates(owner_did)", r#"CREATE TABLE IF NOT EXISTS pull_requests ( id TEXT NOT NULL PRIMARY KEY, repo_id TEXT NOT NULL, @@ -2233,8 +2238,8 @@ impl Db { sqlx::query( "INSERT INTO received_ref_updates (id, node_did, pusher_did, repo, ref_name, old_sha, new_sha, timestamp, - cert_id, received_at, from_peer) - VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11) + cert_id, received_at, from_peer, owner_did) + VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12) ON CONFLICT(id) DO NOTHING", ) .bind(&update.id) @@ -2248,6 +2253,7 @@ impl Db { .bind(&update.cert_id) .bind(&update.received_at) .bind(&update.from_peer) + .bind(&update.owner_did) .execute(&self.pool) .await?; Ok(()) @@ -2277,7 +2283,7 @@ impl Db { after: Option<(&str, &str)>, ) -> Result> { const COLS: &str = "id, node_did, pusher_did, repo, ref_name, old_sha, new_sha, \ - timestamp, cert_id, received_at, from_peer"; + timestamp, cert_id, received_at, from_peer, owner_did"; // Positional params in bind order: repo?, after_ts?, after_id?, limit. let mut conds: Vec = Vec::new(); @@ -2615,6 +2621,7 @@ fn row_to_ref_update(r: sqlx::postgres::PgRow) -> ReceivedRefUpdate { cert_id: r.get("cert_id"), received_at: r.get("received_at"), from_peer: r.get("from_peer"), + owner_did: r.get("owner_did"), } } diff --git a/crates/gitlawb-node/src/p2p/mod.rs b/crates/gitlawb-node/src/p2p/mod.rs index 5a6992b1..1adbb34a 100644 --- a/crates/gitlawb-node/src/p2p/mod.rs +++ b/crates/gitlawb-node/src/p2p/mod.rs @@ -39,6 +39,11 @@ pub struct RefUpdateEvent { pub pusher_did: String, /// Repository identifier (owner/name) pub repo: String, + /// Full owner DID — added in #144 so the feed gate can distinguish + /// different DID methods that share the same trailing segment. + /// Optional for backward compat with older peers that don't include it. + #[serde(default)] + pub owner_did: Option, /// Git ref that changed (e.g., "refs/heads/main") pub ref_name: String, /// SHA before the push (all-zeros for new ref) @@ -307,6 +312,7 @@ pub async fn start( node_did: event.node_did.clone(), pusher_did: event.pusher_did.clone(), repo: event.repo.clone(), + owner_did: event.owner_did.clone(), ref_name: event.ref_name.clone(), old_sha: event.old_sha.clone(), new_sha: event.new_sha.clone(), From 253a3e2ebe6a0c945b5b916cd45bd9e7346e5865 Mon Sep 17 00:00:00 2001 From: Gravirei Date: Thu, 2 Jul 2026 09:10:39 +0600 Subject: [PATCH 02/11] test(node): add ref-update owner_did round-trip, DB, and API tests (#144) --- crates/gitlawb-node/src/api/repos.rs | 14 +++ crates/gitlawb-node/src/db/mod.rs | 159 ++++++++++++++++++++++++ crates/gitlawb-node/src/p2p/mod.rs | 64 ++++++++++ crates/gitlawb-node/src/test_support.rs | 83 +++++++++++++ 4 files changed, 320 insertions(+) diff --git a/crates/gitlawb-node/src/api/repos.rs b/crates/gitlawb-node/src/api/repos.rs index 61d90d05..98eb6dbd 100644 --- a/crates/gitlawb-node/src/api/repos.rs +++ b/crates/gitlawb-node/src/api/repos.rs @@ -772,6 +772,7 @@ async fn notify_peer_of_ref( new_sha: &str, node_did: &str, pusher_did: &str, + owner_did: &str, ) { let body = serde_json::json!({ "repo": repo_slug, @@ -781,6 +782,7 @@ async fn notify_peer_of_ref( "pusher_did": pusher_did, "old_sha": old_sha, "timestamp": chrono::Utc::now().to_rfc3339(), + "owner_did": owner_did, }); let body_bytes = match serde_json::to_vec(&body) { Ok(bytes) => bytes, @@ -828,6 +830,7 @@ async fn notify_peer_of_refs( ref_updates: &[(String, String, String)], node_did: &str, pusher_did: &str, + owner_did: &str, ) { for (ref_name, old_sha, new_sha) in ref_updates { notify_peer_of_ref( @@ -841,6 +844,7 @@ async fn notify_peer_of_refs( new_sha, node_did, pusher_did, + owner_did, ) .await; } @@ -1363,6 +1367,7 @@ pub async fn git_receive_pack( &ref_updates_clone, &node_did_str, &pusher_did_clone, + &record.owner_did, ) .await; } @@ -2403,6 +2408,9 @@ mod tests { mockito::Matcher::PartialJsonString(format!(r#"{{"ref_name":"{ref_a}"}}"#)), mockito::Matcher::PartialJsonString(format!(r#"{{"old_sha":"{old_a}"}}"#)), mockito::Matcher::PartialJsonString(format!(r#"{{"new_sha":"{new_a}"}}"#)), + mockito::Matcher::PartialJsonString( + r#"{"owner_did":"did:key:zOwner"}"#.to_string(), + ), ])) .with_status(200) .expect(1) @@ -2414,6 +2422,9 @@ mod tests { mockito::Matcher::PartialJsonString(format!(r#"{{"ref_name":"{ref_b}"}}"#)), mockito::Matcher::PartialJsonString(format!(r#"{{"old_sha":"{old_b}"}}"#)), mockito::Matcher::PartialJsonString(format!(r#"{{"new_sha":"{new_b}"}}"#)), + mockito::Matcher::PartialJsonString( + r#"{"owner_did":"did:key:zOwner"}"#.to_string(), + ), ])) .with_status(200) .expect(1) @@ -2435,6 +2446,7 @@ mod tests { &ref_updates, "did:key:zNode", "did:key:zPusher", + "did:key:zOwner", ) .await; @@ -2457,6 +2469,7 @@ mod tests { .match_body(mockito::Matcher::AllOf(vec![ mockito::Matcher::PartialJsonString(format!(r#"{{"old_sha":"{zero}"}}"#)), mockito::Matcher::PartialJsonString(format!(r#"{{"new_sha":"{new_sha}"}}"#)), + mockito::Matcher::PartialJsonString(r#"{"owner_did":"did:key:zOwner"}"#.to_string()), ])) .with_status(200) .expect(1) @@ -2479,6 +2492,7 @@ mod tests { &ref_updates, "did:key:zNode", "did:key:zPusher", + "did:key:zOwner", ) .await; diff --git a/crates/gitlawb-node/src/db/mod.rs b/crates/gitlawb-node/src/db/mod.rs index 5c919689..f5f1516c 100644 --- a/crates/gitlawb-node/src/db/mod.rs +++ b/crates/gitlawb-node/src/db/mod.rs @@ -4647,3 +4647,162 @@ mod ref_update_keyset_same_timestamp_tests { ); } } + +#[cfg(test)] +mod ref_update_db_tests { + use super::{Db, ReceivedRefUpdate}; + use sqlx::PgPool; + + async fn db(pool: PgPool) -> Db { + let db = Db::for_testing(pool); + db.run_migrations().await.unwrap(); + db + } + + fn update( + id: &str, + repo: &str, + owner_did: Option<&str>, + ref_name: &str, + sha: &str, + ) -> ReceivedRefUpdate { + ReceivedRefUpdate { + id: id.to_string(), + node_did: "did:key:zNode".into(), + pusher_did: "did:key:zPusher".into(), + repo: repo.to_string(), + owner_did: owner_did.map(|s| s.to_string()), + ref_name: ref_name.to_string(), + old_sha: "0000000000000000000000000000000000000000".into(), + new_sha: sha.to_string(), + timestamp: "2026-07-02T12:00:00Z".into(), + cert_id: None, + received_at: "2026-07-02T12:00:01Z".into(), + from_peer: "12D3KooWTest".into(), + } + } + + #[sqlx::test] + async fn insert_and_list_with_owner_did(pool: PgPool) { + let db = db(pool).await; + db.insert_ref_update(&update( + "u1", + "zOwner/myrepo", + Some("did:key:zOwner"), + "refs/heads/main", + "aaaa", + )) + .await + .unwrap(); + + let all = db.list_ref_updates(100).await.unwrap(); + assert_eq!(all.len(), 1); + assert_eq!(all[0].owner_did.as_deref(), Some("did:key:zOwner")); + assert_eq!(all[0].repo, "zOwner/myrepo"); + } + + #[sqlx::test] + async fn insert_and_list_without_owner_did(pool: PgPool) { + let db = db(pool).await; + db.insert_ref_update(&update( + "u2", + "zOwner/myrepo", + None, + "refs/heads/main", + "bbbb", + )) + .await + .unwrap(); + + let all = db.list_ref_updates(100).await.unwrap(); + assert_eq!(all.len(), 1); + assert_eq!(all[0].owner_did, None); + } + + #[sqlx::test] + async fn list_repo_ref_updates_filters_by_repo(pool: PgPool) { + let db = db(pool).await; + db.insert_ref_update(&update( + "u3", + "alice/repo1", + Some("did:key:zAlice"), + "refs/heads/main", + "cccc", + )) + .await + .unwrap(); + db.insert_ref_update(&update( + "u4", + "bob/repo2", + Some("did:key:zBob"), + "refs/heads/feat", + "dddd", + )) + .await + .unwrap(); + + let alice_events = db.list_repo_ref_updates("alice/repo1", 100).await.unwrap(); + assert_eq!(alice_events.len(), 1); + assert_eq!(alice_events[0].id, "u3"); + assert_eq!(alice_events[0].owner_did.as_deref(), Some("did:key:zAlice")); + + let bob_events = db.list_repo_ref_updates("bob/repo2", 100).await.unwrap(); + assert_eq!(bob_events.len(), 1); + assert_eq!(bob_events[0].id, "u4"); + assert_eq!(bob_events[0].owner_did.as_deref(), Some("did:key:zBob")); + + let empty = db.list_repo_ref_updates("other/repo", 100).await.unwrap(); + assert!(empty.is_empty()); + } + + #[sqlx::test] + async fn list_ref_updates_filtered_by_repo(pool: PgPool) { + let db = db(pool).await; + db.insert_ref_update(&update( + "u5", + "ownerA/proj", + Some("did:key:zA"), + "refs/heads/main", + "eeee", + )) + .await + .unwrap(); + db.insert_ref_update(&update( + "u6", + "ownerB/proj", + Some("did:web:host:zB"), + "refs/heads/main", + "ffff", + )) + .await + .unwrap(); + + let filtered = db + .list_ref_updates_filtered(Some("ownerA/proj"), 100) + .await + .unwrap(); + assert_eq!(filtered.len(), 1); + assert_eq!(filtered[0].id, "u5"); + + let all = db.list_ref_updates_filtered(None, 100).await.unwrap(); + assert_eq!(all.len(), 2); + } + + #[sqlx::test] + async fn insert_update_idempotent_on_conflict(pool: PgPool) { + let db = db(pool).await; + let u = update( + "u7", + "repo/x", + Some("did:key:zX"), + "refs/heads/main", + "gggg", + ); + db.insert_ref_update(&u).await.unwrap(); + db.insert_ref_update(&u).await.unwrap(); + + let all = db.list_ref_updates(100).await.unwrap(); + assert_eq!(all.len(), 1); + assert_eq!(all[0].new_sha, "gggg"); + } +} diff --git a/crates/gitlawb-node/src/p2p/mod.rs b/crates/gitlawb-node/src/p2p/mod.rs index 1adbb34a..ee473011 100644 --- a/crates/gitlawb-node/src/p2p/mod.rs +++ b/crates/gitlawb-node/src/p2p/mod.rs @@ -438,3 +438,67 @@ pub async fn start( Ok(handle) } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn ref_update_event_round_trip_with_owner_did() { + let event = RefUpdateEvent { + node_did: "did:key:zNode".into(), + pusher_did: "did:key:zPusher".into(), + repo: "zOwner/myrepo".into(), + owner_did: Some("did:key:zOwner".into()), + ref_name: "refs/heads/main".into(), + old_sha: "0000000000000000000000000000000000000000".into(), + new_sha: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa".into(), + timestamp: "2026-07-02T12:00:00Z".into(), + cert_id: None, + cid: None, + }; + let json = serde_json::to_value(&event).unwrap(); + // owner_did must be present in the serialized output + assert_eq!(json["owner_did"], "did:key:zOwner"); + assert_eq!(json["repo"], "zOwner/myrepo"); + + let deserialized: RefUpdateEvent = serde_json::from_value(json).unwrap(); + assert_eq!(deserialized.owner_did, Some("did:key:zOwner".into())); + } + + #[test] + fn ref_update_event_backward_compat_no_owner_did() { + let old_json = serde_json::json!({ + "node_did": "did:key:zNode", + "pusher_did": "did:key:zPusher", + "repo": "zOwner/myrepo", + "ref_name": "refs/heads/main", + "old_sha": "0000000000000000000000000000000000000000", + "new_sha": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "timestamp": "2026-07-02T12:00:00Z", + "cert_id": null, + "cid": null + }); + let deserialized: RefUpdateEvent = serde_json::from_value(old_json).unwrap(); + assert_eq!(deserialized.owner_did, None); + assert_eq!(deserialized.repo, "zOwner/myrepo"); + } + + #[test] + fn ref_update_event_backward_compat_null_owner_did() { + let with_null = serde_json::json!({ + "node_did": "did:key:zNode", + "pusher_did": "did:key:zPusher", + "repo": "zOwner/myrepo", + "owner_did": null, + "ref_name": "refs/heads/main", + "old_sha": "0000000000000000000000000000000000000000", + "new_sha": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "timestamp": "2026-07-02T12:00:00Z", + "cert_id": null, + "cid": null + }); + let deserialized: RefUpdateEvent = serde_json::from_value(with_null).unwrap(); + assert_eq!(deserialized.owner_did, None); + } +} diff --git a/crates/gitlawb-node/src/test_support.rs b/crates/gitlawb-node/src/test_support.rs index d692f308..f5243050 100644 --- a/crates/gitlawb-node/src/test_support.rs +++ b/crates/gitlawb-node/src/test_support.rs @@ -3375,6 +3375,55 @@ mod tests { bounties.len(), 0, "anon should see 0 bounties from private repo even with 2 entries" + // ── Ref-update events (issue #144: owner_did wire format) ───────────────── + + fn events_router(state: AppState) -> Router { + Router::new() + .route( + "/api/v1/events/ref-updates", + axum::routing::get(crate::api::events::list_ref_updates), + ) + .with_state(state) + } + + #[sqlx::test] + async fn events_returns_inserted_ref_updates(pool: PgPool) { + let state = test_state(pool).await; + let owner = "did:key:zEVENTSOWNERAAAAAAAAAAAAAAAAAAAAAAAAA"; + + // Insert a gossip event with owner_did set + state + .db + .insert_ref_update(&crate::db::ReceivedRefUpdate { + id: uuid::Uuid::new_v4().to_string(), + node_did: "did:key:zNode".into(), + pusher_did: "did:key:zPusher".into(), + repo: format!("{}/myrepo", owner.split(':').next_back().unwrap()), + owner_did: Some(owner.into()), + ref_name: "refs/heads/main".into(), + old_sha: "0000000000000000000000000000000000000000".into(), + new_sha: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa".into(), + timestamp: "2026-07-02T12:00:00Z".into(), + cert_id: None, + received_at: "2026-07-02T12:00:01Z".into(), + from_peer: "12D3KooWTest".into(), + }) + .await + .unwrap(); + + let resp = events_router(state) + .oneshot(anon_get("/api/v1/events/ref-updates")) + .await + .unwrap(); + assert_eq!(resp.status(), StatusCode::OK); + + let body = json_body(resp).await; + let events = body["events"].as_array().unwrap(); + assert_eq!(events.len(), 1); + assert_eq!( + events[0]["repo"], + format!("{}/myrepo", owner.split(':').next_back().unwrap()) + ); } @@ -3546,6 +3595,40 @@ mod tests { .await .unwrap(); assert!(resp.status().is_success()); + async fn events_limit_respects_limit_param(pool: PgPool) { + let state = test_state(pool).await; + let owner = "did:key:zEVENTLIMITAAAAAAAAAAAAAAAAAAAAAAAA"; + + for i in 0..5 { + state + .db + .insert_ref_update(&crate::db::ReceivedRefUpdate { + id: uuid::Uuid::new_v4().to_string(), + node_did: "did:key:zNode".into(), + pusher_did: "did:key:zPusher".into(), + repo: format!("{}/r{i}", owner.split(':').next_back().unwrap()), + owner_did: Some(owner.into()), + ref_name: "refs/heads/main".into(), + old_sha: "0000000000000000000000000000000000000000".into(), + new_sha: format!("{i:040x}"), + timestamp: format!("2026-07-02T12:00:{i:02}Z"), + cert_id: None, + received_at: format!("2026-07-02T12:00:{i:02}Z"), + from_peer: "12D3KooWTest".into(), + }) + .await + .unwrap(); + } + + let resp = events_router(state) + .oneshot(anon_get("/api/v1/events/ref-updates?limit=2")) + .await + .unwrap(); + assert_eq!(resp.status(), StatusCode::OK); + let body = json_body(resp).await; + assert_eq!(body["count"].as_i64(), Some(2)); + assert_eq!(body["events"].as_array().unwrap().len(), 2); + } #[sqlx::test] From 930f4813d10d9642f0ec401c36c194f2dcdeffdf Mon Sep 17 00:00:00 2001 From: Gravirei Date: Thu, 2 Jul 2026 09:14:46 +0600 Subject: [PATCH 03/11] style(node): cargo fmt --- crates/gitlawb-node/src/api/repos.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/crates/gitlawb-node/src/api/repos.rs b/crates/gitlawb-node/src/api/repos.rs index 98eb6dbd..5f7b2ea0 100644 --- a/crates/gitlawb-node/src/api/repos.rs +++ b/crates/gitlawb-node/src/api/repos.rs @@ -2469,7 +2469,9 @@ mod tests { .match_body(mockito::Matcher::AllOf(vec![ mockito::Matcher::PartialJsonString(format!(r#"{{"old_sha":"{zero}"}}"#)), mockito::Matcher::PartialJsonString(format!(r#"{{"new_sha":"{new_sha}"}}"#)), - mockito::Matcher::PartialJsonString(r#"{"owner_did":"did:key:zOwner"}"#.to_string()), + mockito::Matcher::PartialJsonString( + r#"{"owner_did":"did:key:zOwner"}"#.to_string(), + ), ])) .with_status(200) .expect(1) From 3e7f7e924e4c43e7e14d9c4feeaa567b45b36568 Mon Sep 17 00:00:00 2001 From: Gravirei Date: Thu, 2 Jul 2026 11:21:42 +0600 Subject: [PATCH 04/11] fix(node): move owner_did migration to v10, add upgrade-path test --- crates/gitlawb-node/src/db/mod.rs | 57 +++++++++++++++++++++++++++++-- 1 file changed, 55 insertions(+), 2 deletions(-) diff --git a/crates/gitlawb-node/src/db/mod.rs b/crates/gitlawb-node/src/db/mod.rs index f5f1516c..69140d8b 100644 --- a/crates/gitlawb-node/src/db/mod.rs +++ b/crates/gitlawb-node/src/db/mod.rs @@ -569,10 +569,8 @@ const MIGRATIONS: &[Migration] = &[ received_at TEXT NOT NULL, from_peer TEXT NOT NULL )"#, - "ALTER TABLE received_ref_updates ADD COLUMN IF NOT EXISTS owner_did TEXT", "CREATE INDEX IF NOT EXISTS idx_ref_updates_repo ON received_ref_updates(repo)", "CREATE INDEX IF NOT EXISTS idx_ref_updates_ts ON received_ref_updates(timestamp DESC)", - "CREATE INDEX IF NOT EXISTS idx_ref_updates_owner ON received_ref_updates(owner_did)", r#"CREATE TABLE IF NOT EXISTS pull_requests ( id TEXT NOT NULL PRIMARY KEY, repo_id TEXT NOT NULL, @@ -858,6 +856,14 @@ const MIGRATIONS: &[Migration] = &[ "ALTER TABLE repos ADD COLUMN IF NOT EXISTS quarantined BOOLEAN NOT NULL DEFAULT FALSE", ], }, + Migration { + version: 10, + name: "ref_update_owner_did", + stmts: &[ + "ALTER TABLE received_ref_updates ADD COLUMN IF NOT EXISTS owner_did TEXT", + "CREATE INDEX IF NOT EXISTS idx_ref_updates_owner ON received_ref_updates(owner_did)", + ], + }, ]; // ── Repos ───────────────────────────────────────────────────────────────────── @@ -3342,6 +3348,53 @@ mod migration_tests { // it, you must also update the backfill. assert_eq!(MIGRATIONS[0].name, MIGRATION_V1_NAME); } + + /// Run a full migration from scratch and verify v10 creates the owner_did + /// column and index. Also verifies that an existing node re-running the + /// migration won't error (idempotent ALTER TABLE ADD COLUMN IF NOT EXISTS). + #[sqlx::test] + async fn migration_v10_creates_owner_did_column(pool: sqlx::PgPool) { + let db = super::Db::for_testing(pool); + + // Run the full migration (v1..v10) on a fresh database. + db.migrate().await.unwrap(); + + // Verify the owner_did column exists and is nullable TEXT. + let col: (String, String, String) = sqlx::query_as( + "SELECT column_name, data_type, is_nullable + FROM information_schema.columns + WHERE table_name = 'received_ref_updates' AND column_name = 'owner_did'", + ) + .fetch_one(&db.pool) + .await + .unwrap(); + assert_eq!(col.0, "owner_did"); + assert_eq!(col.1, "text"); + + // Verify the index exists. + let idx: (i64,) = sqlx::query_as( + "SELECT COUNT(*) FROM pg_indexes + WHERE tablename = 'received_ref_updates' AND indexname = 'idx_ref_updates_owner'", + ) + .fetch_one(&db.pool) + .await + .unwrap(); + assert_eq!(idx.0, 1, "idx_ref_updates_owner must exist"); + + // Verify version 10 is recorded as applied. + let v10_count: (i64,) = + sqlx::query_as("SELECT COUNT(*) FROM schema_migrations WHERE version = 10") + .fetch_one(&db.pool) + .await + .unwrap(); + assert_eq!( + v10_count.0, 1, + "migration v10 must be recorded in schema_migrations" + ); + + // Re-run: idempotent — ADD COLUMN IF NOT EXISTS must not error. + db.migrate().await.unwrap(); + } } #[cfg(test)] From 0786869933b9875bdc76b09bf434e68be1443c86 Mon Sep 17 00:00:00 2001 From: Gravirei Date: Sun, 5 Jul 2026 22:40:12 +0600 Subject: [PATCH 05/11] fix(node): add owner_did to GraphQL, broadcast, and REST feed surfaces (#144) --- crates/gitlawb-node/src/api/events.rs | 4 +++ crates/gitlawb-node/src/api/repos.rs | 1 + crates/gitlawb-node/src/db/mod.rs | 28 +++++++++++++------ crates/gitlawb-node/src/graphql/query.rs | 2 ++ .../gitlawb-node/src/graphql/subscription.rs | 1 + crates/gitlawb-node/src/graphql/types.rs | 1 + crates/gitlawb-node/src/state.rs | 1 + 7 files changed, 30 insertions(+), 8 deletions(-) diff --git a/crates/gitlawb-node/src/api/events.rs b/crates/gitlawb-node/src/api/events.rs index 0c35fa60..58b02ce8 100644 --- a/crates/gitlawb-node/src/api/events.rs +++ b/crates/gitlawb-node/src/api/events.rs @@ -152,6 +152,7 @@ pub async fn list_ref_updates( "cert_id": u.cert_id, "received_at": u.received_at, "from_peer": u.from_peer, + "owner_did": u.owner_did, }) }) .collect(); @@ -223,6 +224,7 @@ pub async fn list_repo_events( "pusher_did": c.pusher_did, "node_did": c.node_did, "timestamp": c.issued_at, + "owner_did": record.owner_did, "source": "local", }) }) @@ -256,6 +258,7 @@ pub async fn list_repo_events( "cert_id": u.cert_id, "received_at": u.received_at, "from_peer": u.from_peer, + "owner_did": u.owner_did, "source": "gossipsub", }) }) @@ -322,6 +325,7 @@ mod ref_updates_feed_tests { new_sha: "a".repeat(40), timestamp: Utc::now().to_rfc3339(), cert_id: None, + owner_did: None, received_at: Utc::now().to_rfc3339(), from_peer: "peer1".into(), } diff --git a/crates/gitlawb-node/src/api/repos.rs b/crates/gitlawb-node/src/api/repos.rs index 5f7b2ea0..a5c383b5 100644 --- a/crates/gitlawb-node/src/api/repos.rs +++ b/crates/gitlawb-node/src/api/repos.rs @@ -1297,6 +1297,7 @@ pub async fn git_receive_pack( pusher_did: pusher_did_clone.clone(), node_did: node_did_str.clone(), timestamp: now_ts.clone(), + owner_did: record.owner_did.clone(), }); } } diff --git a/crates/gitlawb-node/src/db/mod.rs b/crates/gitlawb-node/src/db/mod.rs index 69140d8b..03dc78be 100644 --- a/crates/gitlawb-node/src/db/mod.rs +++ b/crates/gitlawb-node/src/db/mod.rs @@ -4443,6 +4443,7 @@ mod ref_update_keyset_paging_tests { cert_id: None, received_at: ts.into(), from_peer: "peer".into(), + owner_did: None, } } @@ -4551,6 +4552,7 @@ mod ref_update_keyset_repo_filtered_tests { cert_id: None, received_at: ts.into(), from_peer: "peer".into(), + owner_did: None, } } @@ -4657,6 +4659,7 @@ mod ref_update_keyset_same_timestamp_tests { cert_id: None, received_at: ts.into(), from_peer: "peer".into(), + owner_did: None, } } @@ -4748,7 +4751,7 @@ mod ref_update_db_tests { .await .unwrap(); - let all = db.list_ref_updates(100).await.unwrap(); + let all = db.list_ref_updates_keyset(None, 100, None).await.unwrap(); assert_eq!(all.len(), 1); assert_eq!(all[0].owner_did.as_deref(), Some("did:key:zOwner")); assert_eq!(all[0].repo, "zOwner/myrepo"); @@ -4767,7 +4770,7 @@ mod ref_update_db_tests { .await .unwrap(); - let all = db.list_ref_updates(100).await.unwrap(); + let all = db.list_ref_updates_keyset(None, 100, None).await.unwrap(); assert_eq!(all.len(), 1); assert_eq!(all[0].owner_did, None); } @@ -4794,17 +4797,26 @@ mod ref_update_db_tests { .await .unwrap(); - let alice_events = db.list_repo_ref_updates("alice/repo1", 100).await.unwrap(); + let alice_events = db + .list_ref_updates_keyset(Some("alice/repo1"), 100, None) + .await + .unwrap(); assert_eq!(alice_events.len(), 1); assert_eq!(alice_events[0].id, "u3"); assert_eq!(alice_events[0].owner_did.as_deref(), Some("did:key:zAlice")); - let bob_events = db.list_repo_ref_updates("bob/repo2", 100).await.unwrap(); + let bob_events = db + .list_ref_updates_keyset(Some("bob/repo2"), 100, None) + .await + .unwrap(); assert_eq!(bob_events.len(), 1); assert_eq!(bob_events[0].id, "u4"); assert_eq!(bob_events[0].owner_did.as_deref(), Some("did:key:zBob")); - let empty = db.list_repo_ref_updates("other/repo", 100).await.unwrap(); + let empty = db + .list_ref_updates_keyset(Some("other/repo"), 100, None) + .await + .unwrap(); assert!(empty.is_empty()); } @@ -4831,13 +4843,13 @@ mod ref_update_db_tests { .unwrap(); let filtered = db - .list_ref_updates_filtered(Some("ownerA/proj"), 100) + .list_ref_updates_keyset(Some("ownerA/proj"), 100, None) .await .unwrap(); assert_eq!(filtered.len(), 1); assert_eq!(filtered[0].id, "u5"); - let all = db.list_ref_updates_filtered(None, 100).await.unwrap(); + let all = db.list_ref_updates_keyset(None, 100, None).await.unwrap(); assert_eq!(all.len(), 2); } @@ -4854,7 +4866,7 @@ mod ref_update_db_tests { db.insert_ref_update(&u).await.unwrap(); db.insert_ref_update(&u).await.unwrap(); - let all = db.list_ref_updates(100).await.unwrap(); + let all = db.list_ref_updates_keyset(None, 100, None).await.unwrap(); assert_eq!(all.len(), 1); assert_eq!(all[0].new_sha, "gggg"); } diff --git a/crates/gitlawb-node/src/graphql/query.rs b/crates/gitlawb-node/src/graphql/query.rs index 55148e02..d10f0798 100644 --- a/crates/gitlawb-node/src/graphql/query.rs +++ b/crates/gitlawb-node/src/graphql/query.rs @@ -83,6 +83,7 @@ impl QueryRoot { pusher_did: u.pusher_did, node_did: u.node_did, timestamp: u.timestamp, + owner_did: u.owner_did, }) .collect()) } @@ -160,6 +161,7 @@ mod tests { new_sha: "a".repeat(40), timestamp: Utc::now().to_rfc3339(), cert_id: None, + owner_did: None, received_at: Utc::now().to_rfc3339(), from_peer: "peer1".into(), } diff --git a/crates/gitlawb-node/src/graphql/subscription.rs b/crates/gitlawb-node/src/graphql/subscription.rs index 6c639bc6..7248cbf4 100644 --- a/crates/gitlawb-node/src/graphql/subscription.rs +++ b/crates/gitlawb-node/src/graphql/subscription.rs @@ -39,6 +39,7 @@ impl SubscriptionRoot { pusher_did: ev.pusher_did, node_did: ev.node_did, timestamp: ev.timestamp, + owner_did: Some(ev.owner_did), }), _ => None, } diff --git a/crates/gitlawb-node/src/graphql/types.rs b/crates/gitlawb-node/src/graphql/types.rs index 918701fd..4264a581 100644 --- a/crates/gitlawb-node/src/graphql/types.rs +++ b/crates/gitlawb-node/src/graphql/types.rs @@ -57,6 +57,7 @@ pub struct RefUpdateType { pub pusher_did: String, pub node_did: String, pub timestamp: String, + pub owner_did: Option, } #[derive(SimpleObject, Clone)] diff --git a/crates/gitlawb-node/src/state.rs b/crates/gitlawb-node/src/state.rs index 5f235b04..c690f130 100644 --- a/crates/gitlawb-node/src/state.rs +++ b/crates/gitlawb-node/src/state.rs @@ -17,6 +17,7 @@ pub struct RefUpdateBroadcast { pub pusher_did: String, pub node_did: String, pub timestamp: String, + pub owner_did: String, } #[derive(Clone, Debug)] From a5d482606555d1fbc1195021c6d3d6b25aff760f Mon Sep 17 00:00:00 2001 From: Gravirei Date: Mon, 6 Jul 2026 10:09:25 +0600 Subject: [PATCH 06/11] =?UTF-8?q?fix(node):=20address=20review=20feedback?= =?UTF-8?q?=20=E2=80=94=20drop=20ipfs=20regression,=20fix=20doc,=20improve?= =?UTF-8?q?=20migration=20test,=20use=20local=20owner=5Fdid?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- crates/gitlawb-node/src/api/events.rs | 2 +- crates/gitlawb-node/src/db/mod.rs | 90 ++++++++++++++++++++++----- crates/gitlawb-node/src/p2p/mod.rs | 6 +- 3 files changed, 77 insertions(+), 21 deletions(-) diff --git a/crates/gitlawb-node/src/api/events.rs b/crates/gitlawb-node/src/api/events.rs index 58b02ce8..9467be43 100644 --- a/crates/gitlawb-node/src/api/events.rs +++ b/crates/gitlawb-node/src/api/events.rs @@ -258,7 +258,7 @@ pub async fn list_repo_events( "cert_id": u.cert_id, "received_at": u.received_at, "from_peer": u.from_peer, - "owner_did": u.owner_did, + "owner_did": serde_json::json!(record.owner_did), "source": "gossipsub", }) }) diff --git a/crates/gitlawb-node/src/db/mod.rs b/crates/gitlawb-node/src/db/mod.rs index 03dc78be..3be5ee90 100644 --- a/crates/gitlawb-node/src/db/mod.rs +++ b/crates/gitlawb-node/src/db/mod.rs @@ -3349,17 +3349,83 @@ mod migration_tests { assert_eq!(MIGRATIONS[0].name, MIGRATION_V1_NAME); } - /// Run a full migration from scratch and verify v10 creates the owner_did - /// column and index. Also verifies that an existing node re-running the - /// migration won't error (idempotent ALTER TABLE ADD COLUMN IF NOT EXISTS). + /// Simulate an existing node at v9 with populated received_ref_updates, + /// then apply v10 and verify (a) owner_did IS NULL on existing rows, + /// (b) the column exists and is nullable TEXT, and (c) idempotent re-run + /// does not error. #[sqlx::test] async fn migration_v10_creates_owner_did_column(pool: sqlx::PgPool) { let db = super::Db::for_testing(pool); - // Run the full migration (v1..v10) on a fresh database. + // Create all tables by running the full migration chain from scratch. db.migrate().await.unwrap(); - // Verify the owner_did column exists and is nullable TEXT. + // Truncate schema_migrations and re-seed at v9 — simulate an existing + // node that has run v1..v9 but not yet v10. + sqlx::query("DELETE FROM schema_migrations") + .execute(&db.pool) + .await + .unwrap(); + for m in MIGRATIONS.iter().take_while(|m| m.version < 10) { + sqlx::query( + "INSERT INTO schema_migrations (version, name, applied_at) + VALUES ($1, $2, $3)", + ) + .bind(m.version) + .bind(m.name) + .bind("2026-07-01T00:00:00Z") + .execute(&db.pool) + .await + .unwrap(); + } + + // ── Simulate an existing node with rows recorded before v10 ──────── + // The owner_did column does not exist yet, so we INSERT without it. + let row_id = uuid::Uuid::new_v4().to_string(); + sqlx::query( + "INSERT INTO received_ref_updates + (id, node_did, pusher_did, repo, ref_name, old_sha, new_sha, + timestamp, cert_id, received_at, from_peer) + VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11)", + ) + .bind(&row_id) + .bind("did:key:zNode") + .bind("did:key:zPusher") + .bind("z6MkOwner/myrepo") + .bind("refs/heads/main") + .bind("0000000000000000000000000000000000000000") + .bind("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa") + .bind("2026-07-01T12:00:00Z") + .bind::>(None) + .bind("2026-07-01T12:00:01Z") + .bind("12D3KooWPeer") + .execute(&db.pool) + .await + .unwrap(); + assert_eq!( + sqlx::query_scalar::<_, i64>("SELECT COUNT(*) FROM received_ref_updates") + .fetch_one(&db.pool) + .await + .unwrap(), + 1, + "pre-migration row must exist" + ); + + // ── Apply v10 ───────────────────────────────────────────────────── + db.migrate().await.unwrap(); + + // ── Assertions ──────────────────────────────────────────────────── + + // (a) Existing row has owner_did IS NULL (not overwritten). + let owner: Option = + sqlx::query_scalar("SELECT owner_did FROM received_ref_updates WHERE id = $1") + .bind(&row_id) + .fetch_one(&db.pool) + .await + .unwrap(); + assert_eq!(owner, None, "existing row's owner_did must be NULL"); + + // (b) Column exists and is nullable TEXT. let col: (String, String, String) = sqlx::query_as( "SELECT column_name, data_type, is_nullable FROM information_schema.columns @@ -3371,17 +3437,7 @@ mod migration_tests { assert_eq!(col.0, "owner_did"); assert_eq!(col.1, "text"); - // Verify the index exists. - let idx: (i64,) = sqlx::query_as( - "SELECT COUNT(*) FROM pg_indexes - WHERE tablename = 'received_ref_updates' AND indexname = 'idx_ref_updates_owner'", - ) - .fetch_one(&db.pool) - .await - .unwrap(); - assert_eq!(idx.0, 1, "idx_ref_updates_owner must exist"); - - // Verify version 10 is recorded as applied. + // (c) Version 10 is recorded as applied. let v10_count: (i64,) = sqlx::query_as("SELECT COUNT(*) FROM schema_migrations WHERE version = 10") .fetch_one(&db.pool) @@ -3392,7 +3448,7 @@ mod migration_tests { "migration v10 must be recorded in schema_migrations" ); - // Re-run: idempotent — ADD COLUMN IF NOT EXISTS must not error. + // (d) Re-run: idempotent — ADD COLUMN IF NOT EXISTS must not error. db.migrate().await.unwrap(); } } diff --git a/crates/gitlawb-node/src/p2p/mod.rs b/crates/gitlawb-node/src/p2p/mod.rs index ee473011..80e28a4a 100644 --- a/crates/gitlawb-node/src/p2p/mod.rs +++ b/crates/gitlawb-node/src/p2p/mod.rs @@ -39,9 +39,9 @@ pub struct RefUpdateEvent { pub pusher_did: String, /// Repository identifier (owner/name) pub repo: String, - /// Full owner DID — added in #144 so the feed gate can distinguish - /// different DID methods that share the same trailing segment. - /// Optional for backward compat with older peers that don't include it. + /// Full owner DID — added in #144 for display and storage; not yet + /// wired into the feed gate matcher. Optional for backward compat with + /// older peers that don't include it. #[serde(default)] pub owner_did: Option, /// Git ref that changed (e.g., "refs/heads/main") From b768be872d8203fa440ecb3b64ec5375a3fb98e4 Mon Sep 17 00:00:00 2001 From: Gravirei Date: Mon, 6 Jul 2026 11:29:19 +0600 Subject: [PATCH 07/11] fix(node): fix migration v10 test to actually start from v9 schema; complete owner_did API assertion --- crates/gitlawb-node/src/db/mod.rs | 7 ++++++- crates/gitlawb-node/src/test_support.rs | 1 + 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/crates/gitlawb-node/src/db/mod.rs b/crates/gitlawb-node/src/db/mod.rs index 3be5ee90..4a6cf079 100644 --- a/crates/gitlawb-node/src/db/mod.rs +++ b/crates/gitlawb-node/src/db/mod.rs @@ -3357,8 +3357,13 @@ mod migration_tests { async fn migration_v10_creates_owner_did_column(pool: sqlx::PgPool) { let db = super::Db::for_testing(pool); - // Create all tables by running the full migration chain from scratch. + // Create all tables by running the full migration chain from scratch, + // then drop the owner_did column to simulate a pre-v10 schema. db.migrate().await.unwrap(); + sqlx::query("ALTER TABLE received_ref_updates DROP COLUMN owner_did") + .execute(&db.pool) + .await + .unwrap(); // Truncate schema_migrations and re-seed at v9 — simulate an existing // node that has run v1..v9 but not yet v10. diff --git a/crates/gitlawb-node/src/test_support.rs b/crates/gitlawb-node/src/test_support.rs index f5243050..90c1bf0d 100644 --- a/crates/gitlawb-node/src/test_support.rs +++ b/crates/gitlawb-node/src/test_support.rs @@ -3425,6 +3425,7 @@ mod tests { format!("{}/myrepo", owner.split(':').next_back().unwrap()) ); + assert_eq!(events[0]["owner_did"], owner); } #[sqlx::test] From b4dfd7dd6db1c88112b70cfb600a0d344f725ebf Mon Sep 17 00:00:00 2001 From: Gravirei Date: Mon, 6 Jul 2026 21:55:45 +0600 Subject: [PATCH 08/11] fix: defer unused owner_did index creation --- crates/gitlawb-node/src/db/mod.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/crates/gitlawb-node/src/db/mod.rs b/crates/gitlawb-node/src/db/mod.rs index 4a6cf079..2810de69 100644 --- a/crates/gitlawb-node/src/db/mod.rs +++ b/crates/gitlawb-node/src/db/mod.rs @@ -861,7 +861,6 @@ const MIGRATIONS: &[Migration] = &[ name: "ref_update_owner_did", stmts: &[ "ALTER TABLE received_ref_updates ADD COLUMN IF NOT EXISTS owner_did TEXT", - "CREATE INDEX IF NOT EXISTS idx_ref_updates_owner ON received_ref_updates(owner_did)", ], }, ]; From eb9b35cd3cbfd50b229127aba008c7ddca17e12a Mon Sep 17 00:00:00 2001 From: Gravirei Date: Tue, 7 Jul 2026 11:14:03 +0600 Subject: [PATCH 09/11] fix(node): show local owner on global feed, assert nullable in migration test, add owner_did echo assertions --- crates/gitlawb-node/src/api/events.rs | 27 ++++++++++++++++++++++-- crates/gitlawb-node/src/db/mod.rs | 1 + crates/gitlawb-node/src/graphql/query.rs | 8 +++++-- 3 files changed, 32 insertions(+), 4 deletions(-) diff --git a/crates/gitlawb-node/src/api/events.rs b/crates/gitlawb-node/src/api/events.rs index 9467be43..17ede4d8 100644 --- a/crates/gitlawb-node/src/api/events.rs +++ b/crates/gitlawb-node/src/api/events.rs @@ -137,9 +137,25 @@ pub async fn list_ref_updates( let caller = auth.as_ref().map(|e| e.0 .0.as_str()); let updates = collect_visible_ref_updates(&state.db, None, limit, caller).await?; + // Resolve the local owner_did for each row so locally-hosted repos + // display their canonical owner rather than a peer-supplied wire value. + let deduped = state.db.list_all_repos_deduped().await?; + let resolve_local = |slug: &str| -> Option<&str> { + for record in &deduped { + if crate::visibility::ref_update_row_names_repo(record, slug) { + return Some(&record.owner_did); + } + } + None + }; + let events: Vec = updates .iter() .map(|u| { + let owner_did = match resolve_local(&u.repo) { + Some(local) => serde_json::json!(local), + None => serde_json::json!(u.owner_did), + }; serde_json::json!({ "id": u.id, "node_did": u.node_did, @@ -152,7 +168,7 @@ pub async fn list_ref_updates( "cert_id": u.cert_id, "received_at": u.received_at, "from_peer": u.from_peer, - "owner_did": u.owner_did, + "owner_did": owner_did, }) }) .collect(); @@ -1107,7 +1123,14 @@ mod ref_updates_feed_tests { .await .unwrap(); assert_eq!(resp.status(), StatusCode::OK); - assert_eq!(count(&body_json(resp).await), 2); + let body = body_json(resp).await; + assert_eq!(count(&body), 2); + for event in body["events"].as_array().unwrap() { + assert_eq!( + event["owner_did"], OWNER, + "each event must carry the local owner_did" + ); + } } // Anon reads a quarantined mirror → 404 (withheld without disclosing existence diff --git a/crates/gitlawb-node/src/db/mod.rs b/crates/gitlawb-node/src/db/mod.rs index 2810de69..806b8fac 100644 --- a/crates/gitlawb-node/src/db/mod.rs +++ b/crates/gitlawb-node/src/db/mod.rs @@ -3440,6 +3440,7 @@ mod migration_tests { .unwrap(); assert_eq!(col.0, "owner_did"); assert_eq!(col.1, "text"); + assert_eq!(col.2, "YES", "owner_did must be nullable"); // (c) Version 10 is recorded as applied. let v10_count: (i64,) = diff --git a/crates/gitlawb-node/src/graphql/query.rs b/crates/gitlawb-node/src/graphql/query.rs index d10f0798..a0dee6af 100644 --- a/crates/gitlawb-node/src/graphql/query.rs +++ b/crates/gitlawb-node/src/graphql/query.rs @@ -247,10 +247,10 @@ mod tests { .await .unwrap(); let schema = schema(db); - let q = r#"{ refUpdates { repo refName } }"#; + let q = r#"{ refUpdates { repo refName ownerDid } }"#; let resp = anon(&schema, q).await; assert_eq!(count(&resp), 1); - // The one row returned must be the public repo's. + // The one row returned must be the public repo's with owner_did echoed. let async_graphql::Value::Object(obj) = &resp.data else { unreachable!() }; @@ -264,6 +264,10 @@ mod tests { row.get("repo").unwrap(), &async_graphql::Value::from("z6MkOwner/openrepo") ); + assert!( + row.contains_key("ownerDid"), + "ownerDid must be present in the refUpdates response" + ); } // Scenario 4 — alias fail-closed: private repo's row stored full-DID form. From a579af8827306e56e62e8f5fa709be6da17a6f80 Mon Sep 17 00:00:00 2001 From: Gravirei Date: Tue, 7 Jul 2026 20:40:44 +0600 Subject: [PATCH 10/11] fix(node): prefer stored wire owner_did over lossy slug-local resolution; mirror projection in GraphQL --- crates/gitlawb-node/src/api/events.rs | 15 +++++--- crates/gitlawb-node/src/graphql/query.rs | 44 +++++++++++++++++------- 2 files changed, 42 insertions(+), 17 deletions(-) diff --git a/crates/gitlawb-node/src/api/events.rs b/crates/gitlawb-node/src/api/events.rs index 17ede4d8..369d9f3d 100644 --- a/crates/gitlawb-node/src/api/events.rs +++ b/crates/gitlawb-node/src/api/events.rs @@ -137,8 +137,10 @@ pub async fn list_ref_updates( let caller = auth.as_ref().map(|e| e.0 .0.as_str()); let updates = collect_visible_ref_updates(&state.db, None, limit, caller).await?; - // Resolve the local owner_did for each row so locally-hosted repos - // display their canonical owner rather than a peer-supplied wire value. + // Prefer the stored wire owner_did (full DID, may differ from local + // record's trailing-segment match). Fall back to the local record's + // owner_did only for backward-compat rows (stored as None) that name a + // repo this node hosts, so legacy rows still display an owner. let deduped = state.db.list_all_repos_deduped().await?; let resolve_local = |slug: &str| -> Option<&str> { for record in &deduped { @@ -152,9 +154,12 @@ pub async fn list_ref_updates( let events: Vec = updates .iter() .map(|u| { - let owner_did = match resolve_local(&u.repo) { - Some(local) => serde_json::json!(local), - None => serde_json::json!(u.owner_did), + let owner_did = match &u.owner_did { + Some(wire) => serde_json::json!(wire), + None => match resolve_local(&u.repo) { + Some(local) => serde_json::json!(local), + None => serde_json::Value::Null, + }, }; serde_json::json!({ "id": u.id, diff --git a/crates/gitlawb-node/src/graphql/query.rs b/crates/gitlawb-node/src/graphql/query.rs index a0dee6af..ce217bc0 100644 --- a/crates/gitlawb-node/src/graphql/query.rs +++ b/crates/gitlawb-node/src/graphql/query.rs @@ -73,17 +73,36 @@ impl QueryRoot { .await .map_err(|e| async_graphql::Error::new(e.to_string()))?; + // Match the REST feed's owner_did projection: prefer the stored wire + // value (full DID), fall back to the local record's owner for + // backward-compat rows (stored as None) that name a local repo. + let deduped = db + .list_all_repos_deduped() + .await + .map_err(|e| async_graphql::Error::new(e.to_string()))?; + let resolve_local = |slug: &str| -> Option { + for record in &deduped { + if crate::visibility::ref_update_row_names_repo(record, slug) { + return Some(record.owner_did.clone()); + } + } + None + }; + Ok(updates .into_iter() - .map(|u| RefUpdateType { - repo: u.repo, - ref_name: u.ref_name, - old_sha: u.old_sha, - new_sha: u.new_sha, - pusher_did: u.pusher_did, - node_did: u.node_did, - timestamp: u.timestamp, - owner_did: u.owner_did, + .map(|u| { + let owner_did = u.owner_did.or_else(|| resolve_local(&u.repo)); + RefUpdateType { + repo: u.repo, + ref_name: u.ref_name, + old_sha: u.old_sha, + new_sha: u.new_sha, + pusher_did: u.pusher_did, + node_did: u.node_did, + timestamp: u.timestamp, + owner_did, + } }) .collect()) } @@ -264,9 +283,10 @@ mod tests { row.get("repo").unwrap(), &async_graphql::Value::from("z6MkOwner/openrepo") ); - assert!( - row.contains_key("ownerDid"), - "ownerDid must be present in the refUpdates response" + assert_eq!( + row.get("ownerDid").unwrap(), + &async_graphql::Value::from(OWNER), + "ownerDid must fall back to the local record's owner for legacy rows" ); } From 58d5c767919fe1c51f633c67de13cff946b976c2 Mon Sep 17 00:00:00 2001 From: Gravirei Date: Wed, 8 Jul 2026 13:57:06 +0600 Subject: [PATCH 11/11] fix(node): close test functions truncated during rebase conflict resolution --- crates/gitlawb-node/src/test_support.rs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/crates/gitlawb-node/src/test_support.rs b/crates/gitlawb-node/src/test_support.rs index 90c1bf0d..f688c06d 100644 --- a/crates/gitlawb-node/src/test_support.rs +++ b/crates/gitlawb-node/src/test_support.rs @@ -3375,6 +3375,9 @@ mod tests { bounties.len(), 0, "anon should see 0 bounties from private repo even with 2 entries" + ); + } + // ── Ref-update events (issue #144: owner_did wire format) ───────────────── fn events_router(state: AppState) -> Router { @@ -3423,7 +3426,6 @@ mod tests { assert_eq!( events[0]["repo"], format!("{}/myrepo", owner.split(':').next_back().unwrap()) - ); assert_eq!(events[0]["owner_did"], owner); } @@ -3596,6 +3598,9 @@ mod tests { .await .unwrap(); assert!(resp.status().is_success()); + } + + #[sqlx::test] async fn events_limit_respects_limit_param(pool: PgPool) { let state = test_state(pool).await; let owner = "did:key:zEVENTLIMITAAAAAAAAAAAAAAAAAAAAAAAA"; @@ -3629,7 +3634,6 @@ mod tests { let body = json_body(resp).await; assert_eq!(body["count"].as_i64(), Some(2)); assert_eq!(body["events"].as_array().unwrap().len(), 2); - } #[sqlx::test]