diff --git a/bobbin/crates/bobbin/src/main.rs b/bobbin/crates/bobbin/src/main.rs index fe23b9ce0..b156cc3fa 100644 --- a/bobbin/crates/bobbin/src/main.rs +++ b/bobbin/crates/bobbin/src/main.rs @@ -257,8 +257,8 @@ async fn run(cfg: BobbinConfig) -> anyhow::Result<()> { limiter: limiter.clone(), budget, }); - let breaker_threshold = FailureThreshold::new(cfg.mirror.breaker_threshold) - .context("mirror.breaker_threshold")?; + let breaker_threshold = + FailureThreshold::new(cfg.mirror.breaker_threshold).context("mirror.breaker_threshold")?; let breaker_cooldown = Duration::from_secs(cfg.mirror.breaker_cooldown_secs); let mirror = cfg .mirror diff --git a/bobbin/crates/edge-index/src/labels.rs b/bobbin/crates/edge-index/src/labels.rs index 837a0eeb5..69e0666bd 100644 --- a/bobbin/crates/edge-index/src/labels.rs +++ b/bobbin/crates/edge-index/src/labels.rs @@ -219,7 +219,11 @@ impl LabelIndex { fn active(&self, subject: LabelSubject, now: UnixMicros) -> SmallVec<[LabelEntry; 1]> { self.by_subject .read_sync(&subject, |_, entries| { - entries.iter().filter(|e| e.is_active(now)).copied().collect() + entries + .iter() + .filter(|e| e.is_active(now)) + .copied() + .collect() }) .unwrap_or_default() } @@ -270,7 +274,7 @@ mod tests { Did::new_owned(BOB_DID).unwrap() } - fn label(target: LabelTarget<'_>, val: &'static str) -> IncomingLabel<'_> { + fn label<'a>(target: LabelTarget<'a>, val: &'static str) -> IncomingLabel<'a> { IncomingLabel { target, cid: None, @@ -283,7 +287,10 @@ mod tests { } fn vals(index: &LabelIndex, entries: &[LabelEntry]) -> Vec { - entries.iter().map(|e| index.val_str(e.val).to_owned()).collect() + entries + .iter() + .map(|e| index.val_str(e.val).to_owned()) + .collect() } #[test] @@ -293,7 +300,10 @@ mod tests { let now = UnixMicros::new(10); index.apply(label(LabelTarget::Account(&bob), "!warn")); index.apply(label(LabelTarget::Account(&bob), "spam")); - assert_eq!(vals(&index, &index.account_labels(&bob, now)), ["!warn", "spam"]); + assert_eq!( + vals(&index, &index.account_labels(&bob, now)), + ["!warn", "spam"] + ); index.apply(IncomingLabel { neg: true, @@ -321,7 +331,10 @@ mod tests { let entries = index.account_labels(&bob, UnixMicros::new(10)); assert_eq!(entries.len(), 1); assert_eq!(entries[0].seq, 7); - assert_eq!(entries[0].val, LabelVal::Moderation(ModerationLabel::Takedown)); + assert_eq!( + entries[0].val, + LabelVal::Moderation(ModerationLabel::Takedown) + ); } #[test] @@ -402,9 +415,15 @@ mod tests { #[test] fn targets_come_from_the_parsed_label_uri() { let record = UriValue::new_owned(REPO).unwrap(); - assert!(matches!(LabelTarget::try_from(&record), Ok(LabelTarget::Record(_)))); + assert!(matches!( + LabelTarget::try_from(&record), + Ok(LabelTarget::Record(_)) + )); let account = UriValue::new_owned(BOB_DID).unwrap(); - assert!(matches!(LabelTarget::try_from(&account), Ok(LabelTarget::Account(_)))); + assert!(matches!( + LabelTarget::try_from(&account), + Ok(LabelTarget::Account(_)) + )); let other = UriValue::new_owned("https://tangled.org").unwrap(); assert!(LabelTarget::try_from(&other).is_err()); } diff --git a/bobbin/crates/edge-index/src/lib.rs b/bobbin/crates/edge-index/src/lib.rs index 45cad9dcc..a5f125086 100644 --- a/bobbin/crates/edge-index/src/lib.rs +++ b/bobbin/crates/edge-index/src/lib.rs @@ -928,8 +928,7 @@ impl EdgeStore { ) { (Some((_, collection, rkey)), Some(did)) => { let author = self.lookup_did(did)?; - let collection = - CollectionId::from_spur(self.collection_interner.get(collection)?); + let collection = CollectionId::from_spur(self.collection_interner.get(collection)?); format!("{}/{}/{}", author.index(), collection.index(), rkey) } _ => source.as_ref().to_owned(), diff --git a/bobbin/crates/xrpc/src/enrich.rs b/bobbin/crates/xrpc/src/enrich.rs index 38526fc2a..99b424793 100644 --- a/bobbin/crates/xrpc/src/enrich.rs +++ b/bobbin/crates/xrpc/src/enrich.rs @@ -7,8 +7,8 @@ use axum::{ http::{Request, StatusCode}, response::Response, }; -use bobbin_types::sh_tangled::query::enrich_response::Labels; use bobbin_types::ids::{EdgeKey, SubjectRef, nsid_static, owner_did_from_aturi}; +use bobbin_types::sh_tangled::query::enrich_response::Labels; use futures::stream::{self, StreamExt as _}; use jacquard_common::DefaultStr; use jacquard_common::IntoStatic; @@ -320,7 +320,7 @@ pub async fn enrich( labels, extra_data: None, }) - .map_err(|e| XrpcError::Internal(format!("labels encode: {e}")))?; + .map_err(|e| XrpcError::Internal(format!("labels encode: {e}")))?; put( &mut data, subject.as_str(), @@ -748,7 +748,10 @@ mod tests { #[test] fn labels_read_the_record_or_its_repo() { let labels = PayloadType::parse(TYPE_LABELS).unwrap(); - for ok in ["org.tangled.actor.profile:.", "org.tangled.feed.comment:.repo"] { + for ok in [ + "org.tangled.actor.profile:.", + "org.tangled.feed.comment:.repo", + ] { assert!(validate_source(&LinkSource::parse(ok).unwrap(), &labels).is_ok()); } let subject = LinkSource::parse("org.tangled.feed.star:subject").unwrap(); diff --git a/bobbin/crates/xrpc/src/repo.rs b/bobbin/crates/xrpc/src/repo.rs index f6ca8be24..6180ad6b3 100644 --- a/bobbin/crates/xrpc/src/repo.rs +++ b/bobbin/crates/xrpc/src/repo.rs @@ -6,10 +6,15 @@ use axum::extract::State; use bobbin_types::{ edges::ticket_editor, ids::owner_did_from_aturi, - org_tangled::track::{ - TicketViewDetailed, get_ticket_view, - ticket::{Ticket, TicketRecord}, + org_tangled::{ + actor as org_actor, + repo as org_repo, + track::{ + TicketViewDetailed, get_ticket_view, + ticket::{Ticket, TicketRecord}, + }, }, + sh_tangled::{actor as sh_actor, repo as sh_repo}, }; use jacquard_axum::{ExtractXrpc, XrpcResponse}; @@ -23,10 +28,10 @@ pub(crate) async fn get_ticket_view( let repo_did = owner_did_from_aturi(¶ms.uri) .ok_or_else(|| XrpcError::InvalidParams("ticket uri authority must be a did".into()))?; - let repo = build_repo_view_basic(&state, viewer, &repo_did).await?; + let repo = org_repo_view(build_repo_view_basic(&state, viewer, &repo_did).await?); let author_did = ticket_editor(&ticket).unwrap_or_else(|| repo_did.clone()); - let author = build_profile_basic(&state, viewer, &author_did).await; + let author = org_actor_view(build_profile_basic(&state, viewer, &author_did).await); Ok(XrpcResponse(get_ticket_view::GetTicketViewOutput { value: TicketViewDetailed { @@ -46,6 +51,49 @@ pub(crate) async fn get_ticket_view( })) } +fn org_actor_view(value: sh_actor::ProfileViewBasic) -> org_actor::ProfileViewBasic { + org_actor::ProfileViewBasic { + avatar: value.avatar, + did: value.did, + handle: value.handle, + viewer: value.viewer.map(org_actor_viewer), + extra_data: value.extra_data, + } +} + +fn org_actor_viewer(value: sh_actor::ViewerState) -> org_actor::ViewerState { + org_actor::ViewerState { + followed_by: value.followed_by, + following: value.following, + extra_data: value.extra_data, + } +} + +fn org_repo_view(value: sh_repo::RepoViewBasic) -> org_repo::RepoViewBasic { + org_repo::RepoViewBasic { + created_at: value.created_at, + description: value.description, + did: value.did, + fork_count: value.fork_count, + is_starred: value.is_starred, + open_issue_count: value.open_issue_count, + open_pull_count: value.open_pull_count, + owner: org_actor_view(value.owner), + slug: value.slug, + star_count: value.star_count, + viewer: value.viewer.map(org_repo_viewer), + extra_data: value.extra_data, + } +} + +fn org_repo_viewer(value: sh_repo::ViewerState) -> org_repo::ViewerState { + org_repo::ViewerState { + fork: value.fork, + star: value.star, + extra_data: value.extra_data, + } +} + // pub(crate) async fn list_pull_views( // State(state): State, // ExtractXrpc(params): ExtractXrpc, diff --git a/gitmirror/crates/gitmirror-db/src/lib.rs b/gitmirror/crates/gitmirror-db/src/lib.rs index ed5824c0d..27ffca80a 100644 --- a/gitmirror/crates/gitmirror-db/src/lib.rs +++ b/gitmirror/crates/gitmirror-db/src/lib.rs @@ -221,7 +221,10 @@ impl Db { ) .fetch_all(&self.0) .await?; - Ok(rows.into_iter().map(|r| (r.state, r.due, r.count)).collect()) + Ok(rows + .into_iter() + .map(|r| (r.state, r.due, r.count)) + .collect()) } pub async fn mark_synced( @@ -429,6 +432,8 @@ impl Db { ) .fetch_all(&self.0) .await?; - Ok(Some(rows.into_iter().map(|r| (r.language, r.size)).collect())) + Ok(Some( + rows.into_iter().map(|r| (r.language, r.size)).collect(), + )) } } diff --git a/gitmirror/crates/gitmirror-sync/src/lib.rs b/gitmirror/crates/gitmirror-sync/src/lib.rs index 0f51ebcc9..849ce1b82 100644 --- a/gitmirror/crates/gitmirror-sync/src/lib.rs +++ b/gitmirror/crates/gitmirror-sync/src/lib.rs @@ -10,11 +10,11 @@ use gitmirror_db::{Db, DbError}; use gitmirror_git::layout::Layout; use gitmirror_types::{KnotPolicy, backoff_us, now_us}; use jacquard_common::types::did::Did; +use metrics::{gauge, histogram}; use thiserror::Error; use tokio::sync::{Notify, Semaphore}; use tokio_util::sync::CancellationToken; -use metrics::{gauge, histogram}; -use tracing::{error, info, warn, Instrument as _}; +use tracing::{Instrument as _, error, info, warn}; mod admission; mod task; @@ -180,7 +180,7 @@ impl BackfillWorker { let duration = started.elapsed().as_secs_f64(); let outcome = match &res { - Ok(task::TaskDisposition::Finished {..}) => "finished", + Ok(task::TaskDisposition::Finished { .. }) => "finished", Ok(task::TaskDisposition::Deferred) => "deferred", Err(_) => "failed", }; @@ -199,8 +199,9 @@ impl BackfillWorker { default_branch_commit, }) => { let commit = default_branch_commit.map(|oid| oid.to_string()); - let res = - db.mark_synced(&row.did, &row.refs_cid, commit.as_deref()).await; + let res = db + .mark_synced(&row.did, &row.refs_cid, commit.as_deref()) + .await; if res.is_ok() { index_notify.notify_one(); } diff --git a/gitmirror/crates/gitmirror-sync/src/task.rs b/gitmirror/crates/gitmirror-sync/src/task.rs index c2bd49410..8c9daa202 100644 --- a/gitmirror/crates/gitmirror-sync/src/task.rs +++ b/gitmirror/crates/gitmirror-sync/src/task.rs @@ -13,8 +13,8 @@ use jacquard_identity::resolver::IdentityResolver as _; use thiserror::Error; use tracing::debug; -use crate::admission::BackfillAdmission; use crate::Directory; +use crate::admission::BackfillAdmission; const AGENT: &str = concat!("gitmirror/", env!("CARGO_PKG_VERSION")); diff --git a/gitmirror/crates/gitmirror-xrpc/src/metrics.rs b/gitmirror/crates/gitmirror-xrpc/src/metrics.rs index cb18c09e4..bef3b1fcd 100644 --- a/gitmirror/crates/gitmirror-xrpc/src/metrics.rs +++ b/gitmirror/crates/gitmirror-xrpc/src/metrics.rs @@ -86,10 +86,7 @@ fn describe_metrics() { "gitmirror_sync_repos", "Repos by sync state and whether they are due for a sync" ); - metrics::describe_gauge!( - "gitmirror_sync_in_flight", - "Sync tasks currently running" - ); + metrics::describe_gauge!("gitmirror_sync_in_flight", "Sync tasks currently running"); metrics::describe_histogram!( "gitmirror_sync_task_duration_seconds", "Sync task duration in seconds" diff --git a/knot2/crates/knot-atproto/src/jwt.rs b/knot2/crates/knot-atproto/src/jwt.rs index 9ea06a80b..4d342a4f7 100644 --- a/knot2/crates/knot-atproto/src/jwt.rs +++ b/knot2/crates/knot-atproto/src/jwt.rs @@ -176,9 +176,11 @@ struct RepoTokenClaims<'a> { #[derive(Debug, thiserror::Error)] pub enum MintExpiryError { -#[error("Exp must be in the future.")] + #[error("Exp must be in the future.")] NotAfterMint, - #[error("Exp is {lifespan} seconds after minting, past the {REPO_TOKEN_MAX_LIFETIME_SECS}-second limit.")] + #[error( + "Exp is {lifespan} seconds after minting, past the {REPO_TOKEN_MAX_LIFETIME_SECS}-second limit." + )] TooLong { lifespan: i64 }, } @@ -447,7 +449,7 @@ mod tests { }, ClaimsCase { name: "bound to another method", - claims: || serde_json::json!({ "iss": SQUID, "aud": KNOT, "exp": 1_000, "iat": 940, "lxm": "sh.tangled.repo.delete" }), + claims: || serde_json::json!({ "iss": SQUID, "aud": KNOT, "exp": 1_000, "iat": 940, "lxm": "org.tangled.repo.deleteRepo" }), now: 900, expect: |r| matches!(r, Err(JwtError::MethodMismatch { .. })), }, diff --git a/knot2/crates/knot-atproto/src/lib.rs b/knot2/crates/knot-atproto/src/lib.rs index d5e6e5b40..eee68614e 100644 --- a/knot2/crates/knot-atproto/src/lib.rs +++ b/knot2/crates/knot-atproto/src/lib.rs @@ -81,6 +81,9 @@ const NEGATIVE_TTL: Duration = Duration::from_secs(30); const STALE_TTL: Duration = Duration::from_secs(30); const PUBKEY_PAGE_LIMIT: u16 = 100; fn repo_collection() -> Nsid { + Nsid::new_static("org.tangled.repo.declaration").expect("Literal NSID parses") +} +fn legacy_repo_collection() -> Nsid { Nsid::new_static("sh.tangled.repo").expect("Literal NSID parses") } const PUBKEY_MAX_PAGES: usize = 8; @@ -534,32 +537,37 @@ impl Atproto { &self, owner: &AccountDid, rkey: &RepoRkey, - ) -> Result { - self.record_present(owner, &repo_collection(), rkey.as_str()) - .await - } - - async fn record_present( - &self, - owner: &AccountDid, - collection: &Nsid, - rkey: &str, ) -> Result { let cached = self.identity(owner).await?; - let read = self - .read_record(&cached.value, owner, collection, rkey) - .await?; + let read = self.read_declared(&cached.value, owner, rkey).await?; match read { RecordPresence::Absent if !cached.fresh => { let current = self.fetch_identity(owner).await?; match current.pds.url() == cached.value.pds.url() { true => Ok(RecordPresence::Absent), - false => self.read_record(¤t, owner, collection, rkey).await, + false => self.read_declared(¤t, owner, rkey).await, } } settled => Ok(settled), } } + async fn read_declared( + &self, + identity: &Identity, + owner: &AccountDid, + rkey: &RepoRkey, + ) -> Result { + match self + .read_record(identity, owner, &repo_collection(), rkey.as_str()) + .await? + { + RecordPresence::Present => Ok(RecordPresence::Present), + RecordPresence::Absent => { + self.read_record(identity, owner, &legacy_repo_collection(), rkey.as_str()) + .await + } + } + } async fn read_record( &self, @@ -1251,20 +1259,30 @@ mod tests { return Ok(ok(squid_doc(&signing))); } assert!(request.url.path().ends_with("com.atproto.repo.getRecord")); - assert!(request.url.query().unwrap().contains("sh.tangled.repo")); + assert!( + request.url.query().unwrap().contains("repo.declaration") + || request.url.query().unwrap().contains("sh.tangled.repo") + ); + let collection = request + .url + .query_pairs() + .find(|(key, _)| key == "collection") + .map(|(_, value)| value.into_owned()) + .unwrap_or_default(); let rkey = request .url .query_pairs() .find(|(key, _)| key == "rkey") .map(|(_, value)| value.into_owned()) .unwrap_or_default(); - let (st, body) = match rkey.as_str() { - "present" => (StatusCode::OK, Bytes::new()), - "missing" => ( + let (st, body) = match (collection.as_str(), rkey.as_str()) { + (_, "boom") => (StatusCode::INTERNAL_SERVER_ERROR, Bytes::new()), + ("sh.tangled.repo", "legacy") => (StatusCode::OK, Bytes::new()), + ("org.tangled.repo.declaration", "present") => (StatusCode::OK, Bytes::new()), + _ => ( StatusCode::BAD_REQUEST, Bytes::from_static(b"{\"error\":\"RecordNotFound\"}"), ), - _ => (StatusCode::INTERNAL_SERVER_ERROR, Bytes::new()), }; Ok(status(st, body)) }); @@ -1284,6 +1302,14 @@ mod tests { .unwrap(), RecordPresence::Absent ); + assert_eq!( + atproto + .repo_record_present(&owner, &RepoRkey::new("legacy").unwrap()) + .await + .unwrap(), + RecordPresence::Present, + "a declaration the previous release wrote still counts as declared" + ); assert!( atproto .repo_record_present(&owner, &RepoRkey::new("boom").unwrap()) @@ -1782,7 +1808,7 @@ mod tests { JwtCase { name: "method mismatch is refused before resolution", header: JWT_HEADER, - mutate: |c| c["lxm"] = serde_json::json!("sh.tangled.repo.delete"), + mutate: |c| c["lxm"] = serde_json::json!("org.tangled.repo.deleteRepo"), expect: |r| matches!(r, Err(AtprotoError::Jwt(JwtError::MethodMismatch { .. }))), zero_network: true, }, @@ -2306,13 +2332,12 @@ mod tests { let body: serde_json::Value = serde_json::from_slice(request.body.as_ref().unwrap()).unwrap(); assert_eq!(body["repo"], SQUID); - assert_eq!(body["collection"], "sh.tangled.knot.member"); + assert_eq!(body["collection"], "org.tangled.knot.member"); assert_eq!(body["rkey"], POINTER_RKEY); - assert_eq!(body["record"]["$type"], "sh.tangled.knot.member"); - assert_eq!(body["record"]["subject"], "did:plc:lyna"); - assert_eq!(body["record"]["domain"], "knot.nel.pet"); + assert_eq!(body["record"]["$type"], "org.tangled.knot.member"); + assert_eq!(body["record"]["createdAt"], "2026-06-11T00:00:00Z"); let receipt = serde_json::json!({ - "uri": format!("at://{SQUID}/sh.tangled.knot.member/{POINTER_RKEY}"), + "uri": format!("at://{SQUID}/org.tangled.knot.member/{POINTER_RKEY}"), "cid": POINTER_CID, }); Ok(ok(Bytes::from(serde_json::to_vec(&receipt).unwrap()))) @@ -2330,7 +2355,7 @@ mod tests { .unwrap(); assert_eq!( receipt.uri.as_str(), - format!("at://{SQUID}/sh.tangled.knot.member/{POINTER_RKEY}") + format!("at://{SQUID}/org.tangled.knot.member/{POINTER_RKEY}") ); assert_eq!(receipt.cid.as_str(), POINTER_CID); } diff --git a/knot2/crates/knot-atproto/src/pointer.rs b/knot2/crates/knot-atproto/src/pointer.rs index 984dffac7..aa93bd1d2 100644 --- a/knot2/crates/knot-atproto/src/pointer.rs +++ b/knot2/crates/knot-atproto/src/pointer.rs @@ -132,13 +132,13 @@ mod tests { #[test] fn a_receipt_round_trips_its_uri_and_cid() { let body = serde_json::json!({ - "uri": "at://did:plc:squid/sh.tangled.knot.member/3jzfcijpj2z2a", + "uri": "at://did:plc:squid/org.tangled.knot.member/3jzfcijpj2z2a", "cid": "bafyreidfayvfuwqa7qlnopdjiqrxzs6blmoeu4rujcjtnci5beludirz2a" }); let receipt = receipt_from_response(&serde_json::to_vec(&body).unwrap()).unwrap(); assert_eq!( receipt.uri.as_str(), - "at://did:plc:squid/sh.tangled.knot.member/3jzfcijpj2z2a" + "at://did:plc:squid/org.tangled.knot.member/3jzfcijpj2z2a" ); assert_eq!( receipt.cid.as_str(), @@ -157,7 +157,7 @@ mod tests { receipt_from_response(&serde_json::to_vec(&bad_uri).unwrap()), Err(AtprotoError::MalformedReceipt(_)) )); - let bad_cid = serde_json::json!({ "uri": "at://did:plc:squid/sh.tangled.knot.member/3jzfcijpj2z2a", "cid": "not-a-cid" }); + let bad_cid = serde_json::json!({ "uri": "at://did:plc:squid/org.tangled.knot.member/3jzfcijpj2z2a", "cid": "not-a-cid" }); assert!(matches!( receipt_from_response(&serde_json::to_vec(&bad_cid).unwrap()), Err(AtprotoError::MalformedReceipt(_)) diff --git a/knot2/crates/knot-atproto/src/test_support.rs b/knot2/crates/knot-atproto/src/test_support.rs index 52cab8838..1eede01da 100644 --- a/knot2/crates/knot-atproto/src/test_support.rs +++ b/knot2/crates/knot-atproto/src/test_support.rs @@ -17,7 +17,7 @@ use crate::{MintNonce, ServiceJwt}; pub(crate) const SQUID: &str = "did:plc:squid"; pub(crate) const LIMPET: &str = "did:plc:limpet"; pub(crate) const KNOT: &str = "did:web:nel.pet"; -pub(crate) const METHOD: &str = "sh.tangled.knot.addMember"; +pub(crate) const METHOD: &str = "org.tangled.knot.member"; pub(crate) fn did(value: &str) -> AccountDid { AccountDid::new(value).unwrap() @@ -182,11 +182,9 @@ pub(crate) fn repo_nonce(seed: u64) -> MintNonce { ) } -pub(crate) fn member_pointer() -> knot_lexicons::sh_tangled::knot::member::Member { - knot_lexicons::sh_tangled::knot::member::Member { +pub(crate) fn member_pointer() -> knot_lexicons::org_tangled::knot::member::Member { + knot_lexicons::org_tangled::knot::member::Member { created_at: knot_types::Datetime::raw_str("2026-06-11T00:00:00Z"), - domain: "knot.nel.pet".into(), - subject: knot_types::Did::new_owned("did:plc:lyna").unwrap(), extra_data: None, } } diff --git a/knot2/crates/knot-cob/src/backend.rs b/knot2/crates/knot-cob/src/backend.rs index b11e16175..7848237c8 100644 --- a/knot2/crates/knot-cob/src/backend.rs +++ b/knot2/crates/knot-cob/src/backend.rs @@ -100,6 +100,28 @@ pub(crate) fn list_cobs(repo: &Repo) -> Result, CobError> .collect()) } +pub fn migrate_legacy_refs(repo: &Repo) -> Result { + let renamed = repo + .references()? + .into_iter() + .filter_map(|record| { + let refname = record.name.as_str(); + let modern = [COBS_PREFIX, CHECKPOINTS_PREFIX] + .into_iter() + .find_map(|prefix| { + let rest = refname.strip_prefix(prefix)?; + let (legacy, oid) = rest.rsplit_once('/')?; + let modern = crate::modernized_type_of(&TypeName::new(legacy).ok()?)?; + RefName::new(format!("{prefix}{}/{}", modern.as_str(), oid)).ok() + })?; + Some((record.name, modern, record.target)) + }) + .collect::>(); + let count = renamed.len(); + repo.rename_references(renamed).map_err(CobError::from)?; + Ok(count) +} + #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub(crate) enum Blob { Payload, diff --git a/knot2/crates/knot-cob/src/change.rs b/knot2/crates/knot-cob/src/change.rs index c513356ae..1a2c278c2 100644 --- a/knot2/crates/knot-cob/src/change.rs +++ b/knot2/crates/knot-cob/src/change.rs @@ -143,7 +143,19 @@ pub fn decode_payload(bytes: &[u8]) -> Result &'static str { + self.0 + } +} #[derive(Serialize)] struct SignedChange<'a> { @@ -169,9 +181,32 @@ pub(crate) fn signing_bytes( author: &ActorId, timestamp: UnixSeconds, object: Option, +) -> Vec { + signing_bytes_in( + SIGNING_CONTEXT, + home, + revision, + parents, + type_name, + author, + timestamp, + object, + ) +} + +#[allow(clippy::too_many_arguments)] +fn signing_bytes_in( + context: SigningContext, + home: &CobHome, + revision: Oid, + parents: &[ChangeId], + type_name: &TypeName, + author: &ActorId, + timestamp: UnixSeconds, + object: Option, ) -> Vec { let view = SignedChange { - context: SIGNING_CONTEXT, + context: context.as_str(), home_kind: home.kind(), home: home.as_str(), revision, @@ -217,9 +252,23 @@ pub(crate) fn verify_signature( timestamp, object_binding(parents, object), ); - VerifyingKey::from(&verifying) - .verify(&message, &signature) - .is_ok() + let verifying = VerifyingKey::from(&verifying); + verifying.verify(&message, &signature).is_ok() + || verifying + .verify( + &signing_bytes_in( + SigningContext::LEGACY, + home, + revision, + parents, + type_name, + author, + timestamp, + object_binding(parents, object), + ), + &signature, + ) + .is_ok() } #[derive(Debug, Clone, PartialEq, Eq)] @@ -266,7 +315,7 @@ mod tests { use super::*; fn type_name() -> TypeName { - TypeName::new("sh.tangled.test.tag").unwrap() + TypeName::new("org.tangled.test.tag").unwrap() } fn cob_home() -> CobHome { @@ -303,6 +352,54 @@ mod tests { } } + #[test] + fn a_change_signed_under_the_legacy_context_still_verifies() { + let signer = K256Signer::generate(&SeededEntropy::new(21)); + let author = ActorId::from_secp256k1(signer.public_key().as_bytes()); + let revision = Oid::from_hex("7777777777777777777777777777777777777777").unwrap(); + let timestamp = UnixSeconds::new(1); + let legacy_type = TypeName::new("sh.tangled.knot.member").unwrap(); + let bytes = signing_bytes_in( + SigningContext::LEGACY, + &cob_home(), + revision, + &[], + &legacy_type, + &author, + timestamp, + None, + ); + let signature = signer.sign(&bytes); + assert!( + verify_signature( + &cob_home(), + revision, + &[], + &legacy_type, + &author, + timestamp, + None, + signature.as_bytes() + ), + "a change the previous release signed still verifies" + ); + let mut tampered = signature.as_bytes().to_vec(); + tampered[0] ^= 0xff; + assert!( + !verify_signature( + &cob_home(), + revision, + &[], + &legacy_type, + &author, + timestamp, + None, + &tampered + ), + "the legacy context is an accepted signing domain, not a bypass" + ); + } + #[test] fn signing_bytes_stay_byte_stable() { let author = { @@ -328,7 +425,7 @@ mod tests { ); assert_eq!( knot_types::lowercase_hex(&bytes), - "a964686f6d656d6469643a706c633a737175696466617574686f7278317a513373684e317652664257527847397234564c3251796466474e675955715a5a385836743971535359774b636a644a50666f626a65637478283333333333333333333333333333333333333333333333333333333333333333333333333333333367636f6e74657874781d73682e74616e676c65642e6b6e6f742e636f622e6368616e67652e763167706172656e74738178283232323232323232323232323232323232323232323232323232323232323232323232323232323268686f6d654b696e64647265706f687265766973696f6e78283131313131313131313131313131313131313131313131313131313131313131313131313131313168747970654e616d657373682e74616e676c65642e746573742e7461676974696d657374616d701a6553f100" + "a964686f6d656d6469643a706c633a737175696466617574686f7278317a513373684e317652664257527847397234564c3251796466474e675955715a5a385836743971535359774b636a644a50666f626a65637478283333333333333333333333333333333333333333333333333333333333333333333333333333333367636f6e74657874781e6f72672e74616e676c65642e6b6e6f742e636f622e6368616e67652e763167706172656e74738178283232323232323232323232323232323232323232323232323232323232323232323232323232323268686f6d654b696e64647265706f687265766973696f6e78283131313131313131313131313131313131313131313131313131313131313131313131313131313168747970654e616d65746f72672e74616e676c65642e746573742e7461676974696d657374616d701a6553f100" ); } diff --git a/knot2/crates/knot-cob/src/lib.rs b/knot2/crates/knot-cob/src/lib.rs index 56ede26b6..6fff630b4 100644 --- a/knot2/crates/knot-cob/src/lib.rs +++ b/knot2/crates/knot-cob/src/lib.rs @@ -15,7 +15,38 @@ pub use object::{ SnapshotStride, StateSize, WriteContext, }; -pub use backend::{CHECKPOINTS_PREFIX, parse_cob_ref}; +pub use backend::{CHECKPOINTS_PREFIX, migrate_legacy_refs, parse_cob_ref}; + +const LEGACY_TYPE_ALIASES: [(&str, &str); 4] = [ + ("sh.tangled.knot.member", "org.tangled.knot.member"), + ("sh.tangled.knot.block", "org.tangled.knot.block"), + ( + "sh.tangled.knot.repoRegistry", + "org.tangled.knot.repoRegistry", + ), + ( + "sh.tangled.repo.collaborator", + "org.tangled.graph.collaborator", + ), +]; + +pub fn legacy_type_of(modern: &TypeName) -> Option { + LEGACY_TYPE_ALIASES + .iter() + .find(|(_, modern_name)| modern.as_str() == *modern_name) + .and_then(|(legacy, _)| TypeName::new(*legacy).ok()) +} + +pub fn modernized_type_of(legacy: &TypeName) -> Option { + LEGACY_TYPE_ALIASES + .iter() + .find(|(legacy_name, _)| legacy.as_str() == *legacy_name) + .and_then(|(_, modern)| TypeName::new(*modern).ok()) +} + +pub fn accepts_change_type(expected: &TypeName, found: &TypeName) -> bool { + found == expected || legacy_type_of(expected).is_some_and(|legacy| &legacy == found) +} use std::collections::BTreeSet; @@ -667,7 +698,7 @@ impl<'r> CobStore<'r> { backend::MAX_GRAPH_CHANGES, )?; graph.into_ordered().into_iter().try_for_each(|change| { - if change.type_name != type_name { + if !accepts_change_type(&type_name, &change.type_name) { return Err(CobError::UnexpectedChangeType { change: change.id, expected: type_name.clone(), @@ -851,7 +882,8 @@ impl<'r> CobStore<'r> { Some((checkpoint_tip, state)) => { match self.changes_since::(object, Some(checkpoint_tip)) { Ok(delta) => { - let applied = object::apply_changes::(state, &delta.changes, &type_name)?; + let applied = + object::apply_changes::(state, &delta.changes, &type_name)?; Ok((applied, tip, delta.changes.len())) } Err( @@ -940,14 +972,15 @@ mod tests { use knot_git::{Layout, Repo}; use knot_runtime::{K256Signer, SeededEntropy, Signature, Signer}; use knot_types::{ - Blocked, ContributionPermission, ContributionPolicy, RecordBody, RecordCid, RepoDid, - RepoRole, UnixSeconds, + Blocked, ContributionPermission, ContributionPolicy, RecordBody, RecordCid, RefName, + RepoDid, RepoRole, UnixSeconds, }; use proptest::prelude::*; use serde::{Deserialize, Serialize}; use tempfile::TempDir; use super::*; + use crate::backend::{checkpoint_ref_name, cob_ref_name}; #[derive(Debug, Serialize, Deserialize)] #[serde(tag = "op", content = "subject")] @@ -957,7 +990,7 @@ mod tests { } impl ChangePayload for Tag { - const TYPE: &'static str = "sh.tangled.test.tag"; + const TYPE: &'static str = "org.tangled.test.tag"; } impl KnotAuthored for Tag {} @@ -1008,7 +1041,7 @@ mod tests { } impl ChangePayload for NameChange { - const TYPE: &'static str = "sh.tangled.test.name"; + const TYPE: &'static str = "org.tangled.test.name"; } impl KnotAuthored for NameChange {} @@ -1865,7 +1898,7 @@ mod tests { assert_eq!(parse_cob_ref(name.as_str()), Some((nsid, object))); assert_eq!(parse_cob_ref("refs/heads/main"), None); assert_eq!( - parse_cob_ref("refs/cobs/sh.tangled.test.tag/not-an-oid"), + parse_cob_ref("refs/cobs/org.tangled.test.tag/not-an-oid"), None ); } @@ -2251,7 +2284,7 @@ mod tests { let (_other_dir, other_repo) = fixture(); let key = signer(53); - let foreign = TypeName::new("sh.tangled.test.other").unwrap(); + let foreign = TypeName::new("org.tangled.test.other").unwrap(); let root = tag_root(&other_repo, &key, "nel", at(1)); let object = CobId::new(root.oid()); let child = backend::write_change( @@ -2405,7 +2438,7 @@ mod tests { } impl ChangePayload for NoteChange { - const TYPE: &'static str = "sh.tangled.test.note"; + const TYPE: &'static str = "org.tangled.test.note"; fn address(&self) -> Option { match self { @@ -2510,7 +2543,7 @@ mod tests { fn note_address(rkey: &str) -> knot_types::RecordAddress { knot_types::RecordAddress::new( - knot_types::RecordCollection::new("sh.tangled.test.note").unwrap(), + knot_types::RecordCollection::new("org.tangled.test.note").unwrap(), knot_types::RecordRkey::new(rkey).unwrap(), ) } @@ -3193,4 +3226,56 @@ mod tests { "Replay of root whose object has moved on conflicts, got {grown:?}" ); } + + #[test] + fn the_alias_table_maps_both_ways() { + let legacy = TypeName::new("sh.tangled.repo.collaborator").unwrap(); + let modern = TypeName::new("org.tangled.graph.collaborator").unwrap(); + assert_eq!(modernized_type_of(&legacy), Some(modern.clone())); + assert_eq!(legacy_type_of(&modern), Some(legacy.clone())); + assert!(accepts_change_type(&modern, &legacy)); + assert!(accepts_change_type(&modern, &modern)); + let stranger = TypeName::new("org.tangled.feed.comment").unwrap(); + assert!(!accepts_change_type(&modern, &stranger)); + assert_eq!(legacy_type_of(&stranger), None); + assert_eq!(modernized_type_of(&stranger), None); + } + + #[test] + fn legacy_cob_refs_rename_to_their_modern_type_once() { + let (dir, repo) = fixture(); + let object = CobId::new(Oid::from_hex("4444444444444444444444444444444444444444").unwrap()); + let legacy = RefName::new(format!( + "refs/cobs/sh.tangled.knot.member/{}", + object.oid().to_hex() + )) + .unwrap(); + let tip = Oid::from_hex("5555555555555555555555555555555555555555").unwrap(); + repo.set_reference(&legacy, tip).unwrap(); + let legacy_checkpoint = RefName::new(format!( + "refs/cob-checkpoints/sh.tangled.repo.collaborator/{}", + object.oid().to_hex() + )) + .unwrap(); + repo.set_reference(&legacy_checkpoint, tip).unwrap(); + + assert_eq!(migrate_legacy_refs(&repo).unwrap(), 2); + let modern = + cob_ref_name(&TypeName::new("org.tangled.knot.member").unwrap(), object).unwrap(); + assert_eq!(repo.find_ref(&modern).unwrap(), Some(tip)); + assert_eq!(repo.find_ref(&legacy).unwrap(), None); + let modern_checkpoint = checkpoint_ref_name( + &TypeName::new("org.tangled.graph.collaborator").unwrap(), + object, + ) + .unwrap(); + assert_eq!(repo.find_ref(&modern_checkpoint).unwrap(), Some(tip)); + assert_eq!(repo.find_ref(&legacy_checkpoint).unwrap(), None); + assert_eq!( + migrate_legacy_refs(&repo).unwrap(), + 0, + "a second sweep finds no refs left to rename" + ); + drop(dir); + } } diff --git a/knot2/crates/knot-cob/src/object.rs b/knot2/crates/knot-cob/src/object.rs index 15d7905ed..15299bad5 100644 --- a/knot2/crates/knot-cob/src/object.rs +++ b/knot2/crates/knot-cob/src/object.rs @@ -93,7 +93,7 @@ pub(crate) fn apply_changes( expected: &TypeName, ) -> Result { changes.iter().try_fold(state, |state, change| { - if change.type_name != *expected { + if !crate::accepts_change_type(expected, &change.type_name) { return Err(CobError::UnexpectedChangeType { change: change.id, expected: expected.clone(), diff --git a/knot2/crates/knot-cobs/src/blocklist.rs b/knot2/crates/knot-cobs/src/blocklist.rs index d8067ed36..61c73ed39 100644 --- a/knot2/crates/knot-cobs/src/blocklist.rs +++ b/knot2/crates/knot-cobs/src/blocklist.rs @@ -5,5 +5,5 @@ grant_cob! { ops = { Add(Grant), Remove(Removal) }, cob = BlocklistCob, state = Blocklist, - type_name = "sh.tangled.knot.block", + type_name = "org.tangled.knot.block", } diff --git a/knot2/crates/knot-cobs/src/collaborators.rs b/knot2/crates/knot-cobs/src/collaborators.rs index 01c3a8cad..4e5e18b88 100644 --- a/knot2/crates/knot-cobs/src/collaborators.rs +++ b/knot2/crates/knot-cobs/src/collaborators.rs @@ -5,5 +5,5 @@ grant_cob! { ops = { Add(Grant), Invite(Invite), Accept(Accept), Remove(Removal) }, cob = CollaboratorsCob, state = Collaborators, - type_name = "sh.tangled.repo.collaborator", + type_name = "org.tangled.graph.collaborator", } diff --git a/knot2/crates/knot-cobs/src/import.rs b/knot2/crates/knot-cobs/src/import.rs index 87e1f0a7b..c04406b49 100644 --- a/knot2/crates/knot-cobs/src/import.rs +++ b/knot2/crates/knot-cobs/src/import.rs @@ -49,6 +49,7 @@ pub fn verify_cob_ref( ) -> Result { let (type_name, object) = parse_cob_ref(refname.as_str()) .ok_or_else(|| ImportError::NotCobRef(refname.as_str().to_string()))?; + let type_name = knot_cob::modernized_type_of(&type_name).unwrap_or(type_name); let verifier = verifier_for(&type_name).ok_or(ImportError::UnknownType(type_name))?; verifier(store, home, object, owner)?; Ok(object) diff --git a/knot2/crates/knot-cobs/src/members.rs b/knot2/crates/knot-cobs/src/members.rs index 410294ce5..bee1ba781 100644 --- a/knot2/crates/knot-cobs/src/members.rs +++ b/knot2/crates/knot-cobs/src/members.rs @@ -5,5 +5,5 @@ grant_cob! { ops = { Add(Grant), Invite(Invite), Accept(Accept), Remove(Removal) }, cob = MembersCob, state = Members, - type_name = "sh.tangled.knot.member", + type_name = "org.tangled.knot.member", } diff --git a/knot2/crates/knot-cobs/src/registry.rs b/knot2/crates/knot-cobs/src/registry.rs index 2afb67e91..ff9acec81 100644 --- a/knot2/crates/knot-cobs/src/registry.rs +++ b/knot2/crates/knot-cobs/src/registry.rs @@ -89,7 +89,7 @@ pub enum RegistryChange { } impl ChangePayload for RegistryChange { - const TYPE: &'static str = "sh.tangled.knot.repoRegistry"; + const TYPE: &'static str = "org.tangled.knot.repoRegistry"; } impl KnotAuthored for RegistryChange {} diff --git a/knot2/crates/knot-cobs/src/social.rs b/knot2/crates/knot-cobs/src/social.rs index e16b30063..6ab6692b2 100644 --- a/knot2/crates/knot-cobs/src/social.rs +++ b/knot2/crates/knot-cobs/src/social.rs @@ -502,7 +502,7 @@ mod tests { body = NoteBody, change = NoteChange, cob = NoteCob, - collection = "sh.tangled.test.note", + collection = "org.tangled.test.note", opening = Opening::Contribution, } diff --git a/knot2/crates/knot-cobs/tests/cobs.rs b/knot2/crates/knot-cobs/tests/cobs.rs index 55ba2456a..5bc0d9caa 100644 --- a/knot2/crates/knot-cobs/tests/cobs.rs +++ b/knot2/crates/knot-cobs/tests/cobs.rs @@ -150,7 +150,7 @@ fn collaborators_live_in_a_per_repo_cob_namespace() { record .name .as_str() - .contains("sh.tangled.repo.collaborator") + .contains("org.tangled.graph.collaborator") }); assert!(cob_ref.is_some(), "collaborators live under refs/cobs"); assert!(repo.advertised_refs().unwrap().is_empty()); diff --git a/knot2/crates/knot-cobs/tests/invariants.rs b/knot2/crates/knot-cobs/tests/invariants.rs index 7d1a7bd37..5397b70db 100644 --- a/knot2/crates/knot-cobs/tests/invariants.rs +++ b/knot2/crates/knot-cobs/tests/invariants.rs @@ -453,7 +453,7 @@ fn verify_cob_ref_boundary_cases() { Err(ImportError::NotCobRef(_)) )); - let stray = cob_ref(&TypeName::new("sh.tangled.test.unknown").unwrap(), object); + let stray = cob_ref(&TypeName::new("org.tangled.test.unknown").unwrap(), object); assert!(matches!( verify_cob_ref(&store, &home(), &stray, &owner), Err(ImportError::UnknownType(_)) diff --git a/knot2/crates/knot-cobs/tests/social.rs b/knot2/crates/knot-cobs/tests/social.rs index cde9fe0a6..4f7137cd1 100644 --- a/knot2/crates/knot-cobs/tests/social.rs +++ b/knot2/crates/knot-cobs/tests/social.rs @@ -21,7 +21,7 @@ social_cob! { body = NoteBody, change = NoteChange, cob = NoteCob, - collection = "sh.tangled.test.note", + collection = "org.tangled.test.note", opening = knot_cobs::Opening::Contribution, } @@ -31,7 +31,7 @@ fn squid() -> RepoDid { fn address() -> RecordAddress { RecordAddress::new( - RecordCollection::new("sh.tangled.test.note").unwrap(), + RecordCollection::new("org.tangled.test.note").unwrap(), RecordRkey::new("3lubrptx57d22").unwrap(), ) } diff --git a/knot2/crates/knot-config/src/lib.rs b/knot2/crates/knot-config/src/lib.rs index be543f689..aaab5580f 100644 --- a/knot2/crates/knot-config/src/lib.rs +++ b/knot2/crates/knot-config/src/lib.rs @@ -259,7 +259,7 @@ pub struct XrpcConfig { pub max_response_bytes: u64, /// Upper bound on bytes that a single archive spools, - /// across all our surfaces: the sh.tangled.repo.archive query, + /// across all our surfaces: the org.tangled.git.getArchive query, /// `git archive --remote` over SSH, /// and the smart HTTP archive route. /// The knot will refuse writing smth that would blast an archive past this bound. diff --git a/knot2/crates/knot-git/src/reads.rs b/knot2/crates/knot-git/src/reads.rs index ea832fcb9..63b8e6706 100644 --- a/knot2/crates/knot-git/src/reads.rs +++ b/knot2/crates/knot-git/src/reads.rs @@ -297,6 +297,43 @@ impl Repo { ) } + pub fn log_window_bounded( + &self, + start: Oid, + skip: LogSkip, + limit: LogLimit, + ) -> Result, GitError> { + self.walk_from(start, None)? + .enumerate() + .filter(|(index, _)| *index >= skip.get()) + .take(limit.get()) + .map(|(_, oid)| oid.and_then(|oid| self.find_commit(oid))) + .collect() + } + + pub fn log_past_bounded( + &self, + start: Oid, + past: Oid, + limit: LogLimit, + ) -> Result>, GitError> { + let mut found = false; + let mut window = Vec::new(); + for oid in self.walk_from(start, None)? { + let oid = oid?; + if found { + if window.len() < limit.get() { + window.push(self.find_commit(oid)?); + } else { + break; + } + } else if oid == past { + found = true; + } + } + Ok(found.then_some(window)) + } + pub fn merge_base(&self, one: Oid, two: Oid) -> Result, GitError> { use gix::repository::merge_base::Error; match self.git().merge_base(one.object_id(), two.object_id()) { diff --git a/knot2/crates/knot-git/src/replay.rs b/knot2/crates/knot-git/src/replay.rs index 16300a8ab..ddccc43ef 100644 --- a/knot2/crates/knot-git/src/replay.rs +++ b/knot2/crates/knot-git/src/replay.rs @@ -1,6 +1,6 @@ use std::collections::{HashMap, VecDeque}; -use knot_types::Oid; +use knot_types::{Oid, RepoPath}; use crate::error::GitError; use crate::objects::{CommitRange, FileChange, Identity}; @@ -12,9 +12,9 @@ pub enum ReplayError { #[error("the commit history has a cycle the replay can't order")] Cycle, #[error("{path} changed on the target branch too")] - Conflict { path: String }, + Conflict { path: RepoPath }, #[error("{path} went missing from its own commit between two reads")] - Vanished { path: String }, + Vanished { path: RepoPath }, #[error(transparent)] Git(#[from] GitError), } @@ -130,7 +130,7 @@ fn restage( let kind = repo .entry_at(commit, &path)? .ok_or_else(|| ReplayError::Vanished { - path: path.as_str().to_owned(), + path: path.clone(), })? .kind; StagedChange { @@ -144,9 +144,7 @@ fn restage( }, }), _ => { - return Err(ReplayError::Conflict { - path: path.as_str().to_owned(), - }); + return Err(ReplayError::Conflict { path }); } }; Ok(staged) diff --git a/knot2/crates/knot-git/src/repo.rs b/knot2/crates/knot-git/src/repo.rs index 28c4e9c42..3f5068385 100644 --- a/knot2/crates/knot-git/src/repo.rs +++ b/knot2/crates/knot-git/src/repo.rs @@ -551,6 +551,21 @@ impl Repo { Ok(()) } + pub fn rename_references( + &self, + renames: impl IntoIterator, + ) -> Result<(), GitError> { + renames + .into_iter() + .try_for_each(|(legacy, modern, target)| { + self.set_reference(&modern, target)?; + self.update_ref(&RefUpdate::Delete { + name: legacy, + old: target, + }) + }) + } + pub fn references(&self) -> Result, GitError> { self.git .references() @@ -1686,7 +1701,7 @@ mod tests { ); repo.update_ref(&RefUpdate::Create { - name: RefName::new("refs/cobs/sh.tangled.repo.collaborator/limpet").unwrap(), + name: RefName::new("refs/cobs/org.tangled.graph.collaborator/limpet").unwrap(), new: oid(B), }) .unwrap(); @@ -1749,7 +1764,7 @@ mod tests { ("refs/atproto/commit", RefClass::Atproto), ("refs/atproto-mirror/commit", RefClass::Public), ("refs/cobs", RefClass::Cob), - ("refs/cobs/sh.tangled.knot.member/beef", RefClass::Cob), + ("refs/cobs/org.tangled.knot.member/beef", RefClass::Cob), ("refs/cob-checkpoints", RefClass::Checkpoint), ("refs/cob-checkpoints/main", RefClass::Checkpoint), ("refs/hidden", RefClass::Hidden), diff --git a/knot2/crates/knot-git/tests/reads.rs b/knot2/crates/knot-git/tests/reads.rs index 672b46d86..33d56c079 100644 --- a/knot2/crates/knot-git/tests/reads.rs +++ b/knot2/crates/knot-git/tests/reads.rs @@ -507,7 +507,7 @@ fn reachable_from_public_excludes_cob_only_commits() { "push", "-q", bare_path.to_str().unwrap(), - "HEAD:refs/cobs/sh.tangled.repo.collaborator/secret", + "HEAD:refs/cobs/org.tangled.graph.collaborator/secret", ], ); diff --git a/knot2/crates/knot-index/src/lib.rs b/knot2/crates/knot-index/src/lib.rs index c3b626482..ff1ef3081 100644 --- a/knot2/crates/knot-index/src/lib.rs +++ b/knot2/crates/knot-index/src/lib.rs @@ -521,7 +521,11 @@ impl Index { }) } - fn materialize_collaborators(&self, repo: &RepoDid, repo_key: RepoKey) -> Result<(), IndexError> { + fn materialize_collaborators( + &self, + repo: &RepoDid, + repo_key: RepoKey, + ) -> Result<(), IndexError> { let git = self.layout.open(repo)?; let store = CobStore::new(&git); match store.list::()?.as_slice() { @@ -697,6 +701,10 @@ impl Index { self.blocklist.holds(&self.interner, did) } + pub fn blocklist_entries(&self) -> Resolved> { + self.blocklist.entries(&self.interner) + } + pub fn blocked(&self) -> Resolved> { self.blocklist.subjects(&self.interner) } diff --git a/knot2/crates/knot-index/tests/common/mod.rs b/knot2/crates/knot-index/tests/common/mod.rs index 1669b5877..676a3be16 100644 --- a/knot2/crates/knot-index/tests/common/mod.rs +++ b/knot2/crates/knot-index/tests/common/mod.rs @@ -562,7 +562,7 @@ knot_cobs::social_cob! { body = NoteBody, change = NoteChange, cob = NoteCob, - collection = "sh.tangled.test.note", + collection = "org.tangled.test.note", opening = knot_cobs::Opening::Contribution, } diff --git a/knot2/crates/knot-index/tests/lifecycle.rs b/knot2/crates/knot-index/tests/lifecycle.rs index e91cc7575..5590af341 100644 --- a/knot2/crates/knot-index/tests/lifecycle.rs +++ b/knot2/crates/knot-index/tests/lifecycle.rs @@ -345,9 +345,9 @@ fn a_repo_missing_on_disk_does_not_fail_the_boot_and_isolates_its_materializatio world.seed_collaborator(&present, "lyna"); let index = world.index(); - index - .rebuild() - .expect("boot materializes members and registry only, so a missing repo dir never fails it"); + index.rebuild().expect( + "boot materializes members and registry only, so a missing repo dir never fails it", + ); index.ensure_collaborators(&present).unwrap(); assert_eq!( diff --git a/knot2/crates/knot-index/tests/projections.rs b/knot2/crates/knot-index/tests/projections.rs index 71d3ff726..e52d1884e 100644 --- a/knot2/crates/knot-index/tests/projections.rs +++ b/knot2/crates/knot-index/tests/projections.rs @@ -28,7 +28,7 @@ enum BadMembers { Explode(u8), } impl ChangePayload for BadMembers { - const TYPE: &'static str = "sh.tangled.knot.member"; + const TYPE: &'static str = "org.tangled.knot.member"; } impl KnotAuthored for BadMembers {} @@ -768,7 +768,9 @@ fn an_unmaterialized_repo_keeps_the_pass_partial_and_an_unopenable_repo_never_gr index.refresh_collaborators(&created).unwrap(); let Resolved::Ready(work) = index.keys().work(at(0), sweep()) else { - panic!("One repo the knot hasn't materialized mustn't stop it filling the keys it can enumerate"); + panic!( + "One repo the knot hasn't materialized mustn't stop it filling the keys it can enumerate" + ); }; assert_eq!( work.hosted, @@ -2289,7 +2291,7 @@ enum BadNote { Explode(u8), } impl ChangePayload for BadNote { - const TYPE: &'static str = "sh.tangled.test.note"; + const TYPE: &'static str = "org.tangled.test.note"; } impl KnotAuthored for BadNote {} diff --git a/knot2/crates/knot-lfs/src/gc.rs b/knot2/crates/knot-lfs/src/gc.rs index d37336522..b55210711 100644 --- a/knot2/crates/knot-lfs/src/gc.rs +++ b/knot2/crates/knot-lfs/src/gc.rs @@ -289,7 +289,7 @@ mod tests { let f = fixture(); let (cob, cob_size) = put_media(&f, b"media under a collaborative object"); - commit_pointer(&f, "refs/cobs/sh.tangled.knot.member/beef", &cob, cob_size); + commit_pointer(&f, "refs/cobs/org.tangled.knot.member/beef", &cob, cob_size); age(&f, &cob, MONTHS); let (chain, chain_size) = put_media(&f, b"media under the atproto commit chain"); @@ -299,7 +299,7 @@ mod tests { let (snapshot, snapshot_size) = put_media(&f, b"media under a cob checkpoint"); commit_pointer( &f, - "refs/cob-checkpoints/sh.tangled.knot.member/beef", + "refs/cob-checkpoints/org.tangled.knot.member/beef", &snapshot, snapshot_size, ); diff --git a/knot2/crates/knot-pack/tests/git_client.rs b/knot2/crates/knot-pack/tests/git_client.rs index 478349f4d..eee5a63aa 100644 --- a/knot2/crates/knot-pack/tests/git_client.rs +++ b/knot2/crates/knot-pack/tests/git_client.rs @@ -504,7 +504,7 @@ async fn http_upload_archive_serves_a_framed_tar_and_guards_refuse_cob_raw_oids_ ); repo.update_ref(&RefUpdate::Create { - name: RefName::new("refs/cobs/sh.tangled.repo.collaborator/secret").unwrap(), + name: RefName::new("refs/cobs/org.tangled.graph.collaborator/secret").unwrap(), new: head, }) .unwrap(); @@ -515,7 +515,7 @@ async fn http_upload_archive_serves_a_framed_tar_and_guards_refuse_cob_raw_oids_ .unwrap(); let cob = archive(&[ "argument --format=tar\n", - "argument refs/cobs/sh.tangled.repo.collaborator/secret\n", + "argument refs/cobs/org.tangled.graph.collaborator/secret\n", ]); assert!( String::from_utf8_lossy(&cob).contains("NACK"), diff --git a/knot2/crates/knot-pack/tests/hardening.rs b/knot2/crates/knot-pack/tests/hardening.rs index 24d6b64d0..52a53967b 100644 --- a/knot2/crates/knot-pack/tests/hardening.rs +++ b/knot2/crates/knot-pack/tests/hardening.rs @@ -145,7 +145,7 @@ fn ls_refs_prefix_and_cob_hiding() { ) .unwrap(); bare.update_ref(&RefUpdate::Create { - name: RefName::new("refs/cobs/sh.tangled.repo.collaborator/secret").unwrap(), + name: RefName::new("refs/cobs/org.tangled.graph.collaborator/secret").unwrap(), new: Oid::from_hex(&c1).unwrap(), }) .unwrap(); @@ -198,8 +198,8 @@ fn push_namespace_gating_accepts_only_unreserved_refs() { let cases: [(&str, &str, bool); 7] = [ ( - "refs/cobs/sh.tangled.repo.collaborator/evil", - "ng refs/cobs/sh.tangled.repo.collaborator/evil", + "refs/cobs/org.tangled.graph.collaborator/evil", + "ng refs/cobs/org.tangled.graph.collaborator/evil", false, ), ( @@ -210,8 +210,8 @@ fn push_namespace_gating_accepts_only_unreserved_refs() { ("refs/atproto/commit", "ng refs/atproto/commit", false), ("refs/atproto", "ng refs/atproto", false), ( - "refs/cob-checkpoints/sh.tangled.knot.member/beef", - "ng refs/cob-checkpoints/sh.tangled.knot.member/beef", + "refs/cob-checkpoints/org.tangled.knot.member/beef", + "ng refs/cob-checkpoints/org.tangled.knot.member/beef", false, ), ("refs/cob-checkpoints", "ng refs/cob-checkpoints", false), diff --git a/knot2/crates/knot-pack/tests/quarantine.rs b/knot2/crates/knot-pack/tests/quarantine.rs index f03bee81e..995614c07 100644 --- a/knot2/crates/knot-pack/tests/quarantine.rs +++ b/knot2/crates/knot-pack/tests/quarantine.rs @@ -257,7 +257,7 @@ fn a_cob_ref_verifies_against_the_owner_key_and_is_refused_for_a_stranger() { .unwrap(); let tip = created.tip.oid().to_hex(); let refname = format!( - "refs/cobs/sh.tangled.knot.member/{}", + "refs/cobs/org.tangled.knot.member/{}", created.object.oid().to_hex() ); @@ -327,7 +327,7 @@ fn a_reserved_ref_update_is_refused_even_when_the_guard_allows_it() { let did = RepoDid::new("did:plc:squid").unwrap(); let bare = layout.create(&did).unwrap(); - let cob_ref = format!("refs/cobs/sh.tangled.knot.member/{}", "a".repeat(40)); + let cob_ref = format!("refs/cobs/org.tangled.knot.member/{}", "a".repeat(40)); let old = "1".repeat(40); let new = "2".repeat(40); let report = receive_pack_guarded( @@ -391,7 +391,7 @@ fn the_push_guard_allows_a_branch_and_names_the_namespace_it_refuses() { [ ("refs/atproto/commit", "atproto commit chain"), ( - "refs/cob-checkpoints/sh.tangled.knot.member/beef", + "refs/cob-checkpoints/org.tangled.knot.member/beef", "knot-written collaborative-object snapshots", ), ("refs/hidden/feature/main", "server-side fork staging"), diff --git a/knot2/crates/knot-record/src/attachment.rs b/knot2/crates/knot-record/src/attachment.rs index c0d3a9e87..8f54930e0 100644 --- a/knot2/crates/knot-record/src/attachment.rs +++ b/knot2/crates/knot-record/src/attachment.rs @@ -503,7 +503,7 @@ mod tests { 12, ), (&[b'B', b'M', 1, 2, 3, 4], "image/bmp", 6), - (b"\n\n knot_types::RecordCollection { knot_types::RecordCollection::new(COLLECTION).unwrap() diff --git a/knot2/crates/knot-record/src/git_ref.rs b/knot2/crates/knot-record/src/git_ref.rs deleted file mode 100644 index 372e47f88..000000000 --- a/knot2/crates/knot-record/src/git_ref.rs +++ /dev/null @@ -1,374 +0,0 @@ -use std::collections::{BTreeMap, BTreeSet}; -use std::sync::Arc; - -use crate::blocks::GitBlocks; -use crate::chain::{self, ChainError}; -use crate::{dag_cbor, encoded}; -use futures::StreamExt; -use futures::TryStreamExt as _; -use jacquard_repo::storage::BlockStore; -use knot_git::{RefUpdate, Repo}; -use knot_types::{AccountDid, MstKey, Oid, PushOption, RecordBody}; -use serde::{Deserialize, Serialize}; - -pub const GIT_REF_COLLECTION: &str = "sh.tangled.git.ref"; - -const MST_KEY_BUDGET: usize = 256; - -pub const MAX_RKEY_BYTES: usize = MST_KEY_BUDGET - GIT_REF_COLLECTION.len() - 1; - -const fn plain(byte: u8) -> bool { - matches!(byte, b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9' | b'-' | b'_') -} - -pub fn escape(refname: &str) -> Option { - let out = refname - .bytes() - .fold(String::with_capacity(refname.len()), |mut out, byte| { - if plain(byte) { - out.push(byte as char); - } else { - out.push_str(&format!("~{byte:02x}")); - } - out - }); - (out.len() <= MAX_RKEY_BYTES).then_some(out) -} - -pub fn unescape(rkey: &str) -> Option { - fn literal(chunk: &[u8]) -> Option> { - chunk - .iter() - .copied() - .map(|byte| plain(byte).then_some(byte)) - .collect() - } - fn escaped(chunk: &[u8]) -> Option> { - let mut out = vec![nibble(chunk.first().copied()?)? << 4 | nibble(chunk.get(1).copied()?)?]; - out.extend(literal(&chunk[2..])?); - Some(out) - } - let mut chunks = rkey.split('~'); - let decoded = literal(chunks.next()?.as_bytes())?; - let decoded = chunks.try_fold(decoded, |mut out, chunk| { - out.extend(escaped(chunk.as_bytes())?); - Some(out) - })?; - String::from_utf8(decoded).ok() -} - -fn nibble(byte: u8) -> Option { - match byte { - b'0'..=b'9' => Some(byte - b'0'), - b'a'..=b'f' => Some(byte - b'a' + 10), - _ => None, - } -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize)] -pub struct RefRecord { - sha: String, - #[serde(rename = "$type")] - record_type: &'static str, - #[serde(rename = "x-tngl-editor", skip_serializing_if = "Option::is_none")] - editor: Option, - #[serde( - rename = "x-tngl-push-options", - skip_serializing_if = "Option::is_none" - )] - push_options: Option>, -} - -impl RefRecord { - pub fn new(target: Oid, editor: Option<&AccountDid>, push_options: &[PushOption]) -> Self { - Self { - sha: target.to_hex(), - record_type: GIT_REF_COLLECTION, - editor: editor.cloned(), - push_options: (!push_options.is_empty()).then(|| push_options.to_vec()), - } - } - - pub fn bytes(&self) -> Vec { - dag_cbor(self) - } - - pub fn body(&self) -> Result { - encoded(self) - } -} - -pub fn sha_of(bytes: &[u8]) -> Option { - #[derive(Deserialize)] - struct Stored { - sha: String, - } - serde_ipld_dagcbor::from_slice::(bytes) - .ok() - .map(|stored| stored.sha) -} - -pub fn desired_records( - repo: &Repo, - applied: &[RefUpdate], - editor: Option<&AccountDid>, - push_options: &[PushOption], -) -> Result, ChainError> { - let prefix = format!("{GIT_REF_COLLECTION}/"); - let stored = stored_records(repo, &prefix)?; - let served = repo - .branches()? - .into_iter() - .chain(repo.tags()?) - .collect::>(); - let touched = applied - .iter() - .filter(|update| match update { - RefUpdate::Delete { name, .. } => !served.iter().any(|record| &record.name == name), - RefUpdate::Create { name, new } | RefUpdate::Update { name, new, .. } => served - .iter() - .any(|record| &record.name == name && &record.target == new), - }) - .map(|update| update.name().as_str()) - .collect::>(); - let served = served - .into_iter() - .filter_map(|record| match escape(record.name.as_str()) { - Some(rkey) => Some((format!("{prefix}{rkey}"), record)), - None => { - tracing::warn!( - ref_name = record.name.as_str(), - "the refname once escaped is over the {MAX_RKEY_BYTES} byte record-key budget, so it gets no record" - ); - None - } - }) - .map(|(key, record)| { - let kept = stored.get(key.as_str()).filter(|body| { - sha_of(body.as_bytes()).is_some_and(|sha| sha == record.target.to_hex()) - }); - let body = match touched.contains(record.name.as_str()) { - true => RefRecord::new(record.target, editor, push_options).body(), - false => match kept.cloned() { - Some(body) => Ok(body), - None => RefRecord::new(record.target, None, &[]).body(), - }, - }; - body.map(|body| { - ( - MstKey::new(key).expect("Escaped ref key fits mst"), - body, - ) - }) - .map_err(|error| ChainError::Malformed(error.to_string())) - }) - .collect::, _>>()?; - Ok(served) -} - -fn stored_records(repo: &Repo, prefix: &str) -> Result, ChainError> { - let tip = match chain::tip(repo)? { - Some(tip) => tip, - None => return Ok(BTreeMap::new()), - }; - let blocks = Arc::new(GitBlocks::at(repo, tip.blocks)); - futures::executor::block_on(async { - futures::stream::iter(chain::load_tree(repo, &tip).leaves().await?) - .filter(|(key, _)| std::future::ready(key.as_str().starts_with(prefix))) - .then(|(key, cid)| { - let blocks = Arc::clone(&blocks); - async move { - match blocks.get(&cid).await.ok().flatten() { - Some(bytes) => Some( - RecordBody::new(bytes.to_vec()).map(|body| { - ( - MstKey::new(key.as_str().to_owned()) - .expect("Stored ref key fits mst"), - body, - ) - }), - ), - None => { - tracing::warn!( - key = key.as_str(), - "a stored ref record block wouldn't read off the chain, so its next sync rewrites it bare" - ); - None - } - } - } - }) - .filter_map(std::future::ready) - .try_collect::>() - .await - .map_err(|error: knot_types::RecordBodyError| { - ChainError::Malformed(error.to_string()) - }) - }) -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::frozen; - use knot_types::{RecordCollection, RepoDid}; - - const PUSHED: &str = "a4637368617828616261626162616261626162616261626162616261626162616261626162616261626162616261626524747970657273682e74616e676c65642e6769742e7265666d782d746e676c2d656469746f726e6469643a706c633a6c696d70657473782d746e676c2d707573682d6f7074696f6e738167736b69702d6369"; - const BARE: &str = "a2637368617840636463646364636463646364636463646364636463646364636463646364636463646364636463646364636463646364636463646364636463646364636463646524747970657273682e74616e676c65642e6769742e726566"; - - fn editor() -> AccountDid { - AccountDid::new("did:plc:limpet").unwrap() - } - - #[test] - fn the_ref_record_bytes_are_frozen() { - let pushed = RefRecord::new( - Oid::from_hex(&"ab".repeat(20)).unwrap(), - Some(&editor()), - &[PushOption::new("skip-ci").unwrap()], - ) - .bytes(); - assert_eq!( - pushed, - frozen(PUSHED), - "the first emitted record freezes this wire format: length-first key order, \ - and the x-tngl fields stay undeclared by any lexicon" - ); - let bare = RefRecord::new(Oid::from_hex(&"cd".repeat(32)).unwrap(), None, &[]).bytes(); - assert_eq!( - bare, - frozen(BARE), - "Catch-up record leaves editor out and options empty, and sha256 oid fits" - ); - assert_eq!( - sha_of(&bare), - Some("cd".repeat(32)), - "the sha check reads exactly what the drift comparison needs" - ); - assert_eq!(sha_of(b"\xa2\x63ab"), None); - } - - #[test] - fn the_rkey_encoding_round_trips_every_unicode_scalar() { - (0u32..=0x10FFFF) - .filter_map(char::from_u32) - .for_each(|scalar| { - let raw = format!("refs/heads/x{scalar}y"); - let rkey = escape(&raw).unwrap(); - assert_eq!( - unescape(&rkey).unwrap(), - raw, - "scalar U+{:04X} round trips", - scalar as u32 - ); - assert!( - rkey.bytes().all(|out| plain(out) || out == b'~'), - "scalar U+{:04X} escapes into the record-key alphabet", - scalar as u32 - ); - }); - assert_eq!( - escape("refs/heads/main").unwrap(), - "refs~2fheads~2fmain", - "the ruled example" - ); - [ - "refs/tags/v1.0.2", - "refs/heads/feature/with--dashes_and.underscores", - "refs/heads/ünïcödé", - ] - .into_iter() - .for_each(|raw| { - let rkey = escape(raw).unwrap(); - assert_eq!(unescape(&rkey).unwrap(), raw); - jacquard_repo::mst::util::validate_key(&format!("{GIT_REF_COLLECTION}/{rkey}")) - .unwrap_or_else(|error| panic!("{raw}'s key {rkey} failed: {error}")); - }); - [ - "refs~2", - "refs~zzmain", - "refs~2fheads~", - "re fs", - "refs/heads/main", - "refs~2Fmain", - "refs~~2f", - ] - .into_iter() - .for_each(|torn| { - assert_eq!(unescape(torn), None, "{torn} mustn't decode"); - }); - let head = escape("refs/heads/").unwrap().len(); - let fits = format!("refs/heads/{}", "a".repeat(MAX_RKEY_BYTES - head)); - assert_eq!(escape(&fits).unwrap().len(), MAX_RKEY_BYTES); - assert!(escape(&format!("refs/heads/{}", "a".repeat(MAX_RKEY_BYTES))).is_none()); - assert!( - escape(&format!("refs/heads/{}", "a/".repeat(MAX_RKEY_BYTES / 2))).is_none(), - "each slash costs three characters" - ); - } - - #[tokio::test] - async fn a_landed_sha_outliving_its_report_keeps_its_stored_provenance() { - let dir = tempfile::tempdir().unwrap(); - let layout = knot_git::Layout::new(dir.path()); - let repo = layout - .create(&knot_types::RepoDid::new("did:plc:squid").unwrap()) - .unwrap(); - let signer = knot_runtime::K256Signer::from_slice(&[7u8; 32]).unwrap(); - futures::executor::block_on(crate::chain::initialize( - &repo, - &RepoDid::new("did:web:knot.example").unwrap(), - &signer, - &mut jacquard_common::types::tid::Ticker::new(), - knot_types::UnixSeconds::new(1), - )) - .unwrap(); - let tip = chain::tip(&repo).unwrap().unwrap().git; - let stored = - RefRecord::new(tip, Some(&editor()), &[PushOption::new("skip-ci").unwrap()]).bytes(); - repo.update_ref(&RefUpdate::Create { - name: knot_types::RefName::new("refs/heads/main".to_string()).unwrap(), - new: tip, - }) - .unwrap(); - let key = format!( - "{GIT_REF_COLLECTION}/{}", - escape("refs/heads/main").unwrap() - ); - futures::executor::block_on(chain::sync( - &repo, - &RepoDid::new("did:web:knot.example").unwrap(), - std::slice::from_ref(&RecordCollection::new(GIT_REF_COLLECTION).unwrap()), - &chain::in_tree(BTreeMap::from([( - MstKey::new(key.clone()).unwrap(), - RecordBody::new(stored.clone()).unwrap(), - )])), - &signer, - &mut jacquard_common::types::tid::Ticker::new(), - knot_types::UnixSeconds::new(2), - )) - .unwrap(); - let name = knot_types::RefName::new("refs/heads/main".to_string()).unwrap(); - let applied = [RefUpdate::Delete { - name: name.clone(), - old: tip, - }]; - let desired = desired_records(&repo, &applied, None, &[]).unwrap(); - assert_eq!( - desired[key.as_str()].as_bytes(), - stored, - "the branch reappeared between the delete and the projection, so the push that owns this sha stays named" - ); - let applied = [RefUpdate::Update { - name, - old: Oid::from_hex(&"cd".repeat(20)).unwrap(), - new: Oid::from_hex(&"ef".repeat(20)).unwrap(), - }]; - let desired = desired_records(&repo, &applied, None, &[]).unwrap(); - assert_eq!( - desired[key.as_str()].as_bytes(), - stored, - "only a sha that landed can replace the stored provenance" - ); - } -} diff --git a/knot2/crates/knot-record/src/grant.rs b/knot2/crates/knot-record/src/grant.rs index deff7df9e..f23044ab7 100644 --- a/knot2/crates/knot-record/src/grant.rs +++ b/knot2/crates/knot-record/src/grant.rs @@ -9,10 +9,13 @@ pub const MEMBER_COLLECTION: &str = "org.tangled.knot.member"; pub const COLLABORATOR_COLLECTION: &str = "org.tangled.graph.collaborator"; +pub const BLOCK_COLLECTION: &str = "org.tangled.knot.block"; + #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum GrantCollection { Member, Collaborator, + Block, } impl GrantCollection { @@ -20,6 +23,7 @@ impl GrantCollection { match self { Self::Member => MEMBER_COLLECTION, Self::Collaborator => COLLABORATOR_COLLECTION, + Self::Block => BLOCK_COLLECTION, } } @@ -31,6 +35,7 @@ impl GrantCollection { match self { Self::Member => "members", Self::Collaborator => "collaborators", + Self::Block => "blocklist", } } } @@ -43,6 +48,15 @@ struct GrantEncoding<'a> { created_at: &'a CreatedAt, } +pub fn record_key(subject: &AccountDid, collection: GrantCollection) -> Result { + DidRkey::new(subject.as_str()) + .map_err(|error| error.to_string()) + .and_then(|rkey| { + MstKey::new(format!("{}/{}", collection.nsid(), rkey.as_str())) + .map_err(|error| error.to_string()) + }) +} + pub fn bodies<'a>( entries: impl IntoIterator, collection: GrantCollection, @@ -50,12 +64,7 @@ pub fn bodies<'a>( entries .into_iter() .filter_map(|(subject, offered_at)| { - let key = DidRkey::new(subject.as_str()) - .map_err(|error| error.to_string()) - .and_then(|rkey| { - MstKey::new(format!("{}/{}", collection.nsid(), rkey.as_str())) - .map_err(|error| error.to_string()) - }); + let key = record_key(subject, collection); let Ok(key) = key else { tracing::warn!( subject = subject.as_str(), @@ -135,7 +144,10 @@ mod tests { let whelk = subject("did:plc:whelk"); let encoded = bodies( [ - stamped(&subject(&format!("did:web:{}.oyster.cafe", "a".repeat(300))), 1), + stamped( + &subject(&format!("did:web:{}.oyster.cafe", "a".repeat(300))), + 1, + ), stamped(&subject("did:web:knot.nel.pet:8080"), 2), stamped(&whelk, i64::MIN), stamped(&whelk, 1_700_000_000), diff --git a/knot2/crates/knot-record/src/lib.rs b/knot2/crates/knot-record/src/lib.rs index 6aaded3cf..712871382 100644 --- a/knot2/crates/knot-record/src/lib.rs +++ b/knot2/crates/knot-record/src/lib.rs @@ -10,7 +10,6 @@ pub mod attachment; pub mod blocks; pub mod chain; pub mod comment; -pub mod git_ref; pub mod git_refs; pub mod grant; pub mod label; diff --git a/knot2/crates/knot-record/src/manifest.rs b/knot2/crates/knot-record/src/manifest.rs index b0a342f73..1ab0174d3 100644 --- a/knot2/crates/knot-record/src/manifest.rs +++ b/knot2/crates/knot-record/src/manifest.rs @@ -3,7 +3,8 @@ use std::str::FromStr; use jacquard_common::deps::codegen::unicode_segmentation::UnicodeSegmentation; use knot_types::{ - AccountDid, OwnerDid, RecordBody, RecordBodyError, RepoDid, RepoName, ServiceDid, UnixSeconds, + AccountDid, BranchName, ContributionPolicy, OwnerDid, RecordBody, RecordBodyError, RepoDid, + RepoName, ServiceDid, UnixSeconds, }; use serde::{Deserialize, Serialize, Serializer}; use url::Url; @@ -102,6 +103,8 @@ pub struct ManifestMeta { pub website: Option, pub topics: Option, pub source: Option, + pub default_branch: Option, + pub contribution_policy: Option, } #[derive(Debug, Clone, PartialEq, Eq)] @@ -156,6 +159,10 @@ struct ManifestEncoding<'a> { topics: &'a Option, #[serde(skip_serializing_if = "Option::is_none")] source: &'a Option, + #[serde(rename = "defaultBranch", skip_serializing_if = "Option::is_none")] + default_branch: Option<&'a BranchName>, + #[serde(rename = "contributionPolicy", skip_serializing_if = "Option::is_none")] + contribution_policy: Option<&'a ContributionPolicy>, #[serde(rename = "createdAt")] created_at: &'a CreatedAt, #[serde(rename = "x-tngl-editor")] @@ -175,6 +182,8 @@ impl Serialize for ManifestRecord { website: self.meta.website.as_ref().map(Url::as_str), topics: &self.meta.topics, source: &self.meta.source, + default_branch: self.meta.default_branch.as_ref(), + contribution_policy: self.meta.contribution_policy.as_ref(), created_at: &self.created_at, editor: &self.editor, } diff --git a/knot2/crates/knot-server/src/main.rs b/knot2/crates/knot-server/src/main.rs index fe1f33e71..e931e8c5e 100644 --- a/knot2/crates/knot-server/src/main.rs +++ b/knot2/crates/knot-server/src/main.rs @@ -247,6 +247,17 @@ async fn run(config: knot_config::Validated) -> anyhow::Result<()> { .meta_path(&knot_did) .context("resolve meta-repo path")?; + let meta_repo = + knot_git::Repo::open(&meta_path).context("open the meta-repo for the legacy ref sweep")?; + let migrated = knot_xrpc::migrate::legacy_meta_refs(&meta_repo) + .context("migrate legacy sh.tangled cob refs in the meta-repo")?; + if migrated > 0 { + tracing::info!( + migrated, + "meta-repo cob refs renamed to org.tangled on boot" + ); + } + let knot_repo = knot_types::DidRkey::new(knot_did.as_str()) .with_context(|| { format!( @@ -672,6 +683,16 @@ async fn run(config: knot_config::Validated) -> anyhow::Result<()> { (shutdown, task) }) .unzip(); + { + let state = Arc::clone(&xrpc_state); + let swept = tokio::task::spawn_blocking(move || knot_xrpc::migrate::legacy_state(&state)) + .await + .context("join the legacy sweep")? + .context("sweep legacy sh.tangled refs and records")?; + if swept > 0 { + tracing::info!(swept, "legacy sh.tangled refs and records swept on boot"); + } + } { let state = Arc::clone(&xrpc_state); tokio::task::spawn_blocking(move || knot_xrpc::materialize::enable_surfaces(&state)); diff --git a/knot2/crates/knot-sim/src/harness.rs b/knot2/crates/knot-sim/src/harness.rs index 71999474b..88c95a54c 100644 --- a/knot2/crates/knot-sim/src/harness.rs +++ b/knot2/crates/knot-sim/src/harness.rs @@ -428,6 +428,10 @@ impl Harness { UnixSeconds::new((self.clock.now_unix_micros().get() / 1_000_000) as i64) } + pub(crate) fn knot_repo(&self) -> &RepoDid { + &self.boot_state.knot_repo + } + pub(crate) fn advance(&self, delta: std::time::Duration) { self.clock.advance(delta); } @@ -765,12 +769,9 @@ struct StateParts { did_document: serde_json::Value, } -fn assemble_router( - parts: StateParts, -) -> ( - Router, - Arc, Arc>>, -) { +type FakeState = Arc, Arc>>; + +fn assemble_router(parts: StateParts) -> (Router, FakeState) { let StateParts { layout, index, diff --git a/knot2/crates/knot-sim/src/realdata.rs b/knot2/crates/knot-sim/src/realdata.rs index 6f560f71b..8d6d598d2 100644 --- a/knot2/crates/knot-sim/src/realdata.rs +++ b/knot2/crates/knot-sim/src/realdata.rs @@ -13,9 +13,10 @@ use knot_types::{ use crate::harness::{Harness, SocialProgress, SocialStage}; use crate::trace::{OperationIndex, Outcome, RoundNumber, Snapshot, Step, Trace, fnv1a}; +use crate::workload::OpSlot; use crate::workload::{ - Request, Rng, body_digest, drive_kill, enc, http_call, maintain_outcome, repo_did_of, - signed_post, + Request, Rng, block_collection, body_digest, collaborator_collection, drive_kill, enc, + grant_body, http_call, iso_utc, maintain_outcome, member_collection, repo_did_of, signed_post, }; pub(crate) const SAMPLE_TAG: u64 = 0x5a3d_e005; @@ -27,7 +28,8 @@ const DRIVE_TAG: u64 = 0x11c7_0005; const TICKET_COLLECTION: &str = "org.tangled.track.ticket"; const PATCH_COLLECTION: &str = "org.tangled.review.patch"; const CREATE_RECORD_NSID: &str = "com.atproto.repo.createRecord"; -const PULL_MERGE_NSID: &str = "sh.tangled.repo.pull.merge"; +const DELETE_RECORD_NSID: &str = "com.atproto.repo.deleteRecord"; +const PULL_MERGE_NSID: &str = "org.tangled.review.mergePatch"; fn nsid(name: &str) -> Nsid { Nsid::new_owned(name).expect("the xrpc method is an NSID") @@ -248,12 +250,12 @@ fn arm_drops(harness: &Harness, execs: &[Exec]) -> Vec { hosts } -fn member_verb(draw: u64) -> (Nsid, &'static str) { +fn member_verb(draw: u64) -> (&'static str, RecordCollection, bool) { match draw { - 0 => (nsid("sh.tangled.knot.addMember"), "addMember"), - 1 => (nsid("sh.tangled.knot.removeMember"), "removeMember"), - 2 => (nsid("sh.tangled.knot.ban"), "ban"), - _ => (nsid("sh.tangled.knot.unban"), "unban"), + 0 => ("addMember", member_collection(), true), + 1 => ("removeMember", member_collection(), false), + 2 => ("ban", block_collection(), true), + _ => ("unban", block_collection(), false), } } @@ -271,18 +273,31 @@ fn mutate( .iter() .filter(|_| rng.chance(2, 3)) .for_each(|subject| { - let (method, short) = member_verb(rng.below(4)); + let (short, collection, creates) = member_verb(rng.below(4)); + let method = nsid(if creates { + CREATE_RECORD_NSID + } else { + DELETE_RECORD_NSID + }); let skew = rng.chance(1, 5); + let body = grant_body( + harness.knot_repo(), + &collection, + subject, + creates.then(|| iso_utc(harness.now_seconds())).as_ref(), + ); let request = signed_post( harness, &harness.admin.signer, &harness.admin.did, &method, &harness.knot_aud, - json!({ "subject": subject }), - skew, - round, - execs.len(), + body, + OpSlot { + skew, + round, + index: OperationIndex::new(execs.len() as u32), + }, ); execs.push(Exec::Http { op: short, @@ -309,12 +324,19 @@ fn mutate( harness, &owner_signer(actors.seed, owner), &AccountDid::from(owner.clone()), - &nsid("sh.tangled.repo.addCollaborator"), - &harness.knot_aud, - json!({ "repo": repo, "subject": subject }), - skew, - round, - execs.len(), + &nsid(CREATE_RECORD_NSID), + repo, + grant_body( + repo, + &collaborator_collection(), + subject, + Some(&iso_utc(harness.now_seconds())), + ), + OpSlot { + skew, + round, + index: OperationIndex::new(execs.len() as u32), + }, ); execs.push(Exec::Http { op: "addCollaborator", @@ -346,9 +368,11 @@ fn mutate( &nsid("org.tangled.repo.createRepo"), &harness.knot_aud, json!({ "name": name }), - skew, - round, - execs.len(), + OpSlot { + skew, + round, + index: OperationIndex::new(execs.len() as u32), + }, ); execs.push(Exec::Http { op: "createRepo", @@ -371,12 +395,19 @@ fn mutate( harness, &stranger.signer, &stranger.did, - &nsid("sh.tangled.knot.addMember"), + &nsid(CREATE_RECORD_NSID), &harness.knot_aud, - json!({ "subject": subjects[0] }), - false, - round, - execs.len(), + grant_body( + harness.knot_repo(), + &member_collection(), + &subjects[0], + Some(&iso_utc(harness.now_seconds())), + ), + OpSlot { + skew: false, + round, + index: OperationIndex::new(execs.len() as u32), + }, ); execs.push(Exec::Http { op: "probe", @@ -456,7 +487,7 @@ fn mutate( nsid(PULL_MERGE_NSID), json!({ "repo": repo, - "rkey": pull.expect("pull rkey") + "patch": pull.expect("pull rkey") }), ), }; @@ -467,9 +498,11 @@ fn mutate( &method, &repo, body, - false, - round, - execs.len(), + OpSlot { + skew: false, + round, + index: OperationIndex::new(execs.len() as u32), + }, ); execs.push(Exec::Http { op, @@ -490,9 +523,9 @@ fn skew_fault(skew: bool) -> &'static str { } fn reads(working: &[RepoDid], rng: &Rng) -> Vec { - let mut execs: Vec = [ + let mut execs: Vec<_> = [ ("version", "/xrpc/org.tangled.knot.describeKnot"), - ("owner", "/xrpc/sh.tangled.owner"), + ("describeKnot", "/xrpc/org.tangled.knot.describeKnot"), ("didJson", "/.well-known/did.json"), ] .into_iter() @@ -505,17 +538,29 @@ fn reads(working: &[RepoDid], rng: &Rng) -> Vec { .for_each(|repo| { let killed = rng.chance(1, 5); let exec = match rng.below(7) { - 0 => repo_read("branches", "repo", repo, killed), - 1 => repo_read("log", "repo", repo, killed), - 2 => repo_read("describeRepo", "repoDid", repo, killed), + 0 => repo_read("branches", "org.tangled.git.listBranches", repo, killed), + 1 => anon_read( + "log", + &format!( + "/xrpc/org.tangled.git.listCommits?repo={}&ref=HEAD", + enc(repo) + ), + killed, + ), + 2 => repo_read( + "describeRepo", + "com.atproto.repo.describeRepo", + repo, + killed, + ), 3 => anon_read( "infoRefs", &format!("/{}/info/refs?service=git-upload-pack", repo), killed, ), - 4 => repo_read("tree", "repo", repo, killed), + 4 => repo_read("tree", "org.tangled.git.getTree", repo, killed), 5 => blob_read(repo, killed), - _ => repo_read("languages", "repo", repo, killed), + _ => repo_read("languages", "org.tangled.git.listLanguages", repo, killed), }; execs.push(exec); }); @@ -534,19 +579,15 @@ fn anon_read(op: &'static str, uri: &str, killed: bool) -> Exec { } } -fn repo_read(op: &'static str, param: &str, repo: &RepoDid, killed: bool) -> Exec { - anon_read( - op, - &format!("/xrpc/sh.tangled.repo.{op}?{param}={}", enc(repo)), - killed, - ) +fn repo_read(op: &'static str, method: &str, repo: &RepoDid, killed: bool) -> Exec { + anon_read(op, &format!("/xrpc/{method}?repo={}", enc(repo)), killed) } fn blob_read(repo: &RepoDid, killed: bool) -> Exec { anon_read( "blob", &format!( - "/xrpc/sh.tangled.repo.blob?repo={}&path=README.md", + "/xrpc/org.tangled.git.getBlob?repo={}&path=README.md&ref=HEAD", enc(repo) ), killed, diff --git a/knot2/crates/knot-sim/src/workload.rs b/knot2/crates/knot-sim/src/workload.rs index e97755585..5ba20bfa9 100644 --- a/knot2/crates/knot-sim/src/workload.rs +++ b/knot2/crates/knot-sim/src/workload.rs @@ -12,13 +12,28 @@ use std::sync::Arc; use tower::ServiceExt; use knot_runtime::{Entropy, K256Signer, SeededEntropy, Signer}; -use knot_types::{AccountDid, HttpStatus, KnotHostname, Nsid, RepoDid, UnixSeconds}; +use knot_types::{ + AccountDid, Datetime, HttpStatus, KnotHostname, Nsid, RecordCollection, RepoDid, UnixSeconds, +}; use crate::harness::{Harness, subject_dids}; use crate::trace::{OperationIndex, Outcome, Projection, RoundNumber, Step, Trace, fnv1a}; const SKEW_BACKDATE_SECS: i64 = 600; const SKEW_LIFETIME_SECS: i64 = 60; +const CREATE_RECORD_NSID: &str = "com.atproto.repo.createRecord"; +const DELETE_RECORD_NSID: &str = "com.atproto.repo.deleteRecord"; +pub(crate) fn member_collection() -> RecordCollection { + RecordCollection::new("org.tangled.knot.member").expect("literal NSID parses") +} + +pub(crate) fn block_collection() -> RecordCollection { + RecordCollection::new("org.tangled.knot.block").expect("literal NSID parses") +} + +pub(crate) fn collaborator_collection() -> RecordCollection { + RecordCollection::new("org.tangled.graph.collaborator").expect("literal NSID parses") +} #[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] struct RepoIndex(usize); @@ -32,7 +47,7 @@ struct StrangerIndex(usize); #[derive(Clone, Copy)] enum ReadOp { Version, - Owner, + DescribeKnot, ListMembers, DidJson, InfoRefs(RepoIndex), @@ -49,7 +64,7 @@ impl ReadOp { fn name(self) -> &'static str { match self { ReadOp::Version => "version", - ReadOp::Owner => "owner", + ReadOp::DescribeKnot => "describeKnot", ReadOp::ListMembers => "listMembers", ReadOp::DidJson => "didJson", ReadOp::InfoRefs(_) => "infoRefs", @@ -305,7 +320,7 @@ fn mutate_round( fn observe_round(rng: &Rng, available: u32) -> Vec { let mut ops: Vec = [ ReadOp::Version, - ReadOp::Owner, + ReadOp::DescribeKnot, ReadOp::ListMembers, ReadOp::DidJson, ] @@ -478,7 +493,7 @@ async fn run_op( } } Planned::Observe(Observation { op, killed }) => { - let request = read_request(repos, op); + let request = read_request(harness, repos, op); if killed { drive_kill(harness.router(), request.method, &request.uri, request.body).await; return OpResult { @@ -580,13 +595,16 @@ impl Request { } } -fn read_request(repos: &[RepoDid], op: ReadOp) -> Request { +fn read_request(harness: &Harness, repos: &[RepoDid], op: ReadOp) -> Request { match op { ReadOp::Version => get("/xrpc/org.tangled.knot.describeKnot"), - ReadOp::Owner => get("/xrpc/sh.tangled.owner"), - ReadOp::ListMembers => { - get("/xrpc/sh.tangled.knot.listMembers?subject=did:web:knot.nel.pet") - } + ReadOp::DescribeKnot => get("/xrpc/org.tangled.knot.describeKnot"), + ReadOp::ListMembers => get(format!( + "/xrpc/com.atproto.repo.listRecords?repo={}&collection={}", + enc(&harness.knot_aud), + member_collection() + ) + .as_str()), ReadOp::DidJson => get("/.well-known/did.json"), ReadOp::InfoRefs(repo) => Request { method: Method::GET, @@ -598,18 +616,27 @@ fn read_request(repos: &[RepoDid], op: ReadOp) -> Request { body: Bytes::new(), actor: "anon".to_string(), }, - ReadOp::Branches(repo) => repo_get("branches", "repo", repo_at(repos, repo)), - ReadOp::Log(repo) => repo_get("log", "repo", repo_at(repos, repo)), - ReadOp::DescribeRepo(repo) => repo_get("describeRepo", "repoDid", repo_at(repos, repo)), - ReadOp::Tree(repo) => repo_get("tree", "repo", repo_at(repos, repo)), - ReadOp::Languages(repo) => repo_get("languages", "repo", repo_at(repos, repo)), - ReadOp::ListCollaborators(repo) => { - repo_get("listCollaborators", "subject", repo_at(repos, repo)) + ReadOp::Branches(repo) => repo_get("org.tangled.git.listBranches", repo_at(repos, repo)), + ReadOp::Log(repo) => get(format!( + "/xrpc/org.tangled.git.listCommits?repo={}&ref=HEAD", + enc(repo_at(repos, repo)) + ) + .as_str()), + ReadOp::DescribeRepo(repo) => { + repo_get("com.atproto.repo.describeRepo", repo_at(repos, repo)) } + ReadOp::Tree(repo) => repo_get("org.tangled.git.getTree", repo_at(repos, repo)), + ReadOp::Languages(repo) => repo_get("org.tangled.git.listLanguages", repo_at(repos, repo)), + ReadOp::ListCollaborators(repo) => get(format!( + "/xrpc/com.atproto.repo.listRecords?repo={}&collection={}", + enc(repo_at(repos, repo)), + collaborator_collection() + ) + .as_str()), ReadOp::Blob(repo) => Request { method: Method::GET, uri: format!( - "/xrpc/sh.tangled.repo.blob?repo={}&path=README.md", + "/xrpc/org.tangled.git.getBlob?repo={}&path=README.md&ref=HEAD", enc(repo_at(repos, repo)) ), token: None, @@ -628,42 +655,59 @@ fn admin_request( index: OperationIndex, ) -> Request { let subjects = &harness.subjects; + let now = iso_utc(harness.now_seconds()); match op { AdminOp::AddMember(subject) => admin_post( harness, "addMember", - &nsid("sh.tangled.knot.addMember"), - json!({ "subject": subjects[subject.0] }), - skew, - round, - index, + &nsid(CREATE_RECORD_NSID), + &harness.knot_aud, + grant_body( + harness.knot_repo(), + &member_collection(), + &subjects[subject.0], + Some(&now), + ), + OpSlot { skew, round, index }, ), AdminOp::RemoveMember(subject) => admin_post( harness, "removeMember", - &nsid("sh.tangled.knot.removeMember"), - json!({ "subject": subjects[subject.0] }), - skew, - round, - index, + &nsid(DELETE_RECORD_NSID), + &harness.knot_aud, + grant_body( + harness.knot_repo(), + &member_collection(), + &subjects[subject.0], + None, + ), + OpSlot { skew, round, index }, ), AdminOp::Ban(subject) => admin_post( harness, "ban", - &nsid("sh.tangled.knot.ban"), - json!({ "subject": subjects[subject.0] }), - skew, - round, - index, + &nsid(CREATE_RECORD_NSID), + &harness.knot_aud, + grant_body( + harness.knot_repo(), + &block_collection(), + &subjects[subject.0], + Some(&now), + ), + OpSlot { skew, round, index }, ), AdminOp::Unban(subject) => admin_post( harness, "unban", - &nsid("sh.tangled.knot.unban"), - json!({ "subject": subjects[subject.0] }), - skew, - round, - index, + &nsid(DELETE_RECORD_NSID), + &harness.knot_aud, + grant_body( + harness.knot_repo(), + &block_collection(), + &subjects[subject.0], + None, + ), + OpSlot { skew, round, index }, ), AdminOp::CreateRepo(key) => { let name = format!("repo{key}"); @@ -671,23 +715,23 @@ fn admin_request( harness, "create", &nsid("org.tangled.repo.createRepo"), + &harness.knot_aud, json!({ "name": name }), - skew, - round, - index, + OpSlot { skew, round, index }, ) } AdminOp::AddCollaborator(repo, subject) => admin_post( harness, "addCollaborator", - &nsid("sh.tangled.repo.addCollaborator"), - json!({ - "repo": repo_at(repos, repo), - "subject": subjects[subject.0], - }), - skew, - round, - index, + &nsid(CREATE_RECORD_NSID), + &repo_at(repos, repo), + grant_body( + repo_at(repos, repo), + &collaborator_collection(), + &subjects[subject.0], + Some(&now), + ), + OpSlot { skew, round, index }, ), } } @@ -703,12 +747,19 @@ fn probe_request( harness, &actor.signer, &actor.did, - &nsid("sh.tangled.knot.addMember"), + &nsid(CREATE_RECORD_NSID), &harness.knot_aud, - json!({ "subject": harness.subjects[0] }), - false, - round, - index.get() as usize, + grant_body( + harness.knot_repo(), + &member_collection(), + &harness.subjects[0], + Some(&iso_utc(harness.now_seconds())), + ), + OpSlot { + skew: false, + round, + index, + }, ); request.actor = actor.host.to_string(); request @@ -718,11 +769,8 @@ fn get(path: &str) -> Request { Request::get(path) } -fn repo_get(method: &str, param: &str, repo: &RepoDid) -> Request { - Request::get(format!( - "/xrpc/sh.tangled.repo.{method}?{param}={}", - enc(repo) - )) +fn repo_get(method: &str, repo: &RepoDid) -> Request { + Request::get(format!("/xrpc/{method}?repo={}", enc(repo))) } fn nsid(name: &str) -> Nsid { @@ -736,9 +784,7 @@ pub(crate) fn signed_post( method: &Nsid, aud: &impl AsRef, body: Value, - skew: bool, - round: RoundNumber, - slot: usize, + OpSlot { skew, round, index }: OpSlot, ) -> Request { let token = mint( signer, @@ -747,7 +793,7 @@ pub(crate) fn signed_post( method, jwt_window(harness, skew), round, - OperationIndex::new(slot as u32), + index, ); Request { method: Method::POST, @@ -771,25 +817,28 @@ pub(crate) fn maintain_outcome(harness: &Harness, repo: &RepoDid) -> Outcome { } } +pub(crate) struct OpSlot { + pub(crate) skew: bool, + pub(crate) round: RoundNumber, + pub(crate) index: OperationIndex, +} + fn admin_post( harness: &Harness, method_short: &str, method: &Nsid, + aud: &impl AsRef, body: Value, - skew: bool, - round: RoundNumber, - index: OperationIndex, + OpSlot { skew, round, index }: OpSlot, ) -> Request { let mut request = signed_post( harness, &harness.admin.signer, &harness.admin.did, method, - &harness.knot_aud, + aud, body, - skew, - round, - index.get() as usize, + OpSlot { skew, round, index }, ); request.actor = format!("admin:{method_short}"); request @@ -917,6 +966,50 @@ pub(crate) fn enc(value: impl std::fmt::Display) -> String { value.to_string().replace(':', "%3A") } +pub(crate) fn grant_body( + repo: &RepoDid, + collection: &RecordCollection, + rkey: &AccountDid, + created_at: Option<&Datetime>, +) -> Value { + match created_at { + Some(now) => json!({ + "repo": repo, + "collection": collection, + "rkey": rkey, + "record": { "createdAt": now } + }), + None => json!({ + "repo": repo, + "collection": collection, + "rkey": rkey, + }), + } +} + +pub(crate) fn iso_utc(seconds: UnixSeconds) -> Datetime { + let seconds = seconds.get(); + let (days, secs_of_day) = (seconds.div_euclid(86_400), seconds.rem_euclid(86_400)); + let z = days + 719_468; + let era = z.div_euclid(146_097); + let doe = z - era * 146_097; + let yoe = (doe - doe / 1460 + doe / 36_524 - doe / 146_096) / 365; + let doy = doe - (365 * yoe + yoe / 4 - yoe / 100); + let mp = (5 * doy + 2) / 153; + let day = doy - (153 * mp + 2) / 5 + 1; + let (month, year) = if mp < 10 { + (mp + 3, yoe + era * 400) + } else { + (mp - 9, yoe + era * 400 + 1) + }; + Datetime::raw_str(format!( + "{year:04}-{month:02}-{day:02}T{:02}:{:02}:{:02}Z", + secs_of_day / 3600, + secs_of_day % 3600 / 60, + secs_of_day % 60 + )) +} + fn repo_at(repos: &[RepoDid], index: RepoIndex) -> &RepoDid { repos.get(index.0).unwrap_or_else(|| { panic!( diff --git a/knot2/crates/knot-sim/tests/lfs_roundtrip.rs b/knot2/crates/knot-sim/tests/lfs_roundtrip.rs index 171cae4c5..60610e987 100644 --- a/knot2/crates/knot-sim/tests/lfs_roundtrip.rs +++ b/knot2/crates/knot-sim/tests/lfs_roundtrip.rs @@ -1137,7 +1137,7 @@ async fn lfs_http_push_stores_an_object_with_a_push_token() { let bearer = format!( "Bearer {}", - service_jwt(&nsid("sh.tangled.repo.push"), "lfs-http-push-1") + service_jwt(&nsid("org.tangled.git.receivePack"), "lfs-http-push-1") ); let (status, body) = lfs_batch(&world, Some(&bearer), "upload", &oid, size).await; assert_eq!( @@ -1213,7 +1213,10 @@ async fn lfs_http_push_stores_an_object_with_a_push_token() { .map(|n| (n.wrapping_mul(29) % 251) as u8) .collect(); let oid2 = LfsOid::from_digest(knot_types::Sha256Digest::hash(&payload2)); - let basic = basic_auth(&service_jwt(&nsid("sh.tangled.repo.push"), "lfs-http-push-2")); + let basic = basic_auth(&service_jwt( + &nsid("org.tangled.git.receivePack"), + "lfs-http-push-2", + )); let (status, body) = lfs_batch(&world, Some(&basic), "upload", &oid2, payload2.len() as u64).await; assert_eq!( @@ -1269,7 +1272,7 @@ async fn lfs_http_push_rejects_missing_and_mismatched_credentials() { service_jwt_as( &Did::new_owned(REPO_DID).unwrap(), &knot_service_did(), - &nsid("sh.tangled.repo.push"), + &nsid("org.tangled.git.receivePack"), "lfs-http-neg-acl", ) ); @@ -1332,7 +1335,7 @@ async fn git_push_over_http_authenticates_and_lands_the_ref() { "an unauthenticated http push must be refused, git reported success:\n{out}" ); - let token = service_jwt(&nsid("sh.tangled.repo.push"), "git-http-push-1"); + let token = service_jwt(&nsid("org.tangled.git.receivePack"), "git-http-push-1"); let header = format!( "http.extraHeader=Authorization: Basic {}", base64::engine::general_purpose::STANDARD.encode(format!("x-tangled-token:{token}")) @@ -1445,11 +1448,15 @@ async fn git_push_over_h3_authenticates_and_lands_the_ref() { let tip = tip.trim().to_string(); let pack = build_pack(&work, &env, &tip); - let advert_uri = - format!("https://localhost/{}/{REPO_NAME}/info/refs?service=git-receive-pack", owner()); + let advert_uri = format!( + "https://localhost/{}/{REPO_NAME}/info/refs?service=git-receive-pack", + owner() + ); let receive_uri = format!("https://localhost/{}/{REPO_NAME}/git-receive-pack", owner()); - let warmup = - format!("https://localhost/{}/{REPO_NAME}/info/refs?service=git-upload-pack", owner()); + let warmup = format!( + "https://localhost/{}/{REPO_NAME}/info/refs?service=git-upload-pack", + owner() + ); const RECEIVE_CT: &str = "application/x-git-receive-pack-request"; let (status, _) = @@ -1460,7 +1467,10 @@ async fn git_push_over_h3_authenticates_and_lands_the_ref() { "an unauthenticated receive advertisement must be challenged over h3" ); - let token = basic_auth(&service_jwt(&nsid("sh.tangled.repo.push"), "git-h3-adv-1")); + let token = basic_auth(&service_jwt( + &nsid("org.tangled.git.receivePack"), + "git-h3-adv-1", + )); let (status, advert) = common::h3_request( edge, Method::GET, @@ -1496,7 +1506,10 @@ async fn git_push_over_h3_authenticates_and_lands_the_ref() { "an unauthenticated receive-pack post must be challenged over h3" ); - let wrong = basic_auth(&service_jwt(&nsid("org.tangled.repo.createRepo"), "git-h3-neg")); + let wrong = basic_auth(&service_jwt( + &nsid("org.tangled.repo.createRepo"), + "git-h3-neg", + )); let (status, _) = common::h3_request( edge, Method::POST, @@ -1515,7 +1528,10 @@ async fn git_push_over_h3_authenticates_and_lands_the_ref() { "a token bound to another method cannot authorize a receive-pack over h3" ); - let good = basic_auth(&service_jwt(&nsid("sh.tangled.repo.push"), "git-h3-push-1")); + let good = basic_auth(&service_jwt( + &nsid("org.tangled.git.receivePack"), + "git-h3-push-1", + )); let (status, report) = common::h3_request( edge, Method::POST, diff --git a/knot2/crates/knot-sim/tests/reproducible.rs b/knot2/crates/knot-sim/tests/reproducible.rs index 52978293a..980a2f61d 100644 --- a/knot2/crates/knot-sim/tests/reproducible.rs +++ b/knot2/crates/knot-sim/tests/reproducible.rs @@ -19,7 +19,7 @@ fn union_has(traces: &[Trace], predicate: impl Fn(&Step) -> bool) -> bool { const READ_OPS: [&str; 12] = [ "version", - "owner", + "describeKnot", "listMembers", "listCollaborators", "didJson", diff --git a/knot2/crates/knot-sim/tests/social_chaos.rs b/knot2/crates/knot-sim/tests/social_chaos.rs index 459343c1f..e532ddac9 100644 --- a/knot2/crates/knot-sim/tests/social_chaos.rs +++ b/knot2/crates/knot-sim/tests/social_chaos.rs @@ -240,7 +240,7 @@ fn social_chaos() { "some trial must be killed before every social write is stored, or the kill delays are all too long" ); assert!( - outcomes.iter().any(|records| *records == full_records), + outcomes.contains(&full_records), "some trial must reach the full social write set, or the kill delays are all too short" ); } diff --git a/knot2/crates/knot-ssh/tests/ssh_push.rs b/knot2/crates/knot-ssh/tests/ssh_push.rs index 0ec4c59a8..c19960dee 100644 --- a/knot2/crates/knot-ssh/tests/ssh_push.rs +++ b/knot2/crates/knot-ssh/tests/ssh_push.rs @@ -606,7 +606,7 @@ fn seed_cob( ) .unwrap(); let cob_ref = format!( - "refs/cobs/sh.tangled.knot.member/{}", + "refs/cobs/org.tangled.knot.member/{}", created.object.oid().to_hex() ); let spec = format!("{cob_ref}:{cob_ref}"); @@ -658,7 +658,12 @@ async fn fixture_with_archive_limit(archive_limit: ArchiveLimit) -> Fixture { false, ) .await; - let url = format!("ssh://git@127.0.0.1:{}/{}/{}", server.port, owner(), repo_name()); + let url = format!( + "ssh://git@127.0.0.1:{}/{}/{}", + server.port, + owner(), + repo_name() + ); let work = scratch.path().join("work"); Fixture { scratch, @@ -690,7 +695,12 @@ async fn incremental_fetch_exit( let scratch = tempfile::tempdir().unwrap(); let (key_path, public_line) = keygen(scratch.path(), "client"); let (server, _index) = spawn_server(public_line, MaxWireBytes::new(1 << 30)).await; - let url = format!("ssh://git@127.0.0.1:{}/{}/{}", server.port, owner(), repo_name()); + let url = format!( + "ssh://git@127.0.0.1:{}/{}/{}", + server.port, + owner(), + repo_name() + ); let work = scratch.path().join("work"); seed_commits(&work, seed_count); @@ -751,7 +761,11 @@ async fn a_display_name_addresses_a_repo_whose_record_key_is_a_tid() { let target = tid_repo_did(); let variants = [ format!("ssh://git@127.0.0.1:{port}/{}/{}", owner(), tid_repo_name()), - format!("ssh://git@127.0.0.1:{port}/{}/{}.git", owner(), tid_repo_name()), + format!( + "ssh://git@127.0.0.1:{port}/{}/{}.git", + owner(), + tid_repo_name() + ), format!("ssh://git@127.0.0.1:{port}/nel.pet/{}", tid_repo_name()), format!("ssh://git@127.0.0.1:{port}/{}/{}", owner(), tid_rkey()), ]; @@ -817,7 +831,12 @@ async fn a_push_while_the_index_is_warming_is_refused() { let scratch = tempfile::tempdir().unwrap(); let (key_path, public_line) = keygen(scratch.path(), "client"); let (server, _index) = spawn_server_with(public_line, MaxWireBytes::new(1 << 30), false).await; - let url = format!("ssh://git@127.0.0.1:{}/{}/{}", server.port, owner(), repo_name()); + let url = format!( + "ssh://git@127.0.0.1:{}/{}/{}", + server.port, + owner(), + repo_name() + ); let work = scratch.path().join("work"); seed_work(&work); @@ -928,7 +947,12 @@ async fn an_oversized_push_is_refused_at_the_ssh_boundary() { let scratch = tempfile::tempdir().unwrap(); let (key_path, public_line) = keygen(scratch.path(), "client"); let (server, _index) = spawn_server(public_line, MaxWireBytes::new(64)).await; - let url = format!("ssh://git@127.0.0.1:{}/{}/{}", server.port, owner(), repo_name()); + let url = format!( + "ssh://git@127.0.0.1:{}/{}/{}", + server.port, + owner(), + repo_name() + ); let work = scratch.path().join("work"); seed_work(&work); @@ -968,7 +992,12 @@ async fn a_denied_push_over_ssh_leaves_no_objects_in_the_live_odb() { let (_registered_path, registered_line) = keygen(scratch.path(), "registered"); let (attacker_path, _attacker_line) = keygen(scratch.path(), "attacker"); let (server, _index) = spawn_server(registered_line, MaxWireBytes::new(1 << 30)).await; - let url = format!("ssh://git@127.0.0.1:{}/{}/{}", server.port, owner(), repo_name()); + let url = format!( + "ssh://git@127.0.0.1:{}/{}/{}", + server.port, + owner(), + repo_name() + ); let work = scratch.path().join("work"); let head = seed_work(&work); @@ -995,7 +1024,7 @@ async fn ref_namespace_policy() { ("main:refs/hidden/feature/main", "server-side fork staging"), ("main:refs/atproto/commit", "atproto commit chain"), ( - "main:refs/cob-checkpoints/sh.tangled.knot.member/beef", + "main:refs/cob-checkpoints/org.tangled.knot.member/beef", "knot-written collaborative-object snapshots", ), ] { @@ -1043,24 +1072,36 @@ async fn cob_ref_guard_lifecycle() { let (ok, out) = push(&fx.work, &fx.url, &fx.key_path, &["main"]).await; assert!(ok, "head must land for the advertisement check:\n{out}"); - let (owned_tip, owned_ref, owned_spec) = - seed_cob(&fx.work, 1, &AccountDid::new("did:plc:limpet").unwrap(), &home); + let (owned_tip, owned_ref, owned_spec) = seed_cob( + &fx.work, + 1, + &AccountDid::new("did:plc:limpet").unwrap(), + &home, + ); let (ok, out) = push(&fx.work, &fx.url, &fx.key_path, &[owned_spec.as_str()]).await; assert!( ok, "COB ref signed by the repository key must verify and land over ssh:\n{out}" ); - let (_forged_tip, forged_ref, forged_spec) = - seed_cob(&fx.work, 9, &AccountDid::new("did:plc:whelk").unwrap(), &home); + let (_forged_tip, forged_ref, forged_spec) = seed_cob( + &fx.work, + 9, + &AccountDid::new("did:plc:whelk").unwrap(), + &home, + ); let (ok, out) = push(&fx.work, &fx.url, &fx.key_path, &[forged_spec.as_str()]).await; assert!( !ok, "COB ref signed by a stranger must be refused at the receive boundary:\n{out}" ); - let (_transplant_tip, transplant_ref, transplant_spec) = - seed_cob(&fx.work, 1, &AccountDid::new("did:plc:mussel").unwrap(), &foreign); + let (_transplant_tip, transplant_ref, transplant_spec) = seed_cob( + &fx.work, + 1, + &AccountDid::new("did:plc:mussel").unwrap(), + &foreign, + ); let (ok, out) = push(&fx.work, &fx.url, &fx.key_path, &[transplant_spec.as_str()]).await; assert!( !ok, @@ -2190,7 +2231,12 @@ async fn an_authorized_ssh_push_projects_the_ref_records_the_http_push_projects( .await; let firehose = firehose.expect("the projection fixture serves a firehose"); - let url = format!("ssh://git@127.0.0.1:{}/{}/{}", server.port, owner(), repo_name()); + let url = format!( + "ssh://git@127.0.0.1:{}/{}/{}", + server.port, + owner(), + repo_name() + ); let work = scratch.path().join("work"); let head = seed_work(&work); @@ -2213,11 +2259,11 @@ async fn an_authorized_ssh_push_projects_the_ref_records_the_http_push_projects( .events .iter() .flat_map(|event| frame_kind_and_ops(&event.frame).1) - .filter(|path| path.starts_with("sh.tangled.git.ref/")) + .filter(|path| path.starts_with("org.tangled.git.refs/")) .collect(); assert_eq!( ref_ops, - vec!["sh.tangled.git.ref/refs~2fheads~2fmain".to_string()], + vec!["org.tangled.git.refs/self".to_string()], "the ssh push projected its ref into an atproto record, exactly as the http push does" ); } diff --git a/knot2/crates/knot-types/src/ids.rs b/knot2/crates/knot-types/src/ids.rs index bbc9ca557..318aef2bd 100644 --- a/knot2/crates/knot-types/src/ids.rs +++ b/knot2/crates/knot-types/src/ids.rs @@ -325,8 +325,7 @@ fn parse_ci_logs_addr(value: &str) -> Option { } fn parse_branch_name(value: &str) -> Option { - RefName::new(format!("refs/heads/{value}")) - .is_ok() + (value.len() <= 244 && RefName::new(format!("refs/heads/{value}")).is_ok()) .then(|| BranchName(value.to_string())) } @@ -1279,7 +1278,7 @@ mod tests { fn ref_names_follow_git_rules() { assert!(RefName::new("refs/heads/main").is_ok()); assert!(RefName::new("refs/heads/feature/x").is_ok()); - assert!(RefName::new("refs/cobs/sh.tangled.repo.collaborator/limpet").is_ok()); + assert!(RefName::new("refs/cobs/org.tangled.graph.collaborator/limpet").is_ok()); assert!(RefName::new("refs/heads/bad..name").is_err()); assert!(RefName::new("refs/heads/space bar").is_err()); assert!(RefName::new("trailing.lock").is_err()); @@ -1339,7 +1338,7 @@ mod tests { #[test] fn type_name_is_an_nsid() { - assert!(TypeName::new("sh.tangled.repo.collaborator").is_ok()); + assert!(TypeName::new("org.tangled.graph.collaborator").is_ok()); assert!(TypeName::new("not an nsid").is_err()); } diff --git a/knot2/crates/knot-types/src/record.rs b/knot2/crates/knot-types/src/record.rs index 4f7efb14e..6995fd4cc 100644 --- a/knot2/crates/knot-types/src/record.rs +++ b/knot2/crates/knot-types/src/record.rs @@ -377,7 +377,7 @@ mod tests { fn address() -> RecordAddress { RecordAddress::new( - RecordCollection::new("sh.tangled.repo.issue").unwrap(), + RecordCollection::new("org.tangled.repo.issue").unwrap(), RecordRkey::new("3lubrptx57d22").unwrap(), ) } @@ -402,11 +402,11 @@ mod tests { let repo = RepoDid::new("did:plc:squid").unwrap(); assert_eq!( address().mst_key().as_str(), - "sh.tangled.repo.issue/3lubrptx57d22" + "org.tangled.repo.issue/3lubrptx57d22" ); assert_eq!( address().at_uri(&repo).as_str(), - "at://did:plc:squid/sh.tangled.repo.issue/3lubrptx57d22", + "at://did:plc:squid/org.tangled.repo.issue/3lubrptx57d22", "At-uri client resolves is an address under repository DID" ); } @@ -472,8 +472,13 @@ mod tests { #[test] fn a_collection_that_would_overrun_an_mst_key_isnt_a_record_collection() { let room = MST_KEY_MAX_BYTES - super::RECORD_RKEY_BYTES - 1; - let segments: Vec = (0..4).map(|_| "a".repeat(57)).collect(); - let longest = format!("sh.tangled.{}", segments.join(".")); + let segments = [ + "a".repeat(57), + "a".repeat(57), + "a".repeat(57), + "a".repeat(56), + ]; + let longest = format!("org.tangled.{}", segments.join(".")); assert_eq!(longest.len(), room); let collection = RecordCollection::new(&longest).expect("Longest key still fits"); assert_eq!( @@ -523,11 +528,11 @@ mod tests { "parse_path ought to undo mst_key exactly" ); assert!( - RecordAddress::parse_path("sh.tangled.repo.issue").is_err(), + RecordAddress::parse_path("org.tangled.repo.issue").is_err(), "a path without a record key can't be a record address" ); assert!( - RecordAddress::parse_path("sh.tangled.repo.issue/not-a-tid-or-key!").is_err(), + RecordAddress::parse_path("org.tangled.repo.issue/not-a-tid-or-key!").is_err(), "a path whose key half isn't an rkey can't be a record address" ); } diff --git a/knot2/interop/atproto_firehose_test.go b/knot2/interop/atproto_firehose_test.go index 6b74838e6..4c9ad1721 100644 --- a/knot2/interop/atproto_firehose_test.go +++ b/knot2/interop/atproto_firehose_test.go @@ -199,7 +199,7 @@ func TestKnotServesSubscribeRepos(t *testing.T) { assert.Less(t, account.Account.Seq, genesis.Seq, "account precedes the genesis commit") assert.Less(t, genesis.Seq, sync.Sync.Seq, "the commit precedes its assertion") - authedPost(t, "/xrpc/sh.tangled.repo.delete", deleteToken, map[string]any{ + authedPost(t, "/xrpc/org.tangled.repo.deleteRepo", deleteToken, map[string]any{ "repo": repoDid, "force": true, }) diff --git a/knot2/interop/knotfeed_consumer_test.go b/knot2/interop/knotfeed_consumer_test.go index 823ef92cc..19559d9f2 100644 --- a/knot2/interop/knotfeed_consumer_test.go +++ b/knot2/interop/knotfeed_consumer_test.go @@ -4,6 +4,7 @@ import ( "context" "log/slog" "os" + "strings" "sync/atomic" "testing" "time" @@ -21,8 +22,6 @@ func TestKnotfeedConsumerReadsRefRecords(t *testing.T) { require.NotEmpty(t, addr, "KNOT_TEST_ADDR names the knot's host:port") repoDid, err := syntax.ParseDID(os.Getenv("KNOT_TEST_REPO_DID")) require.NoError(t, err, "KNOT_TEST_REPO_DID names the pushed repository") - editor := os.Getenv("KNOT_TEST_EDITOR") - require.NotEmpty(t, editor, "KNOT_TEST_EDITOR names the pushing account") wantSha := os.Getenv("KNOT_TEST_SHA") require.NotEmpty(t, wantSha, "KNOT_TEST_SHA names the pushed tip") @@ -51,7 +50,7 @@ func TestKnotfeedConsumerReadsRefRecords(t *testing.T) { return nil } for _, op := range message.Commit.Records { - if op.Collection == knotfeed.GitRefCollection && op.Action == tapc.RecordCreateAction { + if op.Collection.String() == "org.tangled.git.refs" && op.Action == tapc.RecordCreateAction { select { case records <- op: default: @@ -65,18 +64,30 @@ func TestKnotfeedConsumerReadsRefRecords(t *testing.T) { select { case op := <-records: - refname, ok := knotfeed.UnescapeRkey(op.Rkey) - require.True(t, ok, "the record key %q decodes", op.Rkey) - assert.Equal(t, "refs/heads/main", refname) - rec, err := knotfeed.DecodeRefRecord(op.Bytes) + assert.Equal(t, "self", op.Rkey.String(), "the refs snapshot is a single self record") + def, refs, err := knotfeed.DecodeRefsSnapshot(op.Bytes) require.NoError(t, err, "the frozen record envelope decodes") - assert.Equal(t, wantSha, rec.Sha.String(), "the record carries the pushed tip") - assert.Equal(t, editor, rec.Editor.String(), "the record names the pusher") - require.Len(t, rec.PushOptions, 1) - assert.Equal(t, "skip-ci", rec.PushOptions[0], "the push options ride the undeclared field") + assert.Equal(t, "main", def, "the snapshot names the default branch") + var mainSha string + mainLines := 0 + for _, line := range strings.Split(string(refs), "\n") { + line = strings.TrimSpace(line) + if line == "" || line[0] == '#' || line[0] == '^' { + continue + } + sha, name, ok := strings.Cut(line, " ") + require.True(t, ok, "packed-refs lines pair an object id with a refname: %q", line) + if name != "refs/heads/main" { + continue + } + mainLines++ + mainSha = sha + } + require.Equal(t, 1, mainLines, "exactly one packed-refs line maps refs/heads/main") + assert.Equal(t, wantSha, mainSha, "refs/heads/main maps to the pushed tip") require.Eventually(t, func() bool { return cursor.Load() >= int64(1) }, 5*time.Second, 10*time.Millisecond, "the consumer advanced its cursor") case <-ctx.Done(): - t.Fatal("no ref record arrived before the deadline") + t.Fatal("no refs record arrived before the deadline") } } diff --git a/knotfeed/record.go b/knotfeed/record.go index 663d51fca..534ef8517 100644 --- a/knotfeed/record.go +++ b/knotfeed/record.go @@ -3,6 +3,7 @@ package knotfeed import ( "bytes" "fmt" + "io" "slices" "strings" "unicode/utf8" @@ -187,6 +188,56 @@ func readStringArray(r *bytes.Reader) ([]string, error) { return out, nil } +func DecodeRefsSnapshot(data []byte) (defaultBranch string, packedRefs []byte, err error) { + r := bytes.NewReader(data) + maj, count, err := cbg.CborReadHeader(r) + if err != nil { + return "", nil, err + } + if maj != cbg.MajMap { + return "", nil, fmt.Errorf("expected a cbor map, got major type %d", maj) + } + var sawBranch, sawRefs bool + for range count { + key, err := cbg.ReadString(r) + if err != nil { + return "", nil, err + } + switch key { + case "defaultBranch": + if defaultBranch, err = cbg.ReadString(r); err != nil { + return "", nil, err + } + sawBranch = true + case "refs": + maj, n, err := cbg.CborReadHeader(r) + if err != nil { + return "", nil, err + } + if maj != cbg.MajByteString { + return "", nil, fmt.Errorf("refs should be bytes, got major type %d", maj) + } + packedRefs = make([]byte, n) + if _, err := io.ReadFull(r, packedRefs); err != nil { + return "", nil, err + } + sawRefs = true + default: + var skipped cbg.Deferred + if err := skipped.UnmarshalCBOR(r); err != nil { + return "", nil, err + } + } + } + if !sawBranch { + return "", nil, fmt.Errorf("refs snapshot is missing defaultBranch") + } + if !sawRefs { + return "", nil, fmt.Errorf("refs snapshot is missing refs") + } + return defaultBranch, packedRefs, nil +} + type cborBuf struct{ bytes.Buffer } func (b *cborBuf) header(major byte, count uint64) { diff --git a/nix/Cargo.nix b/nix/Cargo.nix index d927d4bb7..182f25cd8 100644 --- a/nix/Cargo.nix +++ b/nix/Cargo.nix @@ -203,6 +203,16 @@ rec { # File a bug if you depend on any for non-debug work! debug = internal.debugCrate { inherit packageId; }; }; + "gitmirror-index" = rec { + packageId = "gitmirror-index"; + build = internal.buildRustCrateWithFeatures { + packageId = "gitmirror-index"; + }; + + # Debug support which might change between releases. + # File a bug if you depend on any for non-debug work! + debug = internal.debugCrate { inherit packageId; }; + }; "gitmirror-ingest" = rec { packageId = "gitmirror-ingest"; build = internal.buildRustCrateWithFeatures { @@ -3398,6 +3408,7 @@ rec { { name = "bobbin-types"; packageId = "bobbin-types"; + features = [ "streaming" ]; } { name = "bytes"; @@ -3437,6 +3448,10 @@ rec { packageId = "serde"; features = [ "derive" ]; } + { + name = "serde_ipld_dagcbor"; + packageId = "serde_ipld_dagcbor"; + } { name = "serde_json"; packageId = "serde_json"; @@ -3486,6 +3501,11 @@ rec { name = "http"; packageId = "http"; } + { + name = "ipld-core"; + packageId = "ipld-core"; + features = [ "serde" ]; + } { name = "opentelemetry_sdk"; packageId = "opentelemetry_sdk"; @@ -10297,6 +10317,10 @@ rec { name = "gitmirror-git"; packageId = "gitmirror-git"; } + { + name = "gitmirror-index"; + packageId = "gitmirror-index"; + } { name = "gitmirror-ingest"; packageId = "gitmirror-ingest"; @@ -10467,6 +10491,82 @@ rec { } ]; + }; + "gitmirror-index" = rec { + crateName = "gitmirror-index"; + version = "0.0.1"; + edition = "2024"; + src = lib.cleanSourceWith { filter = sourceFilter; src = ../gitmirror/crates/gitmirror-index; }; + libName = "gitmirror_index"; + license = "MIT"; + dependencies = [ + { + name = "bobbin-runtime"; + packageId = "bobbin-runtime"; + } + { + name = "gitmirror-db"; + packageId = "gitmirror-db"; + } + { + name = "gitmirror-git"; + packageId = "gitmirror-git"; + } + { + name = "gitmirror-types"; + packageId = "gitmirror-types"; + } + { + name = "jacquard-common"; + packageId = "jacquard-common"; + } + { + name = "knot-git"; + packageId = "knot-git"; + } + { + name = "knot-langs"; + packageId = "knot-langs"; + } + { + name = "knot-types"; + packageId = "knot-types"; + } + { + name = "reqwest"; + packageId = "reqwest 0.13.5"; + usesDefaultFeatures = false; + features = [ "rustls" "http2" "json" "gzip" "stream" ]; + } + { + name = "scc"; + packageId = "scc"; + } + { + name = "serde_json"; + packageId = "serde_json"; + features = [ "raw_value" ]; + } + { + name = "thiserror"; + packageId = "thiserror 2.0.18"; + } + { + name = "tokio"; + packageId = "tokio"; + features = [ "macros" "rt-multi-thread" "time" "signal" "io-util" "net" "sync" ]; + } + { + name = "tokio-util"; + packageId = "tokio-util"; + features = [ "rt" ]; + } + { + name = "tracing"; + packageId = "tracing"; + } + ]; + }; "gitmirror-ingest" = rec { crateName = "gitmirror-ingest"; @@ -10661,6 +10761,10 @@ rec { name = "bobbin-knot-proxy"; packageId = "bobbin-knot-proxy"; } + { + name = "bobbin-runtime"; + packageId = "bobbin-runtime"; + } { name = "jacquard-common"; packageId = "jacquard-common"; @@ -10706,6 +10810,10 @@ rec { name = "futures-lite"; packageId = "futures-lite"; } + { + name = "gitmirror-db"; + packageId = "gitmirror-db"; + } { name = "gitmirror-git"; packageId = "gitmirror-git"; @@ -10748,6 +10856,18 @@ rec { packageId = "jacquard-identity"; features = [ "cache" ]; } + { + name = "knot-git"; + packageId = "knot-git"; + } + { + name = "knot-langs"; + packageId = "knot-langs"; + } + { + name = "knot-types"; + packageId = "knot-types"; + } { name = "lexicons"; packageId = "lexicons"; @@ -19840,16 +19960,20 @@ rec { usesDefaultFeatures = false; features = [ "codegen" ]; } + { + name = "serde_json"; + packageId = "serde_json"; + features = [ "raw_value" ]; + } { name = "walkdir"; packageId = "walkdir"; } ]; features = { - "default" = [ "sh_tangled" ]; - "sh_tangled" = [ "com_atproto" ]; + "default" = [ "com_atproto" "org_tangled" ]; }; - resolvedDefaultFeatures = [ "com_atproto" "com_bad_example" "default" "org_tangled" "sh_tangled" "streaming" ]; + resolvedDefaultFeatures = [ "com_atproto" "default" "org_tangled" "streaming" ]; }; "knot-lfs" = rec { crateName = "knot-lfs"; @@ -20541,6 +20665,11 @@ rec { name = "tracing"; packageId = "tracing"; } + { + name = "url"; + packageId = "url"; + features = [ "serde" ]; + } ]; devDependencies = [ { @@ -21064,6 +21193,10 @@ rec { name = "h3-quinn"; packageId = "h3-quinn"; } + { + name = "jacquard-common"; + packageId = "jacquard-common"; + } { name = "knot-edge"; packageId = "knot-edge"; @@ -21076,6 +21209,10 @@ rec { name = "knot-lfs"; packageId = "knot-lfs"; } + { + name = "knot-record"; + packageId = "knot-record"; + } { name = "knot-ssh"; packageId = "knot-ssh";