diff --git a/slingshot/src/identity.rs b/slingshot/src/identity.rs index a55821c..1abd0d0 100644 --- a/slingshot/src/identity.rs +++ b/slingshot/src/identity.rs @@ -54,16 +54,16 @@ enum IdentityData { /// /// partial because the handle is not verified #[derive(Debug, Clone, Serialize, Deserialize)] -struct PartialMiniDoc { +pub struct PartialMiniDoc { /// an atproto handle (**unverified**) /// /// the first valid atproto handle from the did doc's aka - unverified_handle: Handle, + 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 - pds: String, + pub pds: String, /// for now we're just pulling this straight from the did doc /// /// would be nice to type and validate it @@ -71,7 +71,7 @@ struct PartialMiniDoc { /// 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, + pub signing_key: String, } impl TryFrom for PartialMiniDoc { @@ -212,7 +212,7 @@ impl Identity { Ok(Some(did)) } - /// Resolve (and verify!) a DID to a pds url + /// Resolve a DID to a pds url /// /// This *also* incidentally resolves and verifies the handle, which might /// make it slower than expected @@ -271,7 +271,7 @@ impl Identity { } /// Fetch (and cache) a partial mini doc from a did - async fn did_to_partial_mini_doc( + pub async fn did_to_partial_mini_doc( &self, did: &Did, ) -> Result, IdentityError> { diff --git a/slingshot/src/server.rs b/slingshot/src/server.rs index 26612d6..595d364 100644 --- a/slingshot/src/server.rs +++ b/slingshot/src/server.rs @@ -25,6 +25,9 @@ use poem_openapi::{ 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() } @@ -42,6 +45,12 @@ fn example_uri() -> String { 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)] #[oai(example = true)] @@ -67,13 +76,20 @@ fn xrpc_error(error: impl AsRef, message: impl AsRef) -> XrpcError { }) } -fn bad_request_handler(err: poem::Error) -> GetRecordResponse { +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(Object)] #[oai(example = true)] struct FoundRecordResponseObject { @@ -108,7 +124,7 @@ impl Example for FoundRecordResponseObject { } #[derive(ApiResponse)] -#[oai(bad_request_handler = "bad_request_handler")] +#[oai(bad_request_handler = "bad_request_handler_get_record")] enum GetRecordResponse { /// Record found #[oai(status = 200)] @@ -127,6 +143,44 @@ enum GetRecordResponse { 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), + /// Bad request or identity not resolved + #[oai(status = 400)] + BadRequest(XrpcError), +} + struct Xrpc { cache: HybridCache, identity: Identity, @@ -223,6 +277,87 @@ impl Xrpc { .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, + ) -> 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( &self, repo: String,