Constellation, Spacedust, Slingshot, UFOs: atproto crates and services for microcosm

com.bad-example.identity.resolveMiniDoc

Changed files
+143 -8
slingshot
+6 -6
slingshot/src/identity.rs
···
///
/// partial because the handle is not verified
#[derive(Debug, Clone, Serialize, Deserialize)]
-
struct PartialMiniDoc {
/// an atproto handle (**unverified**)
///
/// the first valid atproto handle from the did doc's aka
-
unverified_handle: Handle,
/// the did's atproto pds url (TODO: type this?)
///
/// note: atrium *does* actually parse it into a URI, it just doesn't return
/// that for some reason
-
pds: String,
/// for now we're just pulling this straight from the did doc
///
/// would be nice to type and validate it
···
/// this is the publicKeyMultibase from the did doc.
/// legacy key encoding not supported.
/// `id`, `type`, and `controller` must be checked, but aren't stored.
-
signing_key: String,
}
impl TryFrom<DidDocument> for PartialMiniDoc {
···
Ok(Some(did))
}
-
/// Resolve (and verify!) a DID to a pds url
///
/// This *also* incidentally resolves and verifies the handle, which might
/// make it slower than expected
···
}
/// Fetch (and cache) a partial mini doc from a did
-
async fn did_to_partial_mini_doc(
&self,
did: &Did,
) -> Result<Option<PartialMiniDoc>, IdentityError> {
···
///
/// partial because the handle is not verified
#[derive(Debug, Clone, Serialize, Deserialize)]
+
pub struct PartialMiniDoc {
/// an atproto handle (**unverified**)
///
/// the first valid atproto handle from the did doc's aka
+
pub unverified_handle: Handle,
/// the did's atproto pds url (TODO: type this?)
///
/// note: atrium *does* actually parse it into a URI, it just doesn't return
/// that for some reason
+
pub pds: String,
/// for now we're just pulling this straight from the did doc
///
/// would be nice to type and validate it
···
/// this is the publicKeyMultibase from the did doc.
/// legacy key encoding not supported.
/// `id`, `type`, and `controller` must be checked, but aren't stored.
+
pub signing_key: String,
}
impl TryFrom<DidDocument> for PartialMiniDoc {
···
Ok(Some(did))
}
+
/// Resolve a DID to a pds url
///
/// This *also* incidentally resolves and verifies the handle, which might
/// make it slower than expected
···
}
/// Fetch (and cache) a partial mini doc from a did
+
pub async fn did_to_partial_mini_doc(
&self,
did: &Did,
) -> Result<Option<PartialMiniDoc>, IdentityError> {
+137 -2
slingshot/src/server.rs
···
ApiResponse, Object, OpenApi, OpenApiService, param::Query, payload::Json, types::Example,
};
fn example_did() -> String {
"did:plc:hdhoaan3xa3jiuq4fg4mefid".to_string()
}
···
example_collection(),
example_rkey()
)
}
#[derive(Object)]
···
})
}
-
fn bad_request_handler(err: poem::Error) -> GetRecordResponse {
GetRecordResponse::BadRequest(Json(XrpcErrorResponseObject {
error: "InvalidRequest".to_string(),
message: format!("Bad request, here's some info that maybe should not be exposed: {err}"),
}))
···
}
#[derive(ApiResponse)]
-
#[oai(bad_request_handler = "bad_request_handler")]
enum GetRecordResponse {
/// Record found
#[oai(status = 200)]
···
ServerError(XrpcError),
}
struct Xrpc {
cache: HybridCache<String, CachedRecord>,
identity: Identity,
···
cid,
)
.await
}
async fn get_record_impl(
···
ApiResponse, Object, OpenApi, OpenApiService, param::Query, payload::Json, types::Example,
};
+
fn example_handle() -> String {
+
"bad-example.com".to_string()
+
}
fn example_did() -> String {
"did:plc:hdhoaan3xa3jiuq4fg4mefid".to_string()
}
···
example_collection(),
example_rkey()
)
+
}
+
fn example_pds() -> String {
+
"https://porcini.us-east.host.bsky.network".to_string()
+
}
+
fn example_signing_key() -> String {
+
"zQ3shpq1g134o7HGDb86CtQFxnHqzx5pZWknrVX2Waum3fF6j".to_string()
}
#[derive(Object)]
···
})
}
+
fn bad_request_handler_get_record(err: poem::Error) -> GetRecordResponse {
GetRecordResponse::BadRequest(Json(XrpcErrorResponseObject {
+
error: "InvalidRequest".to_string(),
+
message: format!("Bad request, here's some info that maybe should not be exposed: {err}"),
+
}))
+
}
+
+
fn bad_request_handler_resolve_mini(err: poem::Error) -> ResolveMiniIDResponse {
+
ResolveMiniIDResponse::BadRequest(Json(XrpcErrorResponseObject {
error: "InvalidRequest".to_string(),
message: format!("Bad request, here's some info that maybe should not be exposed: {err}"),
}))
···
}
#[derive(ApiResponse)]
+
#[oai(bad_request_handler = "bad_request_handler_get_record")]
enum GetRecordResponse {
/// Record found
#[oai(status = 200)]
···
ServerError(XrpcError),
}
+
#[derive(Object)]
+
#[oai(example = true)]
+
struct MiniDocResponseObject {
+
/// DID, bi-directionally verified if a handle was provided in the query.
+
did: String,
+
/// The validated handle of the account or `handle.invalid` if the handle
+
/// did not bi-directionally match the DID document.
+
handle: String,
+
/// The identity's PDS URL
+
pds: String,
+
/// The atproto signing key publicKeyMultibase
+
///
+
/// Legacy key encoding not supported. the key is returned directly; `id`,
+
/// `type`, and `controller` are omitted.
+
signing_key: String,
+
}
+
impl Example for MiniDocResponseObject {
+
fn example() -> Self {
+
Self {
+
did: example_did(),
+
handle: example_handle(),
+
pds: example_pds(),
+
signing_key: example_signing_key(),
+
}
+
}
+
}
+
+
#[derive(ApiResponse)]
+
#[oai(bad_request_handler = "bad_request_handler_resolve_mini")]
+
enum ResolveMiniIDResponse {
+
/// Identity resolved
+
#[oai(status = 200)]
+
Ok(Json<MiniDocResponseObject>),
+
/// Bad request or identity not resolved
+
#[oai(status = 400)]
+
BadRequest(XrpcError),
+
}
+
struct Xrpc {
cache: HybridCache<String, CachedRecord>,
identity: Identity,
···
cid,
)
.await
+
}
+
+
/// com.bad-example.identity.resolveMiniDoc
+
///
+
/// Like [com.atproto.identity.resolveIdentity](https://docs.bsky.app/docs/api/com-atproto-identity-resolve-identity)
+
/// but instead of the full `didDoc` it returns an atproto-relevant subset.
+
#[oai(path = "/com.bad-example.identity.resolveMiniDoc", method = "get")]
+
async fn resolve_mini_id(
+
&self,
+
/// Handle or DID to resolve
+
#[oai(example = "example_handle")]
+
Query(identifier): Query<String>,
+
) -> ResolveMiniIDResponse {
+
let invalid = |reason: &'static str| {
+
ResolveMiniIDResponse::BadRequest(xrpc_error("InvalidRequest", reason))
+
};
+
+
let mut unverified_handle = None;
+
let did = match Did::new(identifier.clone()) {
+
Ok(did) => did,
+
Err(_) => {
+
let Ok(alleged_handle) = Handle::new(identifier) else {
+
return invalid("identifier was not a valid DID or handle");
+
};
+
if let Ok(res) = self.identity.handle_to_did(alleged_handle.clone()).await {
+
if let Some(did) = res {
+
// we did it joe
+
unverified_handle = Some(alleged_handle);
+
did
+
} else {
+
return invalid("Could not resolve handle identifier to a DID");
+
}
+
} else {
+
// TODO: ServerError not BadRequest
+
return invalid("errored while trying to resolve handle to DID");
+
}
+
}
+
};
+
let Ok(partial_doc) = self.identity.did_to_partial_mini_doc(&did).await else {
+
return invalid("failed to get DID doc");
+
};
+
let Some(partial_doc) = partial_doc else {
+
return invalid("failed to find DID doc");
+
};
+
+
// ok so here's where we're at:
+
// ✅ we have a DID
+
// ✅ we have a partial doc
+
// 🔶 if we have a handle, it's from the `identifier` (user-input)
+
// -> then we just need to compare to the partial doc to confirm
+
// -> else we need to resolve the DID doc's to a handle and check
+
let handle = if let Some(h) = unverified_handle {
+
if h == partial_doc.unverified_handle {
+
h.to_string()
+
} else {
+
"handle.invalid".to_string()
+
}
+
} else {
+
let Ok(handle_did) = self
+
.identity
+
.handle_to_did(partial_doc.unverified_handle.clone())
+
.await
+
else {
+
return invalid("failed to get did doc's handle");
+
};
+
let Some(handle_did) = handle_did else {
+
return invalid("failed to resolve did doc's handle");
+
};
+
if handle_did == did {
+
partial_doc.unverified_handle.to_string()
+
} else {
+
"handle.invalid".to_string()
+
}
+
};
+
+
ResolveMiniIDResponse::Ok(Json(MiniDocResponseObject {
+
did: did.to_string(),
+
handle,
+
pds: partial_doc.pds,
+
signing_key: partial_doc.signing_key,
+
}))
}
async fn get_record_impl(