From 6624ccf71b0e47458bbb555539d78c61f866eb02 Mon Sep 17 00:00:00 2001
From: dawn <90008@gaze.systems>
Date: Thu, 26 Mar 2026 01:36:40 +0300
Subject: [PATCH] [lib,api] implement getting mini doc,
blue.microcosm.identity.resolveMiniDoc and custom describeRepo
---
README.md | 23 +++++-
src/api/xrpc/describe_repo.rs | 85 ++++++++++++++++++++++
src/api/xrpc/mod.rs | 20 ++++++
src/control/mod.rs | 10 +--
src/control/repos.rs | 131 +++++++++++++++++++++++++++++++---
src/db/keys.rs | 12 ++--
src/util.rs | 16 ++++-
7 files changed, 278 insertions(+), 19 deletions(-)
create mode 100644 src/api/xrpc/describe_repo.rs
diff --git a/README.md b/README.md
index 35c3285..e2b996c 100644
--- a/README.md
+++ b/README.md
@@ -4,7 +4,7 @@
-> [vs tap](#vs-tap) | [stream](#stream-behavior) | [multi-relay](#multiple-relay-support) | [crawler sources](#crawler-sources)
-> [configuration](#configuration)
-> [rest api](#rest-api) | [filter](#filter-management) | [ingestion](#ingestion-control) | [crawler](#crawler-management) | [firehose](#firehose-management) | [repos](#repository-management)
--> [xrpc api](#data-access-xrpc) | [backlinks](#bluemicrocosmlinks) | [atproto](#comatproto) | [custom](#systemsgazehydrant)
+-> [xrpc api](#data-access-xrpc) | [backlinks](#bluemicrocosmlinks) | [identity](#bluemicrocosmidentity) | [atproto](#comatproto) | [custom](#systemsgazehydrant)
# hydrant
@@ -328,6 +328,19 @@ return the total number of stored records in a collection.
returns `{ count }`.
+#### systems.gaze.hydrant.describeRepo
+
+return account and identity information about this repo.
+this is equal to `com.atproto.repo.describeRepo`, except we don't return the full DID document.
+the handle is bi-directionally verified, if its invalid or the handle does not exist we return
+"handle.invalid".
+
+| param | required | description |
+| :--- | :--- | :--- |
+| `identifier` | yes | DID or handle of the repository. |
+
+returns `{ did, handle, pds, collections }`.
+
### blue.microcosm.links.*
[<- back to toc](#table-of-contents)
@@ -365,3 +378,11 @@ return the number of records that link to a given subject.
| `source` | no | filter by source collection (same format as `getBacklinks`). |
returns `{ count }`.
+
+### blue.microcosm.identity.*
+
+[<- back to toc](#table-of-contents)
+
+#### blue.microcosm.identity.resolveMiniDoc
+
+see [here](https://slingshot.microcosm.blue/#tag/slingshot-specific-queries/GET/xrpc/blue.microcosm.identity.resolveMiniDoc) for this XRPC's documentation.
diff --git a/src/api/xrpc/describe_repo.rs b/src/api/xrpc/describe_repo.rs
new file mode 100644
index 0000000..021f9ef
--- /dev/null
+++ b/src/api/xrpc/describe_repo.rs
@@ -0,0 +1,85 @@
+use std::collections::HashSet;
+
+use futures::TryFutureExt;
+use jacquard_common::types::{did::Did, nsid::Nsid, string::Handle};
+use smol_str::SmolStr;
+
+use crate::control::repos::MiniDocError;
+use crate::db::types::DidKey;
+
+use super::*;
+
+#[derive(Serialize, Deserialize, jacquard_derive::IntoStatic)]
+pub struct DescribeRepoOutput<'d> {
+ #[serde(borrow)]
+ pub did: Did<'d>,
+ #[serde(borrow)]
+ pub handle: Handle<'d>,
+ #[serde(serialize_with = "crate::util::did_key_serialize_str")]
+ #[serde(borrow)]
+ pub signing_key: DidKey<'d>,
+ pub pds: SmolStr,
+ #[serde(borrow)]
+ pub collections: HashSet>,
+}
+
+pub struct DescribeRepoResponse;
+impl jacquard_common::xrpc::XrpcResp for DescribeRepoResponse {
+ const NSID: &'static str = "systems.gaze.hydrant.describeRepo";
+ const ENCODING: &'static str = "application/json";
+ type Output<'de> = DescribeRepoOutput<'de>;
+ type Err<'de> = GenericXrpcError;
+}
+
+#[derive(Serialize, Deserialize, jacquard_derive::IntoStatic)]
+pub struct DescribeRepoRequestData<'i> {
+ #[serde(borrow)]
+ pub identifier: AtIdentifier<'i>,
+}
+
+impl<'a> jacquard_common::xrpc::XrpcRequest for DescribeRepoRequestData<'a> {
+ type Response = DescribeRepoResponse;
+ const NSID: &'static str = Self::Response::NSID;
+ const METHOD: jacquard_common::xrpc::XrpcMethod = jacquard_common::xrpc::XrpcMethod::Query;
+}
+
+pub struct DescribeRepo;
+impl jacquard_common::xrpc::XrpcEndpoint for DescribeRepo {
+ const PATH: &'static str = "/xrpc/systems.gaze.hydrant.describeRepo";
+ const METHOD: jacquard_common::xrpc::XrpcMethod = jacquard_common::xrpc::XrpcMethod::Query;
+ type Request<'de> = DescribeRepoRequestData<'de>;
+ type Response = DescribeRepoResponse;
+}
+
+pub async fn handle(
+ State(hydrant): State,
+ ExtractXrpc(req): ExtractXrpc,
+) -> XrpcResult>> {
+ let nsid = DescribeRepoResponse::NSID;
+ let did = hydrant
+ .state
+ .resolver
+ .resolve_did(&req.identifier)
+ .await
+ .map_err(|e| internal_error(nsid, format!("can't resolve identifier: {e}")))?;
+
+ let repo = hydrant.repos.get(&did);
+ let doc = repo.mini_doc().map_err(|e| match e {
+ MiniDocError::NotSynced => bad_request(nsid, "repo not synced"),
+ MiniDocError::RepoNotFound => bad_request(nsid, "repo not found"),
+ MiniDocError::CouldNotResolveIdentity => {
+ upstream_error(nsid, "identity could not be resolved")
+ }
+ MiniDocError::Other(e) => internal_error(nsid, e),
+ });
+ let collections = repo.collections().map_err(|e| internal_error(nsid, e));
+ let (doc, collections) = tokio::try_join!(doc, collections)?;
+
+ Ok(Json(DescribeRepoOutput {
+ did: doc.did,
+ handle: doc.handle,
+ pds: doc.pds.to_smolstr(),
+ signing_key: doc.signing_key,
+ collections: collections.into_iter().map(|(k, _)| k).collect(),
+ }))
+}
diff --git a/src/api/xrpc/mod.rs b/src/api/xrpc/mod.rs
index 32cc925..2208717 100644
--- a/src/api/xrpc/mod.rs
+++ b/src/api/xrpc/mod.rs
@@ -1,4 +1,5 @@
use crate::api::xrpc::count_records::CountRecords;
+use crate::api::xrpc::describe_repo::DescribeRepo;
use crate::control::Hydrant;
use axum::extract::FromRequest;
use axum::response::IntoResponse;
@@ -9,6 +10,7 @@ use jacquard_api::com_atproto::repo::{
list_records::{ListRecordsOutput, ListRecordsRequest, Record as RepoRecord},
};
use jacquard_common::types::ident::AtIdentifier;
+use jacquard_common::xrpc::XrpcResp;
use jacquard_common::xrpc::{XrpcEndpoint, XrpcMethod};
use jacquard_common::{IntoStatic, xrpc::XrpcRequest};
use jacquard_common::{
@@ -20,6 +22,7 @@ use smol_str::ToSmolStr;
use std::fmt::Display;
mod count_records;
+mod describe_repo;
mod get_record;
mod list_records;
@@ -28,6 +31,7 @@ pub fn router() -> Router {
.route(GetRecordRequest::PATH, get(get_record::handle))
.route(ListRecordsRequest::PATH, get(list_records::handle))
.route(CountRecords::PATH, get(count_records::handle))
+ .route(DescribeRepo::PATH, get(describe_repo::handle))
}
#[derive(Debug)]
@@ -110,3 +114,19 @@ fn bad_request(
}),
}
}
+
+fn upstream_error(
+ nsid: &'static str,
+ message: impl Display,
+) -> XrpcErrorResponse {
+ XrpcErrorResponse {
+ status: StatusCode::BAD_GATEWAY,
+ error: XrpcError::Generic(GenericXrpcError {
+ error: "UpstreamError".into(),
+ message: Some(message.to_smolstr()),
+ nsid,
+ method: "GET",
+ http_status: StatusCode::BAD_GATEWAY,
+ }),
+ }
+}
diff --git a/src/control/mod.rs b/src/control/mod.rs
index 3f92b1b..c03b114 100644
--- a/src/control/mod.rs
+++ b/src/control/mod.rs
@@ -1,8 +1,8 @@
-mod crawler;
-mod filter;
-mod firehose;
-mod repos;
-mod stream;
+pub(crate) mod crawler;
+pub(crate) mod filter;
+pub(crate) mod firehose;
+pub(crate) mod repos;
+pub(crate) mod stream;
pub use crawler::{CrawlerHandle, CrawlerSourceInfo};
pub use filter::{FilterControl, FilterPatch, FilterSnapshot};
diff --git a/src/control/repos.rs b/src/control/repos.rs
index 7cb71e3..7275398 100644
--- a/src/control/repos.rs
+++ b/src/control/repos.rs
@@ -1,3 +1,4 @@
+use std::collections::HashMap;
use std::sync::Arc;
use chrono::{DateTime, Utc};
@@ -5,6 +6,7 @@ use fjall::OwnedWriteBatch;
use jacquard_common::cowstr::ToCowStr;
use jacquard_common::types::cid::{Cid, IpldCid};
use jacquard_common::types::ident::AtIdentifier;
+use jacquard_common::types::nsid::Nsid;
use jacquard_common::types::string::{Did, Handle, Rkey};
use jacquard_common::types::tid::Tid;
use jacquard_common::{CowStr, Data, IntoStatic};
@@ -13,7 +15,7 @@ use rand::Rng;
use smol_str::ToSmolStr;
use url::Url;
-use crate::db::types::{DbRkey, TrimmedDid};
+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};
@@ -37,14 +39,17 @@ pub struct RepoInfo {
#[serde(skip_serializing_if = "Option::is_none")]
pub data: Option,
/// the handle for the DID of this repository.
+ ///
+ /// note that this handle is not bi-directionally verified.
#[serde(skip_serializing_if = "Option::is_none")]
pub handle: Option>,
/// the URL for the PDS in which this repository is hosted on.
#[serde(skip_serializing_if = "Option::is_none")]
pub pds: Option,
/// ATProto signing key of this repository.
+ #[serde(serialize_with = "crate::util::opt_did_key_serialize_str")]
#[serde(skip_serializing_if = "Option::is_none")]
- pub signing_key: Option,
+ pub signing_key: Option>,
/// when this repository was last touched (status update, commit ingested, etc.).
#[serde(skip_serializing_if = "Option::is_none")]
pub last_updated_at: Option>,
@@ -154,11 +159,11 @@ impl ReposControl {
}
/// gets a handle for a repository to read from it.
- pub fn get<'i>(&self, did: &Did<'i>) -> Result> {
- Ok(RepoHandle {
+ pub fn get<'i>(&self, did: &Did<'i>) -> RepoHandle<'i> {
+ RepoHandle {
state: self.0.clone(),
did: did.clone(),
- })
+ }
}
/// same as [`ReposControl::get`] but allows you to pass in an identifier that can be
@@ -171,10 +176,10 @@ impl ReposControl {
})
}
- /// fetch the current state of repository.
+ /// fetch the current state of a repository.
/// returns `None` if hydrant has never seen this repository.
pub async fn info(&self, did: &Did<'_>) -> Result