diff --git a/Cargo.lock b/Cargo.lock index 8fa8498..7747153 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -171,6 +171,7 @@ dependencies = [ "atrium-oauth", "base64", "lgtm-core", + "moka", "rand 0.8.6", "reqwest", "serde", diff --git a/crates/server/src/routes/mod.rs b/crates/server/src/routes/mod.rs index 981062b..0d07c51 100644 --- a/crates/server/src/routes/mod.rs +++ b/crates/server/src/routes/mod.rs @@ -2,6 +2,7 @@ pub mod buyer_auth; pub mod claims; pub mod entitlements; pub mod link; +pub mod pds; pub mod public; pub mod webhooks; pub mod xrpc; diff --git a/crates/server/src/routes/pds.rs b/crates/server/src/routes/pds.rs new file mode 100644 index 0000000..c44581a --- /dev/null +++ b/crates/server/src/routes/pds.rs @@ -0,0 +1,75 @@ +//! `GET /api/pds-capabilities?actor=` — reports whether an actor's PDS is +//! spaces-compatible (SPEC §8 / spikes/spaces-alpha). Consumed by the shop + `ssh lgtm.shop`. + +use std::sync::Arc; + +use axum::extract::Query; +use axum::response::IntoResponse; +use axum::routing::get; +use axum::{Extension, Json, Router}; +use serde_json::json; + +use atproto::{Actor, Resolver}; + +use crate::routes::xrpc::invalid_request; +use crate::state::AppState; + +/// Build the capability JSON. Pure over its inputs so the shape + degradation are unit-tested +/// without a network. `did`/`handle`/`pds` are null when unknown; `reason` is set only when +/// `spaces_compatible` is false. +fn capability_json( + actor: &str, + did: Option<&str>, + handle: Option<&str>, + pds: Option<&str>, + spaces_compatible: bool, + reason: Option<&str>, + checked_at: &str, +) -> serde_json::Value { + json!({ + "actor": actor, + "did": did, + "handle": handle, + "pds": pds, + "spacesCompatible": spaces_compatible, + "reason": reason, + "checkedAt": checked_at, + }) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn compatible_shape() { + let v = capability_json( + "natemoo.re", + Some("did:plc:abc"), + Some("natemoo.re"), + Some("https://pds.example"), + true, + None, + "2026-08-21T00:00:00Z", + ); + assert_eq!(v["spacesCompatible"], true); + assert_eq!(v["did"], "did:plc:abc"); + assert!(v["reason"].is_null()); + } + + #[test] + fn unresolved_degrades_gracefully() { + let v = capability_json( + "ghost.invalid", + None, + None, + None, + false, + Some("unresolved handle"), + "2026-08-21T00:00:00Z", + ); + assert_eq!(v["spacesCompatible"], false); + assert!(v["did"].is_null()); + assert_eq!(v["reason"], "unresolved handle"); + } +}