Files
tranquil-pds/crates/tranquil-api/src/identity/plc/submit.rs
T
2026-06-26 18:52:12 +03:00

153 lines
5.4 KiB
Rust

use axum::{Json, extract::State};
use k256::ecdsa::SigningKey;
use serde::Deserialize;
use serde_json::Value;
use tracing::{error, info, warn};
use tranquil_pds::api::error::DbResultExt;
use tranquil_pds::api::{ApiError, EmptyResponse};
use tranquil_pds::auth::{Auth, Permissive};
use tranquil_pds::circuit_breaker::with_circuit_breaker;
use tranquil_pds::plc::{signing_key_to_did_key, validate_plc_operation};
use tranquil_pds::state::AppState;
#[derive(Debug, Deserialize)]
pub struct SubmitPlcOperationInput {
pub operation: Value,
}
pub async fn submit_plc_operation(
State(state): State<AppState>,
auth: Auth<Permissive>,
Json(input): Json<SubmitPlcOperationInput>,
) -> Result<Json<EmptyResponse>, ApiError> {
tranquil_pds::auth::scope_check::check_identity_scope(
&auth.auth_source,
auth.scope.as_deref(),
tranquil_pds::oauth::scopes::IdentityAttr::Wildcard,
)?;
let did = &auth.did;
if did.starts_with("did:web:") {
return Err(ApiError::InvalidRequest(
"PLC operations are only valid for did:plc identities".into(),
));
}
validate_plc_operation(&input.operation)
.map_err(|e| ApiError::InvalidRequest(format!("Invalid operation: {}", e)))?;
let op = &input.operation;
let hostname = &tranquil_config::get().server.hostname;
let public_url = format!("https://{}", hostname);
let user = state
.repos
.user
.get_id_and_handle_by_did(did)
.await
.log_db_err("fetching user")?
.ok_or(ApiError::AccountNotFound)?;
let key_row = state
.repos
.user
.get_user_key_by_id(user.id)
.await
.log_db_err("fetching user key")?
.ok_or_else(|| ApiError::InternalError(Some("User signing key not found".into())))?;
let key_bytes =
tranquil_pds::config::decrypt_key(&key_row.key_bytes, key_row.encryption_version).map_err(
|e| {
error!("Failed to decrypt user key: {}", e);
ApiError::InternalError(None)
},
)?;
let signing_key = SigningKey::from_slice(&key_bytes).map_err(|e| {
error!("Failed to create signing key: {:?}", e);
ApiError::InternalError(None)
})?;
let user_did_key = signing_key_to_did_key(&signing_key);
if let Some(rotation_keys) = op.get("rotationKeys").and_then(Value::as_array) {
let rotation_key_strs: Vec<&str> = rotation_keys.iter().filter_map(Value::as_str).collect();
if let Some(missing) = tranquil_pds::plc::missing_required_rotation_key(
&rotation_key_strs,
&user_did_key,
tranquil_config::get().secrets.plc_rotation_key.as_deref(),
) {
return Err(ApiError::InvalidRequest(missing.message().into()));
}
}
if let Some(services) = op.get("services").and_then(Value::as_object)
&& let Some(pds) = services.get("atproto_pds").and_then(Value::as_object)
{
let service_type = pds.get("type").and_then(Value::as_str);
let endpoint = pds.get("endpoint").and_then(Value::as_str);
if service_type != Some(tranquil_pds::plc::ServiceType::Pds.as_str()) {
return Err(ApiError::InvalidRequest(
"Incorrect type on atproto_pds service".into(),
));
}
if endpoint != Some(&public_url) {
return Err(ApiError::InvalidRequest(
"Incorrect endpoint on atproto_pds service".into(),
));
}
}
if let Some(verification_methods) = op.get("verificationMethods").and_then(Value::as_object)
&& let Some(atproto_key) = verification_methods.get("atproto").and_then(Value::as_str)
&& atproto_key != user_did_key
{
return Err(ApiError::InvalidRequest(
"Incorrect signing key in verificationMethods".into(),
));
}
if let Some(also_known_as) = (!user.handle.is_empty())
.then(|| op.get("alsoKnownAs").and_then(Value::as_array))
.flatten()
{
let expected_handle = format!("at://{}", user.handle);
let first_aka = also_known_as.first().and_then(Value::as_str);
if first_aka != Some(&expected_handle) {
return Err(ApiError::InvalidRequest(
"Incorrect handle in alsoKnownAs".into(),
));
}
}
let plc_client = state.plc_client();
let operation_clone = input.operation.clone();
let did_clone = did.clone();
with_circuit_breaker(&state.circuit_breakers.plc_directory, || async {
plc_client
.send_operation(&did_clone, &operation_clone)
.await
})
.await
.map_err(ApiError::from)?;
if let Err(e) = state
.repos
.repo
.insert_identity_event(did, Some(&user.handle))
.await
{
warn!("Failed to sequence identity event: {:?}", e);
}
let _ = state
.cache
.delete(&tranquil_pds::cache_keys::handle_key(&user.handle))
.await;
let _ = state
.cache
.delete(&tranquil_pds::cache_keys::plc_doc_key(did))
.await;
let _ = state
.cache
.delete(&tranquil_pds::cache_keys::plc_data_key(did))
.await;
if state.did_resolver.refresh_did(did).await.is_err() {
warn!(did = %did, "Failed to refresh DID cache after PLC update");
}
info!(did = %did, "PLC operation submitted successfully");
Ok(Json(EmptyResponse {}))
}