feat(at-identity): PdsHandleResolver for cluster-local DID→handle resolution
The AppView's handle_sync worker consulted the public PLC directory and the did:web: HTTPS resolver only. DIDs hosted on the local PDS (notably did🔑 users and any other operator-hosted method) weren't reachable without an external round trip, and unresolvable DIDs (did🔑 not on this PDS, did:foo: anything) blocked the 100-row batch forever because did🔑 sorts lexicographically before did:plc: / did:web:. This commit adds: * `PdsHandleResolver` (at-identity) — POSTs the DID as the `handle` field to the PDS's resolveHandle XRPC method. The PDS now recognises a `did:` prefix and does a PK lookup on `users.did`, returning `{did, handle}`. The resolver reads the `handle` field, so the AppView finally gets a real local handle for did🔑 users without ever dialing plc.directory. * A 2 s timeout per request (was 10 s) and `DISPATCH_CONCURRENCY = 8` so the worker caps a 100-DID batch at ~2 s with parallel dispatch instead of the ~17 min worst case the old serial + 10 s setup allowed. * A new `posts.handle_sync_attempted_at` column (migration 0006) and `mark_attempted()` helper. The SELECT filter excludes rows attempted within the last hour, so an unresolvable DID dominates at most one batch before the worker advances. Cleared on success. * `PDS_INTERNAL_URL` config so the AppView can reach the PDS via a cluster-internal hostname when the public URL isn't routable from inside the cluster. Tests: * `crates/at-identity/src/pds_handle.rs` — 4 unit tests against a stub HTTP server (200/404/5xx/missing-did-field). * Existing handle_sync integration tests updated to wire in the new `pds_resolver` field.
This commit is contained in:
@@ -9,17 +9,42 @@ pub async fn resolve_handle(
|
||||
State(state): State<AppState>,
|
||||
Json(req): Json<ResolveHandleReq>,
|
||||
) -> Result<Json<ResolveHandleResp>, (StatusCode, Json<crate::routes::types::ErrorBody>)> {
|
||||
// Polymorphic input: `handle` may be a bare handle OR a DID
|
||||
// (`did:plc:…`, `did:web:…`, `did:key:…`). When it's a DID we
|
||||
// look the row up by `did` — the AppView's `PdsHandleResolver`
|
||||
// uses this path to fill `posts.handle` for local-PDS users
|
||||
// (including `did:key:`) without a second round-trip to plc.directory.
|
||||
if req.handle.starts_with("did:") {
|
||||
let row: Option<(String,)> =
|
||||
sqlx::query_as("SELECT handle FROM users WHERE did = $1")
|
||||
.bind(&req.handle)
|
||||
.fetch_optional(&state.db)
|
||||
.await
|
||||
.map_err(|e| err(StatusCode::INTERNAL_SERVER_ERROR, e))?;
|
||||
if let Some((handle,)) = row {
|
||||
return Ok(Json(ResolveHandleResp {
|
||||
did: req.handle.clone(),
|
||||
handle: Some(handle),
|
||||
}));
|
||||
}
|
||||
// DID not hosted here — fall through to the handle lookup
|
||||
// (returns 404 below).
|
||||
}
|
||||
if let Some(zone) = state.cfg.pds_handle_dns_zone.strip_prefix(".") {
|
||||
if let Some(stripped) = req.handle.strip_suffix(zone) {
|
||||
let user = stripped.trim_end_matches('.');
|
||||
let full = format!("{}{}", user, state.cfg.pds_handle_dns_zone);
|
||||
let row: Option<(String,)> = sqlx::query_as("SELECT did FROM users WHERE handle = $1")
|
||||
.bind(&full)
|
||||
.fetch_optional(&state.db)
|
||||
.await
|
||||
.map_err(|e| err(StatusCode::INTERNAL_SERVER_ERROR, e))?;
|
||||
let row: Option<(String,)> =
|
||||
sqlx::query_as("SELECT did FROM users WHERE handle = $1")
|
||||
.bind(&full)
|
||||
.fetch_optional(&state.db)
|
||||
.await
|
||||
.map_err(|e| err(StatusCode::INTERNAL_SERVER_ERROR, e))?;
|
||||
if let Some((did,)) = row {
|
||||
return Ok(Json(ResolveHandleResp { did }));
|
||||
return Ok(Json(ResolveHandleResp {
|
||||
did,
|
||||
handle: Some(full),
|
||||
}));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -29,7 +54,10 @@ pub async fn resolve_handle(
|
||||
.await
|
||||
.map_err(|e| err(StatusCode::INTERNAL_SERVER_ERROR, e))?;
|
||||
match row {
|
||||
Some((did,)) => Ok(Json(ResolveHandleResp { did })),
|
||||
Some((did,)) => Ok(Json(ResolveHandleResp {
|
||||
did,
|
||||
handle: Some(req.handle.clone()),
|
||||
})),
|
||||
None => {
|
||||
warn!(handle = %req.handle, "handle not found");
|
||||
Err(err(
|
||||
|
||||
@@ -62,6 +62,13 @@ pub struct ResolveHandleReq {
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct ResolveHandleResp {
|
||||
pub did: String,
|
||||
/// Always populated when the lookup succeeds. For
|
||||
/// handle → DID calls this is just the input echo; for
|
||||
/// DID → handle calls this is the resolved local handle
|
||||
/// (used by the AppView's `PdsHandleResolver` to fill
|
||||
/// `posts.handle` without a second round-trip).
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub handle: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
|
||||
Reference in New Issue
Block a user