Skip to main content

didbot_serve/tree/
mod.rs

1//! The ownership tree over HTTP: creating an account beneath a creator,
2//! parking a key for the operator to admit, and a session from a proof.
3//!
4//! A creator is whatever authenticates *as* an account, in one of three
5//! shapes ([`Proof`]): a session this server issued, a JWT signed by a key
6//! the account's credential records name, or an OpenID Connect token one of
7//! those records describes, for the account the request names. The
8//! deployment's operator, who holds
9//! no account here, presents the operator session this server minted at
10//! sign-in (see [`crate::operator`]), and the `bot.did.operator` record this
11//! server reads from their repository is what lets them create. Every other
12//! creator's record is written here, by this server, as the creator.
13//!
14//! `proof` is the verifier, which `createSession` uses as well as
15//! `createAccount`; `oidc` is its OpenID Connect half, `parked` holds keys
16//! waiting for the operator, and `names` splits a requested name. This
17//! file holds the routes and what they share.
18
19mod credentials;
20mod names;
21mod oidc;
22mod parked;
23mod proof;
24
25pub(crate) use credentials::{update_credentials, update_credentials_from_dashboard};
26pub use names::RESERVED_LABELS;
27pub use oidc::{JwksCache, DEFAULT_OIDC_FRESHNESS};
28pub use parked::{
29    fingerprint, ParkedKey, ParkedKeys, DEFAULT_PARK_TTL, PARK_RATE_LIMIT, PARK_RATE_WINDOW,
30};
31pub use proof::{NotSpent, Proof, Replay, MAX_PROOF_LIFETIME};
32
33use std::collections::{BTreeMap, HashMap};
34use std::sync::Mutex;
35use std::time::Instant;
36
37use axum::extract::{Query, State};
38use axum::http::{header, HeaderMap, StatusCode};
39use axum::response::{IntoResponse, Response};
40use axum::Json;
41use didbot_pds::{Creator, EstopRefusal, ProvisionError, ProvisionRequest};
42use serde_json::json;
43use time::{Duration, OffsetDateTime};
44use tracing::{debug, info, warn};
45
46use crate::error::ApiError;
47use crate::rate_limit::{caller_key, Peer};
48use crate::routes::{blocking_task_failed, AppState, XrpcBody};
49use crate::wire::{parse_kind, CreateAccountRequest, ParkKeyRequest, ParkedKeyQuery, SessionQuery};
50
51use names::split_name;
52use proof::{invalid_proof, prove, Named, NamedBy};
53
54/// How long a re-read of the operator's record for a root creator is
55/// reused across creates beneath it.
56const TOP_EDGE_TTL: std::time::Duration = std::time::Duration::from_secs(60);
57
58/// What the tree's routes hold between requests.
59pub struct TreeState {
60    /// Keys waiting for the operator; see [`ParkedKeys`].
61    pub parked: ParkedKeys,
62    /// Proofs already presented; see [`Replay`].
63    pub replay: Replay,
64    /// Issuers' key sets; see [`JwksCache`].
65    pub jwks: JwksCache,
66    /// The operator's record for each root creator, as last re-read, and
67    /// when; see `TOP_EDGE_TTL`.
68    top_edges: Mutex<HashMap<String, (bool, Instant)>>,
69    /// When a read of the operator's record for a requested name last
70    /// failed; see `refuse_operators_name`.
71    name_read_failed: Mutex<Option<Instant>>,
72    /// Reaches the operator's PDS and a DID directory.
73    pub client: reqwest::Client,
74    /// The issuers whose tokens are accepted, as `iss` spells them. Empty
75    /// admits any issuer at a public HTTPS address.
76    pub issuers: Vec<String>,
77    /// How old a token's `iat` may be; see [`DEFAULT_OIDC_FRESHNESS`].
78    pub freshness: Duration,
79    /// Whether this deployment may fetch from its own network: a
80    /// development stack's posture, and a test's. It covers an issuer on
81    /// this host over plain HTTP, and an operator whose repository answers
82    /// at a loopback address. Every deployment on the public internet
83    /// leaves it off, and [`didbot_http::REFUSED_BLOCKS`] then applies to
84    /// every URL a fetched document names.
85    pub loopback_fetches: bool,
86    /// Labels no account may take directly under a zone; see
87    /// [`RESERVED_LABELS`].
88    pub reserved_labels: Vec<String>,
89}
90
91impl TreeState {
92    /// Parks for `park_ttl`, with a fresh HTTP client and the defaults for
93    /// everything else.
94    pub fn new(park_ttl: Duration) -> Self {
95        Self {
96            parked: ParkedKeys::new(park_ttl),
97            replay: Replay::default(),
98            jwks: JwksCache::default(),
99            top_edges: Mutex::new(HashMap::new()),
100            name_read_failed: Mutex::new(None),
101            client: didbot_http::client(),
102            issuers: Vec::new(),
103            freshness: DEFAULT_OIDC_FRESHNESS,
104            loopback_fetches: false,
105            reserved_labels: RESERVED_LABELS.iter().map(|s| (*s).to_owned()).collect(),
106        }
107    }
108
109    /// Keeps the proof window in `store`, holding what it already had.
110    ///
111    /// # Errors
112    ///
113    /// The kept file is there and cannot be read. It stops the boot: see
114    /// [`didbot_pds::side_store`].
115    pub fn with_spent_store(
116        mut self,
117        store: didbot_pds::SpentStore,
118    ) -> Result<Self, didbot_pds::SideStoreError> {
119        self.replay = Replay::kept_in(store)?;
120        Ok(self)
121    }
122
123    /// Reserves `labels` directly under every zone instead of
124    /// [`RESERVED_LABELS`].
125    #[must_use]
126    pub fn with_reserved_labels(mut self, labels: Vec<String>) -> Self {
127        self.reserved_labels = labels;
128        self
129    }
130
131    /// Accepts issuers on this host over plain HTTP.
132    #[must_use]
133    pub fn with_loopback_fetches(mut self, allowed: bool) -> Self {
134        self.loopback_fetches = allowed;
135        self
136    }
137
138    /// [`loopback_fetches`](Self::loopback_fetches) as the guard reads it.
139    pub(crate) fn private_addresses(&self) -> didbot_http::PrivateAddresses {
140        if self.loopback_fetches {
141            didbot_http::PrivateAddresses::Allowed
142        } else {
143            didbot_http::PrivateAddresses::Refused
144        }
145    }
146
147    /// Accepts only `issuers`; empty admits any public HTTPS issuer.
148    #[must_use]
149    pub fn with_issuers(mut self, issuers: Vec<String>) -> Self {
150        self.issuers = issuers;
151        self
152    }
153
154    /// Refuses a token whose `iat` is older than `freshness`.
155    #[must_use]
156    pub fn with_freshness(mut self, freshness: Duration) -> Self {
157        self.freshness = freshness;
158        self
159    }
160
161    /// Whether the operator's record for `did` stood when last re-read
162    /// inside `TOP_EDGE_TTL`, or `None` when it is time to read again.
163    fn top_edge(&self, did: &str) -> Option<bool> {
164        self.top_edges
165            .lock()
166            .unwrap_or_else(|p| p.into_inner())
167            .get(did)
168            .filter(|(_, at)| at.elapsed() < TOP_EDGE_TTL)
169            .map(|(stands, _)| *stands)
170    }
171
172    fn remember_top_edge(&self, did: &str, stands: bool) {
173        self.top_edges
174            .lock()
175            .unwrap_or_else(|p| p.into_inner())
176            .insert(did.to_owned(), (stands, Instant::now()));
177    }
178
179    /// Whether a read of the operator's record for a name failed inside
180    /// `TOP_EDGE_TTL`.
181    fn name_read_failed_lately(&self) -> bool {
182        self.name_read_failed
183            .lock()
184            .unwrap_or_else(|p| p.into_inner())
185            .is_some_and(|at| at.elapsed() < TOP_EDGE_TTL)
186    }
187
188    fn remember_name_read_failed(&self) {
189        *self
190            .name_read_failed
191            .lock()
192            .unwrap_or_else(|p| p.into_inner()) = Some(Instant::now());
193    }
194}
195
196impl Default for TreeState {
197    fn default() -> Self {
198        Self::new(DEFAULT_PARK_TTL)
199    }
200}
201
202/// The most a create refused for want of an allowance waits for the
203/// operator poll to re-read the operator's records before it answers.
204///
205/// Under the ten seconds a didbot client gives a request
206/// ([`didbot_http::DEFAULT_TIMEOUT`]), leaving room to provision and answer.
207const ALLOWANCE_REREAD_WAIT: std::time::Duration = std::time::Duration::from_secs(8);
208
209/// `POST /xrpc/bot.did.createAccount`
210///
211/// One of the e-stop's two issuance gates alongside session creation: a
212/// Pause or a Revoke refuses a new account the same way it refuses a new
213/// session. The e-stop, the lifecycle and the account cap each close the
214/// route to every caller at once, and [`AppState::provision_rate_limiter`]
215/// closes it to one caller address at a time, all before the credential is
216/// read: a server that will not provision has no business spending a
217/// resolution on it.
218///
219/// An operator session cookie makes the caller the operator, and the
220/// session must be live and carry its CSRF token. Otherwise the bearer
221/// credential identifies a hosted creator — see [`prove`] — which becomes
222/// the parent, and which `parent` names. The operator's create also needs
223/// the record this server reads from their repository.
224pub(crate) async fn create_account(
225    State(state): State<AppState>,
226    peer: Peer,
227    headers: HeaderMap,
228    XrpcBody(body): XrpcBody,
229) -> Response {
230    match create_account_inner(&state, peer, &headers, &body).await {
231        Ok(response) => response,
232        Err(error) => error.into_response(),
233    }
234}
235
236async fn create_account_inner(
237    state: &AppState,
238    peer: Peer,
239    headers: &HeaderMap,
240    body: &axum::body::Bytes,
241) -> Result<Response, ApiError> {
242    state.estop.check_issue(EstopRefusal::Account)?;
243    crate::routes::require_lifecycle(state, |p| p.provisions_accounts, "bot.did.createAccount")?;
244    let key = caller_key(headers, peer.0, state.trust_forwarded_headers);
245    if !state.provision_rate_limiter.check(&key) {
246        info!("provisioning refused: too many attempts from this address");
247        return Ok(crate::routes::too_many_provisioning_attempts());
248    }
249    crate::routes::require_account_capacity(state).inspect_err(|_| {
250        info!("provisioning refused: this deployment is at its account cap");
251    })?;
252    let request: CreateAccountRequest = crate::routes::parse_body(body)?;
253    let kind = parse_kind(&request.kind)?;
254    let fragments = fragments_of(&request)?;
255    // No name asks the server to choose one: from the pool named, or from
256    // the first pool policy permits.
257    let (label, zone) = match request.name.as_deref() {
258        Some(name) => {
259            let (label, zone) =
260                split_name(state.registry.as_ref(), &state.tree.reserved_labels, name)?;
261            (label, Some(zone))
262        }
263        None => (String::new(), None),
264    };
265    if request.key.is_some() && request.parked_key.is_some() {
266        return Err(ApiError::bad_request(
267            "name one of `key` and `parkedKey`, not both",
268        ));
269    }
270    let creator = if crate::surface::session_cookie(headers).is_some() {
271        // The operator, who holds no account here, signs in to this
272        // deployment the way the dashboard and `didbot login` do. The
273        // session says who is asking; the record in their own repository
274        // is what lets them create.
275        if let Err(refusal) = crate::surface::signed_in(state, headers, true) {
276            info!("provisioning refused: no live operator session");
277            return Ok(*refusal);
278        }
279        if request.parent.is_some() {
280            return Err(ApiError::bad_request(
281                "the operator creates at the top of the tree, with no `parent`",
282            ));
283        }
284        let Some(name) = request.name.as_deref() else {
285            return Err(ApiError::new(
286                StatusCode::BAD_REQUEST,
287                "NameRequired",
288                "the operator's record is keyed by the name, so the operator names what \
289                 they create",
290            ));
291        };
292        operator_creator(state, &state.registry.operator_did(), name).await?
293    } else {
294        let proof = prove(
295            state,
296            headers,
297            "bot.did.createAccount",
298            state.unauthenticated_creates,
299            Named {
300                did: request.parent.as_deref(),
301                field: NamedBy::Parent,
302            },
303        )
304        .await?;
305        match proof.did() {
306            None if request.parent.is_some() => {
307                return Err(ApiError::bad_request(
308                    "`parent` needs a credential for that account",
309                ));
310            }
311            None => Creator::Server,
312            Some(did) => {
313                let Some(account) = state.registry.account(did) else {
314                    return Err(invalid_proof(format!(
315                        "{did} is not an account this server hosts"
316                    )));
317                };
318                require_top_edge(state, &account).await?;
319                Creator::Account(did.to_owned())
320            }
321        }
322    };
323
324    let by_operator = matches!(creator, Creator::Operator { .. });
325    if let (Some(name), false) = (request.name.as_deref(), by_operator) {
326        refuse_operators_name(state, name).await?;
327    }
328
329    let now = OffsetDateTime::now_utc();
330    let account_key = match request.parked_key.as_deref() {
331        None => request.key.clone(),
332        Some(fingerprint) => {
333            let Some(name) = request.name.as_deref() else {
334                return Err(ApiError::new(
335                    StatusCode::BAD_REQUEST,
336                    "NameRequired",
337                    "a parked key is admitted under the name it was parked for",
338                ));
339            };
340            let parked = state
341                .tree
342                .parked
343                .take(name, fingerprint, now)
344                .ok_or_else(|| {
345                    ApiError::new(
346                        StatusCode::NOT_FOUND,
347                        "ParkedKeyNotFound",
348                        format!("no key with fingerprint {fingerprint} is parked for {name:?}"),
349                    )
350                })?;
351            if parked.kind != kind {
352                return Err(ApiError::new(
353                    StatusCode::BAD_REQUEST,
354                    "ParkedKeyKindMismatch",
355                    format!(
356                        "the key was parked for {}, and this creates {}",
357                        parked.kind.with_article(),
358                        kind.with_article()
359                    ),
360                ));
361            }
362            if let Creator::Operator { claim, .. } = &creator {
363                if parked.parked_at >= claim.created_at {
364                    return Err(ApiError::new(
365                        StatusCode::CONFLICT,
366                        "ParkedKeyNewerThanRecord",
367                        "the key was parked after the operator's record was written; a record \
368                         admits only a key parked before it",
369                    ));
370                }
371            }
372            Some(parked.key)
373        }
374    };
375
376    let mut provision = ProvisionRequest::new(label, None)
377        .with_kind(kind)
378        .with_fragments(fragments)
379        .created_by(creator);
380    if let Some(pool) = request.pool.clone() {
381        provision = provision.in_pool(pool);
382    }
383    if let Some(zone) = zone {
384        provision = provision.in_zone(zone);
385    }
386    if let Some(key) = account_key {
387        provision = provision.with_account_key(key);
388    }
389    if let Some(oidc) = request.oidc {
390        provision = provision.with_oidc(oidc);
391    }
392    let name = request.name.clone().unwrap_or_default();
393    let provision_now = |provision: ProvisionRequest| {
394        let registry = state.registry.clone();
395        tokio::task::spawn_blocking(move || registry.provision(provision))
396    };
397    let mut outcome = provision_now(provision.clone()).await;
398    // The operator's allowances reach this server on the operator poll, so
399    // a host admitted moments ago would otherwise wait out the poll's timer
400    // before its first create. Ask the poll to read them now, and check
401    // once more against what it read.
402    let mut reread = false;
403    if matches!(outcome, Ok(Err(ProvisionError::NotAllowed { .. }))) {
404        debug!(name, "no allowance yet; re-reading the operator's records");
405        if state.nudge.nudge_and_wait(ALLOWANCE_REREAD_WAIT).await {
406            reread = true;
407            outcome = provision_now(provision).await;
408        }
409    }
410    match outcome {
411        Ok(Ok(provisioned)) => Ok(Json(json!({
412            "did": provisioned.account.did.as_str(),
413            "handle": provisioned.document.handle(),
414            "didDocument": provisioned.document,
415            // The only copy of this token that will ever leave this
416            // process; see `didbot_pds::credential`.
417            "accountToken": provisioned.account_token,
418            "operator": provisioned.operator,
419            "parent": provisioned.account.parent.as_ref().map(|parent| parent.as_str()),
420        }))
421        .into_response()),
422        Ok(Err(err)) => {
423            info!(name, error = %err, "provisioning refused");
424            // The operator admits a name by creating it, so a name a hosted
425            // creator took first is named with who took it.
426            if let (ProvisionError::Store(didbot_pds::StoreError::AlreadyExists { did }), true) =
427                (&err, by_operator)
428            {
429                if let Some(parent) = state.registry.account(did).and_then(|held| held.parent) {
430                    let refusal = ApiError::from(&err);
431                    return Err(ApiError::new(
432                        StatusCode::CONFLICT,
433                        refusal.error(),
434                        format!("{}; {parent} created it", refusal.message()),
435                    ));
436                }
437            }
438            let refusal = ApiError::from(&err);
439            if reread && matches!(err, ProvisionError::NotAllowed { .. }) {
440                return Err(ApiError::new(
441                    StatusCode::FORBIDDEN,
442                    refusal.error(),
443                    format!(
444                        "{}; the operator's records were just re-read",
445                        refusal.message()
446                    ),
447                ));
448            }
449            Err(refusal)
450        }
451        Err(join_err) => {
452            Ok(blocking_task_failed("bot.did.createAccount", join_err).into_response())
453        }
454    }
455}
456
457/// A create's fragments, by slot: given only with a pool, and never with a
458/// name, each slot once.
459fn fragments_of(request: &CreateAccountRequest) -> Result<BTreeMap<String, String>, ApiError> {
460    if request.name.is_some() && (request.pool.is_some() || !request.fragments.is_empty()) {
461        return Err(ApiError::bad_request(
462            "give a `name`, or a `pool` and its `fragments`, not both",
463        ));
464    }
465    if request.pool.is_none() && !request.fragments.is_empty() {
466        return Err(ApiError::bad_request(
467            "`fragments` fill a pool's naming slots, so they need a `pool`",
468        ));
469    }
470    let mut fragments = BTreeMap::new();
471    for fragment in &request.fragments {
472        if fragments
473            .insert(fragment.slot.clone(), fragment.value.clone())
474            .is_some()
475        {
476            return Err(ApiError::bad_request(format!(
477                "the {:?} slot is given twice",
478                fragment.slot
479            )));
480        }
481    }
482    Ok(fragments)
483}
484
485/// The operator, whose own repository has to name `name` before anything
486/// is minted beneath them.
487async fn operator_creator(state: &AppState, did: &str, name: &str) -> Result<Creator, ApiError> {
488    let (endpoint, pinned) = crate::operator_poll::resolve_endpoint(
489        &state.tree.client,
490        did,
491        state.tree.private_addresses(),
492    )
493    .await
494    .map_err(|error| operator_unreachable(did, &error))?;
495    let claim = crate::operator_poll::fetch_claim_from(&pinned, &endpoint, did, name)
496        .await
497        .map_err(|error| operator_unreachable(did, &error))?
498        .ok_or_else(|| {
499            info!(
500                creator = did,
501                name, "refused: no operator record for the name"
502            );
503            ApiError::new(
504                StatusCode::FORBIDDEN,
505                "OperatorRecordMissing",
506                format!("{did}'s repository holds no bot.did.operator record at {name:?}"),
507            )
508        })?;
509    Ok(Creator::Operator {
510        did: did.to_owned(),
511        claim,
512    })
513}
514
515/// Re-reads the operator's record for the root above a creator — the
516/// account with no hosted parent, other than the server itself — inside
517/// `TOP_EDGE_TTL`, and refuses a creation beneath a root the operator no
518/// longer names. A creator deeper down is on the same chain, so its root
519/// is read the same way; the hosted edges between are the provisioner's to
520/// check. A repository that cannot be read leaves the poll's last verdict
521/// standing: a lapsed root is already quarantined, and the registry
522/// refuses it.
523async fn require_top_edge(
524    state: &AppState,
525    creator: &didbot_pds::HostedAccount,
526) -> Result<(), ApiError> {
527    let mut root = creator.clone();
528    while let Some(parent) = &root.parent {
529        match state.registry.account(parent.as_str()) {
530            Some(parent) => root = parent,
531            // The provisioner names the missing ancestor itself.
532            None => return Ok(()),
533        }
534    }
535    if root.did.as_str() == state.registry.service_did() {
536        return Ok(());
537    }
538    let account = &root;
539    let did = account.did.as_str();
540    let stands = match state.tree.top_edge(did) {
541        Some(stands) => stands,
542        None => {
543            let operator = state.registry.operator_did();
544            let read = match crate::operator_poll::resolve_endpoint(
545                &state.tree.client,
546                &operator,
547                state.tree.private_addresses(),
548            )
549            .await
550            {
551                Ok((endpoint, pinned)) => {
552                    crate::operator_poll::fetch_claim_from(
553                        &pinned,
554                        &endpoint,
555                        &operator,
556                        &account.did.did().authority(),
557                    )
558                    .await
559                }
560                Err(error) => Err(error),
561            };
562            match read {
563                Ok(found) => {
564                    let stands = found.is_some_and(|claim| claim.subject == did);
565                    state.tree.remember_top_edge(did, stands);
566                    stands
567                }
568                Err(error) => {
569                    warn!(
570                        did,
571                        %error,
572                        "could not re-read the operator's record for a root creator; keeping the poll's verdict"
573                    );
574                    // Remembered like a read that answered, so a repository
575                    // that hangs costs one client timeout per root per
576                    // `TOP_EDGE_TTL`, not one per creation.
577                    state.tree.remember_top_edge(did, true);
578                    true
579                }
580            }
581        }
582    };
583    if stands {
584        return Ok(());
585    }
586    info!(
587        creator = creator.did.as_str(),
588        root = did,
589        "refused: the operator's record for the root above this creator is gone"
590    );
591    Err(ApiError::new(
592        StatusCode::FORBIDDEN,
593        "OperatorRecordMissing",
594        format!("the operator's repository no longer holds a bot.did.operator record naming {did}"),
595    ))
596}
597
598/// Refuses a create of `name`, by any creator but the operator, when the
599/// operator's repository holds `bot.did.operator/<name>`: the name is the
600/// operator's to create, and taking it first would leave the operator's own
601/// create refused as `AccountAlreadyExists`.
602///
603/// The record is read live. When a read fails, the poll's last listing
604/// decides, and it goes on deciding without a read for `TOP_EDGE_TTL`, so
605/// an unreadable repository costs one timeout a minute and stops no create
606/// the listing does not name.
607async fn refuse_operators_name(state: &AppState, name: &str) -> Result<(), ApiError> {
608    let Ok(rkey) = didbot_claim_check::record_key(&didbot_identity::did::did_web_for_host(name))
609    else {
610        // Not a name a DID is minted from; the provisioner refuses it.
611        return Ok(());
612    };
613    let operator = state.registry.operator_did();
614    let stands = if state.tree.name_read_failed_lately() {
615        listing_holds(state, &rkey)
616    } else {
617        let read = match crate::operator_poll::resolve_endpoint(
618            &state.tree.client,
619            &operator,
620            state.tree.private_addresses(),
621        )
622        .await
623        {
624            Ok((endpoint, pinned)) => {
625                crate::operator_poll::fetch_claim_from(&pinned, &endpoint, &operator, &rkey).await
626            }
627            Err(error) => Err(error),
628        };
629        match read {
630            Ok(found) => found.is_some(),
631            Err(error) => {
632                warn!(
633                    operator,
634                    name,
635                    %error,
636                    "could not read the operator's record for a requested name; the last listing decides"
637                );
638                state.tree.remember_name_read_failed();
639                listing_holds(state, &rkey)
640            }
641        }
642    };
643    if !stands {
644        return Ok(());
645    }
646    info!(
647        name,
648        "refused: the operator's repository holds a record for the name"
649    );
650    Err(ApiError::new(
651        StatusCode::FORBIDDEN,
652        "OperatorRecordExists",
653        format!("the operator's repository holds bot.did.operator/{rkey}; only the operator creates {name}"),
654    ))
655}
656
657/// Whether the operator poll's last listing held a record at `rkey`. No
658/// listing yet holds none.
659fn listing_holds(state: &AppState, rkey: &str) -> bool {
660    state.operator_records.read().is_some_and(|lines| {
661        lines
662            .iter()
663            .any(|line| line.uri.rsplit('/').next() == Some(rkey))
664    })
665}
666
667/// The operator's repository could not be read for the record a creation
668/// rests on.
669fn operator_unreachable(did: &str, error: &didbot_pds::ClaimFetchError) -> ApiError {
670    warn!(operator = did, %error, "could not read the operator's repository for a creation");
671    ApiError::new(
672        StatusCode::BAD_GATEWAY,
673        "OperatorUnreachable",
674        format!("could not read {did}'s repository: {error}"),
675    )
676}
677
678/// Refuses a caller address past its park budget.
679fn require_park_budget(state: &AppState, headers: &HeaderMap, peer: Peer) -> Result<(), ApiError> {
680    let key = caller_key(headers, peer.0, state.trust_forwarded_headers);
681    if state.park_rate_limiter.check(&key) {
682        return Ok(());
683    }
684    info!(caller = %key, "park refused: too many from this address");
685    Err(ApiError::rate_limited("too many parkKey calls; slow down")
686        .with_header(header::RETRY_AFTER, PARK_RATE_WINDOW.as_secs().to_string()))
687}
688
689/// `POST /xrpc/bot.did.parkKey`
690///
691/// Unauthenticated and bounded per caller address: a machine that has made
692/// a key parks its public half here for minutes, and prints the fingerprint
693/// for the operator to admit from their side. A park mints nothing.
694///
695/// Parks are held per key, so several may stand under one name at once and
696/// parking is not a way to hold a name. `KeyAlreadyParked` names the same
697/// key parked again before its own park lapsed.
698pub(crate) async fn park_key(
699    State(state): State<AppState>,
700    peer: Peer,
701    headers: HeaderMap,
702    XrpcBody(body): XrpcBody,
703) -> Response {
704    match park_key_inner(&state, peer, &headers, &body) {
705        Ok(response) => response,
706        Err(error) => error.into_response(),
707    }
708}
709
710fn park_key_inner(
711    state: &AppState,
712    peer: Peer,
713    headers: &HeaderMap,
714    body: &axum::body::Bytes,
715) -> Result<Response, ApiError> {
716    require_park_budget(state, headers, peer)?;
717    let request: ParkKeyRequest = crate::routes::parse_body(body)?;
718    let kind = parse_kind(&request.kind)?;
719    let (label, zone) = split_name(
720        state.registry.as_ref(),
721        &state.tree.reserved_labels,
722        &request.name,
723    )?;
724    // Only the operator's create admits a parked key, so the name is
725    // judged as the operator's create of it would be.
726    state
727        .registry
728        .check_name(&label, &zone, kind)
729        .map_err(|error| {
730            info!(name = request.name, %error, "park refused: the name cannot be created");
731            ApiError::from(&error)
732        })?;
733    didbot_pds::account_key_curve(&request.key)
734        .map_err(|error| ApiError::bad_request(format!("key: {error}")))?;
735    let now = OffsetDateTime::now_utc();
736    match state
737        .tree
738        .parked
739        .park(&request.name, kind, &request.key, now)
740    {
741        Ok(parked) => {
742            info!(
743                name = parked.name,
744                fingerprint = parked.fingerprint,
745                "parked a key"
746            );
747            Ok(Json(json!({
748                "fingerprint": parked.fingerprint,
749                "expiresAt": didbot_schema::format::datetime::render(parked.expires_at),
750            }))
751            .into_response())
752        }
753        Err(expires_at) => {
754            debug!(
755                name = request.name,
756                "park refused: this key is already held"
757            );
758            Err(ApiError::new(
759                StatusCode::CONFLICT,
760                "KeyAlreadyParked",
761                format!(
762                    "this key is already parked for {:?} until {}",
763                    request.name,
764                    didbot_schema::format::datetime::render(expires_at)
765                ),
766            ))
767        }
768    }
769}
770
771/// `GET /xrpc/bot.did.describeParkedKey?name=&fingerprint=`
772///
773/// The park `fingerprint` names under `name`, or with none the most recent
774/// one there: its kind and expiry, and the fingerprint again.
775pub(crate) async fn describe_parked_key(
776    State(state): State<AppState>,
777    peer: Peer,
778    headers: HeaderMap,
779    query: Result<Query<ParkedKeyQuery>, axum::extract::rejection::QueryRejection>,
780) -> Response {
781    if let Err(error) = require_park_budget(&state, &headers, peer) {
782        return error.into_response();
783    }
784    let Query(query) = match query {
785        Ok(query) => query,
786        Err(rejection) => return ApiError::bad_request(rejection.body_text()).into_response(),
787    };
788    match state.tree.parked.describe(
789        &query.name,
790        query.fingerprint.as_deref(),
791        OffsetDateTime::now_utc(),
792    ) {
793        // The fingerprint and nothing that could stand in for it: the key
794        // itself goes into the document once the operator admits it.
795        Some(parked) => Json(json!({
796            "name": parked.name,
797            "kind": parked.kind,
798            "fingerprint": parked.fingerprint,
799            "expiresAt": didbot_schema::format::datetime::render(parked.expires_at),
800        }))
801        .into_response(),
802        None => ApiError::new(
803            StatusCode::NOT_FOUND,
804            "ParkedKeyNotFound",
805            match &query.fingerprint {
806                Some(fingerprint) => format!(
807                    "no key with fingerprint {fingerprint} is parked for {:?}",
808                    query.name
809                ),
810                None => format!("no key is parked for {:?}", query.name),
811            },
812        )
813        .into_response(),
814    }
815}
816
817/// `POST /xrpc/bot.did.createSession?did=`
818///
819/// A session as a hosted account, from a proof its credential records
820/// name: a JWT signed by one of its keys, or an OpenID Connect token one of
821/// its identities describes. How a pipeline run becomes its pipeline. An ID
822/// token is checked against the identities of the account `did` names
823/// alone, so it needs `did`; a key proof names its account in `iss`. Gated
824/// by the e-stop the way every session issuance is.
825///
826/// Reading the proof can fetch an issuer's keys before any signature is
827/// checked, so each call spends [`AppState::provision_rate_limiter`]'s
828/// per-address budget first, the one `createAccount` spends.
829pub(crate) async fn create_session(
830    State(state): State<AppState>,
831    peer: Peer,
832    headers: HeaderMap,
833    query: Result<Query<SessionQuery>, axum::extract::rejection::QueryRejection>,
834) -> Response {
835    match create_session_inner(&state, peer, &headers, query).await {
836        Ok(response) => response,
837        Err(error) => error.into_response(),
838    }
839}
840
841async fn create_session_inner(
842    state: &AppState,
843    peer: Peer,
844    headers: &HeaderMap,
845    query: Result<Query<SessionQuery>, axum::extract::rejection::QueryRejection>,
846) -> Result<Response, ApiError> {
847    state.estop.check_issue(EstopRefusal::Token)?;
848    crate::routes::require_lifecycle(state, |p| p.provisions_accounts, "bot.did.createSession")?;
849    let key = caller_key(headers, peer.0, state.trust_forwarded_headers);
850    if !state.provision_rate_limiter.check(&key) {
851        info!(caller = %key, "session refused: too many attempts from this address");
852        return Err(ApiError::rate_limited(
853            "too many createAccount and createSession calls; slow down",
854        ));
855    }
856    let Query(query) = query.map_err(|rejection| ApiError::bad_request(rejection.body_text()))?;
857    let proof = prove(
858        state,
859        headers,
860        "bot.did.createSession",
861        false,
862        Named {
863            did: query.did.as_deref(),
864            field: NamedBy::Did,
865        },
866    )
867    .await?;
868    let did = match proof {
869        Proof::Key(did) | Proof::Oidc(did) => did,
870        Proof::Session(_) | Proof::Server => {
871            return Err(invalid_proof(
872                "createSession takes a key-signed JWT or an OpenID Connect token",
873            ))
874        }
875    };
876    if state.registry.account(&did).is_none() {
877        return Err(invalid_proof(format!(
878            "{did} is not an account this server hosts"
879        )));
880    }
881    let registry = state.registry.clone();
882    let issued = registry
883        .issue_session(&did)
884        .map_err(|error| ApiError::from(&error))?;
885    Ok(Json(json!({ "did": did, "accountToken": issued.token })).into_response())
886}