+6
-6
slingshot/src/identity.rs
+6
-6
slingshot/src/identity.rs
···
54
///
55
/// partial because the handle is not verified
56
#[derive(Debug, Clone, Serialize, Deserialize)]
57
-
struct PartialMiniDoc {
58
/// an atproto handle (**unverified**)
59
///
60
/// the first valid atproto handle from the did doc's aka
61
-
unverified_handle: Handle,
62
/// the did's atproto pds url (TODO: type this?)
63
///
64
/// note: atrium *does* actually parse it into a URI, it just doesn't return
65
/// that for some reason
66
-
pds: String,
67
/// for now we're just pulling this straight from the did doc
68
///
69
/// would be nice to type and validate it
···
71
/// this is the publicKeyMultibase from the did doc.
72
/// legacy key encoding not supported.
73
/// `id`, `type`, and `controller` must be checked, but aren't stored.
74
-
signing_key: String,
75
}
76
77
impl TryFrom<DidDocument> for PartialMiniDoc {
···
212
Ok(Some(did))
213
}
214
215
-
/// Resolve (and verify!) a DID to a pds url
216
///
217
/// This *also* incidentally resolves and verifies the handle, which might
218
/// make it slower than expected
···
271
}
272
273
/// Fetch (and cache) a partial mini doc from a did
274
-
async fn did_to_partial_mini_doc(
275
&self,
276
did: &Did,
277
) -> Result<Option<PartialMiniDoc>, IdentityError> {
···
54
///
55
/// partial because the handle is not verified
56
#[derive(Debug, Clone, Serialize, Deserialize)]
57
+
pub struct PartialMiniDoc {
58
/// an atproto handle (**unverified**)
59
///
60
/// the first valid atproto handle from the did doc's aka
61
+
pub unverified_handle: Handle,
62
/// the did's atproto pds url (TODO: type this?)
63
///
64
/// note: atrium *does* actually parse it into a URI, it just doesn't return
65
/// that for some reason
66
+
pub pds: String,
67
/// for now we're just pulling this straight from the did doc
68
///
69
/// would be nice to type and validate it
···
71
/// this is the publicKeyMultibase from the did doc.
72
/// legacy key encoding not supported.
73
/// `id`, `type`, and `controller` must be checked, but aren't stored.
74
+
pub signing_key: String,
75
}
76
77
impl TryFrom<DidDocument> for PartialMiniDoc {
···
212
Ok(Some(did))
213
}
214
215
+
/// Resolve a DID to a pds url
216
///
217
/// This *also* incidentally resolves and verifies the handle, which might
218
/// make it slower than expected
···
271
}
272
273
/// Fetch (and cache) a partial mini doc from a did
274
+
pub async fn did_to_partial_mini_doc(
275
&self,
276
did: &Did,
277
) -> Result<Option<PartialMiniDoc>, IdentityError> {
+137
-2
slingshot/src/server.rs
+137
-2
slingshot/src/server.rs
···
25
ApiResponse, Object, OpenApi, OpenApiService, param::Query, payload::Json, types::Example,
26
};
27
28
fn example_did() -> String {
29
"did:plc:hdhoaan3xa3jiuq4fg4mefid".to_string()
30
}
···
41
example_collection(),
42
example_rkey()
43
)
44
}
45
46
#[derive(Object)]
···
67
})
68
}
69
70
-
fn bad_request_handler(err: poem::Error) -> GetRecordResponse {
71
GetRecordResponse::BadRequest(Json(XrpcErrorResponseObject {
72
error: "InvalidRequest".to_string(),
73
message: format!("Bad request, here's some info that maybe should not be exposed: {err}"),
74
}))
···
108
}
109
110
#[derive(ApiResponse)]
111
-
#[oai(bad_request_handler = "bad_request_handler")]
112
enum GetRecordResponse {
113
/// Record found
114
#[oai(status = 200)]
···
127
ServerError(XrpcError),
128
}
129
130
struct Xrpc {
131
cache: HybridCache<String, CachedRecord>,
132
identity: Identity,
···
221
cid,
222
)
223
.await
224
}
225
226
async fn get_record_impl(
···
25
ApiResponse, Object, OpenApi, OpenApiService, param::Query, payload::Json, types::Example,
26
};
27
28
+
fn example_handle() -> String {
29
+
"bad-example.com".to_string()
30
+
}
31
fn example_did() -> String {
32
"did:plc:hdhoaan3xa3jiuq4fg4mefid".to_string()
33
}
···
44
example_collection(),
45
example_rkey()
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()
53
}
54
55
#[derive(Object)]
···
76
})
77
}
78
79
+
fn bad_request_handler_get_record(err: poem::Error) -> GetRecordResponse {
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 {
88
error: "InvalidRequest".to_string(),
89
message: format!("Bad request, here's some info that maybe should not be exposed: {err}"),
90
}))
···
124
}
125
126
#[derive(ApiResponse)]
127
+
#[oai(bad_request_handler = "bad_request_handler_get_record")]
128
enum GetRecordResponse {
129
/// Record found
130
#[oai(status = 200)]
···
143
ServerError(XrpcError),
144
}
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
+
184
struct Xrpc {
185
cache: HybridCache<String, CachedRecord>,
186
identity: Identity,
···
275
cid,
276
)
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
+
}))
359
}
360
361
async fn get_record_impl(