Skip to main content

didbot_serve/routes/
mod.rs

1//! The router: every route the development server answers.
2
3use std::sync::Arc;
4
5use axum::body::Bytes;
6use axum::extract::rejection::QueryRejection;
7use axum::extract::ws::{close_code, CloseFrame, Message, WebSocket, WebSocketUpgrade};
8use axum::extract::{Query, RawQuery, State};
9use axum::http::{header, HeaderMap, HeaderName, HeaderValue, Method, StatusCode};
10use axum::response::{IntoResponse, Response};
11use axum::routing::{get, post};
12use axum::{Json, Router};
13use didbot_identity::{canonical_did, ATPROTO_DID_PATH};
14use didbot_pds::{
15    Actor, Delta, Estop, EstopRefusal, Lock, Precondition, ProvisionError, Registry,
16    ServerLifecycle, ServerPolicy,
17};
18use serde::de::DeserializeOwned;
19use serde_json::json;
20use tower_http::cors::{Any, CorsLayer};
21use tower_http::timeout::TimeoutLayer;
22use tower_http::trace::{DefaultOnRequest, DefaultOnResponse, MakeSpan, TraceLayer};
23use tower_http::LatencyUnit;
24use tracing::{debug, info, warn, Level};
25
26use crate::auth;
27use crate::error::ApiError;
28use crate::health::HealthState;
29use crate::oauth::scope::Action;
30use crate::rate_limit::{caller_key, Peer, RateLimiter};
31use crate::subscribe::Repos;
32use crate::wire::{
33    record_uri, refuse_skipped_validation, repeated, require_known_lexicon, AccountSummary,
34    ApplyWritesRequest, CreateRecordRequest, CreateRecordResponse, DeleteRecordRequest,
35    DescribeRepoQuery, DescribeRepoResponse, DescribeServerResponse, DidRequest, GetRecordQuery,
36    GetRecordResponse, GetRepoQuery, LedgerQuery, ListRecordsQuery, ListReposQuery,
37    PutRecordRequest, RecordView, RepoWrite, StatsResponse, SyncGetRecordQuery, WriteResult,
38};
39
40mod accounts;
41mod identity;
42mod oauth;
43mod preferences;
44mod repo;
45mod session;
46mod sync;
47
48pub(crate) use accounts::*;
49pub(crate) use identity::*;
50pub(crate) use oauth::*;
51pub(crate) use preferences::*;
52pub(crate) use repo::*;
53pub(crate) use session::*;
54pub(crate) use sync::*;
55
56/// Session storage, the e-stop latch and the operator credential, bundled so
57/// a caller building [`app_with_auth`] hands them over together.
58///
59/// Separate from [`Registry`] on purpose: neither a session nor an e-stop
60/// engagement is provisioning, and bundling them into the same object-safe
61/// trait every backend must implement would grow that trait for every
62/// deployment whether or not it uses a local stop. The
63/// default is what every other builder here gets: fresh, empty, in-memory
64/// state and no operator credential configured, so `app` keeps behaving
65/// exactly as before this existed.
66#[derive(Clone)]
67pub struct AuthState {
68    /// The local emergency stop.
69    pub estop: Arc<Estop>,
70    /// Signing in as this deployment's operator, when it has one. `None`
71    /// when no `--operator` was given: there is no DID a sign-in could match,
72    /// so the operator routes answer that they are not configured rather
73    /// than offering a sign-in that can only fail. See
74    /// [`crate::operator::OperatorAuth`].
75    pub operator: Option<Arc<crate::operator::OperatorAuth>>,
76    /// The relay an operator may announce this server to. `None` when no
77    /// relay was named, and under a `.localhost` zone regardless; see
78    /// [`crate::relay`].
79    pub relay: Option<crate::relay::RelayTarget>,
80    /// Bounds `GET /dashboard/login` per caller address.
81    ///
82    /// That route takes no credential — signing in is how a caller gets one
83    /// — and answering it costs an identity resolution and a pushed
84    /// authorization request against a host the *caller* named. That is the
85    /// same shape as `POST /oauth/par`, which is already bounded this way,
86    /// and rather more outbound work.
87    pub operator_login_limiter: Arc<RateLimiter>,
88    /// Which `Credential::Disclosure` routes this deployment has closed.
89    /// Defaults to every route open; see `auth::Disclosure`.
90    pub disclosure: auth::Disclosure,
91    /// PAR, `authorize`, `token` and discovery; see [`crate::oauth::OAuthState`].
92    /// Defaults to in-memory stores and every policy hook refusing — see
93    /// that type's own doc.
94    pub oauth: crate::oauth::OAuthState,
95    /// Bounds `bot.did.createAccount` and `bot.did.createSession` calls per
96    /// caller address, the two together; see this module's
97    /// `PROVISION_RATE_LIMIT` and `crate::rate_limit`'s own doc for where
98    /// that address comes from and [`AuthState::trust_forwarded_headers`] for
99    /// when a caller-supplied header may stand in for it.
100    ///
101    /// `authorize` is keyed this way already;
102    /// `createAccount` mints an account — a signing key, a hostname, a blob
103    /// quota, a permanent ledger and firehose entry — and the claim
104    /// `Credential::Attested` checks is verified only after this budget, so
105    /// it needs the same address-keyed budget at least as much as either.
106    /// Both routes read a proof, which can fetch an issuer's keys or a DID
107    /// document before any signature is checked.
108    pub provision_rate_limiter: Arc<RateLimiter>,
109    /// Whether `bot.did.createAccount` with no credential creates beneath
110    /// the deployment's own account; see `tree::prove`.
111    ///
112    /// `false` unless a deployment turns it on: a public deployment creates
113    /// only for a creator that proved itself. `didbot-pds` turns it on
114    /// under `.localhost`, where nothing holds a credential yet, and
115    /// nowhere else.
116    pub unauthenticated_creates: bool,
117    /// How many accounts this deployment may hold before
118    /// `bot.did.createAccount` refuses to mint another; see
119    /// this module's `DEFAULT_MAX_ACCOUNTS` and `create_account`.
120    ///
121    /// Checked against [`didbot_pds::RegistryStats::accounts`] — the same
122    /// count `crate::health`'s periodic line already reports — rather than a
123    /// second counter kept here, so a cap and a dashboard can never disagree
124    /// about what "how many accounts" means.
125    pub max_accounts: u64,
126    /// Whether this deployment answers only callers on this host.
127    ///
128    /// A `.localhost` zone is a development run: `createAccount` mints with
129    /// no attestation claim there, and without a `--data` directory every
130    /// blob is held in memory — so a run reachable from the network hands
131    /// both to anybody sharing it. The listener binds every address because
132    /// that is the only single bind answering both `127.0.0.1` and `::1`
133    /// (see `didbot-pds`'s own comment), so the restriction is here: a
134    /// request whose peer address is not a loopback address is refused
135    /// before it reaches a route.
136    ///
137    /// `false` unless a deployment turns it on, which is the posture a real
138    /// zone wants: a deployment nobody can reach serves no agents.
139    pub loopback_callers_only: bool,
140    /// The `Strict-Transport-Security` header every response carries, if
141    /// any; see [`StrictTransportSecurity`].
142    ///
143    /// `None` unless a deployment turns it on. `didbot-pds` turns it on only
144    /// when it serves a production ACME certificate.
145    pub strict_transport_security: Option<StrictTransportSecurity>,
146    /// Bounds how many never-ending streams — `com.atproto.sync.subscribeRepos`
147    /// — this server holds open at once,
148    /// in total and per caller address; see
149    /// [`crate::subscribers::SubscriberLimit`].
150    ///
151    /// [`crate::MAX_CONCURRENT_REQUESTS`] does not cover these: its permit
152    /// is released when the response head is sent, which for a stream is
153    /// before the stream has carried a single frame.
154    pub subscriber_limit: Arc<crate::subscribers::SubscriberLimit>,
155    /// Bounds `bot.did.parkKey` and `describeParkedKey` per caller address;
156    /// see `tree::PARK_RATE_LIMIT`.
157    pub park_rate_limiter: Arc<RateLimiter>,
158    /// Bounds the `com.atproto.sync.*` reads per caller address; see
159    /// `routes::SYNC_READ_LIMIT`. Those reads are unauthenticated and each
160    /// can rebuild and sign a repository.
161    pub sync_rate_limiter: Arc<RateLimiter>,
162    /// Bounds each account's record writes and blob uploads, by operation
163    /// and by request byte; see [`crate::write_meter`].
164    pub write_meter: Arc<crate::write_meter::WriteMeter>,
165    /// Bounds each account's `atproto-proxy` calls, and caches where their
166    /// targets live; see [`crate::ProxyLimits`].
167    pub proxy: Arc<crate::ProxyLimits>,
168    /// The shortest `idleFor` a `neverWrote` account delete takes; see
169    /// [`crate::NEVER_WROTE_MIN_IDLE`].
170    pub never_wrote_min_idle: std::time::Duration,
171    /// Parked keys, spent proofs and issuers' keys; see [`crate::tree`].
172    pub tree: Arc<crate::tree::TreeState>,
173    /// Whether `X-Forwarded-For`/`X-Real-IP` may be trusted as the caller's
174    /// address for every per-address budget this router keeps —
175    /// [`AuthState::provision_rate_limiter`],
176    /// [`crate::oauth::OAuthState::par_rate_limiter`] and
177    /// [`crate::oauth::OAuthState::authorize_rate_limiter`] — instead of the
178    /// TCP peer address `crate::rate_limit::caller_key` gets by default.
179    ///
180    /// `false` unless a deployment turns it on — see `crate::rate_limit`'s
181    /// own doc for why trusting a caller-supplied header by default would
182    /// make every per-address budget bypassable rather than merely
183    /// degraded. Turn this on only once a real reverse proxy sits in front
184    /// of this server and sets whichever header it reads.
185    pub trust_forwarded_headers: bool,
186    /// Where this deployment is in its own lifecycle; see
187    /// [`didbot_pds::ServerLifecycle`] and this module's own
188    /// `require_lifecycle`, which is the single point every capability
189    /// question goes through.
190    ///
191    /// Defaults to [`ServerLifecycle::claimed`] — a server that exists and
192    /// works — because that is what every builder in this crate other than
193    /// `didbot-pds`'s own path is: a test, or a `.localhost` run. A real
194    /// deployment hands in one built by
195    /// [`ServerLifecycle::booting`](didbot_pds::ServerLifecycle::booting) and
196    /// driven by a store read and an operator poll.
197    pub lifecycle: Arc<ServerLifecycle>,
198    /// Where `bot.did.pollOperatorClaim` delivers its "look now" request;
199    /// see [`crate::PollNudge`] and this module's own `poll_operator_claim`.
200    ///
201    /// Defaults to a channel with no poll task attached, which absorbs
202    /// nudges and does nothing — the correct behaviour for a build with no
203    /// operator repository to read.
204    pub nudge: Arc<crate::PollNudge>,
205    /// What the operator poll's last listing of the operator's repository
206    /// read, for the dashboard's provenance panel. Shared with the poll,
207    /// which is the only thing that writes to it; see
208    /// [`crate::OperatorRecords`].
209    pub operator_records: Arc<crate::OperatorRecords>,
210    /// The operator's handle as the operator poll last verified it, for the
211    /// homepage. Shared with the poll, which is the only thing that writes
212    /// to it; see [`crate::OperatorHandle`].
213    pub operator_handle: Arc<crate::OperatorHandle>,
214    /// Where `com.atproto.identity.resolveHandle` looks up a handle no
215    /// account here holds; see [`crate::handles`]. Defaults to the network,
216    /// through Cloudflare's resolver.
217    pub handles: Arc<dyn crate::handles::HandleDid>,
218    /// Bounds those lookups per caller address; see
219    /// `routes::HANDLE_LOOKUP_LIMIT`.
220    pub handle_lookup_limiter: Arc<RateLimiter>,
221    /// Bounds `com.atproto.identity.resolveDid`'s lookups of DIDs hosted
222    /// elsewhere per caller address; see `routes::DID_LOOKUP_LIMIT`.
223    pub did_lookup_limiter: Arc<RateLimiter>,
224    /// The lookups this deployment's policies declare, shared with the
225    /// policy poll, which is the only thing that writes to it; see
226    /// [`crate::DeclaredLookups`].
227    pub declared_lookups: Arc<crate::DeclaredLookups>,
228}
229
230impl Default for AuthState {
231    fn default() -> Self {
232        Self {
233            estop: Arc::new(Estop::default()),
234            operator: None,
235            relay: None,
236            operator_login_limiter: Arc::new(RateLimiter::new(
237                OPERATOR_LOGIN_LIMIT,
238                OPERATOR_LOGIN_WINDOW,
239            )),
240            disclosure: auth::Disclosure::default(),
241            oauth: crate::oauth::OAuthState::default(),
242            provision_rate_limiter: Arc::new(RateLimiter::new(
243                PROVISION_RATE_LIMIT,
244                PROVISION_RATE_WINDOW,
245            )),
246            max_accounts: DEFAULT_MAX_ACCOUNTS,
247            subscriber_limit: Arc::new(crate::subscribers::SubscriberLimit::default()),
248            park_rate_limiter: Arc::new(RateLimiter::new(
249                crate::tree::PARK_RATE_LIMIT,
250                crate::tree::PARK_RATE_WINDOW,
251            )),
252            sync_rate_limiter: Arc::new(RateLimiter::new(SYNC_READ_LIMIT, SYNC_READ_WINDOW)),
253            write_meter: Arc::default(),
254            proxy: Arc::new(crate::ProxyLimits::default()),
255            never_wrote_min_idle: crate::NEVER_WROTE_MIN_IDLE,
256            tree: Arc::new(crate::tree::TreeState::default()),
257            unauthenticated_creates: false,
258            loopback_callers_only: false,
259            strict_transport_security: None,
260            trust_forwarded_headers: false,
261            lifecycle: Arc::new(ServerLifecycle::claimed()),
262            nudge: Arc::new(crate::PollNudge::new()),
263            operator_records: Arc::new(crate::OperatorRecords::new()),
264            operator_handle: Arc::default(),
265            handles: Arc::new(crate::handles::Network::default()),
266            handle_lookup_limiter: Arc::new(RateLimiter::new(
267                HANDLE_LOOKUP_LIMIT,
268                HANDLE_LOOKUP_WINDOW,
269            )),
270            did_lookup_limiter: Arc::new(RateLimiter::new(DID_LOOKUP_LIMIT, DID_LOOKUP_WINDOW)),
271            declared_lookups: Arc::new(crate::DeclaredLookups::new()),
272        }
273    }
274}
275
276/// Everything a handler needs.
277///
278/// `Registry` is deliberately an `Arc<dyn ..>`: the HTTP layer must not be
279/// generic over the provisioner's three type parameters, or every handler
280/// signature grows them.
281#[derive(Clone)]
282pub(crate) struct AppState {
283    pub(crate) registry: Arc<dyn Registry>,
284    repos: Repos,
285    pub(crate) estop: Arc<Estop>,
286    /// See [`AuthState::operator`].
287    pub(crate) operator: Option<Arc<crate::operator::OperatorAuth>>,
288    /// See [`AuthState::relay`].
289    pub(crate) relay: Option<crate::relay::RelayTarget>,
290    /// See [`AuthState::operator_login_limiter`].
291    pub(crate) operator_login_limiter: Arc<RateLimiter>,
292    /// Which `Credential::Disclosure` routes are closed; see
293    /// `auth::Disclosure`.
294    pub(crate) disclosure: auth::Disclosure,
295    pub(crate) oauth: crate::oauth::OAuthState,
296    /// See [`AuthState::provision_rate_limiter`].
297    pub(crate) provision_rate_limiter: Arc<RateLimiter>,
298    /// See [`AuthState::max_accounts`].
299    pub(crate) max_accounts: u64,
300    /// See [`AuthState::unauthenticated_creates`].
301    pub(crate) unauthenticated_creates: bool,
302    /// See [`AuthState::park_rate_limiter`].
303    pub(crate) park_rate_limiter: Arc<RateLimiter>,
304    /// See [`AuthState::sync_rate_limiter`].
305    pub(crate) sync_rate_limiter: Arc<RateLimiter>,
306    /// See [`AuthState::write_meter`].
307    pub(crate) write_meter: Arc<crate::write_meter::WriteMeter>,
308    /// See [`AuthState::proxy`].
309    pub(crate) proxy: Arc<crate::ProxyLimits>,
310    /// See [`AuthState::never_wrote_min_idle`].
311    pub(crate) never_wrote_min_idle: std::time::Duration,
312    /// See [`AuthState::tree`].
313    pub(crate) tree: Arc<crate::tree::TreeState>,
314    /// See [`AuthState::subscriber_limit`].
315    pub(crate) subscriber_limit: Arc<crate::subscribers::SubscriberLimit>,
316    /// See [`AuthState::trust_forwarded_headers`].
317    pub(crate) trust_forwarded_headers: bool,
318    pub(crate) health: Arc<HealthState>,
319    /// See [`AuthState::lifecycle`] and [`require_lifecycle`].
320    pub(crate) lifecycle: Arc<ServerLifecycle>,
321    /// See [`AuthState::nudge`].
322    pub(crate) nudge: Arc<crate::PollNudge>,
323    /// See [`AuthState::operator_records`].
324    pub(crate) operator_records: Arc<crate::OperatorRecords>,
325    /// See [`AuthState::operator_handle`].
326    pub(crate) operator_handle: Arc<crate::OperatorHandle>,
327    /// See [`AuthState::handles`].
328    pub(crate) handles: Arc<dyn crate::handles::HandleDid>,
329    /// See [`AuthState::handle_lookup_limiter`].
330    pub(crate) handle_lookup_limiter: Arc<RateLimiter>,
331    /// See [`AuthState::did_lookup_limiter`].
332    pub(crate) did_lookup_limiter: Arc<RateLimiter>,
333    /// The one reader for records in repositories this deployment does not
334    /// hold; see `crate::foreign`. Built here rather than carried on
335    /// [`AuthState`] because the two facts it needs — which repositories
336    /// are ours and which names we answer to — are the registry's, and the
337    /// registry arrives with the router rather than with the auth state.
338    pub(crate) foreign: Arc<didbot_lookup::ForeignRecords>,
339    /// The two-phase read of the records a write's policies declared they
340    /// read: this half prefetches, before the write reaches the store's
341    /// synchronous section. See `crate::lookups`.
342    pub(crate) lookups: Arc<crate::LookupReader>,
343}
344
345/// How many `bot.did.createAccount` and `bot.did.createSession` calls one
346/// caller address may make per [`PROVISION_RATE_WINDOW`], the two together.
347///
348/// Sized against a legitimate caller rather than against a read route:
349/// `crates/didbot-swarm`, the local agent simulator, defaults to 8 events per
350/// second (`didbot-swarm --rate`, `scripts/dev-swarm.sh`) and spends most of
351/// them on `Spawn` while its population is below target, all from the one
352/// address a local run shares.
353/// Sustained, that is up to 480 provisioning calls a minute from a single
354/// caller with nothing wrong — the case `plan/pds-xrpc.md`'s "what \[rate
355/// limits\] do to a swarm sharing one address" names. This limit clears that
356/// with better than double the headroom, while still bounding what a caller
357/// this route does not authenticate can spend of this deployment's signing
358/// keys, Route53 record-set quota and blob storage per minute.
359pub const PROVISION_RATE_LIMIT: u32 = 1000;
360
361/// The window [`PROVISION_RATE_LIMIT`] is measured over.
362pub const PROVISION_RATE_WINDOW: std::time::Duration = std::time::Duration::from_secs(60);
363
364/// How many `GET /dashboard/login` calls one caller address may make per
365/// [`OPERATOR_LOGIN_WINDOW`]; see [`AuthState::operator_login_limiter`].
366///
367/// A person signing in makes one, retries a handful of times at worst, and
368/// is done. The number is small because this route is unauthenticated and
369/// each call costs an outbound identity resolution plus a pushed
370/// authorization request to a host the caller named.
371pub const OPERATOR_LOGIN_LIMIT: u32 = 20;
372
373/// The window [`OPERATOR_LOGIN_LIMIT`] is measured over.
374pub const OPERATOR_LOGIN_WINDOW: std::time::Duration = std::time::Duration::from_secs(60);
375
376/// How many `com.atproto.sync.*` reads one caller address may make per
377/// [`SYNC_READ_WINDOW`]; see [`AuthState::sync_rate_limiter`].
378///
379/// **Reasonable range: 60 to 60000.** Each of these reads can rebuild and
380/// sign a repository on the blocking pool, and none of them is
381/// authenticated. Six hundred a minute is ten a second sustained from one
382/// address, which clears a relay backfilling this deployment's repositories
383/// and a `didbot-swarm` run sharing one address, and still bounds what a
384/// stranger can spend of the pool.
385pub const SYNC_READ_LIMIT: u32 = 600;
386
387/// The window [`SYNC_READ_LIMIT`] is measured over.
388pub const SYNC_READ_WINDOW: std::time::Duration = std::time::Duration::from_secs(60);
389
390/// How many handles no account here holds one caller address may ask
391/// `com.atproto.identity.resolveHandle` to look up per
392/// [`HANDLE_LOOKUP_WINDOW`]; see [`AuthState::handle_lookup_limiter`].
393///
394/// **Reasonable range: 10 to 600.** Each one costs a DNS-over-HTTPS query
395/// and can cost an HTTPS fetch from a host the caller named, and none is
396/// authenticated. A browser client resolves one handle per sign-in. A
397/// handle hosted here costs no lookup and spends nothing.
398pub const HANDLE_LOOKUP_LIMIT: u32 = 60;
399
400/// The window [`HANDLE_LOOKUP_LIMIT`] is measured over.
401pub const HANDLE_LOOKUP_WINDOW: std::time::Duration = std::time::Duration::from_secs(60);
402
403/// How many DIDs hosted elsewhere one caller address may ask
404/// `com.atproto.identity.resolveDid` to fetch per [`DID_LOOKUP_WINDOW`]; see
405/// [`AuthState::did_lookup_limiter`].
406///
407/// **Reasonable range: 10 to 600.** Each one costs an HTTPS fetch from a
408/// host the caller named or from the PLC directory, and none is
409/// authenticated. A client resolves a few DIDs as it opens a view. A DID
410/// hosted here costs no lookup and spends nothing.
411pub const DID_LOOKUP_LIMIT: u32 = 60;
412
413/// The window [`DID_LOOKUP_LIMIT`] is measured over.
414pub const DID_LOOKUP_WINDOW: std::time::Duration = std::time::Duration::from_secs(60);
415
416/// How many `com.atproto.sync.*` reads may hold a blocking-pool thread at
417/// once, across every caller.
418///
419/// The same bound `crate::blobs`'s `BLOB_READ_CONCURRENCY` puts on
420/// `getBlob`, and for the same reason: these reads run on the pool session
421/// creation, provisioning, deletion and firehose publishing also draw from,
422/// and each can rebuild the Merkle search tree and sign a commit. Without a
423/// bound, [`crate::MAX_CONCURRENT_REQUESTS`] is the only one, and the
424/// deployment runs two async workers.
425///
426/// A caller that finds every permit taken waits for one, and
427/// [`REQUEST_TIMEOUT`]'s layer answers `503` if that wait outlasts the
428/// request.
429static SYNC_READ_CONCURRENCY: tokio::sync::Semaphore = tokio::sync::Semaphore::const_new(32);
430
431/// How many accounts a deployment may hold at once, absent `--max-accounts`
432/// or a `[capacity]` section; see [`AuthState::max_accounts`].
433///
434/// `plan/capacity.md` computes the raw ceiling this has to stay under: two
435/// Route53 record sets per account against the default 10,000-record quota
436/// is five thousand accounts before AWS's own quota — not this server's —
437/// refuses a write mid-provisioning. A thousand is well under half of that,
438/// leaving headroom for the zone's fixed `_acme-challenge` overhead and for
439/// the time a quota increase takes to land, and it is already generous for
440/// the fleet this project's own README describes: an agent operator's own
441/// instance, not a multi-tenant population.
442pub const DEFAULT_MAX_ACCOUNTS: u64 = 1000;
443/// How long any one request may take before this router gives up on it and
444/// answers `503` with an empty body.
445///
446/// Applies to every route, not only the anonymous ones the rate limiters
447/// single out: a slow backend or a stalled write should not be able to hold
448/// a connection (and the concurrency slot it occupies) open indefinitely.
449/// The body is empty rather than this crate's usual `{"error":..,"message":..}`
450/// shape — `tower_http::timeout::TimeoutLayer` answers with a bare status and
451/// a default body directly, without going through this router's handlers or
452/// its error type at all, which is what makes it a backstop that keeps
453/// working even if a handler itself is the thing hanging.
454pub const REQUEST_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(30);
455
456/// The XRPC methods this server serves: for each one, the handler behind it
457/// **and the credential it accepts**.
458///
459/// One list producing three things — the routes themselves, the names of
460/// them, and the credential table `crate::auth` resolves against. That is
461/// the whole point of the macro: there is no way to write a `.route()` call
462/// for an XRPC method except by adding an entry here, and an entry with no
463/// `as <Credential>` is a syntax error rather than a route that quietly
464/// defaults to open. The three lists cannot drift because there is only one
465/// list.
466///
467/// Expands to `$methods` (the NSIDs, for the conformance harness),
468/// `$credentials` (the NSID → `auth::Credential` table, which
469/// `auth::ROUTE_CREDENTIALS` is assembled from) and `$router` (the
470/// routes those names stand for).
471macro_rules! xrpc_methods {
472    (
473        $(#[$methods_meta:meta])*
474        $methods:ident,
475        $(#[$credentials_meta:meta])*
476        $credentials:ident,
477        $router:ident,
478        { $($nsid:literal => $verb:ident($handler:path) as $credential:ident),* $(,)? }
479    ) => {
480        $(#[$methods_meta])*
481        pub const $methods: &[&str] = &[$($nsid),*];
482
483        $(#[$credentials_meta])*
484        pub const $credentials: &[(&str, auth::Credential)] =
485            &[$(($nsid, auth::Credential::$credential)),*];
486
487        /// The routes those names stand for.
488        fn $router() -> Router<AppState> {
489            Router::new()$(.route(concat!("/xrpc/", $nsid), $verb($handler)))*
490        }
491    };
492}
493
494xrpc_methods! {
495    /// Every `com.atproto.*` method this router answers, in NSID order.
496    ///
497    /// Public because it is what
498    /// `crates/didbot/tests/conformance/wire.rs` reads: every name
499    /// here must be a method in the vendored upstream documents, so a
500    /// route this server invents fails a test rather than shipping.
501    ATPROTO_METHODS,
502    /// The credential each of those methods accepts. See
503    /// `auth::Credential` for what each kind means, and that module's
504    /// "Why every public route is public" for why the read surface is
505    /// `Public` on purpose: protocol-required for `com.atproto.sync.*` and
506    /// `com.atproto.repo.*`'s reads, structurally required for
507    /// `describeServer` and `resolveHandle`. The write half takes the agent
508    /// token `createAccount` hands back, and `routes::write_record` /
509    /// `routes::apply_writes` are where the authenticated DID is checked
510    /// against the repository a write names. `getSession` takes the same
511    /// credentials, and names the account they sign in as.
512    ATPROTO_CREDENTIALS,
513    atproto_routes,
514    {
515        "com.atproto.identity.resolveDid" => get(resolve_did) as Public,
516        "com.atproto.identity.resolveHandle" => get(resolve_handle) as Public,
517        "com.atproto.repo.applyWrites" => post(apply_writes) as AccountToken,
518        "com.atproto.repo.createRecord" => post(create_record) as AccountToken,
519        "com.atproto.repo.deleteRecord" => post(delete_record) as AccountToken,
520        "com.atproto.repo.describeRepo" => get(describe_repo) as Public,
521        "com.atproto.repo.getRecord" => get(get_record) as Public,
522        "com.atproto.repo.listRecords" => get(list_records) as Public,
523        "com.atproto.repo.putRecord" => post(put_record) as AccountToken,
524        "com.atproto.repo.uploadBlob" => post(crate::blobs::upload_blob) as AccountToken,
525        "com.atproto.server.describeServer" => get(describe_server) as Public,
526        "com.atproto.server.getServiceAuth" => get(crate::service_auth::get_service_auth) as AccountToken,
527        "com.atproto.server.getSession" => get(get_session) as AccountToken,
528        "com.atproto.sync.getBlob" => get(crate::blobs::get_blob) as Public,
529        "com.atproto.sync.getBlocks" => get(get_blocks) as Public,
530        "com.atproto.sync.getLatestCommit" => get(get_latest_commit) as Public,
531        "com.atproto.sync.getRecord" => get(sync_get_record) as Public,
532        "com.atproto.sync.getRepo" => get(get_repo) as Public,
533        "com.atproto.sync.getRepoStatus" => get(get_repo_status) as Public,
534        "com.atproto.sync.listBlobs" => get(crate::blobs::list_blobs) as Public,
535        "com.atproto.sync.listRepos" => get(list_repos) as Public,
536        "com.atproto.sync.subscribeRepos" => get(subscribe_repos) as Public,
537    }
538}
539
540xrpc_methods! {
541    /// The `app.bsky.*` methods this server serves itself rather than
542    /// proxies, as the reference PDS does: the account's preferences.
543    ///
544    /// Its own invocation for the reason [`BOT_DID_METHODS`] has one: the
545    /// vendored documents are `com.atproto.*` alone.
546    APP_BSKY_METHODS,
547    /// The credential each of those methods accepts: the ones `getSession`
548    /// takes.
549    APP_BSKY_CREDENTIALS,
550    app_bsky_routes,
551    {
552        "app.bsky.actor.getPreferences" => get(get_preferences) as AccountToken,
553        "app.bsky.actor.putPreferences" => post(put_preferences) as AccountToken,
554    }
555}
556
557xrpc_methods! {
558    /// The `bot.did.*` methods this server serves — its own account admin
559    /// surface, alongside the `com.atproto.*` one [`ATPROTO_METHODS`] names.
560    ///
561    /// Its own invocation rather than folded into the one above: these are
562    /// not vendored anywhere, so `crates/didbot/tests/conformance/wire.rs`'s
563    /// "every route is a vendored method" check must not see them.
564    ///
565    /// `bot.did.hardDeleteAccount` is absent on purpose. It frees a burned
566    /// name, so self-service is exactly wrong for it. The operator reaches
567    /// `Registry::hard_delete` through the dashboard's
568    /// `POST /dashboard/api/accounts/{did}/delete` instead.
569    BOT_DID_METHODS,
570    /// The credential each of those methods accepts.
571    ///
572    /// `createAccount` is `Credential::Creator`: the credential names the
573    /// account creating, in any of the three shapes `tree::prove` resolves,
574    /// and `createSession` is `Credential::Proof`, the two of those that
575    /// are not a session. `updateCredentials` is
576    /// `Credential::KeyProofOrOperator`: a JWT one of the account's keys
577    /// signed, or the operator session. Every
578    /// account-lifecycle mutation names an account that already exists and
579    /// so takes `Credential::AccountSelf` — an agent acting on
580    /// *itself*, the only caller this server can authenticate for them. The
581    /// four authorization-decision routes take it for the same reason from
582    /// the other side: they name no account at all, and the account they act
583    /// on is the one the presented token authenticates as, so a decision can
584    /// only ever be read, approved or declined by the agent it was addressed
585    /// to. `createBrowserSignIn` takes it the same way, and signs a browser in
586    /// as that account alone.
587    /// The four reads are `Credential::Disclosure`: public by
588    /// default, narrowable per route, never widenable. `pollOperatorClaim`
589    /// is `Public` because it confers nothing — see the `auth` module's doc.
590    /// `parkKey` and `describeParkedKey` are `Public` for the same reason:
591    /// a park mints nothing until the operator admits it from their own
592    /// side, and both are bounded per address rather than by a credential.
593    BOT_DID_CREDENTIALS,
594    bot_did_routes,
595    {
596        "bot.did.activateAccount" => post(activate_account) as AccountSelf,
597        "bot.did.approveAuthorization" => post(approve_authorization) as AccountSelf,
598        "bot.did.deactivateAccount" => post(deactivate_account) as AccountSelf,
599        "bot.did.declineAuthorization" => post(decline_authorization) as AccountSelf,
600        "bot.did.deleteAccount" => post(delete_account) as AccountSelf,
601        "bot.did.freezeAccount" => post(freeze_account) as AccountSelf,
602        "bot.did.getAccountLedger" => get(get_account_ledger) as Disclosure,
603        "bot.did.getAuthorization" => get(get_authorization) as AccountSelf,
604        "bot.did.listAccountLedgers" => get(list_ledgers) as Disclosure,
605        "bot.did.listAccounts" => get(list_accounts) as Disclosure,
606        "bot.did.listPendingAuthorizations" => get(list_pending_authorizations) as AccountSelf,
607        "bot.did.pollOperatorClaim" => post(poll_operator_claim) as Public,
608        "bot.did.createAccount" => post(crate::tree::create_account) as Creator,
609        "bot.did.createBrowserSignIn" => post(create_browser_sign_in) as AccountSelf,
610        "bot.did.createSession" => post(crate::tree::create_session) as Proof,
611        "bot.did.describeParkedKey" => get(crate::tree::describe_parked_key) as Public,
612        "bot.did.parkKey" => post(crate::tree::park_key) as Public,
613        "bot.did.stats" => get(stats) as Disclosure,
614        "bot.did.unfreezeAccount" => post(unfreeze_account) as AccountSelf,
615        "bot.did.updateCredentials" => post(crate::tree::update_credentials) as KeyProofOrOperator,
616    }
617}
618
619/// The header a client may name the turn a request belongs to in.
620///
621/// A record crosses three processes — the hook that stamps it, the record
622/// host that writes it, and this server that stores it — and each logs what it
623/// did. Until this existed nothing joined those three lines together at a
624/// finer grain than the agent, and an agent that has been working for an hour
625/// has hundreds of lines under one DID.
626///
627/// It is logged and nothing else. Nothing here parses it, no behaviour depends
628/// on it, and it never reaches a record: it is a harness-internal string, and
629/// writing it into a repository anybody can read would publish an operational
630/// detail permanently for the benefit of nobody reading it there.
631///
632/// The producing half is whatever client sends it, which is why the name and
633/// the bound below are stated here rather than inferred from a caller.
634pub const TURN_HEADER: &str = "quernstone-turn";
635
636/// The longest turn identifier this server will put in a log line.
637///
638/// A sanity bound rather than an agreement: anything longer is dropped, so a
639/// client cannot make an arbitrarily long log line out of a header.
640pub const TURN_HEADER_MAX_LENGTH: usize = 256;
641
642/// The request span, carrying the turn when the client named one.
643///
644/// A [`MakeSpan`] rather than a separate middleware because a span is already
645/// being made here, and putting the field on it means every line emitted while
646/// the request is being served carries it — including the refusals, which are
647/// the lines somebody correlating is most often looking for.
648#[derive(Debug, Clone, Copy, Default)]
649struct TurnSpan;
650
651impl<B> MakeSpan<B> for TurnSpan {
652    fn make_span(&mut self, request: &axum::http::Request<B>) -> tracing::Span {
653        let turn = request
654            .headers()
655            .get(TURN_HEADER)
656            .and_then(|value| value.to_str().ok())
657            .filter(|value| !value.is_empty() && value.chars().count() <= TURN_HEADER_MAX_LENGTH)
658            .unwrap_or("-");
659        // The same three fields `DefaultMakeSpan` puts on a request span, plus
660        // the turn. Named "request" rather than "http.request" for the same
661        // reason the rest of this log is short: it is read by a person
662        // watching a terminal.
663        //
664        // The path, not the URI: `/oauth/authorize` and
665        // `bot.did.getAuthorization` carry a pushed-request reference in their
666        // query, and that reference redeems a pending sign-in. Logs are shipped
667        // and copied like operational data, so nothing that spends a credential
668        // goes in one.
669        tracing::info_span!(
670            "request",
671            method = %request.method(),
672            uri = %request.uri().path(),
673            version = ?request.version(),
674            turn = %turn,
675        )
676    }
677}
678
679/// Builds the router.
680pub fn app(registry: Arc<dyn Registry>) -> Router {
681    app_with_health(registry, HealthState::new())
682}
683
684/// Builds the router with the health pulse wired to an existing producer.
685///
686/// The e-stop and the operator credential stay the
687/// [`AuthState::default`] every other builder but [`app_with_auth`] uses;
688/// call that one directly to wire real ones alongside a real `health`.
689pub fn app_with_health(registry: Arc<dyn Registry>, health: Arc<HealthState>) -> Router {
690    app_with_auth(registry, AuthState::default(), health)
691}
692
693/// Builds the router with every collaborator supplied explicitly.
694///
695/// The full builder every other one in this module calls. A deployment that
696/// wants a durable session store, a configured operator credential, or a
697/// file-backed e-stop latch calls this one directly; everything else keeps
698/// working exactly as it did before [`AuthState`] existed.
699///
700/// `health` should be [`HealthTick::state`](crate::HealthTick::state) from
701/// the tick a caller has spawned for this registry. A router built any other
702/// way here answers `/health`'s `progress` from a state nothing ever ticks.
703pub fn app_with_auth(
704    registry: Arc<dyn Registry>,
705    auth: AuthState,
706    health_state: Arc<HealthState>,
707) -> Router {
708    app_with_repos(registry, Repos::default(), auth, health_state)
709}
710
711/// Builds the router with `com.atproto.sync.subscribeRepos` attached too.
712///
713/// The full builder. `repos` must be the same [`Repos`] handed to
714/// [`Provisioner::with_repo_sink`](didbot_pds::Provisioner::with_repo_sink),
715/// or the method answers with a stream nothing publishes into — and, unlike
716/// the other two streams, one whose sequence numbers do not survive a restart
717/// either. [`app_with_auth`] builds one with an in-memory sequence, which is
718/// the right thing for a test and the wrong thing for a deployment; see
719/// [`Durable::sequence`](didbot_pds::Durable::sequence).
720pub fn app_with_repos(
721    registry: Arc<dyn Registry>,
722    repos: Repos,
723    auth: AuthState,
724    health_state: Arc<HealthState>,
725) -> Router {
726    // Wired here rather than left to whoever built `AuthState`: a Revoke
727    // must tear down outstanding credentials or releasing it resurrects them,
728    // and every router this crate builds needs that true regardless of who
729    // constructed its `Estop` and `OAuthState`. The durable issuer writes
730    // each revocation to the log before this returns, so a Revoke outlives
731    // the restart that follows it. See `Estop::set_revoke_hook`.
732    let auth_loopback_only = auth.loopback_callers_only;
733    let strict_transport_security = auth
734        .strict_transport_security
735        .map(|hsts| hsts.header_value());
736    let nonces = auth.oauth.dpop.clone();
737    let revoke_grants = auth.oauth.tokens.clone();
738    let revoke_browsers = auth.oauth.browser_sessions.clone();
739    let revoke_remembered = auth.oauth.decision_store.clone();
740    auth.estop.set_revoke_hook(Arc::new(move || {
741        revoke_grants.revoke_all();
742        revoke_browsers.revoke_all();
743        revoke_remembered.forget_all();
744    }));
745    // The other credential an account answers with: the agent token every
746    // createAccount and createSession hands out. Ended when the latch
747    // leaves Revoke rather than when it is thrown, because
748    // `bot.did.freezeAccount` and `bot.did.deactivateAccount` are reached
749    // with that token and are exempt from the stop — a stop must never
750    // stand between a caller and restraining itself. The latch refuses the
751    // token for everything else while it stands, and this is what keeps it
752    // refused afterwards.
753    let revoke_tokens = registry.clone();
754    auth.estop.set_release_hook(Arc::new(move || {
755        revoke_tokens.revoke_all_agent_tokens();
756    }));
757    // And the same wiring for a single account, at the other end of an
758    // account's life. The registry holds the agent token store and revokes
759    // through it directly; every other credential bound to a DID is here,
760    // beside the router, so a deletion tells this closure and this closure
761    // ends them. Wired here for the reason above: every router this crate
762    // builds needs it true, and the stores it names are the ones `AuthState`
763    // carries. See `Registry::set_account_revoke_hook`.
764    //
765    // A credential is anything that answers for the account: its OAuth
766    // grants, the authorization codes one hop short of a grant, and the
767    // browsers signed in as it. Its remembered approvals go with them. The
768    // codes, the browsers and the approvals go whatever the grant store
769    // answers.
770    let deleted_tokens = auth.oauth.tokens.clone();
771    let deleted_codes = auth.oauth.code_store.clone();
772    let deleted_browsers = auth.oauth.browser_sessions.clone();
773    let deleted_remembered = auth.oauth.decision_store.clone();
774    registry.set_account_revoke_hook(Arc::new(move |did: &str| {
775        let ended = deleted_tokens.revoke_account(did);
776        deleted_codes.revoke_account(did);
777        deleted_browsers.revoke_account(did);
778        deleted_remembered.forget_account(did);
779        ended
780    }));
781    let registry_for_foreign = registry.clone();
782    let private_addresses = auth.tree.private_addresses();
783    let foreign = crate::foreign::reader(registry_for_foreign, private_addresses);
784    // The reader needs the registry to tell a repository this deployment
785    // holds from a stranger's, so it cannot be built until the registry
786    // is; the registry is told about it here, once, and both halves of the
787    // read -- the route's prefetch and the write path's own -- share it.
788    let lookups = Arc::new(crate::LookupReader::new(
789        auth.declared_lookups.clone(),
790        foreign.clone(),
791        registry.clone(),
792    ));
793    registry.set_lookups(lookups.clone());
794    let state = AppState {
795        registry,
796        repos,
797        estop: auth.estop,
798        operator: auth.operator,
799        relay: auth.relay,
800        operator_login_limiter: auth.operator_login_limiter,
801        disclosure: auth.disclosure,
802        oauth: auth.oauth,
803        provision_rate_limiter: auth.provision_rate_limiter,
804        max_accounts: auth.max_accounts,
805        unauthenticated_creates: auth.unauthenticated_creates,
806        park_rate_limiter: auth.park_rate_limiter,
807        sync_rate_limiter: auth.sync_rate_limiter,
808        write_meter: auth.write_meter,
809        proxy: auth.proxy,
810        never_wrote_min_idle: auth.never_wrote_min_idle,
811        tree: auth.tree,
812        subscriber_limit: auth.subscriber_limit,
813        trust_forwarded_headers: auth.trust_forwarded_headers,
814        health: health_state,
815        lifecycle: auth.lifecycle,
816        nudge: auth.nudge,
817        operator_records: auth.operator_records,
818        operator_handle: auth.operator_handle,
819        handles: auth.handles,
820        handle_lookup_limiter: auth.handle_lookup_limiter,
821        did_lookup_limiter: auth.did_lookup_limiter,
822        foreign,
823        lookups,
824    };
825
826    // The routes a stranger's browser tab may call from its own origin: the
827    // protocol surface, its discovery documents, and the two OAuth endpoints
828    // a web client fetches rather than navigates to. `/oauth/authorize` and
829    // `/oauth/browser` are pages a browser is sent to and the dashboard holds
830    // a person's session — none answers cross-origin, and widening
831    // this set is a security decision, not a convenience (see
832    // `plan/web-launch.md`).
833    let cross_origin = Router::new()
834        .route("/xrpc/_health", get(xrpc_health))
835        .route("/.well-known/did.json", get(did_json))
836        .route(ATPROTO_DID_PATH, get(atproto_did))
837        .route(
838            "/.well-known/oauth-authorization-server",
839            get(oauth_server_metadata),
840        )
841        .route(
842            "/.well-known/oauth-protected-resource",
843            get(oauth_protected_resource_metadata),
844        )
845        .route(
846            "/oauth/par",
847            post(oauth_par).layer(axum::middleware::from_fn(never_cached)),
848        )
849        .route(
850            "/oauth/token",
851            post(oauth_token).layer(axum::middleware::from_fn(never_cached)),
852        )
853        .merge(bot_did_routes())
854        .merge(atproto_routes())
855        .merge(app_bsky_routes())
856        // Set here, before the layer, and not only on the outer router
857        // below: axum layers a route's method-not-allowed slot along with
858        // its handlers, but the outer `method_not_allowed_fallback` replaces
859        // every slot still holding the default — layered or not — and a
860        // preflight is an `OPTIONS` on a route that serves no `OPTIONS`, so
861        // it is exactly that slot the layer has to be wrapped around.
862        .method_not_allowed_fallback(wrong_method)
863        .layer(cross_origin_layer());
864
865    let loopback_callers_only = auth_loopback_only;
866    Router::new()
867        .route("/health", get(health))
868        .route("/oauth/authorize", get(oauth_authorize))
869        .route(crate::oauth::browser::PATH, get(oauth_browser))
870        .merge(crate::home::routes())
871        .merge(crate::dashboard::routes())
872        .merge(crate::analytics::routes())
873        .merge(cross_origin)
874        // A path nothing serves, and a method nothing serves on a path that
875        // exists, are both failures a client has to be able to read. axum's
876        // own answers are an empty 404 and an empty 405, which are not the
877        // shape every other failure here takes. The fallback is also where
878        // an XRPC call this server does not serve is proxied; see
879        // `crate::proxy`.
880        .fallback(crate::proxy::fallback)
881        .with_state(state)
882        .method_not_allowed_fallback(wrong_method)
883        // Requests are logged at INFO because this server exists to be watched.
884        // The default for `TraceLayer` is DEBUG, which would mean turning on
885        // DEBUG for everything else to see them.
886        .layer(
887            TraceLayer::new_for_http()
888                .make_span_with(TurnSpan)
889                .on_request(DefaultOnRequest::new().level(Level::DEBUG))
890                .on_response(
891                    DefaultOnResponse::new()
892                        .level(Level::INFO)
893                        .latency_unit(LatencyUnit::Millis),
894                ),
895        )
896        // A concurrency bound is deliberately *not* layered on here. This
897        // router is built and handed to `crate::serve_router` or
898        // `crate::serve_tls`, which both apply that bound through
899        // `crate::limited_make_service` instead — see
900        // `crate::MAX_CONCURRENT_REQUESTS`'s own doc for why `Router::layer`
901        // cannot be trusted with a stateful limiter like
902        // `tower::limit::ConcurrencyLimitLayer`.
903        .layer(TimeoutLayer::with_status_code(
904            StatusCode::SERVICE_UNAVAILABLE,
905            REQUEST_TIMEOUT,
906        ))
907        // The buffering bound, pinned rather than inherited; see
908        // `MAX_REQUEST_BODY`. `uploadBlob` is unaffected because it takes the
909        // body as a stream, which this layer does not reach.
910        .layer(axum::extract::DefaultBodyLimit::max(MAX_REQUEST_BODY))
911        // Outside the body limit and the timeout, so their refusals carry a
912        // nonce too. See `hand_out_nonce`.
913        .layer(axum::middleware::from_fn(move |request, next| {
914            hand_out_nonce(nonces.clone(), request, next)
915        }))
916        // Outside the body limit, the request log and the timeout, so a
917        // caller this deployment does not answer is refused before any of
918        // them spends anything on it. `security_headers` below is the one
919        // layer outside this one, so the refusal carries them too. See
920        // `AuthState::loopback_callers_only`.
921        .layer(axum::middleware::from_fn(move |request, next| {
922            only_loopback_callers(loopback_callers_only, request, next)
923        }))
924        .layer(axum::middleware::from_fn(move |request, next| {
925            security_headers(strict_transport_security.clone(), request, next)
926        }))
927}
928
929/// A `Strict-Transport-Security` header; see `[tls] hsts_max_age_secs` and
930/// `[tls] hsts_include_subdomains` in `didbot-config`.
931///
932/// `preload` is never sent. The preload list takes only registrable domains,
933/// and getting off it takes months.
934#[derive(Debug, Clone, Copy, PartialEq, Eq)]
935pub struct StrictTransportSecurity {
936    /// How long a browser keeps the header, in seconds.
937    pub max_age_secs: u64,
938    /// Whether the header covers every name below the one it was served on.
939    pub include_subdomains: bool,
940}
941
942impl StrictTransportSecurity {
943    /// Two years, the same as did.bot's own header.
944    pub const DEFAULT_MAX_AGE_SECS: u64 = 63_072_000;
945
946    /// The header's value.
947    pub fn header_value(&self) -> HeaderValue {
948        let value = if self.include_subdomains {
949            format!("max-age={}; includeSubDomains", self.max_age_secs)
950        } else {
951            format!("max-age={}", self.max_age_secs)
952        };
953        HeaderValue::try_from(value).expect("digits and ASCII words are a valid header value")
954    }
955}
956
957/// The headers every response this server sends carries.
958///
959/// This process terminates its own TLS and renders its own pages, so there is
960/// nothing in front of it to add them: `/oauth/authorize` and `/dashboard`
961/// are HTML a browser is handed directly. The set is the one a
962/// layer can apply without knowing the route — a consent page that cannot be
963/// framed, a content type that is believed rather than sniffed, and a request
964/// that says nothing about where the reader came from.
965///
966/// Applied to every response rather than to the HTML routes alone: the JSON,
967/// the dashboard's assets and a blob are each served with the content type
968/// they actually are, and none of them is meant to be put in a frame either.
969///
970/// `frame-ancestors` is the whole of the content security policy. A
971/// `script-src` here would have to admit the `new Function` and the
972/// `WebAssembly` that `dashboard.js` loads its field animation through, and a
973/// policy written to permit `eval` buys nothing; framing is what the consent
974/// page needs refused. `X-Frame-Options` says the same thing again for a
975/// browser that reads only that one.
976///
977/// A route that sets a policy of its own keeps it: `getBlob` sets a sandbox
978/// (see [`crate::blobs`]), and `X-Frame-Options` still refuses framing there.
979///
980/// `no-referrer`, where the static site sends
981/// `strict-origin-when-cross-origin`: `/oauth/authorize`'s own URL carries the
982/// authorization request, and no page here has a reason to pass its address to
983/// anything it links to.
984///
985/// `Strict-Transport-Security` is added when the deployment set one; see
986/// [`AuthState::strict_transport_security`]. Every response goes out over
987/// TLS, so it is never sent over plain HTTP.
988async fn security_headers(
989    strict_transport_security: Option<HeaderValue>,
990    request: axum::extract::Request,
991    next: axum::middleware::Next,
992) -> Response {
993    let mut response = next.run(request).await;
994    let headers = response.headers_mut();
995    if let Some(value) = strict_transport_security {
996        headers.insert(header::STRICT_TRANSPORT_SECURITY, value);
997    }
998    headers.insert(header::X_FRAME_OPTIONS, HeaderValue::from_static("DENY"));
999    if !headers.contains_key(header::CONTENT_SECURITY_POLICY) {
1000        headers.insert(
1001            header::CONTENT_SECURITY_POLICY,
1002            HeaderValue::from_static("frame-ancestors 'none'"),
1003        );
1004    }
1005    headers.insert(
1006        header::X_CONTENT_TYPE_OPTIONS,
1007        HeaderValue::from_static("nosniff"),
1008    );
1009    headers.insert(
1010        header::REFERRER_POLICY,
1011        HeaderValue::from_static("no-referrer"),
1012    );
1013    response
1014}
1015
1016/// Refuses a caller that is not on this host, when this deployment answers
1017/// only callers that are; see [`AuthState::loopback_callers_only`].
1018///
1019/// A request that arrived with no peer address — a test driving the
1020/// [`Router`] directly, or a deployment that wired neither
1021/// [`crate::serve_router`] nor [`crate::serve_tls`] — is served, the same
1022/// posture `crate::rate_limit::caller_key` takes for the same absence: this
1023/// cannot tell such a caller apart from any other, and refusing every
1024/// request on a router nobody put a listener under would make the router
1025/// untestable rather than safe.
1026async fn only_loopback_callers(
1027    enabled: bool,
1028    request: axum::extract::Request,
1029    next: axum::middleware::Next,
1030) -> Response {
1031    if enabled {
1032        let peer = request
1033            .extensions()
1034            .get::<axum::extract::ConnectInfo<std::net::SocketAddr>>()
1035            .map(|axum::extract::ConnectInfo(addr)| addr.ip());
1036        if let Some(ip) = peer {
1037            if !ip.to_canonical().is_loopback() {
1038                info!(
1039                    %ip,
1040                    path = %request.uri().path(),
1041                    "refused: this deployment answers only callers on this host"
1042                );
1043                return ApiError::new(
1044                    StatusCode::FORBIDDEN,
1045                    "NotServedHere",
1046                    "this deployment answers only callers on the host it runs on",
1047                )
1048                .into_response();
1049            }
1050        }
1051    }
1052    next.run(request).await
1053}
1054
1055/// Hands the client a DPoP nonce on the answer to any request that carried
1056/// a `DPoP` proof, success and refusal alike, unless the answer names one
1057/// already, as a nonce challenge does.
1058///
1059/// The atproto OAuth profile has a client refuse an answer to a DPoP request
1060/// that carries no `DPoP-Nonce`, and RFC 9449 §8.2 lets every answer carry
1061/// the nonce for the next request.
1062async fn hand_out_nonce(
1063    verifier: Arc<dyn crate::oauth::dpop_seam::DpopVerifier>,
1064    request: axum::extract::Request,
1065    next: axum::middleware::Next,
1066) -> Response {
1067    let carried = request.headers().contains_key(crate::oauth::dpop::DPOP);
1068    let mut response = next.run(request).await;
1069    if carried
1070        && !response
1071            .headers()
1072            .contains_key(crate::oauth::dpop::DPOP_NONCE)
1073    {
1074        if let Some(value) = verifier
1075            .nonce()
1076            .and_then(|nonce| HeaderValue::from_str(&nonce).ok())
1077        {
1078            response
1079                .headers_mut()
1080                .insert(crate::oauth::dpop::DPOP_NONCE, value);
1081        }
1082    }
1083    response
1084}
1085
1086/// How long a browser may cache a preflight answer before asking again.
1087///
1088/// A day, which is what the reference PDS sets. The answer never varies by
1089/// caller, so there is nothing for a shorter cache to pick up sooner.
1090pub const CORS_MAX_AGE: std::time::Duration = std::time::Duration::from_secs(24 * 60 * 60);
1091
1092/// The request headers an atproto client sends and a preflight must name.
1093///
1094/// `authorization` and `dpop` carry the two credentials, `content-type` is
1095/// on every procedure call, and `atproto-proxy` / `atproto-accept-labelers`
1096/// are what the official client adds to every request whether or not the
1097/// server it reaches acts on them. A header left off this list makes the
1098/// browser refuse the request before it is sent, so the list is the
1099/// client's, not this server's.
1100pub const CORS_ALLOWED_HEADERS: [HeaderName; 5] = [
1101    header::AUTHORIZATION,
1102    header::CONTENT_TYPE,
1103    crate::oauth::dpop::DPOP,
1104    HeaderName::from_static("atproto-accept-labelers"),
1105    HeaderName::from_static("atproto-proxy"),
1106];
1107
1108/// The response headers a browser is allowed to show a script.
1109///
1110/// Anything not named here is stripped by the browser before the calling
1111/// page sees it, and both of these carry the next step of a flow: the
1112/// nonce a DPoP client must echo on its retry, and the challenge that says
1113/// a token was refused and why.
1114pub const CORS_EXPOSED_HEADERS: [HeaderName; 2] =
1115    [crate::oauth::dpop::DPOP_NONCE, header::WWW_AUTHENTICATE];
1116
1117/// The cross-origin policy on the public protocol surface.
1118///
1119/// Any origin, without credentials: the browser then never attaches a
1120/// cookie, so a page on somebody else's origin can present only what its
1121/// own script holds — a bearer or DPoP token it was issued — which is how
1122/// the atproto OAuth flow from a web app and every browser-based checker
1123/// reaches a PDS. What it is layered on is decided in [`app_with_repos`].
1124pub fn cross_origin_layer() -> CorsLayer {
1125    CorsLayer::new()
1126        .allow_origin(Any)
1127        .allow_methods([Method::GET, Method::POST, Method::OPTIONS])
1128        .allow_headers(CORS_ALLOWED_HEADERS)
1129        .expose_headers(CORS_EXPOSED_HEADERS)
1130        .max_age(CORS_MAX_AGE)
1131}
1132
1133/// Parses a JSON body, mapping any failure to a 400 in the atproto shape.
1134///
1135/// `axum::Json` is not used as an extractor here because its rejection is
1136/// axum's own plain-text body, not `{"error":..,"message":..}`.
1137pub(crate) fn parse_body<T: DeserializeOwned>(body: &Bytes) -> Result<T, ApiError> {
1138    serde_json::from_slice(body).map_err(|err| ApiError::bad_request(err.to_string()))
1139}
1140
1141/// The 500 for a `spawn_blocking` task that panicked instead of answering.
1142///
1143/// A panic inside a blocking task never reaches the worker that awaited it —
1144/// it surfaces as a `JoinError` instead — so this is the one place that
1145/// becomes a response rather than a connection that hangs up with nothing
1146/// said.
1147///
1148/// A `JoinError`'s own text is the panic payload, which is whatever the line
1149/// that panicked passed to `panic!` or to `expect`: a file it was reading, a
1150/// row it was holding, an assertion written for whoever is reading the log.
1151/// `route` and that payload go to the log line, and the caller — who may be
1152/// anyone, since `com.atproto.sync.getBlob` reaches here without a
1153/// credential — is told that the request was not answered and nothing else.
1154pub(crate) fn blocking_task_failed(route: &str, error: tokio::task::JoinError) -> ApiError {
1155    tracing::error!(route, %error, "a blocking task panicked before answering");
1156    ApiError::new(
1157        StatusCode::INTERNAL_SERVER_ERROR,
1158        "InternalServerError",
1159        "the server did not answer this request".to_owned(),
1160    )
1161}
1162
1163/// Runs a `com.atproto.sync` read on the blocking pool.
1164///
1165/// Every read this is used for can reach `Registry::repository`, which
1166/// rebuilds the Merkle search tree and signs a commit when the cached one no
1167/// longer names the head — and then serialises it. That is milliseconds to
1168/// seconds of CPU with no await in it, and the deployment in `infra/pds`
1169/// runs two async workers, so two such reads left inline are every other
1170/// route waiting. `spawn_blocking` is the same seam `getBlob` and
1171/// `createAccount` already cross.
1172///
1173/// `route` names the method in the log line a panicking task leaves; see
1174/// [`blocking_task_failed`].
1175pub(crate) async fn sync_read<T, F>(
1176    state: &AppState,
1177    route: &'static str,
1178    work: F,
1179) -> Result<T, Response>
1180where
1181    F: for<'a> FnOnce(&'a dyn Registry) -> Result<T, ProvisionError> + Send + 'static,
1182    T: Send + 'static,
1183{
1184    // Acquired before `spawn_blocking`, not inside it: what it bounds is
1185    // blocking-pool threads, and claiming one of those first would hold it
1186    // idle while this request waits its turn. Released when this function
1187    // returns, which is when the read is done.
1188    let _permit = SYNC_READ_CONCURRENCY
1189        .acquire()
1190        .await
1191        .expect("this semaphore is never closed");
1192    let registry = state.registry.clone();
1193    match tokio::task::spawn_blocking(move || work(registry.as_ref())).await {
1194        Ok(Ok(value)) => Ok(value),
1195        Ok(Err(err)) => Err(sync_error(state, &err).into_response()),
1196        Err(err) => Err(blocking_task_failed(route, err).into_response()),
1197    }
1198}
1199
1200/// Spends one of the caller's [`SYNC_READ_LIMIT`] sync reads, or answers
1201/// the refusal.
1202///
1203/// These routes take no credential, so the address is the only caller there
1204/// is. Checked before the work rather than after, which is the whole point:
1205/// the cost this bounds is the rebuild, not the response.
1206pub(crate) fn require_sync_budget(
1207    state: &AppState,
1208    headers: &axum::http::HeaderMap,
1209    peer: crate::rate_limit::Peer,
1210) -> Result<(), ApiError> {
1211    let key = crate::rate_limit::caller_key(headers, peer.0, state.trust_forwarded_headers);
1212    if state.sync_rate_limiter.check(&key) {
1213        return Ok(());
1214    }
1215    info!(caller = %key, "sync read refused: too many from this address");
1216    Err(
1217        ApiError::rate_limited("too many sync reads from this address; slow down").with_header(
1218            axum::http::header::RETRY_AFTER,
1219            SYNC_READ_WINDOW.as_secs().to_string(),
1220        ),
1221    )
1222}
1223
1224/// The largest request body this server will hold in memory.
1225///
1226/// Two mebibytes, which is what axum bounds a buffering extractor to when
1227/// nothing says otherwise. It is written down here rather than inherited
1228/// silently, because it is the only thing standing between an anonymous
1229/// caller and this process's memory, and a bound that arrives from a
1230/// dependency's default is one nothing in this repository would notice
1231/// changing.
1232///
1233/// It is not a bound on a *blob*: `com.atproto.repo.uploadBlob` takes the
1234/// body as a stream and never buffers it, and its size limit is the blob
1235/// store's own. This is the bound on the JSON every other procedure sends.
1236///
1237/// Enforcement is streaming rather than after the fact — the body is refused
1238/// as it arrives, at the first frame that carries the request past the bound,
1239/// so a caller announcing or sending a gigabyte never has a gigabyte of it
1240/// resident here.
1241pub const MAX_REQUEST_BODY: usize = 2 * 1024 * 1024;
1242
1243/// A buffered request body, refused in the atproto shape.
1244///
1245/// `Bytes` on its own is an extractor whose rejection is axum's own
1246/// plain-text sentence with no `error` name at all — the same reason
1247/// `parse_body` exists rather than `axum::Json`, and the same reason every
1248/// query below is extracted as a `Result`. A body over
1249/// [`MAX_REQUEST_BODY`] is the one failure on this surface a caller cannot
1250/// avoid by writing a better request, so it is the one that most has to be
1251/// machine-readable.
1252pub struct XrpcBody(pub Bytes);
1253
1254impl<S> axum::extract::FromRequest<S> for XrpcBody
1255where
1256    S: Send + Sync,
1257{
1258    type Rejection = ApiError;
1259
1260    async fn from_request(
1261        request: axum::extract::Request,
1262        state: &S,
1263    ) -> Result<Self, Self::Rejection> {
1264        match Bytes::from_request(request, state).await {
1265            Ok(body) => Ok(Self(body)),
1266            // `RequestTooLarge` names no error the lexicons declare, the
1267            // same position `RateLimitExceeded` and `ServerNotReady` are in:
1268            // 413 is the standard status and every client already knows it,
1269            // and the name is there for the ones that switch on it. The
1270            // message is this crate's own rather than the rejection's, so
1271            // nothing about how the body was buffered reaches the caller.
1272            Err(rejection) if rejection.status() == StatusCode::PAYLOAD_TOO_LARGE => {
1273                Err(request_too_large())
1274            }
1275            Err(rejection) => Err(ApiError::bad_request(rejection.body_text())),
1276        }
1277    }
1278}
1279
1280/// `RequestTooLarge`: a body over [`MAX_REQUEST_BODY`].
1281pub(crate) fn request_too_large() -> ApiError {
1282    ApiError::new(
1283        StatusCode::PAYLOAD_TOO_LARGE,
1284        "RequestTooLarge",
1285        format!("a request body may not exceed {MAX_REQUEST_BODY} bytes"),
1286    )
1287}
1288
1289/// Resolves a repo write route's credential — an agent token or a DPoP-bound
1290/// OAuth access token. See `auth::require_agent_token_or_dpop`.
1291fn resource_credential(
1292    state: &AppState,
1293    headers: &HeaderMap,
1294    http_method: &str,
1295    path: &str,
1296) -> Result<auth::Writer, ApiError> {
1297    auth::require_agent_token_or_dpop(
1298        state.registry.as_ref(),
1299        &state.oauth,
1300        headers,
1301        http_method,
1302        path,
1303    )
1304}
1305
1306/// Checks a write against the scope of the token that presented it — see
1307/// `auth::Writer::authorize` — and writes a refusal to the evaluation log
1308/// beside the policy's own, so what a token was refused reads in one place.
1309///
1310/// A collection that is not an NSID is refused first, as the store would
1311/// refuse it, so a caller cannot write arbitrary strings into that log.
1312fn authorize_write<'a>(
1313    state: &AppState,
1314    writer: &'a auth::Writer,
1315    writes: &[(&str, Action)],
1316) -> Result<auth::Authorized<'a>, ApiError> {
1317    for (collection, _) in writes {
1318        didbot_pds::validate_collection(collection)
1319            .map_err(|err| crate::error::repo_surface(&didbot_pds::ProvisionError::Record(err)))?;
1320    }
1321    writer.authorize(writes).map_err(|missing| {
1322        record_scope_refusal(state, writer.did(), &missing);
1323        auth::insufficient_scope(writer.did(), &missing)
1324    })
1325}
1326
1327/// Checks one upload against the scope of the token that presented it, and
1328/// writes a refusal down the way [`authorize_write`] does.
1329pub(crate) fn authorize_upload<'a>(
1330    state: &AppState,
1331    writer: &'a auth::Writer,
1332    mime_type: &str,
1333) -> Result<auth::Authorized<'a>, ApiError> {
1334    writer.authorize_blob(mime_type).map_err(|missing| {
1335        record_scope_refusal(state, writer.did(), &missing);
1336        auth::insufficient_scope(writer.did(), &missing)
1337    })
1338}
1339
1340/// The evaluation log's row for a write outside its token's scope, named
1341/// [`auth::TOKEN_SCOPE_RULE`] where a tree's row names a policy.
1342fn record_scope_refusal(state: &AppState, did: &str, missing: &auth::ScopeMissing) {
1343    let Some(account) = state.registry.account(did) else {
1344        return;
1345    };
1346    let pds_did = deployment_did(state);
1347    let subject = match &missing.wanted {
1348        auth::Wanted::Record { collection, action } => didbot_pds::policy::write_subject(
1349            account.did.as_str(),
1350            account.kind,
1351            account.handle.as_deref(),
1352            &pds_did,
1353            state.registry.zone(),
1354            None,
1355            Some(&missing.client_id),
1356            collection,
1357            match action {
1358                Action::Create => didbot_pds::policy::WriteAction::Create,
1359                Action::Update | Action::Manage => didbot_pds::policy::WriteAction::Update,
1360                Action::Delete => didbot_pds::policy::WriteAction::Delete,
1361            },
1362            &[],
1363            None,
1364            None,
1365            &[],
1366        ),
1367        auth::Wanted::Upload { mime_type } => didbot_pds::policy::blob_subject(
1368            account.did.as_str(),
1369            account.kind,
1370            account.handle.as_deref(),
1371            &pds_did,
1372            state.registry.zone(),
1373            Some(&missing.client_id),
1374            mime_type,
1375            None,
1376            None,
1377        ),
1378        // A call at another service is no write or upload a policy judges,
1379        // so it has no subject to log; `service_auth::authorize` logs it.
1380        auth::Wanted::Rpc { .. } => return,
1381    };
1382    state
1383        .oauth
1384        .evaluation_log
1385        .record_denial(didbot_pds::evaluation_log::Denial {
1386            account: account.did.as_str(),
1387            policies_fired: &[auth::TOKEN_SCOPE_RULE.to_owned()],
1388            verdict: didbot_pds::evaluation_log::Verdict::Reject,
1389            reason: &format!("the token's scope does not cover `{}`", missing.needed()),
1390            cut: &[],
1391            payload_hash: &state.oauth.evaluation_log.hash_subject(&subject),
1392            policy_version: &didbot_pds::policy::PolicyVersion(auth::TOKEN_SCOPE_RULE.to_owned()),
1393            now: subject.universal().now,
1394            subject: &subject,
1395        });
1396}
1397
1398/// `POST /xrpc/bot.did.pollOperatorClaim`
1399///
1400/// Asks this server to look for its operator's claim sooner than its timer
1401/// would. **Urgency, never authority.**
1402///
1403/// # Why this exists, and why it cannot be more than a hint
1404///
1405/// A server reaches [`ServerState::Claimed`](didbot_pds::ServerState) only
1406/// by *observing* a standing `bot.did.operator` record through its own poll.
1407/// Nothing here changes that: this method takes no body, reads no
1408/// credential, names no operator, and its entire effect is to wake a task
1409/// that was going to run anyway. A server that took anybody's word for a
1410/// claim would be a server anybody could claim.
1411///
1412/// What it buys is the gap between an operator running `didbot operate` and
1413/// the next scheduled poll, which is otherwise minutes of staring at a page
1414/// that says `unclaimed` after the work is already done.
1415///
1416/// # Why it cannot be turned into a poll amplifier
1417///
1418/// The bound is on this server's *outbound* rate, not on inbound requests:
1419/// `crate::operator_poll::MIN_POLL_INTERVAL` is a floor between two polls,
1420/// and every nudge arriving inside one window coalesces into the same single
1421/// poll. So an unbounded flood of nudges costs the operator's PDS at most
1422/// one extra request per window — which is what the timer was already going
1423/// to spend — and costs this server a wake-up on a task that exists. A rate
1424/// limiter on this route would bound how many nudges are *accepted* while
1425/// still letting an accepted burst multiply outbound requests, which is the
1426/// wrong quantity to bound.
1427///
1428/// It answers `202` — accepted, nothing has happened yet, which is literally
1429/// true — with a `Retry-After` naming the floor, so a caller that wants to
1430/// nudge again knows when it could matter.
1431async fn poll_operator_claim(State(state): State<AppState>) -> Response {
1432    state.nudge.nudge();
1433    (
1434        StatusCode::ACCEPTED,
1435        [(
1436            header::RETRY_AFTER,
1437            crate::operator_poll::MIN_POLL_INTERVAL
1438                .as_secs()
1439                .to_string(),
1440        )],
1441        Json(json!({})),
1442    )
1443        .into_response()
1444}
1445
1446/// `GET /health`
1447///
1448/// `status` is liveness: it is `"ok"` whenever this process can schedule a
1449/// task to answer at all, which is what a load balancer's health check
1450/// should gate traffic on — see `crates/didbot-serve/src/health.rs` for why
1451/// that is a different claim from `progress`, which reports whether the
1452/// deployment's own heartbeat tick is still advancing rather than merely
1453/// whether a socket accepted.
1454///
1455/// `certificates` is the second claim, and the one an alarm can be pointed
1456/// at: the deployment's health checks read it for certificate state,
1457/// and a renewal that keeps failing reaches expiry — a total outage for
1458/// every hostname in the zone — over weeks. Carried in this same response
1459/// rather than on a route of its own so the check already being made says
1460/// it; see `crates/didbot-serve/src/health.rs`.
1461///
1462/// `oauthPushes` is the third, and it is here because `POST /oauth/par`
1463/// cannot say it anywhere else: that endpoint is unauthenticated, so it
1464/// answers a flood aimed at one account exactly as it answers an ordinary
1465/// push, and the log line it writes is sampled. These are counts since this
1466/// process started, so an operator watches them move rather than reading a
1467/// level; see `crate::oauth::par::DroppedPushes`.
1468///
1469/// `foreignRecords` is the fourth, and the same shape: counts since the
1470/// process started, from the one reader policy evaluation reads other
1471/// people's repositories through. `refusals` is separated from `unknowns`
1472/// because a refusal is this deployment's own decision and an unknown is
1473/// somebody else's outage, and an operator watching either number climb
1474/// wants to know which it is looking at. See `didbot_lookup::Counts`.
1475///
1476/// `disk` is the fifth: the data volume's `freeBytes` and `totalBytes`,
1477/// read as this request is answered, and `alert`, the lowest free-space
1478/// watermark it is below. It is null for a run with no `--data`. See
1479/// `crate::health::HealthState::disk`.
1480///
1481/// `firehose` is the sixth: how many `subscribeRepos` consumers are
1482/// attached right now, and the newest sequence number assigned. A relay
1483/// that has stopped reading is `consumers` at zero while `newestSeq` moves.
1484///
1485/// `revision` is the commit the binary was built from, or null for a build
1486/// that was not told; see `crate::health::REVISION`. A deploy waits for it
1487/// to name the commit it shipped.
1488async fn health(State(state): State<AppState>) -> Response {
1489    let pushes = crate::oauth::par::dropped_pushes();
1490    let foreign = state.foreign.counts();
1491    Json(json!({
1492        "status": "ok",
1493        "revision": crate::health::REVISION,
1494        "zone": state.registry.zone(),
1495        "progress": state.health.snapshot(),
1496        "certificates": state.health.certificates().await,
1497        "oauthPushes": {
1498            "displaced": pushes.displaced,
1499            "unresolvedHint": pushes.unresolved_hint,
1500            "serverFull": pushes.server_full,
1501        },
1502        "foreignRecords": {
1503            "hits": foreign.hits,
1504            "misses": foreign.misses,
1505            "unknowns": foreign.unknowns,
1506            "refusals": foreign.refusals,
1507        },
1508        "disk": state.health.disk().await,
1509        "firehose": {
1510            "consumers": state.repos.subscribers(),
1511            "newestSeq": state.repos.newest(),
1512        },
1513    }))
1514    .into_response()
1515}
1516
1517/// `GET /xrpc/_health`
1518///
1519/// The reference PDS's health route, which pdscheck and debug.hose.cam probe
1520/// and read a server's version from. The same answer that server gives
1521/// (`packages/pds/src/basic-routes.ts` in bluesky-social/atproto at
1522/// `388dac9f`): `{"version": ...}`, or a 503 adding
1523/// `"error": "Service Unavailable"` while this deployment serves no reads.
1524/// `version` is the `revision` [`health`] reports, and is left out, as the
1525/// reference leaves out an unset one, for a build that was not told.
1526async fn xrpc_health(State(state): State<AppState>) -> Response {
1527    let mut body = serde_json::Map::new();
1528    if let Some(revision) = crate::health::REVISION {
1529        body.insert("version".to_owned(), json!(revision));
1530    }
1531    if state.lifecycle.snapshot().policy.serves_reads {
1532        return Json(body).into_response();
1533    }
1534    body.insert("error".to_owned(), json!("Service Unavailable"));
1535    (
1536        StatusCode::SERVICE_UNAVAILABLE,
1537        [(
1538            axum::http::header::RETRY_AFTER,
1539            didbot_pds::RETRY_AFTER_SECONDS.to_string(),
1540        )],
1541        Json(body),
1542    )
1543        .into_response()
1544}
1545
1546/// `GET /.well-known/did.json`
1547///
1548/// Every agent gets its own hostname, and locally they all land on one
1549/// listener, so the Host header is the only thing that says which account the
1550/// caller means.
1551async fn did_json(
1552    State(state): State<AppState>,
1553    uri: axum::http::Uri,
1554    headers: HeaderMap,
1555) -> Response {
1556    // A `404` here says "no such identity", which during `booting` or
1557    // `provisioning` is not yet true -- and for a `did:web` document the
1558    // difference matters, since a verifier treats an absent document as the
1559    // identity never having existed.
1560    if let Err(err) = require_lifecycle(&state, |p| p.serves_reads, "/.well-known/did.json") {
1561        return err.into_response();
1562    }
1563    let Some(host) = request_authority(&uri, &headers) else {
1564        return ApiError::bad_request("request names no host").into_response();
1565    };
1566    let host = host_without_port(host);
1567
1568    match state.registry.did_document(host) {
1569        // `application/json`, the type the did:web method expects a
1570        // document to be served with. atproto's own resolver refuses any
1571        // type but that one and `application/did+ld+json`.
1572        Some(document) => Json(document).into_response(),
1573        None => ApiError::new(
1574            StatusCode::NOT_FOUND,
1575            "NotFound",
1576            format!("no DID document for host `{host}`"),
1577        )
1578        .into_response(),
1579    }
1580}
1581
1582/// `GET /.well-known/atproto-did`
1583///
1584/// The other direction from `did.json`: that one says what a DID resolves to,
1585/// this says which DID answers to a name. A deployment that issues its agents
1586/// handles publishes a second hostname per account, and this is what makes it
1587/// a handle rather than a decoration — the plain-text DID, which is what the
1588/// atproto handle resolution rule asks for.
1589///
1590/// It answers exactly when the account's DID document claims the host as its
1591/// handle, because the registry derives both from one function. That is
1592/// atproto's bidirectional rule holding by construction rather than by
1593/// agreement: a resolver that follows this DID to its document and looks for
1594/// the handle in `alsoKnownAs` finds it, for every account, always. A name
1595/// nobody holds is a 404, which is the whole of what a resolver reads here.
1596async fn atproto_did(
1597    State(state): State<AppState>,
1598    uri: axum::http::Uri,
1599    headers: HeaderMap,
1600) -> Response {
1601    let Some(host) = request_authority(&uri, &headers) else {
1602        return ApiError::bad_request("request names no host").into_response();
1603    };
1604    let host = host_without_port(host);
1605
1606    match state.registry.handle_did(host) {
1607        // Plain text, not JSON: the response body is the DID and nothing else.
1608        Some(did) => ([(header::CONTENT_TYPE, "text/plain")], did).into_response(),
1609        None => ApiError::new(
1610            StatusCode::NOT_FOUND,
1611            "NotFound",
1612            format!("no account answers to the handle `{host}`"),
1613        )
1614        .into_response(),
1615    }
1616}
1617
1618/// The authority a request names, however its protocol version spells it.
1619///
1620/// HTTP/1.1 carries it in `Host`. HTTP/2 carries it as the `:authority`
1621/// pseudo-header, which arrives in the request URI and never as a header —
1622/// so a route that picks an account by hostname reads both, or answers only
1623/// the callers that spoke the older version. This server offers both over
1624/// TLS.
1625pub(crate) fn request_authority<'a>(
1626    uri: &'a axum::http::Uri,
1627    headers: &'a HeaderMap,
1628) -> Option<&'a str> {
1629    uri.authority()
1630        .map(axum::http::uri::Authority::as_str)
1631        .or_else(|| headers.get(header::HOST).and_then(|v| v.to_str().ok()))
1632}
1633
1634/// Strips a port from a Host header value.
1635///
1636/// A development zone is served on an explicit port, so `Host` arrives as
1637/// `agent.agents.localhost:3000` while the DID is registered under the bare
1638/// hostname. IPv6 literals arrive bracketed, and the brackets are dropped with
1639/// the port.
1640pub(crate) fn host_without_port(host: &str) -> &str {
1641    if let Some(rest) = host.strip_prefix('[') {
1642        return rest.split(']').next().unwrap_or(rest);
1643    }
1644    match host.rsplit_once(':') {
1645        Some((name, port)) if !port.is_empty() && port.bytes().all(|b| b.is_ascii_digit()) => name,
1646        _ => host,
1647    }
1648}
1649
1650/// The answer for a path nothing serves, and for an XRPC call nothing
1651/// proxies; see `crate::proxy::fallback`.
1652///
1653/// `MethodNotImplemented` under `/xrpc/`, which is the name atproto's XRPC
1654/// layer uses for a procedure a server does not implement, and a 501 to go
1655/// with it: a client that asked for a method this server does not serve should
1656/// learn that this server has not built it, not that the URL was a typo.
1657/// Anything outside `/xrpc/` is an ordinary 404.
1658pub(crate) fn unknown_route(uri: &axum::http::Uri) -> Response {
1659    if uri.path().starts_with("/xrpc/") {
1660        let nsid = uri.path().trim_start_matches("/xrpc/");
1661        return ApiError::new(
1662            StatusCode::NOT_IMPLEMENTED,
1663            "MethodNotImplemented",
1664            format!("this server does not implement `{nsid}`"),
1665        )
1666        .into_response();
1667    }
1668    ApiError::new(
1669        StatusCode::NOT_FOUND,
1670        "NotFound",
1671        format!("nothing is served at `{}`", uri.path()),
1672    )
1673    .into_response()
1674}
1675
1676/// The fallback for the right path and the wrong method.
1677///
1678/// A query fetched with POST, or a procedure called with GET. It is a real
1679/// mistake a hand-written client makes and an empty 405 does not say so.
1680async fn wrong_method(uri: axum::http::Uri) -> Response {
1681    ApiError::new(
1682        StatusCode::METHOD_NOT_ALLOWED,
1683        "InvalidRequest",
1684        format!("`{}` is not served with that HTTP method", uri.path()),
1685    )
1686    .into_response()
1687}
1688
1689/// `GET /xrpc/bot.did.stats`
1690///
1691/// `Credential::Disclosure`: open by default, this deployment's own
1692/// aggregate inventory — see `auth::Disclosure`. The health tick logs the
1693/// same figures on its own schedule by calling `Registry::stats`
1694/// in-process, so nothing internal reaches this route over HTTP.
1695async fn stats(State(state): State<AppState>) -> Response {
1696    if let Err(err) = require_lifecycle(&state, |p| p.serves_reads, "bot.did.stats") {
1697        return err.into_response();
1698    }
1699    if let Err(err) = auth::require_disclosure(state.disclosure.stats, "bot.did.stats") {
1700        return err.into_response();
1701    }
1702    let enforced = state.registry.enforced_policies();
1703    Json(StatsResponse::from_stats(&state.registry.stats()).with_policy(enforced.as_deref()))
1704        .into_response()
1705}
1706
1707/// Resolves `identifier` — a handle or a DID — to an account DID this
1708/// deployment hosts.
1709///
1710/// What `POST /oauth/par` resolves a `login_hint` with: [`resolve_repo`]
1711/// under another name, kept separate because a sign-in failure and a
1712/// repository lookup failure want different error shapes, and folding them
1713/// into one function would make one of the two callers wrong.
1714fn resolve_identifier(state: &AppState, identifier: &str) -> Option<String> {
1715    if identifier.starts_with("did:") {
1716        state
1717            .registry
1718            .account(&canonical_did(identifier))
1719            .map(|a| a.did.as_str().to_owned())
1720    } else {
1721        state
1722            .registry
1723            .handle_did(identifier)
1724            .filter(|did| state.registry.account(did).is_some())
1725    }
1726}
1727
1728pub(crate) fn too_many_provisioning_attempts() -> Response {
1729    ApiError::rate_limited("too many provisioning attempts; slow down").into_response()
1730}
1731
1732/// `GET /xrpc/com.atproto.server.describeServer`
1733///
1734/// Discovery: what this server is, before a client has any account on it.
1735/// `availableUserDomains` is read out of the registry's real zone
1736/// configuration rather than stated as a fixed string, so a deployment with
1737/// several zones reports all of them — see
1738/// [`Registry::available_user_domains`]. Every other field the lexicon
1739/// declares optional is left off rather than answered with an invented
1740/// value; see [`DescribeServerResponse`]'s own doc for which and why.
1741async fn describe_server(State(state): State<AppState>) -> Response {
1742    // One of the two reads `didbot operate` makes before it can write a claim;
1743    // the other is the DID document above. Both must answer in `unclaimed`
1744    // or the handshake deadlocks, which `didbot_pds::server_state`'s
1745    // `the_handshake_cannot_deadlock` is what actually holds.
1746    if let Err(err) = require_lifecycle(
1747        &state,
1748        |p| p.serves_reads,
1749        "com.atproto.server.describeServer",
1750    ) {
1751        return err.into_response();
1752    }
1753    Json(DescribeServerResponse {
1754        did: state.registry.service_did(),
1755        available_user_domains: state.registry.available_user_domains(),
1756    })
1757    .into_response()
1758}