···5454///
5555/// partial because the handle is not verified
5656#[derive(Debug, Clone, Serialize, Deserialize)]
5757-struct PartialMiniDoc {
5757+pub struct PartialMiniDoc {
5858 /// an atproto handle (**unverified**)
5959 ///
6060 /// the first valid atproto handle from the did doc's aka
6161- unverified_handle: Handle,
6161+ pub unverified_handle: Handle,
6262 /// the did's atproto pds url (TODO: type this?)
6363 ///
6464 /// note: atrium *does* actually parse it into a URI, it just doesn't return
6565 /// that for some reason
6666- pds: String,
6666+ pub pds: String,
6767 /// for now we're just pulling this straight from the did doc
6868 ///
6969 /// would be nice to type and validate it
···7171 /// this is the publicKeyMultibase from the did doc.
7272 /// legacy key encoding not supported.
7373 /// `id`, `type`, and `controller` must be checked, but aren't stored.
7474- signing_key: String,
7474+ pub signing_key: String,
7575}
76767777impl TryFrom<DidDocument> for PartialMiniDoc {
···212212 Ok(Some(did))
213213 }
214214215215- /// Resolve (and verify!) a DID to a pds url
215215+ /// Resolve a DID to a pds url
216216 ///
217217 /// This *also* incidentally resolves and verifies the handle, which might
218218 /// make it slower than expected
···271271 }
272272273273 /// Fetch (and cache) a partial mini doc from a did
274274- async fn did_to_partial_mini_doc(
274274+ pub async fn did_to_partial_mini_doc(
275275 &self,
276276 did: &Did,
277277 ) -> Result<Option<PartialMiniDoc>, IdentityError> {
+137-2
slingshot/src/server.rs
···2525 ApiResponse, Object, OpenApi, OpenApiService, param::Query, payload::Json, types::Example,
2626};
27272828+fn example_handle() -> String {
2929+ "bad-example.com".to_string()
3030+}
2831fn example_did() -> String {
2932 "did:plc:hdhoaan3xa3jiuq4fg4mefid".to_string()
3033}
···4144 example_collection(),
4245 example_rkey()
4346 )
4747+}
4848+fn example_pds() -> String {
4949+ "https://porcini.us-east.host.bsky.network".to_string()
5050+}
5151+fn example_signing_key() -> String {
5252+ "zQ3shpq1g134o7HGDb86CtQFxnHqzx5pZWknrVX2Waum3fF6j".to_string()
4453}
45544655#[derive(Object)]
···6776 })
6877}
69787070-fn bad_request_handler(err: poem::Error) -> GetRecordResponse {
7979+fn bad_request_handler_get_record(err: poem::Error) -> GetRecordResponse {
7180 GetRecordResponse::BadRequest(Json(XrpcErrorResponseObject {
8181+ error: "InvalidRequest".to_string(),
8282+ message: format!("Bad request, here's some info that maybe should not be exposed: {err}"),
8383+ }))
8484+}
8585+8686+fn bad_request_handler_resolve_mini(err: poem::Error) -> ResolveMiniIDResponse {
8787+ ResolveMiniIDResponse::BadRequest(Json(XrpcErrorResponseObject {
7288 error: "InvalidRequest".to_string(),
7389 message: format!("Bad request, here's some info that maybe should not be exposed: {err}"),
7490 }))
···108124}
109125110126#[derive(ApiResponse)]
111111-#[oai(bad_request_handler = "bad_request_handler")]
127127+#[oai(bad_request_handler = "bad_request_handler_get_record")]
112128enum GetRecordResponse {
113129 /// Record found
114130 #[oai(status = 200)]
···127143 ServerError(XrpcError),
128144}
129145146146+#[derive(Object)]
147147+#[oai(example = true)]
148148+struct MiniDocResponseObject {
149149+ /// DID, bi-directionally verified if a handle was provided in the query.
150150+ did: String,
151151+ /// The validated handle of the account or `handle.invalid` if the handle
152152+ /// did not bi-directionally match the DID document.
153153+ handle: String,
154154+ /// The identity's PDS URL
155155+ pds: String,
156156+ /// The atproto signing key publicKeyMultibase
157157+ ///
158158+ /// Legacy key encoding not supported. the key is returned directly; `id`,
159159+ /// `type`, and `controller` are omitted.
160160+ signing_key: String,
161161+}
162162+impl Example for MiniDocResponseObject {
163163+ fn example() -> Self {
164164+ Self {
165165+ did: example_did(),
166166+ handle: example_handle(),
167167+ pds: example_pds(),
168168+ signing_key: example_signing_key(),
169169+ }
170170+ }
171171+}
172172+173173+#[derive(ApiResponse)]
174174+#[oai(bad_request_handler = "bad_request_handler_resolve_mini")]
175175+enum ResolveMiniIDResponse {
176176+ /// Identity resolved
177177+ #[oai(status = 200)]
178178+ Ok(Json<MiniDocResponseObject>),
179179+ /// Bad request or identity not resolved
180180+ #[oai(status = 400)]
181181+ BadRequest(XrpcError),
182182+}
183183+130184struct Xrpc {
131185 cache: HybridCache<String, CachedRecord>,
132186 identity: Identity,
···221275 cid,
222276 )
223277 .await
278278+ }
279279+280280+ /// com.bad-example.identity.resolveMiniDoc
281281+ ///
282282+ /// Like [com.atproto.identity.resolveIdentity](https://docs.bsky.app/docs/api/com-atproto-identity-resolve-identity)
283283+ /// but instead of the full `didDoc` it returns an atproto-relevant subset.
284284+ #[oai(path = "/com.bad-example.identity.resolveMiniDoc", method = "get")]
285285+ async fn resolve_mini_id(
286286+ &self,
287287+ /// Handle or DID to resolve
288288+ #[oai(example = "example_handle")]
289289+ Query(identifier): Query<String>,
290290+ ) -> ResolveMiniIDResponse {
291291+ let invalid = |reason: &'static str| {
292292+ ResolveMiniIDResponse::BadRequest(xrpc_error("InvalidRequest", reason))
293293+ };
294294+295295+ let mut unverified_handle = None;
296296+ let did = match Did::new(identifier.clone()) {
297297+ Ok(did) => did,
298298+ Err(_) => {
299299+ let Ok(alleged_handle) = Handle::new(identifier) else {
300300+ return invalid("identifier was not a valid DID or handle");
301301+ };
302302+ if let Ok(res) = self.identity.handle_to_did(alleged_handle.clone()).await {
303303+ if let Some(did) = res {
304304+ // we did it joe
305305+ unverified_handle = Some(alleged_handle);
306306+ did
307307+ } else {
308308+ return invalid("Could not resolve handle identifier to a DID");
309309+ }
310310+ } else {
311311+ // TODO: ServerError not BadRequest
312312+ return invalid("errored while trying to resolve handle to DID");
313313+ }
314314+ }
315315+ };
316316+ let Ok(partial_doc) = self.identity.did_to_partial_mini_doc(&did).await else {
317317+ return invalid("failed to get DID doc");
318318+ };
319319+ let Some(partial_doc) = partial_doc else {
320320+ return invalid("failed to find DID doc");
321321+ };
322322+323323+ // ok so here's where we're at:
324324+ // ✅ we have a DID
325325+ // ✅ we have a partial doc
326326+ // 🔶 if we have a handle, it's from the `identifier` (user-input)
327327+ // -> then we just need to compare to the partial doc to confirm
328328+ // -> else we need to resolve the DID doc's to a handle and check
329329+ let handle = if let Some(h) = unverified_handle {
330330+ if h == partial_doc.unverified_handle {
331331+ h.to_string()
332332+ } else {
333333+ "handle.invalid".to_string()
334334+ }
335335+ } else {
336336+ let Ok(handle_did) = self
337337+ .identity
338338+ .handle_to_did(partial_doc.unverified_handle.clone())
339339+ .await
340340+ else {
341341+ return invalid("failed to get did doc's handle");
342342+ };
343343+ let Some(handle_did) = handle_did else {
344344+ return invalid("failed to resolve did doc's handle");
345345+ };
346346+ if handle_did == did {
347347+ partial_doc.unverified_handle.to_string()
348348+ } else {
349349+ "handle.invalid".to_string()
350350+ }
351351+ };
352352+353353+ ResolveMiniIDResponse::Ok(Json(MiniDocResponseObject {
354354+ did: did.to_string(),
355355+ handle,
356356+ pds: partial_doc.pds,
357357+ signing_key: partial_doc.signing_key,
358358+ }))
224359 }
225360226361 async fn get_record_impl(