1use super::*;
2
3use crate::write_meter::{Bucket, WriteMeter};
4use tracing::warn;
5
6pub(crate) async fn create_record(
31 State(state): State<AppState>,
32 headers: HeaderMap,
33 XrpcBody(body): XrpcBody,
34) -> Response {
35 if let Err(err) = check_write_not_halted(&state) {
42 return err.into_response();
43 }
44 let writer = match resource_credential(
45 &state,
46 &headers,
47 "POST",
48 "/xrpc/com.atproto.repo.createRecord",
49 ) {
50 Ok(writer) => writer,
51 Err(err) => return err.into_response(),
52 };
53 let request: CreateRecordRequest = match parse_body(&body) {
54 Ok(request) => request,
55 Err(err) => return err.into_response(),
56 };
57 let did = match require_writable_repo(&state, writer.did(), &request.repo) {
58 Ok(did) => did,
59 Err(err) => return err.into_response(),
60 };
61 let mut swap = match request.swap.resolve_commit_only("createRecord") {
62 Ok(swap) => swap,
63 Err(err) => return err.into_response(),
64 };
65 swap.record = Precondition::Absent;
66 let authorized =
67 match authorize_write(&state, &writer, &[(&request.collection, Action::Create)]) {
68 Ok(authorized) => authorized,
69 Err(err) => return err.into_response(),
70 };
71 if let Some(err) = refuse_skipped_validation(request.validate, [request.collection.as_str()]) {
72 return err.into_response();
73 }
74 if let Some(err) = require_known_lexicon(&request.collection, &request.record, request.validate)
75 {
76 return err.into_response();
77 }
78 write_record(
79 &state,
80 &did,
81 authorized.client_id,
82 &request.collection,
83 request.rkey.as_deref(),
84 request.record,
85 &swap,
86 Occupied::Refuse,
87 body.len(),
88 )
89 .await
90}
91
92pub(crate) async fn put_record(
99 State(state): State<AppState>,
100 headers: HeaderMap,
101 XrpcBody(body): XrpcBody,
102) -> Response {
103 if let Err(err) = check_write_not_halted(&state) {
110 return err.into_response();
111 }
112 let writer =
113 match resource_credential(&state, &headers, "POST", "/xrpc/com.atproto.repo.putRecord") {
114 Ok(writer) => writer,
115 Err(err) => return err.into_response(),
116 };
117 let request: PutRecordRequest = match parse_body(&body) {
118 Ok(request) => request,
119 Err(err) => return err.into_response(),
120 };
121 let did = match require_writable_repo(&state, writer.did(), &request.repo) {
122 Ok(did) => did,
123 Err(err) => return err.into_response(),
124 };
125 let swap = match request.swap.resolve() {
126 Ok(swap) => swap,
127 Err(err) => return err.into_response(),
128 };
129 let wanted = [
132 (request.collection.as_str(), Action::Create),
133 (request.collection.as_str(), Action::Update),
134 ];
135 let authorized = match authorize_write(&state, &writer, &wanted) {
136 Ok(authorized) => authorized,
137 Err(err) => return err.into_response(),
138 };
139 if let Some(err) = refuse_skipped_validation(request.validate, [request.collection.as_str()]) {
140 return err.into_response();
141 }
142 if let Some(err) = require_known_lexicon(&request.collection, &request.record, request.validate)
143 {
144 return err.into_response();
145 }
146 write_record(
147 &state,
148 &did,
149 authorized.client_id,
150 &request.collection,
151 Some(&request.rkey),
152 request.record,
153 &swap,
154 Occupied::Replace,
155 body.len(),
156 )
157 .await
158}
159
160#[derive(Debug, Clone, Copy, PartialEq, Eq)]
166pub(crate) enum Occupied {
167 Refuse,
169 Replace,
171}
172
173#[allow(clippy::too_many_arguments)]
179pub(crate) async fn write_record(
180 state: &AppState,
181 did: &str,
182 client_id: Option<&didbot_pds::policy::ClientId>,
183 collection: &str,
184 rkey: Option<&str>,
185 record: serde_json::Value,
186 swap: &didbot_pds::Swap,
187 occupied: Occupied,
188 bytes: usize,
189) -> Response {
190 if let Err(err) = check_write_not_halted(state) {
191 return err.into_response();
192 }
193 state.lookups.prefetch(collection, Some(&record)).await;
199 let registry = state.registry.clone();
203 let did_owned = did.to_owned();
204 let collection_owned = collection.to_owned();
205 let rkey_owned = rkey.map(str::to_owned);
206 let swap_owned = swap.clone();
207 let client_owned = client_id.cloned();
208 let meter = state.write_meter.clone();
209 let outcome = tokio::task::spawn_blocking(move || {
210 let charge = || meter_write(&meter, &did_owned, 1, bytes);
211 registry.put_record_from(
212 &did_owned,
213 &collection_owned,
214 rkey_owned.as_deref(),
215 record,
216 &swap_owned,
217 client_owned.as_ref(),
218 Some(&charge),
219 )
220 })
221 .await;
222 let client = client_id.map(didbot_pds::policy::ClientId::as_str);
223 match outcome {
224 Err(join_err) => blocking_task_failed("a record write", join_err).into_response(),
225 Ok(Ok(written)) => {
226 info!(
227 repo = %did,
228 client,
229 collection,
230 rkey = %written.rkey,
231 cid = %written.cid,
232 "wrote record"
233 );
234 Json(CreateRecordResponse::written(did, collection, &written)).into_response()
235 }
236 Ok(Err(err)) => {
237 info!(repo = %did, client, collection, error = %err, "record write refused");
241 if occupied == Occupied::Refuse && is_swap_failure(&err) {
245 let occupied = match rkey {
250 Some(rkey) => format!("`{}`", record_uri(did, collection, rkey)),
251 None => format!("the key `{collection}` chose for this write"),
252 };
253 return ApiError::bad_request(format!(
254 "`createRecord` will not replace a record, and one is already at \
255 {occupied}; use `putRecord` to replace it"
256 ))
257 .into_response();
258 }
259 refused_write(&err, did, client).into_response()
260 }
261 }
262}
263
264pub(crate) fn refused_write(
270 err: &didbot_pds::ProvisionError,
271 did: &str,
272 client_id: Option<&str>,
273) -> ApiError {
274 let refused = crate::error::repo_surface(err);
275 if matches!(err, didbot_pds::ProvisionError::TooManyRecords { .. }) {
276 crate::refusal::refused(crate::refusal::Refusal {
277 endpoint: "xrpc",
278 client_id,
279 account: Some(did),
280 error: refused.error(),
281 reason: refused.message(),
282 severity: crate::refusal::Severity::Warn,
283 });
284 }
285 refused
286}
287
288pub(crate) fn is_swap_failure(err: &didbot_pds::ProvisionError) -> bool {
290 matches!(
291 err,
292 didbot_pds::ProvisionError::Record(didbot_pds::RecordError::SwapFailed { .. })
293 )
294}
295
296pub(crate) fn require_lifecycle(
321 state: &AppState,
322 capability: impl FnOnce(&ServerPolicy) -> bool,
323 route: &str,
324) -> Result<(), ApiError> {
325 let snapshot = state.lifecycle.snapshot();
326 if capability(&snapshot.policy) {
327 return Ok(());
328 }
329 Err(ApiError::not_ready(route, &snapshot))
330}
331
332pub(crate) fn require_account_capacity(state: &AppState) -> Result<(), ApiError> {
345 let live = state.registry.account_count() as u64;
346 if live < state.max_accounts {
347 return Ok(());
348 }
349 Err(ApiError::account_cap_reached(state.max_accounts))
350}
351
352pub(crate) fn check_write_not_halted(state: &AppState) -> Result<(), ApiError> {
365 state.estop.check_use(EstopRefusal::Operation)?;
366 require_record_writes(state)?;
367 Ok(())
368}
369
370pub(crate) fn require_writable_repo(
386 state: &AppState,
387 authenticated: &str,
388 repo: &str,
389) -> Result<String, ApiError> {
390 let did = resolve_repo(state, repo)?;
391 if did != authenticated {
392 return Err(ApiError::repo_mismatch(authenticated, &did));
393 }
394 state
395 .registry
396 .check_writable(&did)
397 .map_err(|err| crate::error::repo_surface(&err))?;
398 Ok(did)
399}
400
401pub(crate) fn charge_write(
409 state: &AppState,
410 did: &str,
411 ops: usize,
412 bytes: usize,
413) -> Result<(), ApiError> {
414 meter_write(&state.write_meter, did, ops, bytes).map_err(|err| ApiError::from(&err))
415}
416
417pub(crate) fn meter_write(
420 meter: &WriteMeter,
421 did: &str,
422 ops: usize,
423 bytes: usize,
424) -> Result<(), didbot_pds::ProvisionError> {
425 let ops = u32::try_from(ops).unwrap_or(u32::MAX);
426 let Err(refusal) = meter.charge(did, ops, bytes as u64) else {
427 return Ok(());
428 };
429 let limits = meter.limits();
430 let (bucket, rate, burst) = match refusal.bucket {
431 Bucket::Ops => (
432 "operations",
433 u64::from(limits.ops_per_hour),
434 u64::from(limits.ops_burst),
435 ),
436 Bucket::Bytes => ("bytes", limits.bytes_per_hour, limits.bytes_burst),
437 };
438 let wanted = refusal.wanted;
439 let retry_after_secs = refusal
440 .retry_after
441 .map(|wait| (wait.as_secs() + u64::from(wait.subsec_nanos() > 0)).max(1));
442 warn!(
443 repo = %did,
444 bucket,
445 wanted,
446 rate_per_hour = rate,
447 burst,
448 retry_after_secs,
449 "write refused: the account is over its write rate"
450 );
451 let message = match retry_after_secs {
452 Some(secs) => format!(
453 "this write needs {wanted} {bucket}, and `{did}` may write {rate} {bucket} an hour \
454 with at most {burst} at once; retry after {secs}s"
455 ),
456 None => format!(
457 "this write needs {wanted} {bucket}, and `{did}` may write at most {burst} {bucket} \
458 at once"
459 ),
460 };
461 Err(didbot_pds::ProvisionError::OverWriteBudget {
462 message,
463 retry_after_secs,
464 })
465}
466
467pub(crate) fn require_record_writes(state: &AppState) -> Result<(), ApiError> {
472 require_lifecycle(
473 state,
474 |p| p.accepts_record_writes,
475 "com.atproto.repo writes",
476 )
477}
478
479pub(crate) async fn delete_record(
490 State(state): State<AppState>,
491 headers: HeaderMap,
492 XrpcBody(body): XrpcBody,
493) -> Response {
494 if let Err(err) = check_write_not_halted(&state) {
501 return err.into_response();
502 }
503 let writer = match resource_credential(
504 &state,
505 &headers,
506 "POST",
507 "/xrpc/com.atproto.repo.deleteRecord",
508 ) {
509 Ok(writer) => writer,
510 Err(err) => return err.into_response(),
511 };
512 let request: DeleteRecordRequest = match parse_body(&body) {
513 Ok(request) => request,
514 Err(err) => return err.into_response(),
515 };
516 let did = match require_writable_repo(&state, writer.did(), &request.repo) {
517 Ok(did) => did,
518 Err(err) => return err.into_response(),
519 };
520 let swap = match request.swap.resolve() {
521 Ok(swap) => swap,
522 Err(err) => return err.into_response(),
523 };
524 let client_id = match authorize_write(&state, &writer, &[(&request.collection, Action::Delete)])
525 {
526 Ok(authorized) => authorized.client_id,
527 Err(err) => return err.into_response(),
528 };
529 if let Err(err) = check_write_not_halted(&state) {
530 return err.into_response();
531 }
532 let registry = state.registry.clone();
534 let did_owned = did.clone();
535 let collection = request.collection.clone();
536 let rkey = request.rkey.clone();
537 let client_owned = client_id.cloned();
538 let meter = state.write_meter.clone();
539 let bytes = body.len();
540 let outcome = tokio::task::spawn_blocking(move || {
541 let charge = || meter_write(&meter, &did_owned, 1, bytes);
542 registry.delete_record_from(
543 &did_owned,
544 &collection,
545 &rkey,
546 &swap,
547 client_owned.as_ref(),
548 Some(&charge),
549 )
550 })
551 .await;
552 match outcome {
553 Err(join_err) => blocking_task_failed("a record deletion", join_err).into_response(),
554 Ok(Ok(removed)) => {
555 info!(
556 repo = %did,
557 client = client_id.map(didbot_pds::policy::ClientId::as_str),
558 collection = %request.collection,
559 rkey = %request.rkey,
560 removed,
561 "record deletion"
562 );
563 Json(json!({})).into_response()
564 }
565 Ok(Err(err)) => crate::error::repo_surface(&err).into_response(),
566 }
567}
568
569pub(crate) async fn apply_writes(
581 State(state): State<AppState>,
582 headers: HeaderMap,
583 XrpcBody(body): XrpcBody,
584) -> Response {
585 if let Err(err) = check_write_not_halted(&state) {
592 return err.into_response();
593 }
594 let writer = match resource_credential(
595 &state,
596 &headers,
597 "POST",
598 "/xrpc/com.atproto.repo.applyWrites",
599 ) {
600 Ok(writer) => writer,
601 Err(err) => return err.into_response(),
602 };
603 let request: ApplyWritesRequest = match parse_body(&body) {
604 Ok(request) => request,
605 Err(err) => return err.into_response(),
606 };
607 if request.writes.len() > crate::wire::MAX_APPLY_WRITES {
608 return ApiError::bad_request(format!(
609 "`applyWrites` takes at most {} writes, and this batch has {}",
610 crate::wire::MAX_APPLY_WRITES,
611 request.writes.len()
612 ))
613 .into_response();
614 }
615 let did = match require_writable_repo(&state, writer.did(), &request.repo) {
616 Ok(did) => did,
617 Err(err) => return err.into_response(),
618 };
619 let swap = match request.swap.resolve_commit_only("applyWrites") {
620 Ok(swap) => swap,
621 Err(err) => return err.into_response(),
622 };
623 let written = request.writes.iter().filter_map(|write| match write {
624 RepoWrite::Create { collection, .. } | RepoWrite::Update { collection, .. } => {
625 Some(collection.as_str())
626 }
627 RepoWrite::Delete { .. } => None,
628 });
629 if let Some(err) = refuse_skipped_validation(request.validate, written) {
630 return err.into_response();
631 }
632 if let Err(err) = check_write_not_halted(&state) {
633 return err.into_response();
634 }
635
636 let writes: Vec<didbot_pds::BatchOp> = request.writes.into_iter().map(to_batch_op).collect();
637 let wanted: Vec<(&str, Action)> = writes
640 .iter()
641 .flat_map(|op| {
642 let actions: &[Action] = match op {
643 didbot_pds::BatchOp::Create { rkey: None, .. } => &[Action::Create],
644 didbot_pds::BatchOp::Create { rkey: Some(_), .. }
645 | didbot_pds::BatchOp::Update { .. } => &[Action::Create, Action::Update],
646 didbot_pds::BatchOp::Delete { .. } => &[Action::Delete],
647 };
648 actions.iter().map(move |action| (op.collection(), *action))
649 })
650 .collect();
651 let client_id = match authorize_write(&state, &writer, &wanted) {
652 Ok(authorized) => authorized.client_id,
653 Err(err) => return err.into_response(),
654 };
655 let stance = didbot_pds::Stance::from_validate(request.validate);
656 let ops = writes.clone();
661 for op in &writes {
662 let after = match op {
663 didbot_pds::BatchOp::Create { record, .. }
664 | didbot_pds::BatchOp::Update { record, .. } => Some(record),
665 didbot_pds::BatchOp::Delete { .. } => None,
666 };
667 state.lookups.prefetch(op.collection(), after).await;
668 }
669 let client = client_id.map(didbot_pds::policy::ClientId::as_str);
670 let registry = state.registry.clone();
672 let did_owned = did.clone();
673 let client_owned = client_id.cloned();
674 let meter = state.write_meter.clone();
675 let (charged, bytes) = (writes.len().max(1), body.len());
678 let outcome = tokio::task::spawn_blocking(move || {
679 let charge = || meter_write(&meter, &did_owned, charged, bytes);
680 registry.apply_writes_from(
681 &did_owned,
682 writes,
683 stance,
684 swap.commit.as_ref(),
685 client_owned.as_ref(),
686 Some(&charge),
687 )
688 })
689 .await;
690 match outcome {
691 Err(join_err) => blocking_task_failed("a batch write", join_err).into_response(),
692 Ok(Ok(outcomes)) => {
693 let results: Vec<WriteResult> = ops
694 .iter()
695 .zip(outcomes)
696 .map(|(op, outcome)| write_result(&did, op, outcome))
697 .collect();
698 info!(repo = %did, client, writes = results.len(), "applied a batch as one commit");
699 Json(json!({ "results": results })).into_response()
700 }
701 Ok(Err(err)) => {
702 info!(repo = %did, client, error = %err, "batch refused; nothing was written");
703 refused_write(&err, &did, client).into_response()
704 }
705 }
706}
707
708pub(crate) fn to_batch_op(write: RepoWrite) -> didbot_pds::BatchOp {
712 match write {
713 RepoWrite::Create {
714 collection,
715 rkey,
716 value,
717 } => didbot_pds::BatchOp::Create {
718 collection,
719 rkey,
720 record: value,
721 },
722 RepoWrite::Update {
723 collection,
724 rkey,
725 value,
726 } => didbot_pds::BatchOp::Update {
727 collection,
728 rkey,
729 record: value,
730 },
731 RepoWrite::Delete { collection, rkey } => didbot_pds::BatchOp::Delete { collection, rkey },
732 }
733}
734
735pub(crate) fn write_result(
739 did: &str,
740 op: &didbot_pds::BatchOp,
741 outcome: didbot_pds::BatchOutcome,
742) -> WriteResult {
743 match (op, outcome) {
744 (
745 didbot_pds::BatchOp::Create { collection, .. },
746 didbot_pds::BatchOutcome::Written(written),
747 ) => WriteResult::Create {
748 uri: record_uri(did, collection, &written.rkey),
749 cid: written.cid.to_string(),
750 validation_status: didbot_pds::validation_status(
751 collection,
752 didbot_pds::Stance::Optimistic,
753 ),
754 },
755 (
756 didbot_pds::BatchOp::Update { collection, .. },
757 didbot_pds::BatchOutcome::Written(written),
758 ) => WriteResult::Update {
759 uri: record_uri(did, collection, &written.rkey),
760 cid: written.cid.to_string(),
761 validation_status: didbot_pds::validation_status(
762 collection,
763 didbot_pds::Stance::Optimistic,
764 ),
765 },
766 (didbot_pds::BatchOp::Delete { .. }, didbot_pds::BatchOutcome::Deleted) => {
767 WriteResult::Delete {}
768 }
769 (op, outcome) => unreachable!("a {op:?} produced a {outcome:?}"),
773 }
774}
775
776pub(crate) async fn get_record(
781 State(state): State<AppState>,
782 peer: crate::rate_limit::Peer,
783 headers: axum::http::HeaderMap,
784 query: Result<Query<GetRecordQuery>, QueryRejection>,
785) -> Response {
786 if let Err(refusal) = require_sync_budget(&state, &headers, peer) {
787 return refusal.into_response();
788 }
789 let Query(query) = match query {
790 Ok(query) => query,
791 Err(rejection) => return ApiError::bad_request(rejection.body_text()).into_response(),
792 };
793 let did = match resolve_repo(&state, &query.repo) {
794 Ok(did) => did,
795 Err(err) => return err.into_response(),
796 };
797 let uri = record_uri(&did, &query.collection, &query.rkey);
798 match state
799 .registry
800 .get_record(&did, &query.collection, &query.rkey)
801 {
802 Ok(Some(value)) => {
803 let cid = match didbot_pds::content_id(&value) {
804 Ok(cid) => cid.to_string(),
805 Err(err) => return unnameable_record(&uri, &err).into_response(),
806 };
807 if query.cid.is_some_and(|asked| asked != cid) {
814 return ApiError::record_not_found(&uri).into_response();
815 }
816 Json(GetRecordResponse { uri, cid, value }).into_response()
817 }
818 Ok(None) => ApiError::record_not_found(&uri).into_response(),
819 Err(err) => crate::error::repo_surface(&err).into_response(),
820 }
821}
822
823pub(crate) fn unnameable_record(subject: &str, error: &didbot_pds::RecordError) -> ApiError {
830 ApiError::new(
831 StatusCode::INTERNAL_SERVER_ERROR,
832 "InternalServerError",
833 format!("`{subject}` is stored and cannot be named: {error}"),
834 )
835}
836
837pub(crate) async fn describe_repo(
854 State(state): State<AppState>,
855 query: Result<Query<DescribeRepoQuery>, QueryRejection>,
856) -> Response {
857 let Query(query) = match query {
858 Ok(query) => query,
859 Err(rejection) => return ApiError::bad_request(rejection.body_text()).into_response(),
860 };
861 let did = match resolve_repo(&state, &query.repo) {
862 Ok(did) => did,
863 Err(err) => return err.into_response(),
864 };
865 let Some(account) = state.registry.account(&did) else {
866 return ApiError::repo_not_found(&query.repo).into_response();
867 };
868 let Some(document) = state.registry.did_document(account.did.host()) else {
871 return ApiError::new(
872 StatusCode::INTERNAL_SERVER_ERROR,
873 "DidDocumentUnavailable",
874 format!("`{did}` has an account and no DID document"),
875 )
876 .into_response();
877 };
878 let handle = document.handle().unwrap_or_default().to_owned();
881 let handle_is_correct = state
882 .registry
883 .handle_did(&handle)
884 .is_some_and(|resolved| resolved == did);
885 let collections = state.registry.collections(&did).unwrap_or_default();
886 let did_doc = match serde_json::to_value(&document) {
887 Ok(value) => value,
888 Err(error) => {
889 return ApiError::new(
890 StatusCode::INTERNAL_SERVER_ERROR,
891 "DidDocumentUnavailable",
892 error.to_string(),
893 )
894 .into_response()
895 }
896 };
897 Json(DescribeRepoResponse {
898 handle,
899 did,
900 did_doc,
901 collections,
902 handle_is_correct,
903 })
904 .into_response()
905}
906
907pub(crate) async fn list_records(
915 State(state): State<AppState>,
916 query: Result<Query<ListRecordsQuery>, QueryRejection>,
917) -> Response {
918 let Query(query) = match query {
922 Ok(query) => query,
923 Err(rejection) => return ApiError::bad_request(rejection.body_text()).into_response(),
924 };
925
926 let did = match resolve_repo(&state, &query.repo) {
927 Ok(did) => did,
928 Err(err) => return err.into_response(),
929 };
930 let page = query.page();
931 match state.registry.list_records(&did, &query.collection, &page) {
932 Ok(records) => {
933 let cursor = page.next_cursor(&records);
937 let records: Vec<RecordView> = match records
942 .into_iter()
943 .map(|(rkey, value)| RecordView::new(&did, &query.collection, &rkey, value))
944 .collect::<Result<_, _>>()
945 {
946 Ok(records) => records,
947 Err(err) => {
948 let subject = format!("at://{did}/{}", query.collection);
949 return unnameable_record(&subject, &err).into_response();
950 }
951 };
952 let mut body = json!({ "records": records });
956 if let Some(cursor) = cursor {
957 body["cursor"] = json!(cursor);
958 }
959 Json(body).into_response()
960 }
961 Err(err) => crate::error::repo_surface(&err).into_response(),
962 }
963}