From 7fe0698ebe5ea6aafa5bd4d75a9efa9d960643df Mon Sep 17 00:00:00 2001 From: dawn <90008@gaze.systems> Date: Sat, 28 Mar 2026 02:56:15 +0300 Subject: [PATCH] [api] implement com.atproto.repo.describeRepo --- README.md | 1 + src/api/xrpc/com_atproto_describe_repo.rs | 49 +++++++++++++++++++++++ src/api/xrpc/mod.rs | 6 +++ src/control/repos.rs | 20 +++------ src/resolver.rs | 39 ++++++++++++++++++ src/util.rs | 6 ++- 6 files changed, 105 insertions(+), 16 deletions(-) create mode 100644 src/api/xrpc/com_atproto_describe_repo.rs diff --git a/README.md b/README.md index 14dd2c3..0876f42 100644 --- a/README.md +++ b/README.md @@ -307,6 +307,7 @@ these are standard atproto endpoints. you can look at [the atproto api reference the following are implemented currently: - `com.atproto.repo.getRecord` - `com.atproto.repo.listRecords` +- `com.atproto.repo.describeRepo` (also see `systems.gaze.hydrant.describeRepo`) - `com.atproto.sync.getRepo` (`since` parameter not implemented!) - `com.atproto.sync.getHostStatus` - `com.atproto.sync.listHosts` diff --git a/src/api/xrpc/com_atproto_describe_repo.rs b/src/api/xrpc/com_atproto_describe_repo.rs new file mode 100644 index 0000000..094b1ab --- /dev/null +++ b/src/api/xrpc/com_atproto_describe_repo.rs @@ -0,0 +1,49 @@ +use futures::TryFutureExt; +use jacquard_api::com_atproto::repo::describe_repo::{DescribeRepoOutput, DescribeRepoRequest}; + +use crate::util::invalid_handle; + +use super::*; + +pub async fn handle( + State(hydrant): State, + ExtractXrpc(req): ExtractXrpc, +) -> XrpcResult>> { + let nsid = "com.atproto.repo.describeRepo"; + let resolver = &hydrant.state.resolver; + + let did = resolver + .resolve_did(&req.repo) + .await + .map_err(|e| bad_request(nsid, format!("could not resolve identifier: {e}")))?; + + let repo = hydrant.repos.get(&did); + let ((did_doc, handle, handle_is_correct), collections) = tokio::try_join!( + async { + let (doc, handle) = resolver + .resolve_raw_doc(&did) + .map_err(|e| upstream_error(nsid, format!("could not resolve DID document: {e}"))) + .await?; + let (handle, handle_is_correct) = if let Some(h) = handle { + let is_correct = resolver + .verify_handle(&did, &h) + .map_err(|e| bad_request(nsid, format!("could not verify handle {h}: {e}"))) + .await?; + (h, is_correct) + } else { + (invalid_handle(), false) + }; + Ok((doc, handle, handle_is_correct)) + }, + repo.collections().map_err(|e| internal_error(nsid, e)), + )?; + + Ok(Json(DescribeRepoOutput { + did, + handle, + handle_is_correct, + did_doc, + collections: collections.into_iter().map(|(k, _)| k).collect(), + extra_data: Default::default(), + })) +} diff --git a/src/api/xrpc/mod.rs b/src/api/xrpc/mod.rs index 804945f..a868f5f 100644 --- a/src/api/xrpc/mod.rs +++ b/src/api/xrpc/mod.rs @@ -6,6 +6,7 @@ use axum::response::IntoResponse; use axum::routing::get; use axum::{Json, Router, extract::State, http::StatusCode}; use jacquard_api::com_atproto::repo::{ + describe_repo::DescribeRepoRequest as AtprotoDescribeRepoRequest, get_record::{GetRecordError, GetRecordOutput, GetRecordRequest}, list_records::{ListRecordsOutput, ListRecordsRequest, Record as RepoRecord}, }; @@ -27,6 +28,7 @@ use serde::{Deserialize, Serialize}; use smol_str::ToSmolStr; use std::fmt::Display; +mod com_atproto_describe_repo; mod count_records; mod describe_repo; mod get_host_status; @@ -44,6 +46,10 @@ pub fn router() -> Router { .route(ListRecordsRequest::PATH, get(list_records::handle)) .route(CountRecords::PATH, get(count_records::handle)) .route(DescribeRepo::PATH, get(describe_repo::handle)) + .route( + AtprotoDescribeRepoRequest::PATH, + get(com_atproto_describe_repo::handle), + ) .route(GetHostStatusRequest::PATH, get(get_host_status::handle)) .route(ListHostsRequest::PATH, get(list_hosts::handle)) .route(GetLatestCommitRequest::PATH, get(get_latest_commit::handle)) diff --git a/src/control/repos.rs b/src/control/repos.rs index 535a19b..e445999 100644 --- a/src/control/repos.rs +++ b/src/control/repos.rs @@ -20,6 +20,7 @@ use crate::db::types::{DbRkey, DidKey, TrimmedDid}; use crate::db::{self, Db, keys, ser_repo_state}; use crate::state::AppState; use crate::types::{GaugeState, RepoState, RepoStatus}; +use crate::util::invalid_handle; /// information about a tracked or known repository. returned by [`ReposControl`] methods. #[derive(Debug, Clone, serde::Serialize)] @@ -513,10 +514,6 @@ impl<'i> RepoHandle<'i> { /// returns a bi-directionally validated mini doc. pub async fn mini_doc(&self) -> Result, MiniDocError> { - fn invalid_handle() -> Handle<'static> { - unsafe { Handle::unchecked("handle.invalid") } - } - let Some(info) = self.info().await.map_err(MiniDocError::Other)? else { return Err(MiniDocError::RepoNotFound); }; @@ -533,22 +530,15 @@ impl<'i> RepoHandle<'i> { .ok_or_else(|| MiniDocError::CouldNotResolveIdentity)? .into_static(); - let handle = if let Some(handle_unverified) = info.handle { - let id = AtIdentifier::Handle(handle_unverified); - let handle_did = self + let handle = if let Some(h) = info.handle { + let is_valid = self .state .resolver - .resolve_did(&id) + .verify_handle(&self.did, &h) .await .into_diagnostic() .map_err(MiniDocError::Other)?; - - (handle_did == self.did) - .then(|| match id { - AtIdentifier::Handle(h) => h, - _ => unreachable!("can only be handle"), - }) - .unwrap_or_else(invalid_handle) + is_valid.then_some(h).unwrap_or_else(invalid_handle) } else { invalid_handle() }; diff --git a/src/resolver.rs b/src/resolver.rs index 623f9aa..544c7e7 100644 --- a/src/resolver.rs +++ b/src/resolver.rs @@ -4,6 +4,7 @@ use std::sync::Arc; use std::sync::atomic::{AtomicUsize, Ordering}; use std::time::Duration; +use jacquard_common::Data; use jacquard_common::IntoStatic; use jacquard_common::types::crypto::PublicKey; use jacquard_common::types::ident::AtIdentifier; @@ -204,6 +205,44 @@ impl Resolver { .ok_or_else(|| NoSigningKeyError(did)) .into_diagnostic()?) } + + /// resolves the full DID document as raw [`Data`] without caching, and + /// extracts the first `alsoKnownAs` handle if present. + pub async fn resolve_raw_doc( + &self, + did: &Did<'_>, + ) -> Result<(Data<'static>, Option>), ResolverError> { + let doc_resp = self + .req(did.starts_with("did:plc:"), |j| j.resolve_did_doc(did)) + .await?; + + let handle = { + let parsed = doc_resp.parse(); + parsed.ok().and_then(|doc| { + let mut handles = doc.handles(); + handles + .is_empty() + .not() + .then(|| handles.remove(0).into_static()) + }) + }; + + let data: Data<'_> = serde_json::from_slice(&doc_resp.buffer) + .map_err(|e| ResolverError::Generic(miette::miette!("failed to parse DID doc: {e}")))?; + + Ok((data.into_static(), handle)) + } + + /// returns `true` if the given handle bi-directionally resolves to `did`. + pub async fn verify_handle( + &self, + did: &Did<'_>, + handle: &Handle<'_>, + ) -> Result { + let id = AtIdentifier::Handle(handle.clone().into_static()); + let resolved_did = self.resolve_did(&id).await?; + Ok(resolved_did.as_str() == did.as_str()) + } } #[derive(Debug, Diagnostic, Error)] diff --git a/src/util.rs b/src/util.rs index 7596a86..42e0e27 100644 --- a/src/util.rs +++ b/src/util.rs @@ -1,6 +1,6 @@ use std::time::Duration; -use jacquard_common::deps::fluent_uri; +use jacquard_common::{deps::fluent_uri, types::string::Handle}; use rand::RngExt; use reqwest::StatusCode; use serde::{Deserialize, Deserializer, Serializer}; @@ -176,3 +176,7 @@ pub fn url_to_fluent_uri(url: &Url) -> fluent_uri::Uri { .expect("that url is validated") .to_owned() } + +pub(crate) fn invalid_handle() -> Handle<'static> { + unsafe { Handle::unchecked("handle.invalid") } +} -- 2.51.2