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
··· 54 54 /// 55 55 /// partial because the handle is not verified 56 56 #[derive(Debug, Clone, Serialize, Deserialize)] 57 - struct PartialMiniDoc { 57 + pub struct PartialMiniDoc { 58 58 /// an atproto handle (**unverified**) 59 59 /// 60 60 /// the first valid atproto handle from the did doc's aka 61 - unverified_handle: Handle, 61 + pub unverified_handle: Handle, 62 62 /// the did's atproto pds url (TODO: type this?) 63 63 /// 64 64 /// note: atrium *does* actually parse it into a URI, it just doesn't return 65 65 /// that for some reason 66 - pds: String, 66 + pub pds: String, 67 67 /// for now we're just pulling this straight from the did doc 68 68 /// 69 69 /// would be nice to type and validate it ··· 71 71 /// this is the publicKeyMultibase from the did doc. 72 72 /// legacy key encoding not supported. 73 73 /// `id`, `type`, and `controller` must be checked, but aren't stored. 74 - signing_key: String, 74 + pub signing_key: String, 75 75 } 76 76 77 77 impl TryFrom<DidDocument> for PartialMiniDoc { ··· 212 212 Ok(Some(did)) 213 213 } 214 214 215 - /// Resolve (and verify!) a DID to a pds url 215 + /// Resolve a DID to a pds url 216 216 /// 217 217 /// This *also* incidentally resolves and verifies the handle, which might 218 218 /// make it slower than expected ··· 271 271 } 272 272 273 273 /// Fetch (and cache) a partial mini doc from a did 274 - async fn did_to_partial_mini_doc( 274 + pub async fn did_to_partial_mini_doc( 275 275 &self, 276 276 did: &Did, 277 277 ) -> Result<Option<PartialMiniDoc>, IdentityError> {
+137 -2
slingshot/src/server.rs
··· 25 25 ApiResponse, Object, OpenApi, OpenApiService, param::Query, payload::Json, types::Example, 26 26 }; 27 27 28 + fn example_handle() -> String { 29 + "bad-example.com".to_string() 30 + } 28 31 fn example_did() -> String { 29 32 "did:plc:hdhoaan3xa3jiuq4fg4mefid".to_string() 30 33 } ··· 41 44 example_collection(), 42 45 example_rkey() 43 46 ) 47 + } 48 + fn example_pds() -> String { 49 + "https://porcini.us-east.host.bsky.network".to_string() 50 + } 51 + fn example_signing_key() -> String { 52 + "zQ3shpq1g134o7HGDb86CtQFxnHqzx5pZWknrVX2Waum3fF6j".to_string() 44 53 } 45 54 46 55 #[derive(Object)] ··· 67 76 }) 68 77 } 69 78 70 - fn bad_request_handler(err: poem::Error) -> GetRecordResponse { 79 + fn bad_request_handler_get_record(err: poem::Error) -> GetRecordResponse { 71 80 GetRecordResponse::BadRequest(Json(XrpcErrorResponseObject { 81 + error: "InvalidRequest".to_string(), 82 + message: format!("Bad request, here's some info that maybe should not be exposed: {err}"), 83 + })) 84 + } 85 + 86 + fn bad_request_handler_resolve_mini(err: poem::Error) -> ResolveMiniIDResponse { 87 + ResolveMiniIDResponse::BadRequest(Json(XrpcErrorResponseObject { 72 88 error: "InvalidRequest".to_string(), 73 89 message: format!("Bad request, here's some info that maybe should not be exposed: {err}"), 74 90 })) ··· 108 124 } 109 125 110 126 #[derive(ApiResponse)] 111 - #[oai(bad_request_handler = "bad_request_handler")] 127 + #[oai(bad_request_handler = "bad_request_handler_get_record")] 112 128 enum GetRecordResponse { 113 129 /// Record found 114 130 #[oai(status = 200)] ··· 127 143 ServerError(XrpcError), 128 144 } 129 145 146 + #[derive(Object)] 147 + #[oai(example = true)] 148 + struct MiniDocResponseObject { 149 + /// DID, bi-directionally verified if a handle was provided in the query. 150 + did: String, 151 + /// The validated handle of the account or `handle.invalid` if the handle 152 + /// did not bi-directionally match the DID document. 153 + handle: String, 154 + /// The identity's PDS URL 155 + pds: String, 156 + /// The atproto signing key publicKeyMultibase 157 + /// 158 + /// Legacy key encoding not supported. the key is returned directly; `id`, 159 + /// `type`, and `controller` are omitted. 160 + signing_key: String, 161 + } 162 + impl Example for MiniDocResponseObject { 163 + fn example() -> Self { 164 + Self { 165 + did: example_did(), 166 + handle: example_handle(), 167 + pds: example_pds(), 168 + signing_key: example_signing_key(), 169 + } 170 + } 171 + } 172 + 173 + #[derive(ApiResponse)] 174 + #[oai(bad_request_handler = "bad_request_handler_resolve_mini")] 175 + enum ResolveMiniIDResponse { 176 + /// Identity resolved 177 + #[oai(status = 200)] 178 + Ok(Json<MiniDocResponseObject>), 179 + /// Bad request or identity not resolved 180 + #[oai(status = 400)] 181 + BadRequest(XrpcError), 182 + } 183 + 130 184 struct Xrpc { 131 185 cache: HybridCache<String, CachedRecord>, 132 186 identity: Identity, ··· 221 275 cid, 222 276 ) 223 277 .await 278 + } 279 + 280 + /// com.bad-example.identity.resolveMiniDoc 281 + /// 282 + /// Like [com.atproto.identity.resolveIdentity](https://docs.bsky.app/docs/api/com-atproto-identity-resolve-identity) 283 + /// but instead of the full `didDoc` it returns an atproto-relevant subset. 284 + #[oai(path = "/com.bad-example.identity.resolveMiniDoc", method = "get")] 285 + async fn resolve_mini_id( 286 + &self, 287 + /// Handle or DID to resolve 288 + #[oai(example = "example_handle")] 289 + Query(identifier): Query<String>, 290 + ) -> ResolveMiniIDResponse { 291 + let invalid = |reason: &'static str| { 292 + ResolveMiniIDResponse::BadRequest(xrpc_error("InvalidRequest", reason)) 293 + }; 294 + 295 + let mut unverified_handle = None; 296 + let did = match Did::new(identifier.clone()) { 297 + Ok(did) => did, 298 + Err(_) => { 299 + let Ok(alleged_handle) = Handle::new(identifier) else { 300 + return invalid("identifier was not a valid DID or handle"); 301 + }; 302 + if let Ok(res) = self.identity.handle_to_did(alleged_handle.clone()).await { 303 + if let Some(did) = res { 304 + // we did it joe 305 + unverified_handle = Some(alleged_handle); 306 + did 307 + } else { 308 + return invalid("Could not resolve handle identifier to a DID"); 309 + } 310 + } else { 311 + // TODO: ServerError not BadRequest 312 + return invalid("errored while trying to resolve handle to DID"); 313 + } 314 + } 315 + }; 316 + let Ok(partial_doc) = self.identity.did_to_partial_mini_doc(&did).await else { 317 + return invalid("failed to get DID doc"); 318 + }; 319 + let Some(partial_doc) = partial_doc else { 320 + return invalid("failed to find DID doc"); 321 + }; 322 + 323 + // ok so here's where we're at: 324 + // ✅ we have a DID 325 + // ✅ we have a partial doc 326 + // 🔶 if we have a handle, it's from the `identifier` (user-input) 327 + // -> then we just need to compare to the partial doc to confirm 328 + // -> else we need to resolve the DID doc's to a handle and check 329 + let handle = if let Some(h) = unverified_handle { 330 + if h == partial_doc.unverified_handle { 331 + h.to_string() 332 + } else { 333 + "handle.invalid".to_string() 334 + } 335 + } else { 336 + let Ok(handle_did) = self 337 + .identity 338 + .handle_to_did(partial_doc.unverified_handle.clone()) 339 + .await 340 + else { 341 + return invalid("failed to get did doc's handle"); 342 + }; 343 + let Some(handle_did) = handle_did else { 344 + return invalid("failed to resolve did doc's handle"); 345 + }; 346 + if handle_did == did { 347 + partial_doc.unverified_handle.to_string() 348 + } else { 349 + "handle.invalid".to_string() 350 + } 351 + }; 352 + 353 + ResolveMiniIDResponse::Ok(Json(MiniDocResponseObject { 354 + did: did.to_string(), 355 + handle, 356 + pds: partial_doc.pds, 357 + signing_key: partial_doc.signing_key, 358 + })) 224 359 } 225 360 226 361 async fn get_record_impl(