diff --git a/crates/server/src/routes/xrpc.rs b/crates/server/src/routes/xrpc.rs index 7aa8969..5d5970e 100644 --- a/crates/server/src/routes/xrpc.rs +++ b/crates/server/src/routes/xrpc.rs @@ -116,6 +116,28 @@ pub(crate) fn require_channel_assertion( .map_err(|e| assertion_error("InvalidChannelAssertion", &e.to_string())) } +/// Parse an actor query param (`?=`) and resolve it to a DID string. +/// Missing/empty -> 400; unparseable -> 400; a handle that won't resolve -> 404. A DID +/// input resolves with no network, so existing DID callers are unaffected. +async fn resolve_actor_param( + resolver: &atproto::Resolver, + params: &std::collections::HashMap, + name: &str, +) -> Result)> { + let raw = match params.get(name) { + Some(s) if !s.is_empty() => s, + _ => return Err(invalid_request(&format!("missing required param: {name}"))), + }; + let actor: atproto::Actor = raw + .parse() + .map_err(|_| invalid_request(&format!("{name} is not a valid DID or handle")))?; + resolver + .resolve_to_did(&actor) + .await + .map(|d| d.0) + .map_err(|_| not_found(&format!("{name} could not be resolved"))) +} + // ── Route table ─────────────────────────────────────────────────────────────── pub fn xrpc_routes() -> Router { @@ -169,6 +191,7 @@ pub fn xrpc_routes() -> Router { get(get_account), ) .layer(Extension(cart_store)) + .layer(Extension(std::sync::Arc::new(atproto::Resolver::from_env()))) } // ── Query-param / body structs ──────────────────────────────────────────────── @@ -1121,12 +1144,21 @@ async fn resolve_buyer_order_space( /// Returns `{ "addressRef": }`. async fn create_address( State(state): State, + Extension(resolver): Extension>, Json(body): Json, ) -> impl IntoResponse { - // Parse + validate buyer DID. - let buyer_did_str = match body.get("buyer").and_then(|v| v.as_str()) { - Some(s) => s.to_string(), - None => return invalid_request("missing required field: buyer").into_response(), + // Parse + resolve buyer (accepts a handle or a DID). + let buyer_raw = match body.get("buyer").and_then(|v| v.as_str()) { + Some(s) if !s.is_empty() => s, + _ => return invalid_request("missing required field: buyer").into_response(), + }; + let buyer_actor: atproto::Actor = match buyer_raw.parse() { + Ok(a) => a, + Err(_) => return invalid_request("buyer is not a valid DID or handle").into_response(), + }; + let buyer_did_str = match resolver.resolve_to_did(&buyer_actor).await { + Ok(d) => d.0, + Err(_) => return not_found("buyer could not be resolved").into_response(), }; // Parse + validate address body — required fields per lexicon. @@ -1183,11 +1215,12 @@ async fn create_address( /// Returns `{ "addresses": [
] }`. async fn list_addresses( State(state): State, + Extension(resolver): Extension>, Query(params): Query>, ) -> impl IntoResponse { - let buyer_did_str = match params.get("buyer") { - Some(s) if !s.is_empty() => s.clone(), - _ => return invalid_request("missing required param: buyer").into_response(), + let buyer_did_str = match resolve_actor_param(&resolver, ¶ms, "buyer").await { + Ok(d) => d, + Err(e) => return e.into_response(), }; let (_store_did, buyer_did, space, _tenant_id) = @@ -1221,12 +1254,13 @@ async fn list_addresses( async fn get_account( State(state): State, Extension(cart_store): Extension>, + Extension(resolver): Extension>, Query(params): Query>, ) -> impl IntoResponse { - // Require buyer param. - let buyer_did_str = match params.get("buyer") { - Some(s) if !s.is_empty() => s.clone(), - _ => return invalid_request("missing required param: buyer").into_response(), + // Require + resolve buyer param (accepts a handle or a DID). + let buyer_did_str = match resolve_actor_param(&resolver, ¶ms, "buyer").await { + Ok(d) => d, + Err(e) => return e.into_response(), }; // Resolve tenant + optional shop check. @@ -3096,4 +3130,18 @@ mod tests { assert_eq!(status, StatusCode::NOT_FOUND, "expected 404 for wrong shop; got {json}"); assert_eq!(json["error"], "NotFound", "expected NotFound; got {json}"); } + + /// A DID actor resolves with no network (so existing DID callers are unaffected), and a + /// missing param is a 400 — the two invariants the Actor retrofit must preserve. + #[tokio::test] + async fn resolve_actor_param_passes_through_a_did() { + let resolver = atproto::Resolver::from_env(); + let mut params = StdHashMap::new(); + params.insert("buyer".to_string(), "did:plc:abc123".to_string()); + let did = resolve_actor_param(&resolver, ¶ms, "buyer").await.unwrap(); + assert_eq!(did, "did:plc:abc123"); + + let empty: StdHashMap = StdHashMap::new(); + assert!(resolve_actor_param(&resolver, &empty, "buyer").await.is_err()); + } }